From 543199ebdee32fcd4b2abacc926bb59b5ea163c0 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Wed, 15 Dec 2021 18:42:00 +0100 Subject: [PATCH 01/13] Correct build errors --- src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 35660c2..ed3aeae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -477,7 +477,7 @@ where UP: UserPresence, return Err(Error::InvalidParameter); } - Ok(match parameters.sub_command { + Ok( match parameters.sub_command { Subcommand::GetRetries => { debug!("processing CP.GR"); @@ -660,6 +660,10 @@ where UP: UserPresence, } } + _ => { + // todo!("not implemented yet") + return Err(Error::InvalidParameter); + } }) } @@ -2069,7 +2073,7 @@ where UP: UserPresence, fn get_info(&mut self) -> ctap2::get_info::Response { use core::str::FromStr; - let mut versions = Vec::, 3>::new(); + let mut versions = Vec::, 4>::new(); versions.push(String::from_str("U2F_V2").unwrap()).unwrap(); versions.push(String::from_str("FIDO_2_0").unwrap()).unwrap(); // #[cfg(feature = "enable-fido-pre")] From 5e02d4016f07e678603a03e7a51370ab5bd9331b Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Sat, 8 Jan 2022 14:03:12 +0100 Subject: [PATCH 02/13] Make the Credential ID shorter to work with some services Some services do not accept arbitrary long key handle (aka Credential ID), which makes the FIDO operations failing. This patch removes some fields from credential data serialization while making credential ID, and with this it reduces key handle size by around 30% (from ~320 to ~220 using test site [1]). Tested on Gitlab, and this patch makes it working correctly (both registering and signing, as opposed to 500 error code returned otherwise). Presumably the hidden limit is 255 bytes, which would be compatible with CTAP1. Resident Keys stay the same, with full metadata stored on the device. [1] webauthn.bin.coffee --- src/credential.rs | 2 ++ src/lib.rs | 58 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index a21a00a..ceb568e 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -89,7 +89,9 @@ pub struct CredentialData { pub key: Key, // extensions + #[serde(skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, // TODO: add `sig_counter: Option`, diff --git a/src/lib.rs b/src/lib.rs index ed3aeae..2973231 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,10 @@ use ctap_types::{ Result as U2fResult, Error as U2fError, }, + webauthn::{ + PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity + } }; use littlefs2::path::{Path, PathBuf}; @@ -1835,31 +1839,53 @@ where UP: UserPresence, let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap(); info!("nonce = {:?}", &nonce); - let credential = Credential::new( - credential::CtapVersion::Fido21Pre, - ¶meters.rp, - ¶meters.user, - algorithm as i32, - key_parameter, - self.state.persistent.timestamp(&mut self.trussed)?, - hmac_secret_requested.clone(), - cred_protect_requested, - nonce, - ); - - // info!("made credential {:?}", &credential); - // 12.b generate credential ID { = AEAD(Serialize(Credential)) } let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; - let credential_id = credential.id_using_hash(&mut self.trussed, kek, &rp_id_hash)?; // store it. // TODO: overwrite, error handling with KeyStoreFull - let serialized_credential = credential.serialize()?; + // Introduce smaller Credential struct for ID, with extra metadata removed. This ensures + // ID will stay below 255 bytes. + let credential_thin = Credential::new( + credential::CtapVersion::Fido21Pre, + &PublicKeyCredentialRpEntity{ + id: parameters.rp.id.clone(), + name: None, + url: None + }, + &PublicKeyCredentialUserEntity { + id: parameters.user.id.clone(), + icon: None, + name: None, + display_name: None + }, + algorithm as i32, + key_parameter.clone(), + self.state.persistent.timestamp(&mut self.trussed)?, + None, + None, + nonce, + ); + let credential_id = credential_thin.id_using_hash(&mut self.trussed, kek, &rp_id_hash)?; if rk_requested { + // Create full credential for the Resident Key usage, and store it in local memory. + let credential = Credential::new( + credential::CtapVersion::Fido21Pre, + ¶meters.rp, + ¶meters.user, + algorithm as i32, + key_parameter, + self.state.persistent.timestamp(&mut self.trussed)?, + hmac_secret_requested.clone(), + cred_protect_requested, + nonce, + ); + // info!("made credential {:?}", &credential); + let serialized_credential = credential.serialize()?; + // first delete any other RK cred with same RP + UserId if there is one. self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id).ok(); From c16551c754127baab697a3d6534c1daf50cf1f88 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Tue, 1 Feb 2022 13:45:38 +0100 Subject: [PATCH 03/13] Move credential stripping/thinning into type --- src/credential.rs | 96 ++++++++----------- src/credential_management.rs | 2 +- src/lib.rs | 179 +++-------------------------------- 3 files changed, 58 insertions(+), 219 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index ceb568e..ce745ac 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -167,41 +167,38 @@ impl Credential { } } - pub fn id_using_hash<'a, T: client::Chacha8Poly1305>( - &self, - crypto: &mut T, - key_encryption_key: KeyId, - rp_id_hash: &Bytes32, - ) - -> Result - { - let serialized_credential = self.serialize()?; - let message = &serialized_credential; - - let associated_data = &rp_id_hash[..]; - let nonce: [u8; 12] = self.nonce.as_slice().try_into().unwrap(); - let encrypted_serialized_credential = EncryptedSerializedCredential( - syscall!(crypto.encrypt_chacha8poly1305( - key_encryption_key, message, associated_data, Some(&nonce)))); - let credential_id: CredentialId = encrypted_serialized_credential.try_into().unwrap(); - - Ok(credential_id) - } - + // ID (or "keyhandle") for the credential. + // + // Originally, the entire data was serialized, and its encryption + // (binding RP as associated data) used as a keyhandle. + // + // However, this leads to problems with relying parties. According to the old U2F + // spec, the length of a keyhandle is encoded as one byte, whereas this procedure would + // generate keyhandles of length ~320 bytes. + // + // Therefore, inessential metadata is stripped before serialization, ensuring + // the ID will stay below 255 bytes. + // + // Existing keyhandles can still be decoded pub fn id<'a, T: client::Chacha8Poly1305 + client::Sha256>( &self, trussed: &mut T, key_encryption_key: KeyId, + rp_id_hash: Option<&Bytes32>, ) -> Result { - let serialized_credential = self.serialize()?; + let serialized_credential = self.strip().serialize()?; let message = &serialized_credential; - // info!("ser cred = {:?}", message).ok(); - - let rp_id_hash: Bytes32 = syscall!(trussed.hash_sha256(&self.rp.id.as_ref())) - .hash - .to_bytes().map_err(|_| Error::Other)?; + // info!("serialized cred = {:?}", message).ok(); + + let rp_id_hash: Bytes32 = if let Some(hash) = rp_id_hash { + hash.clone() + } else { + syscall!(trussed.hash_sha256(&self.rp.id.as_ref())) + .hash + .to_bytes().map_err(|_| Error::Other)? + }; let associated_data = &rp_id_hash[..]; let nonce: [u8; 12] = self.nonce.as_slice().try_into().unwrap(); @@ -271,32 +268,23 @@ impl Credential { Ok(credential) } - // Does not work, as it would use a new, different nonce! + // Remove inessential metadata from credential. // - // pub fn id(&self) -> Result { - // let serialized_credential = self.serialize()?; - // let key = &self.key_encryption_key()?; - // let message = &serialized_credential; - // let associated_data = parameters.rp.id.as_bytes(); - // let encrypted_serialized_credential = EncryptedSerializedCredential( - // syscall!(self.trussed.encrypt_chacha8poly1305(key, message, associated_data))); - // let credential_id: CredentialId = encrypted_serialized_credential.try_into().unwrap(); - // credential_id - // } - - // pub fn store(&self) -> Result Self { + let mut stripped = self.clone(); + let data = &mut stripped.data; + + data.rp.name = None; + data.rp.url = None; + + data.user.icon = None; + data.user.name = None; + data.user.display_name = None; + + // data.hmac_secret = None; + // data.cred_protect = None; + + stripped + } } diff --git a/src/credential_management.rs b/src/credential_management.rs index 04d8a94..fe3df64 100644 --- a/src/credential_management.rs +++ b/src/credential_management.rs @@ -413,7 +413,7 @@ where UP: UserPresence, let authnr = &mut self.authnr; let kek = authnr.state.persistent.key_encryption_key(&mut authnr.trussed)?; - let credential_id = credential.id(&mut self.trussed, kek)?; + let credential_id = credential.id(&mut self.trussed, kek, None)?; response.credential_id = Some(credential_id.into()); use crate::credential::Key; diff --git a/src/lib.rs b/src/lib.rs index 2973231..6c6029f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,11 +22,7 @@ use trussed::{ use ctap_types::{ Bytes, Bytes32, String, Vec, - // cose::EcdhEsHkdf256PublicKey as CoseEcdhEsHkdf256PublicKey, - // cose::PublicKey as CosePublicKey, operation::VendorOperation, - // rpc::CtapInterchange, - // authenticator::ctap1, authenticator::{ctap2, Error, Request, Response}, ctap1::{ self, @@ -35,10 +31,6 @@ use ctap_types::{ Result as U2fResult, Error as U2fError, }, - webauthn::{ - PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity - } }; use littlefs2::path::{Path, PathBuf}; @@ -238,7 +230,7 @@ where UP: UserPresence, // 12.b generate credential ID { = AEAD(Serialize(Credential)) } let kek = self.state.persistent.key_encryption_key(&mut self.trussed).map_err(|_| U2fError::NotEnoughMemory)?; - let credential_id = credential.id_using_hash(&mut self.trussed, kek, ®.app_id).map_err(|_| U2fError::NotEnoughMemory)?; + let credential_id = credential.id(&mut self.trussed, kek, Some(®.app_id)).map_err(|_| U2fError::NotEnoughMemory)?; syscall!(self.trussed.delete(public_key)); syscall!(self.trussed.delete(private_key)); @@ -481,7 +473,7 @@ where UP: UserPresence, return Err(Error::InvalidParameter); } - Ok( match parameters.sub_command { + Ok(match parameters.sub_command { Subcommand::GetRetries => { debug!("processing CP.GR"); @@ -1102,7 +1094,7 @@ where UP: UserPresence, let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; if keep { - let id = credential.id_using_hash(&mut self.trussed, kek, rp_id_hash)?; + let id = credential.id(&mut self.trussed, kek, Some(rp_id_hash))?; let credential_id_hash = self.hash(&id.0.as_ref()); let timestamp_path = TimestampPath { @@ -1132,7 +1124,7 @@ where UP: UserPresence, if keep { - let id = credential.id_using_hash(&mut self.trussed, kek, rp_id_hash)?; + let id = credential.id(&mut self.trussed, kek, Some(rp_id_hash))?; let credential_id_hash = self.hash(&id.0.as_ref()); let timestamp_path = TimestampPath { @@ -1445,7 +1437,7 @@ where UP: UserPresence, // info!("signing with credential {:?}", &credential); let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; - let credential_id = credential.id_using_hash(&mut self.trussed, kek, &rp_id_hash)?; + let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; use ctap2::AuthenticatorDataFlags as Flags; @@ -1628,9 +1620,6 @@ where UP: UserPresence, &mut self, rk_path: &Path, ) - // rp_id_hash: &Bytes32, - // credential_id_hash: &Bytes32, - // ) -> Result<()> { info!("deleting RK {:?}", &rk_path); @@ -1845,45 +1834,22 @@ where UP: UserPresence, // store it. // TODO: overwrite, error handling with KeyStoreFull - // Introduce smaller Credential struct for ID, with extra metadata removed. This ensures - // ID will stay below 255 bytes. - let credential_thin = Credential::new( + let credential = Credential::new( credential::CtapVersion::Fido21Pre, - &PublicKeyCredentialRpEntity{ - id: parameters.rp.id.clone(), - name: None, - url: None - }, - &PublicKeyCredentialUserEntity { - id: parameters.user.id.clone(), - icon: None, - name: None, - display_name: None - }, + ¶meters.rp, + ¶meters.user, algorithm as i32, - key_parameter.clone(), + key_parameter, self.state.persistent.timestamp(&mut self.trussed)?, - None, - None, + hmac_secret_requested.clone(), + cred_protect_requested, nonce, ); - let credential_id = credential_thin.id_using_hash(&mut self.trussed, kek, &rp_id_hash)?; + let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; if rk_requested { - // Create full credential for the Resident Key usage, and store it in local memory. - let credential = Credential::new( - credential::CtapVersion::Fido21Pre, - ¶meters.rp, - ¶meters.user, - algorithm as i32, - key_parameter, - self.state.persistent.timestamp(&mut self.trussed)?, - hmac_secret_requested.clone(), - cred_protect_requested, - nonce, - ); - // info!("made credential {:?}", &credential); + // serialization with all metadata let serialized_credential = credential.serialize()?; // first delete any other RK cred with same RP + UserId if there is one. @@ -1899,13 +1865,14 @@ where UP: UserPresence, None, )).map_err(|_| Error::KeyStoreFull)?; } + // 13. generate and return attestation statement using clientDataHash // 13.a AuthenticatorData and its serialization use ctap2::AuthenticatorDataFlags as Flags; info!("MC created cred id"); - let (attestation_maybe, aaguid)= self.state.identity.attestation(&mut self.trussed); + let (attestation_maybe, aaguid) = self.state.identity.attestation(&mut self.trussed); let authenticator_data = ctap2::make_credential::AuthenticatorData { rp_id_hash: rp_id_hash.to_bytes().map_err(|_| Error::Other)?, @@ -2046,56 +2013,6 @@ where UP: UserPresence, Ok(attestation_object) } - // fn credential_id(credential: &Credential) -> CredentialId { - // } - - // fn get_assertion(&mut self, ...) - // // let unwrapped_key = syscall!(self.trussed.unwrap_key_chacha8poly1305( - // // &wrapping_key, - // // &wrapped_key, - // // b"", - // // Location::Volatile, - // // )).key; - // // test public key ser/de - // let ser_pk = syscall!(self.trussed.serialize_key( - // Mechanism::P256, public_key.clone(), KeySerialization::Raw - // )).serialized_key; - // debug!("ser pk = {:?}", &ser_pk); - - // let cose_ser_pk = syscall!(self.trussed.serialize_key( - // Mechanism::P256, public_key.clone(), KeySerialization::Cose - // )).serialized_key; - // debug!("COSE ser pk = {:?}", &cose_ser_pk); - - // let deser_pk = syscall!(self.trussed.deserialize_key( - // Mechanism::P256, ser_pk.clone(), KeySerialization::Raw, - // StorageAttributes::new().set_persistence(Location::Volatile) - // )).key; - // debug!("deser pk = {:?}", &deser_pk); - - // let cose_deser_pk = syscall!(self.trussed.deserialize_key( - // Mechanism::P256, cose_ser_pk.clone(), KeySerialization::Cose, - // StorageAttributes::new().set_persistence(Location::Volatile) - // )).key; - // debug!("COSE deser pk = {:?}", &cose_deser_pk); - // debug!("raw ser of COSE deser pk = {:?}", - // syscall!(self.trussed.serialize_key(Mechanism::P256, cose_deser_pk.clone(), KeySerialization::Raw)). - // serialized_key); - - // debug!("priv {:?}", &private_key); - // debug!("pub {:?}", &public_key); - - // let _loaded_credential = syscall!(self.trussed.load_blob( - // prefix.clone(), - // blob_id, - // Location::Volatile, - // )).data; - // // debug!("loaded credential = {:?}", &loaded_credential); - - // debug!("credential = {:?}", &Credential::deserialize(&serialized_credential)?); - - // // debug!("unwrapped_key = {:?}", &unwrapped_key); - fn get_info(&mut self) -> ctap2::get_info::Response { use core::str::FromStr; @@ -2139,72 +2056,6 @@ where UP: UserPresence, ..ctap2::get_info::Response::default() } } - -// fn get_or_create_counter_handle(trussed_client: &mut TrussedClient) -> Result { - -// // there should be either 0 or 1 counters with this name. if not, it's a logic error. -// let attributes = Attributes { -// kind: Counter, -// label: Self::GLOBAL_COUNTER_NAME.into(), -// }; - -// // let reply = syscall!(FindObjects, attributes)?; - -// let reply = block!( -// request::FindObjects { attributes } -// .submit(&mut trussed_client) -// // no pending requests -// .map_err(drop) -// .unwrap() -// )?; - -// // how should this API look like. -// match reply.num_objects() { -// // the happy case -// 1 => Ok(reply.object_handles[0]), - -// // first run - create counter -// 0 => { -// let reply = block!( -// request::FindObjects { attributes } -// .submit(&mut trussed_client) -// // no pending requests -// .map_err(drop) -// .unwrap() -// )?; -// Ok(reply::ReadCounter::from(reply).object_handle) -// } - -// // should not occur -// _ => Err(Error::TooManyCounters), -// } -// } - -// fn get_or_create_counter_handle(trussed_client: &mut TrussedClient) -> Result { -// todo!("not implemented yet, follow counter code"); -// } - -// } - -// impl authenticator::Api for Authenticator -// { -// fn get_info(&mut self) -> AuthenticatorInfo { -// todo!(); -// } - -// fn reset(&mut self) -> Result<()> { -// todo!(); -// } - - -// fn get_assertions(&mut self, params: &GetAssertionParameters) -> Result { -// todo!(); -// } - -// fn make_credential(&mut self, params: &MakeCredentialParameters) -> Result { -// todo!(); -// } - } #[cfg(test)] From 21a743124bb48092b56cc876cdcab19d902b151a Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Tue, 22 Feb 2022 15:03:45 +0100 Subject: [PATCH 04/13] Reorganize code distribution in files; add comments + test --- Cargo.toml | 9 +- src/constants.rs | 2 + src/credential.rs | 208 ++- src/ctap1.rs | 273 +++ src/ctap2.rs | 1797 ++++++++++++++++++- src/{ => ctap2}/credential_management.rs | 99 +- src/lib.rs | 2077 ++-------------------- src/state.rs | 50 +- 8 files changed, 2420 insertions(+), 2095 deletions(-) create mode 100644 src/ctap1.rs rename src/{ => ctap2}/credential_management.rs (86%) diff --git a/Cargo.toml b/Cargo.toml index 6a07c44..70bd95e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "fido-authenticator" version = "0.0.0-unreleased" authors = ["Nicolas Stalder "] -edition = "2018" +edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/solokeys/fido-authenticator" documentation = "https://docs.rs/fido-authenticator" @@ -22,9 +22,8 @@ ctap-types = { git = "https://github.com/solokeys/ctap-types" } trussed = { git = "https://github.com/trussed-dev/trussed" } [features] -enable-fido-pre = [] - disable-reset-time-window = [] +enable-fido-pre = [] log-all = [] log-none = [] @@ -32,3 +31,7 @@ log-info = [] log-debug = [] log-warn = [] log-error = [] + +# [dev-dependencies] +# quickcheck = "1" +# rand = "0.8.4" diff --git a/src/constants.rs b/src/constants.rs index 25b09dc..f671a78 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,3 +1,5 @@ +//! Constants. + use trussed::types::{CertId, KeyId}; pub const FIDO2_UP_TIMEOUT: u32 = 30_000; diff --git a/src/credential.rs b/src/credential.rs index ce745ac..fe32406 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -1,3 +1,5 @@ +//! Internal `Credential` and external `CredentialId` ("keyhandle"). + use core::convert::{TryFrom, TryInto}; use trussed::{ @@ -21,24 +23,27 @@ use crate::{ }; +/// As signaled in `get_info`. +/// +/// Eventual goal is full support for the CTAP2.1 specification. #[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)] -// #[derive(Copy, Clone, Debug, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)] pub enum CtapVersion { U2fV2, Fido20, Fido21Pre, } +/// External ID of a credential, commonly known as "keyhandle". #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] pub struct CredentialId(pub Bytes); // TODO: how to determine necessary size? // pub type SerializedCredential = Bytes<512>; // pub type SerializedCredential = Bytes<256>; -pub type SerializedCredential = trussed::types::Message; +pub(crate) type SerializedCredential = trussed::types::Message; #[derive(Clone, Debug)] -pub struct EncryptedSerializedCredential(pub trussed::api::reply::Encrypt); +struct EncryptedSerializedCredential(pub trussed::api::reply::Encrypt); impl TryFrom for CredentialId { type Error = Error; @@ -61,18 +66,23 @@ impl TryFrom for EncryptedSerializedCredential { } } -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +/// Credential keys can either be "discoverable" or not. +/// +/// The FIDO Alliance likes to refer to "resident keys" as "(client-side) discoverable public key +/// credential sources" now ;) +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub enum Key { ResidentKey(KeyId), // THIS USED TO BE 92 NOW IT'S 96 or 97 or so... waddup? WrappedKey(Bytes<128>), } -#[derive(Clone, Debug, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)] +/// The main content of a `Credential`. +#[derive(Clone, Debug, PartialEq, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)] pub struct CredentialData { // id, name, url pub rp: ctap_types::webauthn::PublicKeyCredentialRpEntity, - // id, name, display_name + // id, icon, name, display_name pub user: ctap_types::webauthn::PublicKeyCredentialUserEntity, // can be just a counter, need to be able to determine "latest" @@ -100,14 +110,22 @@ pub struct CredentialData { // TODO: figure out sizes // We may or may not follow https://github.com/satoshilabs/slips/blob/master/slip-0022.md +/// The core structure this authenticator creates and uses. #[derive(Clone, Debug, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)] -// #[serde_indexed(offset = 1)] pub struct Credential { ctap: CtapVersion, pub data: CredentialData, nonce: Bytes<12>, } +// Alas... it would be more symmetrical to have Credential { meta, data }, +// but let's not break binary compatibility for this. +// +// struct Metadata { +// ctap: CtapVersion, +// nonce: Bytes<12>, +// } + impl core::ops::Deref for Credential { type Target = CredentialData; @@ -116,7 +134,7 @@ impl core::ops::Deref for Credential { } } -pub type CredentialList = Vec; +pub(crate) type CredentialList = Vec; impl Into for CredentialId { fn into(self) -> PublicKeyCredentialDescriptor { @@ -272,6 +290,7 @@ impl Credential { // // Called by the `id` method, see its documentation. pub fn strip(&self) -> Self { + info_now!(":: stripping ID"); let mut stripped = self.clone(); let data = &mut stripped.data; @@ -288,3 +307,176 @@ impl Credential { stripped } } + +#[cfg(test)] +mod test { + use super::*; + + fn credential_data() -> CredentialData { + use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; + + let credential_data = CredentialData { + rp: PublicKeyCredentialRpEntity { + id: String::from("John Doe"), + name: None, + url: None, + }, + user: PublicKeyCredentialUserEntity { + id: Bytes::from_slice(&[1,2,3]).unwrap(), + icon: None, + name: None, + display_name: None, + }, + creation_time: 123, + use_counter: false, + algorithm: -7, + key: Key::WrappedKey(Bytes::from_slice(&[1,2,3]).unwrap()), + hmac_secret: Some(false), + cred_protect: None, + }; + credential_data + } + + fn random_bytes() -> Bytes { + use rand::{RngCore, distributions::{Distribution, Uniform}, rngs::OsRng}; + let mut bytes = Bytes::default(); + + let between = Uniform::from(0..(N + 1)); + let n = between.sample(&mut OsRng); + + bytes.resize_default(n).unwrap(); + + OsRng.fill_bytes(&mut bytes); + bytes + } + + #[allow(dead_code)] + fn maybe_random_bytes() -> Option> { + use rand::{RngCore, rngs::OsRng}; + if OsRng.next_u32() & 1 != 0 { + Some(random_bytes()) + } else { + None + } + } + + fn random_string() -> String { + use std::str::FromStr; + use rand::{Rng, distributions::{Alphanumeric, Distribution, Uniform}, rngs::OsRng}; + + let between = Uniform::from(0..(N + 1)); + let n = between.sample(&mut OsRng); + + let std_string: std::string::String = OsRng.sample_iter(&Alphanumeric).take(n).map(char::from).collect(); + String::from_str(&std_string).unwrap() + } + + fn maybe_random_string() -> Option> { + use rand::{RngCore, rngs::OsRng}; + if OsRng.next_u32() & 1 != 0 { + Some(random_string()) + } else { + None + } + } + + fn random_credential_data() -> CredentialData { + use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; + + let credential_data = CredentialData { + rp: PublicKeyCredentialRpEntity { + id: random_string(), + name: maybe_random_string(), + url: maybe_random_string(), + }, + user: PublicKeyCredentialUserEntity { + id: random_bytes(),//Bytes::from_slice(&[1,2,3]).unwrap(), + icon: maybe_random_string(), + name: maybe_random_string(), + display_name: maybe_random_string(), + }, + creation_time: 123, + use_counter: false, + algorithm: -7, + key: Key::WrappedKey(random_bytes()), + hmac_secret: Some(false), + cred_protect: None, + }; + credential_data + } + + #[test] + fn skip_credential_data_options() { + use trussed::{cbor_deserialize as deserialize, cbor_serialize_bytes as serialize}; + + let credential_data = credential_data(); + let serialization: Bytes<1024> = serialize(&credential_data).unwrap(); + let deserialized: CredentialData = deserialize(&serialization).unwrap(); + + assert_eq!(credential_data, deserialized); + + let credential_data = random_credential_data(); + let serialization: Bytes<1024> = serialize(&credential_data).unwrap(); + let deserialized: CredentialData = deserialize(&serialization).unwrap(); + + assert_eq!(credential_data, deserialized); + } + + // use quickcheck::TestResult; + // quickcheck::quickcheck! { + // fn prop( + // rp_id: std::string::String, + // rp_name: Option, + // rp_url: Option, + // user_id: std::vec::Vec, + // user_name: Option, + // creation_time: u32, + // use_counter: bool, + // algorithm: i32 + // ) -> TestResult { + // use std::str::FromStr; + // use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; + // use trussed::{cbor_deserialize as deserialize, cbor_serialize_bytes as serialize}; + + // let rp_name = &rp_name.as_ref().map(|string| string.as_str()); + // let rp_url = &rp_url.as_ref().map(|string| string.as_str()); + // let user_name = &user_name.as_ref().map(|string| string.as_str()); + // let discard = [ + // rp_id.len() > 256, + // rp_name.unwrap_or(&"").len() > 64, + // rp_url.unwrap_or(&"").len() > 64, + // user_id.len() > 64, + // user_name.unwrap_or(&"").len() > 64, + + // ]; + // if discard.iter().any(|&x| x) { + // return TestResult::discard(); + // } + + // let credential_data = CredentialData { + // rp: PublicKeyCredentialRpEntity { + // id: String::from_str(&rp_id).unwrap(), + // name: rp_name.map(|rp_name| String::from_str(rp_name).unwrap()), + // url: rp_url.map(|rp_url| String::from_str(rp_url).unwrap()), + // }, + // user: PublicKeyCredentialUserEntity { + // id: Bytes::from_slice(&user_id).unwrap(), + // icon: maybe_random_string(), + // name: user_name.map(|user_name| String::from_str(user_name).unwrap()), + // display_name: maybe_random_string(), + // }, + // creation_time, + // use_counter, + // algorithm, + // key: Key::WrappedKey(random_bytes()), + // hmac_secret: Some(false), + // cred_protect: None, + // }; + + // let serialization: Bytes<1024> = serialize(&credential_data).unwrap(); + // let deserialized: CredentialData = deserialize(&serialization).unwrap(); + + // TestResult::from_bool(credential_data == deserialized) + // } + // } +} diff --git a/src/ctap1.rs b/src/ctap1.rs new file mode 100644 index 0000000..bd21afb --- /dev/null +++ b/src/ctap1.rs @@ -0,0 +1,273 @@ +//! The `ctap1::Authenticator` trait and its implementation. + +use ctap_types::{ + Bytes, + ctap1::{ + self, + Command as Request, + Response, + Result, + Error, + }, +}; + +use trussed::{ + syscall, + types::{ + KeySerialization, + Mechanism, + SignatureSerialization, + Location, + }, +}; + +use crate::{ + credential::{ + self, + Credential, + Key, + }, + constants, + SigningAlgorithm, + TrussedRequirements, + UserPresence, +}; + +/// CTAP1 (U2F) authenticator API +/// +/// Ahh... life could be so simple! +// +// TODO: Lift into ctap-types? +pub trait Authenticator { + /// Register a U2F credential. + fn register(&mut self, request: &ctap1::Register) -> Result; + /// Authenticate with a U2F credential. + fn authenticate(&mut self, request: &ctap1::Authenticate) -> Result; + /// Supported U2F version. + fn version() -> [u8; 6] { + *b"U2F_V2" + } +} + +impl crate::Authenticator +where UP: UserPresence, + T: TrussedRequirements, +{ + /// Dispatches the enum of possible requests into the ctap1 [`Authenticator`] trait methods. + pub fn call_ctap1(&mut self, request: &Request) -> Result { + info!("called u2f"); + self.state.persistent.load_if_not_initialised(&mut self.trussed); + + match request { + Request::Register(reg) => + Ok(Response::Register(self.register(reg)?)), + + Request::Authenticate(auth) => + Ok(Response::Authenticate(self.authenticate(auth)?)), + + Request::Version => + Ok(ctap1::Response::Version(Self::version())), + + } + } + + // #[deprecated(note="please use `call_ctap1` instead")] + /// Alias of `call_ctap1`, may be deprecated in the future. + pub fn call_u2f(&mut self, request: &Request) -> Result { + self.call_ctap1(request) + } + +} + +type Commitment = Bytes::<324>; + +/// Implement `ctap1::Authenticator` for our Authenticator. +impl Authenticator for crate::Authenticator +{ + fn register(&mut self, reg: &ctap1::Register) -> Result { + self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) + .map_err(|_| Error::ConditionsOfUseNotSatisfied)?; + + // Generate a new P256 key pair. + let private_key = syscall!(self.trussed.generate_p256_private_key(Location::Volatile)).key; + let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; + + let serialized_cose_public_key = syscall!(self.trussed.serialize_p256_key( + public_key, KeySerialization::EcdhEsHkdf256 + )).serialized_key; + let cose_key: ctap_types::cose::EcdhEsHkdf256PublicKey + = trussed::cbor_deserialize(&serialized_cose_public_key).unwrap(); + + let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed) + .map_err(|_| Error::UnspecifiedCheckingError)?; + debug!("wrapping u2f private key"); + let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( + wrapping_key, + private_key, + ®.app_id, + )).wrapped_key; + // debug!("wrapped_key = {:?}", &wrapped_key); + + let key = Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::UnspecifiedCheckingError)?); + let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap(); + + let mut rp_id = heapless::String::new(); + + // We do not know the rpId string in U2F. Just using placeholder. + rp_id.push_str("u2f").ok(); + let rp = ctap_types::webauthn::PublicKeyCredentialRpEntity{ + id: rp_id, + name: None, + url: None, + }; + + let user = ctap_types::webauthn::PublicKeyCredentialUserEntity { + id: Bytes::from_slice(&[0u8; 8]).unwrap(), + icon: None, + name: None, + display_name: None, + }; + + let credential = Credential::new( + credential::CtapVersion::U2fV2, + &rp, + &user, + + SigningAlgorithm::P256 as i32, + key, + self.state.persistent.timestamp(&mut self.trussed).map_err(|_| Error::NotEnoughMemory)?, + None, + None, + nonce, + ); + + // info!("made credential {:?}", &credential); + + // 12.b generate credential ID { = AEAD(Serialize(Credential)) } + let kek = self.state.persistent.key_encryption_key(&mut self.trussed).map_err(|_| Error::NotEnoughMemory)?; + let credential_id = credential.id(&mut self.trussed, kek, Some(®.app_id)).map_err(|_| Error::NotEnoughMemory)?; + syscall!(self.trussed.delete(public_key)); + syscall!(self.trussed.delete(private_key)); + + let mut commitment = Commitment::new(); + + commitment.push(0).unwrap(); // reserve byte + commitment.extend_from_slice(®.app_id).unwrap(); + commitment.extend_from_slice(®.challenge).unwrap(); + + commitment.extend_from_slice(&credential_id.0).unwrap(); + + commitment.push(0x04).unwrap(); // public key uncompressed byte + commitment.extend_from_slice(&cose_key.x).unwrap(); + commitment.extend_from_slice(&cose_key.y).unwrap(); + + let attestation = self.state.identity.attestation(&mut self.trussed); + + let (signature, cert) = match attestation { + (Some((key, cert)), _aaguid) => { + info!("aaguid: {}", hex_str!(&_aaguid)); + ( + syscall!( + self.trussed.sign(Mechanism::P256, + key, + &commitment, + SignatureSerialization::Asn1Der + )).signature.to_bytes().unwrap(), + cert + ) + }, + _ => { + info!("Not provisioned with attestation key!"); + return Err(Error::KeyReferenceNotFound); + } + }; + + + Ok(ctap1::RegisterResponse::new( + 0x05, + &cose_key, + &credential_id.0, + signature, + &cert, + )) + } + + fn authenticate(&mut self, auth: &ctap1::Authenticate) -> Result { + let cred = Credential::try_from_bytes(self, &auth.app_id, &auth.key_handle); + + let user_presence_byte = match auth.control_byte { + ctap1::ControlByte::CheckOnly => { + // if the control byte is set to 0x07 by the FIDO Client, + // the U2F token is supposed to simply check whether the + // provided key handle was originally created by this token + return if cred.is_ok() { + Err(Error::ConditionsOfUseNotSatisfied) + } else { + Err(Error::IncorrectDataParameter) + }; + }, + ctap1::ControlByte::EnforceUserPresenceAndSign => { + self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) + .map_err(|_| Error::ConditionsOfUseNotSatisfied)?; + 0x01 + }, + ctap1::ControlByte::DontEnforceUserPresenceAndSign => 0x00, + }; + + let cred = cred.map_err(|_| Error::IncorrectDataParameter)?; + + let key = match &cred.key { + Key::WrappedKey(bytes) => { + 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, + b"", + Location::Volatile, + )).key; + match key_result { + Some(key) => { + info!("loaded u2f key!"); + key + } + None => { + info!("issue with unwrapping credential id key"); + return Err(Error::IncorrectDataParameter); + } + } + } + _ => return Err(Error::IncorrectDataParameter), + }; + + if cred.algorithm != -7 { + info!("Unexpected mechanism for u2f"); + return Err(Error::IncorrectDataParameter); + } + + let sig_count = self.state.persistent.timestamp(&mut self.trussed). + map_err(|_| Error::UnspecifiedNonpersistentExecutionError)?; + + let mut commitment = Commitment::new(); + + commitment.extend_from_slice(&auth.app_id).unwrap(); + commitment.push(user_presence_byte).unwrap(); + commitment.extend_from_slice(&sig_count.to_be_bytes()).unwrap(); + commitment.extend_from_slice(&auth.challenge).unwrap(); + + let signature = syscall!( + self.trussed.sign(Mechanism::P256, + key, + &commitment, + SignatureSerialization::Asn1Der + )).signature.to_bytes().unwrap(); + + Ok(ctap1::AuthenticateResponse::new( + user_presence_byte, + sig_count, + signature, + )) + } + +} + diff --git a/src/ctap2.rs b/src/ctap2.rs index 634048c..c8c5623 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,85 +1,1778 @@ -use cortex_m_semihosting::hprintln; +//! The `ctap2::Authenticator` trait and its implementation. -use littlefs2::{ - driver::Storage, -}; - -use usbd_ctaphid::{ +use ctap_types::{ + Bytes, Bytes32, String, Vec, authenticator::{ - self, + ctap2::{ + Request, + Response, + }, Error, - Result, - }, - constants::{ - MESSAGE_SIZE, }, + ctap2::self, + operation::VendorOperation, + sizes, +}; + +use littlefs2::path::Path; + +use trussed::{ + syscall, try_syscall, types::{ - Bytes, - CtapOptions, - consts, - String, - Vec, - AssertionResponses, - AttestationObject, - AuthenticatorInfo, - GetAssertionParameters, - MakeCredentialParameters, + KeyId, + KeySerialization, + Mechanism, + MediumData, + Message, + SignatureSerialization, + Location, + PathBuf, }, }; -use crate::constants::AAGUID; +use crate::{ + credential::{ + self, + Credential, + CredentialList, + Key, + }, + constants, + format_hex, + state::{ + self, + MinCredentialHeap, + TimestampPath, + }, + Result, -impl<'fs, 'storage, R, S> authenticator::Api for crate::Authenticator<'fs, 'storage, R, S> -where - R: embedded_hal::blocking::rng::Read, - S: Storage, + SigningAlgorithm, + UserPresence, + TrussedRequirements, +}; + +pub mod credential_management; + +/// CTAP2 authenticator API +// +// TODO: Lift into ctap-types? +pub trait Authenticator { + fn get_info(&mut self) -> ctap2::get_info::Response; + fn make_credential(&mut self, request: &ctap2::make_credential::Parameters) -> Result; + fn get_assertion(&mut self, request: &ctap2::get_assertion::Parameters) -> Result; + fn get_next_assertion(&mut self) -> Result; + fn reset(&mut self) -> Result<()>; + fn client_pin(&mut self, request: &ctap2::client_pin::Parameters) -> Result; + fn credential_management(&mut self, request: &ctap2::credential_management::Parameters) -> Result; + fn vendor(&mut self, op: VendorOperation) -> Result<()>; +} + +impl crate::Authenticator +where UP: UserPresence, + T: TrussedRequirements, { - fn get_info(&mut self) -> AuthenticatorInfo { + /// Dispatches the enum of possible requests into the ctap2 [`Authenticator`] trait methods. + pub fn call_ctap2(&mut self, request: &Request) -> Result { + info!("called u2f"); + self.state.persistent.load_if_not_initialised(&mut self.trussed); + + // match request { + // Command::Register(reg) => + // Ok(Response::Register(self.register(reg)?)), + + // Command::Authenticate(auth) => + // Ok(Response::Authenticate(self.authenticate(auth)?)), + + // Command::Version => + // Ok(ctap1::Response::Version(Self::version())), + + // } + match request { + // 0x4 + Request::GetInfo => { + debug!("GI"); + Ok(Response::GetInfo(self.get_info())) + } + + // 0x2 + Request::MakeCredential(parameters) => { + debug!("MC request"); + Ok(Response::MakeCredential(self.make_credential(¶meters)?)) + } + + // 0x1 + Request::GetAssertion(parameters) => { + debug!("GA request"); + let response = self.get_assertion(¶meters); + match response { + Ok(response) => Ok(Response::GetAssertion(response)), + Err(error) => { + info_now!("GA request failed, error {:?}", &error); + Err(error) + } + } + } + + // 0x8 + Request::GetNextAssertion => { + debug!("GNA request"); + let response = self.get_next_assertion(); + match response { + Ok(response) => Ok(Response::GetNextAssertion(response)), + Err(error) => Err(error) + } + } + + // 0x7 + Request::Reset => { + debug!("Reset request"); + let response = self.reset(); + match response { + Ok(()) => Ok(Response::Reset), + Err(error) => Err(error) + } + } + + + // 0x6 + Request::ClientPin(parameters) => { + debug!("CP request"); + Ok(Response::ClientPin(self.client_pin(¶meters)?)) + } + + // 0xA + Request::CredentialManagement(parameters) => { + debug!("CM request"); + Ok(Response::CredentialManagement(self.credential_management(¶meters)?)) + } + + + Request::Vendor(op) => { + debug!("Vendor request"); + self.vendor(*op)?; + Ok(Response::Vendor) + } + } + } +} + +/// Implement `ctap2::Authenticator` for our Authenticator. +impl Authenticator for crate::Authenticator { + fn get_info(&mut self) -> ctap2::get_info::Response { + use core::str::FromStr; - let mut versions = Vec::, consts::U3>::new(); + let mut versions = Vec::, 4>::new(); versions.push(String::from_str("U2F_V2").unwrap()).unwrap(); versions.push(String::from_str("FIDO_2_0").unwrap()).unwrap(); - versions.push(String::from_str("FIDO_2_1_PRE").unwrap()).unwrap(); + // #[cfg(feature = "enable-fido-pre")] + // versions.push(String::from_str("FIDO_2_1_PRE").unwrap()).unwrap(); - let mut extensions = Vec::, consts::U4>::new(); + let mut extensions = Vec::, 4>::new(); + // extensions.push(String::from_str("credProtect").unwrap()).unwrap(); extensions.push(String::from_str("credProtect").unwrap()).unwrap(); extensions.push(String::from_str("hmac-secret").unwrap()).unwrap(); - let options = CtapOptions { - plat: false, - rk: true, - client_pin: Some(self.client_pin().is_some()), - up: true, - uv: None, // cannot perform UV within ourselves (e.g. biometrics) - cred_protect: Some(true), - }; + let mut pin_protocols = Vec::::new(); + pin_protocols.push(1).unwrap(); - hprintln!("options = {:?}", &options).ok(); + let mut options = ctap2::get_info::CtapOptions::default(); + options.rk = true; + options.up = true; + options.uv = None; // "uv" here refers to "in itself", e.g. biometric + // options.plat = false; + options.cred_mgmt = Some(true); + // options.client_pin = None; // not capable of PIN + options.client_pin = match self.state.persistent.pin_is_set() { + true => Some(true), + false => Some(false), + }; - self.authnr_channels.send.enqueue( - crate::AuthnrToOsMessages::Heya(String::from_str("GET_INFO").unwrap())); - crate::pac::NVIC::pend(crate::pac::Interrupt::OS_EVENT); + let (_, aaguid)= self.state.identity.attestation(&mut self.trussed); - AuthenticatorInfo { + ctap2::get_info::Response { versions, extensions: Some(extensions), - aaguid: Bytes::from_slice(AAGUID).unwrap(), + aaguid: Bytes::from_slice(&aaguid).unwrap(), options: Some(options), - max_msg_size: Some(MESSAGE_SIZE), - ..AuthenticatorInfo::default() + max_msg_size: Some(ctap_types::sizes::MESSAGE_SIZE), + pin_protocols: Some(pin_protocols), + max_creds_in_list: Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST), + max_cred_id_length: Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH), + ..ctap2::get_info::Response::default() + } + } + + fn get_assertion(&mut self, parameters: &ctap2::get_assertion::Parameters) -> Result { + + let rp_id_hash = self.hash(¶meters.rp_id.as_ref()); + + // 1-4. + let uv_performed = match self.pin_prechecks( + ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, + ¶meters.client_data_hash.as_ref(), + ) { + Ok(b) => b, + Err(Error::PinRequired) => { + // UV is optional for get_assertion + false + } + Err(err) => return Err(err), + }; + + // 5. Locate eligible credentials + // + // Note: If allowList is passed, credential is Some(credential) + // If no allowList is passed, credential is None and the retrieved credentials + // are stored in state.runtime.credential_heap + self.locate_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed)?; + + let credential = self.state.runtime.pop_credential_from_heap(&mut self.trussed); + let num_credentials = match self.state.runtime.credential_heap().len() { + 0 => None, + n => Some(n as u32 + 1), + }; + info!("FIRST cred: {:?}",&credential); + info!("FIRST NUM creds: {:?}",num_credentials); + + // NB: misleading, if we have "1" we return "None" + let human_num_credentials = match num_credentials { + Some(n) => n, + None => 1, + }; + info!("found {:?} applicable credentials", human_num_credentials); + + // 6. process any options present + + // UP occurs by default, but option could specify not to. + let do_up = if parameters.options.is_some() { + parameters.options.as_ref().unwrap().up.unwrap_or(true) + } else { + true + }; + + // 7. collect user presence + let up_performed = if do_up { + info!("asking for up"); + self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + true + } else { + info!("not asking for up"); + false + }; + + let multiple_credentials = human_num_credentials > 1; + self.state.runtime.active_get_assertion = Some(state::ActiveGetAssertionData { + rp_id_hash: { + let mut buf = [0u8; 32]; + buf.copy_from_slice(&rp_id_hash); + buf + }, + client_data_hash: { + let mut buf = [0u8; 32]; + buf.copy_from_slice(¶meters.client_data_hash); + buf + }, + uv_performed, + up_performed, + multiple_credentials, + extensions: parameters.extensions.clone(), + }); + + self.assert_with_credential(num_credentials, credential) + } + + fn get_next_assertion(&mut self) -> Result { + // 1./2. don't remember / don't have left any credentials + if self.state.runtime.credential_heap().is_empty() { + return Err(Error::NotAllowed); + } + + // 3. previous GA/GNA >30s ago -> discard stat + // this is optional over NFC + if false { + self.state.runtime.free_credential_heap(&mut self.trussed); + return Err(Error::NotAllowed); } + + // 4. select credential + // let data = syscall!(self.trussed.read_file( + // timestamp_hash.location, + // timestamp_hash.path, + // )).data; + let credential = self.state.runtime.pop_credential_from_heap(&mut self.trussed); + // Credential::deserialize(&data).unwrap(); + + // 5. suppress PII if no UV was performed in original GA + + // 6. sign + // 7. reset timer + // 8. increment credential counter (not applicable) + + self.assert_with_credential(None, credential) + } + + fn make_credential(&mut self, parameters: &ctap2::make_credential::Parameters) -> Result { + + let rp_id_hash = self.hash(¶meters.rp.id.as_ref()); + + // 1-4. + if let Some(options) = parameters.options.as_ref() { + // up option is not valid for make_credential + if options.up.is_some() { + return Err(Error::InvalidOption); + } + } + let uv_performed = self.pin_prechecks( + ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, + ¶meters.client_data_hash.as_ref(), + )?; + + // 5. "persist credProtect value for this credential" + // --> seems out of place here, see 9. + + // 6. excludeList present, contains credential ID on this authenticator bound to RP? + // --> wait for UP, error CredentialExcluded + if let Some(exclude_list) = ¶meters.exclude_list { + for descriptor in exclude_list.iter() { + let result = Credential::try_from(self, &rp_id_hash, descriptor); + if let Ok(excluded_cred) = result { + use credential::CredentialProtectionPolicy; + // If UV is not performed, than CredProtectRequired credentials should not be visibile. + if !(excluded_cred.cred_protect == Some(CredentialProtectionPolicy::Required) && !uv_performed) { + info!("Excluded!"); + self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + return Err(Error::CredentialExcluded); + } + } + } + } + + // 7. check pubKeyCredParams algorithm is valid + supported COSE identifier + + let mut algorithm: Option = None; + for param in parameters.pub_key_cred_params.iter() { + match param.alg { + -7 => { if algorithm.is_none() { algorithm = Some(SigningAlgorithm::P256); }} + -8 => { algorithm = Some(SigningAlgorithm::Ed25519); } + // -9 => { algorithm = Some(SigningAlgorithm::Totp); } + _ => {} + } + } + let algorithm = match algorithm { + Some(algorithm) => { + info!("algo: {:?}", algorithm as i32); + algorithm + }, + None => { return Err(Error::UnsupportedAlgorithm); } + }; + // debug!("making credential, eddsa = {}", eddsa); + + + // 8. process options; on known but unsupported error UnsupportedOption + + let mut rk_requested = false; + // TODO: why is this unused? + let mut _uv_requested = false; + let _up_requested = true; // can't be toggled + + info!("MC options: {:?}", ¶meters.options); + if let Some(ref options) = ¶meters.options { + if Some(true) == options.rk { + rk_requested = true; + } + if Some(true) == options.uv { + _uv_requested = true; + } + } + + // 9. process extensions + let mut hmac_secret_requested = None; + // let mut cred_protect_requested = CredentialProtectionPolicy::Optional; + let mut cred_protect_requested = None; + if let Some(extensions) = ¶meters.extensions { + + hmac_secret_requested = extensions.hmac_secret; + + if let Some(policy) = &extensions.cred_protect { + cred_protect_requested = Some(credential::CredentialProtectionPolicy::try_from(*policy)?); + } + } + + // debug!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); + + // 10. get UP, if denied error OperationDenied + self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + + // 11. generate credential keypair + let location = match rk_requested { + true => Location::Internal, + false => Location::Volatile, + }; + + let private_key: KeyId; + let public_key: KeyId; + let cose_public_key; + match algorithm { + SigningAlgorithm::P256 => { + private_key = syscall!(self.trussed.generate_p256_private_key(location)).key; + public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; + cose_public_key = syscall!(self.trussed.serialize_key( + Mechanism::P256, public_key.clone(), KeySerialization::Cose + )).serialized_key; + let _success = syscall!(self.trussed.delete(public_key)).success; + info!("deleted public P256 key: {}", _success); + } + SigningAlgorithm::Ed25519 => { + private_key = syscall!(self.trussed.generate_ed255_private_key(location)).key; + public_key = syscall!(self.trussed.derive_ed255_public_key(private_key, Location::Volatile)).key; + cose_public_key = syscall!(self.trussed.serialize_key( + Mechanism::Ed255, public_key.clone(), KeySerialization::Cose + )).serialized_key; + let _success = syscall!(self.trussed.delete(public_key)).success; + info!("deleted public Ed25519 key: {}", _success); + } + // SigningAlgorithm::Totp => { + // if parameters.client_data_hash.len() != 32 { + // return Err(Error::InvalidParameter); + // } + // // b'TOTP---W\x0e\xf1\xe0\xd7\x83\xfe\t\xd1\xc1U\xbf\x08T_\x07v\xb2\xc6--TOTP' + // let totp_secret: [u8; 20] = parameters.client_data_hash[6..26].try_into().unwrap(); + // private_key = syscall!(self.trussed.unsafe_inject_shared_key( + // &totp_secret, Location::Internal)).key; + // // info!("totes injected"); + // let fake_cose_pk = ctap_types::cose::TotpPublicKey {}; + // let fake_serialized_cose_pk = trussed::cbor_serialize_bytes(&fake_cose_pk) + // .map_err(|_| Error::NotAllowed)?; + // cose_public_key = fake_serialized_cose_pk; // Bytes::from_slice(&[0u8; 20]).unwrap(); + // } + } + + // 12. if `rk` is set, store or overwrite key pair, if full error KeyStoreFull + + // 12.a generate credential + let key_parameter = match rk_requested { + true => Key::ResidentKey(private_key), + false => { + // WrappedKey version + let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; + debug!("wrapping private key"); + let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( + wrapping_key, + private_key, + &rp_id_hash, + )).wrapped_key; + // debug!("wrapped_key = {:?}", &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(); + let ret = Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::Other)?); + ret + // debug!("len wrapped key = {}", wrapped_key.len()); + // Key::WrappedKey(wrapped_key.to_bytes().unwrap()) + + } + }; + + // injecting this is a bit mehhh.. + let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap(); + info!("nonce = {:?}", &nonce); + + // 12.b generate credential ID { = AEAD(Serialize(Credential)) } + let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; + + // store it. + // TODO: overwrite, error handling with KeyStoreFull + + let credential = Credential::new( + credential::CtapVersion::Fido21Pre, + ¶meters.rp, + ¶meters.user, + algorithm as i32, + key_parameter, + self.state.persistent.timestamp(&mut self.trussed)?, + hmac_secret_requested.clone(), + cred_protect_requested, + nonce, + ); + + let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; + + if rk_requested { + // serialization with all metadata + let serialized_credential = credential.serialize()?; + + // first delete any other RK cred with same RP + UserId if there is one. + self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id).ok(); + + let credential_id_hash = self.hash(&credential_id.0.as_ref()); + try_syscall!(self.trussed.write_file( + Location::Internal, + rk_path(&rp_id_hash, &credential_id_hash), + serialized_credential.clone(), + // user attribute for later easy lookup + // Some(rp_id_hash.clone()), + None, + )).map_err(|_| Error::KeyStoreFull)?; + } + + // 13. generate and return attestation statement using clientDataHash + + // 13.a AuthenticatorData and its serialization + use ctap2::AuthenticatorDataFlags as Flags; + info!("MC created cred id"); + + let (attestation_maybe, aaguid) = self.state.identity.attestation(&mut self.trussed); + + let authenticator_data = ctap2::make_credential::AuthenticatorData { + rp_id_hash: rp_id_hash.to_bytes().map_err(|_| Error::Other)?, + + flags: { + let mut flags = Flags::USER_PRESENCE; + if uv_performed { + flags |= Flags::USER_VERIFIED; + } + if true { + flags |= Flags::ATTESTED_CREDENTIAL_DATA; + } + if hmac_secret_requested.is_some() || cred_protect_requested.is_some() { + flags |= Flags::EXTENSION_DATA; + } + flags + }, + + sign_count: self.state.persistent.timestamp(&mut self.trussed)?, + + attested_credential_data: { + // debug!("acd in, cid len {}, pk len {}", credential_id.0.len(), cose_public_key.len()); + let attested_credential_data = ctap2::make_credential::AttestedCredentialData { + aaguid: Bytes::from_slice(&aaguid).unwrap(), + credential_id: credential_id.0.to_bytes().unwrap(), + credential_public_key: cose_public_key.to_bytes().unwrap(), + }; + // debug!("cose PK = {:?}", &attested_credential_data.credential_public_key); + Some(attested_credential_data) + }, + + extensions: { + if hmac_secret_requested.is_some() || cred_protect_requested.is_some() { + Some(ctap2::make_credential::Extensions { + cred_protect: parameters.extensions.as_ref().unwrap().cred_protect.clone(), + hmac_secret: parameters.extensions.as_ref().unwrap().hmac_secret.clone(), + }) + + } else { + None + } + }, + }; + // debug!("authData = {:?}", &authenticator_data); + + let serialized_auth_data = authenticator_data.serialize(); + + // 13.b The Signature + + // can we write Sum somehow? + // debug!("seeking commitment, {} + {}", serialized_auth_data.len(), parameters.client_data_hash.len()); + let mut commitment = Bytes::<1024>::new(); + commitment.extend_from_slice(&serialized_auth_data).map_err(|_| Error::Other)?; + // debug!("serialized_auth_data ={:?}", &serialized_auth_data); + commitment.extend_from_slice(¶meters.client_data_hash).map_err(|_| Error::Other)?; + // debug!("client_data_hash = {:?}", ¶meters.client_data_hash); + // debug!("commitment = {:?}", &commitment); + + // NB: the other/normal one is called "basic" or "batch" attestation, + // because it attests the authenticator is part of a batch: the model + // specified by AAGUID. + // "self signed" is also called "surrogate basic". + // + // we should also directly support "none" format, it's a bit weird + // how browsers firefox this + + let (signature, attestation_algorithm) = { + if attestation_maybe.is_none() { + match algorithm { + SigningAlgorithm::Ed25519 => { + let signature = syscall!(self.trussed.sign_ed255(private_key, &commitment)).signature; + (signature.to_bytes().map_err(|_| Error::Other)?, -8) + } + + SigningAlgorithm::P256 => { + // DO NOT prehash here, `trussed` does that + let der_signature = syscall!(self.trussed.sign_p256(private_key, &commitment, SignatureSerialization::Asn1Der)).signature; + (der_signature.to_bytes().map_err(|_| Error::Other)?, -7) + } + // SigningAlgorithm::Totp => { + // // maybe we can fake it here too, but seems kinda weird + // // return Err(Error::UnsupportedAlgorithm); + // // micro-ecc is borked. let's self-sign anyway + // let hash = syscall!(self.trussed.hash_sha256(&commitment.as_ref())).hash; + // let tmp_key = syscall!(self.trussed + // .generate_p256_private_key(Location::Volatile)) + // .key; + + // let signature = syscall!(self.trussed.sign_p256( + // tmp_key, + // &hash, + // SignatureSerialization::Asn1Der, + // )).signature; + // (signature.to_bytes().map_err(|_| Error::Other)?, -7) + // } + } + } else { + + let signature = syscall!(self.trussed.sign_p256( + attestation_maybe.as_ref().unwrap().0, + &commitment, + SignatureSerialization::Asn1Der, + )).signature; + (signature.to_bytes().map_err(|_| Error::Other)?, -7) + } + }; + // debug!("SIG = {:?}", &signature); + + if !rk_requested { + let _success = syscall!(self.trussed.delete(private_key)).success; + info!("deleted private credential key: {}", _success); + } + + let packed_attn_stmt = ctap2::make_credential::PackedAttestationStatement { + alg: attestation_algorithm, + sig: signature, + x5c: match attestation_maybe.is_some() { + false => None, + true => { + // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements + let cert = attestation_maybe.as_ref().unwrap().1.clone(); + let mut x5c = Vec::new(); + x5c.push(cert).ok(); + Some(x5c) + } + }, + }; + + let fmt = String::<32>::from("packed"); + let att_stmt = ctap2::make_credential::AttestationStatement::Packed(packed_attn_stmt); + + let attestation_object = ctap2::make_credential::Response { + fmt, + auth_data: serialized_auth_data, + att_stmt, + }; + + Ok(attestation_object) } fn reset(&mut self) -> Result<()> { - self.reset_master_secret().expect("reset master secret failed, oh noe"); - todo!("reset"); + // 1. >10s after bootup -> NotAllowed + let uptime = syscall!(self.trussed.uptime()).uptime; + if uptime.as_secs() > 10 { + #[cfg(not(feature = "disable-reset-time-window"))] + return Err(Error::NotAllowed); + } + // 2. check for user presence + // denied -> OperationDenied + // timeout -> UserActionTimeout + self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + + // Delete resident keys + syscall!(self.trussed.delete_all(Location::Internal)); + syscall!(self.trussed.remove_dir_all( + Location::Internal, + PathBuf::from("rk"), + )); + + // b. delete persistent state + self.state.persistent.reset(&mut self.trussed)?; + + // c. Reset runtime state + self.state.runtime.reset(&mut self.trussed); + + Ok(()) + } + + fn client_pin(&mut self, parameters: &ctap2::client_pin::Parameters) -> Result { + use ctap2::client_pin::PinV1Subcommand as Subcommand; + debug!("processing CP"); + // info!("{:?}", parameters); + + if parameters.pin_protocol != 1{ + return Err(Error::InvalidParameter); + } + + Ok(match parameters.sub_command { + + Subcommand::GetRetries => { + debug!("processing CP.GR"); + + ctap2::client_pin::Response { + key_agreement: None, + pin_token: None, + retries: Some(self.state.persistent.retries()), + } + } + + Subcommand::GetKeyAgreement => { + debug!("processing CP.GKA"); + + let private_key = self.state.runtime.key_agreement_key(&mut self.trussed); + let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; + let serialized_cose_key = syscall!(self.trussed.serialize_key( + Mechanism::P256, public_key.clone(), KeySerialization::EcdhEsHkdf256)).serialized_key; + let cose_key = trussed::cbor_deserialize(&serialized_cose_key).unwrap(); + + syscall!(self.trussed.delete(public_key)); + + ctap2::client_pin::Response { + key_agreement: cose_key, + pin_token: None, + retries: None, + } + } + + Subcommand::SetPin => { + debug!("processing CP.SP"); + // 1. check mandatory parameters + let platform_kek = match parameters.key_agreement.as_ref() { + Some(key) => key, + None => { return Err(Error::MissingParameter); } + }; + let new_pin_enc = match parameters.new_pin_enc.as_ref() { + Some(pin) => pin, + None => { return Err(Error::MissingParameter); } + }; + let pin_auth = match parameters.pin_auth.as_ref() { + Some(auth) => auth, + None => { return Err(Error::MissingParameter); } + }; + + // 2. is pin already set + if self.state.persistent.pin_is_set() { + return Err(Error::NotAllowed); + } + + // 3. generate shared secret + let shared_secret = self.state.runtime.generate_shared_secret(&mut self.trussed, platform_kek)?; + + // TODO: there are moar early returns!! + // - implement Drop? + // - do garbage collection outside of this? + + // 4. verify pinAuth + self.verify_pin_auth(shared_secret, new_pin_enc, pin_auth)?; + + // 5. decrypt and verify new PIN + let new_pin = self.decrypt_pin_check_length(shared_secret, new_pin_enc)?; + + syscall!(self.trussed.delete(shared_secret)); + + // 6. store LEFT(SHA-256(newPin), 16), set retries to 8 + self.hash_store_pin(&new_pin)?; + self.state.reset_retries(&mut self.trussed).map_err(|_| Error::Other)?; + + ctap2::client_pin::Response { + key_agreement: None, + pin_token: None, + retries: None, + } + } + + Subcommand::ChangePin => { + debug!("processing CP.CP"); + + // 1. check mandatory parameters + let platform_kek = match parameters.key_agreement.as_ref() { + Some(key) => key, + None => { return Err(Error::MissingParameter); } + }; + let pin_hash_enc = match parameters.pin_hash_enc.as_ref() { + Some(hash) => hash, + None => { return Err(Error::MissingParameter); } + }; + let new_pin_enc = match parameters.new_pin_enc.as_ref() { + Some(pin) => pin, + None => { return Err(Error::MissingParameter); } + }; + let pin_auth = match parameters.pin_auth.as_ref() { + Some(auth) => auth, + None => { return Err(Error::MissingParameter); } + }; + + // 2. fail if no retries left + self.state.pin_blocked()?; + + // 3. generate shared secret + let shared_secret = self.state.runtime.generate_shared_secret(&mut self.trussed, platform_kek)?; + + // 4. verify pinAuth + let mut data = MediumData::new(); + data.extend_from_slice(new_pin_enc).map_err(|_| Error::InvalidParameter)?; + data.extend_from_slice(pin_hash_enc).map_err(|_| Error::InvalidParameter)?; + self.verify_pin_auth(shared_secret, &data, pin_auth)?; + + // 5. decrement retries + self.state.decrement_retries(&mut self.trussed)?; + + // 6. decrypt pinHashEnc, compare with stored + self.decrypt_pin_hash_and_maybe_escalate(shared_secret, &pin_hash_enc)?; + + // 7. reset retries + self.state.reset_retries(&mut self.trussed)?; + + // 8. decrypt and verify new PIN + let new_pin = self.decrypt_pin_check_length(shared_secret, new_pin_enc)?; + + syscall!(self.trussed.delete(shared_secret)); + + // 9. store hashed PIN + self.hash_store_pin(&new_pin)?; + + ctap2::client_pin::Response { + key_agreement: None, + pin_token: None, + retries: None, + } + } + + Subcommand::GetPinToken => { + debug!("processing CP.GPT"); + + // 1. check mandatory parameters + let platform_kek = match parameters.key_agreement.as_ref() { + Some(key) => key, + None => { return Err(Error::MissingParameter); } + }; + let pin_hash_enc = match parameters.pin_hash_enc.as_ref() { + Some(hash) => hash, + None => { return Err(Error::MissingParameter); } + }; + + // 2. fail if no retries left + self.state.pin_blocked()?; + + // 3. generate shared secret + let shared_secret = self.state.runtime.generate_shared_secret(&mut self.trussed, platform_kek)?; + + // 4. decrement retires + self.state.decrement_retries(&mut self.trussed)?; + + // 5. decrypt and verify pinHashEnc + self.decrypt_pin_hash_and_maybe_escalate(shared_secret, &pin_hash_enc)?; + + // 6. reset retries + self.state.reset_retries(&mut self.trussed)?; + + // 7. return encrypted pinToken + let pin_token = self.state.runtime.pin_token(&mut self.trussed); + debug!("wrapping pin token"); + // info!("exists? {}", syscall!(self.trussed.exists(shared_secret)).exists); + let pin_token_enc = syscall!(self.trussed.wrap_key_aes256cbc(shared_secret, pin_token)).wrapped_key; + + syscall!(self.trussed.delete(shared_secret)); + + // ble... + if pin_token_enc.len() != 16 { + return Err(Error::Other); + } + let pin_token_enc_32 = Bytes::from_slice(&pin_token_enc).unwrap(); + + ctap2::client_pin::Response { + key_agreement: None, + pin_token: Some(pin_token_enc_32), + retries: None, + } + } + + _ => { + // todo!("not implemented yet") + return Err(Error::InvalidParameter); + } + }) + } + + fn credential_management(&mut self, parameters: &ctap2::credential_management::Parameters) + -> Result { + + use ctap2::credential_management::Subcommand; + use credential_management as cm; + + // TODO: I see "failed pinauth" output, but then still continuation... + self.verify_pin_auth_using_token(¶meters)?; + + let mut cred_mgmt = cm::CredentialManagement::new(self); + let sub_parameters = ¶meters.sub_command_params; + match parameters.sub_command { + + // 0x1 + Subcommand::GetCredsMetadata => + cred_mgmt.get_creds_metadata(), + + // 0x2 + Subcommand::EnumerateRpsBegin => + cred_mgmt.first_relying_party(), + + // 0x3 + Subcommand::EnumerateRpsGetNextRp => + cred_mgmt.next_relying_party(), + + // 0x4 + Subcommand::EnumerateCredentialsBegin => { + let sub_parameters = sub_parameters.as_ref() + .ok_or(Error::MissingParameter)?; + + cred_mgmt.first_credential( + sub_parameters + .rp_id_hash.as_ref() + .ok_or(Error::MissingParameter)?, + ) + } + + // 0x5 + Subcommand::EnumerateCredentialsGetNextCredential => + cred_mgmt.next_credential(), + + // 0x6 + Subcommand::DeleteCredential => { + let sub_parameters = sub_parameters.as_ref() + .ok_or(Error::MissingParameter)?; + + cred_mgmt.delete_credential(sub_parameters + .credential_id.as_ref() + .ok_or(Error::MissingParameter)?, + ) + } + + // _ => todo!("not implemented yet"), + } + } + + fn vendor(&mut self, op: VendorOperation) -> Result<()> { + info!("hello VO {:?}", &op); + match op.into() { + 0x79 => syscall!(self.trussed.debug_dump_store()), + _ => return Err(Error::InvalidCommand), + }; + + Ok(()) + } + + +} + +// impl Authenticator for crate::Authenticator +impl crate::Authenticator +{ + fn decrypt_pin_hash_and_maybe_escalate(&mut self, shared_secret: KeyId, pin_hash_enc: &Bytes<64>) + -> Result<()> + { + let pin_hash = syscall!(self.trussed.decrypt_aes256cbc( + shared_secret, pin_hash_enc)).plaintext.ok_or(Error::Other)?; + + let stored_pin_hash = match self.state.persistent.pin_hash() { + Some(hash) => hash, + None => { return Err(Error::PinNotSet); } + }; + + if &pin_hash != &stored_pin_hash { + // I) generate new KEK + self.state.runtime.rotate_key_agreement_key(&mut self.trussed); + if self.state.persistent.retries() == 0 { + return Err(Error::PinBlocked); + } + if self.state.persistent.pin_blocked() { + return Err(Error::PinAuthBlocked); + } + return Err(Error::PinInvalid); + } + + Ok(()) + } + + fn hash_store_pin(&mut self, pin: &Message) -> Result<()> { + let pin_hash_32 = syscall!(self.trussed.hash_sha256(&pin)).hash; + let pin_hash: [u8; 16] = pin_hash_32[..16].try_into().unwrap(); + self.state.persistent.set_pin_hash(&mut self.trussed, pin_hash).unwrap(); + + Ok(()) } - fn get_assertions(&mut self, params: &GetAssertionParameters) -> Result { - todo!("get_assertions"); + fn decrypt_pin_check_length(&mut self, shared_secret: KeyId, pin_enc: &[u8]) -> Result { + // pin is expected to be filled with null bytes to length at least 64 + if pin_enc.len() < 64 { + // correct error? + return Err(Error::PinPolicyViolation); + } + + let mut pin = syscall!(self.trussed.decrypt_aes256cbc( + shared_secret, &pin_enc)).plaintext.ok_or(Error::Other)?; + + // // temp + // let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len()); + // info!("pin.len() = {}, pin_length = {}, = {:?}", + // pin.len(), pin_length, &pin); + // chop off null bytes + let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len()); + if pin_length < 4 || pin_length >= 64 { + return Err(Error::PinPolicyViolation); + } + + pin.resize_default(pin_length).unwrap(); + + Ok(pin) + } + + + // fn verify_pin(&mut self, pin_auth: &Bytes<16>, client_data_hash: &Bytes<32>) -> bool { + fn verify_pin(&mut self, pin_auth: &[u8; 16], data: &[u8]) -> Result<()> { + let key = self.state.runtime.pin_token(&mut self.trussed); + let tag = syscall!(self.trussed.sign_hmacsha256(key, data)).signature; + if pin_auth == &tag[..16] { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } + + fn verify_pin_auth(&mut self, shared_secret: KeyId, data: &[u8], pin_auth: &Bytes<16>) + -> Result<()> + { + let expected_pin_auth = syscall!(self.trussed.sign_hmacsha256(shared_secret, data)).signature; + + if &expected_pin_auth[..16] == &pin_auth[..] { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } + + // fn verify_pin_auth_using_token(&mut self, data: &[u8], pin_auth: &Bytes<16>) + fn verify_pin_auth_using_token( + &mut self, + parameters: &ctap2::credential_management::Parameters + ) -> Result<()> { + + // info!("CM params: {:?}", parameters); + use ctap2::credential_management::Subcommand; + match parameters.sub_command { + // are we Haskell yet lol + sub_command @ Subcommand::GetCredsMetadata | + sub_command @ Subcommand::EnumerateRpsBegin | + sub_command @ Subcommand::EnumerateCredentialsBegin | + sub_command @ Subcommand::DeleteCredential => { + + // check pinProtocol + let pin_protocol = parameters + // .sub_command_params.as_ref().ok_or(Error::MissingParameter)? + .pin_protocol.ok_or(Error::MissingParameter)?; + if pin_protocol != 1 { + return Err(Error::InvalidParameter); + } + + // check pinAuth + let pin_token = self.state.runtime.pin_token(&mut self.trussed); + let mut data: Bytes<{sizes::MAX_CREDENTIAL_ID_LENGTH_PLUS_256}> = + Bytes::from_slice(&[sub_command as u8]).unwrap(); + let len = 1 + match sub_command { + Subcommand::EnumerateCredentialsBegin | + Subcommand::DeleteCredential => { + data.resize_to_capacity(); + // ble, need to reserialize + ctap_types::serde::cbor_serialize( + ¶meters.sub_command_params + .as_ref() + .ok_or(Error::MissingParameter)?, + &mut data[1..], + ).map_err(|_| Error::LimitExceeded)?.len() + } + _ => 0, + }; + + // info!("input to hmacsha256: {:?}", &data[..len]); + let expected_pin_auth = syscall!(self.trussed.sign_hmacsha256( + pin_token, + &data[..len], + )).signature; + + let pin_auth = parameters + .pin_auth.as_ref().ok_or(Error::MissingParameter)?; + + if &expected_pin_auth[..16] == &pin_auth[..] { + info!("passed pinauth"); + Ok(()) + } else { + info!("failed pinauth!"); + self.state.decrement_retries(&mut self.trussed)?; + let maybe_blocked = self.state.pin_blocked(); + if maybe_blocked.is_err() { + info!("blocked"); + maybe_blocked + } else { + info!("pinAuthInvalid"); + Err(Error::PinAuthInvalid) + } + + } + } + + _ => Ok(()), + } } - fn make_credential(&mut self, params: &MakeCredentialParameters) -> Result { - todo!("make_credential"); + /// Returns whether UV was performed. + fn pin_prechecks(&mut self, + options: &Option, + pin_auth: &Option, + pin_protocol: &Option, + data: &[u8], + ) + -> Result + { + // 1. pinAuth zero length -> wait for user touch, then + // return PinNotSet if not set, PinInvalid if set + // + // the idea is for multi-authnr scenario where platform + // wants to enforce PIN and needs to figure out which authnrs support PIN + if let Some(pin_auth) = pin_auth.as_ref() { + if pin_auth.len() == 0 { + self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + if !self.state.persistent.pin_is_set() { + return Err(Error::PinNotSet); + } else { + return Err(Error::PinAuthInvalid); + } + } + } + + // 2. check PIN protocol is 1 if pinAuth was sent + if let Some(ref _pin_auth) = pin_auth { + if let Some(1) = pin_protocol { + } else { + return Err(Error::PinAuthInvalid); + } + } + + // 3. if no PIN is set (we have no other form of UV), + // and platform sent `uv` or `pinAuth`, return InvalidOption + if !self.state.persistent.pin_is_set() { + if let Some(ref options) = &options { + if Some(true) == options.uv { + return Err(Error::InvalidOption); + } + } + if pin_auth.is_some() { + return Err(Error::InvalidOption); + } + } + + // 4. If authenticator is protected by som form of user verification, do it + // + // TODO: Should we should fail if `uv` is passed? + // Current thinking: no + if self.state.persistent.pin_is_set() { + + // let mut uv_performed = false; + if let Some(ref pin_auth) = pin_auth { + if pin_auth.len() != 16 { + return Err(Error::InvalidParameter); + } + // seems a bit redundant to check here in light of 2. + // I guess the CTAP spec writers aren't implementers :D + if let Some(1) = pin_protocol { + // 5. if pinAuth is present and pinProtocol = 1, verify + // success --> set uv = 1 + // error --> PinAuthInvalid + self.verify_pin( + // unwrap panic ruled out above + pin_auth.as_slice().try_into().unwrap(), + data, + )?; + + return Ok(true); + + } else { + // 7. pinAuth present + pinProtocol != 1 --> error PinAuthInvalid + return Err(Error::PinAuthInvalid); + } + + } else { + // 6. pinAuth not present + clientPin set --> error PinRequired + if self.state.persistent.pin_is_set() { + return Err(Error::PinRequired); + } + } + } + + Ok(false) } + + /// If allow_list is some, select the first one (latest timestamp) that is usable, + /// and return Some(it). + /// + /// If allow_list is none, pull applicable credentials, store + /// in state's credential_heap, and return none + #[inline(never)] + fn locate_credentials( + &mut self, rp_id_hash: &Bytes32, + allow_list: &Option, + uv_performed: bool, + ) + -> Result<()> + { + // validate allowList + let mut allow_list_len = 0; + let allowed_credentials = if let Some(allow_list) = allow_list.as_ref() { + allow_list_len = allow_list.len(); + allow_list.into_iter() + // discard not properly serialized encrypted credentials + .filter_map(|credential_descriptor| { + info!( + "GA try from cred id: {}", + hex_str!(&credential_descriptor.id), + ); + let cred_maybe = Credential::try_from( + self, rp_id_hash, credential_descriptor) + .ok(); + info!("cred_maybe: {:?}", &cred_maybe); + cred_maybe + } ) + .collect() + } else { + CredentialList::new() + }; + + let mut min_heap = MinCredentialHeap::new(); + + let allowed_credentials_passed = allowed_credentials.len() > 0; + + if allowed_credentials_passed { + // "If an allowList is present and is non-empty, + // locate all denoted credentials present on this authenticator + // and bound to the specified rpId." + debug!("allowedList passed with {} creds", allowed_credentials.len()); + let mut rk_count = 0; + let mut applicable_credentials: CredentialList = allowed_credentials + .into_iter() + .filter(|credential| match credential.key.clone() { + // TODO: should check if wrapped key is valid AEAD + // On the other hand, we already decrypted a valid AEAD + Key::WrappedKey(_) => true, + Key::ResidentKey(key) => { + debug!("checking if ResidentKey {:?} exists", &key); + let exists = match credential.algorithm { + -7 => syscall!(self.trussed.exists(Mechanism::P256, key)).exists, + -8 => syscall!(self.trussed.exists(Mechanism::Ed255, key)).exists, + // -9 => { + // let exists = syscall!(self.trussed.exists(Mechanism::Totp, key)).exists; + // info!("found it"); + // exists + // } + _ => false, + }; + if exists { + rk_count = rk_count + 1; + } + exists + } + }) + .filter(|credential| { + use credential::CredentialProtectionPolicy as Policy; + debug!("CredentialProtectionPolicy {:?}", &credential.cred_protect); + match credential.cred_protect { + None | Some(Policy::Optional) => true, + Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, + Some(Policy::Required) => uv_performed, + + } + }) + .collect(); + while applicable_credentials.len() > 0 { + // Store all other applicable credentials in volatile storage and add to our + // credential heap. + let credential = applicable_credentials.pop().unwrap(); + let serialized = credential.serialize()?; + + let mut path = [b'0', b'0']; + format_hex(&[applicable_credentials.len() as u8], &mut path); + let path = PathBuf::from(&path); + // let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; + // let id = credential.id_using_hash(&mut self.trussed, kek, rp_id_hash)?; + // let credential_id_hash = self.hash(&id.0.as_ref()); + + // let path = rk_path(&rp_id_hash, &credential_id_hash); + let timestamp_path = TimestampPath { + timestamp: credential.creation_time, + path: path.clone(), + location: Location::Volatile, + }; + + + info!("added volatile cred: {:?}", ×tamp_path); + info!("{}",hex_str!(&serialized)); + + + try_syscall!(self.trussed.write_file( + Location::Volatile, + path.clone(), + serialized, + None, + )).map_err(|_| { + Error::KeyStoreFull + })?; + + // attempt to read back + // let data = syscall!(self.trussed.read_file( + // Location::Volatile, + // timestamp_path.path.clone(), + // )).data; + // crate::Credential::deserialize(&data).unwrap(); + + + if min_heap.capacity() > min_heap.len() { + min_heap.push(timestamp_path).map_err(drop).unwrap(); + } else { + if timestamp_path.timestamp > min_heap.peek().unwrap().timestamp { + min_heap.pop().unwrap(); + min_heap.push(timestamp_path).map_err(drop).unwrap(); + } + } + // If more than one credential was located in step 1 and allowList is present and not empty, + // select any applicable credential and proceed to step 12. Otherwise, order the credentials + // by the time when they were created in reverse order. + // The first credential is the most recent credential that was created. + if rk_count > 1 { + break + } + + } + } else if allow_list_len == 0 { + // If an allowList is not present, + // locate all credentials that are present on this authenticator + // and bound to the specified rpId; sorted by reverse creation time + + // let rp_id_hash = self.hash(rp_id.as_ref()); + + // + // So here's the idea: + // + // - credentials can be pretty big + // - we declare N := MAX_CREDENTIAL_COUNT_IN_LIST in GetInfo + // - potentially there are more RKs for a given RP (a bit academic ofc) + // + // - first, we use a min-heap to keep only the topN credentials: + // if our "next" one is larger/later than the min of the heap, + // pop this min and push ours + // + // - then, we use a max-heap to sort the remaining <=N credentials + // - these then go into a CredentialList + // - (we don't need to keep that around even) + // + debug!("no allowedList passed"); + + // let mut credentials = CredentialList::new(); + + let data = syscall!(self.trussed.read_dir_files_first( + Location::Internal, + rp_rk_dir(&rp_id_hash), + None, + )).data; + + let data = match data { + Some(data) => data, + None => return Err(Error::NoCredentials), + }; + + let credential = Credential::deserialize(&data).unwrap(); + + use credential::CredentialProtectionPolicy as Policy; + let keep = match credential.cred_protect { + None | Some(Policy::Optional) => true, + Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, + Some(Policy::Required) => uv_performed, + }; + + let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; + + if keep { + let id = credential.id(&mut self.trussed, kek, Some(rp_id_hash))?; + let credential_id_hash = self.hash(&id.0.as_ref()); + + let timestamp_path = TimestampPath { + timestamp: credential.creation_time, + path: rk_path(&rp_id_hash, &credential_id_hash), + location: Location::Internal, + }; + + min_heap.push(timestamp_path).map_err(drop).unwrap(); + // info!("first: {:?}", &self.hash(&id.0)); + } + + loop { + let data = syscall!(self.trussed.read_dir_files_next()).data; + let data = match data { + Some(data) => data, + None => break, + }; + + let credential = Credential::deserialize(&data).unwrap(); + + let keep = match credential.cred_protect { + None | Some(Policy::Optional) => true, + Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, + Some(Policy::Required) => uv_performed, + }; + + if keep { + + let id = credential.id(&mut self.trussed, kek, Some(rp_id_hash))?; + let credential_id_hash = self.hash(&id.0.as_ref()); + + let timestamp_path = TimestampPath { + timestamp: credential.creation_time, + path: rk_path(&rp_id_hash, &credential_id_hash), + location: Location::Internal, + }; + + if min_heap.capacity() > min_heap.len() { + min_heap.push(timestamp_path).map_err(drop).unwrap(); + } else { + if timestamp_path.timestamp > min_heap.peek().unwrap().timestamp { + min_heap.pop().unwrap(); + min_heap.push(timestamp_path).map_err(drop).unwrap(); + } + } + } + } + + }; + + // "If no applicable credentials were found, return CTAP2_ERR_NO_CREDENTIALS" + if min_heap.is_empty() { + return Err(Error::NoCredentials); + } + + // now sort them + self.state.runtime.free_credential_heap(&mut self.trussed); + let max_heap = self.state.runtime.credential_heap(); + while !min_heap.is_empty() { + max_heap.push(min_heap.pop().unwrap()).map_err(drop).unwrap(); + } + + Ok(()) + } + + #[inline(never)] + fn process_assertion_extensions(&mut self, + get_assertion_state: &state::ActiveGetAssertionData, + extensions: &ctap2::get_assertion::ExtensionsInput, + _credential: &Credential, + credential_key: KeyId, + ) -> Result> { + if let Some(hmac_secret) = &extensions.hmac_secret { + if let Some(pin_protocol) = hmac_secret.pin_protocol { + if pin_protocol != 1 { + return Err(Error::InvalidParameter); + } + } + + // We derive credRandom as an hmac of the existing private key. + // UV is used as input data since credRandom should depend UV + // i.e. credRandom = HMAC(private_key, uv) + let cred_random = syscall!(self.trussed.derive_key( + Mechanism::HmacSha256, + credential_key, + Some(Bytes::from_slice(&[get_assertion_state.uv_performed as u8]).unwrap()), + trussed::types::StorageAttributes::new().set_persistence(Location::Volatile) + )).key; + + // Verify the auth tag, which uses the same process as the pinAuth + let kek = self.state.runtime.generate_shared_secret(&mut self.trussed, &hmac_secret.key_agreement)?; + self.verify_pin_auth(kek, &hmac_secret.salt_enc, &hmac_secret.salt_auth).map_err(|_| Error::ExtensionFirst)?; + + if hmac_secret.salt_enc.len() != 32 && hmac_secret.salt_enc.len() != 64 { + return Err(Error::InvalidLength); + } + + // decrypt input salt_enc to get salt1 or (salt1 || salt2) + let salts = syscall!( + self.trussed.decrypt(Mechanism::Aes256Cbc, kek, &hmac_secret.salt_enc, b"", b"", b"") + ).plaintext.ok_or(Error::InvalidOption)?; + + let mut salt_output: Bytes<64> = Bytes::new(); + + // output1 = hmac_sha256(credRandom, salt1) + let output1 = syscall!( + self.trussed.sign_hmacsha256(cred_random, &salts[0..32]) + ).signature; + + salt_output.extend_from_slice(&output1).unwrap(); + + if salts.len() == 64 { + // output2 = hmac_sha256(credRandom, salt2) + let output2 = syscall!( + self.trussed.sign_hmacsha256(cred_random, &salts[32..64]) + ).signature; + + salt_output.extend_from_slice(&output2).unwrap(); + } + + syscall!(self.trussed.delete(cred_random)); + + // output_enc = aes256-cbc(sharedSecret, IV=0, output1 || output2) + let output_enc = syscall!( + self.trussed.encrypt(Mechanism::Aes256Cbc, kek, &salt_output, b"", None) + ).ciphertext; + + Ok(Some(ctap2::get_assertion::ExtensionsOutput { + hmac_secret: Some(Bytes::from_slice(&output_enc).unwrap()) + })) + + } else { + Ok(None) + } + + } + + + fn assert_with_credential(&mut self, num_credentials: Option, credential: Credential) + -> Result + { + let data = self.state.runtime.active_get_assertion.clone().unwrap(); + let rp_id_hash = Bytes::from_slice(&data.rp_id_hash).unwrap(); + + let (key, is_rk) = match credential.key.clone() { + Key::ResidentKey(key) => (key, true), + Key::WrappedKey(bytes) => { + let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; + // info!("unwrapping {:?} with wrapping key {:?}", &bytes, &wrapping_key); + let key_result = syscall!(self.trussed.unwrap_key_chacha8poly1305( + wrapping_key, + &bytes, + b"", + // &rp_id_hash, + Location::Volatile, + )).key; + // debug!("key result: {:?}", &key_result); + info!("key result"); + match key_result { + Some(key) => (key, false), + None => { return Err(Error::Other); } + } + } + }; + + // 8. process any extensions present + let extensions_output = if let Some(extensions) = &data.extensions { + self.process_assertion_extensions(&data, &extensions, &credential, key)? + } else { + None + }; + + // 9./10. sign clientDataHash || authData with "first" credential + + // info!("signing with credential {:?}", &credential); + let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; + let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; + + use ctap2::AuthenticatorDataFlags as Flags; + + let sig_count = self.state.persistent.timestamp(&mut self.trussed)?; + + let authenticator_data = ctap2::get_assertion::AuthenticatorData { + rp_id_hash: rp_id_hash, + + flags: { + let mut flags = Flags::EMPTY; + if data.up_performed { + flags |= Flags::USER_PRESENCE; + } + if data.uv_performed { + flags |= Flags::USER_VERIFIED; + } + if extensions_output.is_some() { + flags |= Flags::EXTENSION_DATA; + } + flags + }, + + sign_count: sig_count, + attested_credential_data: None, + extensions: extensions_output + }; + + let serialized_auth_data = authenticator_data.serialize(); + + let mut commitment = Bytes::<1024>::new(); + commitment.extend_from_slice(&serialized_auth_data).map_err(|_| Error::Other)?; + commitment.extend_from_slice(&data.client_data_hash).map_err(|_| Error::Other)?; + + let (mechanism, serialization) = match credential.algorithm { + -7 => (Mechanism::P256, SignatureSerialization::Asn1Der), + -8 => (Mechanism::Ed255, SignatureSerialization::Raw), + -9 => (Mechanism::Totp, SignatureSerialization::Raw), + _ => { return Err(Error::Other); } + }; + + debug!("signing with {:?}, {:?}", &mechanism, &serialization); + let signature = match mechanism { + // Mechanism::Totp => { + // let timestamp = u64::from_le_bytes(data.client_data_hash[..8].try_into().unwrap()); + // info!("TOTP with timestamp {:?}", ×tamp); + // syscall!(self.trussed.sign_totp(key, timestamp)).signature.to_bytes().unwrap() + // } + _ => syscall!(self.trussed.sign(mechanism, key.clone(), &commitment, serialization)).signature + .to_bytes().unwrap(), + }; + + if !is_rk { + syscall!(self.trussed.delete(key)); + } + + let mut response = ctap2::get_assertion::Response { + credential: Some(credential_id.into()), + auth_data: Bytes::from_slice(&serialized_auth_data).map_err(|_| Error::Other)?, + signature, + user: None, + number_of_credentials: num_credentials, + }; + + if is_rk { + let mut user = credential.user.clone(); + // User identifiable information (name, DisplayName, icon) MUST not + // be returned if user verification is not done by the authenticator. + // For single account per RP case, authenticator returns "id" field. + if !data.uv_performed || !data.multiple_credentials { + user.icon = None; + user.name = None; + user.display_name = None; + } + response.user = Some(user); + } + + Ok(response) + } + + fn delete_resident_key_by_user_id( + &mut self, + rp_id_hash: &Bytes32, + user_id: &Bytes<64>, + ) -> Result<()> { + + // Prepare to iterate over all credentials associated to RP. + let rp_path = rp_rk_dir(&rp_id_hash); + let mut entry = syscall!(self.trussed.read_dir_first( + Location::Internal, + rp_path.clone(), + None, + )).entry; + + loop { + info!("this may be an RK: {:?}", &entry); + let rk_path = match entry { + // no more RKs left + // break breaks inner loop here + None => break, + Some(entry) => PathBuf::from(entry.path()), + }; + + info!("checking RK {:?} for userId ", &rk_path); + let credential_data = syscall!(self.trussed.read_file( + Location::Internal, + PathBuf::from(rk_path.clone()), + )).data; + let credential_maybe = Credential::deserialize(&credential_data); + + if let Ok(old_credential) = credential_maybe { + if old_credential.user.id == user_id { + match old_credential.key { + credential::Key::ResidentKey(key) => { + info!(":: deleting resident key"); + syscall!(self.trussed.delete(key)); + } + _ => { + warn!(":: WARNING: unexpected server credential in rk."); + } + } + syscall!(self.trussed.remove_file( + Location::Internal, + PathBuf::from(rk_path), + )); + + info!("Overwriting previous rk tied to this userId."); + break; + } + } else { + warn_now!("WARNING: Could not read RK."); + } + + // prepare for next loop iteration + entry = syscall!(self.trussed.read_dir_next()).entry; + } + + Ok(()) + + } + + pub(crate) fn delete_resident_key_by_path( + &mut self, + rk_path: &Path, + ) + -> Result<()> + { + info!("deleting RK {:?}", &rk_path); + let credential_data = syscall!(self.trussed.read_file( + Location::Internal, + PathBuf::from(rk_path), + )).data; + let credential_maybe = Credential::deserialize(&credential_data); + // info!("deleting credential {:?}", &credential); + + + if let Ok(credential) = credential_maybe { + + match credential.key { + credential::Key::ResidentKey(key) => { + info!(":: deleting resident key"); + syscall!(self.trussed.delete(key)); + } + credential::Key::WrappedKey(_) => {} + } + } else { + // If for some reason there becomes a corrupt credential, + // we can still at least orphan the key rather then crash. + info!("Warning! Orpaning a key."); + } + + info!(":: deleting RK file {:?} itself", &rk_path); + syscall!(self.trussed.remove_file( + Location::Internal, + PathBuf::from(rk_path), + )); + + + Ok(()) + } + +} + +fn rp_rk_dir(rp_id_hash: &Bytes<32>) -> PathBuf { + // uses only first 8 bytes of hash, which should be "good enough" + let mut hex = [b'0'; 16]; + format_hex(&rp_id_hash[..8], &mut hex); + + let mut dir = PathBuf::from(b"rk"); + dir.push(&PathBuf::from(&hex)); + + dir } + +fn rk_path(rp_id_hash: &Bytes<32>, credential_id_hash: &Bytes<32>) -> PathBuf { + let mut path = rp_rk_dir(rp_id_hash); + + let mut hex = [0u8; 16]; + format_hex(&credential_id_hash[..8], &mut hex); + path.push(&PathBuf::from(&hex)); + + path +} + diff --git a/src/credential_management.rs b/src/ctap2/credential_management.rs similarity index 86% rename from src/credential_management.rs rename to src/ctap2/credential_management.rs index fe3df64..8ba81e0 100644 --- a/src/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -1,9 +1,8 @@ -//! TODO: There is potential need for `fsck` +//! TODO: T use core::convert::TryFrom; use trussed::{ - client, syscall, types::{ DirEntry, @@ -40,9 +39,10 @@ use crate::{ CredentialManagementEnumerateRps, CredentialManagementEnumerateCredentials, }, + TrussedRequirements, }; -pub struct CredentialManagement<'a, UP, T> +pub(crate) struct CredentialManagement<'a, UP, T> where UP: UserPresence, { authnr: &'a mut Authenticator, @@ -75,15 +75,7 @@ where UP: UserPresence, impl CredentialManagement<'_, UP, T> where UP: UserPresence, - T: client::Client - + client::P256 - + client::Chacha8Poly1305 - + client::Aes256Cbc - + client::Sha256 - + client::HmacSha256 - + client::Ed255 - + client::Totp - + client::P256 + T: TrussedRequirements, { pub fn get_creds_metadata(&mut self) -> Result { info!("get metadata"); @@ -204,8 +196,11 @@ where UP: UserPresence, if let Some(total_rps) = response.total_rps { if total_rps > 1 { let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); - self.state.runtime.cache_rp = Some( - CredentialManagementEnumerateRps(total_rps - 1, rp_id_hash)); + self.state.runtime.cached_rp = Some( + CredentialManagementEnumerateRps { + remaining: total_rps - 1, + rp_id_hash, + }); } } } @@ -216,12 +211,10 @@ where UP: UserPresence, pub fn next_relying_party(&mut self) -> Result { info!("next rp"); - let (remaining, last_rp_id_hash) = match self.state.runtime.cache_rp { - Some(CredentialManagementEnumerateRps( - remaining, ref rp_id_hash)) => - (remaining, rp_id_hash), - _ => return Err(Error::NotAllowed), - }; + let CredentialManagementEnumerateRps { + remaining, + rp_id_hash: last_rp_id_hash, + } = self.state.runtime.cached_rp.clone().ok_or_else(|| Error::NotAllowed)?; let dir = PathBuf::from(b"rk"); @@ -272,16 +265,17 @@ where UP: UserPresence, // cache state for next call if remaining > 1 { let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); - self.state.runtime.cache_rp = Some(CredentialManagementEnumerateRps( - remaining - 1, rp_id_hash - )); + self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { + remaining: remaining - 1, + rp_id_hash, + }); } else { - self.state.runtime.cache_rp = None; + self.state.runtime.cached_rp = None; } } } } else { - self.state.runtime.cache_rp = None; + self.state.runtime.cached_rp = None; } Ok(response) @@ -307,7 +301,7 @@ where UP: UserPresence, pub fn first_credential(&mut self, rp_id_hash: &Bytes32) -> Result { info!("first credential"); - self.state.runtime.cache_rk = None; + self.state.runtime.cached_rk = None; let mut hex = [b'0'; 16]; super::format_hex(&rp_id_hash[..8], &mut hex); @@ -324,11 +318,11 @@ where UP: UserPresence, if let Some(num_rks) = response.total_credentials { if num_rks > 1 { // let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); - self.state.runtime.cache_rk = Some(CredentialManagementEnumerateCredentials( - num_rks - 1, - first_rk.path().parent().unwrap(), - PathBuf::from(first_rk.file_name()), - )); + self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { + remaining: num_rks - 1, + rp_dir: first_rk.path().parent().unwrap(), + prev_filename: PathBuf::from(first_rk.file_name()), + }); } } @@ -338,14 +332,19 @@ where UP: UserPresence, pub fn next_credential(&mut self) -> Result { info!("next credential"); - let (remaining, rp_dir, prev_filename) = match self.state.runtime.cache_rk { - Some(CredentialManagementEnumerateCredentials( - x, ref y, ref z)) - => (x, y.clone(), z.clone()), - _ => return Err(Error::NotAllowed), - }; + let CredentialManagementEnumerateCredentials { + remaining, + rp_dir, + prev_filename, + } = self.state.runtime.cached_rk.clone().ok_or(Error::NotAllowed)?; + // let (remaining, rp_dir, prev_filename) = match self.state.runtime.cached_rk { + // Some(CredentialManagementEnumerateCredentials( + // x, ref y, ref z)) + // => (x, y.clone(), z.clone()), + // _ => return Err(Error::NotAllowed), + // }; - self.state.runtime.cache_rk = None; + self.state.runtime.cached_rk = None; // let mut hex = [b'0'; 16]; // super::format_hex(&rp_id_hash[..8], &mut hex); @@ -372,11 +371,11 @@ where UP: UserPresence, // cache state for next call if remaining > 1 { - self.state.runtime.cache_rk = Some(CredentialManagementEnumerateCredentials( - remaining - 1, - rk.path().parent().unwrap(), - PathBuf::from(rk.file_name()), - )); + self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { + remaining: remaining - 1, + rp_dir: rk.path().parent().unwrap(), + prev_filename: PathBuf::from(rk.file_name()), + }); } Ok(response) @@ -422,12 +421,12 @@ where UP: UserPresence, _ => return Err(Error::InvalidCredential), }; - use crate::SupportedAlgorithm; + use crate::SigningAlgorithm; use trussed::types::{KeySerialization, Mechanism}; - let algorithm = SupportedAlgorithm::try_from(credential.algorithm)?; + let algorithm = SigningAlgorithm::try_from(credential.algorithm)?; let cose_public_key = match algorithm { - SupportedAlgorithm::P256 => { + SigningAlgorithm::P256 => { let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; let cose_public_key = syscall!(self.trussed.serialize_key( Mechanism::P256, public_key.clone(), @@ -439,7 +438,7 @@ where UP: UserPresence, ctap_types::serde::cbor_deserialize(&cose_public_key) .unwrap()) } - SupportedAlgorithm::Ed25519 => { + SigningAlgorithm::Ed25519 => { let public_key = syscall!(self.trussed.derive_ed255_public_key( private_key, Location::Volatile)).key; let cose_public_key = syscall!(self.trussed.serialize_ed255_key( @@ -450,9 +449,9 @@ where UP: UserPresence, ctap_types::serde::cbor_deserialize(&cose_public_key) .unwrap()) } - SupportedAlgorithm::Totp => { - PublicKey::TotpKey(Default::default()) - } + // SigningAlgorithm::Totp => { + // PublicKey::TotpKey(Default::default()) + // } }; response.public_key = Some(cose_public_key); response.cred_protect = match credential.cred_protect { diff --git a/src/lib.rs b/src/lib.rs index 6c6029f..ca443f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,48 +1,97 @@ - #![cfg_attr(not(test), no_std)] +//! Open source reference implementation of FIDO CTAP. +//! +//! The core structure is [`Authenticator`], a Trussed® application. +//! +//! It implements [`ctap1::Authenticator`] and [`ctap2::Authenticator`] traits, +//! which express the interface defined in the CTAP specification. -use core::convert::{TryFrom, TryInto}; +#![cfg_attr(not(test), no_std)] +// #![warn(missing_docs)] #[macro_use] extern crate delog; generate_macros!(); use trussed::{ - client, syscall, try_syscall, + client, syscall, Client as TrussedClient, types::{ - KeyId, - KeySerialization, - Mechanism, - MediumData, Message, - SignatureSerialization, - Location, }, }; use ctap_types::{ - Bytes, Bytes32, String, Vec, - operation::VendorOperation, - authenticator::{ctap2, Error, Request, Response}, - ctap1::{ - self, - Command as U2fCommand, - Response as U2fResponse, - Result as U2fResult, - Error as U2fError, - }, + Bytes, + authenticator::{Request, Response}, }; -use littlefs2::path::{Path, PathBuf}; +/// Re-export of `ctap-types` authenticator errors. +pub use ctap_types::authenticator::Error; + +pub mod ctap1; +pub mod ctap2; -pub mod credential_management; -pub mod state; pub mod constants; +pub mod credential; +pub mod state; -use state::{ - MinCredentialHeap, - TimestampPath, -}; +pub use ctap1::Authenticator as Ctap1Authenticator; +pub use ctap2::Authenticator as Ctap2Authenticator; + + +/// Results with our [`Error`]. +pub type Result = core::result::Result; + + +/// Trait bound on our implementation's requirements from a Trussed client. +/// +/// - Client is core Trussed client functionality. +/// - Ed25519 and P-256 are the core signature algorithms. +/// - AES-256, SHA-256 and its HMAC are used within the CTAP protocols. +/// - ChaCha8Poly1305 is our AEAD of choice, used e.g. for the key handles. +pub trait TrussedRequirements: + client::Client + + client::P256 + + client::Chacha8Poly1305 + + client::Aes256Cbc + + client::Sha256 + + client::HmacSha256 + + client::Ed255 + // + client::Totp +{} + +impl TrussedRequirements for T +where T: + client::Client + + client::P256 + + client::Chacha8Poly1305 + + client::Aes256Cbc + + client::Sha256 + + client::HmacSha256 + + client::Ed255 + // + client::Totp +{} + +/// Trussed® app implementing a FIDO authenticator. +/// +/// It implements [`ctap1::Authenticator`] and [`ctap2::Authenticator`] traits, +/// which, in turn, express the interfaces defined in the CTAP specification. +/// +/// The type parameter `T` selects a Trussed® client implementation, which +/// must meet the [`TrussedRequirements`] in our implementation. +/// +/// NB: `T` should be the first parameter, `UP` should default to `Conforming`, +/// and probably `UP` shouldn't be a generic parameter at all, at least not this kind. +pub struct Authenticator +// TODO: changing the order is breaking, but default generic parameters must be trailing. +// pub struct Authenticator +where + UP: UserPresence, +{ + trussed: T, + state: state::State, + up: UP, +} // EWW.. this is a bit unsafe isn't it fn format_hex(data: &[u8], mut buffer: &mut [u8]) { @@ -54,72 +103,57 @@ fn format_hex(data: &[u8], mut buffer: &mut [u8]) { } } -fn rp_rk_dir(rp_id_hash: &Bytes<32>) -> PathBuf { - // uses only first 8 bytes of hash, which should be "good enough" - let mut hex = [b'0'; 16]; - format_hex(&rp_id_hash[..8], &mut hex); - - let mut dir = PathBuf::from(b"rk"); - dir.push(&PathBuf::from(&hex)); - - dir -} - -fn rk_path(rp_id_hash: &Bytes<32>, credential_id_hash: &Bytes<32>) -> PathBuf { - let mut path = rp_rk_dir(rp_id_hash); - - let mut hex = [0u8; 16]; - format_hex(&credential_id_hash[..8], &mut hex); - path.push(&PathBuf::from(&hex)); - - path -} - -pub mod credential; -pub use credential::*; - -pub type Result = core::result::Result; - +/// Currently Ed25519 and P256. #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(i32)] -pub enum SupportedAlgorithm { - P256 = -7, +#[non_exhaustive] +pub enum SigningAlgorithm { + /// The Ed25519 signature algorithm. Ed25519 = -8, - Totp = -9, + /// The NIST P-256 signature algorithm. + P256 = -7, + // #[doc(hidden)] + // Totp = -9, } -impl core::convert::TryFrom for SupportedAlgorithm { +impl core::convert::TryFrom for SigningAlgorithm { type Error = Error; fn try_from(alg: i32) -> Result { Ok(match alg { - -7 => SupportedAlgorithm::P256, - -8 => SupportedAlgorithm::Ed25519, - -9 => SupportedAlgorithm::Totp, + -7 => SigningAlgorithm::P256, + -8 => SigningAlgorithm::Ed25519, + // -9 => SigningAlgorithm::Totp, _ => return Err(Error::UnsupportedAlgorithm), }) } } -/// Idea is to maybe send a request over a queue, -/// and return upon button press. -/// TODO: Do we need a timeout? +/// Method to check for user presence. pub trait UserPresence: Copy { fn user_present(self, trussed: &mut T, timeout_milliseconds: u32) -> Result<()>; } +#[deprecated(note="use `Silent` directly`")] +pub type SilentAuthenticator = Silent; + +/// No user presence verification. #[derive(Copy, Clone)] -pub struct SilentAuthenticator {} +pub struct Silent {} -impl UserPresence for SilentAuthenticator { +impl UserPresence for Silent { fn user_present(self, _: &mut T, _:u32) -> Result<()> { Ok(()) } } +#[deprecated(note="use `Conforming` directly")] +pub type NonSilentAuthenticator = Conforming; + +/// User presence verification via Trussed. #[derive(Copy, Clone)] -pub struct NonSilentAuthenticator {} +pub struct Conforming {} -impl UserPresence for NonSilentAuthenticator { +impl UserPresence for Conforming { fn user_present(self, trussed: &mut T, timeout_milliseconds: u32) -> Result<()> { let result = syscall!(trussed.confirm_user_present(timeout_milliseconds)).result; result.map_err(|err| match err { @@ -133,25 +167,9 @@ fn cbor_serialize_message(object: &T) -> core::result::Resu Ok(trussed::cbor_serialize_bytes(object)?) } -pub struct Authenticator -where UP: UserPresence, -{ - trussed: T, - state: state::State, - up: UP, -} - impl Authenticator where UP: UserPresence, - T: client::Client - + client::P256 - + client::Chacha8Poly1305 - + client::Aes256Cbc - + client::Sha256 - + client::HmacSha256 - + client::Ed255 - + client::Totp - // + TrussedClient + T: TrussedRequirements, { pub fn new(trussed: T, up: UP) -> Self { @@ -161,1901 +179,28 @@ where UP: UserPresence, authenticator } - pub fn call_u2f(&mut self, request: &U2fCommand) -> U2fResult { - info!("called u2f"); + pub fn call(&mut self, request: &Request) -> Result { self.state.persistent.load_if_not_initialised(&mut self.trussed); - let mut commitment = Bytes::<324>::new(); - match request { - U2fCommand::Register(reg) => { - - self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) - .map_err(|_| U2fError::ConditionsOfUseNotSatisfied)?; - - // Generate a new P256 key pair. - let private_key = syscall!(self.trussed.generate_p256_private_key(Location::Volatile)).key; - let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; - - let serialized_cose_public_key = syscall!(self.trussed.serialize_p256_key( - public_key, KeySerialization::EcdhEsHkdf256 - )).serialized_key; - let cose_key: ctap_types::cose::EcdhEsHkdf256PublicKey - = trussed::cbor_deserialize(&serialized_cose_public_key).unwrap(); - - let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed) - .map_err(|_| U2fError::UnspecifiedCheckingError)?; - debug!("wrapping u2f private key"); - let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( - wrapping_key, - private_key, - ®.app_id, - )).wrapped_key; - // debug!("wrapped_key = {:?}", &wrapped_key); - - let key = Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| U2fError::UnspecifiedCheckingError)?); - let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap(); - - let mut rp_id = heapless::String::new(); - - // We do not know the rpId string in U2F. Just using placeholder. - rp_id.push_str("u2f").ok(); - let rp = ctap_types::webauthn::PublicKeyCredentialRpEntity{ - id: rp_id, - name: None, - url: None, - }; - - let user = ctap_types::webauthn::PublicKeyCredentialUserEntity { - id: Bytes::from_slice(&[0u8; 8]).unwrap(), - icon: None, - name: None, - display_name: None, - }; - - let credential = Credential::new( - credential::CtapVersion::U2fV2, - &rp, - &user, - - SupportedAlgorithm::P256 as i32, - key, - self.state.persistent.timestamp(&mut self.trussed).map_err(|_| U2fError::NotEnoughMemory)?, - None, - None, - nonce, - ); - - // info!("made credential {:?}", &credential); - - // 12.b generate credential ID { = AEAD(Serialize(Credential)) } - let kek = self.state.persistent.key_encryption_key(&mut self.trussed).map_err(|_| U2fError::NotEnoughMemory)?; - let credential_id = credential.id(&mut self.trussed, kek, Some(®.app_id)).map_err(|_| U2fError::NotEnoughMemory)?; - syscall!(self.trussed.delete(public_key)); - syscall!(self.trussed.delete(private_key)); - - commitment.push(0).unwrap(); // reserve byte - commitment.extend_from_slice(®.app_id).unwrap(); - commitment.extend_from_slice(®.challenge).unwrap(); - - commitment.extend_from_slice(&credential_id.0).unwrap(); - - commitment.push(0x04).unwrap(); // public key uncompressed byte - commitment.extend_from_slice(&cose_key.x).unwrap(); - commitment.extend_from_slice(&cose_key.y).unwrap(); - - let attestation = self.state.identity.attestation(&mut self.trussed); - - let (signature, cert) = match attestation { - (Some((key, cert)), _aaguid) => { - info!("aaguid: {}", hex_str!(&_aaguid)); - ( - syscall!( - self.trussed.sign(Mechanism::P256, - key, - &commitment, - SignatureSerialization::Asn1Der - )).signature.to_bytes().unwrap(), - cert - ) - }, - _ => { - info!("Not provisioned with attestation key!"); - return Err(U2fError::KeyReferenceNotFound); - } - }; - - - Ok(U2fResponse::Register(ctap1::RegisterResponse::new( - 0x05, - &cose_key, - &credential_id.0, - signature, - &cert, - ))) - } - U2fCommand::Authenticate(auth) => { - - let cred = Credential::try_from_bytes(self, &auth.app_id, &auth.key_handle); - - let user_presence_byte = match auth.control_byte { - ctap1::ControlByte::CheckOnly => { - // if the control byte is set to 0x07 by the FIDO Client, - // the U2F token is supposed to simply check whether the - // provided key handle was originally created by this token - return if cred.is_ok() { - Err(U2fError::ConditionsOfUseNotSatisfied) - } else { - Err(U2fError::IncorrectDataParameter) - }; - }, - ctap1::ControlByte::EnforceUserPresenceAndSign => { - self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) - .map_err(|_| U2fError::ConditionsOfUseNotSatisfied)?; - 0x01 - }, - ctap1::ControlByte::DontEnforceUserPresenceAndSign => 0x00, - }; - - let cred = cred.map_err(|_| U2fError::IncorrectDataParameter)?; - - let key = match &cred.key { - Key::WrappedKey(bytes) => { - let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed) - .map_err(|_| U2fError::IncorrectDataParameter)?; - let key_result = syscall!(self.trussed.unwrap_key_chacha8poly1305( - wrapping_key, - bytes, - b"", - Location::Volatile, - )).key; - match key_result { - Some(key) => { - info!("loaded u2f key!"); - key - } - None => { - info!("issue with unwrapping credential id key"); - return Err(U2fError::IncorrectDataParameter); - } - } - } - _ => return Err(U2fError::IncorrectDataParameter), - }; - - if cred.algorithm != -7 { - info!("Unexpected mechanism for u2f"); - return Err(U2fError::IncorrectDataParameter); - } - - let sig_count = self.state.persistent.timestamp(&mut self.trussed). - map_err(|_| U2fError::UnspecifiedNonpersistentExecutionError)?; - - commitment.extend_from_slice(&auth.app_id).unwrap(); - commitment.push(user_presence_byte).unwrap(); - commitment.extend_from_slice(&sig_count.to_be_bytes()).unwrap(); - commitment.extend_from_slice(&auth.challenge).unwrap(); - - let signature = syscall!( - self.trussed.sign(Mechanism::P256, - key, - &commitment, - SignatureSerialization::Asn1Der - )).signature.to_bytes().unwrap(); - - Ok(U2fResponse::Authenticate(ctap1::AuthenticateResponse::new( - user_presence_byte, - sig_count, - signature, - ))) - - } - U2fCommand::Version => { - // "U2F_V2" - Ok(U2fResponse::Version([0x55, 0x32, 0x46, 0x5f, 0x56, 0x32])) - } - } - - } - - pub fn call(&mut self, request: &Request) -> Result { - // if let Some(request) = self.interchange.take_request() { - // debug!("request: {:?}", &request); - self.state.persistent.load_if_not_initialised(&mut self.trussed); - - match request { - Request::Ctap2(request) => { - match request { - - // 0x4 - ctap2::Request::GetInfo => { - debug!("GI"); - let response = self.get_info(); - Ok(Response::Ctap2(ctap2::Response::GetInfo(response))) - } - - // 0x2 - ctap2::Request::MakeCredential(parameters) => { - debug!("MC request"); - let response = self.make_credential(¶meters); - match response { - Ok(response) => Ok(Response::Ctap2(ctap2::Response::MakeCredential(response))), - Err(error) => Err(error) - } - } - - // 0x1 - ctap2::Request::GetAssertion(parameters) => { - debug!("GA request"); - let response = self.get_assertion(¶meters); - match response { - Ok(response) => Ok(Response::Ctap2(ctap2::Response::GetAssertion(response))), - Err(error) => Err(error) - } - } - - // 0x8 - ctap2::Request::GetNextAssertion => { - debug!("GNA request"); - let response = self.get_next_assertion(); - match response { - Ok(response) => Ok(Response::Ctap2(ctap2::Response::GetNextAssertion(response))), - Err(error) => Err(error) - } - } - - // 0x7 - ctap2::Request::Reset => { - debug!("Reset request"); - let response = self.reset(); - match response { - Ok(()) => Ok(Response::Ctap2(ctap2::Response::Reset)), - Err(error) => Err(error) - } - } - - - // 0x6 - ctap2::Request::ClientPin(parameters) => { - debug!("CP request"); - let response = self.client_pin(¶meters); - match response { - Ok(response) => Ok(Response::Ctap2(ctap2::Response::ClientPin(response))), - Err(error) => Err(error) - } - } - - // 0xA - ctap2::Request::CredentialManagement(parameters) => { - debug!("CM request"); - let response = self.credential_management(¶meters); - match response { - Ok(response) => { - // let mut buf = [0u8; 512]; - // info!("{:?}", ctap_types::serde::cbor_serialize(&response, &mut buf)); - Ok(Response::Ctap2(ctap2::Response::CredentialManagement(response))) - } - Err(error) => Err(error) - } - } - - - ctap2::Request::Vendor(op) => { - debug!("Vendor request"); - let response = self.vendor(*op); - match response { - Ok(()) => Ok(Response::Ctap2(ctap2::Response::Vendor)), - Err(error) => Err(error) - } - } - - // _ => { - // // debug!("not implemented: {:?}", &request); - // debug!("request not implemented"); - // self.interchange.respond(Err(Error::InvalidCommand)).expect("internal error"); - // } - } - } - Request::Ctap1(_request) => { - debug!("ctap1 not implemented: {:?}", &_request); - Err(Error::InvalidCommand) - } - } - // } - } - - fn client_pin(&mut self, parameters: &ctap2::client_pin::Parameters) -> Result { - use ctap2::client_pin::PinV1Subcommand as Subcommand; - debug!("processing CP"); - // info!("{:?}", parameters); - - if parameters.pin_protocol != 1{ - return Err(Error::InvalidParameter); - } - - Ok(match parameters.sub_command { - - Subcommand::GetRetries => { - debug!("processing CP.GR"); - - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: Some(self.state.persistent.retries()), - } - } - - Subcommand::GetKeyAgreement => { - debug!("processing CP.GKA"); - - let private_key = self.state.runtime.key_agreement_key(&mut self.trussed); - let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; - let serialized_cose_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, public_key.clone(), KeySerialization::EcdhEsHkdf256)).serialized_key; - let cose_key = trussed::cbor_deserialize(&serialized_cose_key).unwrap(); - - syscall!(self.trussed.delete(public_key)); - - ctap2::client_pin::Response { - key_agreement: cose_key, - pin_token: None, - retries: None, - } - } - - Subcommand::SetPin => { - debug!("processing CP.SP"); - // 1. check mandatory parameters - let platform_kek = match parameters.key_agreement.as_ref() { - Some(key) => key, - None => { return Err(Error::MissingParameter); } - }; - let new_pin_enc = match parameters.new_pin_enc.as_ref() { - Some(pin) => pin, - None => { return Err(Error::MissingParameter); } - }; - let pin_auth = match parameters.pin_auth.as_ref() { - Some(auth) => auth, - None => { return Err(Error::MissingParameter); } - }; - - // 2. is pin already set - if self.state.persistent.pin_is_set() { - return Err(Error::NotAllowed); - } - - // 3. generate shared secret - let shared_secret = self.state.runtime.generate_shared_secret(&mut self.trussed, platform_kek)?; - - // TODO: there are moar early returns!! - // - implement Drop? - // - do garbage collection outside of this? - - // 4. verify pinAuth - self.verify_pin_auth(shared_secret, new_pin_enc, pin_auth)?; - - // 5. decrypt and verify new PIN - let new_pin = self.decrypt_pin_check_length(shared_secret, new_pin_enc)?; - - syscall!(self.trussed.delete(shared_secret)); - - // 6. store LEFT(SHA-256(newPin), 16), set retries to 8 - self.hash_store_pin(&new_pin)?; - self.state.reset_retries(&mut self.trussed).map_err(|_| Error::Other)?; - - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: None, - } - } - - Subcommand::ChangePin => { - debug!("processing CP.CP"); - - // 1. check mandatory parameters - let platform_kek = match parameters.key_agreement.as_ref() { - Some(key) => key, - None => { return Err(Error::MissingParameter); } - }; - let pin_hash_enc = match parameters.pin_hash_enc.as_ref() { - Some(hash) => hash, - None => { return Err(Error::MissingParameter); } - }; - let new_pin_enc = match parameters.new_pin_enc.as_ref() { - Some(pin) => pin, - None => { return Err(Error::MissingParameter); } - }; - let pin_auth = match parameters.pin_auth.as_ref() { - Some(auth) => auth, - None => { return Err(Error::MissingParameter); } - }; - - // 2. fail if no retries left - self.state.pin_blocked()?; - - // 3. generate shared secret - let shared_secret = self.state.runtime.generate_shared_secret(&mut self.trussed, platform_kek)?; - - // 4. verify pinAuth - let mut data = MediumData::new(); - data.extend_from_slice(new_pin_enc).map_err(|_| Error::InvalidParameter)?; - data.extend_from_slice(pin_hash_enc).map_err(|_| Error::InvalidParameter)?; - self.verify_pin_auth(shared_secret, &data, pin_auth)?; - - // 5. decrement retries - self.state.decrement_retries(&mut self.trussed)?; - - // 6. decrypt pinHashEnc, compare with stored - self.decrypt_pin_hash_and_maybe_escalate(shared_secret, &pin_hash_enc)?; - - // 7. reset retries - self.state.reset_retries(&mut self.trussed)?; - - // 8. decrypt and verify new PIN - let new_pin = self.decrypt_pin_check_length(shared_secret, new_pin_enc)?; - - syscall!(self.trussed.delete(shared_secret)); - - // 9. store hashed PIN - self.hash_store_pin(&new_pin)?; - - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: None, - } - } - - Subcommand::GetPinToken => { - debug!("processing CP.GPT"); - - // 1. check mandatory parameters - let platform_kek = match parameters.key_agreement.as_ref() { - Some(key) => key, - None => { return Err(Error::MissingParameter); } - }; - let pin_hash_enc = match parameters.pin_hash_enc.as_ref() { - Some(hash) => hash, - None => { return Err(Error::MissingParameter); } - }; - - // 2. fail if no retries left - self.state.pin_blocked()?; - - // 3. generate shared secret - let shared_secret = self.state.runtime.generate_shared_secret(&mut self.trussed, platform_kek)?; - - // 4. decrement retires - self.state.decrement_retries(&mut self.trussed)?; - - // 5. decrypt and verify pinHashEnc - self.decrypt_pin_hash_and_maybe_escalate(shared_secret, &pin_hash_enc)?; - - // 6. reset retries - self.state.reset_retries(&mut self.trussed)?; - - // 7. return encrypted pinToken - let pin_token = self.state.runtime.pin_token(&mut self.trussed); - debug!("wrapping pin token"); - // info!("exists? {}", syscall!(self.trussed.exists(shared_secret)).exists); - let pin_token_enc = syscall!(self.trussed.wrap_key_aes256cbc(shared_secret, pin_token)).wrapped_key; - - syscall!(self.trussed.delete(shared_secret)); - - // ble... - if pin_token_enc.len() != 16 { - return Err(Error::Other); - } - let pin_token_enc_32 = Bytes::from_slice(&pin_token_enc).unwrap(); - - ctap2::client_pin::Response { - key_agreement: None, - pin_token: Some(pin_token_enc_32), - retries: None, - } - } - - _ => { - // todo!("not implemented yet") - return Err(Error::InvalidParameter); - } - }) - } - - fn decrypt_pin_hash_and_maybe_escalate(&mut self, shared_secret: KeyId, pin_hash_enc: &Bytes<64>) - -> Result<()> - { - let pin_hash = syscall!(self.trussed.decrypt_aes256cbc( - shared_secret, pin_hash_enc)).plaintext.ok_or(Error::Other)?; - - let stored_pin_hash = match self.state.persistent.pin_hash() { - Some(hash) => hash, - None => { return Err(Error::PinNotSet); } - }; - - if &pin_hash != &stored_pin_hash { - // I) generate new KEK - self.state.runtime.rotate_key_agreement_key(&mut self.trussed); - if self.state.persistent.retries() == 0 { - return Err(Error::PinBlocked); - } - if self.state.persistent.pin_blocked() { - return Err(Error::PinAuthBlocked); - } - return Err(Error::PinInvalid); - } - - Ok(()) - } - - fn hash_store_pin(&mut self, pin: &Message) -> Result<()> { - let pin_hash_32 = syscall!(self.trussed.hash_sha256(&pin)).hash; - let pin_hash: [u8; 16] = pin_hash_32[..16].try_into().unwrap(); - self.state.persistent.set_pin_hash(&mut self.trussed, pin_hash).unwrap(); - - Ok(()) - } - - fn decrypt_pin_check_length(&mut self, shared_secret: KeyId, pin_enc: &[u8]) -> Result { - // pin is expected to be filled with null bytes to length at least 64 - if pin_enc.len() < 64 { - // correct error? - return Err(Error::PinPolicyViolation); - } - - let mut pin = syscall!(self.trussed.decrypt_aes256cbc( - shared_secret, &pin_enc)).plaintext.ok_or(Error::Other)?; - - // // temp - // let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len()); - // info!("pin.len() = {}, pin_length = {}, = {:?}", - // pin.len(), pin_length, &pin); - // chop off null bytes - let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len()); - if pin_length < 4 || pin_length >= 64 { - return Err(Error::PinPolicyViolation); - } - - pin.resize_default(pin_length).unwrap(); - - Ok(pin) - } - - - // fn verify_pin(&mut self, pin_auth: &Bytes<16>, client_data_hash: &Bytes<32>) -> bool { - fn verify_pin(&mut self, pin_auth: &[u8; 16], data: &[u8]) -> Result<()> { - let key = self.state.runtime.pin_token(&mut self.trussed); - let tag = syscall!(self.trussed.sign_hmacsha256(key, data)).signature; - if pin_auth == &tag[..16] { - Ok(()) - } else { - Err(Error::PinAuthInvalid) - } - } - - fn verify_pin_auth(&mut self, shared_secret: KeyId, data: &[u8], pin_auth: &Bytes<16>) - -> Result<()> - { - let expected_pin_auth = syscall!(self.trussed.sign_hmacsha256(shared_secret, data)).signature; - - if &expected_pin_auth[..16] == &pin_auth[..] { - Ok(()) - } else { - Err(Error::PinAuthInvalid) - } - } - - // fn verify_pin_auth_using_token(&mut self, data: &[u8], pin_auth: &Bytes<16>) - fn verify_pin_auth_using_token( - &mut self, - parameters: &ctap2::credential_management::Parameters - ) -> Result<()> { - - // info!("CM params: {:?}", parameters); - use ctap2::credential_management::Subcommand; - match parameters.sub_command { - // are we Haskell yet lol - sub_command @ Subcommand::GetCredsMetadata | - sub_command @ Subcommand::EnumerateRpsBegin | - sub_command @ Subcommand::EnumerateCredentialsBegin | - sub_command @ Subcommand::DeleteCredential => { - - // check pinProtocol - let pin_protocol = parameters - // .sub_command_params.as_ref().ok_or(Error::MissingParameter)? - .pin_protocol.ok_or(Error::MissingParameter)?; - if pin_protocol != 1 { - return Err(Error::InvalidParameter); - } - - // check pinAuth - let pin_token = self.state.runtime.pin_token(&mut self.trussed); - let mut data: Bytes = - Bytes::from_slice(&[sub_command as u8]).unwrap(); - let len = 1 + match sub_command { - Subcommand::EnumerateCredentialsBegin | - Subcommand::DeleteCredential => { - data.resize_to_capacity(); - // ble, need to reserialize - ctap_types::serde::cbor_serialize( - ¶meters.sub_command_params - .as_ref() - .ok_or(Error::MissingParameter)?, - &mut data[1..], - ).map_err(|_| Error::LimitExceeded)?.len() - } - _ => 0, - }; - - // info!("input to hmacsha256: {:?}", &data[..len]); - let expected_pin_auth = syscall!(self.trussed.sign_hmacsha256( - pin_token, - &data[..len], - )).signature; - - let pin_auth = parameters - .pin_auth.as_ref().ok_or(Error::MissingParameter)?; - - if &expected_pin_auth[..16] == &pin_auth[..] { - info!("passed pinauth"); - Ok(()) - } else { - info!("failed pinauth!"); - self.state.decrement_retries(&mut self.trussed)?; - let maybe_blocked = self.state.pin_blocked(); - if maybe_blocked.is_err() { - info!("blocked"); - maybe_blocked - } else { - info!("pinAuthInvalid"); - Err(Error::PinAuthInvalid) - } - - } - } - - _ => Ok(()), - } - } - - /// Returns whether UV was performed. - fn pin_prechecks(&mut self, - options: &Option, - pin_auth: &Option, - pin_protocol: &Option, - data: &[u8], - ) - -> Result - { - // 1. pinAuth zero length -> wait for user touch, then - // return PinNotSet if not set, PinInvalid if set - // - // the idea is for multi-authnr scenario where platform - // wants to enforce PIN and needs to figure out which authnrs support PIN - if let Some(pin_auth) = pin_auth.as_ref() { - if pin_auth.len() == 0 { - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; - if !self.state.persistent.pin_is_set() { - return Err(Error::PinNotSet); - } else { - return Err(Error::PinAuthInvalid); - } - } - } - - // 2. check PIN protocol is 1 if pinAuth was sent - if let Some(ref _pin_auth) = pin_auth { - if let Some(1) = pin_protocol { - } else { - return Err(Error::PinAuthInvalid); - } - } - - // 3. if no PIN is set (we have no other form of UV), - // and platform sent `uv` or `pinAuth`, return InvalidOption - if !self.state.persistent.pin_is_set() { - if let Some(ref options) = &options { - if Some(true) == options.uv { - return Err(Error::InvalidOption); - } - } - if pin_auth.is_some() { - return Err(Error::InvalidOption); - } - } - - // 4. If authenticator is protected by som form of user verification, do it - // - // TODO: Should we should fail if `uv` is passed? - // Current thinking: no - if self.state.persistent.pin_is_set() { - - // let mut uv_performed = false; - if let Some(ref pin_auth) = pin_auth { - if pin_auth.len() != 16 { - return Err(Error::InvalidParameter); - } - // seems a bit redundant to check here in light of 2. - // I guess the CTAP spec writers aren't implementers :D - if let Some(1) = pin_protocol { - // 5. if pinAuth is present and pinProtocol = 1, verify - // success --> set uv = 1 - // error --> PinAuthInvalid - self.verify_pin( - // unwrap panic ruled out above - pin_auth.as_slice().try_into().unwrap(), - data, - )?; - - return Ok(true); - - } else { - // 7. pinAuth present + pinProtocol != 1 --> error PinAuthInvalid - return Err(Error::PinAuthInvalid); - } - - } else { - // 6. pinAuth not present + clientPin set --> error PinRequired - if self.state.persistent.pin_is_set() { - return Err(Error::PinRequired); - } - } - } - - Ok(false) - } - - /// If allow_list is some, select the first one that is usable, - /// and return some(it). - /// - /// If allow_list is none, pull applicable credentials, store - /// in state's credential_heap, and return none - #[inline(never)] - fn locate_credentials( - &mut self, rp_id_hash: &Bytes32, - allow_list: &Option, - uv_performed: bool, - ) - -> Result<()> - { - // validate allowList - let mut allow_list_len = 0; - let allowed_credentials = if let Some(allow_list) = allow_list.as_ref() { - allow_list_len = allow_list.len(); - allow_list.into_iter() - // discard not properly serialized encrypted credentials - .filter_map(|credential_descriptor| { - info!( - "GA try from cred id: {}", - hex_str!(&credential_descriptor.id), - ); - let cred_maybe = Credential::try_from( - self, rp_id_hash, credential_descriptor) - .ok(); - info!("cred_maybe: {:?}", &cred_maybe); - cred_maybe - } ) - .collect() - } else { - CredentialList::new() - }; - - let mut min_heap = MinCredentialHeap::new(); - - let allowed_credentials_passed = allowed_credentials.len() > 0; - - if allowed_credentials_passed { - // "If an allowList is present and is non-empty, - // locate all denoted credentials present on this authenticator - // and bound to the specified rpId." - debug!("allowedList passed with {} creds", allowed_credentials.len()); - let mut rk_count = 0; - let mut applicable_credentials: CredentialList = allowed_credentials - .into_iter() - .filter(|credential| match credential.key.clone() { - // TODO: should check if wrapped key is valid AEAD - // On the other hand, we already decrypted a valid AEAD - Key::WrappedKey(_) => true, - Key::ResidentKey(key) => { - debug!("checking if ResidentKey {:?} exists", &key); - let exists = match credential.algorithm { - -7 => syscall!(self.trussed.exists(Mechanism::P256, key)).exists, - -8 => syscall!(self.trussed.exists(Mechanism::Ed255, key)).exists, - -9 => { - let exists = syscall!(self.trussed.exists(Mechanism::Totp, key)).exists; - info!("found it"); - exists - } - _ => false, - }; - if exists { - rk_count = rk_count + 1; - } - exists - } - }) - .filter(|credential| { - use credential::CredentialProtectionPolicy as Policy; - debug!("CredentialProtectionPolicy {:?}", &credential.cred_protect); - match credential.cred_protect { - None | Some(Policy::Optional) => true, - Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, - Some(Policy::Required) => uv_performed, - - } - }) - .collect(); - while applicable_credentials.len() > 0 { - // Store all other applicable credentials in volatile storage and add to our - // credential heap. - let credential = applicable_credentials.pop().unwrap(); - let serialized = credential.serialize()?; - - let mut path = [b'0', b'0']; - format_hex(&[applicable_credentials.len() as u8], &mut path); - let path = PathBuf::from(&path); - // let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; - // let id = credential.id_using_hash(&mut self.trussed, kek, rp_id_hash)?; - // let credential_id_hash = self.hash(&id.0.as_ref()); - - // let path = rk_path(&rp_id_hash, &credential_id_hash); - let timestamp_path = TimestampPath { - timestamp: credential.creation_time, - path: path.clone(), - location: Location::Volatile, - }; - - - info!("added volatile cred: {:?}", ×tamp_path); - info!("{}",hex_str!(&serialized)); - - - try_syscall!(self.trussed.write_file( - Location::Volatile, - path.clone(), - serialized, - None, - )).map_err(|_| { - Error::KeyStoreFull - })?; - - // attempt to read back - // let data = syscall!(self.trussed.read_file( - // Location::Volatile, - // timestamp_path.path.clone(), - // )).data; - // crate::Credential::deserialize(&data).unwrap(); - - - if min_heap.capacity() > min_heap.len() { - min_heap.push(timestamp_path).map_err(drop).unwrap(); - } else { - if timestamp_path.timestamp > min_heap.peek().unwrap().timestamp { - min_heap.pop().unwrap(); - min_heap.push(timestamp_path).map_err(drop).unwrap(); - } - } - // If more than one credential was located in step 1 and allowList is present and not empty, - // select any applicable credential and proceed to step 12. Otherwise, order the credentials - // by the time when they were created in reverse order. - // The first credential is the most recent credential that was created. - if rk_count > 1 { - break - } - - } - } else if allow_list_len == 0 { - // If an allowList is not present, - // locate all credentials that are present on this authenticator - // and bound to the specified rpId; sorted by reverse creation time - - // let rp_id_hash = self.hash(rp_id.as_ref()); - - // - // So here's the idea: - // - // - credentials can be pretty big - // - we declare N := MAX_CREDENTIAL_COUNT_IN_LIST in GetInfo - // - potentially there are more RKs for a given RP (a bit academic ofc) - // - // - first, we use a min-heap to keep only the topN credentials: - // if our "next" one is larger/later than the min of the heap, - // pop this min and push ours - // - // - then, we use a max-heap to sort the remaining <=N credentials - // - these then go into a CredentialList - // - (we don't need to keep that around even) - // - debug!("no allowedList passed"); - - // let mut credentials = CredentialList::new(); - - let data = syscall!(self.trussed.read_dir_files_first( - Location::Internal, - rp_rk_dir(&rp_id_hash), - None, - )).data; - - let data = match data { - Some(data) => data, - None => return Err(Error::NoCredentials), - }; - - let credential = Credential::deserialize(&data).unwrap(); - - use credential::CredentialProtectionPolicy as Policy; - let keep = match credential.cred_protect { - None | Some(Policy::Optional) => true, - Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, - Some(Policy::Required) => uv_performed, - }; - - let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; - - if keep { - let id = credential.id(&mut self.trussed, kek, Some(rp_id_hash))?; - let credential_id_hash = self.hash(&id.0.as_ref()); - - let timestamp_path = TimestampPath { - timestamp: credential.creation_time, - path: rk_path(&rp_id_hash, &credential_id_hash), - location: Location::Internal, - }; - - min_heap.push(timestamp_path).map_err(drop).unwrap(); - // info!("first: {:?}", &self.hash(&id.0)); - } - - loop { - let data = syscall!(self.trussed.read_dir_files_next()).data; - let data = match data { - Some(data) => data, - None => break, - }; - - let credential = Credential::deserialize(&data).unwrap(); - - let keep = match credential.cred_protect { - None | Some(Policy::Optional) => true, - Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, - Some(Policy::Required) => uv_performed, - }; - - if keep { - - let id = credential.id(&mut self.trussed, kek, Some(rp_id_hash))?; - let credential_id_hash = self.hash(&id.0.as_ref()); - - let timestamp_path = TimestampPath { - timestamp: credential.creation_time, - path: rk_path(&rp_id_hash, &credential_id_hash), - location: Location::Internal, - }; - - if min_heap.capacity() > min_heap.len() { - min_heap.push(timestamp_path).map_err(drop).unwrap(); - } else { - if timestamp_path.timestamp > min_heap.peek().unwrap().timestamp { - min_heap.pop().unwrap(); - min_heap.push(timestamp_path).map_err(drop).unwrap(); - } - } - } - } - - }; - - // "If no applicable credentials were found, return CTAP2_ERR_NO_CREDENTIALS" - if min_heap.is_empty() { - return Err(Error::NoCredentials); - } - - // now sort them - self.state.runtime.free_credential_heap(&mut self.trussed); - let max_heap = self.state.runtime.credential_heap(); - while !min_heap.is_empty() { - max_heap.push(min_heap.pop().unwrap()).map_err(drop).unwrap(); - } - - Ok(()) - } - - fn get_next_assertion(&mut self) -> Result { - // 1./2. don't remember / don't have left any credentials - if self.state.runtime.credential_heap().is_empty() { - return Err(Error::NotAllowed); - } - - // 3. previous GA/GNA >30s ago -> discard stat - // this is optional over NFC - if false { - self.state.runtime.free_credential_heap(&mut self.trussed); - return Err(Error::NotAllowed); - } - - // 4. select credential - // let data = syscall!(self.trussed.read_file( - // timestamp_hash.location, - // timestamp_hash.path, - // )).data; - let credential = self.state.runtime.pop_credential_from_heap(&mut self.trussed); - // Credential::deserialize(&data).unwrap(); - - // 5. suppress PII if no UV was performed in original GA - - // 6. sign - // 7. reset timer - // 8. increment credential counter (not applicable) - - self.assert_with_credential(None, credential) - } - - fn credential_management(&mut self, parameters: &ctap2::credential_management::Parameters) - -> Result { - - use ctap2::credential_management::Subcommand; - use crate::credential_management as cm; - - // TODO: I see "failed pinauth" output, but then still continuation... - self.verify_pin_auth_using_token(¶meters)?; - - let mut cred_mgmt = cm::CredentialManagement::new(self); - let sub_parameters = ¶meters.sub_command_params; - match parameters.sub_command { - - // 0x1 - Subcommand::GetCredsMetadata => - cred_mgmt.get_creds_metadata(), - - // 0x2 - Subcommand::EnumerateRpsBegin => - cred_mgmt.first_relying_party(), - - // 0x3 - Subcommand::EnumerateRpsGetNextRp => - cred_mgmt.next_relying_party(), - - // 0x4 - Subcommand::EnumerateCredentialsBegin => { - let sub_parameters = sub_parameters.as_ref() - .ok_or(Error::MissingParameter)?; - - cred_mgmt.first_credential( - sub_parameters - .rp_id_hash.as_ref() - .ok_or(Error::MissingParameter)?, - ) - } - - // 0x5 - Subcommand::EnumerateCredentialsGetNextCredential => - cred_mgmt.next_credential(), - - // 0x6 - Subcommand::DeleteCredential => { - let sub_parameters = sub_parameters.as_ref() - .ok_or(Error::MissingParameter)?; - - cred_mgmt.delete_credential(sub_parameters - .credential_id.as_ref() - .ok_or(Error::MissingParameter)?, - ) - } - - // _ => todo!("not implemented yet"), - } - } - - fn get_assertion(&mut self, parameters: &ctap2::get_assertion::Parameters) -> Result { - - let rp_id_hash = self.hash(¶meters.rp_id.as_ref()); - - // 1-4. - let uv_performed = match self.pin_prechecks( - ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, - ¶meters.client_data_hash.as_ref(), - ) { - Ok(b) => b, - Err(Error::PinRequired) => { - // UV is optional for get_assertion - false - } - Err(err) => return Err(err), - }; - - // 5. Locate eligible credentials - // - // Note: If allowList is passed, credential is Some(credential) - // If no allowList is passed, credential is None and the retrieved credentials - // are stored in state.runtime.credential_heap - self.locate_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed)?; - - let credential = self.state.runtime.pop_credential_from_heap(&mut self.trussed); - let num_credentials = match self.state.runtime.credential_heap().len() { - 0 => None, - n => Some(n as u32 + 1), - }; - info!("FIRST cred: {:?}",&credential); - info!("FIRST NUM creds: {:?}",num_credentials); - - // NB: misleading, if we have "1" we return "None" - let human_num_credentials = match num_credentials { - Some(n) => n, - None => 1, - }; - info!("found {:?} applicable credentials", human_num_credentials); - - // 6. process any options present - - // UP occurs by default, but option could specify not to. - let do_up = if parameters.options.is_some() { - parameters.options.as_ref().unwrap().up.unwrap_or(true) - } else { - true - }; - - // 7. collect user presence - let up_performed = if do_up { - info!("asking for up"); - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; - true - } else { - info!("not asking for up"); - false - }; - - let multiple_credentials = human_num_credentials > 1; - self.state.runtime.active_get_assertion = Some(state::ActiveGetAssertionData { - rp_id_hash: { - let mut buf = [0u8; 32]; - buf.copy_from_slice(&rp_id_hash); - buf - }, - client_data_hash: { - let mut buf = [0u8; 32]; - buf.copy_from_slice(¶meters.client_data_hash); - buf - }, - uv_performed, - up_performed, - multiple_credentials, - extensions: parameters.extensions.clone(), - }); - - self.assert_with_credential(num_credentials, credential) - } - - #[inline(never)] - fn process_assertion_extensions(&mut self, - get_assertion_state: &state::ActiveGetAssertionData, - extensions: &ctap2::get_assertion::ExtensionsInput, - _credential: &Credential, - credential_key: KeyId, - ) -> Result> { - if let Some(hmac_secret) = &extensions.hmac_secret { - if let Some(pin_protocol) = hmac_secret.pin_protocol { - if pin_protocol != 1 { - return Err(Error::InvalidParameter); - } - } - - // We derive credRandom as an hmac of the existing private key. - // UV is used as input data since credRandom should depend UV - // i.e. credRandom = HMAC(private_key, uv) - let cred_random = syscall!(self.trussed.derive_key( - Mechanism::HmacSha256, - credential_key, - Some(Bytes::from_slice(&[get_assertion_state.uv_performed as u8]).unwrap()), - trussed::types::StorageAttributes::new().set_persistence(Location::Volatile) - )).key; - - // Verify the auth tag, which uses the same process as the pinAuth - let kek = self.state.runtime.generate_shared_secret(&mut self.trussed, &hmac_secret.key_agreement)?; - self.verify_pin_auth(kek, &hmac_secret.salt_enc, &hmac_secret.salt_auth).map_err(|_| Error::ExtensionFirst)?; - - if hmac_secret.salt_enc.len() != 32 && hmac_secret.salt_enc.len() != 64 { - return Err(Error::InvalidLength); - } - - // decrypt input salt_enc to get salt1 or (salt1 || salt2) - let salts = syscall!( - self.trussed.decrypt(Mechanism::Aes256Cbc, kek, &hmac_secret.salt_enc, b"", b"", b"") - ).plaintext.ok_or(Error::InvalidOption)?; - - let mut salt_output: Bytes<64> = Bytes::new(); - - // output1 = hmac_sha256(credRandom, salt1) - let output1 = syscall!( - self.trussed.sign_hmacsha256(cred_random, &salts[0..32]) - ).signature; - - salt_output.extend_from_slice(&output1).unwrap(); - - if salts.len() == 64 { - // output2 = hmac_sha256(credRandom, salt2) - let output2 = syscall!( - self.trussed.sign_hmacsha256(cred_random, &salts[32..64]) - ).signature; - - salt_output.extend_from_slice(&output2).unwrap(); - } - - syscall!(self.trussed.delete(cred_random)); - - // output_enc = aes256-cbc(sharedSecret, IV=0, output1 || output2) - let output_enc = syscall!( - self.trussed.encrypt(Mechanism::Aes256Cbc, kek, &salt_output, b"", None) - ).ciphertext; - - Ok(Some(ctap2::get_assertion::ExtensionsOutput { - hmac_secret: Some(Bytes::from_slice(&output_enc).unwrap()) - })) - - } else { - Ok(None) - } - - } - - - fn assert_with_credential(&mut self, num_credentials: Option, credential: Credential) - -> Result - { - let data = self.state.runtime.active_get_assertion.clone().unwrap(); - let rp_id_hash = Bytes::from_slice(&data.rp_id_hash).unwrap(); - - let (key, is_rk) = match credential.key.clone() { - Key::ResidentKey(key) => (key, true), - Key::WrappedKey(bytes) => { - let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - // info!("unwrapping {:?} with wrapping key {:?}", &bytes, &wrapping_key); - let key_result = syscall!(self.trussed.unwrap_key_chacha8poly1305( - wrapping_key, - &bytes, - b"", - // &rp_id_hash, - Location::Volatile, - )).key; - // debug!("key result: {:?}", &key_result); - info!("key result"); - match key_result { - Some(key) => (key, false), - None => { return Err(Error::Other); } - } - } - }; - - // 8. process any extensions present - let extensions_output = if let Some(extensions) = &data.extensions { - self.process_assertion_extensions(&data, &extensions, &credential, key)? - } else { - None - }; - - // 9./10. sign clientDataHash || authData with "first" credential - - // info!("signing with credential {:?}", &credential); - let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; - let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; - - use ctap2::AuthenticatorDataFlags as Flags; - - let sig_count = self.state.persistent.timestamp(&mut self.trussed)?; - - let authenticator_data = ctap2::get_assertion::AuthenticatorData { - rp_id_hash: rp_id_hash, - - flags: { - let mut flags = Flags::EMPTY; - if data.up_performed { - flags |= Flags::USER_PRESENCE; - } - if data.uv_performed { - flags |= Flags::USER_VERIFIED; - } - if extensions_output.is_some() { - flags |= Flags::EXTENSION_DATA; - } - flags - }, - - sign_count: sig_count, - attested_credential_data: None, - extensions: extensions_output - }; - - let serialized_auth_data = authenticator_data.serialize(); - - let mut commitment = Bytes::<1024>::new(); - commitment.extend_from_slice(&serialized_auth_data).map_err(|_| Error::Other)?; - commitment.extend_from_slice(&data.client_data_hash).map_err(|_| Error::Other)?; - - let (mechanism, serialization) = match credential.algorithm { - -7 => (Mechanism::P256, SignatureSerialization::Asn1Der), - -8 => (Mechanism::Ed255, SignatureSerialization::Raw), - -9 => (Mechanism::Totp, SignatureSerialization::Raw), - _ => { return Err(Error::Other); } - }; - - debug!("signing with {:?}, {:?}", &mechanism, &serialization); - let signature = match mechanism { - Mechanism::Totp => { - let timestamp = u64::from_le_bytes(data.client_data_hash[..8].try_into().unwrap()); - info!("TOTP with timestamp {:?}", ×tamp); - syscall!(self.trussed.sign_totp(key, timestamp)).signature.to_bytes().unwrap() + Request::Ctap2(request) => { + Ok(Response::Ctap2(self.call_ctap2(request)?)) } - _ => syscall!(self.trussed.sign(mechanism, key.clone(), &commitment, serialization)).signature - .to_bytes().unwrap(), - }; - - if !is_rk { - syscall!(self.trussed.delete(key)); - } - - let mut response = ctap2::get_assertion::Response { - credential: Some(credential_id.into()), - auth_data: Bytes::from_slice(&serialized_auth_data).map_err(|_| Error::Other)?, - signature, - user: None, - number_of_credentials: num_credentials, - }; - - if is_rk { - let mut user = credential.user.clone(); - // User identifiable information (name, DisplayName, icon) MUST not - // be returned if user verification is not done by the authenticator. - // For single account per RP case, authenticator returns "id" field. - if !data.uv_performed || !data.multiple_credentials { - user.icon = None; - user.name = None; - user.display_name = None; + Request::Ctap1(_request) => { + // ctap_types::authenticator::ctap1::Request redefineds + // the already existing ctap_types::ctap1::Command + // + // need to merge + todo!(); + // Ok(Response::Ctap1(self.call_u2f(request)?)) } - response.user = Some(user); } - - Ok(response) - } - - fn vendor(&mut self, op: VendorOperation) -> Result<()> { - info!("hello VO {:?}", &op); - match op.into() { - 0x79 => syscall!(self.trussed.debug_dump_store()), - _ => return Err(Error::InvalidCommand), - }; - - Ok(()) - } - - fn reset(&mut self) -> Result<()> { - // 1. >10s after bootup -> NotAllowed - let uptime = syscall!(self.trussed.uptime()).uptime; - if uptime.as_secs() > 10 { - #[cfg(not(feature = "disable-reset-time-window"))] - return Err(Error::NotAllowed); - } - // 2. check for user presence - // denied -> OperationDenied - // timeout -> UserActionTimeout - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; - - // Delete resident keys - syscall!(self.trussed.delete_all(Location::Internal)); - syscall!(self.trussed.remove_dir_all( - Location::Internal, - PathBuf::from("rk"), - )); - - // b. delete persistent state - self.state.persistent.reset(&mut self.trussed)?; - - // c. Reset runtime state - self.state.runtime.reset(&mut self.trussed); - - Ok(()) - } - - pub fn delete_resident_key_by_user_id( - &mut self, - rp_id_hash: &Bytes32, - user_id: &Bytes<64>, - ) -> Result<()> { - - // Prepare to iterate over all credentials associated to RP. - let rp_path = rp_rk_dir(&rp_id_hash); - let mut entry = syscall!(self.trussed.read_dir_first( - Location::Internal, - rp_path.clone(), - None, - )).entry; - - loop { - info!("this may be an RK: {:?}", &entry); - let rk_path = match entry { - // no more RKs left - // break breaks inner loop here - None => break, - Some(entry) => PathBuf::from(entry.path()), - }; - - info!("checking RK {:?} for userId ", &rk_path); - let credential_data = syscall!(self.trussed.read_file( - Location::Internal, - PathBuf::from(rk_path.clone()), - )).data; - let credential_maybe = Credential::deserialize(&credential_data); - - if let Ok(old_credential) = credential_maybe { - if old_credential.user.id == user_id { - match old_credential.key { - credential::Key::ResidentKey(key) => { - info!(":: deleting resident key"); - syscall!(self.trussed.delete(key)); - } - _ => { - warn!(":: WARNING: unexpected server credential in rk."); - } - } - syscall!(self.trussed.remove_file( - Location::Internal, - PathBuf::from(rk_path), - )); - - info!("Overwriting previous rk tied to this userId."); - break; - } - } else { - warn_now!("WARNING: Could not read RK."); - } - - // prepare for next loop iteration - entry = syscall!(self.trussed.read_dir_next()).entry; - } - - Ok(()) - - } - - pub fn delete_resident_key_by_path( - &mut self, - rk_path: &Path, - ) - -> Result<()> - { - info!("deleting RK {:?}", &rk_path); - let credential_data = syscall!(self.trussed.read_file( - Location::Internal, - PathBuf::from(rk_path), - )).data; - let credential_maybe = Credential::deserialize(&credential_data); - // info!("deleting credential {:?}", &credential); - - - if let Ok(credential) = credential_maybe { - - match credential.key { - credential::Key::ResidentKey(key) => { - info!(":: deleting resident key"); - syscall!(self.trussed.delete(key)); - } - credential::Key::WrappedKey(_) => {} - } - } else { - // If for some reason there becomes a corrupt credential, - // we can still at least orphan the key rather then crash. - info!("Warning! Orpaning a key."); - } - - info!(":: deleting RK file {:?} itself", &rk_path); - syscall!(self.trussed.remove_file( - Location::Internal, - PathBuf::from(rk_path), - )); - - - Ok(()) } fn hash(&mut self, data: &[u8]) -> Bytes<32> { let hash = syscall!(self.trussed.hash_sha256(&data)).hash; hash.to_bytes().expect("hash should fit") } - - fn make_credential(&mut self, parameters: &ctap2::make_credential::Parameters) -> Result { - - let rp_id_hash = self.hash(¶meters.rp.id.as_ref()); - - // 1-4. - if let Some(options) = parameters.options.as_ref() { - // up option is not valid for make_credential - if options.up.is_some() { - return Err(Error::InvalidOption); - } - } - let uv_performed = self.pin_prechecks( - ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, - ¶meters.client_data_hash.as_ref(), - )?; - - // 5. "persist credProtect value for this credential" - // --> seems out of place here, see 9. - - // 6. excludeList present, contains credential ID on this authenticator bound to RP? - // --> wait for UP, error CredentialExcluded - if let Some(exclude_list) = ¶meters.exclude_list { - for descriptor in exclude_list.iter() { - let result = Credential::try_from(self, &rp_id_hash, descriptor); - if let Ok(excluded_cred) = result { - // If UV is not performed, than CredProtectRequired credentials should not be visibile. - if !(excluded_cred.cred_protect == Some(CredentialProtectionPolicy::Required) && !uv_performed) { - info!("Excluded!"); - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; - return Err(Error::CredentialExcluded); - } - } - } - } - - // 7. check pubKeyCredParams algorithm is valid + supported COSE identifier - - let mut algorithm: Option = None; - for param in parameters.pub_key_cred_params.iter() { - match param.alg { - -7 => { if algorithm.is_none() { algorithm = Some(SupportedAlgorithm::P256); }} - -8 => { algorithm = Some(SupportedAlgorithm::Ed25519); } - -9 => { algorithm = Some(SupportedAlgorithm::Totp); } - _ => {} - } - } - let algorithm = match algorithm { - Some(algorithm) => { - info!("algo: {:?}", algorithm as i32); - algorithm - }, - None => { return Err(Error::UnsupportedAlgorithm); } - }; - // debug!("making credential, eddsa = {}", eddsa); - - - // 8. process options; on known but unsupported error UnsupportedOption - - let mut rk_requested = false; - // TODO: why is this unused? - let mut _uv_requested = false; - let _up_requested = true; // can't be toggled - - info!("MC options: {:?}", ¶meters.options); - if let Some(ref options) = ¶meters.options { - if Some(true) == options.rk { - rk_requested = true; - } - if Some(true) == options.uv { - _uv_requested = true; - } - } - - // 9. process extensions - let mut hmac_secret_requested = None; - // let mut cred_protect_requested = CredentialProtectionPolicy::Optional; - let mut cred_protect_requested = None; - if let Some(extensions) = ¶meters.extensions { - - hmac_secret_requested = extensions.hmac_secret; - - if let Some(policy) = &extensions.cred_protect { - cred_protect_requested = Some(CredentialProtectionPolicy::try_from(*policy)?); - } - } - - // debug!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); - - // 10. get UP, if denied error OperationDenied - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; - - // 11. generate credential keypair - let location = match rk_requested { - true => Location::Internal, - false => Location::Volatile, - }; - - let private_key: KeyId; - let public_key: KeyId; - let cose_public_key; - match algorithm { - SupportedAlgorithm::P256 => { - private_key = syscall!(self.trussed.generate_p256_private_key(location)).key; - public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; - cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, public_key.clone(), KeySerialization::Cose - )).serialized_key; - let _success = syscall!(self.trussed.delete(public_key)).success; - info!("deleted public P256 key: {}", _success); - } - SupportedAlgorithm::Ed25519 => { - private_key = syscall!(self.trussed.generate_ed255_private_key(location)).key; - public_key = syscall!(self.trussed.derive_ed255_public_key(private_key, Location::Volatile)).key; - cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::Ed255, public_key.clone(), KeySerialization::Cose - )).serialized_key; - let _success = syscall!(self.trussed.delete(public_key)).success; - info!("deleted public Ed25519 key: {}", _success); - } - SupportedAlgorithm::Totp => { - if parameters.client_data_hash.len() != 32 { - return Err(Error::InvalidParameter); - } - // b'TOTP---W\x0e\xf1\xe0\xd7\x83\xfe\t\xd1\xc1U\xbf\x08T_\x07v\xb2\xc6--TOTP' - let totp_secret: [u8; 20] = parameters.client_data_hash[6..26].try_into().unwrap(); - private_key = syscall!(self.trussed.unsafe_inject_shared_key( - &totp_secret, Location::Internal)).key; - // info!("totes injected"); - let fake_cose_pk = ctap_types::cose::TotpPublicKey {}; - let fake_serialized_cose_pk = trussed::cbor_serialize_bytes(&fake_cose_pk) - .map_err(|_| Error::NotAllowed)?; - cose_public_key = fake_serialized_cose_pk; // Bytes::from_slice(&[0u8; 20]).unwrap(); - } - } - - // 12. if `rk` is set, store or overwrite key pair, if full error KeyStoreFull - - // 12.a generate credential - let key_parameter = match rk_requested { - true => Key::ResidentKey(private_key), - false => { - // WrappedKey version - let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - debug!("wrapping private key"); - let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( - wrapping_key, - private_key, - &rp_id_hash, - )).wrapped_key; - // debug!("wrapped_key = {:?}", &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(); - let ret = Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::Other)?); - ret - // debug!("len wrapped key = {}", wrapped_key.len()); - // Key::WrappedKey(wrapped_key.to_bytes().unwrap()) - - } - }; - - // injecting this is a bit mehhh.. - let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap(); - info!("nonce = {:?}", &nonce); - - // 12.b generate credential ID { = AEAD(Serialize(Credential)) } - let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; - - // store it. - // TODO: overwrite, error handling with KeyStoreFull - - let credential = Credential::new( - credential::CtapVersion::Fido21Pre, - ¶meters.rp, - ¶meters.user, - algorithm as i32, - key_parameter, - self.state.persistent.timestamp(&mut self.trussed)?, - hmac_secret_requested.clone(), - cred_protect_requested, - nonce, - ); - - let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; - - if rk_requested { - // serialization with all metadata - let serialized_credential = credential.serialize()?; - - // first delete any other RK cred with same RP + UserId if there is one. - self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id).ok(); - - let credential_id_hash = self.hash(&credential_id.0.as_ref()); - try_syscall!(self.trussed.write_file( - Location::Internal, - rk_path(&rp_id_hash, &credential_id_hash), - serialized_credential.clone(), - // user attribute for later easy lookup - // Some(rp_id_hash.clone()), - None, - )).map_err(|_| Error::KeyStoreFull)?; - } - - // 13. generate and return attestation statement using clientDataHash - - // 13.a AuthenticatorData and its serialization - use ctap2::AuthenticatorDataFlags as Flags; - info!("MC created cred id"); - - let (attestation_maybe, aaguid) = self.state.identity.attestation(&mut self.trussed); - - let authenticator_data = ctap2::make_credential::AuthenticatorData { - rp_id_hash: rp_id_hash.to_bytes().map_err(|_| Error::Other)?, - - flags: { - let mut flags = Flags::USER_PRESENCE; - if uv_performed { - flags |= Flags::USER_VERIFIED; - } - if true { - flags |= Flags::ATTESTED_CREDENTIAL_DATA; - } - if hmac_secret_requested.is_some() || cred_protect_requested.is_some() { - flags |= Flags::EXTENSION_DATA; - } - flags - }, - - sign_count: self.state.persistent.timestamp(&mut self.trussed)?, - - attested_credential_data: { - // debug!("acd in, cid len {}, pk len {}", credential_id.0.len(), cose_public_key.len()); - let attested_credential_data = ctap2::make_credential::AttestedCredentialData { - aaguid: Bytes::from_slice(&aaguid).unwrap(), - credential_id: credential_id.0.to_bytes().unwrap(), - credential_public_key: cose_public_key.to_bytes().unwrap(), - }; - // debug!("cose PK = {:?}", &attested_credential_data.credential_public_key); - Some(attested_credential_data) - }, - - extensions: { - if hmac_secret_requested.is_some() || cred_protect_requested.is_some() { - Some(ctap2::make_credential::Extensions { - cred_protect: parameters.extensions.as_ref().unwrap().cred_protect.clone(), - hmac_secret: parameters.extensions.as_ref().unwrap().hmac_secret.clone(), - }) - - } else { - None - } - }, - }; - // debug!("authData = {:?}", &authenticator_data); - - let serialized_auth_data = authenticator_data.serialize(); - - // 13.b The Signature - - // can we write Sum somehow? - // debug!("seeking commitment, {} + {}", serialized_auth_data.len(), parameters.client_data_hash.len()); - let mut commitment = Bytes::<1024>::new(); - commitment.extend_from_slice(&serialized_auth_data).map_err(|_| Error::Other)?; - // debug!("serialized_auth_data ={:?}", &serialized_auth_data); - commitment.extend_from_slice(¶meters.client_data_hash).map_err(|_| Error::Other)?; - // debug!("client_data_hash = {:?}", ¶meters.client_data_hash); - // debug!("commitment = {:?}", &commitment); - - // NB: the other/normal one is called "basic" or "batch" attestation, - // because it attests the authenticator is part of a batch: the model - // specified by AAGUID. - // "self signed" is also called "surrogate basic". - // - // we should also directly support "none" format, it's a bit weird - // how browsers firefox this - - let (signature, attestation_algorithm) = { - if attestation_maybe.is_none() { - match algorithm { - SupportedAlgorithm::Ed25519 => { - let signature = syscall!(self.trussed.sign_ed255(private_key, &commitment)).signature; - (signature.to_bytes().map_err(|_| Error::Other)?, -8) - } - - SupportedAlgorithm::P256 => { - // DO NOT prehash here, `trussed` does that - let der_signature = syscall!(self.trussed.sign_p256(private_key, &commitment, SignatureSerialization::Asn1Der)).signature; - (der_signature.to_bytes().map_err(|_| Error::Other)?, -7) - } - SupportedAlgorithm::Totp => { - // maybe we can fake it here too, but seems kinda weird - // return Err(Error::UnsupportedAlgorithm); - // micro-ecc is borked. let's self-sign anyway - let hash = syscall!(self.trussed.hash_sha256(&commitment.as_ref())).hash; - let tmp_key = syscall!(self.trussed - .generate_p256_private_key(Location::Volatile)) - .key; - - let signature = syscall!(self.trussed.sign_p256( - tmp_key, - &hash, - SignatureSerialization::Asn1Der, - )).signature; - (signature.to_bytes().map_err(|_| Error::Other)?, -7) - } - } - } else { - - let signature = syscall!(self.trussed.sign_p256( - attestation_maybe.as_ref().unwrap().0, - &commitment, - SignatureSerialization::Asn1Der, - )).signature; - (signature.to_bytes().map_err(|_| Error::Other)?, -7) - } - }; - // debug!("SIG = {:?}", &signature); - - if !rk_requested { - let _success = syscall!(self.trussed.delete(private_key)).success; - info!("deleted private credential key: {}", _success); - } - - let packed_attn_stmt = ctap2::make_credential::PackedAttestationStatement { - alg: attestation_algorithm, - sig: signature, - x5c: match attestation_maybe.is_some() { - false => None, - true => { - // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements - let cert = attestation_maybe.as_ref().unwrap().1.clone(); - let mut x5c = Vec::new(); - x5c.push(cert).ok(); - Some(x5c) - } - }, - }; - - let fmt = String::<32>::from("packed"); - let att_stmt = ctap2::make_credential::AttestationStatement::Packed(packed_attn_stmt); - - let attestation_object = ctap2::make_credential::Response { - fmt, - auth_data: serialized_auth_data, - att_stmt, - }; - - Ok(attestation_object) - } - - fn get_info(&mut self) -> ctap2::get_info::Response { - - use core::str::FromStr; - let mut versions = Vec::, 4>::new(); - versions.push(String::from_str("U2F_V2").unwrap()).unwrap(); - versions.push(String::from_str("FIDO_2_0").unwrap()).unwrap(); - // #[cfg(feature = "enable-fido-pre")] - // versions.push(String::from_str("FIDO_2_1_PRE").unwrap()).unwrap(); - - let mut extensions = Vec::, 4>::new(); - // extensions.push(String::from_str("credProtect").unwrap()).unwrap(); - extensions.push(String::from_str("credProtect").unwrap()).unwrap(); - extensions.push(String::from_str("hmac-secret").unwrap()).unwrap(); - - let mut pin_protocols = Vec::::new(); - pin_protocols.push(1).unwrap(); - - let mut options = ctap2::get_info::CtapOptions::default(); - options.rk = true; - options.up = true; - options.uv = None; // "uv" here refers to "in itself", e.g. biometric - // options.plat = false; - options.cred_mgmt = Some(true); - // options.client_pin = None; // not capable of PIN - options.client_pin = match self.state.persistent.pin_is_set() { - true => Some(true), - false => Some(false), - }; - - let (_, aaguid)= self.state.identity.attestation(&mut self.trussed); - - ctap2::get_info::Response { - versions, - extensions: Some(extensions), - aaguid: Bytes::from_slice(&aaguid).unwrap(), - options: Some(options), - max_msg_size: Some(ctap_types::sizes::MESSAGE_SIZE), - pin_protocols: Some(pin_protocols), - max_creds_in_list: Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST), - max_cred_id_length: Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH), - ..ctap2::get_info::Response::default() - } - } } #[cfg(test)] diff --git a/src/state.rs b/src/state.rs index 6b83683..546f674 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,7 @@ +//! Various state of the authenticator. +//! +//! Needs cleanup. + use core::cmp::Ordering; use trussed::{ @@ -20,14 +24,19 @@ use ctap_types::{ use heapless::binary_heap::{BinaryHeap, Max, Min}; use littlefs2::path::PathBuf; -use crate::Result; -use crate::cbor_serialize_message; +use crate::{ + cbor_serialize_message, + credential::Credential, + Result, +}; pub type MaxCredentialHeap = BinaryHeap; pub type MinCredentialHeap = BinaryHeap; #[derive(Clone, Debug, /*uDebug, Eq, PartialEq,*/ serde::Deserialize, serde::Serialize)] pub struct State { + + /// Batch device identity (aaguid, certificate, key). pub identity: Identity, pub persistent: PersistentState, pub runtime: RuntimeState, @@ -73,6 +82,7 @@ impl State { } +/// Batch device identity (aaguid, certificate, key). #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct Identity { // can this be [u8; 16] or need Bytes for serialization? @@ -119,6 +129,7 @@ impl Identity { Some(aaguid) } + /// Lookup batch key and certificate, together with AAUGID. pub fn attestation(&mut self, trussed: &mut T) -> (Option<(KeyId, Certificate)>, Aaguid) { let key = crate::constants::ATTESTATION_KEY_ID; @@ -147,10 +158,17 @@ impl Identity { } #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct CredentialManagementEnumerateRps(pub u32, pub Bytes32); +pub struct CredentialManagementEnumerateRps { + pub remaining: u32, + pub rp_id_hash: Bytes32, +} #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct CredentialManagementEnumerateCredentials(pub u32, pub PathBuf, pub PathBuf); +pub struct CredentialManagementEnumerateCredentials { + pub remaining: u32, + pub rp_dir: PathBuf, + pub prev_filename: PathBuf, +} #[derive(Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize)] pub struct ActiveGetAssertionData { @@ -171,11 +189,11 @@ pub struct RuntimeState { consecutive_pin_mismatches: u8, // both of these are a cache for previous Get{Next,}Assertion call - credentials: Option, + cached_credentials: Option, pub active_get_assertion: Option, channel: Option, - pub cache_rp: Option, - pub cache_rk: Option, + pub cached_rp: Option, + pub cached_rk: Option, } // TODO: Plan towards future extensibility @@ -401,21 +419,21 @@ impl RuntimeState { } pub fn credential_heap(&mut self) -> &mut MaxCredentialHeap { - if self.credentials.is_none() { + // Can't seem to avoid both borrow checker + unwrap + if self.cached_credentials.is_none() { self.create_credential_heap() } else { - self.credentials.as_mut().unwrap() + self.cached_credentials.as_mut().unwrap() } } fn create_credential_heap(&mut self) -> &mut MaxCredentialHeap { - self.credentials = Some(MaxCredentialHeap::new()); - self.credentials.as_mut().unwrap() + self.cached_credentials = Some(MaxCredentialHeap::new()); + self.cached_credentials.as_mut().unwrap() } pub fn free_credential_heap(&mut self, trussed: &mut T) -> () { - if self.credentials.is_some() { - let max_heap = self.credential_heap(); + if let Some(max_heap) = self.cached_credentials.as_mut() { while max_heap.len() > 0 { let timestamp_path = max_heap.pop().unwrap(); // Only assume that runtime credentials are still valid. @@ -430,7 +448,7 @@ impl RuntimeState { } } - pub fn pop_credential_from_heap(&mut self, trussed: &mut T) -> crate::Credential { + pub fn pop_credential_from_heap(&mut self, trussed: &mut T) -> Credential { let max_heap = self.credential_heap(); let timestamp_hash = max_heap.pop().unwrap(); info!("{:?} @ {} {:?}", ×tamp_hash.path, timestamp_hash.timestamp, timestamp_hash.location); @@ -445,7 +463,7 @@ impl RuntimeState { timestamp_hash.path, )); } - crate::Credential::deserialize(&data).unwrap() + Credential::deserialize(&data).unwrap() } pub fn key_agreement_key(&mut self, trussed: &mut T) -> KeyId { @@ -493,7 +511,7 @@ impl RuntimeState { self.rotate_pin_token(trussed); self.rotate_key_agreement_key(trussed); - self.credentials = None; + self.cached_credentials = None; self.active_get_assertion = None; } From c425ad5e0744168e2935aa94a0ae76ddb0f5a8f6 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Sat, 26 Feb 2022 03:30:43 +0100 Subject: [PATCH 05/13] Remove the temporary serialization of credentials --- src/credential.rs | 40 ++++++- src/ctap1.rs | 2 +- src/ctap2.rs | 273 ++++++++++++++++++++-------------------------- src/state.rs | 76 +++++-------- 4 files changed, 184 insertions(+), 207 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index fe32406..9e80c65 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -1,6 +1,6 @@ //! Internal `Credential` and external `CredentialId` ("keyhandle"). -use core::convert::{TryFrom, TryInto}; +use core::cmp::Ordering; use trussed::{ client, syscall, try_syscall, @@ -134,6 +134,44 @@ impl core::ops::Deref for Credential { } } +/// Compare credentials based on key + timestamp. +/// +/// Likely comparison based on timestamp would be good enough? +impl PartialEq for Credential { + fn eq(&self, other: &Self) -> bool { + (self.creation_time == other.creation_time) + && + (self.key == other.key) + } +} + +impl PartialEq<&Credential> for Credential { + fn eq(&self, other: &&Self) -> bool { + self == *other + } +} + +impl Eq for Credential {} + +impl Ord for Credential { + fn cmp(&self, other: &Self) -> Ordering { + self.data.creation_time.cmp(&other.data.creation_time) + } +} + +/// Order by timestamp of creation. +impl PartialOrd for Credential { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialOrd<&Credential> for Credential { + fn partial_cmp(&self, other: &&Self) -> Option { + Some(self.cmp(*other)) + } +} + pub(crate) type CredentialList = Vec; impl Into for CredentialId { diff --git a/src/ctap1.rs b/src/ctap1.rs index bd21afb..8b6c307 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -55,7 +55,7 @@ where UP: UserPresence, { /// Dispatches the enum of possible requests into the ctap1 [`Authenticator`] trait methods. pub fn call_ctap1(&mut self, request: &Request) -> Result { - info!("called u2f"); + info!("called ctap1"); self.state.persistent.load_if_not_initialised(&mut self.trussed); match request { diff --git a/src/ctap2.rs b/src/ctap2.rs index c8c5623..ed11662 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -42,7 +42,6 @@ use crate::{ state::{ self, MinCredentialHeap, - TimestampPath, }, Result, @@ -73,7 +72,7 @@ where UP: UserPresence, { /// Dispatches the enum of possible requests into the ctap2 [`Authenticator`] trait methods. pub fn call_ctap2(&mut self, request: &Request) -> Result { - info!("called u2f"); + info_now!("called ctap2"); self.state.persistent.load_if_not_initialised(&mut self.trussed); // match request { @@ -90,19 +89,19 @@ where UP: UserPresence, match request { // 0x4 Request::GetInfo => { - debug!("GI"); + debug_now!("GI"); Ok(Response::GetInfo(self.get_info())) } // 0x2 Request::MakeCredential(parameters) => { - debug!("MC request"); + debug_now!("MC request"); Ok(Response::MakeCredential(self.make_credential(¶meters)?)) } // 0x1 Request::GetAssertion(parameters) => { - debug!("GA request"); + debug_now!("GA request"); let response = self.get_assertion(¶meters); match response { Ok(response) => Ok(Response::GetAssertion(response)), @@ -115,7 +114,7 @@ where UP: UserPresence, // 0x8 Request::GetNextAssertion => { - debug!("GNA request"); + debug_now!("GNA request"); let response = self.get_next_assertion(); match response { Ok(response) => Ok(Response::GetNextAssertion(response)), @@ -125,7 +124,7 @@ where UP: UserPresence, // 0x7 Request::Reset => { - debug!("Reset request"); + debug_now!("Reset request"); let response = self.reset(); match response { Ok(()) => Ok(Response::Reset), @@ -136,19 +135,19 @@ where UP: UserPresence, // 0x6 Request::ClientPin(parameters) => { - debug!("CP request"); + debug_now!("CP request"); Ok(Response::ClientPin(self.client_pin(¶meters)?)) } // 0xA Request::CredentialManagement(parameters) => { - debug!("CM request"); + debug_now!("CM request"); Ok(Response::CredentialManagement(self.credential_management(¶meters)?)) } Request::Vendor(op) => { - debug!("Vendor request"); + debug_now!("Vendor request"); self.vendor(*op)?; Ok(Response::Vendor) } @@ -226,20 +225,20 @@ impl Authenticator for crate::Authenti // are stored in state.runtime.credential_heap self.locate_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed)?; - let credential = self.state.runtime.pop_credential_from_heap(&mut self.trussed); + let credential = self.state.runtime.pop_credential_from_heap(); let num_credentials = match self.state.runtime.credential_heap().len() { 0 => None, n => Some(n as u32 + 1), }; - info!("FIRST cred: {:?}",&credential); - info!("FIRST NUM creds: {:?}",num_credentials); + info_now!("FIRST cred: {:?}",&credential); + info_now!("FIRST NUM creds: {:?}",num_credentials); // NB: misleading, if we have "1" we return "None" let human_num_credentials = match num_credentials { Some(n) => n, None => 1, }; - info!("found {:?} applicable credentials", human_num_credentials); + info_now!("found {:?} applicable credentials", human_num_credentials); // 6. process any options present @@ -252,11 +251,11 @@ impl Authenticator for crate::Authenti // 7. collect user presence let up_performed = if do_up { - info!("asking for up"); + info_now!("asking for up"); self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; true } else { - info!("not asking for up"); + info_now!("not asking for up"); false }; @@ -290,7 +289,7 @@ impl Authenticator for crate::Authenti // 3. previous GA/GNA >30s ago -> discard stat // this is optional over NFC if false { - self.state.runtime.free_credential_heap(&mut self.trussed); + self.state.runtime.free_credential_heap(); return Err(Error::NotAllowed); } @@ -299,7 +298,7 @@ impl Authenticator for crate::Authenti // timestamp_hash.location, // timestamp_hash.path, // )).data; - let credential = self.state.runtime.pop_credential_from_heap(&mut self.trussed); + let credential = self.state.runtime.pop_credential_from_heap(); // Credential::deserialize(&data).unwrap(); // 5. suppress PII if no UV was performed in original GA @@ -339,7 +338,7 @@ impl Authenticator for crate::Authenti use credential::CredentialProtectionPolicy; // If UV is not performed, than CredProtectRequired credentials should not be visibile. if !(excluded_cred.cred_protect == Some(CredentialProtectionPolicy::Required) && !uv_performed) { - info!("Excluded!"); + info_now!("Excluded!"); self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; return Err(Error::CredentialExcluded); } @@ -360,12 +359,12 @@ impl Authenticator for crate::Authenti } let algorithm = match algorithm { Some(algorithm) => { - info!("algo: {:?}", algorithm as i32); + info_now!("algo: {:?}", algorithm as i32); algorithm }, None => { return Err(Error::UnsupportedAlgorithm); } }; - // debug!("making credential, eddsa = {}", eddsa); + // debug_now!("making credential, eddsa = {}", eddsa); // 8. process options; on known but unsupported error UnsupportedOption @@ -375,7 +374,7 @@ impl Authenticator for crate::Authenti let mut _uv_requested = false; let _up_requested = true; // can't be toggled - info!("MC options: {:?}", ¶meters.options); + info_now!("MC options: {:?}", ¶meters.options); if let Some(ref options) = ¶meters.options { if Some(true) == options.rk { rk_requested = true; @@ -398,7 +397,7 @@ impl Authenticator for crate::Authenti } } - // debug!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); + // debug_now!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); // 10. get UP, if denied error OperationDenied self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; @@ -420,7 +419,7 @@ impl Authenticator for crate::Authenti Mechanism::P256, public_key.clone(), KeySerialization::Cose )).serialized_key; let _success = syscall!(self.trussed.delete(public_key)).success; - info!("deleted public P256 key: {}", _success); + info_now!("deleted public P256 key: {}", _success); } SigningAlgorithm::Ed25519 => { private_key = syscall!(self.trussed.generate_ed255_private_key(location)).key; @@ -429,7 +428,7 @@ impl Authenticator for crate::Authenti Mechanism::Ed255, public_key.clone(), KeySerialization::Cose )).serialized_key; let _success = syscall!(self.trussed.delete(public_key)).success; - info!("deleted public Ed25519 key: {}", _success); + info_now!("deleted public Ed25519 key: {}", _success); } // SigningAlgorithm::Totp => { // if parameters.client_data_hash.len() != 32 { @@ -439,7 +438,7 @@ impl Authenticator for crate::Authenti // let totp_secret: [u8; 20] = parameters.client_data_hash[6..26].try_into().unwrap(); // private_key = syscall!(self.trussed.unsafe_inject_shared_key( // &totp_secret, Location::Internal)).key; - // // info!("totes injected"); + // // info_now!("totes injected"); // let fake_cose_pk = ctap_types::cose::TotpPublicKey {}; // let fake_serialized_cose_pk = trussed::cbor_serialize_bytes(&fake_cose_pk) // .map_err(|_| Error::NotAllowed)?; @@ -455,13 +454,13 @@ impl Authenticator for crate::Authenti false => { // WrappedKey version let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - debug!("wrapping private key"); + debug_now!("wrapping private key"); let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( wrapping_key, private_key, &rp_id_hash, )).wrapped_key; - // debug!("wrapped_key = {:?}", &wrapped_key); + // debug_now!("wrapped_key = {:?}", &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...) @@ -469,7 +468,7 @@ impl Authenticator for crate::Authenti // wrapped_key.extend_from_slice(&wrapped_key_msg).unwrap(); let ret = Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::Other)?); ret - // debug!("len wrapped key = {}", wrapped_key.len()); + // debug_now!("len wrapped key = {}", wrapped_key.len()); // Key::WrappedKey(wrapped_key.to_bytes().unwrap()) } @@ -477,7 +476,7 @@ impl Authenticator for crate::Authenti // injecting this is a bit mehhh.. let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap(); - info!("nonce = {:?}", &nonce); + info_now!("nonce = {:?}", &nonce); // 12.b generate credential ID { = AEAD(Serialize(Credential)) } let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; @@ -521,7 +520,7 @@ impl Authenticator for crate::Authenti // 13.a AuthenticatorData and its serialization use ctap2::AuthenticatorDataFlags as Flags; - info!("MC created cred id"); + info_now!("MC created cred id"); let (attestation_maybe, aaguid) = self.state.identity.attestation(&mut self.trussed); @@ -545,13 +544,13 @@ impl Authenticator for crate::Authenti sign_count: self.state.persistent.timestamp(&mut self.trussed)?, attested_credential_data: { - // debug!("acd in, cid len {}, pk len {}", credential_id.0.len(), cose_public_key.len()); + // debug_now!("acd in, cid len {}, pk len {}", credential_id.0.len(), cose_public_key.len()); let attested_credential_data = ctap2::make_credential::AttestedCredentialData { aaguid: Bytes::from_slice(&aaguid).unwrap(), credential_id: credential_id.0.to_bytes().unwrap(), credential_public_key: cose_public_key.to_bytes().unwrap(), }; - // debug!("cose PK = {:?}", &attested_credential_data.credential_public_key); + // debug_now!("cose PK = {:?}", &attested_credential_data.credential_public_key); Some(attested_credential_data) }, @@ -567,20 +566,20 @@ impl Authenticator for crate::Authenti } }, }; - // debug!("authData = {:?}", &authenticator_data); + // debug_now!("authData = {:?}", &authenticator_data); let serialized_auth_data = authenticator_data.serialize(); // 13.b The Signature // can we write Sum somehow? - // debug!("seeking commitment, {} + {}", serialized_auth_data.len(), parameters.client_data_hash.len()); + // debug_now!("seeking commitment, {} + {}", serialized_auth_data.len(), parameters.client_data_hash.len()); let mut commitment = Bytes::<1024>::new(); commitment.extend_from_slice(&serialized_auth_data).map_err(|_| Error::Other)?; - // debug!("serialized_auth_data ={:?}", &serialized_auth_data); + // debug_now!("serialized_auth_data ={:?}", &serialized_auth_data); commitment.extend_from_slice(¶meters.client_data_hash).map_err(|_| Error::Other)?; - // debug!("client_data_hash = {:?}", ¶meters.client_data_hash); - // debug!("commitment = {:?}", &commitment); + // debug_now!("client_data_hash = {:?}", ¶meters.client_data_hash); + // debug_now!("commitment = {:?}", &commitment); // NB: the other/normal one is called "basic" or "batch" attestation, // because it attests the authenticator is part of a batch: the model @@ -630,11 +629,11 @@ impl Authenticator for crate::Authenti (signature.to_bytes().map_err(|_| Error::Other)?, -7) } }; - // debug!("SIG = {:?}", &signature); + // debug_now!("SIG = {:?}", &signature); if !rk_requested { let _success = syscall!(self.trussed.delete(private_key)).success; - info!("deleted private credential key: {}", _success); + info_now!("deleted private credential key: {}", _success); } let packed_attn_stmt = ctap2::make_credential::PackedAttestationStatement { @@ -694,8 +693,8 @@ impl Authenticator for crate::Authenti fn client_pin(&mut self, parameters: &ctap2::client_pin::Parameters) -> Result { use ctap2::client_pin::PinV1Subcommand as Subcommand; - debug!("processing CP"); - // info!("{:?}", parameters); + debug_now!("processing CP"); + // info_now!("{:?}", parameters); if parameters.pin_protocol != 1{ return Err(Error::InvalidParameter); @@ -704,7 +703,7 @@ impl Authenticator for crate::Authenti Ok(match parameters.sub_command { Subcommand::GetRetries => { - debug!("processing CP.GR"); + debug_now!("processing CP.GR"); ctap2::client_pin::Response { key_agreement: None, @@ -714,7 +713,7 @@ impl Authenticator for crate::Authenti } Subcommand::GetKeyAgreement => { - debug!("processing CP.GKA"); + debug_now!("processing CP.GKA"); let private_key = self.state.runtime.key_agreement_key(&mut self.trussed); let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; @@ -732,7 +731,7 @@ impl Authenticator for crate::Authenti } Subcommand::SetPin => { - debug!("processing CP.SP"); + debug_now!("processing CP.SP"); // 1. check mandatory parameters let platform_kek = match parameters.key_agreement.as_ref() { Some(key) => key, @@ -779,7 +778,7 @@ impl Authenticator for crate::Authenti } Subcommand::ChangePin => { - debug!("processing CP.CP"); + debug_now!("processing CP.CP"); // 1. check mandatory parameters let platform_kek = match parameters.key_agreement.as_ref() { @@ -836,7 +835,7 @@ impl Authenticator for crate::Authenti } Subcommand::GetPinToken => { - debug!("processing CP.GPT"); + debug_now!("processing CP.GPT"); // 1. check mandatory parameters let platform_kek = match parameters.key_agreement.as_ref() { @@ -865,8 +864,8 @@ impl Authenticator for crate::Authenti // 7. return encrypted pinToken let pin_token = self.state.runtime.pin_token(&mut self.trussed); - debug!("wrapping pin token"); - // info!("exists? {}", syscall!(self.trussed.exists(shared_secret)).exists); + debug_now!("wrapping pin token"); + // info_now!("exists? {}", syscall!(self.trussed.exists(shared_secret)).exists); let pin_token_enc = syscall!(self.trussed.wrap_key_aes256cbc(shared_secret, pin_token)).wrapped_key; syscall!(self.trussed.delete(shared_secret)); @@ -884,7 +883,9 @@ impl Authenticator for crate::Authenti } } - _ => { + Subcommand::GetPinUvAuthTokenUsingUvWithPermissions | + Subcommand::GetUVRetries | + Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => { // todo!("not implemented yet") return Err(Error::InvalidParameter); } @@ -942,13 +943,11 @@ impl Authenticator for crate::Authenti .ok_or(Error::MissingParameter)?, ) } - - // _ => todo!("not implemented yet"), } } fn vendor(&mut self, op: VendorOperation) -> Result<()> { - info!("hello VO {:?}", &op); + info_now!("hello VO {:?}", &op); match op.into() { 0x79 => syscall!(self.trussed.debug_dump_store()), _ => return Err(Error::InvalidCommand), @@ -1009,7 +1008,7 @@ impl crate::Authenticator // // temp // let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len()); - // info!("pin.len() = {}, pin_length = {}, = {:?}", + // info_now!("pin.len() = {}, pin_length = {}, = {:?}", // pin.len(), pin_length, &pin); // chop off null bytes let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len()); @@ -1052,7 +1051,7 @@ impl crate::Authenticator parameters: &ctap2::credential_management::Parameters ) -> Result<()> { - // info!("CM params: {:?}", parameters); + // info_now!("CM params: {:?}", parameters); use ctap2::credential_management::Subcommand; match parameters.sub_command { // are we Haskell yet lol @@ -1088,7 +1087,7 @@ impl crate::Authenticator _ => 0, }; - // info!("input to hmacsha256: {:?}", &data[..len]); + // info_now!("input to hmacsha256: {:?}", &data[..len]); let expected_pin_auth = syscall!(self.trussed.sign_hmacsha256( pin_token, &data[..len], @@ -1098,24 +1097,28 @@ impl crate::Authenticator .pin_auth.as_ref().ok_or(Error::MissingParameter)?; if &expected_pin_auth[..16] == &pin_auth[..] { - info!("passed pinauth"); + info_now!("passed pinauth"); Ok(()) } else { - info!("failed pinauth!"); + info_now!("failed pinauth!"); self.state.decrement_retries(&mut self.trussed)?; let maybe_blocked = self.state.pin_blocked(); if maybe_blocked.is_err() { - info!("blocked"); + info_now!("blocked"); maybe_blocked } else { - info!("pinAuthInvalid"); + info_now!("pinAuthInvalid"); Err(Error::PinAuthInvalid) } } } - _ => Ok(()), + // don't need the PIN auth, they're continuations + // of already checked CredMgmt subcommands + Subcommand::EnumerateRpsGetNextRp | + Subcommand::EnumerateCredentialsGetNextCredential + => Ok(()), } } @@ -1220,51 +1223,62 @@ impl crate::Authenticator -> Result<()> { // validate allowList + info_now!("locating"); + let x = 0; + info_now!("addr(x) = {:p}", &x); let mut allow_list_len = 0; let allowed_credentials = if let Some(allow_list) = allow_list.as_ref() { + info_now!("x"); allow_list_len = allow_list.len(); - allow_list.into_iter() + info_now!("len: {}", allow_list_len); + allow_list.into_iter().enumerate() // discard not properly serialized encrypted credentials - .filter_map(|credential_descriptor| { - info!( - "GA try from cred id: {}", + .filter_map(|(i, credential_descriptor)| { + info_now!( + "GA try from {}th cred id: {}", i, hex_str!(&credential_descriptor.id), ); let cred_maybe = Credential::try_from( self, rp_id_hash, credential_descriptor) .ok(); - info!("cred_maybe: {:?}", &cred_maybe); + info_now!("cred_maybe: {:?}", &cred_maybe); cred_maybe } ) .collect() } else { + info_now!("y"); CredentialList::new() }; + info_now!("new min_heap"); let mut min_heap = MinCredentialHeap::new(); let allowed_credentials_passed = allowed_credentials.len() > 0; + info_now!("a"); if allowed_credentials_passed { + info_now!("if"); // "If an allowList is present and is non-empty, // locate all denoted credentials present on this authenticator // and bound to the specified rpId." - debug!("allowedList passed with {} creds", allowed_credentials.len()); + debug_now!("allowedList passed with {} creds", allowed_credentials.len()); let mut rk_count = 0; + info_now!("1"); let mut applicable_credentials: CredentialList = allowed_credentials - .into_iter() + .into_iter().enumerate() + .map(|(i, credential)| { info_now!("{}th", i); credential }) .filter(|credential| match credential.key.clone() { // TODO: should check if wrapped key is valid AEAD // On the other hand, we already decrypted a valid AEAD Key::WrappedKey(_) => true, Key::ResidentKey(key) => { - debug!("checking if ResidentKey {:?} exists", &key); + debug_now!("checking if ResidentKey {:?} exists", &key); let exists = match credential.algorithm { -7 => syscall!(self.trussed.exists(Mechanism::P256, key)).exists, -8 => syscall!(self.trussed.exists(Mechanism::Ed255, key)).exists, // -9 => { // let exists = syscall!(self.trussed.exists(Mechanism::Totp, key)).exists; - // info!("found it"); + // info_now!("found it"); // exists // } _ => false, @@ -1277,7 +1291,7 @@ impl crate::Authenticator }) .filter(|credential| { use credential::CredentialProtectionPolicy as Policy; - debug!("CredentialProtectionPolicy {:?}", &credential.cred_protect); + debug_now!("CredentialProtectionPolicy {:?}", &credential.cred_protect); match credential.cred_protect { None | Some(Policy::Optional) => true, Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, @@ -1286,54 +1300,18 @@ impl crate::Authenticator } }) .collect(); + info_now!("2"); while applicable_credentials.len() > 0 { // Store all other applicable credentials in volatile storage and add to our // credential heap. let credential = applicable_credentials.pop().unwrap(); - let serialized = credential.serialize()?; - - let mut path = [b'0', b'0']; - format_hex(&[applicable_credentials.len() as u8], &mut path); - let path = PathBuf::from(&path); - // let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; - // let id = credential.id_using_hash(&mut self.trussed, kek, rp_id_hash)?; - // let credential_id_hash = self.hash(&id.0.as_ref()); - - // let path = rk_path(&rp_id_hash, &credential_id_hash); - let timestamp_path = TimestampPath { - timestamp: credential.creation_time, - path: path.clone(), - location: Location::Volatile, - }; - - - info!("added volatile cred: {:?}", ×tamp_path); - info!("{}",hex_str!(&serialized)); - - - try_syscall!(self.trussed.write_file( - Location::Volatile, - path.clone(), - serialized, - None, - )).map_err(|_| { - Error::KeyStoreFull - })?; - - // attempt to read back - // let data = syscall!(self.trussed.read_file( - // Location::Volatile, - // timestamp_path.path.clone(), - // )).data; - // crate::Credential::deserialize(&data).unwrap(); - if min_heap.capacity() > min_heap.len() { - min_heap.push(timestamp_path).map_err(drop).unwrap(); + min_heap.push(credential).map_err(drop).unwrap(); } else { - if timestamp_path.timestamp > min_heap.peek().unwrap().timestamp { + if credential > min_heap.peek().unwrap() { min_heap.pop().unwrap(); - min_heap.push(timestamp_path).map_err(drop).unwrap(); + min_heap.push(credential).map_err(drop).unwrap(); } } // If more than one credential was located in step 1 and allowList is present and not empty, @@ -1346,6 +1324,7 @@ impl crate::Authenticator } } else if allow_list_len == 0 { + info_now!("else"); // If an allowList is not present, // locate all credentials that are present on this authenticator // and bound to the specified rpId; sorted by reverse creation time @@ -1367,7 +1346,7 @@ impl crate::Authenticator // - these then go into a CredentialList // - (we don't need to keep that around even) // - debug!("no allowedList passed"); + debug_now!("no allowedList passed"); // let mut credentials = CredentialList::new(); @@ -1391,20 +1370,9 @@ impl crate::Authenticator Some(Policy::Required) => uv_performed, }; - let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; - if keep { - let id = credential.id(&mut self.trussed, kek, Some(rp_id_hash))?; - let credential_id_hash = self.hash(&id.0.as_ref()); - - let timestamp_path = TimestampPath { - timestamp: credential.creation_time, - path: rk_path(&rp_id_hash, &credential_id_hash), - location: Location::Internal, - }; - - min_heap.push(timestamp_path).map_err(drop).unwrap(); - // info!("first: {:?}", &self.hash(&id.0)); + min_heap.push(credential).map_err(drop).unwrap(); + // info_now!("first: {:?}", &self.hash(&id.0)); } loop { @@ -1424,27 +1392,19 @@ impl crate::Authenticator if keep { - let id = credential.id(&mut self.trussed, kek, Some(rp_id_hash))?; - let credential_id_hash = self.hash(&id.0.as_ref()); - - let timestamp_path = TimestampPath { - timestamp: credential.creation_time, - path: rk_path(&rp_id_hash, &credential_id_hash), - location: Location::Internal, - }; - if min_heap.capacity() > min_heap.len() { - min_heap.push(timestamp_path).map_err(drop).unwrap(); + min_heap.push(credential).map_err(drop).unwrap(); } else { - if timestamp_path.timestamp > min_heap.peek().unwrap().timestamp { + if credential > min_heap.peek().unwrap() { min_heap.pop().unwrap(); - min_heap.push(timestamp_path).map_err(drop).unwrap(); + min_heap.push(credential).map_err(drop).unwrap(); } } } } }; + info_now!("near end of locating"); // "If no applicable credentials were found, return CTAP2_ERR_NO_CREDENTIALS" if min_heap.is_empty() { @@ -1452,11 +1412,14 @@ impl crate::Authenticator } // now sort them - self.state.runtime.free_credential_heap(&mut self.trussed); + info_now!("freeing heap"); + self.state.runtime.free_credential_heap(); + info_now!("freed"); let max_heap = self.state.runtime.credential_heap(); while !min_heap.is_empty() { max_heap.push(min_heap.pop().unwrap()).map_err(drop).unwrap(); } + info_now!("aight"); Ok(()) } @@ -1544,7 +1507,7 @@ impl crate::Authenticator Key::ResidentKey(key) => (key, true), Key::WrappedKey(bytes) => { let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - // info!("unwrapping {:?} with wrapping key {:?}", &bytes, &wrapping_key); + // info_now!("unwrapping {:?} with wrapping key {:?}", &bytes, &wrapping_key); let key_result = syscall!(self.trussed.unwrap_key_chacha8poly1305( wrapping_key, &bytes, @@ -1552,8 +1515,8 @@ impl crate::Authenticator // &rp_id_hash, Location::Volatile, )).key; - // debug!("key result: {:?}", &key_result); - info!("key result"); + // debug_now!("key result: {:?}", &key_result); + info_now!("key result"); match key_result { Some(key) => (key, false), None => { return Err(Error::Other); } @@ -1570,7 +1533,7 @@ impl crate::Authenticator // 9./10. sign clientDataHash || authData with "first" credential - // info!("signing with credential {:?}", &credential); + // info_now!("signing with credential {:?}", &credential); let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; @@ -1609,15 +1572,15 @@ impl crate::Authenticator let (mechanism, serialization) = match credential.algorithm { -7 => (Mechanism::P256, SignatureSerialization::Asn1Der), -8 => (Mechanism::Ed255, SignatureSerialization::Raw), - -9 => (Mechanism::Totp, SignatureSerialization::Raw), + // -9 => (Mechanism::Totp, SignatureSerialization::Raw), _ => { return Err(Error::Other); } }; - debug!("signing with {:?}, {:?}", &mechanism, &serialization); + debug_now!("signing with {:?}, {:?}", &mechanism, &serialization); let signature = match mechanism { // Mechanism::Totp => { // let timestamp = u64::from_le_bytes(data.client_data_hash[..8].try_into().unwrap()); - // info!("TOTP with timestamp {:?}", ×tamp); + // info_now!("TOTP with timestamp {:?}", ×tamp); // syscall!(self.trussed.sign_totp(key, timestamp)).signature.to_bytes().unwrap() // } _ => syscall!(self.trussed.sign(mechanism, key.clone(), &commitment, serialization)).signature @@ -1667,7 +1630,7 @@ impl crate::Authenticator )).entry; loop { - info!("this may be an RK: {:?}", &entry); + info_now!("this may be an RK: {:?}", &entry); let rk_path = match entry { // no more RKs left // break breaks inner loop here @@ -1675,7 +1638,7 @@ impl crate::Authenticator Some(entry) => PathBuf::from(entry.path()), }; - info!("checking RK {:?} for userId ", &rk_path); + info_now!("checking RK {:?} for userId ", &rk_path); let credential_data = syscall!(self.trussed.read_file( Location::Internal, PathBuf::from(rk_path.clone()), @@ -1686,11 +1649,11 @@ impl crate::Authenticator if old_credential.user.id == user_id { match old_credential.key { credential::Key::ResidentKey(key) => { - info!(":: deleting resident key"); + info_now!(":: deleting resident key"); syscall!(self.trussed.delete(key)); } _ => { - warn!(":: WARNING: unexpected server credential in rk."); + warn_now!(":: WARNING: unexpected server credential in rk."); } } syscall!(self.trussed.remove_file( @@ -1698,7 +1661,7 @@ impl crate::Authenticator PathBuf::from(rk_path), )); - info!("Overwriting previous rk tied to this userId."); + info_now!("Overwriting previous rk tied to this userId."); break; } } else { @@ -1719,20 +1682,20 @@ impl crate::Authenticator ) -> Result<()> { - info!("deleting RK {:?}", &rk_path); + info_now!("deleting RK {:?}", &rk_path); let credential_data = syscall!(self.trussed.read_file( Location::Internal, PathBuf::from(rk_path), )).data; let credential_maybe = Credential::deserialize(&credential_data); - // info!("deleting credential {:?}", &credential); + // info_now!("deleting credential {:?}", &credential); if let Ok(credential) = credential_maybe { match credential.key { credential::Key::ResidentKey(key) => { - info!(":: deleting resident key"); + info_now!(":: deleting resident key"); syscall!(self.trussed.delete(key)); } credential::Key::WrappedKey(_) => {} @@ -1740,10 +1703,10 @@ impl crate::Authenticator } else { // If for some reason there becomes a corrupt credential, // we can still at least orphan the key rather then crash. - info!("Warning! Orpaning a key."); + info_now!("Warning! Orpaning a key."); } - info!(":: deleting RK file {:?} itself", &rk_path); + info_now!(":: deleting RK file {:?} itself", &rk_path); syscall!(self.trussed.remove_file( Location::Internal, PathBuf::from(rk_path), diff --git a/src/state.rs b/src/state.rs index 546f674..74807ca 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,8 +2,6 @@ //! //! Needs cleanup. -use core::cmp::Ordering; - use trussed::{ client, syscall, try_syscall, Client as TrussedClient, @@ -30,8 +28,8 @@ use crate::{ Result, }; -pub type MaxCredentialHeap = BinaryHeap; -pub type MinCredentialHeap = BinaryHeap; +pub type MaxCredentialHeap = BinaryHeap; +pub type MinCredentialHeap = BinaryHeap; #[derive(Clone, Debug, /*uDebug, Eq, PartialEq,*/ serde::Deserialize, serde::Serialize)] pub struct State { @@ -432,38 +430,16 @@ impl RuntimeState { self.cached_credentials.as_mut().unwrap() } - pub fn free_credential_heap(&mut self, trussed: &mut T) -> () { - if let Some(max_heap) = self.cached_credentials.as_mut() { - while max_heap.len() > 0 { - let timestamp_path = max_heap.pop().unwrap(); - // Only assume that runtime credentials are still valid. - if timestamp_path.location == Location::Volatile { - syscall!(trussed.remove_file( - Location::Volatile, - timestamp_path.path, - )); - } - - } + pub fn free_credential_heap(&mut self) { + let heap = self.cached_credentials.take(); + if let Some(mut heap) = heap { + heap.clear(); } } - pub fn pop_credential_from_heap(&mut self, trussed: &mut T) -> Credential { + pub fn pop_credential_from_heap(&mut self) -> Credential { let max_heap = self.credential_heap(); - let timestamp_hash = max_heap.pop().unwrap(); - info!("{:?} @ {} {:?}", ×tamp_hash.path, timestamp_hash.timestamp, timestamp_hash.location); - let data = syscall!(trussed.read_file( - timestamp_hash.location, - timestamp_hash.path.clone(), - )).data; - // Remove any volatile creds - if timestamp_hash.location == Location::Volatile { - syscall!(trussed.remove_file( - timestamp_hash.location, - timestamp_hash.path, - )); - } - Credential::deserialize(&data).unwrap() + max_heap.pop().unwrap() } pub fn key_agreement_key(&mut self, trussed: &mut T) -> KeyId { @@ -546,22 +522,22 @@ impl RuntimeState { } -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct TimestampPath { - pub timestamp: u32, - pub path: PathBuf, - pub location: Location, -} - -impl Ord for TimestampPath { - fn cmp(&self, other: &Self) -> Ordering { - self.timestamp.cmp(&other.timestamp) - } -} - -impl PartialOrd for TimestampPath { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} +// #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +// pub struct TimestampPath { +// pub timestamp: u32, +// pub path: PathBuf, +// pub location: Location, +// } + +// impl Ord for TimestampPath { +// fn cmp(&self, other: &Self) -> Ordering { +// self.timestamp.cmp(&other.timestamp) +// } +// } + +// impl PartialOrd for TimestampPath { +// fn partial_cmp(&self, other: &Self) -> Option { +// Some(self.cmp(other)) +// } +// } From a8601081389d893933bf697ff024fec16243534a Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Sat, 5 Mar 2022 21:16:50 +0100 Subject: [PATCH 06/13] Redesign how credentials are looked up in GetAssertion --- CHANGELOG.md | 13 + Cargo.toml | 14 +- README.md | 15 + src/credential.rs | 9 +- src/ctap1.rs | 97 ++--- src/ctap2.rs | 666 ++++++++++------------------- src/ctap2/credential_management.rs | 33 +- src/ctap2/pin.rs | 8 + src/dispatch.rs | 141 ++++++ src/dispatch/apdu.rs | 82 ++++ src/dispatch/ctaphid.rs | 36 ++ src/lib.rs | 32 +- src/state.rs | 128 +++--- 13 files changed, 694 insertions(+), 580 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/ctap2/pin.rs create mode 100644 src/dispatch.rs create mode 100644 src/dispatch/apdu.rs create mode 100644 src/dispatch/ctaphid.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3a81ef9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +- use 2021 edition +- use @szszszsz's credential ID shortening +- get rid of the two big heaps, only cache timestamp + filename in GetAssertion +- bump to the released dependencies +- integrate `dispatch-fido` diff --git a/Cargo.toml b/Cargo.toml index 70bd95e..96f18ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fido-authenticator" -version = "0.0.0-unreleased" +version = "0.1.0" authors = ["Nicolas Stalder "] edition = "2021" license = "Apache-2.0 OR MIT" @@ -10,6 +10,7 @@ documentation = "https://docs.rs/fido-authenticator" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +ctap-types = "0.1.0" delog = "0.1.0" heapless = "0.7" interchange = "0.2.0" @@ -18,10 +19,15 @@ serde = { version = "1.0", default-features = false } serde_cbor = { version = "0.11.0", default-features = false } serde-indexed = "0.1.0" -ctap-types = { git = "https://github.com/solokeys/ctap-types" } trussed = { git = "https://github.com/trussed-dev/trussed" } +apdu-dispatch = { version = "0.1", optional = true } +ctaphid-dispatch = { version = "0.1", optional = true } +iso7816 = { version = "0.1", optional = true } + [features] +default = [] +dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] disable-reset-time-window = [] enable-fido-pre = [] @@ -32,6 +38,6 @@ log-debug = [] log-warn = [] log-error = [] -# [dev-dependencies] +[dev-dependencies] # quickcheck = "1" -# rand = "0.8.4" +rand = "0.8.4" diff --git a/README.md b/README.md index 6d0e2e4..a5264f8 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,21 @@ As used in the [SoloKeys][solokeys] [Solo 2][solo2] and [Nitrokey 3][nitro3]. [ctap21ps]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html [webauthnl2]: https://www.w3.org/TR/webauthn-2/ +### Setup + +For attestation to work, the authenticator's state needs to be provisioned with a batch +attestation key and certificate. They are expected in files `/fido/sec/00` and `/fido/x5c/00`, +respectively. + +In the context of the SoloKeys Solo 2, "secure" devices are pre-provisioned; for "unlocked" devices, +if the firmware contains the provisioner app, this can be done with the CLI: + +```sh +solo2 pki dev fido batch.key batch.cert +solo2 app provision store-fido-batch-key batch.key +solo2 app provision store-fido-batch-cert batch.cert +``` + #### License `fido-authenticator` is fully open source. diff --git a/src/credential.rs b/src/credential.rs index 9e80c65..f74b286 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -8,7 +8,7 @@ use trussed::{ }; pub(crate) use ctap_types::{ - Bytes, Bytes32, String, Vec, + Bytes, String, // authenticator::{ctap1, ctap2, Error, Request, Response}, ctap2::credential_management::CredentialProtectionPolicy, sizes::*, @@ -172,7 +172,8 @@ impl PartialOrd<&Credential> for Credential { } } -pub(crate) type CredentialList = Vec; +// Bad idea - huge stack +// pub(crate) type CredentialList = Vec; impl Into for CredentialId { fn into(self) -> PublicKeyCredentialDescriptor { @@ -240,7 +241,7 @@ impl Credential { &self, trussed: &mut T, key_encryption_key: KeyId, - rp_id_hash: Option<&Bytes32>, + rp_id_hash: Option<&Bytes<32>>, ) -> Result { @@ -248,7 +249,7 @@ impl Credential { let message = &serialized_credential; // info!("serialized cred = {:?}", message).ok(); - let rp_id_hash: Bytes32 = if let Some(hash) = rp_id_hash { + let rp_id_hash: Bytes<32> = if let Some(hash) = rp_id_hash { hash.clone() } else { syscall!(trussed.hash_sha256(&self.rp.id.as_ref())) diff --git a/src/ctap1.rs b/src/ctap1.rs index 8b6c307..2df1cd5 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -1,14 +1,14 @@ //! The `ctap1::Authenticator` trait and its implementation. use ctap_types::{ - Bytes, ctap1::{ - self, - Command as Request, - Response, + Authenticator, + ControlByte, + register, authenticate, Result, Error, }, + heapless_bytes::Bytes, }; use trussed::{ @@ -33,58 +33,25 @@ use crate::{ UserPresence, }; -/// CTAP1 (U2F) authenticator API -/// -/// Ahh... life could be so simple! -// -// TODO: Lift into ctap-types? -pub trait Authenticator { - /// Register a U2F credential. - fn register(&mut self, request: &ctap1::Register) -> Result; - /// Authenticate with a U2F credential. - fn authenticate(&mut self, request: &ctap1::Authenticate) -> Result; - /// Supported U2F version. - fn version() -> [u8; 6] { - *b"U2F_V2" - } -} - -impl crate::Authenticator -where UP: UserPresence, - T: TrussedRequirements, -{ - /// Dispatches the enum of possible requests into the ctap1 [`Authenticator`] trait methods. - pub fn call_ctap1(&mut self, request: &Request) -> Result { - info!("called ctap1"); - self.state.persistent.load_if_not_initialised(&mut self.trussed); - - match request { - Request::Register(reg) => - Ok(Response::Register(self.register(reg)?)), - - Request::Authenticate(auth) => - Ok(Response::Authenticate(self.authenticate(auth)?)), - - Request::Version => - Ok(ctap1::Response::Version(Self::version())), - - } - } - - // #[deprecated(note="please use `call_ctap1` instead")] - /// Alias of `call_ctap1`, may be deprecated in the future. - pub fn call_u2f(&mut self, request: &Request) -> Result { - self.call_ctap1(request) - } - -} - type Commitment = Bytes::<324>; /// Implement `ctap1::Authenticator` for our Authenticator. +/// +/// ## References +/// The "proposed standard" of U2F V1.2 applies to CTAP1. +/// - [Message formats](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html) +/// - [App ID](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-appid-and-facets-v1.2-ps-20170411.html) impl Authenticator for crate::Authenticator { - fn register(&mut self, reg: &ctap1::Register) -> Result { + /// Register a new credential, this always uses P-256 keys. + /// + /// Note that attestation is mandatory in CTAP1/U2F, so if the state + /// is not provisioned with a key/cert, this method will fail. + /// + /// + /// Also note that CTAP1 credentials should be assertable over CTAP2. I believe this is + /// currently not the case. + fn register(&mut self, reg: ®ister::Request) -> Result { self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) .map_err(|_| Error::ConditionsOfUseNotSatisfied)?; @@ -95,12 +62,14 @@ impl Authenticator for crate::Authenti let serialized_cose_public_key = syscall!(self.trussed.serialize_p256_key( public_key, KeySerialization::EcdhEsHkdf256 )).serialized_key; + syscall!(self.trussed.delete(public_key)); let cose_key: ctap_types::cose::EcdhEsHkdf256PublicKey = trussed::cbor_deserialize(&serialized_cose_public_key).unwrap(); let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed) .map_err(|_| Error::UnspecifiedCheckingError)?; - debug!("wrapping u2f private key"); + // debug!("wrapping u2f private key"); + let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( wrapping_key, private_key, @@ -108,12 +77,16 @@ impl Authenticator for crate::Authenti )).wrapped_key; // debug!("wrapped_key = {:?}", &wrapped_key); + syscall!(self.trussed.delete(private_key)); + let key = Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::UnspecifiedCheckingError)?); let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap(); let mut rp_id = heapless::String::new(); // We do not know the rpId string in U2F. Just using placeholder. + // TODO: Is this true? + // rp_id.push_str("u2f").ok(); let rp = ctap_types::webauthn::PublicKeyCredentialRpEntity{ id: rp_id, @@ -146,8 +119,6 @@ impl Authenticator for crate::Authenti // 12.b generate credential ID { = AEAD(Serialize(Credential)) } let kek = self.state.persistent.key_encryption_key(&mut self.trussed).map_err(|_| Error::NotEnoughMemory)?; let credential_id = credential.id(&mut self.trussed, kek, Some(®.app_id)).map_err(|_| Error::NotEnoughMemory)?; - syscall!(self.trussed.delete(public_key)); - syscall!(self.trussed.delete(private_key)); let mut commitment = Commitment::new(); @@ -183,7 +154,7 @@ impl Authenticator for crate::Authenti }; - Ok(ctap1::RegisterResponse::new( + Ok(register::Response::new( 0x05, &cose_key, &credential_id.0, @@ -192,11 +163,11 @@ impl Authenticator for crate::Authenti )) } - fn authenticate(&mut self, auth: &ctap1::Authenticate) -> Result { + fn authenticate(&mut self, auth: &authenticate::Request) -> Result { let cred = Credential::try_from_bytes(self, &auth.app_id, &auth.key_handle); let user_presence_byte = match auth.control_byte { - ctap1::ControlByte::CheckOnly => { + ControlByte::CheckOnly => { // if the control byte is set to 0x07 by the FIDO Client, // the U2F token is supposed to simply check whether the // provided key handle was originally created by this token @@ -206,12 +177,12 @@ impl Authenticator for crate::Authenti Err(Error::IncorrectDataParameter) }; }, - ctap1::ControlByte::EnforceUserPresenceAndSign => { + ControlByte::EnforceUserPresenceAndSign => { self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) .map_err(|_| Error::ConditionsOfUseNotSatisfied)?; 0x01 }, - ctap1::ControlByte::DontEnforceUserPresenceAndSign => 0x00, + ControlByte::DontEnforceUserPresenceAndSign => 0x00, }; let cred = cred.map_err(|_| Error::IncorrectDataParameter)?; @@ -262,11 +233,11 @@ impl Authenticator for crate::Authenti SignatureSerialization::Asn1Der )).signature.to_bytes().unwrap(); - Ok(ctap1::AuthenticateResponse::new( - user_presence_byte, - sig_count, + Ok(authenticate::Response { + user_presence: user_presence_byte, + count: sig_count, signature, - )) + }) } } diff --git a/src/ctap2.rs b/src/ctap2.rs index ed11662..d70760a 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,16 +1,14 @@ //! The `ctap2::Authenticator` trait and its implementation. use ctap_types::{ - Bytes, Bytes32, String, Vec, - authenticator::{ - ctap2::{ - Request, - Response, - }, - Error, + heapless::{String, Vec}, + Error, + ctap2::{ + self, + Authenticator, + VendorOperation, }, - ctap2::self, - operation::VendorOperation, + heapless_bytes::Bytes, sizes, }; @@ -34,14 +32,15 @@ use crate::{ credential::{ self, Credential, - CredentialList, + // CredentialList, Key, }, constants, format_hex, state::{ self, - MinCredentialHeap, + // // (2022-02-27): 9288 bytes + // MinCredentialHeap, }, Result, @@ -50,121 +49,25 @@ use crate::{ TrussedRequirements, }; -pub mod credential_management; - -/// CTAP2 authenticator API -// -// TODO: Lift into ctap-types? -pub trait Authenticator { - fn get_info(&mut self) -> ctap2::get_info::Response; - fn make_credential(&mut self, request: &ctap2::make_credential::Parameters) -> Result; - fn get_assertion(&mut self, request: &ctap2::get_assertion::Parameters) -> Result; - fn get_next_assertion(&mut self) -> Result; - fn reset(&mut self) -> Result<()>; - fn client_pin(&mut self, request: &ctap2::client_pin::Parameters) -> Result; - fn credential_management(&mut self, request: &ctap2::credential_management::Parameters) -> Result; - fn vendor(&mut self, op: VendorOperation) -> Result<()>; -} - -impl crate::Authenticator -where UP: UserPresence, - T: TrussedRequirements, -{ - /// Dispatches the enum of possible requests into the ctap2 [`Authenticator`] trait methods. - pub fn call_ctap2(&mut self, request: &Request) -> Result { - info_now!("called ctap2"); - self.state.persistent.load_if_not_initialised(&mut self.trussed); - - // match request { - // Command::Register(reg) => - // Ok(Response::Register(self.register(reg)?)), - - // Command::Authenticate(auth) => - // Ok(Response::Authenticate(self.authenticate(auth)?)), - - // Command::Version => - // Ok(ctap1::Response::Version(Self::version())), +#[allow(unused_imports)] +use crate::msp; - // } - match request { - // 0x4 - Request::GetInfo => { - debug_now!("GI"); - Ok(Response::GetInfo(self.get_info())) - } - - // 0x2 - Request::MakeCredential(parameters) => { - debug_now!("MC request"); - Ok(Response::MakeCredential(self.make_credential(¶meters)?)) - } - - // 0x1 - Request::GetAssertion(parameters) => { - debug_now!("GA request"); - let response = self.get_assertion(¶meters); - match response { - Ok(response) => Ok(Response::GetAssertion(response)), - Err(error) => { - info_now!("GA request failed, error {:?}", &error); - Err(error) - } - } - } - - // 0x8 - Request::GetNextAssertion => { - debug_now!("GNA request"); - let response = self.get_next_assertion(); - match response { - Ok(response) => Ok(Response::GetNextAssertion(response)), - Err(error) => Err(error) - } - } - - // 0x7 - Request::Reset => { - debug_now!("Reset request"); - let response = self.reset(); - match response { - Ok(()) => Ok(Response::Reset), - Err(error) => Err(error) - } - } - - - // 0x6 - Request::ClientPin(parameters) => { - debug_now!("CP request"); - Ok(Response::ClientPin(self.client_pin(¶meters)?)) - } - - // 0xA - Request::CredentialManagement(parameters) => { - debug_now!("CM request"); - Ok(Response::CredentialManagement(self.credential_management(¶meters)?)) - } - - - Request::Vendor(op) => { - debug_now!("Vendor request"); - self.vendor(*op)?; - Ok(Response::Vendor) - } - } - } -} +pub mod credential_management; +// pub mod pin; /// Implement `ctap2::Authenticator` for our Authenticator. impl Authenticator for crate::Authenticator { + #[inline(never)] fn get_info(&mut self) -> ctap2::get_info::Response { + debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); + use core::str::FromStr; let mut versions = Vec::, 4>::new(); versions.push(String::from_str("U2F_V2").unwrap()).unwrap(); versions.push(String::from_str("FIDO_2_0").unwrap()).unwrap(); // #[cfg(feature = "enable-fido-pre")] - // versions.push(String::from_str("FIDO_2_1_PRE").unwrap()).unwrap(); + versions.push(String::from_str("FIDO_2_1_PRE").unwrap()).unwrap(); let mut extensions = Vec::, 4>::new(); // extensions.push(String::from_str("credProtect").unwrap()).unwrap(); @@ -193,7 +96,8 @@ impl Authenticator for crate::Authenti extensions: Some(extensions), aaguid: Bytes::from_slice(&aaguid).unwrap(), options: Some(options), - max_msg_size: Some(ctap_types::sizes::MESSAGE_SIZE), + // 1200 + max_msg_size: Some(ctap_types::sizes::REALISTIC_MAX_MESSAGE_SIZE), pin_protocols: Some(pin_protocols), max_creds_in_list: Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST), max_cred_id_length: Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH), @@ -201,105 +105,27 @@ impl Authenticator for crate::Authenti } } - fn get_assertion(&mut self, parameters: &ctap2::get_assertion::Parameters) -> Result { - - let rp_id_hash = self.hash(¶meters.rp_id.as_ref()); - - // 1-4. - let uv_performed = match self.pin_prechecks( - ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, - ¶meters.client_data_hash.as_ref(), - ) { - Ok(b) => b, - Err(Error::PinRequired) => { - // UV is optional for get_assertion - false - } - Err(err) => return Err(err), - }; - - // 5. Locate eligible credentials - // - // Note: If allowList is passed, credential is Some(credential) - // If no allowList is passed, credential is None and the retrieved credentials - // are stored in state.runtime.credential_heap - self.locate_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed)?; - - let credential = self.state.runtime.pop_credential_from_heap(); - let num_credentials = match self.state.runtime.credential_heap().len() { - 0 => None, - n => Some(n as u32 + 1), - }; - info_now!("FIRST cred: {:?}",&credential); - info_now!("FIRST NUM creds: {:?}",num_credentials); - - // NB: misleading, if we have "1" we return "None" - let human_num_credentials = match num_credentials { - Some(n) => n, - None => 1, - }; - info_now!("found {:?} applicable credentials", human_num_credentials); - - // 6. process any options present - - // UP occurs by default, but option could specify not to. - let do_up = if parameters.options.is_some() { - parameters.options.as_ref().unwrap().up.unwrap_or(true) - } else { - true - }; - - // 7. collect user presence - let up_performed = if do_up { - info_now!("asking for up"); - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; - true - } else { - info_now!("not asking for up"); - false - }; - - let multiple_credentials = human_num_credentials > 1; - self.state.runtime.active_get_assertion = Some(state::ActiveGetAssertionData { - rp_id_hash: { - let mut buf = [0u8; 32]; - buf.copy_from_slice(&rp_id_hash); - buf - }, - client_data_hash: { - let mut buf = [0u8; 32]; - buf.copy_from_slice(¶meters.client_data_hash); - buf - }, - uv_performed, - up_performed, - multiple_credentials, - extensions: parameters.extensions.clone(), - }); - - self.assert_with_credential(num_credentials, credential) - } - + #[inline(never)] fn get_next_assertion(&mut self) -> Result { - // 1./2. don't remember / don't have left any credentials - if self.state.runtime.credential_heap().is_empty() { - return Err(Error::NotAllowed); - } - // 3. previous GA/GNA >30s ago -> discard stat // this is optional over NFC if false { - self.state.runtime.free_credential_heap(); + self.state.runtime.clear_credential_cache(); + self.state.runtime.active_get_assertion = None; return Err(Error::NotAllowed); } - + // + // 1./2. don't remember / don't have left any credentials // 4. select credential // let data = syscall!(self.trussed.read_file( // timestamp_hash.location, // timestamp_hash.path, // )).data; - let credential = self.state.runtime.pop_credential_from_heap(); - // Credential::deserialize(&data).unwrap(); + if self.state.runtime.active_get_assertion.is_none() { + return Err(Error::NotAllowed); + } + let credential = self.state.runtime.pop_credential(&mut self.trussed) + .ok_or(Error::NotAllowed)?; // 5. suppress PII if no UV was performed in original GA @@ -310,7 +136,8 @@ impl Authenticator for crate::Authenti self.assert_with_credential(None, credential) } - fn make_credential(&mut self, parameters: &ctap2::make_credential::Parameters) -> Result { + #[inline(never)] + fn make_credential(&mut self, parameters: &ctap2::make_credential::Request) -> Result { let rp_id_hash = self.hash(¶meters.rp.id.as_ref()); @@ -364,7 +191,6 @@ impl Authenticator for crate::Authenti }, None => { return Err(Error::UnsupportedAlgorithm); } }; - // debug_now!("making credential, eddsa = {}", eddsa); // 8. process options; on known but unsupported error UnsupportedOption @@ -454,23 +280,17 @@ impl Authenticator for crate::Authenti false => { // WrappedKey version let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - debug_now!("wrapping private key"); let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( wrapping_key, private_key, &rp_id_hash, )).wrapped_key; - // debug_now!("wrapped_key = {:?}", &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(); - let ret = Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::Other)?); - ret - // debug_now!("len wrapped key = {}", wrapped_key.len()); - // Key::WrappedKey(wrapped_key.to_bytes().unwrap()) - + Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::Other)?) } }; @@ -496,6 +316,7 @@ impl Authenticator for crate::Authenti nonce, ); + // note that this does the "stripping" of OptionalUI etc. let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; if rk_requested { @@ -505,6 +326,7 @@ impl Authenticator for crate::Authenti // first delete any other RK cred with same RP + UserId if there is one. self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id).ok(); + // then store key, making it resident let credential_id_hash = self.hash(&credential_id.0.as_ref()); try_syscall!(self.trussed.write_file( Location::Internal, @@ -663,6 +485,7 @@ impl Authenticator for crate::Authenti Ok(attestation_object) } + #[inline(never)] fn reset(&mut self) -> Result<()> { // 1. >10s after bootup -> NotAllowed let uptime = syscall!(self.trussed.uptime()).uptime; @@ -691,11 +514,13 @@ impl Authenticator for crate::Authenti Ok(()) } - fn client_pin(&mut self, parameters: &ctap2::client_pin::Parameters) -> Result { + #[inline(never)] + fn client_pin(&mut self, parameters: &ctap2::client_pin::Request) -> Result { use ctap2::client_pin::PinV1Subcommand as Subcommand; - debug_now!("processing CP"); + debug_now!("CTAP2.PIN..."); // info_now!("{:?}", parameters); + // TODO: Handle pin protocol V2 if parameters.pin_protocol != 1{ return Err(Error::InvalidParameter); } @@ -703,7 +528,7 @@ impl Authenticator for crate::Authenti Ok(match parameters.sub_command { Subcommand::GetRetries => { - debug_now!("processing CP.GR"); + debug_now!("CTAP2.Pin.GetRetries"); ctap2::client_pin::Response { key_agreement: None, @@ -713,7 +538,7 @@ impl Authenticator for crate::Authenti } Subcommand::GetKeyAgreement => { - debug_now!("processing CP.GKA"); + debug_now!("CTAP2.Pin.GetKeyAgreement"); let private_key = self.state.runtime.key_agreement_key(&mut self.trussed); let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; @@ -731,7 +556,7 @@ impl Authenticator for crate::Authenti } Subcommand::SetPin => { - debug_now!("processing CP.SP"); + debug_now!("CTAP2.Pin.SetPin"); // 1. check mandatory parameters let platform_kek = match parameters.key_agreement.as_ref() { Some(key) => key, @@ -778,7 +603,7 @@ impl Authenticator for crate::Authenti } Subcommand::ChangePin => { - debug_now!("processing CP.CP"); + debug_now!("CTAP2.Pin.ChangePin"); // 1. check mandatory parameters let platform_kek = match parameters.key_agreement.as_ref() { @@ -835,7 +660,7 @@ impl Authenticator for crate::Authenti } Subcommand::GetPinToken => { - debug_now!("processing CP.GPT"); + debug_now!("CTAP2.Pin.GetPinToken"); // 1. check mandatory parameters let platform_kek = match parameters.key_agreement.as_ref() { @@ -892,7 +717,8 @@ impl Authenticator for crate::Authenti }) } - fn credential_management(&mut self, parameters: &ctap2::credential_management::Parameters) + #[inline(never)] + fn credential_management(&mut self, parameters: &ctap2::credential_management::Request) -> Result { use ctap2::credential_management::Subcommand; @@ -946,6 +772,7 @@ impl Authenticator for crate::Authenti } } + #[inline(never)] fn vendor(&mut self, op: VendorOperation) -> Result<()> { info_now!("hello VO {:?}", &op); match op.into() { @@ -957,11 +784,176 @@ impl Authenticator for crate::Authenti } + #[inline(never)] + fn get_assertion(&mut self, parameters: &ctap2::get_assertion::Request) -> Result { + + debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); + + let rp_id_hash = self.hash(¶meters.rp_id.as_ref()); + + // 1-4. + let uv_performed = match self.pin_prechecks( + ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, + ¶meters.client_data_hash.as_ref(), + ) { + Ok(b) => b, + Err(Error::PinRequired) => { + // UV is optional for get_assertion + false + } + Err(err) => return Err(err), + }; + + // 5. Locate eligible credentials + // + // Note: If allowList is passed, credential is Some(credential) + // If no allowList is passed, credential is None and the retrieved credentials + // are stored in state.runtime.credential_heap + let (credential, num_credentials) = self.prepare_credentials( + &rp_id_hash, ¶meters.allow_list, uv_performed + ).ok_or(Error::NoCredentials)?; + + info_now!("found {:?} applicable credentials", num_credentials); + + // 6. process any options present + + // UP occurs by default, but option could specify not to. + let do_up = if parameters.options.is_some() { + parameters.options.as_ref().unwrap().up.unwrap_or(true) + } else { + true + }; + + // 7. collect user presence + let up_performed = if do_up { + info_now!("asking for up"); + self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + true + } else { + info_now!("not asking for up"); + false + }; + + let multiple_credentials = num_credentials > 1; + self.state.runtime.active_get_assertion = Some(state::ActiveGetAssertionData { + rp_id_hash: { + let mut buf = [0u8; 32]; + buf.copy_from_slice(&rp_id_hash); + buf + }, + client_data_hash: { + let mut buf = [0u8; 32]; + buf.copy_from_slice(¶meters.client_data_hash); + buf + }, + uv_performed, + up_performed, + multiple_credentials, + extensions: parameters.extensions.clone(), + }); + + let num_credentials = match num_credentials { + 1 => None, + n => Some(n as u32), + }; + + self.assert_with_credential(num_credentials, credential) + } + } // impl Authenticator for crate::Authenticator impl crate::Authenticator { + #[inline(never)] + fn check_credential_applicable(&mut self, credential: &Credential, allowlist_passed: bool, uv_performed: bool) -> bool { + + if !self.check_key_exists(credential.algorithm, &credential.key) { + return false; + } + + if !{ + use credential::CredentialProtectionPolicy as Policy; + debug_now!("CredentialProtectionPolicy {:?}", &credential.cred_protect); + match credential.cred_protect { + None | Some(Policy::Optional) => true, + Some(Policy::OptionalWithCredentialIdList) => allowlist_passed || uv_performed, + Some(Policy::Required) => uv_performed, + + } + } { + return false; + } + return true; + } + + #[inline(never)] + fn prepare_credentials( + &mut self, rp_id_hash: &Bytes<32>, + allow_list: &Option, + uv_performed: bool, + ) -> Option<(Credential, u32)> + { + debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); + + self.state.runtime.clear_credential_cache(); + self.state.runtime.active_get_assertion = None; + + if let Some(allow_list) = allow_list { + // we will have at most one credential, and an empty cache. + + for credential_id in allow_list { + let credential = match Credential::try_from(self, rp_id_hash, credential_id) { + Ok(credential) => credential, + _ => continue, + }; + + if !self.check_credential_applicable(&credential, true, uv_performed) { + continue; + } + + return Some((credential, 1)); + } + return None; + } else { + // we are only dealing with discoverable credentials. + + let mut maybe_path = syscall!(self.trussed.read_dir_first( + Location::Internal, + rp_rk_dir(&rp_id_hash), + None, + )).entry.map(|entry| PathBuf::try_from(entry.file_name()).unwrap()); + + use core::str::FromStr; + use crate::state::CachedCredential; + + while let Some(path) = maybe_path { + let credential_data = syscall!(self.trussed.read_file( + Location::Internal, + path.clone(), + )).data; + + let credential = Credential::deserialize(&credential_data).ok()?; + + if self.check_credential_applicable(&credential, false, uv_performed) { + self.state.runtime.push_credential(CachedCredential { + timestamp: credential.creation_time, + path: String::from_str(&path.as_str_ref_with_trailing_nul()).ok()?, + }); + } + + maybe_path = syscall!(self.trussed.read_dir_next()) + .entry.map(|entry| PathBuf::try_from(entry.file_name()).unwrap()); + } + + let num_credentials = self.state.runtime.remaining_credentials(); + let credential = self.state.runtime.pop_credential(&mut self.trussed); + credential.map(|credential| (credential, num_credentials)) + + } + + } + fn decrypt_pin_hash_and_maybe_escalate(&mut self, shared_secret: KeyId, pin_hash_enc: &Bytes<64>) -> Result<()> { @@ -1048,7 +1040,7 @@ impl crate::Authenticator // fn verify_pin_auth_using_token(&mut self, data: &[u8], pin_auth: &Bytes<16>) fn verify_pin_auth_using_token( &mut self, - parameters: &ctap2::credential_management::Parameters + parameters: &ctap2::credential_management::Request ) -> Result<()> { // info_now!("CM params: {:?}", parameters); @@ -1209,219 +1201,26 @@ impl crate::Authenticator Ok(false) } - /// If allow_list is some, select the first one (latest timestamp) that is usable, - /// and return Some(it). - /// - /// If allow_list is none, pull applicable credentials, store - /// in state's credential_heap, and return none #[inline(never)] - fn locate_credentials( - &mut self, rp_id_hash: &Bytes32, - allow_list: &Option, - uv_performed: bool, - ) - -> Result<()> - { - // validate allowList - info_now!("locating"); - let x = 0; - info_now!("addr(x) = {:p}", &x); - let mut allow_list_len = 0; - let allowed_credentials = if let Some(allow_list) = allow_list.as_ref() { - info_now!("x"); - allow_list_len = allow_list.len(); - info_now!("len: {}", allow_list_len); - allow_list.into_iter().enumerate() - // discard not properly serialized encrypted credentials - .filter_map(|(i, credential_descriptor)| { - info_now!( - "GA try from {}th cred id: {}", i, - hex_str!(&credential_descriptor.id), - ); - let cred_maybe = Credential::try_from( - self, rp_id_hash, credential_descriptor) - .ok(); - info_now!("cred_maybe: {:?}", &cred_maybe); - cred_maybe - } ) - .collect() - } else { - info_now!("y"); - CredentialList::new() - }; - - info_now!("new min_heap"); - let mut min_heap = MinCredentialHeap::new(); - - let allowed_credentials_passed = allowed_credentials.len() > 0; - info_now!("a"); - - if allowed_credentials_passed { - info_now!("if"); - // "If an allowList is present and is non-empty, - // locate all denoted credentials present on this authenticator - // and bound to the specified rpId." - debug_now!("allowedList passed with {} creds", allowed_credentials.len()); - let mut rk_count = 0; - info_now!("1"); - let mut applicable_credentials: CredentialList = allowed_credentials - .into_iter().enumerate() - .map(|(i, credential)| { info_now!("{}th", i); credential }) - .filter(|credential| match credential.key.clone() { - // TODO: should check if wrapped key is valid AEAD - // On the other hand, we already decrypted a valid AEAD - Key::WrappedKey(_) => true, - Key::ResidentKey(key) => { - debug_now!("checking if ResidentKey {:?} exists", &key); - let exists = match credential.algorithm { - -7 => syscall!(self.trussed.exists(Mechanism::P256, key)).exists, - -8 => syscall!(self.trussed.exists(Mechanism::Ed255, key)).exists, - // -9 => { - // let exists = syscall!(self.trussed.exists(Mechanism::Totp, key)).exists; - // info_now!("found it"); - // exists - // } - _ => false, - }; - if exists { - rk_count = rk_count + 1; - } - exists - } - }) - .filter(|credential| { - use credential::CredentialProtectionPolicy as Policy; - debug_now!("CredentialProtectionPolicy {:?}", &credential.cred_protect); - match credential.cred_protect { - None | Some(Policy::Optional) => true, - Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, - Some(Policy::Required) => uv_performed, - - } - }) - .collect(); - info_now!("2"); - while applicable_credentials.len() > 0 { - // Store all other applicable credentials in volatile storage and add to our - // credential heap. - let credential = applicable_credentials.pop().unwrap(); - - if min_heap.capacity() > min_heap.len() { - min_heap.push(credential).map_err(drop).unwrap(); - } else { - if credential > min_heap.peek().unwrap() { - min_heap.pop().unwrap(); - min_heap.push(credential).map_err(drop).unwrap(); - } - } - // If more than one credential was located in step 1 and allowList is present and not empty, - // select any applicable credential and proceed to step 12. Otherwise, order the credentials - // by the time when they were created in reverse order. - // The first credential is the most recent credential that was created. - if rk_count > 1 { - break - } - - } - } else if allow_list_len == 0 { - info_now!("else"); - // If an allowList is not present, - // locate all credentials that are present on this authenticator - // and bound to the specified rpId; sorted by reverse creation time - - // let rp_id_hash = self.hash(rp_id.as_ref()); - - // - // So here's the idea: - // - // - credentials can be pretty big - // - we declare N := MAX_CREDENTIAL_COUNT_IN_LIST in GetInfo - // - potentially there are more RKs for a given RP (a bit academic ofc) - // - // - first, we use a min-heap to keep only the topN credentials: - // if our "next" one is larger/later than the min of the heap, - // pop this min and push ours - // - // - then, we use a max-heap to sort the remaining <=N credentials - // - these then go into a CredentialList - // - (we don't need to keep that around even) - // - debug_now!("no allowedList passed"); - - // let mut credentials = CredentialList::new(); - - let data = syscall!(self.trussed.read_dir_files_first( - Location::Internal, - rp_rk_dir(&rp_id_hash), - None, - )).data; - - let data = match data { - Some(data) => data, - None => return Err(Error::NoCredentials), - }; - - let credential = Credential::deserialize(&data).unwrap(); - - use credential::CredentialProtectionPolicy as Policy; - let keep = match credential.cred_protect { - None | Some(Policy::Optional) => true, - Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, - Some(Policy::Required) => uv_performed, - }; - - if keep { - min_heap.push(credential).map_err(drop).unwrap(); - // info_now!("first: {:?}", &self.hash(&id.0)); - } - - loop { - let data = syscall!(self.trussed.read_dir_files_next()).data; - let data = match data { - Some(data) => data, - None => break, - }; - - let credential = Credential::deserialize(&data).unwrap(); - - let keep = match credential.cred_protect { - None | Some(Policy::Optional) => true, - Some(Policy::OptionalWithCredentialIdList) => allowed_credentials_passed || uv_performed, - Some(Policy::Required) => uv_performed, - }; - - if keep { - - if min_heap.capacity() > min_heap.len() { - min_heap.push(credential).map_err(drop).unwrap(); - } else { - if credential > min_heap.peek().unwrap() { - min_heap.pop().unwrap(); - min_heap.push(credential).map_err(drop).unwrap(); - } - } + fn check_key_exists(&mut self, alg: i32, key: &Key) -> bool { + match key { + // TODO: should check if wrapped key is valid AEAD + // On the other hand, we already decrypted a valid AEAD + Key::WrappedKey(_) => true, + Key::ResidentKey(key) => { + debug_now!("checking if ResidentKey {:?} exists", key); + match alg { + -7 => syscall!(self.trussed.exists(Mechanism::P256, key.clone())).exists, + -8 => syscall!(self.trussed.exists(Mechanism::Ed255, key.clone())).exists, + // -9 => { + // let exists = syscall!(self.trussed.exists(Mechanism::Totp, key)).exists; + // info_now!("found it"); + // exists + // } + _ => false, } } - - }; - info_now!("near end of locating"); - - // "If no applicable credentials were found, return CTAP2_ERR_NO_CREDENTIALS" - if min_heap.is_empty() { - return Err(Error::NoCredentials); - } - - // now sort them - info_now!("freeing heap"); - self.state.runtime.free_credential_heap(); - info_now!("freed"); - let max_heap = self.state.runtime.credential_heap(); - while !min_heap.is_empty() { - max_heap.push(min_heap.pop().unwrap()).map_err(drop).unwrap(); } - info_now!("aight"); - - Ok(()) } #[inline(never)] @@ -1497,6 +1296,7 @@ impl crate::Authenticator } + #[inline(never)] fn assert_with_credential(&mut self, num_credentials: Option, credential: Credential) -> Result { @@ -1615,9 +1415,10 @@ impl crate::Authenticator Ok(response) } + #[inline(never)] fn delete_resident_key_by_user_id( &mut self, - rp_id_hash: &Bytes32, + rp_id_hash: &Bytes<32>, user_id: &Bytes<64>, ) -> Result<()> { @@ -1676,6 +1477,7 @@ impl crate::Authenticator } + #[inline(never)] pub(crate) fn delete_resident_key_by_path( &mut self, rk_path: &Path, diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 8ba81e0..1d3e80b 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -11,21 +11,14 @@ use trussed::{ }; use ctap_types::{ - Bytes32, - authenticator::{ - Error, - ctap2::{ - self, - credential_management::{ - CredentialProtectionPolicy, - Response, - }, - }, + heapless_bytes::Bytes, + Error, + ctap2::credential_management::{ + CredentialProtectionPolicy, + Response, }, cose::PublicKey, - webauthn::{ - PublicKeyCredentialDescriptor, - }, + webauthn::PublicKeyCredentialDescriptor, }; use littlefs2::path::{Path, PathBuf}; @@ -79,7 +72,7 @@ where UP: UserPresence, { pub fn get_creds_metadata(&mut self) -> Result { info!("get metadata"); - let mut response: ctap2::credential_management::Response = + let mut response: Response = Default::default(); let guesstimate = self.state.persistent @@ -139,8 +132,7 @@ where UP: UserPresence, // rpIDHash (0x04) : RP ID SHA-256 hash. // totalRPs (0x05) : Total number of RPs present on the authenticator. - let mut response: ctap2::credential_management::Response = - Default::default(); + let mut response: Response = Default::default(); let dir = PathBuf::from(b"rk"); @@ -235,7 +227,7 @@ where UP: UserPresence, return Err(Error::NotAllowed); } - let mut response: ctap2::credential_management::Response = Default::default(); + let mut response: Response = Default::default(); if let Some(rp) = maybe_next_rp { // load credential and extract rp and rpIdHash @@ -298,7 +290,7 @@ where UP: UserPresence, Ok((num_rks, first_rk)) } - pub fn first_credential(&mut self, rp_id_hash: &Bytes32) -> Result { + pub fn first_credential(&mut self, rp_id_hash: &Bytes<32>) -> Result { info!("first credential"); self.state.runtime.cached_rk = None; @@ -403,8 +395,7 @@ where UP: UserPresence, .map_err(|_| Error::InvalidCredential)?; // now fill response - let mut response: ctap2::credential_management::Response = - Default::default(); + let mut response: Response = Default::default(); response.user = Some(credential.data.user.clone()); @@ -509,7 +500,7 @@ where UP: UserPresence, ); } // just return OK - let response: ctap2::credential_management::Response = Default::default(); + let response = Default::default(); Ok(response) } } diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs new file mode 100644 index 0000000..e0ec212 --- /dev/null +++ b/src/ctap2/pin.rs @@ -0,0 +1,8 @@ +// TODO: extract this, like credential_management.rs + +pub(crate) struct ClientPin<'a, UP, T> +where UP: UserPresence, +{ + authnr: &'a mut Authenticator, +} + diff --git a/src/dispatch.rs b/src/dispatch.rs new file mode 100644 index 0000000..2b80cad --- /dev/null +++ b/src/dispatch.rs @@ -0,0 +1,141 @@ +//! Dispatch of incoming requests over CTAPHID or NFC APDUs into CTAP1 and CTAP2. + +pub mod apdu; +pub mod ctaphid; + +use crate::{Authenticator, TrussedRequirements, UserPresence}; +#[allow(unused_imports)] +use crate::msp; + +use ctap_types::{ctap1, ctap2}; +use iso7816::Status; + +impl iso7816::App for Authenticator +where UP: UserPresence, +{ + fn aid(&self) -> iso7816::Aid { + iso7816::Aid::new(&[ 0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01]) + } +} + + +#[inline(never)] +/// Deserialize U2F, call authenticator, serialize response *Result*. +fn handle_ctap1(authenticator: &mut Authenticator, data: &[u8], response: &mut apdu_dispatch::response::Data) +where + T: TrussedRequirements, + UP: UserPresence, +{ + debug_now!("handle CTAP1: remaining stack: {} bytes", msp() - 0x2000_0000); + debug_now!("1A SP: {:X}", msp()); + match try_handle_ctap1(authenticator, data, response) { + Ok(()) => { + debug!("U2F response {} bytes", response.len()); + // Need to add x9000 success code (normally the apdu-dispatch does this, but + // since u2f uses apdus over ctaphid, we must do it here.) + response.extend_from_slice(&[0x90, 0x00]).ok(); + }, + Err(status) => { + let code: [u8; 2] = status.into(); + debug_now!("CTAP1 error: {:?} ({})", status, hex_str!(&code)); + response.extend_from_slice(&code).ok(); + }, + } + debug_now!("1B SP: {:X}", msp()); + debug_now!("end handle CTAP1"); +} + +#[inline(never)] +/// Deserialize CBOR, call authenticator, serialize response *Result*. +fn handle_ctap2(authenticator: &mut Authenticator, data: &[u8], response: &mut apdu_dispatch::response::Data) +where + T: TrussedRequirements, + UP: UserPresence, +{ + debug_now!("handle CTAP2: remaining stack: {} bytes", msp() - 0x2000_0000); + debug_now!("2A SP: {:X}", msp()); + if let Err(error) = try_handle_ctap2(authenticator, data, response) { + debug_now!("CTAP2 error: {})", error); + response.push(error).ok(); + } + debug_now!("2B SP: {:X}", msp()); + debug_now!("end handle CTAP2"); +} + +#[inline(never)] +fn try_handle_ctap1(authenticator: &mut Authenticator, data: &[u8], response: &mut apdu_dispatch::response::Data) + -> Result<(), Status> +where + T: TrussedRequirements, + UP: UserPresence, +{ + // Annoyance: We can't load in fido-authenticator constructor. + authenticator.state.persistent.load_if_not_initialised(&mut authenticator.trussed); + + // let command = apdu_dispatch::Command::try_from(data) + // .map_err(|_| Status::IncorrectDataParameter)?; + // let ctap_request = ctap1::Request::try_from(&command) + // .map_err(|_| Status::IncorrectDataParameter)?; + // let ctap_response = ctap1::Authenticator::call_ctap1(authenticator, &ctap_request)?; + + // Goal of these nested scopes is to keep stack small. + let ctap_response = { + let ctap_request = { + let command = apdu_dispatch::Command::try_from(data) + .map_err(|_| Status::IncorrectDataParameter)?; + debug_now!("1a SP: {:X}", msp()); + ctap1::Request::try_from(&command) + .map_err(|_| Status::IncorrectDataParameter)? + }; + ctap1::Authenticator::call_ctap1(authenticator, &ctap_request)? + }; + debug_now!("1b SP: {:X}", msp()); + + ctap_response.serialize(response).ok(); + Ok(()) +} + +#[inline(never)] +fn try_handle_ctap2(authenticator: &mut Authenticator, data: &[u8], response: &mut apdu_dispatch::response::Data) + -> Result<(), u8> +where + T: TrussedRequirements, + UP: UserPresence, +{ + // Annoyance: We can't load in fido-authenticator constructor. + authenticator.state.persistent.load_if_not_initialised(&mut authenticator.trussed); + + debug_now!("try_handle CTAP2: remaining stack: {} bytes", msp() - 0x2000_0000); + + // let ctap_request = ctap2::Request::deserialize(data) + // .map_err(|error| error as u8)?; + // let ctap_response = ctap2::Authenticator::call_ctap2(authenticator, &ctap_request) + // .map_err(|error| error as u8)?; + + // Goal of these nested scopes is to keep stack small. + let ctap_response = try_get_ctap2_response(authenticator, data)?; + ctap_response.serialize(response); + Ok(()) +} + +#[inline(never)] +fn try_get_ctap2_response(authenticator: &mut Authenticator, data: &[u8]) + -> Result +where + T: TrussedRequirements, + UP: UserPresence, +{ + // Annoyance: We can't load in fido-authenticator constructor. + authenticator.state.persistent.load_if_not_initialised(&mut authenticator.trussed); + + debug_now!("try_get CTAP2: remaining stack: {} bytes", msp() - 0x2000_0000); + debug_now!("size(Request): {} bytes", core::mem::size_of::()); + + // Goal of these nested scopes is to keep stack small. + let ctap_request = ctap2::Request::deserialize(data) + .map_err(|error| error as u8)?; + debug_now!("2a SP: {:X}", msp()); + use ctap2::Authenticator; + authenticator.call_ctap2(&ctap_request) + .map_err(|error| error as u8) +} diff --git a/src/dispatch/apdu.rs b/src/dispatch/apdu.rs new file mode 100644 index 0000000..9e66fc0 --- /dev/null +++ b/src/dispatch/apdu.rs @@ -0,0 +1,82 @@ +use apdu_dispatch::{Command, response::Data, app as apdu}; +use ctaphid_dispatch::app as ctaphid; +use ctap_types::{ + Error, + serde::error::Error as SerdeError, +}; +use iso7816::Status; + +use crate::{Authenticator, TrussedRequirements, UserPresence}; + +pub enum CtapMappingError { + InvalidCommand(u8), + ParsingError(SerdeError), +} + +impl From for Error { + fn from(mapping_error: CtapMappingError) -> Error { + match mapping_error { + CtapMappingError::InvalidCommand(_cmd) => { + Error::InvalidCommand + } + CtapMappingError::ParsingError(cbor_error) => { + match cbor_error { + SerdeError::SerdeMissingField => Error::MissingParameter, + _ => Error::InvalidCbor + } + } + } + + } +} + +impl apdu::App<{apdu_dispatch::command::SIZE}, {apdu_dispatch::response::SIZE} > +for Authenticator + where UP: UserPresence, + T: TrussedRequirements, +{ + + fn select(&mut self, _: &Command, reply: &mut Data) -> apdu::Result { + reply.extend_from_slice(b"U2F_V2").unwrap(); + Ok(()) + } + + fn deselect(&mut self) {} + + fn call(&mut self, interface: apdu::Interface, apdu: &Command, response: &mut Data) -> apdu::Result { + // FIDO-over-CCID does not seem to officially be a thing; we don't support it. + // If we would, need to review the following cases catering to semi-documented U2F legacy. + if interface != apdu::Interface::Contactless { + return Err(Status::ConditionsOfUseNotSatisfied); + } + + let instruction: u8 = apdu.instruction().into(); + + // Officially, only NFCCTAP_MSG (0x10) should occur, which is our FidoCommand::Cbor: + // + // + // However, for U2F legacy support (presumably very widespread), after registration + // "3. Client sends a command for an operation (register / authenticate)" + // + + Ok(match instruction { + // U2F instruction codes + // NB(nickray): I don't think 0x00 is a valid case. + 0x00 | 0x01 | 0x02 => super::handle_ctap1(self, apdu.data(), response),//self.call_authenticator_u2f(apdu, response), + + _ => { + match ctaphid::Command::try_from(instruction) { + // 0x10 + Ok(ctaphid::Command::Cbor) => super::handle_ctap2(self, apdu.data(), response), + Ok(ctaphid::Command::Msg) => super::handle_ctap1(self, apdu.data(), response), + Ok(ctaphid::Command::Deselect) => self.deselect(), + _ => { + info!("Unsupported ins for fido app {:02x}", instruction); + return Err(iso7816::Status::InstructionNotSupportedOrInvalid); + } + } + } + }) + } +} + diff --git a/src/dispatch/ctaphid.rs b/src/dispatch/ctaphid.rs new file mode 100644 index 0000000..77aa140 --- /dev/null +++ b/src/dispatch/ctaphid.rs @@ -0,0 +1,36 @@ +use ctaphid_dispatch::app as ctaphid; + +use crate::{Authenticator, TrussedRequirements, UserPresence}; +#[allow(unused_imports)] +use crate::msp; + +impl ctaphid::App for Authenticator +where UP: UserPresence, + T: TrussedRequirements, +{ + + fn commands(&self,) -> &'static [ctaphid::Command] { + &[ ctaphid::Command::Cbor, ctaphid::Command::Msg ] + } + + #[inline(never)] + fn call(&mut self, command: ctaphid::Command, request: &ctaphid::Message, response: &mut ctaphid::Message) -> ctaphid::AppResult { + + debug_now!("ctaphid-dispatch: remaining stack: {} bytes", msp() - 0x2000_0000); + + if request.len() < 1 { + return Err(ctaphid::Error::InvalidLength); + } + + // info_now!("request: "); + // blocking::dump_hex(request, request.len()); + Ok(match command { + + ctaphid::Command::Cbor => super::handle_ctap2(self, request, response), + ctaphid::Command::Msg => super::handle_ctap1(self, request, response), + _ => { + debug_now!("ctaphid trying to dispatch {:?}", command); + } + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index ca443f8..30b4063 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,24 +21,23 @@ use trussed::{ }; use ctap_types::{ - Bytes, + heapless_bytes::Bytes, authenticator::{Request, Response}, }; /// Re-export of `ctap-types` authenticator errors. -pub use ctap_types::authenticator::Error; +pub use ctap_types::Error; pub mod ctap1; pub mod ctap2; +#[cfg(feature="dispatch")] +pub mod dispatch; + pub mod constants; pub mod credential; pub mod state; -pub use ctap1::Authenticator as Ctap1Authenticator; -pub use ctap2::Authenticator as Ctap2Authenticator; - - /// Results with our [`Error`]. pub type Result = core::result::Result; @@ -103,6 +102,22 @@ fn format_hex(data: &[u8], mut buffer: &mut [u8]) { } } +// NB: to actually use this, replace the constant implementation with the inline assembly. +// Once we move to a new cortex-m release, can use the version from there. +// +// use core::arch::asm; + +// #[inline] +// pub fn msp() -> u32 { +// let r; +// unsafe { asm!("mrs {}, MSP", out(reg) r, options(nomem, nostack, preserves_flags)) }; +// r +// } + +#[inline] +#[allow(dead_code)] +pub(crate) fn msp() -> u32 { 0x2000_000 } + /// Currently Ed25519 and P256. #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(i32)] @@ -184,15 +199,16 @@ where UP: UserPresence, match request { Request::Ctap2(request) => { + use ctap_types::ctap2::Authenticator as _; Ok(Response::Ctap2(self.call_ctap2(request)?)) } Request::Ctap1(_request) => { - // ctap_types::authenticator::ctap1::Request redefineds + // ctap_types::authenticator::ctap1::Request redefines // the already existing ctap_types::ctap1::Command // // need to merge todo!(); - // Ok(Response::Ctap1(self.call_u2f(request)?)) + // Ok(Response::Ctap1(self.call_ctap1(request)?)) } } } diff --git a/src/state.rs b/src/state.rs index 74807ca..92d92af 100644 --- a/src/state.rs +++ b/src/state.rs @@ -13,13 +13,14 @@ use trussed::{ }, }; use ctap_types::{ - Bytes32, - authenticator::Error, + Bytes, String, + Error, cose::EcdhEsHkdf256PublicKey as CoseEcdhEsHkdf256PublicKey, + // 2022-02-27: 10 credentials sizes::MAX_CREDENTIAL_COUNT_IN_LIST, // U8 currently }; -use heapless::binary_heap::{BinaryHeap, Max, Min}; +use heapless::binary_heap::{BinaryHeap, Max}; use littlefs2::path::PathBuf; use crate::{ @@ -28,8 +29,55 @@ use crate::{ Result, }; -pub type MaxCredentialHeap = BinaryHeap; -pub type MinCredentialHeap = BinaryHeap; +#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct CachedCredential { + pub timestamp: u32, + // PathBuf has length 255 + 1, we only need 36 + 1 + // with `rk/<16B rp_id>/<16B cred_id>` = 4 + 2*32 + pub path: String<37>, +} + +impl PartialOrd for CachedCredential { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CachedCredential { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.timestamp.cmp(&other.timestamp) + } +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct CredentialCacheGeneric(BinaryHeap); +impl CredentialCacheGeneric { + pub fn push(&mut self, item: CachedCredential) { + if self.0.len() == self.0.capacity() { + self.0.pop(); + } + // self.0.push(item).ok(); + self.0.push(item).map_err(drop).unwrap(); + } + + pub fn pop(&mut self) -> Option { + self.0.pop() + } + + pub fn len(&self) -> u32 { + self.0.len() as u32 + } + + pub fn is_empty(&mut self) -> bool { + self.0.is_empty() + } + + pub fn clear(&mut self) { + self.0.clear() + } +} + +pub type CredentialCache = CredentialCacheGeneric; #[derive(Clone, Debug, /*uDebug, Eq, PartialEq,*/ serde::Deserialize, serde::Serialize)] pub struct State { @@ -158,7 +206,7 @@ impl Identity { #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CredentialManagementEnumerateRps { pub remaining: u32, - pub rp_id_hash: Bytes32, + pub rp_id_hash: Bytes<32>, } #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] @@ -187,7 +235,7 @@ pub struct RuntimeState { consecutive_pin_mismatches: u8, // both of these are a cache for previous Get{Next,}Assertion call - cached_credentials: Option, + cached_credentials: CredentialCache, pub active_get_assertion: Option, channel: Option, pub cached_rp: Option, @@ -416,30 +464,35 @@ impl RuntimeState { self.consecutive_pin_mismatches >= Self::POWERCYCLE_RETRIES } - pub fn credential_heap(&mut self) -> &mut MaxCredentialHeap { - // Can't seem to avoid both borrow checker + unwrap - if self.cached_credentials.is_none() { - self.create_credential_heap() - } else { - self.cached_credentials.as_mut().unwrap() - } + // pub fn cached_credentials(&mut self) -> &mut CredentialCache { + // &mut self.cached_credentials + // // if let Some(cache) = self.cached_credentials.as_mut() { + // // return cache + // // } + // // self.cached_credentials.insert(CredentialCache::new()) + // } + + pub fn clear_credential_cache(&mut self) { + self.cached_credentials.clear() } - fn create_credential_heap(&mut self) -> &mut MaxCredentialHeap { - self.cached_credentials = Some(MaxCredentialHeap::new()); - self.cached_credentials.as_mut().unwrap() + pub fn push_credential(&mut self, credential: CachedCredential) { + self.cached_credentials.push(credential); } - pub fn free_credential_heap(&mut self) { - let heap = self.cached_credentials.take(); - if let Some(mut heap) = heap { - heap.clear(); - } + pub fn pop_credential(&mut self, trussed: &mut T) -> Option { + let cached_credential = self.cached_credentials.pop()?; + + let credential_data = syscall!(trussed.read_file( + Location::Internal, + PathBuf::from(cached_credential.path.as_str()), + )).data; + + Credential::deserialize(&credential_data).ok() } - pub fn pop_credential_from_heap(&mut self) -> Credential { - let max_heap = self.credential_heap(); - max_heap.pop().unwrap() + pub fn remaining_credentials(&self) -> u32 { + self.cached_credentials.len() as _ } pub fn key_agreement_key(&mut self, trussed: &mut T) -> KeyId { @@ -482,13 +535,12 @@ impl RuntimeState { pub fn reset(&mut self, trussed: &mut T) { // Could use `free_credential_heap`, but since we're deleting everything here, this is quicker. syscall!(trussed.delete_all(Location::Volatile)); - self.credential_heap().clear(); + self.clear_credential_cache(); + self.active_get_assertion = None; self.rotate_pin_token(trussed); self.rotate_key_agreement_key(trussed); - self.cached_credentials = None; - self.active_get_assertion = None; } pub fn generate_shared_secret(&mut self, trussed: &mut T, platform_key_agreement_key: &CoseEcdhEsHkdf256PublicKey) -> Result { @@ -521,23 +573,3 @@ impl RuntimeState { } } - -// #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -// pub struct TimestampPath { -// pub timestamp: u32, -// pub path: PathBuf, -// pub location: Location, -// } - -// impl Ord for TimestampPath { -// fn cmp(&self, other: &Self) -> Ordering { -// self.timestamp.cmp(&other.timestamp) -// } -// } - -// impl PartialOrd for TimestampPath { -// fn partial_cmp(&self, other: &Self) -> Option { -// Some(self.cmp(other)) -// } -// } - From 323301b8a7cd814562b30ff7ec53dd27f809878a Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Sat, 5 Mar 2022 21:59:14 +0100 Subject: [PATCH 07/13] Use path, not filename, to load RKs --- src/ctap2.rs | 7 +++++-- src/dispatch.rs | 13 ++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index d70760a..5804a99 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -814,6 +814,7 @@ impl Authenticator for crate::Authenti ).ok_or(Error::NoCredentials)?; info_now!("found {:?} applicable credentials", num_credentials); + info_now!("{:?}", &credential); // 6. process any options present @@ -900,6 +901,7 @@ impl crate::Authenticator self.state.runtime.active_get_assertion = None; if let Some(allow_list) = allow_list { + debug_now!("Allowlist passed, filtering"); // we will have at most one credential, and an empty cache. for credential_id in allow_list { @@ -917,12 +919,13 @@ impl crate::Authenticator return None; } else { // we are only dealing with discoverable credentials. + debug_now!("Allowlist not passed, fetching RKs"); let mut maybe_path = syscall!(self.trussed.read_dir_first( Location::Internal, rp_rk_dir(&rp_id_hash), None, - )).entry.map(|entry| PathBuf::try_from(entry.file_name()).unwrap()); + )).entry.map(|entry| PathBuf::try_from(entry.path()).unwrap()); use core::str::FromStr; use crate::state::CachedCredential; @@ -943,7 +946,7 @@ impl crate::Authenticator } maybe_path = syscall!(self.trussed.read_dir_next()) - .entry.map(|entry| PathBuf::try_from(entry.file_name()).unwrap()); + .entry.map(|entry| PathBuf::try_from(entry.path()).unwrap()); } let num_credentials = self.state.runtime.remaining_credentials(); diff --git a/src/dispatch.rs b/src/dispatch.rs index 2b80cad..7cbf454 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -27,7 +27,7 @@ where UP: UserPresence, { debug_now!("handle CTAP1: remaining stack: {} bytes", msp() - 0x2000_0000); - debug_now!("1A SP: {:X}", msp()); + // debug_now!("1A SP: {:X}", msp()); match try_handle_ctap1(authenticator, data, response) { Ok(()) => { debug!("U2F response {} bytes", response.len()); @@ -41,7 +41,7 @@ where response.extend_from_slice(&code).ok(); }, } - debug_now!("1B SP: {:X}", msp()); + // debug_now!("1B SP: {:X}", msp()); debug_now!("end handle CTAP1"); } @@ -53,12 +53,12 @@ where UP: UserPresence, { debug_now!("handle CTAP2: remaining stack: {} bytes", msp() - 0x2000_0000); - debug_now!("2A SP: {:X}", msp()); + // debug_now!("2A SP: {:X}", msp()); if let Err(error) = try_handle_ctap2(authenticator, data, response) { debug_now!("CTAP2 error: {})", error); response.push(error).ok(); } - debug_now!("2B SP: {:X}", msp()); + // debug_now!("2B SP: {:X}", msp()); debug_now!("end handle CTAP2"); } @@ -83,13 +83,13 @@ where let ctap_request = { let command = apdu_dispatch::Command::try_from(data) .map_err(|_| Status::IncorrectDataParameter)?; - debug_now!("1a SP: {:X}", msp()); + // debug_now!("1a SP: {:X}", msp()); ctap1::Request::try_from(&command) .map_err(|_| Status::IncorrectDataParameter)? }; ctap1::Authenticator::call_ctap1(authenticator, &ctap_request)? }; - debug_now!("1b SP: {:X}", msp()); + // debug_now!("1b SP: {:X}", msp()); ctap_response.serialize(response).ok(); Ok(()) @@ -129,7 +129,6 @@ where authenticator.state.persistent.load_if_not_initialised(&mut authenticator.trussed); debug_now!("try_get CTAP2: remaining stack: {} bytes", msp() - 0x2000_0000); - debug_now!("size(Request): {} bytes", core::mem::size_of::()); // Goal of these nested scopes is to keep stack small. let ctap_request = ctap2::Request::deserialize(data) From 98b17ee7737f7bc467e9f389e0651b433e081191 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Mon, 7 Mar 2022 23:03:57 +0100 Subject: [PATCH 08/13] Fixes after running solokeys/fido2-tests --- src/ctap2.rs | 138 ++++++++++++++++++++++++++-------------- src/dispatch.rs | 7 +- src/dispatch/ctaphid.rs | 1 + src/lib.rs | 27 +++++++- 4 files changed, 118 insertions(+), 55 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 5804a99..e5e8b93 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -77,17 +77,35 @@ impl Authenticator for crate::Authenti let mut pin_protocols = Vec::::new(); pin_protocols.push(1).unwrap(); - let mut options = ctap2::get_info::CtapOptions::default(); - options.rk = true; - options.up = true; - options.uv = None; // "uv" here refers to "in itself", e.g. biometric - // options.plat = false; - options.cred_mgmt = Some(true); - // options.client_pin = None; // not capable of PIN - options.client_pin = match self.state.persistent.pin_is_set() { - true => Some(true), - false => Some(false), + let options = ctap2::get_info::CtapOptions { + ep: Some(true), + rk: true, + up: true, + uv: None, + plat: Some(false), + cred_mgmt: Some(true), + client_pin: match self.state.persistent.pin_is_set() { + true => Some(true), + false => Some(false), + }, + credential_mgmt_preview: Some(true), + ..Default::default() }; + // options.rk = true; + // options.up = true; + // options.uv = None; // "uv" here refers to "in itself", e.g. biometric + // options.plat = Some(false); + // options.cred_mgmt = Some(true); + // options.credential_mgmt_preview = Some(true); + // // options.client_pin = None; // not capable of PIN + // options.client_pin = match self.state.persistent.pin_is_set() { + // true => Some(true), + // false => Some(false), + // }; + + let mut transports = Vec::new(); + transports.push(String::from("nfc")).unwrap(); + transports.push(String::from("usb")).unwrap(); let (_, aaguid)= self.state.identity.attestation(&mut self.trussed); @@ -96,8 +114,9 @@ impl Authenticator for crate::Authenti extensions: Some(extensions), aaguid: Bytes::from_slice(&aaguid).unwrap(), options: Some(options), + transports: Some(transports), // 1200 - max_msg_size: Some(ctap_types::sizes::REALISTIC_MAX_MESSAGE_SIZE), + max_msg_size: Some(self.config.max_msg_size), pin_protocols: Some(pin_protocols), max_creds_in_list: Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST), max_cred_id_length: Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH), @@ -489,6 +508,7 @@ impl Authenticator for crate::Authenti fn reset(&mut self) -> Result<()> { // 1. >10s after bootup -> NotAllowed let uptime = syscall!(self.trussed.uptime()).uptime; + debug_now!("uptime: {:?}", uptime); if uptime.as_secs() > 10 { #[cfg(not(feature = "disable-reset-time-window"))] return Err(Error::NotAllowed); @@ -514,6 +534,10 @@ impl Authenticator for crate::Authenti Ok(()) } + fn selection(&mut self) -> Result<()> { + self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT) + } + #[inline(never)] fn client_pin(&mut self, parameters: &ctap2::client_pin::Request) -> Result { use ctap2::client_pin::PinV1Subcommand as Subcommand; @@ -900,61 +924,76 @@ impl crate::Authenticator self.state.runtime.clear_credential_cache(); self.state.runtime.active_get_assertion = None; + // NB: CTAP 2.1 specifies to return the first applicable credential, and set + // numberOfCredentials to None. + // However, CTAP 2.0 says to send numberOfCredentials that are applicable, + // which implies we'd have to respond to GetNextAssertion. + // + // We are using CTAP 2.1 behaviour here, as it allows us not to cache the (length) + // credential IDs. Presumably, most clients use this to just get any old signatures, + // but we did change the github.com/solokeys/fido2-tests to accommodate this change + // of behaviour. if let Some(allow_list) = allow_list { - debug_now!("Allowlist passed, filtering"); + debug_now!("Allowlist of len {} passed, filtering", allow_list.len()); // we will have at most one credential, and an empty cache. - for credential_id in allow_list { - let credential = match Credential::try_from(self, rp_id_hash, credential_id) { - Ok(credential) => credential, - _ => continue, - }; + // client is not supposed to send Some(empty list): + // + // but some still do (and CTAP 2.0 does not rule it out). + // they probably meant to send None. + if allow_list.len() > 0 { + for credential_id in allow_list { + let credential = match Credential::try_from(self, rp_id_hash, credential_id) { + Ok(credential) => credential, + _ => continue, + }; + + if !self.check_credential_applicable(&credential, true, uv_performed) { + continue; + } - if !self.check_credential_applicable(&credential, true, uv_performed) { - continue; + return Some((credential, 1)); } - return Some((credential, 1)); + // we don't recognize any credentials in the allowlist + return None; } - return None; - } else { - // we are only dealing with discoverable credentials. - debug_now!("Allowlist not passed, fetching RKs"); + } - let mut maybe_path = syscall!(self.trussed.read_dir_first( - Location::Internal, - rp_rk_dir(&rp_id_hash), - None, - )).entry.map(|entry| PathBuf::try_from(entry.path()).unwrap()); + // we are only dealing with discoverable credentials. + debug_now!("Allowlist not passed, fetching RKs"); - use core::str::FromStr; - use crate::state::CachedCredential; + let mut maybe_path = syscall!(self.trussed.read_dir_first( + Location::Internal, + rp_rk_dir(&rp_id_hash), + None, + )).entry.map(|entry| PathBuf::try_from(entry.path()).unwrap()); - while let Some(path) = maybe_path { - let credential_data = syscall!(self.trussed.read_file( - Location::Internal, - path.clone(), - )).data; + use core::str::FromStr; + use crate::state::CachedCredential; - let credential = Credential::deserialize(&credential_data).ok()?; + while let Some(path) = maybe_path { + let credential_data = syscall!(self.trussed.read_file( + Location::Internal, + path.clone(), + )).data; - if self.check_credential_applicable(&credential, false, uv_performed) { - self.state.runtime.push_credential(CachedCredential { - timestamp: credential.creation_time, - path: String::from_str(&path.as_str_ref_with_trailing_nul()).ok()?, - }); - } + let credential = Credential::deserialize(&credential_data).ok()?; - maybe_path = syscall!(self.trussed.read_dir_next()) - .entry.map(|entry| PathBuf::try_from(entry.path()).unwrap()); + if self.check_credential_applicable(&credential, false, uv_performed) { + self.state.runtime.push_credential(CachedCredential { + timestamp: credential.creation_time, + path: String::from_str(&path.as_str_ref_with_trailing_nul()).ok()?, + }); } - let num_credentials = self.state.runtime.remaining_credentials(); - let credential = self.state.runtime.pop_credential(&mut self.trussed); - credential.map(|credential| (credential, num_credentials)) - + maybe_path = syscall!(self.trussed.read_dir_next()) + .entry.map(|entry| PathBuf::try_from(entry.path()).unwrap()); } + let num_credentials = self.state.runtime.remaining_credentials(); + let credential = self.state.runtime.pop_credential(&mut self.trussed); + credential.map(|credential| (credential, num_credentials)) } fn decrypt_pin_hash_and_maybe_escalate(&mut self, shared_secret: KeyId, pin_hash_enc: &Bytes<64>) @@ -1255,6 +1294,7 @@ impl crate::Authenticator self.verify_pin_auth(kek, &hmac_secret.salt_enc, &hmac_secret.salt_auth).map_err(|_| Error::ExtensionFirst)?; if hmac_secret.salt_enc.len() != 32 && hmac_secret.salt_enc.len() != 64 { + debug_now!("invalid hmac-secret length"); return Err(Error::InvalidLength); } diff --git a/src/dispatch.rs b/src/dispatch.rs index 7cbf454..6953d6c 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -55,7 +55,7 @@ where debug_now!("handle CTAP2: remaining stack: {} bytes", msp() - 0x2000_0000); // debug_now!("2A SP: {:X}", msp()); if let Err(error) = try_handle_ctap2(authenticator, data, response) { - debug_now!("CTAP2 error: {})", error); + debug_now!("CTAP2 error: {:02X}", error); response.push(error).ok(); } // debug_now!("2B SP: {:X}", msp()); @@ -76,7 +76,9 @@ where // .map_err(|_| Status::IncorrectDataParameter)?; // let ctap_request = ctap1::Request::try_from(&command) // .map_err(|_| Status::IncorrectDataParameter)?; + // drop(command); // let ctap_response = ctap1::Authenticator::call_ctap1(authenticator, &ctap_request)?; + // drop(ctap_request); // Goal of these nested scopes is to keep stack small. let ctap_response = { @@ -84,8 +86,7 @@ where let command = apdu_dispatch::Command::try_from(data) .map_err(|_| Status::IncorrectDataParameter)?; // debug_now!("1a SP: {:X}", msp()); - ctap1::Request::try_from(&command) - .map_err(|_| Status::IncorrectDataParameter)? + ctap1::Request::try_from(&command)? }; ctap1::Authenticator::call_ctap1(authenticator, &ctap_request)? }; diff --git a/src/dispatch/ctaphid.rs b/src/dispatch/ctaphid.rs index 77aa140..e6943a5 100644 --- a/src/dispatch/ctaphid.rs +++ b/src/dispatch/ctaphid.rs @@ -19,6 +19,7 @@ where UP: UserPresence, debug_now!("ctaphid-dispatch: remaining stack: {} bytes", msp() - 0x2000_0000); if request.len() < 1 { + debug_now!("invalid request length in ctaphid.call"); return Err(ctaphid::Error::InvalidLength); } diff --git a/src/lib.rs b/src/lib.rs index 30b4063..3bfbdf5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,25 @@ where T: // + client::Totp {} +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Config { + /// Typically determined by surrounding USB-level decoder. + /// For Solo 2, this is usbd-ctaphid (and its buffer size). + pub max_msg_size: usize, + // pub max_creds_in_list: usize, + // pub max_cred_id_length: usize, +} + +// impl Default for Config { +// fn default() -> Self { +// Self { +// max_message_size: ctap_types::sizes::REALISTIC_MAX_MESSAGE_SIZE, +// max_credential_count_in_list: ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST, +// max_credential_id_length: ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH, +// } +// } +// } + /// Trussed® app implementing a FIDO authenticator. /// /// It implements [`ctap1::Authenticator`] and [`ctap2::Authenticator`] traits, @@ -90,6 +109,7 @@ where trussed: T, state: state::State, up: UP, + config: Config, } // EWW.. this is a bit unsafe isn't it @@ -172,7 +192,8 @@ impl UserPresence for Conforming { fn user_present(self, trussed: &mut T, timeout_milliseconds: u32) -> Result<()> { let result = syscall!(trussed.confirm_user_present(timeout_milliseconds)).result; result.map_err(|err| match err { - trussed::types::consent::Error::TimedOut => Error::KeepaliveCancel, + trussed::types::consent::Error::TimedOut => Error::UserActionTimeout, + // trussed::types::consent::Error::TimedOut => Error::KeepaliveCancel, _ => Error::OperationDenied, }) } @@ -186,10 +207,10 @@ impl Authenticator where UP: UserPresence, T: TrussedRequirements, { - pub fn new(trussed: T, up: UP) -> Self { + pub fn new(trussed: T, up: UP, config: Config) -> Self { let state = state::State::new(); - let authenticator = Self { trussed, state, up }; + let authenticator = Self { trussed, state, up, config }; authenticator } From 9e4b7e806ace8a8eed2286fa0eb8b8be86b813bd Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Tue, 8 Mar 2022 23:29:18 +0100 Subject: [PATCH 09/13] No enterprise attestation support just yet --- src/ctap2.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index e5e8b93..a786156 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -78,7 +78,7 @@ impl Authenticator for crate::Authenti pin_protocols.push(1).unwrap(); let options = ctap2::get_info::CtapOptions { - ep: Some(true), + ep: None, rk: true, up: true, uv: None, From c4e4b7950272faaf51784782a1fd1f969e230666 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 17 Mar 2022 02:17:51 +0100 Subject: [PATCH 10/13] Clippy round --- Cargo.toml | 3 ++ src/constants.rs | 2 +- src/credential.rs | 15 ++++--- src/ctap1.rs | 2 +- src/ctap2.rs | 72 +++++++++++++++--------------- src/ctap2/credential_management.rs | 31 +++++++------ src/dispatch.rs | 4 +- src/lib.rs | 26 ++++++----- src/state.rs | 18 ++++---- 9 files changed, 93 insertions(+), 80 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96f18ec..7377347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,3 +41,6 @@ log-error = [] [dev-dependencies] # quickcheck = "1" rand = "0.8.4" + +[package.metadata.docs.rs] +features = ["dispatch"] diff --git a/src/constants.rs b/src/constants.rs index f671a78..6271354 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -3,7 +3,7 @@ use trussed::types::{CertId, KeyId}; pub const FIDO2_UP_TIMEOUT: u32 = 30_000; -pub const U2F_UP_TIMEOUT: u32 = 0_250; +pub const U2F_UP_TIMEOUT: u32 = 250; pub const ATTESTATION_CERT_ID: CertId = CertId::from_special(0); pub const ATTESTATION_KEY_ID: KeyId = KeyId::from_special(0); diff --git a/src/credential.rs b/src/credential.rs index f74b286..4ea6413 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -175,10 +175,10 @@ impl PartialOrd<&Credential> for Credential { // Bad idea - huge stack // pub(crate) type CredentialList = Vec; -impl Into for CredentialId { - fn into(self) -> PublicKeyCredentialDescriptor { +impl From for PublicKeyCredentialDescriptor { + fn from(id: CredentialId) -> PublicKeyCredentialDescriptor { PublicKeyCredentialDescriptor { - id: self.0, + id: id.0, key_type: { let mut key_type = String::new(); key_type.push_str("public-key").unwrap(); @@ -189,6 +189,7 @@ impl Into for CredentialId { } impl Credential { + #[allow(clippy::too_many_arguments)] pub fn new( ctap: CtapVersion, // parameters: &ctap2::make_credential::Parameters, @@ -210,7 +211,7 @@ impl Credential { creation_time: timestamp, use_counter: true, - algorithm: algorithm, + algorithm, key, hmac_secret, @@ -237,7 +238,7 @@ impl Credential { // the ID will stay below 255 bytes. // // Existing keyhandles can still be decoded - pub fn id<'a, T: client::Chacha8Poly1305 + client::Sha256>( + pub fn id( &self, trussed: &mut T, key_encryption_key: KeyId, @@ -252,7 +253,7 @@ impl Credential { let rp_id_hash: Bytes<32> = if let Some(hash) = rp_id_hash { hash.clone() } else { - syscall!(trussed.hash_sha256(&self.rp.id.as_ref())) + syscall!(trussed.hash_sha256(self.rp.id.as_ref())) .hash .to_bytes().map_err(|_| Error::Other)? }; @@ -268,7 +269,7 @@ impl Credential { } pub fn serialize(&self) -> Result { - Ok(trussed::cbor_serialize_bytes(self).map_err(|_| Error::Other)?) + trussed::cbor_serialize_bytes(self).map_err(|_| Error::Other) } pub fn deserialize(bytes: &SerializedCredential) -> Result { diff --git a/src/ctap1.rs b/src/ctap1.rs index 2df1cd5..bab737d 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -1,4 +1,4 @@ -//! The `ctap1::Authenticator` trait and its implementation. +//! The `ctap_types::ctap1::Authenticator` implementation. use ctap_types::{ ctap1::{ diff --git a/src/ctap2.rs b/src/ctap2.rs index a786156..1a4c663 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,4 +1,4 @@ -//! The `ctap2::Authenticator` trait and its implementation. +//! The `ctap_types::ctap2::Authenticator` implementation. use ctap_types::{ heapless::{String, Vec}, @@ -158,7 +158,7 @@ impl Authenticator for crate::Authenti #[inline(never)] fn make_credential(&mut self, parameters: &ctap2::make_credential::Request) -> Result { - let rp_id_hash = self.hash(¶meters.rp.id.as_ref()); + let rp_id_hash = self.hash(parameters.rp.id.as_ref()); // 1-4. if let Some(options) = parameters.options.as_ref() { @@ -169,7 +169,7 @@ impl Authenticator for crate::Authenti } let uv_performed = self.pin_prechecks( ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, - ¶meters.client_data_hash.as_ref(), + parameters.client_data_hash.as_ref(), )?; // 5. "persist credProtect value for this credential" @@ -183,7 +183,7 @@ impl Authenticator for crate::Authenti if let Ok(excluded_cred) = result { use credential::CredentialProtectionPolicy; // If UV is not performed, than CredProtectRequired credentials should not be visibile. - if !(excluded_cred.cred_protect == Some(CredentialProtectionPolicy::Required) && !uv_performed) { + if !(excluded_cred.cred_protect == Some(CredentialProtectionPolicy::Required)) || uv_performed { info_now!("Excluded!"); self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; return Err(Error::CredentialExcluded); @@ -261,7 +261,7 @@ impl Authenticator for crate::Authenti private_key = syscall!(self.trussed.generate_p256_private_key(location)).key; public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, public_key.clone(), KeySerialization::Cose + Mechanism::P256, public_key, KeySerialization::Cose )).serialized_key; let _success = syscall!(self.trussed.delete(public_key)).success; info_now!("deleted public P256 key: {}", _success); @@ -270,7 +270,7 @@ impl Authenticator for crate::Authenti private_key = syscall!(self.trussed.generate_ed255_private_key(location)).key; public_key = syscall!(self.trussed.derive_ed255_public_key(private_key, Location::Volatile)).key; cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::Ed255, public_key.clone(), KeySerialization::Cose + Mechanism::Ed255, public_key, KeySerialization::Cose )).serialized_key; let _success = syscall!(self.trussed.delete(public_key)).success; info_now!("deleted public Ed25519 key: {}", _success); @@ -330,7 +330,7 @@ impl Authenticator for crate::Authenti algorithm as i32, key_parameter, self.state.persistent.timestamp(&mut self.trussed)?, - hmac_secret_requested.clone(), + hmac_secret_requested, cred_protect_requested, nonce, ); @@ -346,11 +346,11 @@ impl Authenticator for crate::Authenti self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id).ok(); // then store key, making it resident - let credential_id_hash = self.hash(&credential_id.0.as_ref()); + let credential_id_hash = self.hash(credential_id.0.as_ref()); try_syscall!(self.trussed.write_file( Location::Internal, rk_path(&rp_id_hash, &credential_id_hash), - serialized_credential.clone(), + serialized_credential, // user attribute for later easy lookup // Some(rp_id_hash.clone()), None, @@ -398,8 +398,8 @@ impl Authenticator for crate::Authenti extensions: { if hmac_secret_requested.is_some() || cred_protect_requested.is_some() { Some(ctap2::make_credential::Extensions { - cred_protect: parameters.extensions.as_ref().unwrap().cred_protect.clone(), - hmac_secret: parameters.extensions.as_ref().unwrap().hmac_secret.clone(), + cred_protect: parameters.extensions.as_ref().unwrap().cred_protect, + hmac_secret: parameters.extensions.as_ref().unwrap().hmac_secret, }) } else { @@ -567,7 +567,7 @@ impl Authenticator for crate::Authenti let private_key = self.state.runtime.key_agreement_key(&mut self.trussed); let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; let serialized_cose_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, public_key.clone(), KeySerialization::EcdhEsHkdf256)).serialized_key; + Mechanism::P256, public_key, KeySerialization::EcdhEsHkdf256)).serialized_key; let cose_key = trussed::cbor_deserialize(&serialized_cose_key).unwrap(); syscall!(self.trussed.delete(public_key)); @@ -663,7 +663,7 @@ impl Authenticator for crate::Authenti self.state.decrement_retries(&mut self.trussed)?; // 6. decrypt pinHashEnc, compare with stored - self.decrypt_pin_hash_and_maybe_escalate(shared_secret, &pin_hash_enc)?; + self.decrypt_pin_hash_and_maybe_escalate(shared_secret, pin_hash_enc)?; // 7. reset retries self.state.reset_retries(&mut self.trussed)?; @@ -706,7 +706,7 @@ impl Authenticator for crate::Authenti self.state.decrement_retries(&mut self.trussed)?; // 5. decrypt and verify pinHashEnc - self.decrypt_pin_hash_and_maybe_escalate(shared_secret, &pin_hash_enc)?; + self.decrypt_pin_hash_and_maybe_escalate(shared_secret, pin_hash_enc)?; // 6. reset retries self.state.reset_retries(&mut self.trussed)?; @@ -749,7 +749,7 @@ impl Authenticator for crate::Authenti use credential_management as cm; // TODO: I see "failed pinauth" output, but then still continuation... - self.verify_pin_auth_using_token(¶meters)?; + self.verify_pin_auth_using_token(parameters)?; let mut cred_mgmt = cm::CredentialManagement::new(self); let sub_parameters = ¶meters.sub_command_params; @@ -813,12 +813,12 @@ impl Authenticator for crate::Authenti debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); - let rp_id_hash = self.hash(¶meters.rp_id.as_ref()); + let rp_id_hash = self.hash(parameters.rp_id.as_ref()); // 1-4. let uv_performed = match self.pin_prechecks( ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, - ¶meters.client_data_hash.as_ref(), + parameters.client_data_hash.as_ref(), ) { Ok(b) => b, Err(Error::PinRequired) => { @@ -909,7 +909,7 @@ impl crate::Authenticator } { return false; } - return true; + true } #[inline(never)] @@ -941,7 +941,7 @@ impl crate::Authenticator // // but some still do (and CTAP 2.0 does not rule it out). // they probably meant to send None. - if allow_list.len() > 0 { + if !allow_list.is_empty() { for credential_id in allow_list { let credential = match Credential::try_from(self, rp_id_hash, credential_id) { Ok(credential) => credential, @@ -965,7 +965,7 @@ impl crate::Authenticator let mut maybe_path = syscall!(self.trussed.read_dir_first( Location::Internal, - rp_rk_dir(&rp_id_hash), + rp_rk_dir(rp_id_hash), None, )).entry.map(|entry| PathBuf::try_from(entry.path()).unwrap()); @@ -983,7 +983,7 @@ impl crate::Authenticator if self.check_credential_applicable(&credential, false, uv_performed) { self.state.runtime.push_credential(CachedCredential { timestamp: credential.creation_time, - path: String::from_str(&path.as_str_ref_with_trailing_nul()).ok()?, + path: String::from_str(path.as_str_ref_with_trailing_nul()).ok()?, }); } @@ -1007,7 +1007,7 @@ impl crate::Authenticator None => { return Err(Error::PinNotSet); } }; - if &pin_hash != &stored_pin_hash { + if pin_hash != stored_pin_hash { // I) generate new KEK self.state.runtime.rotate_key_agreement_key(&mut self.trussed); if self.state.persistent.retries() == 0 { @@ -1023,7 +1023,7 @@ impl crate::Authenticator } fn hash_store_pin(&mut self, pin: &Message) -> Result<()> { - let pin_hash_32 = syscall!(self.trussed.hash_sha256(&pin)).hash; + let pin_hash_32 = syscall!(self.trussed.hash_sha256(pin)).hash; let pin_hash: [u8; 16] = pin_hash_32[..16].try_into().unwrap(); self.state.persistent.set_pin_hash(&mut self.trussed, pin_hash).unwrap(); @@ -1038,7 +1038,7 @@ impl crate::Authenticator } let mut pin = syscall!(self.trussed.decrypt_aes256cbc( - shared_secret, &pin_enc)).plaintext.ok_or(Error::Other)?; + shared_secret, pin_enc)).plaintext.ok_or(Error::Other)?; // // temp // let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len()); @@ -1046,7 +1046,7 @@ impl crate::Authenticator // pin.len(), pin_length, &pin); // chop off null bytes let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len()); - if pin_length < 4 || pin_length >= 64 { + if !(4..64).contains(&pin_length) { return Err(Error::PinPolicyViolation); } @@ -1072,7 +1072,7 @@ impl crate::Authenticator { let expected_pin_auth = syscall!(self.trussed.sign_hmacsha256(shared_secret, data)).signature; - if &expected_pin_auth[..16] == &pin_auth[..] { + if expected_pin_auth[..16] == pin_auth[..] { Ok(()) } else { Err(Error::PinAuthInvalid) @@ -1130,7 +1130,7 @@ impl crate::Authenticator let pin_auth = parameters .pin_auth.as_ref().ok_or(Error::MissingParameter)?; - if &expected_pin_auth[..16] == &pin_auth[..] { + if expected_pin_auth[..16] == pin_auth[..] { info_now!("passed pinauth"); Ok(()) } else { @@ -1252,8 +1252,8 @@ impl crate::Authenticator Key::ResidentKey(key) => { debug_now!("checking if ResidentKey {:?} exists", key); match alg { - -7 => syscall!(self.trussed.exists(Mechanism::P256, key.clone())).exists, - -8 => syscall!(self.trussed.exists(Mechanism::Ed255, key.clone())).exists, + -7 => syscall!(self.trussed.exists(Mechanism::P256, *key)).exists, + -8 => syscall!(self.trussed.exists(Mechanism::Ed255, *key)).exists, // -9 => { // let exists = syscall!(self.trussed.exists(Mechanism::Totp, key)).exists; // info_now!("found it"); @@ -1369,7 +1369,7 @@ impl crate::Authenticator // 8. process any extensions present let extensions_output = if let Some(extensions) = &data.extensions { - self.process_assertion_extensions(&data, &extensions, &credential, key)? + self.process_assertion_extensions(&data, extensions, &credential, key)? } else { None }; @@ -1385,7 +1385,7 @@ impl crate::Authenticator let sig_count = self.state.persistent.timestamp(&mut self.trussed)?; let authenticator_data = ctap2::get_assertion::AuthenticatorData { - rp_id_hash: rp_id_hash, + rp_id_hash, flags: { let mut flags = Flags::EMPTY; @@ -1426,7 +1426,7 @@ impl crate::Authenticator // info_now!("TOTP with timestamp {:?}", ×tamp); // syscall!(self.trussed.sign_totp(key, timestamp)).signature.to_bytes().unwrap() // } - _ => syscall!(self.trussed.sign(mechanism, key.clone(), &commitment, serialization)).signature + _ => syscall!(self.trussed.sign(mechanism, key, &commitment, serialization)).signature .to_bytes().unwrap(), }; @@ -1466,10 +1466,10 @@ impl crate::Authenticator ) -> Result<()> { // Prepare to iterate over all credentials associated to RP. - let rp_path = rp_rk_dir(&rp_id_hash); + let rp_path = rp_rk_dir(rp_id_hash); let mut entry = syscall!(self.trussed.read_dir_first( Location::Internal, - rp_path.clone(), + rp_path, None, )).entry; @@ -1485,7 +1485,7 @@ impl crate::Authenticator info_now!("checking RK {:?} for userId ", &rk_path); let credential_data = syscall!(self.trussed.read_file( Location::Internal, - PathBuf::from(rk_path.clone()), + rk_path.clone(), )).data; let credential_maybe = Credential::deserialize(&credential_data); @@ -1502,7 +1502,7 @@ impl crate::Authenticator } syscall!(self.trussed.remove_file( Location::Internal, - PathBuf::from(rk_path), + rk_path, )); info_now!("Overwriting previous rk tied to this userId."); diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 1d3e80b..9f2557d 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -46,7 +46,7 @@ where UP: UserPresence, { type Target = Authenticator; fn deref(&self) -> &Self::Target { - &self.authnr + self.authnr } } @@ -54,7 +54,7 @@ impl core::ops::DerefMut for CredentialManagement<'_, UP, T> where UP: UserPresence, { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.authnr + self.authnr } } @@ -178,7 +178,7 @@ where UP: UserPresence, let rp = credential.data.rp; - response.rp_id_hash = Some(self.hash(&rp.id.as_ref())); + response.rp_id_hash = Some(self.hash(rp.id.as_ref())); response.rp = Some(rp); } @@ -206,7 +206,7 @@ where UP: UserPresence, let CredentialManagementEnumerateRps { remaining, rp_id_hash: last_rp_id_hash, - } = self.state.runtime.cached_rp.clone().ok_or_else(|| Error::NotAllowed)?; + } = self.state.runtime.cached_rp.clone().ok_or(Error::NotAllowed)?; let dir = PathBuf::from(b"rk"); @@ -251,7 +251,7 @@ where UP: UserPresence, let rp = credential.data.rp; - response.rp_id_hash = Some(self.hash(&rp.id.as_ref())); + response.rp_id_hash = Some(self.hash(rp.id.as_ref())); response.rp = Some(rp); // cache state for next call @@ -395,16 +395,12 @@ where UP: UserPresence, .map_err(|_| Error::InvalidCredential)?; // now fill response - let mut response: Response = Default::default(); - - response.user = Some(credential.data.user.clone()); // why these contortions to get kek. sheesh let authnr = &mut self.authnr; let kek = authnr.state.persistent.key_encryption_key(&mut authnr.trussed)?; - let credential_id = credential.id(&mut self.trussed, kek, None)?; - response.credential_id = Some(credential_id.into()); + let credential_id = credential.id(&mut self.trussed, kek, None)?; use crate::credential::Key; let private_key = match credential.key { @@ -420,7 +416,7 @@ where UP: UserPresence, SigningAlgorithm::P256 => { let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; let cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, public_key.clone(), + Mechanism::P256, public_key, // KeySerialization::EcdhEsHkdf256 KeySerialization::Cose, )).serialized_key; @@ -444,12 +440,19 @@ where UP: UserPresence, // PublicKey::TotpKey(Default::default()) // } }; - response.public_key = Some(cose_public_key); - response.cred_protect = match credential.cred_protect { + let cred_protect = match credential.cred_protect { Some(x) => Some(x), None => Some(CredentialProtectionPolicy::Optional), }; + let response = Response { + user: Some(credential.data.user), + credential_id: Some(credential_id.into()), + public_key: Some(cose_public_key), + cred_protect, + ..Default::default() + }; + Ok(response) } @@ -467,7 +470,7 @@ where UP: UserPresence, let rk_path = syscall!(self.trussed.locate_file( Location::Internal, - Some(dir.clone()), + Some(dir), filename, )).path.ok_or(Error::InvalidCredential)?; diff --git a/src/dispatch.rs b/src/dispatch.rs index 6953d6c..fe09e3e 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -1,7 +1,7 @@ //! Dispatch of incoming requests over CTAPHID or NFC APDUs into CTAP1 and CTAP2. -pub mod apdu; -pub mod ctaphid; +mod apdu; +mod ctaphid; use crate::{Authenticator, TrussedRequirements, UserPresence}; #[allow(unused_imports)] diff --git a/src/lib.rs b/src/lib.rs index 3bfbdf5..d549f70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,11 @@ //! //! The core structure is [`Authenticator`], a Trussed® application. //! -//! It implements [`ctap1::Authenticator`] and [`ctap2::Authenticator`] traits, +//! It implements the [`ctap_types::ctap1::Authenticator`] and [`ctap_types::ctap2::Authenticator`] traits, //! which express the interface defined in the CTAP specification. +//! +//! With feature `dispatch` activated, it also implements the `App` traits +//! of [`apdu_dispatch`] and [`ctaphid_dispatch`]. #![cfg_attr(not(test), no_std)] // #![warn(missing_docs)] @@ -28,11 +31,11 @@ use ctap_types::{ /// Re-export of `ctap-types` authenticator errors. pub use ctap_types::Error; -pub mod ctap1; -pub mod ctap2; +mod ctap1; +mod ctap2; #[cfg(feature="dispatch")] -pub mod dispatch; +mod dispatch; pub mod constants; pub mod credential; @@ -72,6 +75,7 @@ where T: {} #[derive(Copy, Clone, Debug, Eq, PartialEq)] +/// Externally defined configuration. pub struct Config { /// Typically determined by surrounding USB-level decoder. /// For Solo 2, this is usbd-ctaphid (and its buffer size). @@ -92,7 +96,7 @@ pub struct Config { /// Trussed® app implementing a FIDO authenticator. /// -/// It implements [`ctap1::Authenticator`] and [`ctap2::Authenticator`] traits, +/// It implements the [`ctap_types::ctap1::Authenticator`] and [`ctap_types::ctap2::Authenticator`] traits, /// which, in turn, express the interfaces defined in the CTAP specification. /// /// The type parameter `T` selects a Trussed® client implementation, which @@ -136,7 +140,7 @@ fn format_hex(data: &[u8], mut buffer: &mut [u8]) { #[inline] #[allow(dead_code)] -pub(crate) fn msp() -> u32 { 0x2000_000 } +pub(crate) fn msp() -> u32 { 0x2000_0000 } /// Currently Ed25519 and P256. #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -169,6 +173,7 @@ pub trait UserPresence: Copy { } #[deprecated(note="use `Silent` directly`")] +#[doc(hidden)] pub type SilentAuthenticator = Silent; /// No user presence verification. @@ -182,6 +187,7 @@ impl UserPresence for Silent { } #[deprecated(note="use `Conforming` directly")] +#[doc(hidden)] pub type NonSilentAuthenticator = Conforming; /// User presence verification via Trussed. @@ -200,7 +206,7 @@ impl UserPresence for Conforming { } fn cbor_serialize_message(object: &T) -> core::result::Result { - Ok(trussed::cbor_serialize_bytes(object)?) + trussed::cbor_serialize_bytes(object) } impl Authenticator @@ -210,9 +216,7 @@ where UP: UserPresence, pub fn new(trussed: T, up: UP, config: Config) -> Self { let state = state::State::new(); - let authenticator = Self { trussed, state, up, config }; - - authenticator + Self { trussed, state, up, config } } pub fn call(&mut self, request: &Request) -> Result { @@ -235,7 +239,7 @@ where UP: UserPresence, } fn hash(&mut self, data: &[u8]) -> Bytes<32> { - let hash = syscall!(self.trussed.hash_sha256(&data)).hash; + let hash = syscall!(self.trussed.hash_sha256(data)).hash; hash.to_bytes().expect("hash should fit") } } diff --git a/src/state.rs b/src/state.rs index 92d92af..e921937 100644 --- a/src/state.rs +++ b/src/state.rs @@ -68,7 +68,7 @@ impl CredentialCacheGeneric { self.0.len() as u32 } - pub fn is_empty(&mut self) -> bool { + pub fn is_empty(&self) -> bool { self.0.is_empty() } @@ -88,6 +88,12 @@ pub struct State { pub runtime: RuntimeState, } +impl Default for State { + fn default() -> Self { + Self::new() + } +} + impl State { // pub fn new(trussed: &mut TrussedClient) -> Self { @@ -169,9 +175,7 @@ impl Identity { cert_reader = &cert_reader[aaguid_start_sequence.len()..]; let mut aaguid = [0u8; 16]; - for i in 0 .. 16 { - aaguid[i] = cert_reader[i] - } + aaguid[..16].clone_from_slice(&cert_reader[..16]); Some(aaguid) } @@ -187,7 +191,7 @@ impl Identity { crate::constants::ATTESTATION_CERT_ID )).der; - let mut aaguid = self.yank_aaguid(&cert.as_slice()); + let mut aaguid = self.yank_aaguid(cert.as_slice()); if aaguid.is_none() { // Provide a default @@ -308,9 +312,7 @@ impl PersistentState { return Err(Error::Other); } - let previous_state = result.map_err(|_| Error::Other); - - previous_state + result.map_err(|_| Error::Other) } pub fn save(&self, trussed: &mut T) -> Result<()> { From 7a449c9cfe092437573fee26acae1fb1c2907a65 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 17 Mar 2022 02:39:21 +0100 Subject: [PATCH 11/13] Cargo fmt round --- src/constants.rs | 2 +- src/credential.rs | 111 +++-- src/ctap1.rs | 151 ++++--- src/ctap2.rs | 700 +++++++++++++++++------------ src/ctap2/credential_management.rs | 267 ++++++----- src/dispatch.rs | 87 ++-- src/dispatch/apdu.rs | 42 +- src/dispatch/ctaphid.rs | 26 +- src/lib.rs | 86 ++-- src/state.rs | 171 ++++--- 10 files changed, 918 insertions(+), 725 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index 6271354..43e88da 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -3,7 +3,7 @@ use trussed::types::{CertId, KeyId}; pub const FIDO2_UP_TIMEOUT: u32 = 30_000; -pub const U2F_UP_TIMEOUT: u32 = 250; +pub const U2F_UP_TIMEOUT: u32 = 250; pub const ATTESTATION_CERT_ID: CertId = CertId::from_special(0); pub const ATTESTATION_KEY_ID: KeyId = KeyId::from_special(0); diff --git a/src/credential.rs b/src/credential.rs index 4ea6413..c0d677a 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -2,26 +2,18 @@ use core::cmp::Ordering; -use trussed::{ - client, syscall, try_syscall, - types::KeyId, -}; +use trussed::{client, syscall, try_syscall, types::KeyId}; pub(crate) use ctap_types::{ - Bytes, String, // authenticator::{ctap1, ctap2, Error, Request, Response}, ctap2::credential_management::CredentialProtectionPolicy, sizes::*, webauthn::PublicKeyCredentialDescriptor, + Bytes, + String, }; -use crate::{ - Authenticator, - Error, - Result, - UserPresence, -}; - +use crate::{Authenticator, Error, Result, UserPresence}; /// As signaled in `get_info`. /// @@ -49,7 +41,9 @@ impl TryFrom for CredentialId { type Error = Error; fn try_from(esc: EncryptedSerializedCredential) -> Result { - Ok(CredentialId(trussed::cbor_serialize_bytes(&esc.0).map_err(|_| Error::Other)?)) + Ok(CredentialId( + trussed::cbor_serialize_bytes(&esc.0).map_err(|_| Error::Other)?, + )) } } @@ -60,7 +54,7 @@ impl TryFrom for EncryptedSerializedCredential { fn try_from(cid: CredentialId) -> Result { let encrypted_serialized_credential = EncryptedSerializedCredential( - ctap_types::serde::cbor_deserialize(&cid.0).map_err(|_| Error::InvalidCredential)? + ctap_types::serde::cbor_deserialize(&cid.0).map_err(|_| Error::InvalidCredential)?, ); Ok(encrypted_serialized_credential) } @@ -78,7 +72,9 @@ pub enum Key { } /// The main content of a `Credential`. -#[derive(Clone, Debug, PartialEq, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)] +#[derive( + Clone, Debug, PartialEq, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed, +)] pub struct CredentialData { // id, name, url pub rp: ctap_types::webauthn::PublicKeyCredentialRpEntity, @@ -103,7 +99,6 @@ pub struct CredentialData { pub hmac_secret: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, - // TODO: add `sig_counter: Option`, // and grant RKs a per-credential sig-counter. } @@ -139,9 +134,7 @@ impl core::ops::Deref for Credential { /// Likely comparison based on timestamp would be good enough? impl PartialEq for Credential { fn eq(&self, other: &Self) -> bool { - (self.creation_time == other.creation_time) - && - (self.key == other.key) + (self.creation_time == other.creation_time) && (self.key == other.key) } } @@ -183,7 +176,7 @@ impl From for PublicKeyCredentialDescriptor { let mut key_type = String::new(); key_type.push_str("public-key").unwrap(); key_type - } + }, } } } @@ -201,9 +194,7 @@ impl Credential { hmac_secret: Option, cred_protect: Option, nonce: [u8; 12], - ) - -> Self - { + ) -> Self { info!("credential for algorithm {}", algorithm); let data = CredentialData { rp: rp.clone(), @@ -243,9 +234,7 @@ impl Credential { trussed: &mut T, key_encryption_key: KeyId, rp_id_hash: Option<&Bytes<32>>, - ) - -> Result - { + ) -> Result { let serialized_credential = self.strip().serialize()?; let message = &serialized_credential; // info!("serialized cred = {:?}", message).ok(); @@ -255,14 +244,14 @@ impl Credential { } else { syscall!(trussed.hash_sha256(self.rp.id.as_ref())) .hash - .to_bytes().map_err(|_| Error::Other)? + .to_bytes() + .map_err(|_| Error::Other)? }; let associated_data = &rp_id_hash[..]; let nonce: [u8; 12] = self.nonce.as_slice().try_into().unwrap(); - let encrypted_serialized_credential = EncryptedSerializedCredential( - syscall!(trussed.encrypt_chacha8poly1305( - key_encryption_key, message, associated_data, Some(&nonce)))); + let encrypted_serialized_credential = EncryptedSerializedCredential(syscall!(trussed + .encrypt_chacha8poly1305(key_encryption_key, message, associated_data, Some(&nonce)))); let credential_id: CredentialId = encrypted_serialized_credential.try_into().unwrap(); Ok(credential_id) @@ -283,12 +272,10 @@ impl Credential { } pub fn try_from( - authnr: &mut Authenticator, + authnr: &mut Authenticator, rp_id_hash: &Bytes<32>, descriptor: &PublicKeyCredentialDescriptor, - ) - -> Result - { + ) -> Result { Self::try_from_bytes(authnr, rp_id_hash, &descriptor.id) } @@ -296,18 +283,17 @@ impl Credential { authnr: &mut Authenticator, rp_id_hash: &Bytes<32>, id: &[u8], - ) - -> Result - { - + ) -> Result { let mut cred: Bytes = Bytes::new(); - cred.extend_from_slice(id).map_err(|_| Error::InvalidCredential)?; + cred.extend_from_slice(id) + .map_err(|_| Error::InvalidCredential)?; - let encrypted_serialized = EncryptedSerializedCredential::try_from( - CredentialId(cred) - )?; + let encrypted_serialized = EncryptedSerializedCredential::try_from(CredentialId(cred))?; - let kek = authnr.state.persistent.key_encryption_key(&mut authnr.trussed)?; + let kek = authnr + .state + .persistent + .key_encryption_key(&mut authnr.trussed)?; let serialized = try_syscall!(authnr.trussed.decrypt_chacha8poly1305( // TODO: use RpId as associated data here? @@ -317,11 +303,12 @@ impl Credential { &encrypted_serialized.0.nonce, &encrypted_serialized.0.tag, )) - .map_err(|_| Error::InvalidCredential)?.plaintext - .ok_or(Error::InvalidCredential)?; + .map_err(|_| Error::InvalidCredential)? + .plaintext + .ok_or(Error::InvalidCredential)?; - let credential = Credential::deserialize(&serialized) - .map_err(|_| Error::InvalidCredential)?; + let credential = + Credential::deserialize(&serialized).map_err(|_| Error::InvalidCredential)?; Ok(credential) } @@ -362,7 +349,7 @@ mod test { url: None, }, user: PublicKeyCredentialUserEntity { - id: Bytes::from_slice(&[1,2,3]).unwrap(), + id: Bytes::from_slice(&[1, 2, 3]).unwrap(), icon: None, name: None, display_name: None, @@ -370,7 +357,7 @@ mod test { creation_time: 123, use_counter: false, algorithm: -7, - key: Key::WrappedKey(Bytes::from_slice(&[1,2,3]).unwrap()), + key: Key::WrappedKey(Bytes::from_slice(&[1, 2, 3]).unwrap()), hmac_secret: Some(false), cred_protect: None, }; @@ -378,7 +365,11 @@ mod test { } fn random_bytes() -> Bytes { - use rand::{RngCore, distributions::{Distribution, Uniform}, rngs::OsRng}; + use rand::{ + distributions::{Distribution, Uniform}, + rngs::OsRng, + RngCore, + }; let mut bytes = Bytes::default(); let between = Uniform::from(0..(N + 1)); @@ -392,7 +383,7 @@ mod test { #[allow(dead_code)] fn maybe_random_bytes() -> Option> { - use rand::{RngCore, rngs::OsRng}; + use rand::{rngs::OsRng, RngCore}; if OsRng.next_u32() & 1 != 0 { Some(random_bytes()) } else { @@ -401,18 +392,26 @@ mod test { } fn random_string() -> String { + use rand::{ + distributions::{Alphanumeric, Distribution, Uniform}, + rngs::OsRng, + Rng, + }; use std::str::FromStr; - use rand::{Rng, distributions::{Alphanumeric, Distribution, Uniform}, rngs::OsRng}; let between = Uniform::from(0..(N + 1)); let n = between.sample(&mut OsRng); - let std_string: std::string::String = OsRng.sample_iter(&Alphanumeric).take(n).map(char::from).collect(); + let std_string: std::string::String = OsRng + .sample_iter(&Alphanumeric) + .take(n) + .map(char::from) + .collect(); String::from_str(&std_string).unwrap() } fn maybe_random_string() -> Option> { - use rand::{RngCore, rngs::OsRng}; + use rand::{rngs::OsRng, RngCore}; if OsRng.next_u32() & 1 != 0 { Some(random_string()) } else { @@ -430,7 +429,7 @@ mod test { url: maybe_random_string(), }, user: PublicKeyCredentialUserEntity { - id: random_bytes(),//Bytes::from_slice(&[1,2,3]).unwrap(), + id: random_bytes(), //Bytes::from_slice(&[1,2,3]).unwrap(), icon: maybe_random_string(), name: maybe_random_string(), display_name: maybe_random_string(), @@ -518,5 +517,5 @@ mod test { // TestResult::from_bool(credential_data == deserialized) // } - // } + // } } diff --git a/src/ctap1.rs b/src/ctap1.rs index bab737d..5be6414 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -1,39 +1,22 @@ //! The `ctap_types::ctap1::Authenticator` implementation. use ctap_types::{ - ctap1::{ - Authenticator, - ControlByte, - register, authenticate, - Result, - Error, - }, + ctap1::{authenticate, register, Authenticator, ControlByte, Error, Result}, heapless_bytes::Bytes, }; use trussed::{ syscall, - types::{ - KeySerialization, - Mechanism, - SignatureSerialization, - Location, - }, + types::{KeySerialization, Location, Mechanism, SignatureSerialization}, }; use crate::{ - credential::{ - self, - Credential, - Key, - }, constants, - SigningAlgorithm, - TrussedRequirements, - UserPresence, + credential::{self, Credential, Key}, + SigningAlgorithm, TrussedRequirements, UserPresence, }; -type Commitment = Bytes::<324>; +type Commitment = Bytes<324>; /// Implement `ctap1::Authenticator` for our Authenticator. /// @@ -41,8 +24,7 @@ type Commitment = Bytes::<324>; /// The "proposed standard" of U2F V1.2 applies to CTAP1. /// - [Message formats](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html) /// - [App ID](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-appid-and-facets-v1.2-ps-20170411.html) -impl Authenticator for crate::Authenticator -{ +impl Authenticator for crate::Authenticator { /// Register a new credential, this always uses P-256 keys. /// /// Note that attestation is mandatory in CTAP1/U2F, so if the state @@ -52,35 +34,51 @@ impl Authenticator for crate::Authenti /// Also note that CTAP1 credentials should be assertable over CTAP2. I believe this is /// currently not the case. fn register(&mut self, reg: ®ister::Request) -> Result { - self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) + self.up + .user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) .map_err(|_| Error::ConditionsOfUseNotSatisfied)?; // Generate a new P256 key pair. let private_key = syscall!(self.trussed.generate_p256_private_key(Location::Volatile)).key; - let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; - - let serialized_cose_public_key = syscall!(self.trussed.serialize_p256_key( - public_key, KeySerialization::EcdhEsHkdf256 - )).serialized_key; + let public_key = syscall!(self + .trussed + .derive_p256_public_key(private_key, Location::Volatile)) + .key; + + let serialized_cose_public_key = syscall!(self + .trussed + .serialize_p256_key(public_key, KeySerialization::EcdhEsHkdf256)) + .serialized_key; syscall!(self.trussed.delete(public_key)); - let cose_key: ctap_types::cose::EcdhEsHkdf256PublicKey - = trussed::cbor_deserialize(&serialized_cose_public_key).unwrap(); + let cose_key: ctap_types::cose::EcdhEsHkdf256PublicKey = + trussed::cbor_deserialize(&serialized_cose_public_key).unwrap(); - let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed) + let wrapping_key = self + .state + .persistent + .key_wrapping_key(&mut self.trussed) .map_err(|_| Error::UnspecifiedCheckingError)?; // debug!("wrapping u2f private key"); - let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( - wrapping_key, - private_key, - ®.app_id, - )).wrapped_key; + let wrapped_key = + syscall!(self + .trussed + .wrap_key_chacha8poly1305(wrapping_key, private_key, ®.app_id,)) + .wrapped_key; // debug!("wrapped_key = {:?}", &wrapped_key); syscall!(self.trussed.delete(private_key)); - let key = Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::UnspecifiedCheckingError)?); - let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap(); + let key = Key::WrappedKey( + wrapped_key + .to_bytes() + .map_err(|_| Error::UnspecifiedCheckingError)?, + ); + let nonce = syscall!(self.trussed.random_bytes(12)) + .bytes + .as_slice() + .try_into() + .unwrap(); let mut rp_id = heapless::String::new(); @@ -88,7 +86,7 @@ impl Authenticator for crate::Authenti // TODO: Is this true? // rp_id.push_str("u2f").ok(); - let rp = ctap_types::webauthn::PublicKeyCredentialRpEntity{ + let rp = ctap_types::webauthn::PublicKeyCredentialRpEntity { id: rp_id, name: None, url: None, @@ -105,10 +103,12 @@ impl Authenticator for crate::Authenti credential::CtapVersion::U2fV2, &rp, &user, - SigningAlgorithm::P256 as i32, key, - self.state.persistent.timestamp(&mut self.trussed).map_err(|_| Error::NotEnoughMemory)?, + self.state + .persistent + .timestamp(&mut self.trussed) + .map_err(|_| Error::NotEnoughMemory)?, None, None, nonce, @@ -117,18 +117,24 @@ impl Authenticator for crate::Authenti // info!("made credential {:?}", &credential); // 12.b generate credential ID { = AEAD(Serialize(Credential)) } - let kek = self.state.persistent.key_encryption_key(&mut self.trussed).map_err(|_| Error::NotEnoughMemory)?; - let credential_id = credential.id(&mut self.trussed, kek, Some(®.app_id)).map_err(|_| Error::NotEnoughMemory)?; + let kek = self + .state + .persistent + .key_encryption_key(&mut self.trussed) + .map_err(|_| Error::NotEnoughMemory)?; + let credential_id = credential + .id(&mut self.trussed, kek, Some(®.app_id)) + .map_err(|_| Error::NotEnoughMemory)?; let mut commitment = Commitment::new(); - commitment.push(0).unwrap(); // reserve byte + commitment.push(0).unwrap(); // reserve byte commitment.extend_from_slice(®.app_id).unwrap(); commitment.extend_from_slice(®.challenge).unwrap(); commitment.extend_from_slice(&credential_id.0).unwrap(); - commitment.push(0x04).unwrap(); // public key uncompressed byte + commitment.push(0x04).unwrap(); // public key uncompressed byte commitment.extend_from_slice(&cose_key.x).unwrap(); commitment.extend_from_slice(&cose_key.y).unwrap(); @@ -138,22 +144,24 @@ impl Authenticator for crate::Authenti (Some((key, cert)), _aaguid) => { info!("aaguid: {}", hex_str!(&_aaguid)); ( - syscall!( - self.trussed.sign(Mechanism::P256, + syscall!(self.trussed.sign( + Mechanism::P256, key, &commitment, SignatureSerialization::Asn1Der - )).signature.to_bytes().unwrap(), - cert + )) + .signature + .to_bytes() + .unwrap(), + cert, ) - }, + } _ => { info!("Not provisioned with attestation key!"); return Err(Error::KeyReferenceNotFound); } }; - Ok(register::Response::new( 0x05, &cose_key, @@ -176,12 +184,13 @@ impl Authenticator for crate::Authenti } else { Err(Error::IncorrectDataParameter) }; - }, + } ControlByte::EnforceUserPresenceAndSign => { - self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) + self.up + .user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) .map_err(|_| Error::ConditionsOfUseNotSatisfied)?; 0x01 - }, + } ControlByte::DontEnforceUserPresenceAndSign => 0x00, }; @@ -189,14 +198,18 @@ impl Authenticator for crate::Authenti let key = match &cred.key { Key::WrappedKey(bytes) => { - let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed) + 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, b"", Location::Volatile, - )).key; + )) + .key; match key_result { Some(key) => { info!("loaded u2f key!"); @@ -216,22 +229,30 @@ impl Authenticator for crate::Authenti return Err(Error::IncorrectDataParameter); } - let sig_count = self.state.persistent.timestamp(&mut self.trussed). - map_err(|_| Error::UnspecifiedNonpersistentExecutionError)?; + let sig_count = self + .state + .persistent + .timestamp(&mut self.trussed) + .map_err(|_| Error::UnspecifiedNonpersistentExecutionError)?; let mut commitment = Commitment::new(); commitment.extend_from_slice(&auth.app_id).unwrap(); commitment.push(user_presence_byte).unwrap(); - commitment.extend_from_slice(&sig_count.to_be_bytes()).unwrap(); + commitment + .extend_from_slice(&sig_count.to_be_bytes()) + .unwrap(); commitment.extend_from_slice(&auth.challenge).unwrap(); - let signature = syscall!( - self.trussed.sign(Mechanism::P256, + let signature = syscall!(self.trussed.sign( + Mechanism::P256, key, &commitment, SignatureSerialization::Asn1Der - )).signature.to_bytes().unwrap(); + )) + .signature + .to_bytes() + .unwrap(); Ok(authenticate::Response { user_presence: user_presence_byte, @@ -239,6 +260,4 @@ impl Authenticator for crate::Authenti signature, }) } - } - diff --git a/src/ctap2.rs b/src/ctap2.rs index 1a4c663..71d0a93 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,15 +1,10 @@ //! The `ctap_types::ctap2::Authenticator` implementation. use ctap_types::{ + ctap2::{self, Authenticator, VendorOperation}, heapless::{String, Vec}, - Error, - ctap2::{ - self, - Authenticator, - VendorOperation, - }, heapless_bytes::Bytes, - sizes, + sizes, Error, }; use littlefs2::path::Path; @@ -17,36 +12,26 @@ use littlefs2::path::Path; use trussed::{ syscall, try_syscall, types::{ - KeyId, - KeySerialization, - Mechanism, - MediumData, - Message, + KeyId, KeySerialization, Location, Mechanism, MediumData, Message, PathBuf, SignatureSerialization, - Location, - PathBuf, }, }; use crate::{ + constants, credential::{ self, Credential, // CredentialList, Key, }, - constants, format_hex, state::{ self, // // (2022-02-27): 9288 bytes // MinCredentialHeap, }, - Result, - - SigningAlgorithm, - UserPresence, - TrussedRequirements, + Result, SigningAlgorithm, TrussedRequirements, UserPresence, }; #[allow(unused_imports)] @@ -59,20 +44,27 @@ pub mod credential_management; impl Authenticator for crate::Authenticator { #[inline(never)] fn get_info(&mut self) -> ctap2::get_info::Response { - debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); use core::str::FromStr; let mut versions = Vec::, 4>::new(); versions.push(String::from_str("U2F_V2").unwrap()).unwrap(); - versions.push(String::from_str("FIDO_2_0").unwrap()).unwrap(); + versions + .push(String::from_str("FIDO_2_0").unwrap()) + .unwrap(); // #[cfg(feature = "enable-fido-pre")] - versions.push(String::from_str("FIDO_2_1_PRE").unwrap()).unwrap(); + versions + .push(String::from_str("FIDO_2_1_PRE").unwrap()) + .unwrap(); let mut extensions = Vec::, 4>::new(); // extensions.push(String::from_str("credProtect").unwrap()).unwrap(); - extensions.push(String::from_str("credProtect").unwrap()).unwrap(); - extensions.push(String::from_str("hmac-secret").unwrap()).unwrap(); + extensions + .push(String::from_str("credProtect").unwrap()) + .unwrap(); + extensions + .push(String::from_str("hmac-secret").unwrap()) + .unwrap(); let mut pin_protocols = Vec::::new(); pin_protocols.push(1).unwrap(); @@ -84,7 +76,7 @@ impl Authenticator for crate::Authenti uv: None, plat: Some(false), cred_mgmt: Some(true), - client_pin: match self.state.persistent.pin_is_set() { + client_pin: match self.state.persistent.pin_is_set() { true => Some(true), false => Some(false), }, @@ -107,7 +99,7 @@ impl Authenticator for crate::Authenti transports.push(String::from("nfc")).unwrap(); transports.push(String::from("usb")).unwrap(); - let (_, aaguid)= self.state.identity.attestation(&mut self.trussed); + let (_, aaguid) = self.state.identity.attestation(&mut self.trussed); ctap2::get_info::Response { versions, @@ -143,7 +135,10 @@ impl Authenticator for crate::Authenti if self.state.runtime.active_get_assertion.is_none() { return Err(Error::NotAllowed); } - let credential = self.state.runtime.pop_credential(&mut self.trussed) + let credential = self + .state + .runtime + .pop_credential(&mut self.trussed) .ok_or(Error::NotAllowed)?; // 5. suppress PII if no UV was performed in original GA @@ -156,8 +151,10 @@ impl Authenticator for crate::Authenti } #[inline(never)] - fn make_credential(&mut self, parameters: &ctap2::make_credential::Request) -> Result { - + fn make_credential( + &mut self, + parameters: &ctap2::make_credential::Request, + ) -> Result { let rp_id_hash = self.hash(parameters.rp.id.as_ref()); // 1-4. @@ -168,7 +165,9 @@ impl Authenticator for crate::Authenti } } let uv_performed = self.pin_prechecks( - ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, + ¶meters.options, + ¶meters.pin_auth, + ¶meters.pin_protocol, parameters.client_data_hash.as_ref(), )?; @@ -183,9 +182,12 @@ impl Authenticator for crate::Authenti if let Ok(excluded_cred) = result { use credential::CredentialProtectionPolicy; // If UV is not performed, than CredProtectRequired credentials should not be visibile. - if !(excluded_cred.cred_protect == Some(CredentialProtectionPolicy::Required)) || uv_performed { + if !(excluded_cred.cred_protect == Some(CredentialProtectionPolicy::Required)) + || uv_performed + { info_now!("Excluded!"); - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + self.up + .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; return Err(Error::CredentialExcluded); } } @@ -197,8 +199,14 @@ impl Authenticator for crate::Authenti let mut algorithm: Option = None; for param in parameters.pub_key_cred_params.iter() { match param.alg { - -7 => { if algorithm.is_none() { algorithm = Some(SigningAlgorithm::P256); }} - -8 => { algorithm = Some(SigningAlgorithm::Ed25519); } + -7 => { + if algorithm.is_none() { + algorithm = Some(SigningAlgorithm::P256); + } + } + -8 => { + algorithm = Some(SigningAlgorithm::Ed25519); + } // -9 => { algorithm = Some(SigningAlgorithm::Totp); } _ => {} } @@ -207,11 +215,12 @@ impl Authenticator for crate::Authenti Some(algorithm) => { info_now!("algo: {:?}", algorithm as i32); algorithm - }, - None => { return Err(Error::UnsupportedAlgorithm); } + } + None => { + return Err(Error::UnsupportedAlgorithm); + } }; - // 8. process options; on known but unsupported error UnsupportedOption let mut rk_requested = false; @@ -234,18 +243,19 @@ impl Authenticator for crate::Authenti // let mut cred_protect_requested = CredentialProtectionPolicy::Optional; let mut cred_protect_requested = None; if let Some(extensions) = ¶meters.extensions { - hmac_secret_requested = extensions.hmac_secret; if let Some(policy) = &extensions.cred_protect { - cred_protect_requested = Some(credential::CredentialProtectionPolicy::try_from(*policy)?); + cred_protect_requested = + Some(credential::CredentialProtectionPolicy::try_from(*policy)?); } } // debug_now!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); // 10. get UP, if denied error OperationDenied - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + self.up + .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; // 11. generate credential keypair let location = match rk_requested { @@ -259,36 +269,47 @@ impl Authenticator for crate::Authenti match algorithm { SigningAlgorithm::P256 => { private_key = syscall!(self.trussed.generate_p256_private_key(location)).key; - public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; + public_key = syscall!(self + .trussed + .derive_p256_public_key(private_key, Location::Volatile)) + .key; cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, public_key, KeySerialization::Cose - )).serialized_key; + Mechanism::P256, + public_key, + KeySerialization::Cose + )) + .serialized_key; let _success = syscall!(self.trussed.delete(public_key)).success; info_now!("deleted public P256 key: {}", _success); } SigningAlgorithm::Ed25519 => { private_key = syscall!(self.trussed.generate_ed255_private_key(location)).key; - public_key = syscall!(self.trussed.derive_ed255_public_key(private_key, Location::Volatile)).key; + public_key = syscall!(self + .trussed + .derive_ed255_public_key(private_key, Location::Volatile)) + .key; cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::Ed255, public_key, KeySerialization::Cose - )).serialized_key; + Mechanism::Ed255, + public_key, + KeySerialization::Cose + )) + .serialized_key; let _success = syscall!(self.trussed.delete(public_key)).success; info_now!("deleted public Ed25519 key: {}", _success); - } - // SigningAlgorithm::Totp => { - // if parameters.client_data_hash.len() != 32 { - // return Err(Error::InvalidParameter); - // } - // // b'TOTP---W\x0e\xf1\xe0\xd7\x83\xfe\t\xd1\xc1U\xbf\x08T_\x07v\xb2\xc6--TOTP' - // let totp_secret: [u8; 20] = parameters.client_data_hash[6..26].try_into().unwrap(); - // private_key = syscall!(self.trussed.unsafe_inject_shared_key( - // &totp_secret, Location::Internal)).key; - // // info_now!("totes injected"); - // let fake_cose_pk = ctap_types::cose::TotpPublicKey {}; - // let fake_serialized_cose_pk = trussed::cbor_serialize_bytes(&fake_cose_pk) - // .map_err(|_| Error::NotAllowed)?; - // cose_public_key = fake_serialized_cose_pk; // Bytes::from_slice(&[0u8; 20]).unwrap(); - // } + } // SigningAlgorithm::Totp => { + // if parameters.client_data_hash.len() != 32 { + // return Err(Error::InvalidParameter); + // } + // // b'TOTP---W\x0e\xf1\xe0\xd7\x83\xfe\t\xd1\xc1U\xbf\x08T_\x07v\xb2\xc6--TOTP' + // let totp_secret: [u8; 20] = parameters.client_data_hash[6..26].try_into().unwrap(); + // private_key = syscall!(self.trussed.unsafe_inject_shared_key( + // &totp_secret, Location::Internal)).key; + // // info_now!("totes injected"); + // let fake_cose_pk = ctap_types::cose::TotpPublicKey {}; + // let fake_serialized_cose_pk = trussed::cbor_serialize_bytes(&fake_cose_pk) + // .map_err(|_| Error::NotAllowed)?; + // cose_public_key = fake_serialized_cose_pk; // Bytes::from_slice(&[0u8; 20]).unwrap(); + // } } // 12. if `rk` is set, store or overwrite key pair, if full error KeyStoreFull @@ -303,7 +324,8 @@ impl Authenticator for crate::Authenti wrapping_key, private_key, &rp_id_hash, - )).wrapped_key; + )) + .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...) @@ -314,11 +336,18 @@ impl Authenticator for crate::Authenti }; // injecting this is a bit mehhh.. - let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap(); + let nonce = syscall!(self.trussed.random_bytes(12)) + .bytes + .as_slice() + .try_into() + .unwrap(); info_now!("nonce = {:?}", &nonce); // 12.b generate credential ID { = AEAD(Serialize(Credential)) } - let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; + let kek = self + .state + .persistent + .key_encryption_key(&mut self.trussed)?; // store it. // TODO: overwrite, error handling with KeyStoreFull @@ -343,7 +372,8 @@ impl Authenticator for crate::Authenti let serialized_credential = credential.serialize()?; // first delete any other RK cred with same RP + UserId if there is one. - self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id).ok(); + self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id) + .ok(); // then store key, making it resident let credential_id_hash = self.hash(credential_id.0.as_ref()); @@ -354,7 +384,8 @@ impl Authenticator for crate::Authenti // user attribute for later easy lookup // Some(rp_id_hash.clone()), None, - )).map_err(|_| Error::KeyStoreFull)?; + )) + .map_err(|_| Error::KeyStoreFull)?; } // 13. generate and return attestation statement using clientDataHash @@ -401,7 +432,6 @@ impl Authenticator for crate::Authenti cred_protect: parameters.extensions.as_ref().unwrap().cred_protect, hmac_secret: parameters.extensions.as_ref().unwrap().hmac_secret, }) - } else { None } @@ -416,9 +446,13 @@ impl Authenticator for crate::Authenti // can we write Sum somehow? // debug_now!("seeking commitment, {} + {}", serialized_auth_data.len(), parameters.client_data_hash.len()); let mut commitment = Bytes::<1024>::new(); - commitment.extend_from_slice(&serialized_auth_data).map_err(|_| Error::Other)?; + commitment + .extend_from_slice(&serialized_auth_data) + .map_err(|_| Error::Other)?; // debug_now!("serialized_auth_data ={:?}", &serialized_auth_data); - commitment.extend_from_slice(¶meters.client_data_hash).map_err(|_| Error::Other)?; + commitment + .extend_from_slice(¶meters.client_data_hash) + .map_err(|_| Error::Other)?; // debug_now!("client_data_hash = {:?}", ¶meters.client_data_hash); // debug_now!("commitment = {:?}", &commitment); @@ -434,39 +468,44 @@ impl Authenticator for crate::Authenti if attestation_maybe.is_none() { match algorithm { SigningAlgorithm::Ed25519 => { - let signature = syscall!(self.trussed.sign_ed255(private_key, &commitment)).signature; + let signature = + syscall!(self.trussed.sign_ed255(private_key, &commitment)).signature; (signature.to_bytes().map_err(|_| Error::Other)?, -8) } SigningAlgorithm::P256 => { // DO NOT prehash here, `trussed` does that - let der_signature = syscall!(self.trussed.sign_p256(private_key, &commitment, SignatureSerialization::Asn1Der)).signature; + let der_signature = syscall!(self.trussed.sign_p256( + private_key, + &commitment, + SignatureSerialization::Asn1Der + )) + .signature; (der_signature.to_bytes().map_err(|_| Error::Other)?, -7) - } - // SigningAlgorithm::Totp => { - // // maybe we can fake it here too, but seems kinda weird - // // return Err(Error::UnsupportedAlgorithm); - // // micro-ecc is borked. let's self-sign anyway - // let hash = syscall!(self.trussed.hash_sha256(&commitment.as_ref())).hash; - // let tmp_key = syscall!(self.trussed - // .generate_p256_private_key(Location::Volatile)) - // .key; - - // let signature = syscall!(self.trussed.sign_p256( - // tmp_key, - // &hash, - // SignatureSerialization::Asn1Der, - // )).signature; - // (signature.to_bytes().map_err(|_| Error::Other)?, -7) - // } + } // SigningAlgorithm::Totp => { + // // maybe we can fake it here too, but seems kinda weird + // // return Err(Error::UnsupportedAlgorithm); + // // micro-ecc is borked. let's self-sign anyway + // let hash = syscall!(self.trussed.hash_sha256(&commitment.as_ref())).hash; + // let tmp_key = syscall!(self.trussed + // .generate_p256_private_key(Location::Volatile)) + // .key; + + // let signature = syscall!(self.trussed.sign_p256( + // tmp_key, + // &hash, + // SignatureSerialization::Asn1Der, + // )).signature; + // (signature.to_bytes().map_err(|_| Error::Other)?, -7) + // } } } else { - let signature = syscall!(self.trussed.sign_p256( attestation_maybe.as_ref().unwrap().0, &commitment, SignatureSerialization::Asn1Der, - )).signature; + )) + .signature; (signature.to_bytes().map_err(|_| Error::Other)?, -7) } }; @@ -516,14 +555,14 @@ impl Authenticator for crate::Authenti // 2. check for user presence // denied -> OperationDenied // timeout -> UserActionTimeout - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + self.up + .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; // Delete resident keys syscall!(self.trussed.delete_all(Location::Internal)); - syscall!(self.trussed.remove_dir_all( - Location::Internal, - PathBuf::from("rk"), - )); + syscall!(self + .trussed + .remove_dir_all(Location::Internal, PathBuf::from("rk"),)); // b. delete persistent state self.state.persistent.reset(&mut self.trussed)?; @@ -535,22 +574,25 @@ impl Authenticator for crate::Authenti } fn selection(&mut self) -> Result<()> { - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT) + self.up + .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT) } #[inline(never)] - fn client_pin(&mut self, parameters: &ctap2::client_pin::Request) -> Result { + fn client_pin( + &mut self, + parameters: &ctap2::client_pin::Request, + ) -> Result { use ctap2::client_pin::PinV1Subcommand as Subcommand; debug_now!("CTAP2.PIN..."); // info_now!("{:?}", parameters); // TODO: Handle pin protocol V2 - if parameters.pin_protocol != 1{ + if parameters.pin_protocol != 1 { return Err(Error::InvalidParameter); } Ok(match parameters.sub_command { - Subcommand::GetRetries => { debug_now!("CTAP2.Pin.GetRetries"); @@ -565,9 +607,16 @@ impl Authenticator for crate::Authenti debug_now!("CTAP2.Pin.GetKeyAgreement"); let private_key = self.state.runtime.key_agreement_key(&mut self.trussed); - let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; + let public_key = syscall!(self + .trussed + .derive_p256_public_key(private_key, Location::Volatile)) + .key; let serialized_cose_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, public_key, KeySerialization::EcdhEsHkdf256)).serialized_key; + Mechanism::P256, + public_key, + KeySerialization::EcdhEsHkdf256 + )) + .serialized_key; let cose_key = trussed::cbor_deserialize(&serialized_cose_key).unwrap(); syscall!(self.trussed.delete(public_key)); @@ -584,15 +633,21 @@ impl Authenticator for crate::Authenti // 1. check mandatory parameters let platform_kek = match parameters.key_agreement.as_ref() { Some(key) => key, - None => { return Err(Error::MissingParameter); } + None => { + return Err(Error::MissingParameter); + } }; let new_pin_enc = match parameters.new_pin_enc.as_ref() { Some(pin) => pin, - None => { return Err(Error::MissingParameter); } + None => { + return Err(Error::MissingParameter); + } }; let pin_auth = match parameters.pin_auth.as_ref() { Some(auth) => auth, - None => { return Err(Error::MissingParameter); } + None => { + return Err(Error::MissingParameter); + } }; // 2. is pin already set @@ -601,7 +656,10 @@ impl Authenticator for crate::Authenti } // 3. generate shared secret - let shared_secret = self.state.runtime.generate_shared_secret(&mut self.trussed, platform_kek)?; + let shared_secret = self + .state + .runtime + .generate_shared_secret(&mut self.trussed, platform_kek)?; // TODO: there are moar early returns!! // - implement Drop? @@ -617,7 +675,9 @@ impl Authenticator for crate::Authenti // 6. store LEFT(SHA-256(newPin), 16), set retries to 8 self.hash_store_pin(&new_pin)?; - self.state.reset_retries(&mut self.trussed).map_err(|_| Error::Other)?; + self.state + .reset_retries(&mut self.trussed) + .map_err(|_| Error::Other)?; ctap2::client_pin::Response { key_agreement: None, @@ -632,31 +692,44 @@ impl Authenticator for crate::Authenti // 1. check mandatory parameters let platform_kek = match parameters.key_agreement.as_ref() { Some(key) => key, - None => { return Err(Error::MissingParameter); } + None => { + return Err(Error::MissingParameter); + } }; let pin_hash_enc = match parameters.pin_hash_enc.as_ref() { Some(hash) => hash, - None => { return Err(Error::MissingParameter); } + None => { + return Err(Error::MissingParameter); + } }; let new_pin_enc = match parameters.new_pin_enc.as_ref() { Some(pin) => pin, - None => { return Err(Error::MissingParameter); } + None => { + return Err(Error::MissingParameter); + } }; let pin_auth = match parameters.pin_auth.as_ref() { Some(auth) => auth, - None => { return Err(Error::MissingParameter); } + None => { + return Err(Error::MissingParameter); + } }; // 2. fail if no retries left self.state.pin_blocked()?; // 3. generate shared secret - let shared_secret = self.state.runtime.generate_shared_secret(&mut self.trussed, platform_kek)?; + let shared_secret = self + .state + .runtime + .generate_shared_secret(&mut self.trussed, platform_kek)?; // 4. verify pinAuth let mut data = MediumData::new(); - data.extend_from_slice(new_pin_enc).map_err(|_| Error::InvalidParameter)?; - data.extend_from_slice(pin_hash_enc).map_err(|_| Error::InvalidParameter)?; + data.extend_from_slice(new_pin_enc) + .map_err(|_| Error::InvalidParameter)?; + data.extend_from_slice(pin_hash_enc) + .map_err(|_| Error::InvalidParameter)?; self.verify_pin_auth(shared_secret, &data, pin_auth)?; // 5. decrement retries @@ -689,18 +762,25 @@ impl Authenticator for crate::Authenti // 1. check mandatory parameters let platform_kek = match parameters.key_agreement.as_ref() { Some(key) => key, - None => { return Err(Error::MissingParameter); } + None => { + return Err(Error::MissingParameter); + } }; let pin_hash_enc = match parameters.pin_hash_enc.as_ref() { Some(hash) => hash, - None => { return Err(Error::MissingParameter); } + None => { + return Err(Error::MissingParameter); + } }; // 2. fail if no retries left self.state.pin_blocked()?; // 3. generate shared secret - let shared_secret = self.state.runtime.generate_shared_secret(&mut self.trussed, platform_kek)?; + let shared_secret = self + .state + .runtime + .generate_shared_secret(&mut self.trussed, platform_kek)?; // 4. decrement retires self.state.decrement_retries(&mut self.trussed)?; @@ -715,7 +795,8 @@ impl Authenticator for crate::Authenti let pin_token = self.state.runtime.pin_token(&mut self.trussed); debug_now!("wrapping pin token"); // info_now!("exists? {}", syscall!(self.trussed.exists(shared_secret)).exists); - let pin_token_enc = syscall!(self.trussed.wrap_key_aes256cbc(shared_secret, pin_token)).wrapped_key; + let pin_token_enc = + syscall!(self.trussed.wrap_key_aes256cbc(shared_secret, pin_token)).wrapped_key; syscall!(self.trussed.delete(shared_secret)); @@ -732,9 +813,9 @@ impl Authenticator for crate::Authenti } } - Subcommand::GetPinUvAuthTokenUsingUvWithPermissions | - Subcommand::GetUVRetries | - Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => { + Subcommand::GetPinUvAuthTokenUsingUvWithPermissions + | Subcommand::GetUVRetries + | Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => { // todo!("not implemented yet") return Err(Error::InvalidParameter); } @@ -742,11 +823,12 @@ impl Authenticator for crate::Authenti } #[inline(never)] - fn credential_management(&mut self, parameters: &ctap2::credential_management::Request) - -> Result { - - use ctap2::credential_management::Subcommand; + fn credential_management( + &mut self, + parameters: &ctap2::credential_management::Request, + ) -> Result { use credential_management as cm; + use ctap2::credential_management::Subcommand; // TODO: I see "failed pinauth" output, but then still continuation... self.verify_pin_auth_using_token(parameters)?; @@ -754,44 +836,40 @@ impl Authenticator for crate::Authenti let mut cred_mgmt = cm::CredentialManagement::new(self); let sub_parameters = ¶meters.sub_command_params; match parameters.sub_command { - // 0x1 - Subcommand::GetCredsMetadata => - cred_mgmt.get_creds_metadata(), + Subcommand::GetCredsMetadata => cred_mgmt.get_creds_metadata(), // 0x2 - Subcommand::EnumerateRpsBegin => - cred_mgmt.first_relying_party(), + Subcommand::EnumerateRpsBegin => cred_mgmt.first_relying_party(), // 0x3 - Subcommand::EnumerateRpsGetNextRp => - cred_mgmt.next_relying_party(), + Subcommand::EnumerateRpsGetNextRp => cred_mgmt.next_relying_party(), // 0x4 Subcommand::EnumerateCredentialsBegin => { - let sub_parameters = sub_parameters.as_ref() - .ok_or(Error::MissingParameter)?; + let sub_parameters = sub_parameters.as_ref().ok_or(Error::MissingParameter)?; cred_mgmt.first_credential( sub_parameters - .rp_id_hash.as_ref() + .rp_id_hash + .as_ref() .ok_or(Error::MissingParameter)?, ) } // 0x5 - Subcommand::EnumerateCredentialsGetNextCredential => - cred_mgmt.next_credential(), + Subcommand::EnumerateCredentialsGetNextCredential => cred_mgmt.next_credential(), // 0x6 Subcommand::DeleteCredential => { - let sub_parameters = sub_parameters.as_ref() - .ok_or(Error::MissingParameter)?; + let sub_parameters = sub_parameters.as_ref().ok_or(Error::MissingParameter)?; - cred_mgmt.delete_credential(sub_parameters - .credential_id.as_ref() + cred_mgmt.delete_credential( + sub_parameters + .credential_id + .as_ref() .ok_or(Error::MissingParameter)?, - ) + ) } } } @@ -807,18 +885,21 @@ impl Authenticator for crate::Authenti Ok(()) } - #[inline(never)] - fn get_assertion(&mut self, parameters: &ctap2::get_assertion::Request) -> Result { - + fn get_assertion( + &mut self, + parameters: &ctap2::get_assertion::Request, + ) -> Result { debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); let rp_id_hash = self.hash(parameters.rp_id.as_ref()); // 1-4. let uv_performed = match self.pin_prechecks( - ¶meters.options, ¶meters.pin_auth, ¶meters.pin_protocol, - parameters.client_data_hash.as_ref(), + ¶meters.options, + ¶meters.pin_auth, + ¶meters.pin_protocol, + parameters.client_data_hash.as_ref(), ) { Ok(b) => b, Err(Error::PinRequired) => { @@ -833,9 +914,9 @@ impl Authenticator for crate::Authenti // Note: If allowList is passed, credential is Some(credential) // If no allowList is passed, credential is None and the retrieved credentials // are stored in state.runtime.credential_heap - let (credential, num_credentials) = self.prepare_credentials( - &rp_id_hash, ¶meters.allow_list, uv_performed - ).ok_or(Error::NoCredentials)?; + let (credential, num_credentials) = self + .prepare_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed) + .ok_or(Error::NoCredentials)?; info_now!("found {:?} applicable credentials", num_credentials); info_now!("{:?}", &credential); @@ -852,7 +933,8 @@ impl Authenticator for crate::Authenti // 7. collect user presence let up_performed = if do_up { info_now!("asking for up"); - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + self.up + .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; true } else { info_now!("not asking for up"); @@ -884,41 +966,42 @@ impl Authenticator for crate::Authenti self.assert_with_credential(num_credentials, credential) } - } // impl Authenticator for crate::Authenticator -impl crate::Authenticator -{ +impl crate::Authenticator { #[inline(never)] - fn check_credential_applicable(&mut self, credential: &Credential, allowlist_passed: bool, uv_performed: bool) -> bool { - + fn check_credential_applicable( + &mut self, + credential: &Credential, + allowlist_passed: bool, + uv_performed: bool, + ) -> bool { if !self.check_key_exists(credential.algorithm, &credential.key) { return false; } - if !{ - use credential::CredentialProtectionPolicy as Policy; - debug_now!("CredentialProtectionPolicy {:?}", &credential.cred_protect); - match credential.cred_protect { - None | Some(Policy::Optional) => true, - Some(Policy::OptionalWithCredentialIdList) => allowlist_passed || uv_performed, - Some(Policy::Required) => uv_performed, - - } - } { - return false; - } - true + if !{ + use credential::CredentialProtectionPolicy as Policy; + debug_now!("CredentialProtectionPolicy {:?}", &credential.cred_protect); + match credential.cred_protect { + None | Some(Policy::Optional) => true, + Some(Policy::OptionalWithCredentialIdList) => allowlist_passed || uv_performed, + Some(Policy::Required) => uv_performed, + } + } { + return false; + } + true } #[inline(never)] fn prepare_credentials( - &mut self, rp_id_hash: &Bytes<32>, + &mut self, + rp_id_hash: &Bytes<32>, allow_list: &Option, uv_performed: bool, - ) -> Option<(Credential, u32)> - { + ) -> Option<(Credential, u32)> { debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); self.state.runtime.clear_credential_cache(); @@ -963,20 +1046,19 @@ impl crate::Authenticator // we are only dealing with discoverable credentials. debug_now!("Allowlist not passed, fetching RKs"); - let mut maybe_path = syscall!(self.trussed.read_dir_first( - Location::Internal, - rp_rk_dir(rp_id_hash), - None, - )).entry.map(|entry| PathBuf::try_from(entry.path()).unwrap()); + let mut maybe_path = + syscall!(self + .trussed + .read_dir_first(Location::Internal, rp_rk_dir(rp_id_hash), None,)) + .entry + .map(|entry| PathBuf::try_from(entry.path()).unwrap()); - use core::str::FromStr; use crate::state::CachedCredential; + use core::str::FromStr; while let Some(path) = maybe_path { - let credential_data = syscall!(self.trussed.read_file( - Location::Internal, - path.clone(), - )).data; + let credential_data = + syscall!(self.trussed.read_file(Location::Internal, path.clone(),)).data; let credential = Credential::deserialize(&credential_data).ok()?; @@ -988,7 +1070,8 @@ impl crate::Authenticator } maybe_path = syscall!(self.trussed.read_dir_next()) - .entry.map(|entry| PathBuf::try_from(entry.path()).unwrap()); + .entry + .map(|entry| PathBuf::try_from(entry.path()).unwrap()); } let num_credentials = self.state.runtime.remaining_credentials(); @@ -996,20 +1079,27 @@ impl crate::Authenticator credential.map(|credential| (credential, num_credentials)) } - fn decrypt_pin_hash_and_maybe_escalate(&mut self, shared_secret: KeyId, pin_hash_enc: &Bytes<64>) - -> Result<()> - { - let pin_hash = syscall!(self.trussed.decrypt_aes256cbc( - shared_secret, pin_hash_enc)).plaintext.ok_or(Error::Other)?; + fn decrypt_pin_hash_and_maybe_escalate( + &mut self, + shared_secret: KeyId, + pin_hash_enc: &Bytes<64>, + ) -> Result<()> { + let pin_hash = syscall!(self.trussed.decrypt_aes256cbc(shared_secret, pin_hash_enc)) + .plaintext + .ok_or(Error::Other)?; let stored_pin_hash = match self.state.persistent.pin_hash() { Some(hash) => hash, - None => { return Err(Error::PinNotSet); } + None => { + return Err(Error::PinNotSet); + } }; if pin_hash != stored_pin_hash { // I) generate new KEK - self.state.runtime.rotate_key_agreement_key(&mut self.trussed); + self.state + .runtime + .rotate_key_agreement_key(&mut self.trussed); if self.state.persistent.retries() == 0 { return Err(Error::PinBlocked); } @@ -1025,20 +1115,28 @@ impl crate::Authenticator fn hash_store_pin(&mut self, pin: &Message) -> Result<()> { let pin_hash_32 = syscall!(self.trussed.hash_sha256(pin)).hash; let pin_hash: [u8; 16] = pin_hash_32[..16].try_into().unwrap(); - self.state.persistent.set_pin_hash(&mut self.trussed, pin_hash).unwrap(); + self.state + .persistent + .set_pin_hash(&mut self.trussed, pin_hash) + .unwrap(); Ok(()) } - fn decrypt_pin_check_length(&mut self, shared_secret: KeyId, pin_enc: &[u8]) -> Result { + fn decrypt_pin_check_length( + &mut self, + shared_secret: KeyId, + pin_enc: &[u8], + ) -> Result { // pin is expected to be filled with null bytes to length at least 64 if pin_enc.len() < 64 { // correct error? return Err(Error::PinPolicyViolation); } - let mut pin = syscall!(self.trussed.decrypt_aes256cbc( - shared_secret, pin_enc)).plaintext.ok_or(Error::Other)?; + let mut pin = syscall!(self.trussed.decrypt_aes256cbc(shared_secret, pin_enc)) + .plaintext + .ok_or(Error::Other)?; // // temp // let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len()); @@ -1055,7 +1153,6 @@ impl crate::Authenticator Ok(pin) } - // fn verify_pin(&mut self, pin_auth: &Bytes<16>, client_data_hash: &Bytes<32>) -> bool { fn verify_pin(&mut self, pin_auth: &[u8; 16], data: &[u8]) -> Result<()> { let key = self.state.runtime.pin_token(&mut self.trussed); @@ -1067,10 +1164,14 @@ impl crate::Authenticator } } - fn verify_pin_auth(&mut self, shared_secret: KeyId, data: &[u8], pin_auth: &Bytes<16>) - -> Result<()> - { - let expected_pin_auth = syscall!(self.trussed.sign_hmacsha256(shared_secret, data)).signature; + fn verify_pin_auth( + &mut self, + shared_secret: KeyId, + data: &[u8], + pin_auth: &Bytes<16>, + ) -> Result<()> { + let expected_pin_auth = + syscall!(self.trussed.sign_hmacsha256(shared_secret, data)).signature; if expected_pin_auth[..16] == pin_auth[..] { Ok(()) @@ -1082,53 +1183,54 @@ impl crate::Authenticator // fn verify_pin_auth_using_token(&mut self, data: &[u8], pin_auth: &Bytes<16>) fn verify_pin_auth_using_token( &mut self, - parameters: &ctap2::credential_management::Request + parameters: &ctap2::credential_management::Request, ) -> Result<()> { - // info_now!("CM params: {:?}", parameters); use ctap2::credential_management::Subcommand; match parameters.sub_command { // are we Haskell yet lol - sub_command @ Subcommand::GetCredsMetadata | - sub_command @ Subcommand::EnumerateRpsBegin | - sub_command @ Subcommand::EnumerateCredentialsBegin | - sub_command @ Subcommand::DeleteCredential => { - + sub_command @ Subcommand::GetCredsMetadata + | sub_command @ Subcommand::EnumerateRpsBegin + | sub_command @ Subcommand::EnumerateCredentialsBegin + | sub_command @ Subcommand::DeleteCredential => { // check pinProtocol let pin_protocol = parameters // .sub_command_params.as_ref().ok_or(Error::MissingParameter)? - .pin_protocol.ok_or(Error::MissingParameter)?; + .pin_protocol + .ok_or(Error::MissingParameter)?; if pin_protocol != 1 { return Err(Error::InvalidParameter); } // check pinAuth let pin_token = self.state.runtime.pin_token(&mut self.trussed); - let mut data: Bytes<{sizes::MAX_CREDENTIAL_ID_LENGTH_PLUS_256}> = + let mut data: Bytes<{ sizes::MAX_CREDENTIAL_ID_LENGTH_PLUS_256 }> = Bytes::from_slice(&[sub_command as u8]).unwrap(); let len = 1 + match sub_command { - Subcommand::EnumerateCredentialsBegin | - Subcommand::DeleteCredential => { + Subcommand::EnumerateCredentialsBegin | Subcommand::DeleteCredential => { data.resize_to_capacity(); // ble, need to reserialize ctap_types::serde::cbor_serialize( - ¶meters.sub_command_params - .as_ref() - .ok_or(Error::MissingParameter)?, + ¶meters + .sub_command_params + .as_ref() + .ok_or(Error::MissingParameter)?, &mut data[1..], - ).map_err(|_| Error::LimitExceeded)?.len() + ) + .map_err(|_| Error::LimitExceeded)? + .len() } _ => 0, }; // info_now!("input to hmacsha256: {:?}", &data[..len]); - let expected_pin_auth = syscall!(self.trussed.sign_hmacsha256( - pin_token, - &data[..len], - )).signature; + let expected_pin_auth = + syscall!(self.trussed.sign_hmacsha256(pin_token, &data[..len],)).signature; let pin_auth = parameters - .pin_auth.as_ref().ok_or(Error::MissingParameter)?; + .pin_auth + .as_ref() + .ok_or(Error::MissingParameter)?; if expected_pin_auth[..16] == pin_auth[..] { info_now!("passed pinauth"); @@ -1144,27 +1246,24 @@ impl crate::Authenticator info_now!("pinAuthInvalid"); Err(Error::PinAuthInvalid) } - } } // don't need the PIN auth, they're continuations // of already checked CredMgmt subcommands - Subcommand::EnumerateRpsGetNextRp | - Subcommand::EnumerateCredentialsGetNextCredential - => Ok(()), + Subcommand::EnumerateRpsGetNextRp + | Subcommand::EnumerateCredentialsGetNextCredential => Ok(()), } } /// Returns whether UV was performed. - fn pin_prechecks(&mut self, + fn pin_prechecks( + &mut self, options: &Option, pin_auth: &Option, pin_protocol: &Option, data: &[u8], - ) - -> Result - { + ) -> Result { // 1. pinAuth zero length -> wait for user touch, then // return PinNotSet if not set, PinInvalid if set // @@ -1172,7 +1271,8 @@ impl crate::Authenticator // wants to enforce PIN and needs to figure out which authnrs support PIN if let Some(pin_auth) = pin_auth.as_ref() { if pin_auth.len() == 0 { - self.up.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + self.up + .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; if !self.state.persistent.pin_is_set() { return Err(Error::PinNotSet); } else { @@ -1207,7 +1307,6 @@ impl crate::Authenticator // TODO: Should we should fail if `uv` is passed? // Current thinking: no if self.state.persistent.pin_is_set() { - // let mut uv_performed = false; if let Some(ref pin_auth) = pin_auth { if pin_auth.len() != 16 { @@ -1226,12 +1325,10 @@ impl crate::Authenticator )?; return Ok(true); - } else { // 7. pinAuth present + pinProtocol != 1 --> error PinAuthInvalid return Err(Error::PinAuthInvalid); } - } else { // 6. pinAuth not present + clientPin set --> error PinRequired if self.state.persistent.pin_is_set() { @@ -1266,7 +1363,8 @@ impl crate::Authenticator } #[inline(never)] - fn process_assertion_extensions(&mut self, + fn process_assertion_extensions( + &mut self, get_assertion_state: &state::ActiveGetAssertionData, extensions: &ctap2::get_assertion::ExtensionsInput, _credential: &Credential, @@ -1287,11 +1385,16 @@ impl crate::Authenticator credential_key, Some(Bytes::from_slice(&[get_assertion_state.uv_performed as u8]).unwrap()), trussed::types::StorageAttributes::new().set_persistence(Location::Volatile) - )).key; + )) + .key; // Verify the auth tag, which uses the same process as the pinAuth - let kek = self.state.runtime.generate_shared_secret(&mut self.trussed, &hmac_secret.key_agreement)?; - self.verify_pin_auth(kek, &hmac_secret.salt_enc, &hmac_secret.salt_auth).map_err(|_| Error::ExtensionFirst)?; + let kek = self + .state + .runtime + .generate_shared_secret(&mut self.trussed, &hmac_secret.key_agreement)?; + self.verify_pin_auth(kek, &hmac_secret.salt_enc, &hmac_secret.salt_auth) + .map_err(|_| Error::ExtensionFirst)?; if hmac_secret.salt_enc.len() != 32 && hmac_secret.salt_enc.len() != 64 { debug_now!("invalid hmac-secret length"); @@ -1299,24 +1402,29 @@ impl crate::Authenticator } // decrypt input salt_enc to get salt1 or (salt1 || salt2) - let salts = syscall!( - self.trussed.decrypt(Mechanism::Aes256Cbc, kek, &hmac_secret.salt_enc, b"", b"", b"") - ).plaintext.ok_or(Error::InvalidOption)?; + let salts = syscall!(self.trussed.decrypt( + Mechanism::Aes256Cbc, + kek, + &hmac_secret.salt_enc, + b"", + b"", + b"" + )) + .plaintext + .ok_or(Error::InvalidOption)?; let mut salt_output: Bytes<64> = Bytes::new(); // output1 = hmac_sha256(credRandom, salt1) - let output1 = syscall!( - self.trussed.sign_hmacsha256(cred_random, &salts[0..32]) - ).signature; + let output1 = + syscall!(self.trussed.sign_hmacsha256(cred_random, &salts[0..32])).signature; salt_output.extend_from_slice(&output1).unwrap(); if salts.len() == 64 { // output2 = hmac_sha256(credRandom, salt2) - let output2 = syscall!( - self.trussed.sign_hmacsha256(cred_random, &salts[32..64]) - ).signature; + let output2 = + syscall!(self.trussed.sign_hmacsha256(cred_random, &salts[32..64])).signature; salt_output.extend_from_slice(&output2).unwrap(); } @@ -1324,25 +1432,26 @@ impl crate::Authenticator syscall!(self.trussed.delete(cred_random)); // output_enc = aes256-cbc(sharedSecret, IV=0, output1 || output2) - let output_enc = syscall!( - self.trussed.encrypt(Mechanism::Aes256Cbc, kek, &salt_output, b"", None) - ).ciphertext; + let output_enc = + syscall!(self + .trussed + .encrypt(Mechanism::Aes256Cbc, kek, &salt_output, b"", None)) + .ciphertext; Ok(Some(ctap2::get_assertion::ExtensionsOutput { - hmac_secret: Some(Bytes::from_slice(&output_enc).unwrap()) + hmac_secret: Some(Bytes::from_slice(&output_enc).unwrap()), })) - } else { Ok(None) } - } - #[inline(never)] - fn assert_with_credential(&mut self, num_credentials: Option, credential: Credential) - -> Result - { + fn assert_with_credential( + &mut self, + num_credentials: Option, + credential: Credential, + ) -> Result { let data = self.state.runtime.active_get_assertion.clone().unwrap(); let rp_id_hash = Bytes::from_slice(&data.rp_id_hash).unwrap(); @@ -1357,12 +1466,15 @@ impl crate::Authenticator b"", // &rp_id_hash, Location::Volatile, - )).key; + )) + .key; // debug_now!("key result: {:?}", &key_result); info_now!("key result"); match key_result { Some(key) => (key, false), - None => { return Err(Error::Other); } + None => { + return Err(Error::Other); + } } } }; @@ -1377,7 +1489,10 @@ impl crate::Authenticator // 9./10. sign clientDataHash || authData with "first" credential // info_now!("signing with credential {:?}", &credential); - let kek = self.state.persistent.key_encryption_key(&mut self.trussed)?; + let kek = self + .state + .persistent + .key_encryption_key(&mut self.trussed)?; let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; use ctap2::AuthenticatorDataFlags as Flags; @@ -1403,20 +1518,26 @@ impl crate::Authenticator sign_count: sig_count, attested_credential_data: None, - extensions: extensions_output + extensions: extensions_output, }; let serialized_auth_data = authenticator_data.serialize(); let mut commitment = Bytes::<1024>::new(); - commitment.extend_from_slice(&serialized_auth_data).map_err(|_| Error::Other)?; - commitment.extend_from_slice(&data.client_data_hash).map_err(|_| Error::Other)?; + commitment + .extend_from_slice(&serialized_auth_data) + .map_err(|_| Error::Other)?; + commitment + .extend_from_slice(&data.client_data_hash) + .map_err(|_| Error::Other)?; let (mechanism, serialization) = match credential.algorithm { -7 => (Mechanism::P256, SignatureSerialization::Asn1Der), -8 => (Mechanism::Ed255, SignatureSerialization::Raw), // -9 => (Mechanism::Totp, SignatureSerialization::Raw), - _ => { return Err(Error::Other); } + _ => { + return Err(Error::Other); + } }; debug_now!("signing with {:?}, {:?}", &mechanism, &serialization); @@ -1426,8 +1547,12 @@ impl crate::Authenticator // info_now!("TOTP with timestamp {:?}", ×tamp); // syscall!(self.trussed.sign_totp(key, timestamp)).signature.to_bytes().unwrap() // } - _ => syscall!(self.trussed.sign(mechanism, key, &commitment, serialization)).signature - .to_bytes().unwrap(), + _ => syscall!(self + .trussed + .sign(mechanism, key, &commitment, serialization)) + .signature + .to_bytes() + .unwrap(), }; if !is_rk { @@ -1464,14 +1589,12 @@ impl crate::Authenticator rp_id_hash: &Bytes<32>, user_id: &Bytes<64>, ) -> Result<()> { - // Prepare to iterate over all credentials associated to RP. let rp_path = rp_rk_dir(rp_id_hash); - let mut entry = syscall!(self.trussed.read_dir_first( - Location::Internal, - rp_path, - None, - )).entry; + let mut entry = syscall!(self + .trussed + .read_dir_first(Location::Internal, rp_path, None,)) + .entry; loop { info_now!("this may be an RK: {:?}", &entry); @@ -1483,10 +1606,8 @@ impl crate::Authenticator }; info_now!("checking RK {:?} for userId ", &rk_path); - let credential_data = syscall!(self.trussed.read_file( - Location::Internal, - rk_path.clone(), - )).data; + let credential_data = + syscall!(self.trussed.read_file(Location::Internal, rk_path.clone(),)).data; let credential_maybe = Credential::deserialize(&credential_data); if let Ok(old_credential) = credential_maybe { @@ -1500,10 +1621,7 @@ impl crate::Authenticator warn_now!(":: WARNING: unexpected server credential in rk."); } } - syscall!(self.trussed.remove_file( - Location::Internal, - rk_path, - )); + syscall!(self.trussed.remove_file(Location::Internal, rk_path,)); info_now!("Overwriting previous rk tied to this userId."); break; @@ -1517,27 +1635,19 @@ impl crate::Authenticator } Ok(()) - } #[inline(never)] - pub(crate) fn delete_resident_key_by_path( - &mut self, - rk_path: &Path, - ) - -> Result<()> - { + pub(crate) fn delete_resident_key_by_path(&mut self, rk_path: &Path) -> Result<()> { info_now!("deleting RK {:?}", &rk_path); - let credential_data = syscall!(self.trussed.read_file( - Location::Internal, - PathBuf::from(rk_path), - )).data; + let credential_data = syscall!(self + .trussed + .read_file(Location::Internal, PathBuf::from(rk_path),)) + .data; let credential_maybe = Credential::deserialize(&credential_data); // info_now!("deleting credential {:?}", &credential); - if let Ok(credential) = credential_maybe { - match credential.key { credential::Key::ResidentKey(key) => { info_now!(":: deleting resident key"); @@ -1552,15 +1662,12 @@ impl crate::Authenticator } info_now!(":: deleting RK file {:?} itself", &rk_path); - syscall!(self.trussed.remove_file( - Location::Internal, - PathBuf::from(rk_path), - )); - + syscall!(self + .trussed + .remove_file(Location::Internal, PathBuf::from(rk_path),)); Ok(()) } - } fn rp_rk_dir(rp_id_hash: &Bytes<32>) -> PathBuf { @@ -1583,4 +1690,3 @@ fn rk_path(rp_id_hash: &Bytes<32>, credential_id_hash: &Bytes<32>) -> PathBuf { path } - diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 9f2557d..d8bc26f 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -4,45 +4,35 @@ use core::convert::TryFrom; use trussed::{ syscall, - types::{ - DirEntry, - Location, - }, + types::{DirEntry, Location}, }; use ctap_types::{ - heapless_bytes::Bytes, - Error, - ctap2::credential_management::{ - CredentialProtectionPolicy, - Response, - }, cose::PublicKey, + ctap2::credential_management::{CredentialProtectionPolicy, Response}, + heapless_bytes::Bytes, webauthn::PublicKeyCredentialDescriptor, + Error, }; use littlefs2::path::{Path, PathBuf}; use crate::{ - Authenticator, - Result, - UserPresence, credential::Credential, - state::{ - CredentialManagementEnumerateRps, - CredentialManagementEnumerateCredentials, - }, - TrussedRequirements, + state::{CredentialManagementEnumerateCredentials, CredentialManagementEnumerateRps}, + Authenticator, Result, TrussedRequirements, UserPresence, }; pub(crate) struct CredentialManagement<'a, UP, T> -where UP: UserPresence, +where + UP: UserPresence, { authnr: &'a mut Authenticator, } impl core::ops::Deref for CredentialManagement<'_, UP, T> -where UP: UserPresence, +where + UP: UserPresence, { type Target = Authenticator; fn deref(&self) -> &Self::Target { @@ -51,7 +41,8 @@ where UP: UserPresence, } impl core::ops::DerefMut for CredentialManagement<'_, UP, T> -where UP: UserPresence, +where + UP: UserPresence, { fn deref_mut(&mut self) -> &mut Self::Target { self.authnr @@ -59,7 +50,8 @@ where UP: UserPresence, } impl<'a, UP, T> CredentialManagement<'a, UP, T> -where UP: UserPresence, +where + UP: UserPresence, { pub fn new(authnr: &'a mut Authenticator) -> Self { Self { authnr } @@ -67,25 +59,26 @@ where UP: UserPresence, } impl CredentialManagement<'_, UP, T> -where UP: UserPresence, - T: TrussedRequirements, +where + UP: UserPresence, + T: TrussedRequirements, { pub fn get_creds_metadata(&mut self) -> Result { info!("get metadata"); - let mut response: Response = - Default::default(); + let mut response: Response = Default::default(); - let guesstimate = self.state.persistent - .max_resident_credentials_guesstimate(); + let guesstimate = self.state.persistent.max_resident_credentials_guesstimate(); response.existing_resident_credentials_count = Some(0); - response.max_possible_remaining_residential_credentials_count = - Some(guesstimate); + response.max_possible_remaining_residential_credentials_count = Some(guesstimate); let dir = PathBuf::from(b"rk"); - let maybe_first_rp = syscall!(self.trussed.read_dir_first( - Location::Internal, dir.clone(), None)).entry; + let maybe_first_rp = + syscall!(self + .trussed + .read_dir_first(Location::Internal, dir.clone(), None)) + .entry; - let first_rp = match maybe_first_rp{ + let first_rp = match maybe_first_rp { None => return Ok(response), Some(rp) => rp, }; @@ -94,17 +87,16 @@ where UP: UserPresence, let mut last_rp = PathBuf::from(first_rp.file_name()); loop { - syscall!(self.trussed.read_dir_first( - Location::Internal, - dir.clone(), - Some(last_rp), - )).entry.unwrap(); + syscall!(self + .trussed + .read_dir_first(Location::Internal, dir.clone(), Some(last_rp),)) + .entry + .unwrap(); let maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; match maybe_next_rp { None => { - response.existing_resident_credentials_count = - Some(num_rks); + response.existing_resident_credentials_count = Some(num_rks); response.max_possible_remaining_residential_credentials_count = Some(if num_rks >= guesstimate { 0 @@ -116,8 +108,7 @@ where UP: UserPresence, Some(rp) => { last_rp = PathBuf::from(rp.file_name()); info!("counting.."); - let (this_rp_rk_count, _) = - self.count_rp_rks(PathBuf::from(rp.path()))?; + let (this_rp_rk_count, _) = self.count_rp_rks(PathBuf::from(rp.path()))?; info!("{:?}", this_rp_rk_count); num_rks += this_rp_rk_count; } @@ -136,16 +127,15 @@ where UP: UserPresence, let dir = PathBuf::from(b"rk"); - let maybe_first_rp = syscall!(self.trussed.read_dir_first( - Location::Internal, dir, None)).entry; + let maybe_first_rp = + syscall!(self.trussed.read_dir_first(Location::Internal, dir, None)).entry; response.total_rps = Some(match maybe_first_rp { None => 0, _ => { let mut num_rps = 1; loop { - let maybe_next_rp = syscall!(self.trussed.read_dir_next()) - .entry; + let maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; match maybe_next_rp { None => break, _ => num_rps += 1, @@ -156,21 +146,21 @@ where UP: UserPresence, }); if let Some(rp) = maybe_first_rp { - // load credential and extract rp and rpIdHash let maybe_first_credential = syscall!(self.trussed.read_dir_first( Location::Internal, PathBuf::from(rp.path()), None - )).entry; + )) + .entry; match maybe_first_credential { None => panic!("chaos! disorder!"), Some(rk_entry) => { - let serialized = syscall!(self.trussed.read_file( - Location::Internal, - rk_entry.path().into(), - )).data; + let serialized = syscall!(self + .trussed + .read_file(Location::Internal, rk_entry.path().into(),)) + .data; let credential = Credential::deserialize(&serialized) // this may be a confusing error message @@ -180,7 +170,6 @@ where UP: UserPresence, response.rp_id_hash = Some(self.hash(rp.id.as_ref())); response.rp = Some(rp); - } } @@ -188,11 +177,10 @@ where UP: UserPresence, if let Some(total_rps) = response.total_rps { if total_rps > 1 { let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); - self.state.runtime.cached_rp = Some( - CredentialManagementEnumerateRps { - remaining: total_rps - 1, - rp_id_hash, - }); + self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { + remaining: total_rps - 1, + rp_id_hash, + }); } } } @@ -206,7 +194,12 @@ where UP: UserPresence, let CredentialManagementEnumerateRps { remaining, rp_id_hash: last_rp_id_hash, - } = self.state.runtime.cached_rp.clone().ok_or(Error::NotAllowed)?; + } = self + .state + .runtime + .cached_rp + .clone() + .ok_or(Error::NotAllowed)?; let dir = PathBuf::from(b"rk"); @@ -214,11 +207,11 @@ where UP: UserPresence, super::format_hex(&last_rp_id_hash[..8], &mut hex); let filename = PathBuf::from(&hex); - let mut maybe_next_rp = syscall!(self.trussed.read_dir_first( - Location::Internal, - dir, - Some(filename), - )).entry; + let mut maybe_next_rp = + syscall!(self + .trussed + .read_dir_first(Location::Internal, dir, Some(filename),)) + .entry; // Advance to the next if maybe_next_rp.is_some() { @@ -235,15 +228,16 @@ where UP: UserPresence, Location::Internal, PathBuf::from(rp.path()), None - )).entry; + )) + .entry; match maybe_first_credential { None => panic!("chaos! disorder!"), Some(rk_entry) => { - let serialized = syscall!(self.trussed.read_file( - Location::Internal, - rk_entry.path().into(), - )).data; + let serialized = syscall!(self + .trussed + .read_file(Location::Internal, rk_entry.path().into(),)) + .data; let credential = Credential::deserialize(&serialized) // this may be a confusing error message @@ -274,11 +268,11 @@ where UP: UserPresence, } fn count_rp_rks(&mut self, rp_dir: PathBuf) -> Result<(u32, DirEntry)> { - let maybe_first_rk = syscall!(self.trussed.read_dir_first( - Location::Internal, - rp_dir, - None - )).entry; + let maybe_first_rk = + syscall!(self + .trussed + .read_dir_first(Location::Internal, rp_dir, None)) + .entry; let first_rk = maybe_first_rk.ok_or(Error::NoCredentials)?; @@ -302,8 +296,7 @@ where UP: UserPresence, let (num_rks, first_rk) = self.count_rp_rks(rp_dir)?; // extract data required into response - let mut response = self.extract_response_from_credential_file( - first_rk.path())?; + let mut response = self.extract_response_from_credential_file(first_rk.path())?; response.total_credentials = Some(num_rks); // cache state for next call @@ -328,7 +321,12 @@ where UP: UserPresence, remaining, rp_dir, prev_filename, - } = self.state.runtime.cached_rk.clone().ok_or(Error::NotAllowed)?; + } = self + .state + .runtime + .cached_rk + .clone() + .ok_or(Error::NotAllowed)?; // let (remaining, rp_dir, prev_filename) = match self.state.runtime.cached_rk { // Some(CredentialManagementEnumerateCredentials( // x, ref y, ref z)) @@ -342,11 +340,11 @@ where UP: UserPresence, // super::format_hex(&rp_id_hash[..8], &mut hex); // let rp_dir = PathBuf::from(b"rk").join(&PathBuf::from(&hex)); - let mut maybe_next_rk = syscall!(self.trussed.read_dir_first( - Location::Internal, - rp_dir, - Some(prev_filename) - )).entry; + let mut maybe_next_rk = + syscall!(self + .trussed + .read_dir_first(Location::Internal, rp_dir, Some(prev_filename))) + .entry; // Advance to the next if maybe_next_rk.is_some() { @@ -358,8 +356,7 @@ where UP: UserPresence, match maybe_next_rk { Some(rk) => { // extract data required into response - let response = self.extract_response_from_credential_file( - rk.path())?; + let response = self.extract_response_from_credential_file(rk.path())?; // cache state for next call if remaining > 1 { @@ -376,19 +373,14 @@ where UP: UserPresence, } } - fn extract_response_from_credential_file(&mut self, rk_path: &Path) -> Result { - // user (0x06) // credentialID (0x07): PublicKeyCredentialDescriptor // publicKey (0x08): public key of the credential in COSE_Key format // totalCredentials (0x09): total number of credentials for this RP // credProtect (0x0A): credential protection policy - let serialized = syscall!(self.trussed.read_file( - Location::Internal, - rk_path.into(), - )).data; + let serialized = syscall!(self.trussed.read_file(Location::Internal, rk_path.into(),)).data; let credential = Credential::deserialize(&serialized) // this may be a confusing error message @@ -398,7 +390,10 @@ where UP: UserPresence, // why these contortions to get kek. sheesh let authnr = &mut self.authnr; - let kek = authnr.state.persistent.key_encryption_key(&mut authnr.trussed)?; + let kek = authnr + .state + .persistent + .key_encryption_key(&mut authnr.trussed)?; let credential_id = credential.id(&mut self.trussed, kek, None)?; @@ -412,33 +407,38 @@ where UP: UserPresence, use trussed::types::{KeySerialization, Mechanism}; let algorithm = SigningAlgorithm::try_from(credential.algorithm)?; - let cose_public_key = match algorithm { + let cose_public_key = match algorithm { SigningAlgorithm::P256 => { - let public_key = syscall!(self.trussed.derive_p256_public_key(private_key, Location::Volatile)).key; + let public_key = syscall!(self + .trussed + .derive_p256_public_key(private_key, Location::Volatile)) + .key; let cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, public_key, + Mechanism::P256, + public_key, // KeySerialization::EcdhEsHkdf256 KeySerialization::Cose, - )).serialized_key; + )) + .serialized_key; syscall!(self.trussed.delete(public_key)); - PublicKey::P256Key( - ctap_types::serde::cbor_deserialize(&cose_public_key) - .unwrap()) + PublicKey::P256Key(ctap_types::serde::cbor_deserialize(&cose_public_key).unwrap()) } SigningAlgorithm::Ed25519 => { - let public_key = syscall!(self.trussed.derive_ed255_public_key( - private_key, Location::Volatile)).key; - let cose_public_key = syscall!(self.trussed.serialize_ed255_key( - public_key, KeySerialization::Cose - )).serialized_key; + let public_key = syscall!(self + .trussed + .derive_ed255_public_key(private_key, Location::Volatile)) + .key; + let cose_public_key = syscall!(self + .trussed + .serialize_ed255_key(public_key, KeySerialization::Cose)) + .serialized_key; syscall!(self.trussed.delete(public_key)); PublicKey::Ed25519Key( - ctap_types::serde::cbor_deserialize(&cose_public_key) - .unwrap()) - } - // SigningAlgorithm::Totp => { - // PublicKey::TotpKey(Default::default()) - // } + ctap_types::serde::cbor_deserialize(&cose_public_key).unwrap(), + ) + } // SigningAlgorithm::Totp => { + // PublicKey::TotpKey(Default::default()) + // } }; let cred_protect = match credential.cred_protect { Some(x) => Some(x), @@ -456,11 +456,10 @@ where UP: UserPresence, Ok(response) } - pub fn delete_credential(&mut self, + pub fn delete_credential( + &mut self, credential_descriptor: &PublicKeyCredentialDescriptor, - ) - -> Result - { + ) -> Result { info!("delete credential"); let credential_id_hash = self.hash(&credential_descriptor.id[..]); let mut hex = [b'0'; 16]; @@ -468,43 +467,39 @@ where UP: UserPresence, let dir = PathBuf::from(b"rk"); let filename = PathBuf::from(&hex); - let rk_path = syscall!(self.trussed.locate_file( - Location::Internal, - Some(dir), - filename, - )).path.ok_or(Error::InvalidCredential)?; - + let rk_path = syscall!(self + .trussed + .locate_file(Location::Internal, Some(dir), filename,)) + .path + .ok_or(Error::InvalidCredential)?; // DELETE self.delete_resident_key_by_path(&rk_path)?; // get rid of directory if it's now empty - let rp_path = rk_path.parent() + let rp_path = rk_path + .parent() // by construction, RK has a parent, its RP .unwrap(); - let maybe_first_remaining_rk = syscall!(self.trussed.read_dir_first( - Location::Internal, - rp_path.clone(), - None, - )).entry; + let maybe_first_remaining_rk = + syscall!(self + .trussed + .read_dir_first(Location::Internal, rp_path.clone(), None,)) + .entry; if maybe_first_remaining_rk.is_none() { - info!("deleting parent {:?} as this was its last RK", - &rp_path); - syscall!(self.trussed.remove_dir( - Location::Internal, - rp_path, - )); + info!("deleting parent {:?} as this was its last RK", &rp_path); + syscall!(self.trussed.remove_dir(Location::Internal, rp_path,)); } else { - info!("not deleting deleting parent {:?} as there is {:?}", - &rp_path, - &maybe_first_remaining_rk.unwrap().path(), - ); + info!( + "not deleting deleting parent {:?} as there is {:?}", + &rp_path, + &maybe_first_remaining_rk.unwrap().path(), + ); } // just return OK let response = Default::default(); Ok(response) } } - diff --git a/src/dispatch.rs b/src/dispatch.rs index fe09e3e..e1da024 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -3,30 +3,36 @@ mod apdu; mod ctaphid; -use crate::{Authenticator, TrussedRequirements, UserPresence}; #[allow(unused_imports)] use crate::msp; +use crate::{Authenticator, TrussedRequirements, UserPresence}; use ctap_types::{ctap1, ctap2}; use iso7816::Status; impl iso7816::App for Authenticator -where UP: UserPresence, +where + UP: UserPresence, { fn aid(&self) -> iso7816::Aid { - iso7816::Aid::new(&[ 0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01]) + iso7816::Aid::new(&[0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01]) } } - #[inline(never)] /// Deserialize U2F, call authenticator, serialize response *Result*. -fn handle_ctap1(authenticator: &mut Authenticator, data: &[u8], response: &mut apdu_dispatch::response::Data) -where +fn handle_ctap1( + authenticator: &mut Authenticator, + data: &[u8], + response: &mut apdu_dispatch::response::Data, +) where T: TrussedRequirements, UP: UserPresence, { - debug_now!("handle CTAP1: remaining stack: {} bytes", msp() - 0x2000_0000); + debug_now!( + "handle CTAP1: remaining stack: {} bytes", + msp() - 0x2000_0000 + ); // debug_now!("1A SP: {:X}", msp()); match try_handle_ctap1(authenticator, data, response) { Ok(()) => { @@ -34,12 +40,12 @@ where // Need to add x9000 success code (normally the apdu-dispatch does this, but // since u2f uses apdus over ctaphid, we must do it here.) response.extend_from_slice(&[0x90, 0x00]).ok(); - }, + } Err(status) => { let code: [u8; 2] = status.into(); debug_now!("CTAP1 error: {:?} ({})", status, hex_str!(&code)); response.extend_from_slice(&code).ok(); - }, + } } // debug_now!("1B SP: {:X}", msp()); debug_now!("end handle CTAP1"); @@ -47,12 +53,18 @@ where #[inline(never)] /// Deserialize CBOR, call authenticator, serialize response *Result*. -fn handle_ctap2(authenticator: &mut Authenticator, data: &[u8], response: &mut apdu_dispatch::response::Data) -where +fn handle_ctap2( + authenticator: &mut Authenticator, + data: &[u8], + response: &mut apdu_dispatch::response::Data, +) where T: TrussedRequirements, UP: UserPresence, { - debug_now!("handle CTAP2: remaining stack: {} bytes", msp() - 0x2000_0000); + debug_now!( + "handle CTAP2: remaining stack: {} bytes", + msp() - 0x2000_0000 + ); // debug_now!("2A SP: {:X}", msp()); if let Err(error) = try_handle_ctap2(authenticator, data, response) { debug_now!("CTAP2 error: {:02X}", error); @@ -63,14 +75,20 @@ where } #[inline(never)] -fn try_handle_ctap1(authenticator: &mut Authenticator, data: &[u8], response: &mut apdu_dispatch::response::Data) - -> Result<(), Status> +fn try_handle_ctap1( + authenticator: &mut Authenticator, + data: &[u8], + response: &mut apdu_dispatch::response::Data, +) -> Result<(), Status> where T: TrussedRequirements, UP: UserPresence, { // Annoyance: We can't load in fido-authenticator constructor. - authenticator.state.persistent.load_if_not_initialised(&mut authenticator.trussed); + authenticator + .state + .persistent + .load_if_not_initialised(&mut authenticator.trussed); // let command = apdu_dispatch::Command::try_from(data) // .map_err(|_| Status::IncorrectDataParameter)?; @@ -97,16 +115,25 @@ where } #[inline(never)] -fn try_handle_ctap2(authenticator: &mut Authenticator, data: &[u8], response: &mut apdu_dispatch::response::Data) - -> Result<(), u8> +fn try_handle_ctap2( + authenticator: &mut Authenticator, + data: &[u8], + response: &mut apdu_dispatch::response::Data, +) -> Result<(), u8> where T: TrussedRequirements, UP: UserPresence, { // Annoyance: We can't load in fido-authenticator constructor. - authenticator.state.persistent.load_if_not_initialised(&mut authenticator.trussed); + authenticator + .state + .persistent + .load_if_not_initialised(&mut authenticator.trussed); - debug_now!("try_handle CTAP2: remaining stack: {} bytes", msp() - 0x2000_0000); + debug_now!( + "try_handle CTAP2: remaining stack: {} bytes", + msp() - 0x2000_0000 + ); // let ctap_request = ctap2::Request::deserialize(data) // .map_err(|error| error as u8)?; @@ -120,22 +147,30 @@ where } #[inline(never)] -fn try_get_ctap2_response(authenticator: &mut Authenticator, data: &[u8]) - -> Result +fn try_get_ctap2_response( + authenticator: &mut Authenticator, + data: &[u8], +) -> Result where T: TrussedRequirements, UP: UserPresence, { // Annoyance: We can't load in fido-authenticator constructor. - authenticator.state.persistent.load_if_not_initialised(&mut authenticator.trussed); + authenticator + .state + .persistent + .load_if_not_initialised(&mut authenticator.trussed); - debug_now!("try_get CTAP2: remaining stack: {} bytes", msp() - 0x2000_0000); + debug_now!( + "try_get CTAP2: remaining stack: {} bytes", + msp() - 0x2000_0000 + ); // Goal of these nested scopes is to keep stack small. - let ctap_request = ctap2::Request::deserialize(data) - .map_err(|error| error as u8)?; + let ctap_request = ctap2::Request::deserialize(data).map_err(|error| error as u8)?; debug_now!("2a SP: {:X}", msp()); use ctap2::Authenticator; - authenticator.call_ctap2(&ctap_request) + authenticator + .call_ctap2(&ctap_request) .map_err(|error| error as u8) } diff --git a/src/dispatch/apdu.rs b/src/dispatch/apdu.rs index 9e66fc0..7d4e834 100644 --- a/src/dispatch/apdu.rs +++ b/src/dispatch/apdu.rs @@ -1,9 +1,6 @@ -use apdu_dispatch::{Command, response::Data, app as apdu}; +use apdu_dispatch::{app as apdu, response::Data, Command}; +use ctap_types::{serde::error::Error as SerdeError, Error}; use ctaphid_dispatch::app as ctaphid; -use ctap_types::{ - Error, - serde::error::Error as SerdeError, -}; use iso7816::Status; use crate::{Authenticator, TrussedRequirements, UserPresence}; @@ -16,26 +13,21 @@ pub enum CtapMappingError { impl From for Error { fn from(mapping_error: CtapMappingError) -> Error { match mapping_error { - CtapMappingError::InvalidCommand(_cmd) => { - Error::InvalidCommand - } - CtapMappingError::ParsingError(cbor_error) => { - match cbor_error { - SerdeError::SerdeMissingField => Error::MissingParameter, - _ => Error::InvalidCbor - } - } + CtapMappingError::InvalidCommand(_cmd) => Error::InvalidCommand, + CtapMappingError::ParsingError(cbor_error) => match cbor_error { + SerdeError::SerdeMissingField => Error::MissingParameter, + _ => Error::InvalidCbor, + }, } - } } -impl apdu::App<{apdu_dispatch::command::SIZE}, {apdu_dispatch::response::SIZE} > -for Authenticator - where UP: UserPresence, - T: TrussedRequirements, +impl apdu::App<{ apdu_dispatch::command::SIZE }, { apdu_dispatch::response::SIZE }> + for Authenticator +where + UP: UserPresence, + T: TrussedRequirements, { - fn select(&mut self, _: &Command, reply: &mut Data) -> apdu::Result { reply.extend_from_slice(b"U2F_V2").unwrap(); Ok(()) @@ -43,7 +35,12 @@ for Authenticator fn deselect(&mut self) {} - fn call(&mut self, interface: apdu::Interface, apdu: &Command, response: &mut Data) -> apdu::Result { + fn call( + &mut self, + interface: apdu::Interface, + apdu: &Command, + response: &mut Data, + ) -> apdu::Result { // FIDO-over-CCID does not seem to officially be a thing; we don't support it. // If we would, need to review the following cases catering to semi-documented U2F legacy. if interface != apdu::Interface::Contactless { @@ -62,7 +59,7 @@ for Authenticator Ok(match instruction { // U2F instruction codes // NB(nickray): I don't think 0x00 is a valid case. - 0x00 | 0x01 | 0x02 => super::handle_ctap1(self, apdu.data(), response),//self.call_authenticator_u2f(apdu, response), + 0x00 | 0x01 | 0x02 => super::handle_ctap1(self, apdu.data(), response), //self.call_authenticator_u2f(apdu, response), _ => { match ctaphid::Command::try_from(instruction) { @@ -79,4 +76,3 @@ for Authenticator }) } } - diff --git a/src/dispatch/ctaphid.rs b/src/dispatch/ctaphid.rs index e6943a5..fac824c 100644 --- a/src/dispatch/ctaphid.rs +++ b/src/dispatch/ctaphid.rs @@ -1,22 +1,29 @@ use ctaphid_dispatch::app as ctaphid; -use crate::{Authenticator, TrussedRequirements, UserPresence}; #[allow(unused_imports)] use crate::msp; +use crate::{Authenticator, TrussedRequirements, UserPresence}; impl ctaphid::App for Authenticator -where UP: UserPresence, - T: TrussedRequirements, +where + UP: UserPresence, + T: TrussedRequirements, { - - fn commands(&self,) -> &'static [ctaphid::Command] { - &[ ctaphid::Command::Cbor, ctaphid::Command::Msg ] + fn commands(&self) -> &'static [ctaphid::Command] { + &[ctaphid::Command::Cbor, ctaphid::Command::Msg] } #[inline(never)] - fn call(&mut self, command: ctaphid::Command, request: &ctaphid::Message, response: &mut ctaphid::Message) -> ctaphid::AppResult { - - debug_now!("ctaphid-dispatch: remaining stack: {} bytes", msp() - 0x2000_0000); + fn call( + &mut self, + command: ctaphid::Command, + request: &ctaphid::Message, + response: &mut ctaphid::Message, + ) -> ctaphid::AppResult { + debug_now!( + "ctaphid-dispatch: remaining stack: {} bytes", + msp() - 0x2000_0000 + ); if request.len() < 1 { debug_now!("invalid request length in ctaphid.call"); @@ -26,7 +33,6 @@ where UP: UserPresence, // info_now!("request: "); // blocking::dump_hex(request, request.len()); Ok(match command { - ctaphid::Command::Cbor => super::handle_ctap2(self, request, response), ctaphid::Command::Msg => super::handle_ctap1(self, request, response), _ => { diff --git a/src/lib.rs b/src/lib.rs index d549f70..8cd6ab3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,17 +15,11 @@ extern crate delog; generate_macros!(); -use trussed::{ - client, syscall, - Client as TrussedClient, - types::{ - Message, - }, -}; +use trussed::{client, syscall, types::Message, Client as TrussedClient}; use ctap_types::{ - heapless_bytes::Bytes, authenticator::{Request, Response}, + heapless_bytes::Bytes, }; /// Re-export of `ctap-types` authenticator errors. @@ -34,7 +28,7 @@ pub use ctap_types::Error; mod ctap1; mod ctap2; -#[cfg(feature="dispatch")] +#[cfg(feature = "dispatch")] mod dispatch; pub mod constants; @@ -44,7 +38,6 @@ pub mod state; /// Results with our [`Error`]. pub type Result = core::result::Result; - /// Trait bound on our implementation's requirements from a Trussed client. /// /// - Client is core Trussed client functionality. @@ -58,21 +51,20 @@ pub trait TrussedRequirements: + client::Aes256Cbc + client::Sha256 + client::HmacSha256 - + client::Ed255 - // + client::Totp -{} + + client::Ed255 // + client::Totp +{ +} -impl TrussedRequirements for T -where T: - client::Client - + client::P256 - + client::Chacha8Poly1305 - + client::Aes256Cbc - + client::Sha256 - + client::HmacSha256 - + client::Ed255 - // + client::Totp -{} +impl TrussedRequirements for T where + T: client::Client + + client::P256 + + client::Chacha8Poly1305 + + client::Aes256Cbc + + client::Sha256 + + client::HmacSha256 + + client::Ed255 // + client::Totp +{ +} #[derive(Copy, Clone, Debug, Eq, PartialEq)] /// Externally defined configuration. @@ -140,7 +132,9 @@ fn format_hex(data: &[u8], mut buffer: &mut [u8]) { #[inline] #[allow(dead_code)] -pub(crate) fn msp() -> u32 { 0x2000_0000 } +pub(crate) fn msp() -> u32 { + 0x2000_0000 +} /// Currently Ed25519 and P256. #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -169,10 +163,14 @@ impl core::convert::TryFrom for SigningAlgorithm { /// Method to check for user presence. pub trait UserPresence: Copy { - fn user_present(self, trussed: &mut T, timeout_milliseconds: u32) -> Result<()>; + fn user_present( + self, + trussed: &mut T, + timeout_milliseconds: u32, + ) -> Result<()>; } -#[deprecated(note="use `Silent` directly`")] +#[deprecated(note = "use `Silent` directly`")] #[doc(hidden)] pub type SilentAuthenticator = Silent; @@ -181,12 +179,12 @@ pub type SilentAuthenticator = Silent; pub struct Silent {} impl UserPresence for Silent { - fn user_present(self, _: &mut T, _:u32) -> Result<()> { + fn user_present(self, _: &mut T, _: u32) -> Result<()> { Ok(()) } } -#[deprecated(note="use `Conforming` directly")] +#[deprecated(note = "use `Conforming` directly")] #[doc(hidden)] pub type NonSilentAuthenticator = Conforming; @@ -195,7 +193,11 @@ pub type NonSilentAuthenticator = Conforming; pub struct Conforming {} impl UserPresence for Conforming { - fn user_present(self, trussed: &mut T, timeout_milliseconds: u32) -> Result<()> { + fn user_present( + self, + trussed: &mut T, + timeout_milliseconds: u32, + ) -> Result<()> { let result = syscall!(trussed.confirm_user_present(timeout_milliseconds)).result; result.map_err(|err| match err { trussed::types::consent::Error::TimedOut => Error::UserActionTimeout, @@ -205,22 +207,31 @@ impl UserPresence for Conforming { } } -fn cbor_serialize_message(object: &T) -> core::result::Result { +fn cbor_serialize_message( + object: &T, +) -> core::result::Result { trussed::cbor_serialize_bytes(object) } impl Authenticator -where UP: UserPresence, - T: TrussedRequirements, +where + UP: UserPresence, + T: TrussedRequirements, { pub fn new(trussed: T, up: UP, config: Config) -> Self { - let state = state::State::new(); - Self { trussed, state, up, config } + Self { + trussed, + state, + up, + config, + } } pub fn call(&mut self, request: &Request) -> Result { - self.state.persistent.load_if_not_initialised(&mut self.trussed); + self.state + .persistent + .load_if_not_initialised(&mut self.trussed); match request { Request::Ctap2(request) => { @@ -245,5 +256,4 @@ where UP: UserPresence, } #[cfg(test)] -mod test { -} +mod test {} diff --git a/src/state.rs b/src/state.rs index e921937..69d608f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,32 +2,24 @@ //! //! Needs cleanup. -use trussed::{ - client, syscall, try_syscall, - Client as TrussedClient, - types::{ - self, - KeyId, - Location, - Mechanism, - }, -}; use ctap_types::{ - Bytes, String, - Error, cose::EcdhEsHkdf256PublicKey as CoseEcdhEsHkdf256PublicKey, // 2022-02-27: 10 credentials sizes::MAX_CREDENTIAL_COUNT_IN_LIST, // U8 currently + Bytes, + Error, + String, +}; +use trussed::{ + client, syscall, try_syscall, + types::{self, KeyId, Location, Mechanism}, + Client as TrussedClient, }; use heapless::binary_heap::{BinaryHeap, Max}; use littlefs2::path::PathBuf; -use crate::{ - cbor_serialize_message, - credential::Credential, - Result, -}; +use crate::{cbor_serialize_message, credential::Credential, Result}; #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CachedCredential { @@ -81,7 +73,6 @@ pub type CredentialCache = CredentialCacheGeneric; #[derive(Clone, Debug, /*uDebug, Eq, PartialEq,*/ serde::Deserialize, serde::Serialize)] pub struct State { - /// Batch device identity (aaguid, certificate, key). pub identity: Identity, pub persistent: PersistentState, @@ -95,7 +86,6 @@ impl Default for State { } impl State { - // pub fn new(trussed: &mut TrussedClient) -> Self { pub fn new() -> Self { // let identity = Identity::get(trussed); @@ -104,7 +94,11 @@ impl State { // let persistent = PersistentState::load_or_reset(trussed); let persistent = Default::default(); - Self { identity, persistent, runtime } + Self { + identity, + persistent, + runtime, + } } pub fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { @@ -119,9 +113,7 @@ impl State { Ok(()) } - pub fn pin_blocked(&self) -> Result<()> { - if self.persistent.pin_blocked() { return Err(Error::PinBlocked); } @@ -131,7 +123,6 @@ impl State { Ok(()) } - } /// Batch device identity (aaguid, certificate, key). @@ -146,16 +137,13 @@ pub type Aaguid = [u8; 16]; pub type Certificate = trussed::types::Message; impl Identity { - // Attempt to yank out the aaguid of a certificate. fn yank_aaguid(&mut self, der: &[u8]) -> Option<[u8; 16]> { - let aaguid_start_sequence = [ // OBJECT IDENTIFIER 1.3.6.1.4.1.45724.1.1.4 (AAGUID) 0x06u8, 0x0B, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x01, 0x01, 0x04, - // Sequence, 16 bytes - 0x04, 0x12, 0x04, 0x10 + 0x04, 0x12, 0x04, 0x10, ]; // Scan for the beginning sequence for AAGUID. @@ -180,16 +168,16 @@ impl Identity { } /// Lookup batch key and certificate, together with AAUGID. - pub fn attestation(&mut self, trussed: &mut T) -> (Option<(KeyId, Certificate)>, Aaguid) - { + pub fn attestation( + &mut self, + trussed: &mut T, + ) -> (Option<(KeyId, Certificate)>, Aaguid) { let key = crate::constants::ATTESTATION_KEY_ID; let attestation_key_exists = syscall!(trussed.exists(Mechanism::P256, key)).exists; if attestation_key_exists { - // Will panic if certificate does not exist. - let cert = syscall!(trussed.read_certificate( - crate::constants::ATTESTATION_CERT_ID - )).der; + let cert = + syscall!(trussed.read_certificate(crate::constants::ATTESTATION_CERT_ID)).der; let mut aaguid = self.yank_aaguid(cert.as_slice()); @@ -204,7 +192,6 @@ impl Identity { (None, *b"AAGUID0123456789") } } - } #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] @@ -220,7 +207,9 @@ pub struct CredentialManagementEnumerateCredentials { pub prev_filename: PathBuf, } -#[derive(Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize)] +#[derive( + Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize, +)] pub struct ActiveGetAssertionData { pub rp_id_hash: [u8; 32], pub client_data_hash: [u8; 32], @@ -230,7 +219,9 @@ pub struct ActiveGetAssertionData { pub extensions: Option, } -#[derive(Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize)] +#[derive( + Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize, +)] pub struct RuntimeState { key_agreement_key: Option, pin_token: Option, @@ -280,7 +271,6 @@ pub struct PersistentState { } impl PersistentState { - const RESET_RETRIES: u8 = 8; const FILENAME: &'static [u8] = b"persistent-state.cbor"; const MAX_RESIDENT_CREDENTIALS_GUESSTIMATE: u32 = 100; @@ -290,12 +280,10 @@ impl PersistentState { } pub fn load(trussed: &mut T) -> Result { - // TODO: add "exists_file" method instead? - let result = try_syscall!(trussed.read_file( - Location::Internal, - PathBuf::from(Self::FILENAME), - )).map_err(|_| Error::Other); + let result = + try_syscall!(trussed.read_file(Location::Internal, PathBuf::from(Self::FILENAME),)) + .map_err(|_| Error::Other); if result.is_err() { info!("err loading: {:?}", result.err().unwrap()); @@ -342,13 +330,16 @@ impl PersistentState { 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, + ) { if !self.initialised { match Self::load(trussed) { Ok(previous_self) => { info!("loaded previous state!"); *self = previous_self - }, + } Err(_err) => { info!("error with previous state! {:?}", _err); } @@ -364,33 +355,47 @@ impl PersistentState { Ok(now) } - pub fn key_encryption_key(&mut self, trussed: &mut T) -> Result - { + pub fn key_encryption_key( + &mut self, + trussed: &mut T, + ) -> Result { match self.key_encryption_key { Some(key) => Ok(key), None => self.rotate_key_encryption_key(trussed), } } - pub fn rotate_key_encryption_key(&mut self, trussed: &mut T) -> Result { - if let Some(key) = self.key_encryption_key { syscall!(trussed.delete(key)); } + pub fn rotate_key_encryption_key( + &mut self, + trussed: &mut T, + ) -> 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); self.save(trussed)?; Ok(key) } - pub fn key_wrapping_key(&mut self, trussed: &mut T) -> Result - { + pub fn key_wrapping_key( + &mut self, + trussed: &mut T, + ) -> Result { match self.key_wrapping_key { Some(key) => Ok(key), None => self.rotate_key_wrapping_key(trussed), } } - pub fn rotate_key_wrapping_key(&mut self, trussed: &mut T) -> Result { + pub fn rotate_key_wrapping_key( + &mut self, + trussed: &mut T, + ) -> Result { self.load_if_not_initialised(trussed); - if let Some(key) = self.key_wrapping_key { syscall!(trussed.delete(key)); } + 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); self.save(trussed)?; @@ -409,7 +414,7 @@ impl PersistentState { self.consecutive_pin_mismatches >= Self::RESET_RETRIES } - fn reset_retries(&mut self, trussed: &mut T) -> Result<()> { + fn reset_retries(&mut self, trussed: &mut T) -> Result<()> { if self.consecutive_pin_mismatches > 0 { self.consecutive_pin_mismatches = 0; self.save(trussed)?; @@ -433,17 +438,18 @@ impl PersistentState { self.pin_hash } - pub fn set_pin_hash(&mut self, trussed: &mut T, pin_hash: [u8; 16]) -> Result<()> { + pub fn set_pin_hash( + &mut self, + trussed: &mut T, + pin_hash: [u8; 16], + ) -> Result<()> { self.pin_hash = Some(pin_hash); self.save(trussed)?; Ok(()) } - - } impl RuntimeState { - const POWERCYCLE_RETRIES: u8 = 3; fn decrement_retries(&mut self) -> Result<()> { @@ -461,7 +467,6 @@ impl RuntimeState { self.consecutive_pin_mismatches = 0; } - pub fn pin_blocked(&self) -> bool { self.consecutive_pin_mismatches >= Self::POWERCYCLE_RETRIES } @@ -482,13 +487,17 @@ impl RuntimeState { self.cached_credentials.push(credential); } - pub fn pop_credential(&mut self, trussed: &mut T) -> Option { + pub fn pop_credential( + &mut self, + trussed: &mut T, + ) -> Option { let cached_credential = self.cached_credentials.pop()?; let credential_data = syscall!(trussed.read_file( Location::Internal, PathBuf::from(cached_credential.path.as_str()), - )).data; + )) + .data; Credential::deserialize(&credential_data).ok() } @@ -528,13 +537,18 @@ impl RuntimeState { pub fn rotate_pin_token(&mut self, trussed: &mut T) -> KeyId { // TODO: need to rotate key agreement key? - if let Some(token) = self.pin_token { syscall!(trussed.delete(token)); } + if let Some(token) = self.pin_token { + syscall!(trussed.delete(token)); + } let token = syscall!(trussed.generate_secret_key(16, Location::Volatile)).key; self.pin_token = Some(token); token } - pub fn reset(&mut self, trussed: &mut T) { + pub fn reset( + &mut self, + trussed: &mut T, + ) { // Could use `free_credential_heap`, but since we're deleting everything here, this is quicker. syscall!(trussed.delete_all(Location::Volatile)); self.clear_credential_cache(); @@ -542,22 +556,32 @@ impl RuntimeState { self.rotate_pin_token(trussed); self.rotate_key_agreement_key(trussed); - } - pub fn generate_shared_secret(&mut self, trussed: &mut T, platform_key_agreement_key: &CoseEcdhEsHkdf256PublicKey) -> Result { + pub fn generate_shared_secret( + &mut self, + trussed: &mut T, + platform_key_agreement_key: &CoseEcdhEsHkdf256PublicKey, + ) -> Result { let private_key = self.key_agreement_key(trussed); - let serialized_pkak = cbor_serialize_message(platform_key_agreement_key).map_err(|_| Error::InvalidParameter)?; + let serialized_pkak = cbor_serialize_message(platform_key_agreement_key) + .map_err(|_| Error::InvalidParameter)?; let platform_kak = try_syscall!(trussed.deserialize_p256_key( - &serialized_pkak, types::KeySerialization::EcdhEsHkdf256, + &serialized_pkak, + types::KeySerialization::EcdhEsHkdf256, types::StorageAttributes::new().set_persistence(types::Location::Volatile) - )).map_err(|_| Error::InvalidParameter)?.key; + )) + .map_err(|_| Error::InvalidParameter)? + .key; let pre_shared_secret = syscall!(trussed.agree( - types::Mechanism::P256, private_key, platform_kak, + types::Mechanism::P256, + private_key, + platform_kak, types::StorageAttributes::new().set_persistence(types::Location::Volatile), - )).shared_secret; + )) + .shared_secret; syscall!(trussed.delete(platform_kak)); if let Some(previous_shared_secret) = self.shared_secret { @@ -565,13 +589,16 @@ impl RuntimeState { } let shared_secret = syscall!(trussed.derive_key( - types::Mechanism::Sha256, pre_shared_secret, None, types::StorageAttributes::new().set_persistence(types::Location::Volatile) - )).key; + types::Mechanism::Sha256, + pre_shared_secret, + None, + types::StorageAttributes::new().set_persistence(types::Location::Volatile) + )) + .key; self.shared_secret = Some(shared_secret); syscall!(trussed.delete(pre_shared_secret)); Ok(shared_secret) } - } From d3963644ec6702d26bfbc6577dd07edc443739aa Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 17 Mar 2022 02:43:01 +0100 Subject: [PATCH 12/13] Remove some dead code --- src/lib.rs | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8cd6ab3..423fe85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,10 +17,7 @@ generate_macros!(); use trussed::{client, syscall, types::Message, Client as TrussedClient}; -use ctap_types::{ - authenticator::{Request, Response}, - heapless_bytes::Bytes, -}; +use ctap_types::heapless_bytes::Bytes; /// Re-export of `ctap-types` authenticator errors. pub use ctap_types::Error; @@ -228,27 +225,6 @@ where } } - pub fn call(&mut self, request: &Request) -> Result { - self.state - .persistent - .load_if_not_initialised(&mut self.trussed); - - match request { - Request::Ctap2(request) => { - use ctap_types::ctap2::Authenticator as _; - Ok(Response::Ctap2(self.call_ctap2(request)?)) - } - Request::Ctap1(_request) => { - // ctap_types::authenticator::ctap1::Request redefines - // the already existing ctap_types::ctap1::Command - // - // need to merge - todo!(); - // Ok(Response::Ctap1(self.call_ctap1(request)?)) - } - } - } - fn hash(&mut self, data: &[u8]) -> Bytes<32> { let hash = syscall!(self.trussed.hash_sha256(data)).hash; hash.to_bytes().expect("hash should fit") From e399a110b1a79a686ccfd7ed7866c902bc7b9b9d Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 17 Mar 2022 03:42:44 +0100 Subject: [PATCH 13/13] Use published trussed --- Cargo.toml | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7377347..83ece9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/solokeys/fido-authenticator" documentation = "https://docs.rs/fido-authenticator" +description = "FIDO authenticator Trussed app" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -18,8 +19,7 @@ littlefs2 = "0.3.1" serde = { version = "1.0", default-features = false } serde_cbor = { version = "0.11.0", default-features = false } serde-indexed = "0.1.0" - -trussed = { git = "https://github.com/trussed-dev/trussed" } +trussed = "0.1" apdu-dispatch = { version = "0.1", optional = true } ctaphid-dispatch = { version = "0.1", optional = true } diff --git a/README.md b/README.md index a5264f8..b907df8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # fido-authenticator -Open source [FIDO][fido] authenticator implementation. +[FIDO][fido] authenticator [Trussed][trussed]® app. Built with [Trussed][trussed].