diff --git a/CHANGELOG.md b/CHANGELOG.md index eb783b4..f20e3c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- + +## [v0.4.0-rc.2](https://github.com/trussed-dev/fido-authenticator/releases/tag/v0.4.0-rc.2) (2026-05-31) + - Fix signature counter to improve spec compliance: - Set the initial signature counter to 1. - Correctly handle signature counter overflows by returning 0. - Increment the signature counter by a positive random number per assertion. +- Add the `Config::new` method to create an instance with the default values. +- Add support for multiple credential ID versions and add the `credential_id_version` field to `Config`. +- Add `CredentialIdVersion::V2` using AES-256-GCM. +- Make firmware version configurable depending on the current credential ID format. ## [v0.4.0-rc.1](https://github.com/trussed-dev/fido-authenticator/releases/tag/v0.4.0-rc.1) (2026-05-29) diff --git a/Cargo.toml b/Cargo.toml index eb66182..0f5b166 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fido-authenticator" -version = "0.4.0-rc.1" +version = "0.4.0-rc.2" authors = ["The Trussed developers", "Nicolas Stalder ", "Nitrokey GmbH"] edition = "2021" license = "Apache-2.0 OR MIT" @@ -24,7 +24,7 @@ serde_bytes = { version = "0.11.14", default-features = false } serde-indexed = "0.1" sha2 = { version = "0.10", default-features = false } trussed-chunked = { version = "0.3", optional = true } -trussed-core = { version = "0.2", features = ["aes256-cbc", "certificate-client", "chacha8-poly1305", "crypto-client", "ed255", "filesystem-client", "hmac-sha256", "management-client", "p256", "sha256", "ui-client"] } +trussed-core = { version = "0.2.2", features = ["aes256-cbc", "certificate-client", "chacha8-poly1305", "crypto-client", "ed255", "filesystem-client", "hmac-sha256", "management-client", "p256", "sha256", "ui-client"] } trussed-fs-info = "0.3" trussed-hkdf = "0.4" @@ -34,6 +34,8 @@ apdu-dispatch = ["dep:apdu-app"] ctaphid-dispatch = ["dep:ctaphid-app"] disable-reset-time-window = [] +credential-id-format-v2 = ["trussed-core/aes256-gcm"] + # enables support for a large-blob array longer than 1024 bytes chunked = ["dep:trussed-chunked"] @@ -67,7 +69,7 @@ rand = "0.8.4" rand_chacha = "0.3" sha2 = "0.10" serde_test = "1.0.176" -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "0f8df68be879acdde1f8cf428c11e5d29692a47b", features = ["virt"] } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "ad577412599156ac98f29ee969e76537e506f2bc", features = ["virt"] } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.4.0", features = ["chunked", "hkdf", "virt", "fs-info"] } trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "017921df0930707c4af68882ccb1f8b3f1bbf7c5", default-features = false, features = ["ctaphid"] } usbd-ctaphid = "0.4" @@ -77,7 +79,7 @@ x509-parser = "0.16" features = ["chunked", "dispatch"] [patch.crates-io] -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "0f8df68be879acdde1f8cf428c11e5d29692a47b" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "ad577412599156ac98f29ee969e76537e506f2bc" } [profile.test] opt-level = 2 diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2c86f52..9baff13 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -24,5 +24,5 @@ doc = false bench = false [patch.crates-io] -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "0f8df68be879acdde1f8cf428c11e5d29692a47b" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "ad577412599156ac98f29ee969e76537e506f2bc" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.4.0" } diff --git a/fuzz/fuzz_targets/ctap.rs b/fuzz/fuzz_targets/ctap.rs index 2b9b045..127fc83 100644 --- a/fuzz/fuzz_targets/ctap.rs +++ b/fuzz/fuzz_targets/ctap.rs @@ -19,7 +19,8 @@ fuzz_target!(|requests: Vec>| { large_blobs: None, nfc_transport: false, ccid_transport: false, - firmware_version: Some(0), + firmware_version: Some(0.into()), + credential_id_version: None, }, ); diff --git a/src/credential.rs b/src/credential.rs index f248e11..ad43e82 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -6,9 +6,9 @@ use ctap_types::sizes::MAX_CRED_BLOB_LENGTH; use serde::Serialize; use serde_bytes::ByteArray; use trussed_core::{ - mechanisms::{Chacha8Poly1305, Sha256}, + mechanisms::Sha256, syscall, try_syscall, - types::{EncryptedData, KeyId}, + types::{EncryptedData, KeyId, Location, Mechanism, StorageAttributes}, CryptoClient, FilesystemClient, }; @@ -36,29 +36,68 @@ pub enum CtapVersion { Fido21Pre, } -/// External ID of a credential, commonly known as "keyhandle". -#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct CredentialId(pub Bytes); +/// Format version for the credential ID. +/// +/// The version is stored with the persistent state so that it is only changed if a reset is +/// performed and thus old credentials are invalidated. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub enum CredentialIdVersion { + /// Private keys for non-resident credentials are wrapped using Chacha8Poly1305, serialized + /// credential is encrypted using Chacha8Poly1305. + V1, + /// Like `V1`, but using AES-256-GCM instead of Chacha8Poly1305. + #[cfg(feature = "credential-id-format-v2")] + V2, +} -impl CredentialId { - fn new( +impl CredentialIdVersion { + fn mechanism(self) -> Mechanism { + match self { + Self::V1 => Mechanism::Chacha8Poly1305, + #[cfg(feature = "credential-id-format-v2")] + Self::V2 => Mechanism::Aes256Gcm, + } + } + + fn generate_key(&self, trussed: &mut T) -> KeyId { + syscall!(trussed.generate_key( + self.mechanism(), + StorageAttributes::new().set_persistence(Location::Internal) + )) + .key + } + + pub fn generate_key_encryption_key( + &self, + trussed: &mut T, + ) -> KeyEncryptionKey { + KeyEncryptionKey(self.generate_key(trussed)) + } + + pub fn generate_key_wrapping_key(&self, trussed: &mut T) -> KeyWrappingKey { + KeyWrappingKey(self.generate_key(trussed)) + } + + pub fn id( + &self, trussed: &mut T, credential: &C, - key_encryption_key: KeyId, + key_encryption_key: KeyEncryptionKey, rp_id_hash: &[u8; 32], nonce: &[u8; 12], - ) -> Result { + ) -> Result { let mut serialized_credential = SerializedCredential::new(); cbor_smol::cbor_serialize_to(credential, &mut serialized_credential) .map_err(|_| Error::Other)?; let message = &serialized_credential; // info!("serialized cred = {:?}", message).ok(); let associated_data = &rp_id_hash[..]; - let encrypted_serialized_credential = syscall!(trussed.encrypt_chacha8poly1305( - key_encryption_key, + let encrypted_serialized_credential = syscall!(trussed.encrypt( + self.mechanism(), + key_encryption_key.0, message, associated_data, - Some(nonce) + Some(nonce.into()), )); let mut credential_id = Bytes::new(); cbor_smol::cbor_serialize_to( @@ -66,10 +105,80 @@ impl CredentialId { &mut credential_id, ) .map_err(|_| Error::RequestTooLarge)?; - Ok(Self(credential_id)) + Ok(CredentialId(credential_id)) + } + + pub fn credential( + &self, + trussed: &mut T, + key_encryption_key: KeyEncryptionKey, + rp_id_hash: &[u8; 32], + id: &[u8], + ) -> Result { + let encrypted_serialized = CredentialIdRef(id).deserialize()?; + + let serialized = try_syscall!(trussed.decrypt( + self.mechanism(), + key_encryption_key.0, + &encrypted_serialized.ciphertext, + &rp_id_hash[..], + &encrypted_serialized.nonce, + &encrypted_serialized.tag, + )) + .map_err(|_| Error::InvalidCredential)? + .plaintext + .ok_or(Error::InvalidCredential)?; + + // In older versions of this app, we serialized the full credential. Now we only serialize + // the stripped credential. For compatibility, we have to try both. + FullCredential::deserialize(&serialized) + .map(Credential::Full) + .or_else(|_| StrippedCredential::deserialize(&serialized).map(Credential::Stripped)) + .map_err(|_| Error::InvalidCredential) + } + + pub fn wrap_key( + &self, + trussed: &mut T, + key_wrapping_key: KeyWrappingKey, + key: KeyId, + ) -> Result { + let wrapped_key = + syscall!(trussed.wrap_key(self.mechanism(), key_wrapping_key.0, key, &[], None)) + .wrapped_key; + Ok(Key::WrappedKey( + Bytes::try_from(wrapped_key.as_slice()).map_err(|_| Error::Other)?, + )) + } + + pub fn unwrap_key( + &self, + trussed: &mut T, + key_wrapping_key: KeyWrappingKey, + key: &Bytes<128>, + ) -> Option { + syscall!(trussed.unwrap_key( + self.mechanism(), + key_wrapping_key.0, + key.increase_capacity(), + &[], + &[], + StorageAttributes::new().set_persistence(Location::Volatile), + )) + .key } } +#[derive(Clone, Copy, Debug)] +pub struct KeyEncryptionKey(pub KeyId); + +#[derive(Clone, Copy, Debug)] +pub struct KeyWrappingKey(pub KeyId); + +/// External ID of a credential, commonly known as "keyhandle". +#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct CredentialId(pub Bytes); + struct CredentialIdRef<'a>(&'a [u8]); impl CredentialIdRef<'_> { @@ -111,7 +220,7 @@ pub enum Credential { } impl Credential { - pub fn try_from( + pub fn try_from( authnr: &mut Authenticator, rp_id_hash: &[u8; 32], descriptor: &PublicKeyCredentialDescriptorRef, @@ -119,49 +228,32 @@ impl Credential { Self::try_from_bytes(authnr, rp_id_hash, descriptor.id) } - pub fn try_from_bytes< - UP: UserPresence, - T: CryptoClient + Chacha8Poly1305 + FilesystemClient, - >( + pub fn try_from_bytes( authnr: &mut Authenticator, rp_id_hash: &[u8; 32], id: &[u8], ) -> Result { - let encrypted_serialized = CredentialIdRef(id).deserialize()?; - + let id_version = authnr.state.persistent.credential_id_version(); let kek = authnr .state .persistent .key_encryption_key(&mut authnr.trussed)?; - let serialized = try_syscall!(authnr.trussed.decrypt_chacha8poly1305( - kek, - &encrypted_serialized.ciphertext, - &rp_id_hash[..], - &encrypted_serialized.nonce, - &encrypted_serialized.tag, - )) - .map_err(|_| Error::InvalidCredential)? - .plaintext - .ok_or(Error::InvalidCredential)?; - - // In older versions of this app, we serialized the full credential. Now we only serialize - // the stripped credential. For compatibility, we have to try both. - FullCredential::deserialize(&serialized) - .map(Self::Full) - .or_else(|_| StrippedCredential::deserialize(&serialized).map(Self::Stripped)) - .map_err(|_| Error::InvalidCredential) + id_version.credential(&mut authnr.trussed, kek, rp_id_hash, id) } - pub fn id( + pub fn id( &self, trussed: &mut T, - key_encryption_key: KeyId, + id_version: CredentialIdVersion, + key_encryption_key: KeyEncryptionKey, rp_id_hash: &[u8; 32], ) -> Result { match self { - Self::Full(credential) => credential.id(trussed, key_encryption_key, Some(rp_id_hash)), - Self::Stripped(credential) => CredentialId::new( + Self::Full(credential) => { + credential.id(trussed, id_version, key_encryption_key, Some(rp_id_hash)) + } + Self::Stripped(credential) => id_version.id( trussed, credential, key_encryption_key, @@ -667,10 +759,11 @@ impl FullCredential { // the ID will stay below 255 bytes. // // Existing keyhandles can still be decoded - pub fn id( + pub fn id( &self, trussed: &mut T, - key_encryption_key: KeyId, + id_version: CredentialIdVersion, + key_encryption_key: KeyEncryptionKey, rp_id_hash: Option<&[u8; 32]>, ) -> Result { let rp_id_hash: [u8; 32] = if let Some(hash) = rp_id_hash { @@ -683,10 +776,10 @@ impl FullCredential { .map_err(|_| Error::Other)? }; if self.use_short_id.unwrap_or_default() { - StrippedCredential::from(self).id(trussed, key_encryption_key, &rp_id_hash) + StrippedCredential::from(self).id(trussed, id_version, key_encryption_key, &rp_id_hash) } else { let stripped_credential = self.strip(); - CredentialId::new( + id_version.id( trussed, &stripped_credential, key_encryption_key, @@ -763,13 +856,14 @@ impl StrippedCredential { } } - pub fn id( + pub fn id( &self, trussed: &mut T, - key_encryption_key: KeyId, + id_version: CredentialIdVersion, + key_encryption_key: KeyEncryptionKey, rp_id_hash: &[u8; 32], ) -> Result { - CredentialId::new(trussed, self, key_encryption_key, rp_id_hash, &self.nonce) + id_version.id(trussed, self, key_encryption_key, rp_id_hash, &self.nonce) } } @@ -994,12 +1088,13 @@ mod test { ) .unwrap() }; + let kek = KeyEncryptionKey(kek); platform.run_client(client_id.as_str(), |mut client| { let data = old_credential_data(); let rp_id_hash = syscall!(client.hash_sha256(data.rp.id().as_ref())).hash; let encrypted_serialized = CredentialIdRef(OLD_ID).deserialize().unwrap(); let serialized = syscall!(client.decrypt_chacha8poly1305( - kek, + kek.0, &encrypted_serialized.ciphertext, &rp_id_hash, &encrypted_serialized.nonce, @@ -1036,7 +1131,12 @@ mod test { let credential = Credential::Full(full); let id = credential - .id(&mut client, kek, rp_id_hash.as_ref().try_into().unwrap()) + .id( + &mut client, + CredentialIdVersion::V1, + kek, + rp_id_hash.as_ref().try_into().unwrap(), + ) .unwrap() .0; assert_eq!( @@ -1050,7 +1150,8 @@ mod test { #[test] fn credential_ids() { trussed::virt::with_client(StoreConfig::ram(), "fido", |mut client| { - let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; + let format = CredentialIdVersion::V1; + let kek = format.generate_key_encryption_key(&mut client); let nonce = ByteArray::new([0; 12]); let data = credential_data(); let mut full_credential = FullCredential { @@ -1068,10 +1169,10 @@ mod test { full_credential.data.use_short_id = Some(true); let stripped_credential = StrippedCredential::from(&full_credential); let full_id = full_credential - .id(&mut client, kek, Some(&rp_id_hash)) + .id(&mut client, format, kek, Some(&rp_id_hash)) .unwrap(); let short_id = stripped_credential - .id(&mut client, kek, &rp_id_hash) + .id(&mut client, format, kek, &rp_id_hash) .unwrap(); assert_eq!(full_id.0, short_id.0); @@ -1079,16 +1180,17 @@ mod test { full_credential.data.use_short_id = None; let stripped_credential = full_credential.strip(); let full_id = full_credential - .id(&mut client, kek, Some(&rp_id_hash)) + .id(&mut client, format, kek, Some(&rp_id_hash)) + .unwrap(); + let long_id = format + .id( + &mut client, + &stripped_credential, + kek, + &rp_id_hash, + &full_credential.nonce, + ) .unwrap(); - let long_id = CredentialId::new( - &mut client, - &stripped_credential, - kek, - &rp_id_hash, - &full_credential.nonce, - ) - .unwrap(); assert_eq!(full_id.0, long_id.0); assert!(short_id.0.len() < long_id.0.len()); @@ -1112,13 +1214,16 @@ mod test { third_party_payment: Some(true), }; trussed::virt::with_client(StoreConfig::ram(), "fido", |mut client| { - let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; + let id_version = CredentialIdVersion::V1; + let kek = id_version.generate_key_encryption_key(&mut client); let rp_id_hash = syscall!(client.hash_sha256(rp_id.as_ref())) .hash .as_slice() .try_into() .unwrap(); - let id = credential.id(&mut client, kek, &rp_id_hash).unwrap(); + let id = credential + .id(&mut client, id_version, kek, &rp_id_hash) + .unwrap(); assert_eq!(id.0.len(), 241); }); } diff --git a/src/ctap1.rs b/src/ctap1.rs index 7a0a478..24494d2 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -68,18 +68,13 @@ impl Authenticator for crate::Authenti .map_err(|_| Error::UnspecifiedCheckingError)?; // debug!("wrapping u2f private key"); - let wrapped_key = - syscall!(self - .trussed - .wrap_key_chacha8poly1305(wrapping_key, private_key, &[], None)) - .wrapped_key; - // debug!("wrapped_key = {:?}", &wrapped_key); + let credential_id_version = self.state.persistent.credential_id_version(); + let key = credential_id_version + .wrap_key(&mut self.trussed, wrapping_key, private_key) + .map_err(|_| Error::UnspecifiedCheckingError)?; syscall!(self.trussed.delete(private_key)); - let key = Key::WrappedKey( - Bytes::try_from(&*wrapped_key).map_err(|_| Error::UnspecifiedCheckingError)?, - ); let nonce = ByteArray::new(self.nonce()); let credential = StrippedCredential { @@ -108,7 +103,7 @@ impl Authenticator for crate::Authenti .key_encryption_key(&mut self.trussed) .map_err(|_| Error::NotEnoughMemory)?; let credential_id = credential - .id(&mut self.trussed, kek, reg.app_id) + .id(&mut self.trussed, credential_id_version, kek, reg.app_id) .map_err(|_| Error::NotEnoughMemory)?; let mut commitment = Commitment::new(); @@ -191,18 +186,14 @@ impl Authenticator for crate::Authenti let key = match cred.key() { Key::WrappedKey(bytes) => { + let credential_id_version = self.state.persistent.credential_id_version(); let wrapping_key = self .state .persistent .key_wrapping_key(&mut self.trussed) .map_err(|_| Error::IncorrectDataParameter)?; - let key_result = syscall!(self.trussed.unwrap_key_chacha8poly1305( - wrapping_key, - bytes, - &[], - Location::Volatile, - )) - .key; + let key_result = + credential_id_version.unwrap_key(&mut self.trussed, wrapping_key, bytes); match key_result { Some(key) => { info!("loaded u2f key!"); diff --git a/src/ctap2.rs b/src/ctap2.rs index 8fcc26e..4bfb452 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -149,7 +149,10 @@ impl Authenticator for crate::Authenti response.max_creds_in_list = Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST); response.max_cred_id_length = Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH); response.algorithms = Some(algorithms); - response.firmware_version = self.config.firmware_version; + response.firmware_version = self + .config + .firmware_version + .map(|version| version.value(self.state.persistent.credential_id_version())); response.remaining_discoverable_credentials = remaining_discoverable_credentials.map(|count| count as usize); response.max_cred_blob_length = Some(MAX_CRED_BLOB_LENGTH); @@ -400,6 +403,7 @@ impl Authenticator for crate::Authenti }; // 12. if `rk` is set, store or overwrite key pair, if full error KeyStoreFull + let credential_id_version = self.state.persistent.credential_id_version(); // 12.a generate credential let key_parameter = match rk_requested { @@ -407,19 +411,7 @@ impl Authenticator for crate::Authenti false => { // WrappedKey version let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( - wrapping_key, - private_key, - &[], - None - )) - .wrapped_key; - - // 32B key, 12B nonce, 16B tag + some info on algorithm (P256/Ed25519) - // Turns out it's size 92 (enum serialization not optimized yet...) - // let mut wrapped_key = Bytes::<60>::new(); - // wrapped_key.extend_from_slice(&wrapped_key_msg).unwrap(); - Key::WrappedKey(Bytes::try_from(&*wrapped_key).map_err(|_| Error::Other)?) + credential_id_version.wrap_key(&mut self.trussed, wrapping_key, private_key)? } }; @@ -459,8 +451,12 @@ impl Authenticator for crate::Authenti ); // note that this does the "stripping" of OptionalUI etc. - let credential_id = - StrippedCredential::from(&credential).id(&mut self.trussed, kek, &rp_id_hash)?; + let credential_id = StrippedCredential::from(&credential).id( + &mut self.trussed, + credential_id_version, + kek, + &rp_id_hash, + )?; if rk_requested { // serialization with all metadata @@ -655,7 +651,9 @@ impl Authenticator for crate::Authenti large_blobs::reset(&mut self.trussed); // b. delete persistent state - self.state.persistent.reset(&mut self.trussed)?; + self.state + .persistent + .reset(&mut self.trussed, &self.config)?; // c. Reset runtime state self.state.runtime.reset(&mut self.trussed); @@ -2049,6 +2047,7 @@ impl crate::Authenticator { credential: Credential, ) -> Result { let data = self.state.runtime.active_get_assertion.clone().unwrap(); + let credential_id_version = self.state.persistent.credential_id_version(); let rp_id_hash = &data.rp_id_hash; let (key, is_rk) = match credential.key().clone() { @@ -2056,13 +2055,8 @@ impl crate::Authenticator { Key::WrappedKey(bytes) => { let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; // info_now!("unwrapping {:?} with wrapping key {:?}", &bytes, &wrapping_key); - let key_result = syscall!(self.trussed.unwrap_key_chacha8poly1305( - wrapping_key, - &bytes, - &[], - Location::Volatile, - )) - .key; + let key_result = + credential_id_version.unwrap_key(&mut self.trussed, wrapping_key, &bytes); // debug_now!("key result: {:?}", &key_result); info_now!("key result"); match key_result { @@ -2096,7 +2090,8 @@ impl crate::Authenticator { .state .persistent .key_encryption_key(&mut self.trussed)?; - let credential_id = credential.id(&mut self.trussed, kek, rp_id_hash)?; + let credential_id = + credential.id(&mut self.trussed, credential_id_version, kek, rp_id_hash)?; use ctap2::AuthenticatorDataFlags as Flags; diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 1218766..bdef8b1 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -383,12 +383,13 @@ where // why these contortions to get kek. sheesh let authnr = &mut self.authnr; + let credential_id_version = authnr.state.persistent.credential_id_version(); let kek = authnr .state .persistent .key_encryption_key(&mut authnr.trussed)?; - let credential_id = credential.id(&mut self.trussed, kek, None)?; + let credential_id = credential.id(&mut self.trussed, credential_id_version, kek, None)?; use crate::credential::Key; let private_key = match credential.key { diff --git a/src/dispatch.rs b/src/dispatch.rs index 92e6b69..6c8cb8f 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -103,7 +103,7 @@ where authenticator .state .persistent - .load_if_not_initialised(&mut authenticator.trussed); + .load_if_not_initialised(&mut authenticator.trussed, &authenticator.config); // let command = apdu_dispatch::Command::try_from(data) // .map_err(|_| Status::IncorrectDataParameter)?; @@ -138,7 +138,7 @@ where authenticator .state .persistent - .load_if_not_initialised(&mut authenticator.trussed); + .load_if_not_initialised(&mut authenticator.trussed, &authenticator.config); debug!( "try_handle CTAP2: remaining stack: {} bytes", @@ -169,7 +169,7 @@ where authenticator .state .persistent - .load_if_not_initialised(&mut authenticator.trussed); + .load_if_not_initialised(&mut authenticator.trussed, &authenticator.config); debug!( "try_get CTAP2: remaining stack: {} bytes", diff --git a/src/lib.rs b/src/lib.rs index 03f400b..65ad612 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,15 +133,69 @@ pub struct Config { /// Firmware version reported by `authenticatorGetInfo` (CTAP 2.1 ยง6.4 0x0E). /// /// The runner is expected to plumb its own version constant in here. - pub firmware_version: Option, + pub firmware_version: Option, + /// The credential ID format to use for new credentials. + /// + /// To avoid invalidating existing credentials, this value is only used if the state is clean, + /// i. e. on the first start or after a reset. Otherwise, `V1` is used. + pub credential_id_version: Option, } impl Config { + pub fn new(max_msg_size: usize) -> Self { + Self { + max_msg_size, + skip_up_timeout: None, + max_resident_credential_count: None, + large_blobs: None, + nfc_transport: false, + ccid_transport: false, + firmware_version: None, + credential_id_version: None, + } + } + pub fn supports_large_blobs(&self) -> bool { self.large_blobs.is_some() } } +/// This struct makes it possible to define the firmware version based on the credential ID format +/// that is currently used by the authenticator. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct FirmwareVersion { + pub default: usize, + pub credential_id_v1: Option, + #[cfg(feature = "credential-id-format-v2")] + pub credential_id_v2: Option, +} + +impl FirmwareVersion { + pub fn new(default: usize) -> Self { + Self { + default, + credential_id_v1: None, + #[cfg(feature = "credential-id-format-v2")] + credential_id_v2: None, + } + } + + pub fn value(&self, credential_id_version: credential::CredentialIdVersion) -> usize { + let value = match credential_id_version { + credential::CredentialIdVersion::V1 => self.credential_id_v1, + #[cfg(feature = "credential-id-format-v2")] + credential::CredentialIdVersion::V2 => self.credential_id_v2, + }; + value.unwrap_or(self.default) + } +} + +impl From for FirmwareVersion { + fn from(default: usize) -> Self { + Self::new(default) + } +} + // impl Default for Config { // fn default() -> Self { // Self { diff --git a/src/state.rs b/src/state.rs index 847e748..b437c59 100644 --- a/src/state.rs +++ b/src/state.rs @@ -18,7 +18,7 @@ use ctap_types::{ }; use littlefs2_core::{path, Path}; use trussed_core::{ - mechanisms::{Chacha8Poly1305, P256}, + mechanisms::P256, syscall, try_syscall, types::{KeyId, Location, Mechanism, Message, PathBuf}, CertificateClient, CryptoClient, FilesystemClient, @@ -30,9 +30,9 @@ use heapless::{ }; use crate::{ - credential::FullCredential, + credential::{CredentialIdVersion, FullCredential, KeyEncryptionKey, KeyWrappingKey}, ctap2::{self, pin::PinProtocolState}, - Result, + Config, Result, }; #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -269,6 +269,7 @@ pub struct PersistentState { // We could alternatively make all methods take a TrussedClient as parameter initialised: bool, + credential_id_version: Option, key_encryption_key: Option, key_wrapping_key: Option, consecutive_pin_mismatches: u8, @@ -312,11 +313,12 @@ impl PersistentState { const FILENAME: &'static Path = path!("persistent-state.cbor"); /// The default value for the state if it is initialized for the first time or reset. - fn reset_value() -> Self { + fn reset_value(config: &Config) -> Self { Self { initialised: false, key_encryption_key: None, key_wrapping_key: None, + credential_id_version: config.credential_id_version, consecutive_pin_mismatches: 0, pin_hash: None, pin_code_point_length: 0, @@ -367,19 +369,27 @@ impl PersistentState { Ok(()) } - pub fn reset(&mut self, trussed: &mut T) -> Result<()> { + pub fn reset( + &mut self, + trussed: &mut T, + config: &Config, + ) -> Result<()> { if let Some(key) = self.key_encryption_key { syscall!(trussed.delete(key)); } if let Some(key) = self.key_wrapping_key { syscall!(trussed.delete(key)); } - *self = Self::reset_value(); + *self = Self::reset_value(config); self.initialised = true; self.save(trussed) } - pub fn load_if_not_initialised(&mut self, trussed: &mut T) { + pub fn load_if_not_initialised( + &mut self, + trussed: &mut T, + config: &Config, + ) { if !self.initialised { match Self::load(trussed) { Ok(previous_self) => { @@ -387,8 +397,9 @@ impl PersistentState { *self = previous_self } Err(_err) => { + // if the state is missing or corrupted, start fresh with a clean state info!("error with previous state! {:?}", _err); - *self = Self::reset_value(); + *self = Self::reset_value(config); } } self.initialised = true; @@ -420,49 +431,57 @@ impl PersistentState { Ok(now) } - pub fn key_encryption_key( + pub fn credential_id_version(&self) -> CredentialIdVersion { + self.credential_id_version + .unwrap_or(CredentialIdVersion::V1) + } + + pub fn key_encryption_key( &mut self, trussed: &mut T, - ) -> Result { + ) -> Result { match self.key_encryption_key { - Some(key) => Ok(key), + Some(key) => Ok(KeyEncryptionKey(key)), None => self.rotate_key_encryption_key(trussed), } } - pub fn rotate_key_encryption_key( + pub fn rotate_key_encryption_key( &mut self, trussed: &mut T, - ) -> Result { + ) -> Result { if let Some(key) = self.key_encryption_key { syscall!(trussed.delete(key)); } - let key = syscall!(trussed.generate_chacha8poly1305_key(Location::Internal)).key; - self.key_encryption_key = Some(key); + let key = self + .credential_id_version() + .generate_key_encryption_key(trussed); + self.key_encryption_key = Some(key.0); self.save(trussed)?; Ok(key) } - pub fn key_wrapping_key( + pub fn key_wrapping_key( &mut self, trussed: &mut T, - ) -> Result { + ) -> Result { match self.key_wrapping_key { - Some(key) => Ok(key), + Some(key) => Ok(KeyWrappingKey(key)), None => self.rotate_key_wrapping_key(trussed), } } - pub fn rotate_key_wrapping_key( + pub fn rotate_key_wrapping_key( &mut self, trussed: &mut T, - ) -> Result { - self.load_if_not_initialised(trussed); + ) -> Result { if let Some(key) = self.key_wrapping_key { syscall!(trussed.delete(key)); } - let key = syscall!(trussed.generate_chacha8poly1305_key(Location::Internal)).key; - self.key_wrapping_key = Some(key); + let key = self + .credential_id_version() + .generate_key_wrapping_key(trussed); + self.key_wrapping_key = Some(key.0); self.save(trussed)?; Ok(key) } @@ -694,6 +713,7 @@ impl RuntimeState { #[cfg(test)] mod tests { use super::*; + use crate::Config; use hex_literal::hex; use trussed::{ backend::BackendId, @@ -717,6 +737,7 @@ mod tests { #[test] fn test_signature_counter() { + let config = Config::new(0); virt::with_platform(StoreConfig::ram(), |platform| { platform.run_client_with_backends( "fido", @@ -727,7 +748,7 @@ mod tests { ], |mut client| { let mut state = PersistentState::default(); - state.load_if_not_initialised(&mut client); + state.load_if_not_initialised(&mut client, &config); let counter1 = state.signature_counter(&mut client).unwrap(); let counter2 = state.signature_counter(&mut client).unwrap(); diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 71f54c5..48468ea 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -75,7 +75,8 @@ where large_blobs: None, nfc_transport: options.nfc_transport, ccid_transport: options.ccid_transport, - firmware_version: Some(0), + firmware_version: Some(0.into()), + credential_id_version: None, }, );