diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index a39a983..758f94a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -37,7 +37,6 @@ use self::data_formats::{ PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; -use self::key_material::{AAGUID, ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY}; use self::response::{ AuthenticatorClientPinResponse, AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, ResponseData, @@ -509,7 +508,7 @@ where }; let mut auth_data = self.generate_auth_data(&rp_id_hash, flags); - auth_data.extend(AAGUID); + auth_data.extend(self.persistent_store.aaguid()?); // The length is fixed to 0x20 or 0x70 and fits one byte. if credential_id.len() > 0xFF { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG); @@ -534,10 +533,11 @@ where signature_data.extend(client_data_hash); let (signature, x5c) = if USE_BATCH_ATTESTATION { let attestation_key = - crypto::ecdsa::SecKey::from_bytes(ATTESTATION_PRIVATE_KEY).unwrap(); + crypto::ecdsa::SecKey::from_bytes(self.persistent_store.attestation_private_key()?) + .unwrap(); ( attestation_key.sign_rfc6979::(&signature_data), - Some(vec![ATTESTATION_CERTIFICATE.to_vec()]), + Some(vec![self.persistent_store.attestation_certificate()?]), ) } else { ( @@ -769,7 +769,7 @@ where String::from(FIDO2_VERSION_STRING), ], extensions: Some(vec![String::from("hmac-secret")]), - aaguid: *AAGUID, + aaguid: *self.persistent_store.aaguid()?, options: Some(options_map), max_msg_size: Some(1024), pin_protocols: Some(vec![ @@ -1124,7 +1124,7 @@ mod test { 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x03, 0x50, ]); - expected_response.extend(AAGUID); + expected_response.extend(ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend(&[ 0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, @@ -1197,7 +1197,7 @@ mod test { 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, ]; - expected_auth_data.extend(AAGUID); + expected_auth_data.extend(ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, 0x20]); assert_eq!( auth_data[0..expected_auth_data.len()], @@ -1234,7 +1234,7 @@ mod test { 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, ]; - expected_auth_data.extend(AAGUID); + expected_auth_data.extend(ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, ENCRYPTED_CREDENTIAL_ID_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], @@ -1330,7 +1330,7 @@ mod test { 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, ]; - expected_auth_data.extend(AAGUID); + expected_auth_data.extend(ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, 0x20]); assert_eq!( auth_data[0..expected_auth_data.len()], diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 1e0c2bf..b58b8d0 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -67,5 +67,11 @@ pub enum Ctap2StatusCode { // CTAP2_ERR_VENDOR_FIRST = 0xF0, CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG = 0xF0, CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR = 0xF1, + + /// An internal invariant is broken. + /// + /// This type of error is unexpected and the current state is undefined. + CTAP2_ERR_VENDOR_INTERNAL_ERROR = 0xF2, + CTAP2_ERR_VENDOR_LAST = 0xFF, } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index f0a2ca4..b8cfa37 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -14,6 +14,7 @@ use crate::crypto::rng256::Rng256; use crate::ctap::data_formats::PublicKeyCredentialSource; +use crate::ctap::key_material; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::PIN_AUTH_LENGTH; use alloc::string::String; @@ -56,9 +57,14 @@ const GLOBAL_SIGNATURE_COUNTER: usize = 1; const MASTER_KEYS: usize = 2; const PIN_HASH: usize = 3; const PIN_RETRIES: usize = 4; -const NUM_TAGS: usize = 5; +const ATTESTATION_PRIVATE_KEY: usize = 5; +const ATTESTATION_CERTIFICATE: usize = 6; +const AAGUID: usize = 7; +const NUM_TAGS: usize = 8; const MAX_PIN_RETRIES: u8 = 6; +const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; +const AAGUID_LENGTH: usize = 16; #[derive(PartialEq, Eq, PartialOrd, Ord)] enum Key { @@ -73,6 +79,9 @@ enum Key { MasterKeys, PinHash, PinRetries, + AttestationPrivateKey, + AttestationCertificate, + Aaguid, } pub struct MasterKeys<'a> { @@ -124,6 +133,9 @@ impl StoreConfig for Config { MASTER_KEYS => add(Key::MasterKeys), PIN_HASH => add(Key::PinHash), PIN_RETRIES => add(Key::PinRetries), + ATTESTATION_PRIVATE_KEY => add(Key::AttestationPrivateKey), + ATTESTATION_CERTIFICATE => add(Key::AttestationCertificate), + AAGUID => add(Key::Aaguid), _ => debug_assert!(false), } } @@ -211,6 +223,33 @@ impl PersistentStore { }) .unwrap(); } + if self.store.find_one(&Key::AttestationPrivateKey).is_none() { + self.store + .insert(StoreEntry { + tag: ATTESTATION_PRIVATE_KEY, + data: key_material::ATTESTATION_PRIVATE_KEY, + sensitive: false, + }) + .unwrap(); + } + if self.store.find_one(&Key::AttestationCertificate).is_none() { + self.store + .insert(StoreEntry { + tag: ATTESTATION_CERTIFICATE, + data: key_material::ATTESTATION_CERTIFICATE, + sensitive: false, + }) + .unwrap(); + } + if self.store.find_one(&Key::Aaguid).is_none() { + self.store + .insert(StoreEntry { + tag: AAGUID, + data: key_material::AAGUID, + sensitive: false, + }) + .unwrap(); + } } pub fn find_credential( @@ -394,10 +433,44 @@ impl PersistentStore { .unwrap(); } + pub fn attestation_private_key( + &self, + ) -> Result<&[u8; ATTESTATION_PRIVATE_KEY_LENGTH], Ctap2StatusCode> { + let (_, entry) = self + .store + .find_one(&Key::AttestationPrivateKey) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + let data = entry.data; + if data.len() != ATTESTATION_PRIVATE_KEY_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + Ok(array_ref!(data, 0, ATTESTATION_PRIVATE_KEY_LENGTH)) + } + + pub fn attestation_certificate(&self) -> Result, Ctap2StatusCode> { + let (_, entry) = self + .store + .find_one(&Key::AttestationCertificate) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + Ok(entry.data.to_vec()) + } + + pub fn aaguid(&self) -> Result<&[u8; AAGUID_LENGTH], Ctap2StatusCode> { + let (_, entry) = self + .store + .find_one(&Key::Aaguid) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + let data = entry.data; + if data.len() != AAGUID_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + Ok(array_ref!(data, 0, AAGUID_LENGTH)) + } + pub fn reset(&mut self, rng: &mut impl Rng256) { loop { let index = { - let mut iter = self.store.iter(); + let mut iter = self.store.iter().filter(|(_, entry)| should_reset(entry)); match iter.next() { None => break, Some((index, _)) => index, @@ -419,6 +492,13 @@ impl From for Ctap2StatusCode { } } +fn should_reset<'a>(entry: &StoreEntry<'a>) -> bool { + match entry.tag { + ATTESTATION_PRIVATE_KEY | ATTESTATION_CERTIFICATE | AAGUID => false, + _ => true, + } +} + fn deserialize_credential(data: &[u8]) -> Option { let cbor = cbor::read(data).ok()?; cbor.try_into().ok() @@ -696,4 +776,33 @@ mod test { persistent_store.reset_pin_retries(); assert_eq!(persistent_store.pin_retries(), MAX_PIN_RETRIES); } + + #[test] + fn test_persistent_keys() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + // The persistent keys are initialized on a fresh store. + assert_eq!( + persistent_store.attestation_private_key().unwrap(), + key_material::ATTESTATION_PRIVATE_KEY + ); + assert_eq!( + persistent_store.attestation_certificate().unwrap(), + key_material::ATTESTATION_CERTIFICATE + ); + assert_eq!(persistent_store.aaguid().unwrap(), key_material::AAGUID); + + // The persistent keys stay initialized and preserve their value after a reset. + persistent_store.reset(&mut rng); + assert_eq!( + persistent_store.attestation_private_key().unwrap(), + key_material::ATTESTATION_PRIVATE_KEY + ); + assert_eq!( + persistent_store.attestation_certificate().unwrap(), + key_material::ATTESTATION_CERTIFICATE + ); + assert_eq!(persistent_store.aaguid().unwrap(), key_material::AAGUID); + } }