From f9699cb37382f62ff0064c69d02c497b6acee02f Mon Sep 17 00:00:00 2001 From: Jean-Pierre De Jesus DIAZ Date: Mon, 15 Apr 2024 18:54:06 +0200 Subject: [PATCH] fixup! SFT-3538: Add foundation-firmware crate. --- Cargo.toml | 1 + firmware/Cargo.toml | 15 +++ firmware/src/bin/foundation-firmware.rs | 55 ++++++++ firmware/src/lib.rs | 166 ++++++++++++++++++++---- 4 files changed, 212 insertions(+), 25 deletions(-) create mode 100644 firmware/src/bin/foundation-firmware.rs diff --git a/Cargo.toml b/Cargo.toml index 58a181e..9189206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ homepage = "https://github.com/Foundation-Devices/foundation-rs" [workspace.dependencies] +anyhow = "1" arbitrary = { version = "1", features = ["derive"] } bech32 = { version = "0.9", default-features = false } bip39 = { version = "2", default-features = false } diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml index 5ab9bb2..a095127 100644 --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -5,7 +5,22 @@ description = "Firmware image format" edition = "2021" license = "GPL-3.0-or-later AND GPL-3.0-only" +[[bin]] +name = "foundation-firmware" +required-features = ["binary"] + +[features] +default = ["std"] +std = ["nom/std", "secp256k1/std"] +binary = [ + "anyhow", + "secp256k1/global-context", + "std", +] + [dependencies] bitcoin_hashes = { workspace = true } +heapless = { workspace = true } nom = { workspace = true } secp256k1 = { workspace = true } +anyhow = { workspace = true, optional = true } diff --git a/firmware/src/bin/foundation-firmware.rs b/firmware/src/bin/foundation-firmware.rs new file mode 100644 index 0000000..abf5f07 --- /dev/null +++ b/firmware/src/bin/foundation-firmware.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +use anyhow::{anyhow, bail, Context, Result}; +use bitcoin_hashes::{sha256d, Hash, HashEngine}; +use nom::Finish; +use secp256k1::global::SECP256K1; +use std::{fs, io::Read}; + +fn main() -> Result<()> { + let file_name = std::env::args_os() + .nth(1) + .ok_or_else(|| anyhow!("Please provide a file name."))?; + + let mut file = fs::File::open(file_name).context("Failed to open firmware.")?; + + let header_len = usize::try_from(foundation_firmware::HEADER_LEN).unwrap(); + let mut header_buf = vec![0; header_len]; + + file.read(&mut header_buf) + .context("Failed to read firmware header.")?; + + let header = match foundation_firmware::header(&header_buf).finish() { + Ok((_, hdr)) => hdr, + Err(_) => bail!("Failed to parse firmware header."), + }; + + header.verify().context("Header verification failed.")?; + + println!("Firmware:"); + println!("- Magic: {:#08X}", header.information.magic); + println!("- Timestamp: {}", header.information.timestamp); + println!("- Date: {}", header.information.date); + println!("- Version: {}", header.information.version); + println!("- Length: {} bytes", header.information.length); + println!(); + + let firmware_len = + usize::try_from(header.information.length - foundation_firmware::HEADER_LEN).unwrap(); + let mut firmware_buf = vec![0; firmware_len]; + file.read(&mut firmware_buf) + .context("Failed to read firmware contents.")?; + + let mut engine = sha256d::Hash::engine(); + engine.input(&header.information.serialize()); + engine.input(&firmware_buf); + let hash = sha256d::Hash::from_engine(engine); + + foundation_firmware::verify_signature(&SECP256K1, &header, &hash, None) + .context("Firmware signature verification failed.")?; + + println!("Firmware signature is valid!"); + + Ok(()) +} diff --git a/firmware/src/lib.rs b/firmware/src/lib.rs index 8bc439a..01cc5cc 100644 --- a/firmware/src/lib.rs +++ b/firmware/src/lib.rs @@ -6,16 +6,23 @@ //! Firmware images verification. -#![no_std] +#![cfg_attr(not(feature = "std"), no_std)] #![warn(missing_docs)] -use bitcoin_hashes::{sha256, Hash}; +use bitcoin_hashes::{sha256d, Hash}; +use heapless::{String, Vec}; use nom::IResult; use secp256k1::{ecdsa, Message, PublicKey, Secp256k1, Verification}; /// Length of the header, in bytes. pub const HEADER_LEN: u32 = 2048; +/// Length of the firmware date in bytes. +pub const DATE_LEN: usize = 14; + +/// Length of the firmware version, in bytes. +pub const VERSION_LEN: usize = 8; + /// Maximum length of the firmware, in bytes. pub const MAX_LEN: u32 = (1792 * 1024) - 256; @@ -145,22 +152,50 @@ pub struct Information { /// The time stamp of the firmware. pub timestamp: u32, /// The date of the firmware as a string, ending with a NUL. - pub date: [u8; Self::DATE_LEN], + pub date: String, /// Version of the firmware as a string. - pub version: [u8; Self::VERSION_LEN], + pub version: String, /// The length of the firmware, in bytes. pub length: u32, } impl Information { - /// Length of the firmware date in bytes. - pub const DATE_LEN: usize = 14; - /// Length of the firmware version, in bytes. - pub const VERSION_LEN: usize = 8; /// Magic constant for mono devices. pub const MAGIC_MONO: u32 = 0x50415353; /// Magic constant for color devices. pub const MAGIC_COLOR: u32 = 0x53534150; + /// The size of this structure when serialized, in bytes. + pub const LEN: usize = (4 * 2) + DATE_LEN + VERSION_LEN + 4; + + /// Serialize the structure. + pub fn serialize(&self) -> [u8; Self::LEN] { + let mut off = 0; + let mut buf = [0; Self::LEN]; + + buf[off..off + 4].copy_from_slice(&self.magic.to_le_bytes()); + off += 4; + + buf[off..off + 4].copy_from_slice(&self.timestamp.to_le_bytes()); + off += 4; + + buf[off..off + self.date.len()].copy_from_slice(self.date.as_bytes()); + off += self.date.len(); + + // Fill with zeroes the rest of the date. + buf[off..off + (DATE_LEN - self.date.len())].fill(0); + off += DATE_LEN - self.date.len(); + + buf[off..off + self.version.len()].copy_from_slice(&self.version.as_bytes()); + off += self.version.len(); + + // Fill with zeroes the rest of the version. + buf[off..off + (VERSION_LEN - self.version.len())].fill(0); + off += VERSION_LEN - self.version.len(); + + buf[off..off + 4].copy_from_slice(&self.length.to_le_bytes()); + + buf + } } /// Firmware signature information. @@ -222,6 +257,36 @@ pub enum VerifyHeaderError { SamePublicKeys(u32), } +impl core::fmt::Display for VerifyHeaderError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + VerifyHeaderError::UnknownMagic(magic) => { + write!(f, "Invalid magic bytes: {magic:#08X}") + } + VerifyHeaderError::InvalidTimestamp => write!(f, "Invalid timestamp"), + VerifyHeaderError::FirmwareTooSmall(size) => { + write!(f, "Firmware is too small: {size} bytes") + } + VerifyHeaderError::FirmwareTooBig(size) => { + write!(f, "Firmware is too big: {size} bytes") + } + VerifyHeaderError::InvalidPublicKey1Index(index) => { + write!(f, "Public key 1 index is out of range: {index}") + } + VerifyHeaderError::InvalidPublicKey2Index(index) => { + write!(f, "Public key 2 index is out of range: {index}") + } + VerifyHeaderError::SamePublicKeys(index) => write!( + f, + "The same public key ({index}) was used to sign the firmware." + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for VerifyHeaderError {} + /// Parse the firmware's [`Header`]. pub fn header(i: &[u8]) -> IResult<&[u8], Header> { nom::combinator::map( @@ -234,27 +299,22 @@ pub fn header(i: &[u8]) -> IResult<&[u8], Header> { } fn information(i: &[u8]) -> IResult<&[u8], Information> { - let mut date = [0; Information::DATE_LEN]; - let mut version = [0; Information::VERSION_LEN]; - - let (i, (magic, timestamp, (), (), length)) = nom::sequence::tuple(( - nom::number::complete::le_u32, - nom::number::complete::le_u32, - nom::multi::fill(nom::number::complete::u8, &mut date), - nom::multi::fill(nom::number::complete::u8, &mut version), - nom::number::complete::le_u32, - ))(i)?; - - Ok(( - i, - Information { + nom::combinator::map( + nom::sequence::tuple(( + nom::number::complete::le_u32, + nom::number::complete::le_u32, + string::<_, DATE_LEN>, + string::<_, VERSION_LEN>, + nom::number::complete::le_u32, + )), + |(magic, timestamp, date, version, length)| Information { magic, timestamp, date, version, length, }, - )) + )(i) } fn signature(i: &[u8]) -> IResult<&[u8], Signature> { @@ -292,6 +352,35 @@ where }) } +fn string<'a, E, const N: usize>(i: &'a [u8]) -> IResult<&'a [u8], String, E> +where + E: nom::error::ParseError<&'a [u8]> + + nom::error::FromExternalError<&'a [u8], core::str::Utf8Error>, +{ + let start_input = i; + let mut buf: Vec = Vec::new(); + buf.resize(N, 0).unwrap(); + let (i, ()) = nom::multi::fill(nom::number::complete::u8, &mut buf)(i)?; + + // Find NUL terminator to set the string length or just use the full + // data if not found. + let len = buf + .iter() + .enumerate() + .find_map(|(i, &b)| if b == b'\0' { Some(i) } else { None }); + if let Some(len) = len { + buf.truncate(len); + } + + String::from_utf8(buf).map(|v| (i, v)).map_err(|e| { + nom::Err::Failure(E::from_external_error( + start_input, + nom::error::ErrorKind::Fail, + e, + )) + }) +} + /// Keys that are used in Passport to verify the validity of a firmware, they /// are in a specific order and map to an index in pub fn foundation_public_keys() -> [PublicKey; 4] { @@ -319,7 +408,7 @@ fn from_raw_public_key(raw_public_key: &[u8; 64]) -> Result( secp: &Secp256k1, header: &Header, - firmware_hash: &sha256::Hash, + firmware_hash: &sha256d::Hash, user_public_key: Option, ) -> Result<(), VerifySignatureError> { assert!(header.verify().is_ok()); @@ -396,6 +485,33 @@ pub enum VerifySignatureError { MissingUserPublicKey, } +impl core::fmt::Display for VerifySignatureError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + VerifySignatureError::InvalidUserSignature { .. } => { + write!(f, "Invalid user signature") + } + VerifySignatureError::InvalidSignature1 { .. } => write!(f, "Invalid first signature"), + VerifySignatureError::InvalidSignature2 { .. } => write!(f, "Invalid second signature"), + VerifySignatureError::MissingUserPublicKey => { + write!(f, "Firmware is user signed but user public key is missing") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for VerifySignatureError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + VerifySignatureError::InvalidUserSignature { error, .. } => Some(error), + VerifySignatureError::InvalidSignature1 { error, .. } => Some(error), + VerifySignatureError::InvalidSignature2 { error, .. } => Some(error), + _ => None, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -403,7 +519,7 @@ mod tests { #[test] fn test_constants_consistency() { // Originally the date field was designed to hold that string. - assert_eq!(Information::DATE_LEN, b"Jan. 01, 2021".len() + 1); + assert_eq!(DATE_LEN, b"Jan. 01, 2021".len() + 1); // These are the same. assert_eq!(foundation_public_keys().len(), 4);