Skip to content

Commit

Permalink
fixup! SFT-3538: Add foundation-firmware crate.
Browse files Browse the repository at this point in the history
  • Loading branch information
jeandudey committed Apr 15, 2024
1 parent 68cbeb9 commit f9699cb
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 25 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
15 changes: 15 additions & 0 deletions firmware/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
55 changes: 55 additions & 0 deletions firmware/src/bin/foundation-firmware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. <[email protected]>
// 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(())
}
166 changes: 141 additions & 25 deletions firmware/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<DATE_LEN>,
/// Version of the firmware as a string.
pub version: [u8; Self::VERSION_LEN],
pub version: String<VERSION_LEN>,
/// 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.
Expand Down Expand Up @@ -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(
Expand All @@ -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> {
Expand Down Expand Up @@ -292,6 +352,35 @@ where
})
}

fn string<'a, E, const N: usize>(i: &'a [u8]) -> IResult<&'a [u8], String<N>, E>
where
E: nom::error::ParseError<&'a [u8]>
+ nom::error::FromExternalError<&'a [u8], core::str::Utf8Error>,
{
let start_input = i;
let mut buf: Vec<u8, N> = 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] {
Expand Down Expand Up @@ -319,7 +408,7 @@ fn from_raw_public_key(raw_public_key: &[u8; 64]) -> Result<PublicKey, secp256k1
pub fn verify_signature<C: Verification>(
secp: &Secp256k1<C>,
header: &Header,
firmware_hash: &sha256::Hash,
firmware_hash: &sha256d::Hash,
user_public_key: Option<PublicKey>,
) -> Result<(), VerifySignatureError> {
assert!(header.verify().is_ok());
Expand Down Expand Up @@ -396,14 +485,41 @@ 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::*;

#[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);
Expand Down

0 comments on commit f9699cb

Please sign in to comment.