From 598c21071e7770cd23549d48264a177062ff15cb Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 31 Aug 2022 14:35:45 +0200 Subject: [PATCH] New metadata format (#539) * new metadata format is used * Update bootloader/src/main.rs Co-authored-by: ztoked * splits the metadata signed and unsigned parts evenly * fixes pylint Co-authored-by: ztoked --- bootloader/src/main.rs | 34 ++++---- deploy.py | 22 ++++- docs/boards/nrf52840dk.md | 12 +-- src/ctap/command.rs | 26 ++---- src/ctap/data_formats.rs | 106 ----------------------- src/ctap/mod.rs | 177 +++++++++++++++++++++----------------- tools/deploy_partition.py | 73 +++++++++------- 7 files changed, 190 insertions(+), 260 deletions(-) diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index b14e976..df09c24 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -34,6 +34,7 @@ use rtt_target::{rprintln, rtt_init_print}; /// Size of a flash page in bytes. const PAGE_SIZE: usize = 0x1000; +const METADATA_SIGN_OFFSET: usize = 0x800; /// A flash page. type Page = [u8; PAGE_SIZE]; @@ -48,21 +49,19 @@ unsafe fn read_page(address: usize) -> Page { /// Parsed metadata for a firmware partition. struct Metadata { checksum: [u8; 32], - timestamp: u32, + _signature: [u8; 64], + version: u64, address: u32, } -impl Metadata { - pub const DATA_LEN: usize = 40; -} - /// Reads the metadata from a flash page. impl From for Metadata { fn from(page: Page) -> Self { Metadata { checksum: page[0..32].try_into().unwrap(), - timestamp: LittleEndian::read_u32(&page[32..36]), - address: LittleEndian::read_u32(&page[36..Metadata::DATA_LEN]), + _signature: page[32..96].try_into().unwrap(), + version: LittleEndian::read_u64(&page[METADATA_SIGN_OFFSET..][..8]), + address: LittleEndian::read_u32(&page[METADATA_SIGN_OFFSET + 8..][..4]), } } } @@ -76,15 +75,15 @@ struct BootPartition { impl BootPartition { const FIRMWARE_LENGTH: usize = 0x00040000; - /// Reads the metadata, returns the timestamp if all checks pass. - pub fn read_timestamp(&self) -> Result { + /// Reads the metadata, returns the firmware version if all checks pass. + pub fn read_version(&self) -> Result { let metadata_page = unsafe { read_page(self.metadata_address) }; let hash_value = self.compute_upgrade_hash(&metadata_page); let metadata = Metadata::from(metadata_page); if self.firmware_address != metadata.address as usize { #[cfg(debug_assertions)] rprintln!( - "Firmware address mismatch: expected 0x{:08X}, metadata 0x{:08X}", + "Partition address mismatch: expected 0x{:08X}, metadata 0x{:08X}", self.firmware_address, metadata.address as usize ); @@ -95,7 +94,7 @@ impl BootPartition { rprintln!("Hash mismatch"); return Err(()); } - Ok(metadata.timestamp) + Ok(metadata.version) } /// Computes the SHA256 of metadata information and partition data. @@ -107,11 +106,14 @@ impl BootPartition { debug_assert!(self.firmware_address % PAGE_SIZE == 0); debug_assert!(BootPartition::FIRMWARE_LENGTH % PAGE_SIZE == 0); let cc310 = crypto_cell::CryptoCell310::new(); + cc310.update(&metadata_page[METADATA_SIGN_OFFSET..], false); for page_offset in (0..BootPartition::FIRMWARE_LENGTH).step_by(PAGE_SIZE) { let page = unsafe { read_page(self.firmware_address + page_offset) }; - cc310.update(&page, false); + cc310.update( + &page, + page_offset + PAGE_SIZE == BootPartition::FIRMWARE_LENGTH, + ); } - cc310.update(&metadata_page[32..Metadata::DATA_LEN], true); cc310.finalize_and_clear() } @@ -156,12 +158,12 @@ fn main() -> ! { }; #[cfg(debug_assertions)] rprintln!("Reading partition A"); - let timestamp_a = partition_a.read_timestamp(); + let version_a = partition_a.read_version(); #[cfg(debug_assertions)] rprintln!("Reading partition B"); - let timestamp_b = partition_b.read_timestamp(); + let version_b = partition_b.read_version(); - match (timestamp_a, timestamp_b) { + match (version_a, version_b) { (Ok(t1), Ok(t2)) => { if t1 >= t2 { partition_a.boot() diff --git a/deploy.py b/deploy.py index 76296b2..df0529f 100755 --- a/deploy.py +++ b/deploy.py @@ -38,7 +38,7 @@ from tockloader import tockloader as loader from tockloader.exceptions import TockLoaderException import tools.configure -from tools.deploy_partition import create_metadata, pad_to +from tools.deploy_partition import create_metadata, load_priv_key, pad_to PROGRAMMERS = frozenset(("jlink", "openocd", "pyocd", "nordicdfu", "none")) @@ -622,7 +622,9 @@ class OpenSKInstaller: # The kernel is already padded when read. firmware_image = kernel + pad_to(app, app_size) - metadata = create_metadata(firmware_image, board_props.kernel_address) + priv_key = load_priv_key(self.args.upgrade_priv_key) + metadata = create_metadata(firmware_image, board_props.kernel_address, + self.args.version, priv_key) if self.args.verbose_build: info(f"Metadata bytes: {metadata}") @@ -1131,6 +1133,22 @@ if __name__ == "__main__": help=("Don't check that patches are in sync with their submodules."), ) + main_parser.add_argument( + "--private-key", + type=str, + default="crypto_data/opensk_upgrade.key", + dest="upgrade_priv_key", + help=("PEM file for signing the firmware."), + ) + + main_parser.add_argument( + "--version", + type=int, + default=-1, + dest="version", + help=("Firmware version that is built."), + ) + main_parser.set_defaults(features=["with_ctap1"]) # Start parsing to know if we're going to list things or not. diff --git a/docs/boards/nrf52840dk.md b/docs/boards/nrf52840dk.md index 86c9421..d7e15e9 100644 --- a/docs/boards/nrf52840dk.md +++ b/docs/boards/nrf52840dk.md @@ -55,15 +55,15 @@ There are variants of the board that introduce A/B partitions for upgrading the firmware. You can bootstrap an upgradable board using one of the two commands: ```shell -./deploy.py --board=nrf52840dk_opensk_a --opensk -./deploy.py --board=nrf52840dk_opensk_b --opensk +./deploy.py --board=nrf52840dk_opensk_a --opensk --version=0 +./deploy.py --board=nrf52840dk_opensk_b --opensk --version=0 ``` Afterwards, you can upgrade the other partition with ```shell -./tools/perform_upgrade.sh nrf52840dk_opensk_b -./tools/perform_upgrade.sh nrf52840dk_opensk_a +./tools/perform_upgrade.sh nrf52840dk_opensk_b --version=1 +./tools/perform_upgrade.sh nrf52840dk_opensk_a --version=1 ``` respectively. You can only upgrade the partition that is not currently running, @@ -75,6 +75,6 @@ If you deploy with `--vendor-hid`, also add this flag to `perform_upgrade.sh`, for example: ```shell -./deploy.py --board=nrf52840dk_opensk_a --opensk --vendor-hid -./tools/perform_upgrade.sh nrf52840dk_opensk_b --vendor-hid +./deploy.py --board=nrf52840dk_opensk_a --opensk --version=0 --vendor-hid +./tools/perform_upgrade.sh nrf52840dk_opensk_b --version=1 --vendor-hid ``` diff --git a/src/ctap/command.rs b/src/ctap/command.rs index c537913..2464eb5 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -15,11 +15,10 @@ use super::data_formats::{ extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, - CoseKey, CoseSignature, CredentialManagementSubCommand, - CredentialManagementSubCommandParameters, GetAssertionExtensions, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PinUvAuthProtocol, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, SetMinPinLengthParams, + CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, + GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, + PinUvAuthProtocol, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams, }; use super::status_code::Ctap2StatusCode; use super::{cbor_read, key_material}; @@ -596,7 +595,6 @@ pub struct AuthenticatorVendorUpgradeParameters { pub address: Option, pub data: Vec, pub hash: Vec, - pub signature: Option, } impl TryFrom for AuthenticatorVendorUpgradeParameters { @@ -608,7 +606,6 @@ impl TryFrom for AuthenticatorVendorUpgradeParameters { 0x01 => address, 0x02 => data, 0x03 => hash, - 0x04 => signature, } = extract_map(cbor_value)?; } let address = address @@ -617,12 +614,10 @@ impl TryFrom for AuthenticatorVendorUpgradeParameters { .map(|u| u as usize); let data = extract_byte_string(ok_or_missing(data)?)?; let hash = extract_byte_string(ok_or_missing(hash)?)?; - let signature = signature.map(CoseSignature::try_from).transpose()?; Ok(AuthenticatorVendorUpgradeParameters { address, data, hash, - signature, }) } } @@ -631,7 +626,7 @@ impl TryFrom for AuthenticatorVendorUpgradeParameters { mod test { use super::super::data_formats::{ AuthenticatorTransport, PublicKeyCredentialRpEntity, PublicKeyCredentialType, - PublicKeyCredentialUserEntity, SignatureAlgorithm, + PublicKeyCredentialUserEntity, }; use super::super::ES256_CRED_PARAM; use super::*; @@ -1096,10 +1091,6 @@ mod test { let cbor_value = cbor_map! { 0x02 => [0xFF; 0x100], 0x03 => [0x44; 32], - 0x04 => cbor_map! { - "alg" => -7, - "signature" => [0x55; 64], - }, }; assert_eq!( AuthenticatorVendorUpgradeParameters::try_from(cbor_value), @@ -1107,14 +1098,10 @@ mod test { address: None, data: vec![0xFF; 0x100], hash: vec![0x44; 32], - signature: Some(CoseSignature { - algorithm: SignatureAlgorithm::Es256, - bytes: [0x55; 64], - }), }) ); - // Valid without signature + // Valid with address let cbor_value = cbor_map! { 0x01 => 0x1000, 0x02 => [0xFF; 0x100], @@ -1126,7 +1113,6 @@ mod test { address: Some(0x1000), data: vec![0xFF; 0x100], hash: vec![0x44; 32], - signature: None, }) ); } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 23f589d..a137672 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -912,53 +912,6 @@ impl TryFrom for ecdsa::PubKey { } } -/// Data structure for receiving a signature. -/// -/// See https://datatracker.ietf.org/doc/html/rfc8152#appendix-C.1.1 for reference. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CoseSignature { - pub algorithm: SignatureAlgorithm, - pub bytes: [u8; ecdsa::Signature::BYTES_LENGTH], -} - -impl TryFrom for CoseSignature { - type Error = Ctap2StatusCode; - - fn try_from(cbor_value: cbor::Value) -> Result { - destructure_cbor_map! { - let { - "alg" => algorithm, - "signature" => bytes, - } = extract_map(cbor_value)?; - } - - let algorithm = SignatureAlgorithm::try_from(ok_or_missing(algorithm)?)?; - let bytes = extract_byte_string(ok_or_missing(bytes)?)?; - if bytes.len() != ecdsa::Signature::BYTES_LENGTH { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - - Ok(CoseSignature { - algorithm, - bytes: *array_ref![bytes.as_slice(), 0, ecdsa::Signature::BYTES_LENGTH], - }) - } -} - -impl TryFrom for ecdsa::Signature { - type Error = Ctap2StatusCode; - - fn try_from(cose_signature: CoseSignature) -> Result { - match cose_signature.algorithm { - SignatureAlgorithm::Es256 => ecdsa::Signature::from_bytes(&cose_signature.bytes) - .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), - #[cfg(feature = "ed25519")] - SignatureAlgorithm::Eddsa => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), - SignatureAlgorithm::Unknown => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), - } - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "fuzz", derive(Arbitrary))] pub enum PinUvAuthProtocol { @@ -1292,7 +1245,6 @@ mod test { cbor_array, cbor_bool, cbor_bytes, cbor_bytes_lit, cbor_false, cbor_int, cbor_null, cbor_text, cbor_unsigned, }; - use crypto::sha256::Sha256; use rng256::Rng256; #[test] @@ -2003,64 +1955,6 @@ mod test { assert_eq!(cose_key.algorithm, ES256_ALGORITHM); } - #[test] - fn test_from_into_cose_signature() { - let mut env = TestEnv::new(); - let sk = crypto::ecdsa::SecKey::gensk(env.rng()); - let dummy_signature = sk.sign_rfc6979::(&[]); - let mut bytes = [0; ecdsa::Signature::BYTES_LENGTH]; - dummy_signature.to_bytes(&mut bytes); - let cbor_value = cbor_map! { - "alg" => ES256_ALGORITHM, - "signature" => bytes, - }; - let cose_signature = CoseSignature::try_from(cbor_value).unwrap(); - let created_signature = crypto::ecdsa::Signature::try_from(cose_signature).unwrap(); - let mut created_bytes = [0; ecdsa::Signature::BYTES_LENGTH]; - created_signature.to_bytes(&mut created_bytes); - assert_eq!(bytes[..], created_bytes[..]); - } - - #[test] - fn test_cose_signature_wrong_algorithm() { - let mut env = TestEnv::new(); - let sk = crypto::ecdsa::SecKey::gensk(env.rng()); - let dummy_signature = sk.sign_rfc6979::(&[]); - let mut bytes = [0; ecdsa::Signature::BYTES_LENGTH]; - dummy_signature.to_bytes(&mut bytes); - let cbor_value = cbor_map! { - "alg" => -1, // unused algorithm - "signature" => bytes, - }; - let cose_signature = CoseSignature::try_from(cbor_value).unwrap(); - let created_signature = crypto::ecdsa::Signature::try_from(cose_signature); - // Can not compare directly, since ecdsa::Signature does not implement Debug. - assert_eq!( - created_signature.err(), - Some(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) - ); - } - - #[test] - fn test_cose_signature_wrong_signature_length() { - let cbor_value = cbor_map! { - "alg" => ES256_ALGORITHM, - "signature" => [0; ecdsa::Signature::BYTES_LENGTH - 1], - }; - assert_eq!( - CoseSignature::try_from(cbor_value), - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - let cbor_value = cbor_map! { - "alg" => ES256_ALGORITHM, - "signature" => [0; ecdsa::Signature::BYTES_LENGTH + 1], - }; - assert_eq!( - CoseSignature::try_from(cbor_value), - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - } - #[test] fn test_from_pin_uv_auth_protocol() { let cbor_protocol: cbor::Value = cbor_int!(0x01); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index b47a8f2..824490d 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -47,11 +47,10 @@ use self::credential_id::{ use self::credential_management::process_credential_management; use self::crypto_wrapper::PrivateKey; use self::data_formats::{ - AuthenticatorTransport, CoseKey, CoseSignature, CredentialProtectionPolicy, - EnterpriseAttestationMode, GetAssertionExtensions, PackedAttestationStatement, - PinUvAuthProtocol, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, - PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, - SignatureAlgorithm, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, + GetAssertionExtensions, PackedAttestationStatement, PinUvAuthProtocol, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, + PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::{ChannelID, CtapHid, CtapHidCommand, KeepaliveStatus, ProcessedPacket}; use self::large_blobs::LargeBlobs; @@ -77,7 +76,7 @@ use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; -use byteorder::{BigEndian, ByteOrder}; +use byteorder::{BigEndian, ByteOrder, LittleEndian}; use core::convert::TryFrom; use crypto::hmac::hmac_256; use crypto::sha256::Sha256; @@ -212,45 +211,61 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { /// Parses the metadata of an upgrade, and checks its correctness. /// -/// Returns the hash over the upgrade, including partition and some metadata. -/// The metadata consists of: -/// - 32B upgrade hash (SHA256) -/// - 4B timestamp (little endian encoding) -/// - 4B partition address (little endian encoding) -/// The upgrade hash is computed over the firmware image and all metadata, -/// except the hash itself. +/// The metadata is a page starting with: +/// - 32 B upgrade hash (SHA256) +/// - 64 B signature, +/// that are not signed over. The second part is included in the signature with +/// - 8 B version and +/// - 4 B partition address in little endian encoding +/// written at METADATA_SIGN_OFFSET. +/// +/// Checks hash and signature correctness, and whether the partition offset matches. fn parse_metadata( upgrade_locations: &impl UpgradeStorage, + public_key_bytes: &[u8], metadata: &[u8], -) -> Result<[u8; 32], Ctap2StatusCode> { - const METADATA_LEN: usize = 40; +) -> Result<(), Ctap2StatusCode> { + const METADATA_LEN: usize = 0x1000; + const METADATA_SIGN_OFFSET: usize = 0x800; if metadata.len() != METADATA_LEN { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } + + let metadata_address = LittleEndian::read_u32(&metadata[METADATA_SIGN_OFFSET + 8..][..4]); + if metadata_address as usize != upgrade_locations.partition_address() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + // The hash implementation handles this in chunks, so no memory issues. let partition_slice = upgrade_locations .read_partition(0, upgrade_locations.partition_length()) .map_err(|_| Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; let mut hasher = Sha256::new(); + hasher.update(&metadata[METADATA_SIGN_OFFSET..]); hasher.update(partition_slice); - hasher.update(&metadata[32..METADATA_LEN]); let computed_hash = hasher.finalize(); if &computed_hash != array_ref!(metadata, 0, 32) { return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE); } - Ok(computed_hash) + + verify_signature( + array_ref!(metadata, 32, 64), + public_key_bytes, + &computed_hash, + )?; + Ok(()) } /// Verifies the signature over the given hash. /// /// The public key is COSE encoded, and the hash is a SHA256. fn verify_signature( - signature: Option, + signature_bytes: &[u8; 64], public_key_bytes: &[u8], signed_hash: &[u8; 32], ) -> Result<(), Ctap2StatusCode> { - let signature = - ecdsa::Signature::try_from(signature.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?)?; + let signature = ecdsa::Signature::from_bytes(signature_bytes) + .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; let cbor_public_key = cbor_read(public_key_bytes)?; let cose_key = CoseKey::try_from(cbor_public_key)?; let public_key = ecdsa::PubKey::try_from(cose_key)?; @@ -1454,7 +1469,6 @@ impl CtapState { address, data, hash, - signature, } = params; let upgrade_locations = env .upgrade_storage() @@ -1468,9 +1482,7 @@ impl CtapState { .map_err(|_| Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)? } else { // Compares the hash inside the metadata to the actual hash. - let upgrade_hash = parse_metadata(upgrade_locations, &data)?; - // Only signed firmware images may be fully written. - verify_signature(signature, key_material::UPGRADE_PUBLIC_KEY, &upgrade_hash)?; + parse_metadata(upgrade_locations, key_material::UPGRADE_PUBLIC_KEY, &data)?; // Write the metadata page after verifying that its hash is signed. upgrade_locations .write_metadata(&data) @@ -3456,37 +3468,64 @@ mod test { #[test] fn test_parse_metadata() { let mut env = TestEnv::new(); - // The test buffer starts fully erased with 0xFF bytes. - // The compiler issues an incorrect warning. - #[allow(unused_mut)] - let mut upgrade_locations = env.upgrade_storage().unwrap(); + let private_key = crypto::ecdsa::SecKey::gensk(env.rng()); + let upgrade_locations = env.upgrade_storage().unwrap(); + + const METADATA_LEN: usize = 0x1000; + const METADATA_SIGN_OFFSET: usize = 0x800; + let mut metadata = vec![0xFF; METADATA_LEN]; + LittleEndian::write_u32(&mut metadata[METADATA_SIGN_OFFSET + 8..][..4], 0x60000); + + let partition_length = upgrade_locations.partition_length(); + let mut signed_over_data = metadata[METADATA_SIGN_OFFSET..].to_vec(); + signed_over_data.extend( + upgrade_locations + .read_partition(0, partition_length) + .unwrap(), + ); + let signed_hash = Sha256::hash(&signed_over_data); + + metadata[..32].copy_from_slice(&signed_hash); + let signature = private_key.sign_rfc6979::(&signed_over_data); + let mut signature_bytes = [0; ecdsa::Signature::BYTES_LENGTH]; + signature.to_bytes(&mut signature_bytes); + metadata[32..96].copy_from_slice(&signature_bytes); + + let public_key = private_key.genpk(); + let mut public_key_bytes = vec![]; + cbor_write( + cbor::Value::from(CoseKey::from(public_key)), + &mut public_key_bytes, + ) + .unwrap(); - // Partition of 0x40000 bytes and 8 bytes metadata are hashed. - let hashed_data = vec![0xFF; 0x40000 + 8]; - let expected_hash = Sha256::hash(&hashed_data); - let mut metadata = vec![0xFF; 40]; - metadata[..32].copy_from_slice(&expected_hash); assert_eq!( - parse_metadata(upgrade_locations, &metadata), - Ok(expected_hash) + parse_metadata(upgrade_locations, &public_key_bytes, &metadata), + Ok(()) ); // Any manipulation of data fails. - metadata[32] = 0x88; + metadata[METADATA_SIGN_OFFSET] = 0x88; assert_eq!( - parse_metadata(upgrade_locations, &metadata), + parse_metadata(upgrade_locations, &public_key_bytes, &metadata), Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE) ); - metadata[32] = 0xFF; + metadata[METADATA_SIGN_OFFSET] = 0xFF; metadata[0] ^= 0x01; assert_eq!( - parse_metadata(upgrade_locations, &metadata), + parse_metadata(upgrade_locations, &public_key_bytes, &metadata), Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE) ); metadata[0] ^= 0x01; + metadata[32] ^= 0x01; + assert_eq!( + parse_metadata(upgrade_locations, &public_key_bytes, &metadata), + Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE) + ); + metadata[32] ^= 0x01; upgrade_locations.write_partition(0, &[0x88; 1]).unwrap(); assert_eq!( - parse_metadata(upgrade_locations, &metadata), + parse_metadata(upgrade_locations, &public_key_bytes, &metadata), Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE) ); } @@ -3501,10 +3540,6 @@ mod test { let mut signature_bytes = [0; ecdsa::Signature::BYTES_LENGTH]; signature.to_bytes(&mut signature_bytes); - let cose_signature = CoseSignature { - algorithm: SignatureAlgorithm::Es256, - bytes: signature_bytes, - }; let public_key = private_key.genpk(); let mut public_key_bytes = vec![]; @@ -3515,34 +3550,22 @@ mod test { .unwrap(); assert_eq!( - verify_signature( - Some(cose_signature.clone()), - &public_key_bytes, - &signed_hash - ), + verify_signature(&signature_bytes, &public_key_bytes, &signed_hash), Ok(()) ); assert_eq!( - verify_signature(Some(cose_signature.clone()), &public_key_bytes, &[0x55; 32]), + verify_signature(&signature_bytes, &public_key_bytes, &[0x55; 32]), Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE) ); public_key_bytes[0] ^= 0x01; assert_eq!( - verify_signature(Some(cose_signature), &public_key_bytes, &signed_hash), + verify_signature(&signature_bytes, &public_key_bytes, &signed_hash), Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR) ); public_key_bytes[0] ^= 0x01; - assert_eq!( - verify_signature(None, &public_key_bytes, &signed_hash), - Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) - ); signature_bytes[0] ^= 0x01; - let cose_signature = CoseSignature { - algorithm: SignatureAlgorithm::Es256, - bytes: signature_bytes, - }; assert_eq!( - verify_signature(Some(cose_signature), &public_key_bytes, &signed_hash), + verify_signature(&signature_bytes, &public_key_bytes, &signed_hash), Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE) ); } @@ -3555,38 +3578,37 @@ mod test { let mut env = TestEnv::new(); let private_key = crypto::ecdsa::SecKey::gensk(env.rng()); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); - const METADATA_LEN: usize = 40; + const METADATA_LEN: usize = 0x1000; + const METADATA_SIGN_OFFSET: usize = 0x800; + let mut metadata = vec![0xFF; METADATA_LEN]; + LittleEndian::write_u32(&mut metadata[METADATA_SIGN_OFFSET + 8..][..4], 0x60000); let data = vec![0xFF; 0x1000]; let hash = Sha256::hash(&data).to_vec(); let upgrade_locations = env.upgrade_storage().unwrap(); let partition_length = upgrade_locations.partition_length(); - let mut signed_over_data = upgrade_locations - .read_partition(0, partition_length) - .unwrap() - .to_vec(); - signed_over_data.extend(&[0xFF; METADATA_LEN - 32]); + let mut signed_over_data = metadata[METADATA_SIGN_OFFSET..].to_vec(); + signed_over_data.extend( + upgrade_locations + .read_partition(0, partition_length) + .unwrap(), + ); let signed_hash = Sha256::hash(&signed_over_data); - let mut metadata = vec![0xFF; METADATA_LEN]; - metadata[..32].copy_from_slice(&signed_hash); - let metadata_hash = Sha256::hash(&metadata).to_vec(); + metadata[..32].copy_from_slice(&signed_hash); let signature = private_key.sign_rfc6979::(&signed_over_data); let mut signature_bytes = [0; ecdsa::Signature::BYTES_LENGTH]; signature.to_bytes(&mut signature_bytes); - let cose_signature = CoseSignature { - algorithm: SignatureAlgorithm::Es256, - bytes: signature_bytes, - }; + metadata[32..96].copy_from_slice(&signature_bytes); + let metadata_hash = Sha256::hash(&metadata).to_vec(); - // Write to partition and metadata. + // Write to partition. let response = ctap_state.process_vendor_upgrade( &mut env, AuthenticatorVendorUpgradeParameters { address: Some(0x20000), data: data.clone(), hash: hash.clone(), - signature: None, }, ); assert_eq!(response, Ok(ResponseData::AuthenticatorVendorUpgrade)); @@ -3599,7 +3621,6 @@ mod test { address: None, data: metadata.clone(), hash: metadata_hash.clone(), - signature: Some(cose_signature.clone()), }, ); assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE)); @@ -3611,7 +3632,6 @@ mod test { address: None, data: metadata[..METADATA_LEN - 1].to_vec(), hash: metadata_hash, - signature: Some(cose_signature), }, ); assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); @@ -3623,7 +3643,6 @@ mod test { address: Some(0x40000), data: data.clone(), hash, - signature: None, }, ); assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); @@ -3635,7 +3654,6 @@ mod test { address: Some(0x20000), data, hash: [0xEE; 32].to_vec(), - signature: None, }, ); assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE)); @@ -3655,7 +3673,6 @@ mod test { address: Some(0), data, hash, - signature: None, }, ); assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)); diff --git a/tools/deploy_partition.py b/tools/deploy_partition.py index 36e0805..509cf80 100755 --- a/tools/deploy_partition.py +++ b/tools/deploy_partition.py @@ -20,7 +20,6 @@ from __future__ import division from __future__ import print_function import argparse -import datetime import hashlib import os import struct @@ -44,6 +43,7 @@ OPENSK_VID_PID = (0x1915, 0x521F) OPENSK_VENDOR_UPGRADE = 0x42 OPENSK_VENDOR_UPGRADE_INFO = 0x43 PAGE_SIZE = 0x1000 +METADATA_SIGN_OFFSET = 0x800 KERNEL_SIZE = 0x20000 APP_SIZE = 0x20000 PARTITION_ADDRESS = { @@ -54,7 +54,15 @@ ES256_ALGORITHM = -7 ARCH = "thumbv7em-none-eabi" -def create_metadata(firmware_image: bytes, partition_address: int) -> bytes: +def hash_message(message: bytes) -> bytes: + """Uses SHA256 to hash a message.""" + sha256_hash = hashlib.sha256() + sha256_hash.update(message) + return sha256_hash.digest() + + +def create_metadata(firmware_image: bytes, partition_address: int, version: int, + priv_key: Any) -> bytes: """Creates the matching metadata for the given firmware. The metadata consists of a timestamp, the expected address and a hash of @@ -65,25 +73,26 @@ def create_metadata(firmware_image: bytes, partition_address: int) -> bytes: partition_address: The address to be written as a metadata property. Returns: - A byte array consisting of 32B hash, 4B timestamp and 4B partition address - in little endian encoding. + A byte array of page size, consisting of + - 32 B hash, + - 64 B signature, + at the beginning and + - 8 B version and + - 4 B partition address in little endian encoding + after METADATA_SIGN_OFFSET. All other bytes are 0xFF. """ - t = datetime.datetime.utcnow().timestamp() - timestamp = struct.pack("= 2**63: + fatal("The version must fit into an unsigned integer with 63 bit.\n" + "Please pass it using --version") + version_bytes = struct.pack(" bytes: - """Uses SHA256 to hash a message.""" - sha256_hash = hashlib.sha256() - sha256_hash.update(message) - return sha256_hash.digest() + # Prefix sizes that are a multiple of 64 suit our bootloader's SHA. + signed_metadata = pad_to(version_bytes + partition_start, + PAGE_SIZE - METADATA_SIGN_OFFSET) + signed_data = signed_metadata + firmware_image + checksum = hash_message(signed_data) + signature = sign_firmware(signed_data, priv_key) + return pad_to(checksum + signature, METADATA_SIGN_OFFSET) + signed_metadata def check_info(partition_address: int, authenticator: Any): @@ -95,7 +104,8 @@ def check_info(partition_address: int, authenticator: Any): data={}, ) if result[0x01] != partition_address: - fatal("Identifiers do not match.") + fatal(f"Identifiers do not match, received 0x{result[0x01]:0x}, " + f"expected 0x{partition_address:0x}.") except ctap.CtapError as ex: fatal(f"Failed to read OpenSK upgrade info (error: {ex})") @@ -163,19 +173,14 @@ def sign_firmware(data: bytes, priv_key: Any) -> bytes: def main(args): colorama.init() + if not args.priv_key: + fatal("Please pass in a private key file using --private-key.") firmware_image = generate_firmware_image(args.board) partition_address = PARTITION_ADDRESS[args.board] - metadata = create_metadata(firmware_image, partition_address) - - if not args.priv_key: - fatal("Please pass in a private key file using --private-key.") priv_key = load_priv_key(args.priv_key) - signed_data = firmware_image + metadata[32:40] - signature = { - "alg": ES256_ALGORITHM, - "signature": sign_firmware(signed_data, priv_key) - } + metadata = create_metadata(firmware_image, partition_address, args.version, + priv_key) if args.use_vendor_hid: patcher = patch.object(hid.base, "FIDO_USAGE_PAGE", 0xFF00) @@ -206,11 +211,13 @@ def main(args): ) info("Writing metadata...") - cbor_data = {2: metadata, 3: hash_message(metadata), 4: signature} + # TODO Write the correct address when the metadata is transparent. + cbor_data = {2: metadata, 3: hash_message(metadata)} authenticator.send_cbor( OPENSK_VENDOR_UPGRADE, data=cbor_data, ) + except ctap.CtapError as ex: message = "Failed to upgrade OpenSK" if ex.code.value == ctap.CtapError.ERR.INVALID_COMMAND: @@ -263,4 +270,10 @@ if __name__ == "__main__": dest="use_vendor_hid", help=("Whether to upgrade the device using the Vendor HID interface."), ) + parser.add_argument( + "--version", + type=int, + dest="version", + help=("Firmware version that is built."), + ) main(parser.parse_args())