From 437ff5f816e381a12554b799f4943c1234990b83 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Wed, 15 Dec 2021 18:42:00 +0100 Subject: [PATCH 001/135] 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 c9c70c34687581370de22073ada82d4cd1418810 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Sat, 8 Jan 2022 14:03:12 +0100 Subject: [PATCH 002/135] 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 3a3329443562244e97040eefff99bd7566412d2c Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 5 Jul 2022 17:26:01 +0200 Subject: [PATCH 003/135] Skip user presence check directly after boot This patch adds a configuration option to skip the additional user presence check for the first Get Assertion or Authenticate request within a certain duration after boot. In this case, the device insertion is interpreted as a user presence indicator. --- src/ctap1.rs | 8 +++++--- src/ctap2.rs | 8 +++++--- src/lib.rs | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/ctap1.rs b/src/ctap1.rs index 5be6414..e56e0b0 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -186,9 +186,11 @@ impl Authenticator for crate::Authenti }; } ControlByte::EnforceUserPresenceAndSign => { - self.up - .user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) - .map_err(|_| Error::ConditionsOfUseNotSatisfied)?; + if !self.skip_up_check() { + self.up + .user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT) + .map_err(|_| Error::ConditionsOfUseNotSatisfied)?; + } 0x01 } ControlByte::DontEnforceUserPresenceAndSign => 0x00, diff --git a/src/ctap2.rs b/src/ctap2.rs index 71d0a93..9be3333 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -932,9 +932,11 @@ 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)?; + if !self.skip_up_check() { + info_now!("asking for up"); + self.up + .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; + } true } else { info_now!("not asking for up"); diff --git a/src/lib.rs b/src/lib.rs index 423fe85..e1f776b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,8 @@ extern crate delog; generate_macros!(); +use core::time::Duration; + use trussed::{client, syscall, types::Message, Client as TrussedClient}; use ctap_types::heapless_bytes::Bytes; @@ -71,6 +73,9 @@ pub struct Config { pub max_msg_size: usize, // pub max_creds_in_list: usize, // pub max_cred_id_length: usize, + /// If set, the first Get Assertion or Authenticate request within the specified time after + /// boot is accepted without additional user presence verification. + pub skip_up_timeout: Option, } // impl Default for Config { @@ -229,6 +234,19 @@ where let hash = syscall!(self.trussed.hash_sha256(data)).hash; hash.to_bytes().expect("hash should fit") } + + fn skip_up_check(&mut self) -> bool { + // If enabled in the configuration, we don't require an additional user presence + // verification for a certain duration after boot. + if let Some(timeout) = self.config.skip_up_timeout.take() { + let uptime = syscall!(self.trussed.uptime()).uptime; + if uptime < timeout { + info_now!("skip up check directly after boot"); + return true; + } + } + false + } } #[cfg(test)] From 184e667950a8555b7e043ac88f167ecd5c815970 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 1 Aug 2022 12:45:57 +0200 Subject: [PATCH 004/135] Return error if credential ID is too long Instead of panicking, we now return a RequestTooLarge error if the encrypted and serialized credential ID is longer than 255 bytes. Fixes: https://github.com/solokeys/fido-authenticator/issues/15 --- src/credential.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/credential.rs b/src/credential.rs index c0d677a..d71fd50 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -252,7 +252,8 @@ impl Credential { 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 credential_id: CredentialId = encrypted_serialized_credential.try_into().unwrap(); + let credential_id: CredentialId = encrypted_serialized_credential.try_into() + .map_err(|_| Error::RequestTooLarge)?; Ok(credential_id) } From a040871260a2f68d0c7645d9a452bfb64a81084e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 16 Mar 2023 15:17:38 +0100 Subject: [PATCH 005/135] Run cargo fmt --- src/credential.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/credential.rs b/src/credential.rs index d71fd50..f00f98f 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -252,7 +252,8 @@ impl Credential { 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 credential_id: CredentialId = encrypted_serialized_credential.try_into() + let credential_id: CredentialId = encrypted_serialized_credential + .try_into() .map_err(|_| Error::RequestTooLarge)?; Ok(credential_id) From f0fd6450e23fc11d5bfc83b44101f2b12d47cd0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 16 Mar 2023 15:18:24 +0100 Subject: [PATCH 006/135] Fix clippy warnings --- src/credential.rs | 12 ++++++------ src/ctap2.rs | 21 +++++++-------------- src/dispatch/apdu.rs | 5 +++-- src/dispatch/ctaphid.rs | 7 ++++--- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index f00f98f..ee45d6e 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -344,7 +344,8 @@ mod test { fn credential_data() -> CredentialData { use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; - let credential_data = CredentialData { + + CredentialData { rp: PublicKeyCredentialRpEntity { id: String::from("John Doe"), name: None, @@ -362,8 +363,7 @@ mod test { key: Key::WrappedKey(Bytes::from_slice(&[1, 2, 3]).unwrap()), hmac_secret: Some(false), cred_protect: None, - }; - credential_data + } } fn random_bytes() -> Bytes { @@ -424,7 +424,8 @@ mod test { fn random_credential_data() -> CredentialData { use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; - let credential_data = CredentialData { + + CredentialData { rp: PublicKeyCredentialRpEntity { id: random_string(), name: maybe_random_string(), @@ -442,8 +443,7 @@ mod test { key: Key::WrappedKey(random_bytes()), hmac_secret: Some(false), cred_protect: None, - }; - credential_data + } } #[test] diff --git a/src/ctap2.rs b/src/ctap2.rs index 9be3333..9fd18e1 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -963,7 +963,7 @@ impl Authenticator for crate::Authenti let num_credentials = match num_credentials { 1 => None, - n => Some(n as u32), + n => Some(n), }; self.assert_with_credential(num_credentials, credential) @@ -1543,19 +1543,12 @@ impl crate::Authenticator { }; 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_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(), - }; + let signature = syscall!(self + .trussed + .sign(mechanism, key, &commitment, serialization)) + .signature + .to_bytes() + .unwrap(); if !is_rk { syscall!(self.trussed.delete(key)); diff --git a/src/dispatch/apdu.rs b/src/dispatch/apdu.rs index 7d4e834..11b4be6 100644 --- a/src/dispatch/apdu.rs +++ b/src/dispatch/apdu.rs @@ -56,7 +56,7 @@ where // "3. Client sends a command for an operation (register / authenticate)" // - Ok(match instruction { + 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), @@ -73,6 +73,7 @@ where } } } - }) + }; + Ok(()) } } diff --git a/src/dispatch/ctaphid.rs b/src/dispatch/ctaphid.rs index fac824c..d27558e 100644 --- a/src/dispatch/ctaphid.rs +++ b/src/dispatch/ctaphid.rs @@ -25,19 +25,20 @@ where msp() - 0x2000_0000 ); - if request.len() < 1 { + if request.is_empty() { debug_now!("invalid request length in ctaphid.call"); return Err(ctaphid::Error::InvalidLength); } // info_now!("request: "); // blocking::dump_hex(request, request.len()); - Ok(match command { + 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); } - }) + }; + Ok(()) } } From 61763a92327fadf51bfcadf4886c1d1a21748d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 16 Mar 2023 15:07:47 +0100 Subject: [PATCH 007/135] Remove associated data in wrapping of keys Trussed itself already ignored this associated data (https://github.com/trussed-dev/trussed/pull/108), and the unwrapping was already performed with no associated data. Not removing it would lead to breakage once (https://github.com/trussed-dev/trussed/pull/108) is merged. Adding the AD to the unwrapping step would break compatibility with currently registerd credentials. Security: This is not an issue because the credentials stored locally are encrypted with the proper app id as associated data which is checked when the credential is decrypted. --- src/credential.rs | 2 -- src/ctap1.rs | 4 ++-- src/ctap2.rs | 14 ++++++-------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index ee45d6e..dac78c6 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -344,7 +344,6 @@ mod test { fn credential_data() -> CredentialData { use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; - CredentialData { rp: PublicKeyCredentialRpEntity { id: String::from("John Doe"), @@ -424,7 +423,6 @@ mod test { fn random_credential_data() -> CredentialData { use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; - CredentialData { rp: PublicKeyCredentialRpEntity { id: random_string(), diff --git a/src/ctap1.rs b/src/ctap1.rs index e56e0b0..e519b82 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -63,7 +63,7 @@ impl Authenticator for crate::Authenti let wrapped_key = syscall!(self .trussed - .wrap_key_chacha8poly1305(wrapping_key, private_key, ®.app_id,)) + .wrap_key_chacha8poly1305(wrapping_key, private_key, &[])) .wrapped_key; // debug!("wrapped_key = {:?}", &wrapped_key); @@ -208,7 +208,7 @@ impl Authenticator for crate::Authenti let key_result = syscall!(self.trussed.unwrap_key_chacha8poly1305( wrapping_key, bytes, - b"", + &[], Location::Volatile, )) .key; diff --git a/src/ctap2.rs b/src/ctap2.rs index 9fd18e1..ca857a1 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -320,12 +320,11 @@ impl Authenticator for crate::Authenti false => { // WrappedKey version let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( - wrapping_key, - private_key, - &rp_id_hash, - )) - .wrapped_key; + let wrapped_key = + syscall!(self + .trussed + .wrap_key_chacha8poly1305(wrapping_key, private_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...) @@ -1465,8 +1464,7 @@ impl crate::Authenticator { let key_result = syscall!(self.trussed.unwrap_key_chacha8poly1305( wrapping_key, &bytes, - b"", - // &rp_id_hash, + &[], Location::Volatile, )) .key; From 1d0b4b563b73f7c80d525256170393dbcbfda3fe Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 8 Feb 2023 13:20:11 +0100 Subject: [PATCH 008/135] Bump trussed This patch bumps the Trussed dependency to include the update to littlefs2 0.4.0. --- Cargo.toml | 4 +++- src/ctap2.rs | 4 +--- src/ctap2/credential_management.rs | 4 +--- src/state.rs | 3 +-- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a9124f0..9cde74c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ ctap-types = "0.1.0" delog = "0.1.0" heapless = "0.7" interchange = "0.2.0" -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" @@ -44,3 +43,6 @@ rand = "0.8.4" [package.metadata.docs.rs] features = ["dispatch"] + +[patch.crates-io] +trussed = { git = "https://github.com/trussed-dev/trussed", rev = "55ea391367fce4bf5093ff2d3c79041d7aef0485" } diff --git a/src/ctap2.rs b/src/ctap2.rs index ca857a1..789ca04 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -7,12 +7,10 @@ use ctap_types::{ sizes, Error, }; -use littlefs2::path::Path; - use trussed::{ syscall, try_syscall, types::{ - KeyId, KeySerialization, Location, Mechanism, MediumData, Message, PathBuf, + KeyId, KeySerialization, Location, Mechanism, MediumData, Message, Path, PathBuf, SignatureSerialization, }, }; diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index d8bc26f..ed6f75e 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -4,7 +4,7 @@ use core::convert::TryFrom; use trussed::{ syscall, - types::{DirEntry, Location}, + types::{DirEntry, Location, Path, PathBuf}, }; use ctap_types::{ @@ -15,8 +15,6 @@ use ctap_types::{ Error, }; -use littlefs2::path::{Path, PathBuf}; - use crate::{ credential::Credential, state::{CredentialManagementEnumerateCredentials, CredentialManagementEnumerateRps}, diff --git a/src/state.rs b/src/state.rs index 69d608f..20eaeef 100644 --- a/src/state.rs +++ b/src/state.rs @@ -12,12 +12,11 @@ use ctap_types::{ }; use trussed::{ client, syscall, try_syscall, - types::{self, KeyId, Location, Mechanism}, + types::{self, KeyId, Location, Mechanism, PathBuf}, Client as TrussedClient, }; use heapless::binary_heap::{BinaryHeap, Max}; -use littlefs2::path::PathBuf; use crate::{cbor_serialize_message, credential::Credential, Result}; From 9282539f2216e3ccdd07a1207d61f31846e1ae15 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 29 Mar 2023 10:17:42 +0200 Subject: [PATCH 009/135] Make maximum resident credential count configurable Previously, we estimated that we can handle 100 resident keys when returning the number of remaining resident keys in the credential management command. This patch introduces a config option to set a maximum count of resident keys that is used to report the number of remaining resident keys and that is enforced when trying to create a new resident key. --- CHANGELOG.md | 5 ++++- src/constants.rs | 2 ++ src/ctap2.rs | 10 ++++++++++ src/ctap2/credential_management.rs | 14 +++++++------- src/lib.rs | 2 ++ src/state.rs | 5 ----- 6 files changed, 25 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e826f7e..7ece47c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,15 @@ 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 +- Add config option for setting a maximum number of resident credentials. + ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp - Add config option to skip UP when device was just booted, as insertion is a kind of UP check @robin-nitrokey -## [Unreleased] +## [0.1.0] - 2022-03-17 - use 2021 edition - use @szszszsz's credential ID shortening diff --git a/src/constants.rs b/src/constants.rs index 43e88da..dbab710 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -7,3 +7,5 @@ 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); + +pub const MAX_RESIDENT_CREDENTIALS_GUESSTIMATE: u32 = 100; diff --git a/src/ctap2.rs b/src/ctap2.rs index 789ca04..ea1ad40 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -372,6 +372,16 @@ impl Authenticator for crate::Authenti self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id) .ok(); + // then check the maximum number of RK credentials + if let Some(max_count) = self.config.max_resident_credential_count { + let mut cm = credential_management::CredentialManagement::new(self); + let metadata = cm.get_creds_metadata()?; + let count = metadata.existing_resident_credentials_count.unwrap_or(max_count); + if count >= max_count { + return Err(Error::KeyStoreFull); + } + } + // then store key, making it resident let credential_id_hash = self.hash(credential_id.0.as_ref()); try_syscall!(self.trussed.write_file( diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index ed6f75e..39eaaf8 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -16,6 +16,7 @@ use ctap_types::{ }; use crate::{ + constants::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE, credential::Credential, state::{CredentialManagementEnumerateCredentials, CredentialManagementEnumerateRps}, Authenticator, Result, TrussedRequirements, UserPresence, @@ -65,9 +66,12 @@ where info!("get metadata"); let mut response: Response = Default::default(); - let guesstimate = self.state.persistent.max_resident_credentials_guesstimate(); + let max_resident_credentials = self + .config + .max_resident_credential_count + .unwrap_or(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(max_resident_credentials); let dir = PathBuf::from(b"rk"); let maybe_first_rp = @@ -96,11 +100,7 @@ where None => { response.existing_resident_credentials_count = Some(num_rks); response.max_possible_remaining_residential_credentials_count = - Some(if num_rks >= guesstimate { - 0 - } else { - guesstimate - num_rks - }); + Some(max_resident_credentials.saturating_sub(num_rks)); return Ok(response); } Some(rp) => { diff --git a/src/lib.rs b/src/lib.rs index e1f776b..5cd444a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,6 +76,8 @@ pub struct Config { /// If set, the first Get Assertion or Authenticate request within the specified time after /// boot is accepted without additional user presence verification. pub skip_up_timeout: Option, + /// The maximum number of resident credentials. + pub max_resident_credential_count: Option, } // impl Default for Config { diff --git a/src/state.rs b/src/state.rs index 20eaeef..c7a0f7a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -272,11 +272,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; - - pub fn max_resident_credentials_guesstimate(&self) -> u32 { - Self::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE - } pub fn load(trussed: &mut T) -> Result { // TODO: add "exists_file" method instead? From db14bcf54a5780f595317cd20f7d19b0563719d7 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 28 Apr 2023 19:33:47 +0200 Subject: [PATCH 010/135] Allow empty RP dirs in get_creds_metadata If we overwrite an existing resident credential, the corresponding RP directory can be empty when we call CredentialManagement::get_creds_metadata to count the existing credentials. This causes an error in the current implementation. This patch changes CredentialManagement::get_creds_metadata to gracefully handle empty RP directories. Fixes https://github.com/Nitrokey/nitrokey-3-firmware/issues/254 --- src/ctap2.rs | 10 +++++++--- src/ctap2/credential_management.rs | 25 +++++++++++++++---------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index ea1ad40..0f279a5 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -375,9 +375,13 @@ impl Authenticator for crate::Authenti // then check the maximum number of RK credentials if let Some(max_count) = self.config.max_resident_credential_count { let mut cm = credential_management::CredentialManagement::new(self); - let metadata = cm.get_creds_metadata()?; - let count = metadata.existing_resident_credentials_count.unwrap_or(max_count); + let metadata = cm.get_creds_metadata(); + let count = metadata + .existing_resident_credentials_count + .unwrap_or(max_count); + debug!("resident cred count: {} (max: {})", count, max_count); if count >= max_count { + error!("maximum resident credential count reached"); return Err(Error::KeyStoreFull); } } @@ -844,7 +848,7 @@ impl Authenticator for crate::Authenti let sub_parameters = ¶meters.sub_command_params; match parameters.sub_command { // 0x1 - Subcommand::GetCredsMetadata => cred_mgmt.get_creds_metadata(), + Subcommand::GetCredsMetadata => Ok(cred_mgmt.get_creds_metadata()), // 0x2 Subcommand::EnumerateRpsBegin => cred_mgmt.first_relying_party(), diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 39eaaf8..c88dc4d 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -62,7 +62,7 @@ where UP: UserPresence, T: TrussedRequirements, { - pub fn get_creds_metadata(&mut self) -> Result { + pub fn get_creds_metadata(&mut self) -> Response { info!("get metadata"); let mut response: Response = Default::default(); @@ -71,7 +71,8 @@ where .max_resident_credential_count .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE); response.existing_resident_credentials_count = Some(0); - response.max_possible_remaining_residential_credentials_count = Some(max_resident_credentials); + response.max_possible_remaining_residential_credentials_count = + Some(max_resident_credentials); let dir = PathBuf::from(b"rk"); let maybe_first_rp = @@ -81,11 +82,11 @@ where .entry; let first_rp = match maybe_first_rp { - None => return Ok(response), + None => return response, Some(rp) => rp, }; - let (mut num_rks, _) = self.count_rp_rks(PathBuf::from(first_rp.path()))?; + let (mut num_rks, _) = self.count_rp_rks(PathBuf::from(first_rp.path())); let mut last_rp = PathBuf::from(first_rp.file_name()); loop { @@ -101,12 +102,12 @@ where response.existing_resident_credentials_count = Some(num_rks); response.max_possible_remaining_residential_credentials_count = Some(max_resident_credentials.saturating_sub(num_rks)); - return Ok(response); + return response; } 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; } @@ -265,21 +266,24 @@ where Ok(response) } - fn count_rp_rks(&mut self, rp_dir: PathBuf) -> Result<(u32, DirEntry)> { + fn count_rp_rks(&mut self, rp_dir: PathBuf) -> (u32, Option) { 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)?; + let Some(first_rk) = maybe_first_rk else { + warn!("empty RP directory"); + return (0, None); + }; // count the rest of them let mut num_rks = 1; while syscall!(self.trussed.read_dir_next()).entry.is_some() { num_rks += 1; } - Ok((num_rks, first_rk)) + (num_rks, Some(first_rk)) } pub fn first_credential(&mut self, rp_id_hash: &Bytes<32>) -> Result { @@ -291,7 +295,8 @@ where super::format_hex(&rp_id_hash[..8], &mut hex); let rp_dir = PathBuf::from(b"rk").join(&PathBuf::from(&hex)); - let (num_rks, first_rk) = self.count_rp_rks(rp_dir)?; + let (num_rks, first_rk) = self.count_rp_rks(rp_dir); + let first_rk = first_rk.ok_or(Error::NoCredentials)?; // extract data required into response let mut response = self.extract_response_from_credential_file(first_rk.path())?; From 708d5f7f526556b766ec83e5bff6afe363da72dd Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 28 Apr 2023 19:48:11 +0200 Subject: [PATCH 011/135] Clean up empty RP dirs if key store is full If we try to register a new resident credential, we delete existing credentials with the same RP and user ID. If we then cannot store the new credential because the credential limit is reached or there is a filesystem write error, we may be left with an empty RP dir. This patch adds a check to delete empty RP dirs in this case. --- src/ctap2.rs | 56 +++++++++++++++++++++++------- src/ctap2/credential_management.rs | 17 +-------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 0f279a5..73129ce 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -372,6 +372,8 @@ impl Authenticator for crate::Authenti self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id) .ok(); + let mut key_store_full = false; + // then check the maximum number of RK credentials if let Some(max_count) = self.config.max_resident_credential_count { let mut cm = credential_management::CredentialManagement::new(self); @@ -382,21 +384,32 @@ impl Authenticator for crate::Authenti debug!("resident cred count: {} (max: {})", count, max_count); if count >= max_count { error!("maximum resident credential count reached"); - return Err(Error::KeyStoreFull); + key_store_full = true; } } - // 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, - rk_path(&rp_id_hash, &credential_id_hash), - serialized_credential, - // user attribute for later easy lookup - // Some(rp_id_hash.clone()), - None, - )) - .map_err(|_| Error::KeyStoreFull)?; + if !key_store_full { + // then store key, making it resident + let credential_id_hash = self.hash(credential_id.0.as_ref()); + let result = try_syscall!(self.trussed.write_file( + Location::Internal, + rk_path(&rp_id_hash, &credential_id_hash), + serialized_credential, + // user attribute for later easy lookup + // Some(rp_id_hash.clone()), + None, + )); + key_store_full = result.is_err(); + } + + if key_store_full { + // If we previously deleted an existing cred with the same RP + UserId but then + // failed to store the new cred, the RP directory could now be empty. This is not + // a valid state so we have to delete it. + let rp_dir = rp_rk_dir(&rp_id_hash); + self.delete_rp_dir_if_empty(rp_dir); + return Err(Error::KeyStoreFull); + } } // 13. generate and return attestation statement using clientDataHash @@ -1673,6 +1686,25 @@ impl crate::Authenticator { Ok(()) } + + pub(crate) fn delete_rp_dir_if_empty(&mut self, rp_path: PathBuf) { + 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,)); + } else { + info!( + "not deleting deleting parent {:?} as there is {:?}", + &rp_path, + &maybe_first_remaining_rk.unwrap().path(), + ); + } + } } fn rp_rk_dir(rp_id_hash: &Bytes<32>) -> PathBuf { diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index c88dc4d..4ae63c9 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -484,23 +484,8 @@ where .parent() // by construction, RK has a parent, its RP .unwrap(); + self.delete_rp_dir_if_empty(rp_path); - 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,)); - } else { - 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) From 76d3d88b374658d96bd3fe8bb02775f954db11c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Tue, 23 May 2023 09:35:27 +0200 Subject: [PATCH 012/135] Adapt to interrupt mechanism --- Cargo.toml | 4 +++- src/dispatch/ctaphid.rs | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9cde74c..3d4f8f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,4 +45,6 @@ rand = "0.8.4" features = ["dispatch"] [patch.crates-io] -trussed = { git = "https://github.com/trussed-dev/trussed", rev = "55ea391367fce4bf5093ff2d3c79041d7aef0485" } +ref-swap = { git = "https://github.com/nitrokey/ref-swap.git", rev = "de0330e85b479074ae03bcc05888cfeff682f61e" } +ctaphid-dispatch = { git = "https://github.com/sosthene-nitrokey/ctaphid-dispatch.git", rev = "a5a3696d7cf0665414cf57cdea384dbc8a157f33" } +trussed = { git = "https://github.com/sosthene-nitrokey/trussed.git", rev = "6f095e14b27bd58ab8fa56cf6c616266d92fbfb4" } diff --git a/src/dispatch/ctaphid.rs b/src/dispatch/ctaphid.rs index 60f0c8e..adb931f 100644 --- a/src/dispatch/ctaphid.rs +++ b/src/dispatch/ctaphid.rs @@ -3,8 +3,9 @@ use ctaphid_dispatch::app as ctaphid; #[allow(unused_imports)] use crate::msp; use crate::{Authenticator, TrussedRequirements, UserPresence}; +use trussed::interrupt::InterruptFlag; -impl ctaphid::App for Authenticator +impl ctaphid::App<'static> for Authenticator where UP: UserPresence, T: TrussedRequirements, @@ -41,4 +42,8 @@ where }; Ok(()) } + + fn interrupt(&self) -> Option<&'static InterruptFlag> { + self.trussed.interrupt() + } } From 0d2ddcf1f6e2c1b66f2d1bf1d2a607a9db6af6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 31 May 2023 10:58:28 +0200 Subject: [PATCH 013/135] Use published ref-swap --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3d4f8f9..8703a4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,5 @@ rand = "0.8.4" features = ["dispatch"] [patch.crates-io] -ref-swap = { git = "https://github.com/nitrokey/ref-swap.git", rev = "de0330e85b479074ae03bcc05888cfeff682f61e" } ctaphid-dispatch = { git = "https://github.com/sosthene-nitrokey/ctaphid-dispatch.git", rev = "a5a3696d7cf0665414cf57cdea384dbc8a157f33" } trussed = { git = "https://github.com/sosthene-nitrokey/trussed.git", rev = "6f095e14b27bd58ab8fa56cf6c616266d92fbfb4" } From 887c75104512c3bfc2b51733f83e86ad14ee198f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Tue, 6 Jun 2023 17:41:13 +0200 Subject: [PATCH 014/135] Bump deps --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8703a4b..53feb52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,5 +45,5 @@ rand = "0.8.4" features = ["dispatch"] [patch.crates-io] -ctaphid-dispatch = { git = "https://github.com/sosthene-nitrokey/ctaphid-dispatch.git", rev = "a5a3696d7cf0665414cf57cdea384dbc8a157f33" } +ctaphid-dispatch = { git = "https://github.com/sosthene-nitrokey/ctaphid-dispatch.git", rev = "cb3c35f622b578d8d364e85c5e29f207201a5060" } trussed = { git = "https://github.com/sosthene-nitrokey/trussed.git", rev = "6f095e14b27bd58ab8fa56cf6c616266d92fbfb4" } From 26c34a8894e9c5b00d0d53aa0d0918b44e7446e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Tue, 20 Jun 2023 17:48:55 +0200 Subject: [PATCH 015/135] Use merged PRs --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 53feb52..9d9eaec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,5 +45,5 @@ rand = "0.8.4" features = ["dispatch"] [patch.crates-io] -ctaphid-dispatch = { git = "https://github.com/sosthene-nitrokey/ctaphid-dispatch.git", rev = "cb3c35f622b578d8d364e85c5e29f207201a5060" } -trussed = { git = "https://github.com/sosthene-nitrokey/trussed.git", rev = "6f095e14b27bd58ab8fa56cf6c616266d92fbfb4" } +ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "51e68500d7601d04f884f5e95567d14b9018a6cb" } From 8e93bc99586038d8ab8ec38ad27c5ea668c0fad5 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 28 Jun 2023 12:02:17 +0200 Subject: [PATCH 016/135] Set makeCredUvNotRqd in CTAP options This patch sets the makeCredUvNotRqd CTAP option to true to indicate that we support makeCredential operations without user verification. See also: https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#getinfo-makecreduvnotrqd Fixes: https://github.com/solokeys/fido-authenticator/issues/26 --- src/ctap2.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ctap2.rs b/src/ctap2.rs index 73129ce..6cef808 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -79,6 +79,7 @@ impl Authenticator for crate::Authenti false => Some(false), }, credential_mgmt_preview: Some(true), + make_cred_uv_not_rqd: Some(true), ..Default::default() }; // options.rk = true; From 695bf3e83a2ad2f2a742f869c133af0ee367a9c3 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 6 Jul 2023 22:21:25 +0200 Subject: [PATCH 017/135] Reject RK option in get_assertion The getAssertion command does not use the rk option so we return an InvalidOption error if it is set. Fixes: https://github.com/Nitrokey/fido-authenticator/issues/23 --- src/ctap2.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ctap2.rs b/src/ctap2.rs index 6cef808..88c4a82 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -948,6 +948,11 @@ impl Authenticator for crate::Authenti // 6. process any options present + // RK is not supported in get_assertion + if parameters.options.as_ref().and_then(|options| options.rk).is_some() { + return Err(Error::InvalidOption); + } + // 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) From 08c21a222be156fea076d1283890e29d4ed7c56e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 29 Jun 2023 22:23:16 +0200 Subject: [PATCH 018/135] Allow three instead of two PIN retries per boot Previously, a PinAuthBlocked error was already returned after two wrong PIN entries. The reason for this as that decrement_retries also checks if the allowed retries are exceeded. This as unnecessary because pin_blocked is always checked before decrement_retries is called. This patch removes the check in decrement_retries. Fixes: https://github.com/Nitrokey/fido-authenticator/issues/27 --- src/state.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/state.rs b/src/state.rs index c7a0f7a..8e34c29 100644 --- a/src/state.rs +++ b/src/state.rs @@ -102,7 +102,7 @@ impl State { pub fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { self.persistent.decrement_retries(trussed)?; - self.runtime.decrement_retries()?; + self.runtime.decrement_retries(); Ok(()) } @@ -446,15 +446,10 @@ impl PersistentState { impl RuntimeState { const POWERCYCLE_RETRIES: u8 = 3; - fn decrement_retries(&mut self) -> Result<()> { + fn decrement_retries(&mut self) { if self.consecutive_pin_mismatches < Self::POWERCYCLE_RETRIES { self.consecutive_pin_mismatches += 1; } - if self.consecutive_pin_mismatches == Self::POWERCYCLE_RETRIES { - Err(Error::PinAuthBlocked) - } else { - Ok(()) - } } fn reset_retries(&mut self) { From 2ef8446d56ffa75ae421719d0793277c255eb323 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 6 Jul 2023 00:00:25 +0200 Subject: [PATCH 019/135] Ignore key parameters with unsupported type As required by the Webauthn spec, we now ignore public key credential parameters with a type other than "public-key". Fixes: https://github.com/Nitrokey/fido-authenticator/issues/20 --- CHANGELOG.md | 4 ++++ src/ctap2.rs | 16 +++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ece47c..dc5f597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - Add config option for setting a maximum number of resident credentials. +- Ignore public key credential paramters with an unknown type, as required by + the Webauthn spec ([#28][]) + +[#28]: https://github.com/solokeys/fido-authenticator/issues/28 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/src/ctap2.rs b/src/ctap2.rs index 88c4a82..b4862f3 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -197,6 +197,11 @@ impl Authenticator for crate::Authenti let mut algorithm: Option = None; for param in parameters.pub_key_cred_params.iter() { + // Ignore unknown key types + if param.key_type != "public-key" { + continue; + } + match param.alg { -7 => { if algorithm.is_none() { @@ -210,15 +215,8 @@ impl Authenticator for crate::Authenti _ => {} } } - let algorithm = match algorithm { - Some(algorithm) => { - info_now!("algo: {:?}", algorithm as i32); - algorithm - } - None => { - return Err(Error::UnsupportedAlgorithm); - } - }; + let algorithm = algorithm.ok_or(Error::UnsupportedAlgorithm)?; + info_now!("algo: {:?}", algorithm as i32); // 8. process options; on known but unsupported error UnsupportedOption From e4962d5c561790e68f0bbf76decb9f3dc466d4cc Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 6 Jul 2023 22:26:57 +0200 Subject: [PATCH 020/135] Ignore user data with empty ID in get_assertion Users with an empty ID should not be returned by getAssertion to avoid compatibility issues. https://github.com/Nitrokey/fido-authenticator/issues/24 --- src/ctap2.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index b4862f3..ec08078 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1589,7 +1589,8 @@ impl crate::Authenticator { number_of_credentials: num_credentials, }; - if is_rk { + // User with empty IDs are ignored for compatibility + if is_rk && !credential.user.id.is_empty() { 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. From 21f24dee41ded450d7f90691c5d42ea498122235 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 7 Jul 2023 19:25:02 +0200 Subject: [PATCH 021/135] Update ctap-types --- Cargo.toml | 1 + src/credential.rs | 6 +++--- src/ctap1.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d9eaec..97363ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,5 +45,6 @@ rand = "0.8.4" features = ["dispatch"] [patch.crates-io] +ctap-types = { git = "https://github.com/Nitrokey/ctap-types.git", tag = "v0.1.2-nitrokey.2" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "51e68500d7601d04f884f5e95567d14b9018a6cb" } diff --git a/src/credential.rs b/src/credential.rs index dac78c6..c08dd01 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -324,7 +324,7 @@ impl Credential { let data = &mut stripped.data; data.rp.name = None; - data.rp.url = None; + data.rp.icon = None; data.user.icon = None; data.user.name = None; @@ -348,7 +348,7 @@ mod test { rp: PublicKeyCredentialRpEntity { id: String::from("John Doe"), name: None, - url: None, + icon: None, }, user: PublicKeyCredentialUserEntity { id: Bytes::from_slice(&[1, 2, 3]).unwrap(), @@ -427,7 +427,7 @@ mod test { rp: PublicKeyCredentialRpEntity { id: random_string(), name: maybe_random_string(), - url: maybe_random_string(), + icon: None, }, user: PublicKeyCredentialUserEntity { id: random_bytes(), //Bytes::from_slice(&[1,2,3]).unwrap(), diff --git a/src/ctap1.rs b/src/ctap1.rs index e519b82..60ff709 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -89,7 +89,7 @@ impl Authenticator for crate::Authenti let rp = ctap_types::webauthn::PublicKeyCredentialRpEntity { id: rp_id, name: None, - url: None, + icon: None, }; let user = ctap_types::webauthn::PublicKeyCredentialUserEntity { From 7cb4edc0a6778a8244ebf8f6e74b6598fbc10087 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 10 Jul 2023 15:14:02 +0200 Subject: [PATCH 022/135] Update changelog and fix formatting This patch fixes two oversights in the recent PRs: missing changelog entries and a formatting error. --- CHANGELOG.md | 9 +++++++++ src/ctap2.rs | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc5f597..ebbdf41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add config option for setting a maximum number of resident credentials. - Ignore public key credential paramters with an unknown type, as required by the Webauthn spec ([#28][]) +- Set the `makeCredUvNotRqd` CTAP option to `true` to indicate that we support + makeCredential operations without user verification ([#26][]) +- Reject `rk` option in getAssertion ([#31][]) +- Ignore user data with empty ID in getAssertion ([#32][]) +- Allow three instead of two PIN retries per boot ([#35][]) +[#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 +[#31]: https://github.com/solokeys/fido-authenticator/issues/31 +[#32]: https://github.com/solokeys/fido-authenticator/issues/32 +[#35]: https://github.com/solokeys/fido-authenticator/issues/35 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/src/ctap2.rs b/src/ctap2.rs index ec08078..0d4cef5 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -947,7 +947,12 @@ impl Authenticator for crate::Authenti // 6. process any options present // RK is not supported in get_assertion - if parameters.options.as_ref().and_then(|options| options.rk).is_some() { + if parameters + .options + .as_ref() + .and_then(|options| options.rk) + .is_some() + { return Err(Error::InvalidOption); } From ca50a48fe41e4d1a96d768ad63ffcab306a19de1 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 26 Jul 2023 22:09:27 +0200 Subject: [PATCH 023/135] Add log messages for requests, responses and errors This patch adds log messages for each request and response (and deserialization or protocol errors) in the dispatch module. This makes it easier to keep track of the executed commands by just looking at the log output from fido_authenticator::dispatch. --- src/dispatch.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/dispatch.rs b/src/dispatch.rs index 3393f6c..7aaf841 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -174,10 +174,58 @@ where ); // 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(|request| { + info!("Received CTAP2 request {:?}", request_operation(&request)); + trace!("CTAP2 request: {:?}", request); + request + }) + .map_err(|error| { + error!("Failed to deserialize CTAP2 request: {:?}", error); + trace!("The problematic input data was: {}", hex_str!(data)); + error as u8 + })?; debug!("2a SP: {:X}", msp()); use ctap2::Authenticator; authenticator .call_ctap2(&ctap_request) - .map_err(|error| error as u8) + .map(|response| { + info!("Sending CTAP2 response {:?}", response_operation(&response)); + trace!("CTAP2 response: {:?}", response); + response + }) + .map_err(|error| { + info!("CTAP2 error: {:?}", error); + error as u8 + }) +} + +#[allow(unused)] +fn request_operation(request: &ctap2::Request) -> ctap2::Operation { + match request { + ctap2::Request::MakeCredential(_) => ctap2::Operation::MakeCredential, + ctap2::Request::GetAssertion(_) => ctap2::Operation::GetAssertion, + ctap2::Request::GetNextAssertion => ctap2::Operation::GetNextAssertion, + ctap2::Request::GetInfo => ctap2::Operation::GetInfo, + ctap2::Request::ClientPin(_) => ctap2::Operation::ClientPin, + ctap2::Request::Reset => ctap2::Operation::Reset, + ctap2::Request::CredentialManagement(_) => ctap2::Operation::CredentialManagement, + ctap2::Request::Selection => ctap2::Operation::Selection, + ctap2::Request::Vendor(operation) => ctap2::Operation::Vendor(*operation), + } +} + +#[allow(unused)] +fn response_operation(request: &ctap2::Response) -> Option { + match request { + ctap2::Response::MakeCredential(_) => Some(ctap2::Operation::MakeCredential), + ctap2::Response::GetAssertion(_) => Some(ctap2::Operation::GetAssertion), + ctap2::Response::GetNextAssertion(_) => Some(ctap2::Operation::GetNextAssertion), + ctap2::Response::GetInfo(_) => Some(ctap2::Operation::GetInfo), + ctap2::Response::ClientPin(_) => Some(ctap2::Operation::ClientPin), + ctap2::Response::Reset => Some(ctap2::Operation::Reset), + ctap2::Response::CredentialManagement(_) => Some(ctap2::Operation::CredentialManagement), + ctap2::Response::Selection => Some(ctap2::Operation::Selection), + ctap2::Response::Vendor => None, + } } From 5099f800a8511bcac103ad57e1c97ebe951e90b8 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 26 Jul 2023 21:57:50 +0200 Subject: [PATCH 024/135] Revert "Set makeCredUvNotRqd in CTAP options" This reverts commit 8e93bc99586038d8ab8ec38ad27c5ea668c0fad5. We do not actually support makeCredential operations without PIN even if we should. The proper fix would be to relax this restriction, but this revert at least restores the previous behaviour. --- src/ctap2.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 0d4cef5..8348ec7 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -79,7 +79,6 @@ impl Authenticator for crate::Authenti false => Some(false), }, credential_mgmt_preview: Some(true), - make_cred_uv_not_rqd: Some(true), ..Default::default() }; // options.rk = true; From 0e3e56558505f5fdc755c41ff91727c20cdd3ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 2 Aug 2023 10:57:14 +0200 Subject: [PATCH 025/135] Update ctap types See https://github.com/Nitrokey/ctap-types/pull/10 --- Cargo.toml | 3 +-- src/ctap2.rs | 7 +------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 97363ba..a4d1382 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,6 @@ 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 = [] @@ -45,6 +44,6 @@ rand = "0.8.4" features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/Nitrokey/ctap-types.git", tag = "v0.1.2-nitrokey.2" } +ctap-types = { git = "https://github.com/Nitrokey/ctap-types.git", rev = "42751efdc3c717135e8f26ceaa6ce23fb57d0498" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "51e68500d7601d04f884f5e95567d14b9018a6cb" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 8348ec7..0fba645 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -195,12 +195,7 @@ impl Authenticator for crate::Authenti // 7. check pubKeyCredParams algorithm is valid + supported COSE identifier let mut algorithm: Option = None; - for param in parameters.pub_key_cred_params.iter() { - // Ignore unknown key types - if param.key_type != "public-key" { - continue; - } - + for param in parameters.pub_key_cred_params.0.iter() { match param.alg { -7 => { if algorithm.is_none() { From 82a85dc5fd88d32259b9e3f4f5542bc28b8c4f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 2 Aug 2023 16:54:27 +0200 Subject: [PATCH 026/135] Use updated CTAP types with zero-copy deserialization using much less stack --- Cargo.toml | 3 ++- src/credential.rs | 4 ++-- src/ctap2.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a4d1382..c594b07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ rand = "0.8.4" features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/Nitrokey/ctap-types.git", rev = "42751efdc3c717135e8f26ceaa6ce23fb57d0498" } +ctap-types = { git = "https://github.com/nitrokey/ctap-types.git", rev = "0011b36d2a97779e77ff6f154b08008c9608f904" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "51e68500d7601d04f884f5e95567d14b9018a6cb" } +serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } diff --git a/src/credential.rs b/src/credential.rs index c08dd01..a830cb5 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -8,7 +8,7 @@ pub(crate) use ctap_types::{ // authenticator::{ctap1, ctap2, Error, Request, Response}, ctap2::credential_management::CredentialProtectionPolicy, sizes::*, - webauthn::PublicKeyCredentialDescriptor, + webauthn::{PublicKeyCredentialDescriptor, PublicKeyCredentialDescriptorRef}, Bytes, String, }; @@ -276,7 +276,7 @@ impl Credential { pub fn try_from( authnr: &mut Authenticator, rp_id_hash: &Bytes<32>, - descriptor: &PublicKeyCredentialDescriptor, + descriptor: &PublicKeyCredentialDescriptorRef, ) -> Result { Self::try_from_bytes(authnr, rp_id_hash, &descriptor.id) } diff --git a/src/ctap2.rs b/src/ctap2.rs index 0fba645..70c9ea6 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1289,7 +1289,7 @@ impl crate::Authenticator { fn pin_prechecks( &mut self, options: &Option, - pin_auth: &Option, + pin_auth: &Option<&ctap2::PinAuth>, pin_protocol: &Option, data: &[u8], ) -> Result { From d318c117a26ce75194e122a36f6e03ec95c960e0 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 10 Jul 2023 19:47:31 +0200 Subject: [PATCH 027/135] Reduce ID length for new credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch implements the following changes to reduce the ID length for new credentials: - Rename the old Credential type to FullCredential and introduce a StrippedCredential type and a Credential enum to differentiate between full and reduced credential data. - Flatten the credential data to reduce encoding overhead. - Remove the RP id from the credential data to reduce the total length. - Add a marker field use_short_id to FullCredential so that we don’t change the credential ID for existing RKs. Fixes: https://github.com/Nitrokey/fido-authenticator/issues/29 --- CHANGELOG.md | 2 + Cargo.toml | 1 + src/credential.rs | 373 +++++++++++++++++++++++------ src/ctap1.rs | 54 ++--- src/ctap2.rs | 75 +++--- src/ctap2/credential_management.rs | 8 +- src/state.rs | 6 +- 7 files changed, 361 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebbdf41..5a855af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Reject `rk` option in getAssertion ([#31][]) - Ignore user data with empty ID in getAssertion ([#32][]) - Allow three instead of two PIN retries per boot ([#35][]) +- Reduce ID length for new credentials ([#37][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 [#31]: https://github.com/solokeys/fido-authenticator/issues/31 [#32]: https://github.com/solokeys/fido-authenticator/issues/32 [#35]: https://github.com/solokeys/fido-authenticator/issues/35 +[#37]: https://github.com/solokeys/fido-authenticator/issues/37 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/Cargo.toml b/Cargo.toml index c594b07..267ff99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ log-error = [] [dev-dependencies] # quickcheck = "1" rand = "0.8.4" +trussed = { version = "0.1", features = ["virt"] } [package.metadata.docs.rs] features = ["dispatch"] diff --git a/src/credential.rs b/src/credential.rs index a830cb5..32cd94a 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -2,6 +2,7 @@ use core::cmp::Ordering; +use serde::Serialize; use trussed::{client, syscall, try_syscall, types::KeyId}; pub(crate) use ctap_types::{ @@ -29,6 +30,32 @@ pub enum CtapVersion { #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] pub struct CredentialId(pub Bytes); +impl CredentialId { + fn new( + trussed: &mut T, + credential: &C, + key_encryption_key: KeyId, + rp_id_hash: &Bytes<32>, + nonce: &Bytes<12>, + ) -> Result { + let serialized_credential: SerializedCredential = + trussed::cbor_serialize_bytes(credential).map_err(|_| Error::Other)?; + let message = &serialized_credential; + // info!("serialized cred = {:?}", message).ok(); + let associated_data = &rp_id_hash[..]; + let nonce: [u8; 12] = nonce.as_slice().try_into().unwrap(); + let encrypted_serialized_credential = syscall!(trussed.encrypt_chacha8poly1305( + key_encryption_key, + message, + associated_data, + Some(&nonce) + )); + EncryptedSerializedCredential(encrypted_serialized_credential) + .try_into() + .map_err(|_| Error::RequestTooLarge) + } +} + // TODO: how to determine necessary size? // pub type SerializedCredential = Bytes<512>; // pub type SerializedCredential = Bytes<256>; @@ -71,7 +98,107 @@ pub enum Key { WrappedKey(Bytes<128>), } -/// The main content of a `Credential`. +/// A credential that is managed by the authenticator. +/// +/// The authenticator uses two credential representations: +/// - [`FullCredential`][] contains all data available for a credential and is used for resident +/// credentials that are stored on the filesystem. Older versions of this app used this +/// reprensentation for non-resident credentials too. +/// - [`StrippedCredential`][] contains the minimal data required for non-resident credentials. As +/// the data for these credentials is encoded in the credential ID, we try to keep it as small as +/// possible. +#[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] +pub enum Credential { + Full(FullCredential), + Stripped(StrippedCredential), +} + +impl Credential { + pub fn try_from( + authnr: &mut Authenticator, + rp_id_hash: &Bytes<32>, + descriptor: &PublicKeyCredentialDescriptorRef, + ) -> Result { + Self::try_from_bytes(authnr, rp_id_hash, descriptor.id) + } + + pub fn try_from_bytes( + authnr: &mut Authenticator, + rp_id_hash: &Bytes<32>, + id: &[u8], + ) -> Result { + let mut cred: Bytes = Bytes::new(); + cred.extend_from_slice(id) + .map_err(|_| Error::InvalidCredential)?; + + let encrypted_serialized = EncryptedSerializedCredential::try_from(CredentialId(cred))?; + + let kek = authnr + .state + .persistent + .key_encryption_key(&mut authnr.trussed)?; + + let serialized = try_syscall!(authnr.trussed.decrypt_chacha8poly1305( + kek, + &encrypted_serialized.0.ciphertext, + &rp_id_hash[..], + &encrypted_serialized.0.nonce, + &encrypted_serialized.0.tag, + )) + .map_err(|_| Error::InvalidCredential)? + .plaintext + .ok_or(Error::InvalidCredential)?; + + // In older versions of this app, we serialized the full credential. Now we only serialize + // the stripped credential. For compatibility, we have to try both. + FullCredential::deserialize(&serialized) + .map(Self::Full) + .or_else(|_| StrippedCredential::deserialize(&serialized).map(Self::Stripped)) + .map_err(|_| Error::InvalidCredential) + } + + pub fn id( + &self, + trussed: &mut T, + key_encryption_key: KeyId, + rp_id_hash: &Bytes<32>, + ) -> Result { + match self { + Self::Full(credential) => credential.id(trussed, key_encryption_key, Some(rp_id_hash)), + Self::Stripped(credential) => CredentialId::new( + trussed, + credential, + key_encryption_key, + rp_id_hash, + &credential.nonce, + ), + } + } + + pub fn algorithm(&self) -> i32 { + match self { + Self::Full(credential) => credential.algorithm, + Self::Stripped(credential) => credential.algorithm, + } + } + + pub fn cred_protect(&self) -> Option { + match self { + Self::Full(credential) => credential.cred_protect, + Self::Stripped(credential) => credential.cred_protect, + } + } + + pub fn key(&self) -> &Key { + match self { + Self::Full(credential) => &credential.key, + Self::Stripped(credential) => &credential.key, + } + } +} + +/// The main content of a `FullCredential`. #[derive( Clone, Debug, PartialEq, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed, )] @@ -101,13 +228,20 @@ pub struct CredentialData { pub cred_protect: Option, // TODO: add `sig_counter: Option`, // and grant RKs a per-credential sig-counter. + + // In older app versions, we serialized the full credential to determine the credential ID. In + // newer app versions, we strip unnecessary fields to generate a shorter credential ID. To + // make sure that the credential ID does not change for an existing credential, this field is + // used as a marker for new credentials. + #[serde(skip_serializing_if = "Option::is_none")] + use_short_id: Option, } // 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)] -pub struct Credential { +pub struct FullCredential { ctap: CtapVersion, pub data: CredentialData, nonce: Bytes<12>, @@ -121,7 +255,7 @@ pub struct Credential { // nonce: Bytes<12>, // } -impl core::ops::Deref for Credential { +impl core::ops::Deref for FullCredential { type Target = CredentialData; fn deref(&self) -> &Self::Target { @@ -132,34 +266,34 @@ 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 { +impl PartialEq for FullCredential { fn eq(&self, other: &Self) -> bool { (self.creation_time == other.creation_time) && (self.key == other.key) } } -impl PartialEq<&Credential> for Credential { +impl PartialEq<&FullCredential> for FullCredential { fn eq(&self, other: &&Self) -> bool { self == *other } } -impl Eq for Credential {} +impl Eq for FullCredential {} -impl Ord for Credential { +impl Ord for FullCredential { fn cmp(&self, other: &Self) -> Ordering { self.data.creation_time.cmp(&other.data.creation_time) } } /// Order by timestamp of creation. -impl PartialOrd for Credential { +impl PartialOrd for FullCredential { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialOrd<&Credential> for Credential { +impl PartialOrd<&FullCredential> for FullCredential { fn partial_cmp(&self, other: &&Self) -> Option { Some(self.cmp(*other)) } @@ -181,7 +315,7 @@ impl From for PublicKeyCredentialDescriptor { } } -impl Credential { +impl FullCredential { #[allow(clippy::too_many_arguments)] pub fn new( ctap: CtapVersion, @@ -207,9 +341,11 @@ impl Credential { hmac_secret, cred_protect, + + use_short_id: Some(true), }; - Credential { + FullCredential { ctap, data, nonce: Bytes::from_slice(&nonce).unwrap(), @@ -235,10 +371,6 @@ impl Credential { key_encryption_key: KeyId, rp_id_hash: Option<&Bytes<32>>, ) -> Result { - let serialized_credential = self.strip().serialize()?; - let message = &serialized_credential; - // info!("serialized cred = {:?}", message).ok(); - let rp_id_hash: Bytes<32> = if let Some(hash) = rp_id_hash { hash.clone() } else { @@ -247,16 +379,18 @@ impl Credential { .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 credential_id: CredentialId = encrypted_serialized_credential - .try_into() - .map_err(|_| Error::RequestTooLarge)?; - - Ok(credential_id) + if self.use_short_id.unwrap_or_default() { + StrippedCredential::from(self).id(trussed, key_encryption_key, &rp_id_hash) + } else { + let stripped_credential = self.strip(); + CredentialId::new( + trussed, + &stripped_credential, + key_encryption_key, + &rp_id_hash, + &self.nonce, + ) + } } pub fn serialize(&self) -> Result { @@ -273,52 +407,10 @@ impl Credential { } } - pub fn try_from( - authnr: &mut Authenticator, - rp_id_hash: &Bytes<32>, - descriptor: &PublicKeyCredentialDescriptorRef, - ) -> Result { - Self::try_from_bytes(authnr, rp_id_hash, &descriptor.id) - } - - pub fn try_from_bytes( - authnr: &mut Authenticator, - rp_id_hash: &Bytes<32>, - id: &[u8], - ) -> Result { - let mut cred: Bytes = Bytes::new(); - cred.extend_from_slice(id) - .map_err(|_| Error::InvalidCredential)?; - - let encrypted_serialized = EncryptedSerializedCredential::try_from(CredentialId(cred))?; - - 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? - kek, - &encrypted_serialized.0.ciphertext, - &rp_id_hash[..], - &encrypted_serialized.0.nonce, - &encrypted_serialized.0.tag, - )) - .map_err(|_| Error::InvalidCredential)? - .plaintext - .ok_or(Error::InvalidCredential)?; - - let credential = - Credential::deserialize(&serialized).map_err(|_| Error::InvalidCredential)?; - - Ok(credential) - } - - // Remove inessential metadata from credential. - // - // Called by the `id` method, see its documentation. - pub fn strip(&self) -> Self { + // This method is only kept for compatibility. To strip new credentials, use + // `StrippedCredential`. + #[must_use] + fn strip(&self) -> Self { info_now!(":: stripping ID"); let mut stripped = self.clone(); let data = &mut stripped.data; @@ -337,13 +429,71 @@ impl Credential { } } +/// A reduced version of `FullCredential` that is used for non-resident credentials. +/// +/// As the credential data is encodeded in the credential ID, we only want to include necessary +/// data to keep the credential ID as short as possible. +#[derive(Clone, Debug, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)] +pub struct StrippedCredential { + pub ctap: CtapVersion, + pub creation_time: u32, + pub use_counter: bool, + pub algorithm: i32, + pub key: Key, + pub nonce: Bytes<12>, + // extensions + #[serde(skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_protect: Option, +} + +impl StrippedCredential { + fn deserialize(bytes: &SerializedCredential) -> Result { + match ctap_types::serde::cbor_deserialize(bytes) { + Ok(s) => Ok(s), + Err(_) => { + info_now!("could not deserialize {:?}", bytes); + Err(Error::Other) + } + } + } + + pub fn id( + &self, + trussed: &mut T, + key_encryption_key: KeyId, + rp_id_hash: &Bytes<32>, + ) -> Result { + CredentialId::new(trussed, self, key_encryption_key, rp_id_hash, &self.nonce) + } +} + +impl From<&FullCredential> for StrippedCredential { + fn from(credential: &FullCredential) -> Self { + Self { + ctap: credential.ctap, + creation_time: credential.data.creation_time, + use_counter: credential.data.use_counter, + algorithm: credential.data.algorithm, + key: credential.data.key.clone(), + nonce: credential.nonce.clone(), + hmac_secret: credential.data.hmac_secret, + cred_protect: credential.data.cred_protect, + } + } +} + #[cfg(test)] mod test { use super::*; + use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; + use trussed::{ + client::{Chacha8Poly1305, Sha256}, + types::Location, + }; fn credential_data() -> CredentialData { - use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; - CredentialData { rp: PublicKeyCredentialRpEntity { id: String::from("John Doe"), @@ -362,6 +512,7 @@ mod test { key: Key::WrappedKey(Bytes::from_slice(&[1, 2, 3]).unwrap()), hmac_secret: Some(false), cred_protect: None, + use_short_id: Some(true), } } @@ -421,8 +572,6 @@ mod test { } fn random_credential_data() -> CredentialData { - use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; - CredentialData { rp: PublicKeyCredentialRpEntity { id: random_string(), @@ -441,6 +590,7 @@ mod test { key: Key::WrappedKey(random_bytes()), hmac_secret: Some(false), cred_protect: None, + use_short_id: Some(true), } } @@ -461,6 +611,79 @@ mod test { assert_eq!(credential_data, deserialized); } + #[test] + fn credential_ids() { + trussed::virt::with_ram_client("fido", |mut client| { + let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; + let mut nonce = Bytes::new(); + nonce.extend_from_slice(&[0; 12]).unwrap(); + let data = credential_data(); + let mut full_credential = FullCredential { + ctap: CtapVersion::Fido21Pre, + data, + nonce, + }; + let rp_id_hash = syscall!(client.hash_sha256(full_credential.rp.id.as_ref())) + .hash + .to_bytes() + .unwrap(); + + // Case 1: credential with use_short_id = Some(true) uses new (short) format + full_credential.data.use_short_id = Some(true); + let stripped_credential = StrippedCredential::from(&full_credential); + let full_id = full_credential + .id(&mut client, kek, Some(&rp_id_hash)) + .unwrap(); + let short_id = stripped_credential + .id(&mut client, kek, &rp_id_hash) + .unwrap(); + assert_eq!(full_id.0, short_id.0); + + // Case 2: credential with use_short_id = None uses old (long) format + full_credential.data.use_short_id = None; + let stripped_credential = full_credential.strip(); + let full_id = full_credential + .id(&mut client, kek, Some(&rp_id_hash)) + .unwrap(); + let long_id = CredentialId::new( + &mut client, + &stripped_credential, + kek, + &rp_id_hash, + &full_credential.nonce, + ) + .unwrap(); + assert_eq!(full_id.0, long_id.0); + + assert!(short_id.0.len() < long_id.0.len()); + }); + } + + #[test] + fn max_credential_id() { + let rp_id: String<256> = core::iter::repeat('?').take(256).collect(); + let key = Bytes::from_slice(&[u8::MAX; 128]).unwrap(); + let credential = StrippedCredential { + ctap: CtapVersion::Fido21Pre, + creation_time: u32::MAX, + use_counter: true, + algorithm: i32::MAX, + key: Key::WrappedKey(key), + nonce: Bytes::from_slice(&[u8::MAX; 12]).unwrap(), + hmac_secret: Some(true), + cred_protect: Some(CredentialProtectionPolicy::Required), + }; + trussed::virt::with_ram_client("fido", |mut client| { + let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; + let rp_id_hash = syscall!(client.hash_sha256(rp_id.as_ref())) + .hash + .to_bytes() + .unwrap(); + let id = credential.id(&mut client, kek, &rp_id_hash).unwrap(); + assert_eq!(id.0.len(), 204); + }); + } + // use quickcheck::TestResult; // quickcheck::quickcheck! { // fn prop( diff --git a/src/ctap1.rs b/src/ctap1.rs index 60ff709..f3df339 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -12,7 +12,7 @@ use trussed::{ use crate::{ constants, - credential::{self, Credential, Key}, + credential::{self, Credential, Key, StrippedCredential}, SigningAlgorithm, TrussedRequirements, UserPresence, }; @@ -74,45 +74,23 @@ impl Authenticator for crate::Authenti .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, - name: None, - icon: None, - }; - - let user = ctap_types::webauthn::PublicKeyCredentialUserEntity { - id: Bytes::from_slice(&[0u8; 8]).unwrap(), - icon: None, - name: None, - display_name: None, - }; + let nonce = syscall!(self.trussed.random_bytes(12)).bytes; + let nonce = Bytes::from_slice(&nonce).unwrap(); - let credential = Credential::new( - credential::CtapVersion::U2fV2, - &rp, - &user, - SigningAlgorithm::P256 as i32, - key, - self.state + let credential = StrippedCredential { + ctap: credential::CtapVersion::U2fV2, + creation_time: self + .state .persistent .timestamp(&mut self.trussed) .map_err(|_| Error::NotEnoughMemory)?, - None, - None, + use_counter: true, + algorithm: SigningAlgorithm::P256 as i32, + key, nonce, - ); + hmac_secret: None, + cred_protect: None, + }; // info!("made credential {:?}", &credential); @@ -123,7 +101,7 @@ impl Authenticator for crate::Authenti .key_encryption_key(&mut self.trussed) .map_err(|_| Error::NotEnoughMemory)?; let credential_id = credential - .id(&mut self.trussed, kek, Some(®.app_id)) + .id(&mut self.trussed, kek, ®.app_id) .map_err(|_| Error::NotEnoughMemory)?; let mut commitment = Commitment::new(); @@ -198,7 +176,7 @@ impl Authenticator for crate::Authenti let cred = cred.map_err(|_| Error::IncorrectDataParameter)?; - let key = match &cred.key { + let key = match cred.key() { Key::WrappedKey(bytes) => { let wrapping_key = self .state @@ -226,7 +204,7 @@ impl Authenticator for crate::Authenti _ => return Err(Error::IncorrectDataParameter), }; - if cred.algorithm != -7 { + if cred.algorithm() != -7 { info!("Unexpected mechanism for u2f"); return Err(Error::IncorrectDataParameter); } diff --git a/src/ctap2.rs b/src/ctap2.rs index 70c9ea6..6abf87e 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -17,12 +17,7 @@ use trussed::{ use crate::{ constants, - credential::{ - self, - Credential, - // CredentialList, - Key, - }, + credential::{self, Credential, FullCredential, Key, StrippedCredential}, format_hex, state::{ self, @@ -145,7 +140,7 @@ impl Authenticator for crate::Authenti // 7. reset timer // 8. increment credential counter (not applicable) - self.assert_with_credential(None, credential) + self.assert_with_credential(None, Credential::Full(credential)) } #[inline(never)] @@ -180,7 +175,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)) + if !(excluded_cred.cred_protect() == Some(CredentialProtectionPolicy::Required)) || uv_performed { info_now!("Excluded!"); @@ -342,7 +337,7 @@ impl Authenticator for crate::Authenti // store it. // TODO: overwrite, error handling with KeyStoreFull - let credential = Credential::new( + let credential = FullCredential::new( credential::CtapVersion::Fido21Pre, ¶meters.rp, ¶meters.user, @@ -355,7 +350,8 @@ impl Authenticator for crate::Authenti ); // note that this does the "stripping" of OptionalUI etc. - let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; + let credential_id = + StrippedCredential::from(&credential).id(&mut self.trussed, kek, &rp_id_hash)?; if rk_requested { // serialization with all metadata @@ -468,7 +464,7 @@ impl Authenticator for crate::Authenti .map_err(|_| Error::Other)?; // debug_now!("serialized_auth_data ={:?}", &serialized_auth_data); commitment - .extend_from_slice(¶meters.client_data_hash) + .extend_from_slice(parameters.client_data_hash) .map_err(|_| Error::Other)?; // debug_now!("client_data_hash = {:?}", ¶meters.client_data_hash); // debug_now!("commitment = {:?}", &commitment); @@ -1006,14 +1002,14 @@ impl crate::Authenticator { allowlist_passed: bool, uv_performed: bool, ) -> bool { - if !self.check_key_exists(credential.algorithm, &credential.key) { + 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 { + 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, @@ -1089,11 +1085,13 @@ impl crate::Authenticator { let credential_data = syscall!(self.trussed.read_file(Location::Internal, path.clone(),)).data; - let credential = Credential::deserialize(&credential_data).ok()?; + let credential = FullCredential::deserialize(&credential_data).ok()?; + let timestamp = credential.creation_time; + let credential = Credential::Full(credential); if self.check_credential_applicable(&credential, false, uv_performed) { self.state.runtime.push_credential(CachedCredential { - timestamp: credential.creation_time, + timestamp, path: String::from_str(path.as_str_ref_with_trailing_nul()).ok()?, }); } @@ -1105,7 +1103,7 @@ impl crate::Authenticator { let num_credentials = self.state.runtime.remaining_credentials(); let credential = self.state.runtime.pop_credential(&mut self.trussed); - credential.map(|credential| (credential, num_credentials)) + credential.map(|credential| (Credential::Full(credential), num_credentials)) } fn decrypt_pin_hash_and_maybe_escalate( @@ -1311,11 +1309,8 @@ impl crate::Authenticator { } // 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); - } + if pin_auth.is_some() && pin_protocol != &Some(1) { + return Err(Error::PinAuthInvalid); } // 3. if no PIN is set (we have no other form of UV), @@ -1337,7 +1332,7 @@ impl crate::Authenticator { // Current thinking: no if self.state.persistent.pin_is_set() { // let mut uv_performed = false; - if let Some(ref pin_auth) = pin_auth { + if let Some(pin_auth) = pin_auth { if pin_auth.len() != 16 { return Err(Error::InvalidParameter); } @@ -1484,7 +1479,7 @@ impl crate::Authenticator { 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() { + 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)?; @@ -1521,7 +1516,7 @@ impl crate::Authenticator { .state .persistent .key_encryption_key(&mut self.trussed)?; - let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; + let credential_id = credential.id(&mut self.trussed, kek, &rp_id_hash)?; use ctap2::AuthenticatorDataFlags as Flags; @@ -1559,7 +1554,7 @@ impl crate::Authenticator { .extend_from_slice(&data.client_data_hash) .map_err(|_| Error::Other)?; - let (mechanism, serialization) = match credential.algorithm { + let (mechanism, serialization) = match credential.algorithm() { -7 => (Mechanism::P256, SignatureSerialization::Asn1Der), -8 => (Mechanism::Ed255, SignatureSerialization::Raw), // -9 => (Mechanism::Totp, SignatureSerialization::Raw), @@ -1589,17 +1584,21 @@ impl crate::Authenticator { }; // User with empty IDs are ignored for compatibility - if is_rk && !credential.user.id.is_empty() { - 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; + if is_rk { + if let Credential::Full(credential) = credential { + if !credential.user.id.is_empty() { + 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); + } } - response.user = Some(user); } Ok(response) @@ -1630,7 +1629,7 @@ 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_maybe = Credential::deserialize(&credential_data); + let credential_maybe = FullCredential::deserialize(&credential_data); if let Ok(old_credential) = credential_maybe { if old_credential.user.id == user_id { @@ -1666,7 +1665,7 @@ impl crate::Authenticator { .trussed .read_file(Location::Internal, PathBuf::from(rk_path),)) .data; - let credential_maybe = Credential::deserialize(&credential_data); + let credential_maybe = FullCredential::deserialize(&credential_data); // info_now!("deleting credential {:?}", &credential); if let Ok(credential) = credential_maybe { diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 4ae63c9..627858a 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -17,7 +17,7 @@ use ctap_types::{ use crate::{ constants::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE, - credential::Credential, + credential::FullCredential, state::{CredentialManagementEnumerateCredentials, CredentialManagementEnumerateRps}, Authenticator, Result, TrussedRequirements, UserPresence, }; @@ -161,7 +161,7 @@ where .read_file(Location::Internal, rk_entry.path().into(),)) .data; - let credential = Credential::deserialize(&serialized) + let credential = FullCredential::deserialize(&serialized) // this may be a confusing error message .map_err(|_| Error::InvalidCredential)?; @@ -238,7 +238,7 @@ where .read_file(Location::Internal, rk_entry.path().into(),)) .data; - let credential = Credential::deserialize(&serialized) + let credential = FullCredential::deserialize(&serialized) // this may be a confusing error message .map_err(|_| Error::InvalidCredential)?; @@ -385,7 +385,7 @@ where let serialized = syscall!(self.trussed.read_file(Location::Internal, rk_path.into(),)).data; - let credential = Credential::deserialize(&serialized) + let credential = FullCredential::deserialize(&serialized) // this may be a confusing error message .map_err(|_| Error::InvalidCredential)?; diff --git a/src/state.rs b/src/state.rs index 8e34c29..0e7e43c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -18,7 +18,7 @@ use trussed::{ use heapless::binary_heap::{BinaryHeap, Max}; -use crate::{cbor_serialize_message, credential::Credential, Result}; +use crate::{cbor_serialize_message, credential::FullCredential, Result}; #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CachedCredential { @@ -479,7 +479,7 @@ impl RuntimeState { pub fn pop_credential( &mut self, trussed: &mut T, - ) -> Option { + ) -> Option { let cached_credential = self.cached_credentials.pop()?; let credential_data = syscall!(trussed.read_file( @@ -488,7 +488,7 @@ impl RuntimeState { )) .data; - Credential::deserialize(&credential_data).ok() + FullCredential::deserialize(&credential_data).ok() } pub fn remaining_credentials(&self) -> u32 { From 2f49017eec5a87bd2570593571b87a15a9890ed0 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 27 Oct 2023 21:45:25 +0200 Subject: [PATCH 028/135] Update ctap-types --- Cargo.toml | 2 +- src/ctap2.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 267ff99..a81630d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ trussed = { version = "0.1", features = ["virt"] } features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/nitrokey/ctap-types.git", rev = "0011b36d2a97779e77ff6f154b08008c9608f904" } +ctap-types = { git = "https://github.com/nitrokey/ctap-types.git", tag = "v0.1.2-nitrokey.4" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "51e68500d7601d04f884f5e95567d14b9018a6cb" } serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 6abf87e..0967846 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -159,7 +159,7 @@ impl Authenticator for crate::Authenti } let uv_performed = self.pin_prechecks( ¶meters.options, - ¶meters.pin_auth, + parameters.pin_auth.map(AsRef::as_ref), ¶meters.pin_protocol, parameters.client_data_hash.as_ref(), )?; @@ -910,7 +910,7 @@ impl Authenticator for crate::Authenti // 1-4. let uv_performed = match self.pin_prechecks( ¶meters.options, - ¶meters.pin_auth, + parameters.pin_auth.map(AsRef::as_ref), ¶meters.pin_protocol, parameters.client_data_hash.as_ref(), ) { @@ -1287,7 +1287,7 @@ impl crate::Authenticator { fn pin_prechecks( &mut self, options: &Option, - pin_auth: &Option<&ctap2::PinAuth>, + pin_auth: Option<&[u8]>, pin_protocol: &Option, data: &[u8], ) -> Result { @@ -1296,7 +1296,7 @@ impl crate::Authenticator { // // 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 let Some(pin_auth) = pin_auth { if pin_auth.len() == 0 { self.up .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; @@ -1344,7 +1344,7 @@ impl crate::Authenticator { // error --> PinAuthInvalid self.verify_pin( // unwrap panic ruled out above - pin_auth.as_slice().try_into().unwrap(), + pin_auth.try_into().unwrap(), data, )?; From 288fc030d16c137a482caf3a76146dca1b9ade3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 8 Nov 2023 11:46:12 +0100 Subject: [PATCH 029/135] Update apdu-dispatch This allows rejecting calls to `Select` over CCID --- Cargo.toml | 1 + src/dispatch/apdu.rs | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a81630d..9e82879 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,5 +47,6 @@ features = ["dispatch"] [patch.crates-io] ctap-types = { git = "https://github.com/nitrokey/ctap-types.git", tag = "v0.1.2-nitrokey.4" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } +apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "51e68500d7601d04f884f5e95567d14b9018a6cb" } serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } diff --git a/src/dispatch/apdu.rs b/src/dispatch/apdu.rs index cbc2ed4..2b233f3 100644 --- a/src/dispatch/apdu.rs +++ b/src/dispatch/apdu.rs @@ -1,4 +1,4 @@ -use apdu_dispatch::{app as apdu, response::Data, Command}; +use apdu_dispatch::{app as apdu, dispatch::Interface, response::Data, Command}; use ctap_types::{serde::error::Error as SerdeError, Error}; use ctaphid_dispatch::app as ctaphid; use iso7816::Status; @@ -28,7 +28,13 @@ where UP: UserPresence, T: TrussedRequirements, { - fn select(&mut self, _: &Command, reply: &mut Data) -> apdu::Result { + fn select(&mut self, interface: Interface, _: &Command, reply: &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); + } + reply.extend_from_slice(b"U2F_V2").unwrap(); Ok(()) } From bb6c07c12a8ea94db1dc897513cdf54f73a275e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 8 Nov 2023 11:48:32 +0100 Subject: [PATCH 030/135] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a855af..9a5649d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ignore user data with empty ID in getAssertion ([#32][]) - Allow three instead of two PIN retries per boot ([#35][]) - Reduce ID length for new credentials ([#37][]) +- Update apdu-dispatch and reject calls to `select` ([#40][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#32]: https://github.com/solokeys/fido-authenticator/issues/32 [#35]: https://github.com/solokeys/fido-authenticator/issues/35 [#37]: https://github.com/solokeys/fido-authenticator/issues/37 +[#40]: https://github.com/nitrokey/fido-authenticator/pull/40 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp From 6800f4928bc49446c1d640d74efce5f2a5ce0dc2 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 13:33:42 +0100 Subject: [PATCH 031/135] Update ctap-types This patch updates the ctap-types dependency to pull in support for the largeBlobKey extension and the largeBlobs command. --- Cargo.toml | 2 +- src/ctap2.rs | 9 +++++++-- src/dispatch.rs | 2 ++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9e82879..96b6516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ trussed = { version = "0.1", features = ["virt"] } features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/nitrokey/ctap-types.git", tag = "v0.1.2-nitrokey.4" } +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "785bcc52720ce2e2054ae32034a2a24c500e1043" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "51e68500d7601d04f884f5e95567d14b9018a6cb" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 0967846..8c93ae7 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -50,7 +50,7 @@ impl Authenticator for crate::Authenti .push(String::from_str("FIDO_2_1_PRE").unwrap()) .unwrap(); - let mut extensions = Vec::, 4>::new(); + let mut extensions = Vec::, 4>::new(); // extensions.push(String::from_str("credProtect").unwrap()).unwrap(); extensions .push(String::from_str("credProtect").unwrap()) @@ -444,6 +444,7 @@ impl Authenticator for crate::Authenti Some(ctap2::make_credential::Extensions { cred_protect: parameters.extensions.as_ref().unwrap().cred_protect, hmac_secret: parameters.extensions.as_ref().unwrap().hmac_secret, + large_blob_key: None, }) } else { None @@ -551,6 +552,8 @@ impl Authenticator for crate::Authenti fmt, auth_data: serialized_auth_data, att_stmt, + ep_att: None, + large_blob_key: None, }; Ok(attestation_object) @@ -1526,7 +1529,7 @@ impl crate::Authenticator { rp_id_hash, flags: { - let mut flags = Flags::EMPTY; + let mut flags = Flags::empty(); if data.up_performed { flags |= Flags::USER_PRESENCE; } @@ -1581,6 +1584,8 @@ impl crate::Authenticator { signature, user: None, number_of_credentials: num_credentials, + user_selected: None, + large_blob_key: None, }; // User with empty IDs are ignored for compatibility diff --git a/src/dispatch.rs b/src/dispatch.rs index 7aaf841..50849e6 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -211,6 +211,7 @@ fn request_operation(request: &ctap2::Request) -> ctap2::Operation { ctap2::Request::Reset => ctap2::Operation::Reset, ctap2::Request::CredentialManagement(_) => ctap2::Operation::CredentialManagement, ctap2::Request::Selection => ctap2::Operation::Selection, + ctap2::Request::LargeBlobs(_) => ctap2::Operation::LargeBlobs, ctap2::Request::Vendor(operation) => ctap2::Operation::Vendor(*operation), } } @@ -226,6 +227,7 @@ fn response_operation(request: &ctap2::Response) -> Option { ctap2::Response::Reset => Some(ctap2::Operation::Reset), ctap2::Response::CredentialManagement(_) => Some(ctap2::Operation::CredentialManagement), ctap2::Response::Selection => Some(ctap2::Operation::Selection), + ctap2::Response::LargeBlobs(_) => Some(ctap2::Operation::LargeBlobs), ctap2::Response::Vendor => None, } } From 71d14ff073a592c9e678f3b2c353d03164e881cf Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 13:45:55 +0100 Subject: [PATCH 032/135] Add largeBlobKey support to get_info This patch adds support for the largeBlobKey extension to the get_info command. It also adds a config entry to be able to enable or disable the extension. --- src/ctap2.rs | 6 ++++++ src/lib.rs | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/src/ctap2.rs b/src/ctap2.rs index 8c93ae7..1a66bc1 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -58,6 +58,11 @@ impl Authenticator for crate::Authenti extensions .push(String::from_str("hmac-secret").unwrap()) .unwrap(); + if self.config.supports_large_blobs() { + extensions + .push(String::from_str("largeBlobKey").unwrap()) + .unwrap(); + } let mut pin_protocols = Vec::::new(); pin_protocols.push(1).unwrap(); @@ -74,6 +79,7 @@ impl Authenticator for crate::Authenti false => Some(false), }, credential_mgmt_preview: Some(true), + large_blobs: Some(self.config.supports_large_blobs()), ..Default::default() }; // options.rk = true; diff --git a/src/lib.rs b/src/lib.rs index 5cd444a..abff732 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,14 @@ pub struct Config { pub skip_up_timeout: Option, /// The maximum number of resident credentials. pub max_resident_credential_count: Option, + /// Enable the largeBlobKey extension and the largeBlobs command. + pub large_blobs: bool, +} + +impl Config { + pub fn supports_large_blobs(&self) -> bool { + self.large_blobs + } } // impl Default for Config { From c43da04a260f40b31302b413706eca2fe505b5bf Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 14:02:05 +0100 Subject: [PATCH 033/135] Add largeBlobKey support to make_credential This patch adds support for the largeBlobKey extension to make_credential. This means that we have to generate a 32-bit key and store it together with the credential if requested by the platform. --- src/credential.rs | 4 ++++ src/ctap2.rs | 25 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/credential.rs b/src/credential.rs index 32cd94a..b67ea62 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -226,6 +226,8 @@ pub struct CredentialData { pub hmac_secret: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option>, // TODO: add `sig_counter: Option`, // and grant RKs a per-credential sig-counter. @@ -327,6 +329,7 @@ impl FullCredential { timestamp: u32, hmac_secret: Option, cred_protect: Option, + large_blob_key: Option>, nonce: [u8; 12], ) -> Self { info!("credential for algorithm {}", algorithm); @@ -341,6 +344,7 @@ impl FullCredential { hmac_secret, cred_protect, + large_blob_key, use_short_id: Some(true), }; diff --git a/src/ctap2.rs b/src/ctap2.rs index 1a66bc1..502e268 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -234,6 +234,7 @@ impl Authenticator for crate::Authenti let mut hmac_secret_requested = None; // let mut cred_protect_requested = CredentialProtectionPolicy::Optional; let mut cred_protect_requested = None; + let mut large_blob_key_requested = false; if let Some(extensions) = ¶meters.extensions { hmac_secret_requested = extensions.hmac_secret; @@ -241,6 +242,21 @@ impl Authenticator for crate::Authenti cred_protect_requested = Some(credential::CredentialProtectionPolicy::try_from(*policy)?); } + + if self.config.supports_large_blobs() { + if let Some(large_blob_key) = extensions.large_blob_key { + if large_blob_key { + if !rk_requested { + // the largeBlobKey extension is only available for resident keys + return Err(Error::InvalidOption); + } + large_blob_key_requested = true; + } else { + // large_blob_key must be Some(true) or omitted, Some(false) is invalid + return Err(Error::InvalidOption); + } + } + } } // debug_now!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); @@ -343,6 +359,12 @@ impl Authenticator for crate::Authenti // store it. // TODO: overwrite, error handling with KeyStoreFull + let large_blob_key = if large_blob_key_requested { + Some(Bytes::from_slice(&syscall!(self.trussed.random_bytes(32)).bytes).unwrap()) + } else { + None + }; + let credential = FullCredential::new( credential::CtapVersion::Fido21Pre, ¶meters.rp, @@ -352,6 +374,7 @@ impl Authenticator for crate::Authenti self.state.persistent.timestamp(&mut self.trussed)?, hmac_secret_requested, cred_protect_requested, + large_blob_key.clone(), nonce, ); @@ -559,7 +582,7 @@ impl Authenticator for crate::Authenti auth_data: serialized_auth_data, att_stmt, ep_att: None, - large_blob_key: None, + large_blob_key, }; Ok(attestation_object) From 48d66c08555885f163458308cf0dc9c031c0b579 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 14:09:33 +0100 Subject: [PATCH 034/135] Add largeBlobKey support to get_assertion This patch adds support for the largeBlobKey extension to get_assertion. This means that we have to return the key stored together with the credential if it is present and requested by the platform. --- src/ctap2.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ctap2.rs b/src/ctap2.rs index 502e268..a54163e 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1535,7 +1535,15 @@ impl crate::Authenticator { }; // 8. process any extensions present + let mut large_blob_key_requested = false; let extensions_output = if let Some(extensions) = &data.extensions { + if self.config.supports_large_blobs() { + if extensions.large_blob_key == Some(false) { + // large_blob_key must be Some(true) or omitted + return Err(Error::InvalidOption); + } + large_blob_key_requested = extensions.large_blob_key == Some(true); + } self.process_assertion_extensions(&data, extensions, &credential, key)? } else { None @@ -1632,6 +1640,10 @@ impl crate::Authenticator { } response.user = Some(user); } + + if large_blob_key_requested { + response.large_blob_key = credential.large_blob_key.clone(); + } } } From f3128f8ef6c57fe505bf371a8976dd6d9240483a Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 19:58:28 +0100 Subject: [PATCH 035/135] Implement largeBlobs command This patch implements the largeBlobs command for reading and writing the large-blob array. Currently, the maximum size of the total array with metadata is 1024 bytes because it has to fit in a Trussed message. The storage location can be configured by the runner. --- CHANGELOG.md | 2 + src/ctap2.rs | 171 ++++++++++++++++++++++++++++++++++ src/ctap2/large_blobs.rs | 193 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 10 +- src/state.rs | 5 +- 5 files changed, 377 insertions(+), 4 deletions(-) create mode 100644 src/ctap2/large_blobs.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5649d..4a00803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow three instead of two PIN retries per boot ([#35][]) - Reduce ID length for new credentials ([#37][]) - Update apdu-dispatch and reject calls to `select` ([#40][]) +- Implement the `largeBlobKey` extension and the `largeBlobs` command ([#38][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -23,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#35]: https://github.com/solokeys/fido-authenticator/issues/35 [#37]: https://github.com/solokeys/fido-authenticator/issues/37 [#40]: https://github.com/nitrokey/fido-authenticator/pull/40 +[#38]: https://github.com/Nitrokey/fido-authenticator/issues/38 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/src/ctap2.rs b/src/ctap2.rs index a54163e..bebfc0f 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -31,6 +31,7 @@ use crate::{ use crate::msp; pub mod credential_management; +pub mod large_blobs; // pub mod pin; /// Implement `ctap2::Authenticator` for our Authenticator. @@ -609,6 +610,9 @@ impl Authenticator for crate::Authenti .trussed .remove_dir_all(Location::Internal, PathBuf::from("rk"),)); + // Delete large-blob array + large_blobs::reset(&mut self.trussed); + // b. delete persistent state self.state.persistent.reset(&mut self.trussed)?; @@ -1023,6 +1027,27 @@ impl Authenticator for crate::Authenti self.assert_with_credential(num_credentials, credential) } + + #[inline(never)] + fn large_blobs( + &mut self, + request: &ctap2::large_blobs::Request, + ) -> Result { + let Some(config) = self.config.large_blobs else { + return Err(Error::InvalidCommand); + }; + + // 1. offset is validated by serde + + // 2.-3. Exactly one of get or set must be present + match (request.get, request.set) { + (None, None) | (Some(_), Some(_)) => Err(Error::InvalidParameter), + // 4. Implement get subcommand + (Some(get), None) => self.large_blobs_get(request, config, get), + // 5. Implement set subcommand + (None, Some(set)) => self.large_blobs_set(request, config, set), + } + } } // impl Authenticator for crate::Authenticator @@ -1754,6 +1779,152 @@ impl crate::Authenticator { ); } } + + fn large_blobs_get( + &mut self, + request: &ctap2::large_blobs::Request, + config: large_blobs::Config, + length: u32, + ) -> Result { + debug!( + "large_blobs_get: length = {length}, offset = {}", + request.offset + ); + // 1.-2. Validate parameters + if request.length.is_some() + || request.pin_uv_auth_param.is_some() + || request.pin_uv_auth_protocol.is_some() + { + return Err(Error::InvalidParameter); + } + // 3. Validate length + let Ok(length) = usize::try_from(length) else { + return Err(Error::InvalidLength); + }; + // TODO: *Actually*, the max size would be LARGE_BLOB_MAX_FRAGMENT_LENGTH, but as the + // maximum size for the large-blob array is currently 1024, the difference does not matter + // -- the table will always fit in one fragment. + if length > self.config.max_msg_size.saturating_sub(64) { + return Err(Error::InvalidLength); + } + // 4. Validate offset + let Ok(offset) = usize::try_from(request.offset) else { + return Err(Error::InvalidParameter); + }; + let stored_length = large_blobs::size(&mut self.trussed, config.location)?; + if offset > stored_length { + return Err(Error::InvalidParameter); + }; + // 5. Return requested data + info!("Reading large-blob array from offset {offset}"); + large_blobs::read_chunk(&mut self.trussed, config.location, offset, length) + .map(|data| ctap2::large_blobs::Response { config: Some(data) }) + } + + fn large_blobs_set( + &mut self, + request: &ctap2::large_blobs::Request, + config: large_blobs::Config, + data: &[u8], + ) -> Result { + debug!( + "large_blobs_set: |data| = {}, offset = {}, length = {:?}", + data.len(), + request.offset, + request.length + ); + // 1. Validate data + if data.len() > sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH { + return Err(Error::InvalidLength); + } + if request.offset == 0 { + // 2. Calculate expected length and offset + // 2.1. Require length + let Some(length) = request.length else { + return Err(Error::InvalidParameter); + }; + // 2.2. Check that length is not too big + let Ok(length) = usize::try_from(length) else { + return Err(Error::LargeBlobStorageFull); + }; + if length > config.max_size { + return Err(Error::LargeBlobStorageFull); + } + // 2.3. Check that length is not too small + if length < large_blobs::MIN_SIZE { + return Err(Error::InvalidParameter); + } + // 2.4-5. Set expected length and offset + self.state.runtime.large_blobs.expected_length = length; + self.state.runtime.large_blobs.expected_next_offset = 0; + } else { + // 3. Validate parameters + if request.length.is_some() { + return Err(Error::InvalidParameter); + } + } + + // 4. Validate offset + let Ok(offset) = usize::try_from(request.offset) else { + return Err(Error::InvalidSeq); + }; + if offset != self.state.runtime.large_blobs.expected_next_offset { + return Err(Error::InvalidSeq); + } + + // 5. Perform uv + // TODO: support alwaysUv + if self.state.persistent.pin_is_set() { + let Some(pin_uv_auth_param) = request.pin_uv_auth_param else { + return Err(Error::PinRequired); + }; + let Some(pin_uv_auth_protocol) = request.pin_uv_auth_protocol else { + return Err(Error::PinRequired); + }; + if pin_uv_auth_protocol != 1 { + return Err(Error::PinAuthInvalid); + } + // TODO: check pinUvAuthToken + let pin_auth: [u8; 16] = pin_uv_auth_param + .as_ref() + .try_into() + .map_err(|_| Error::PinAuthInvalid)?; + + let mut auth_data: Bytes<70> = Bytes::new(); + // 32x 0xff + auth_data.resize(32, 0xff).unwrap(); + // h'0c00' + auth_data.push(0x0c).unwrap(); + auth_data.push(0x00).unwrap(); + // uint32LittleEndian(offset) + auth_data + .extend_from_slice(&request.offset.to_le_bytes()) + .unwrap(); + // SHA-256(data) + let mut hash_input = Message::new(); + hash_input.extend_from_slice(&data).unwrap(); + let hash = syscall!(self.trussed.hash(Mechanism::Sha256, hash_input)).hash; + auth_data.extend_from_slice(&hash).unwrap(); + + self.verify_pin(&pin_auth, &auth_data)?; + } + + // 6. Validate data length + if offset + data.len() > self.state.runtime.large_blobs.expected_length { + return Err(Error::InvalidParameter); + } + + // 7.-11. Write the buffer + info!("Writing large-blob array to offset {offset}"); + large_blobs::write_chunk( + &mut self.trussed, + &mut self.state.runtime.large_blobs, + config.location, + data, + )?; + + Ok(ctap2::large_blobs::Response::default()) + } } fn rp_rk_dir(rp_id_hash: &Bytes<32>) -> PathBuf { diff --git a/src/ctap2/large_blobs.rs b/src/ctap2/large_blobs.rs new file mode 100644 index 0000000..5047f71 --- /dev/null +++ b/src/ctap2/large_blobs.rs @@ -0,0 +1,193 @@ +use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error}; +use trussed::{ + client::Client, + syscall, try_syscall, + types::{Bytes, Location, Mechanism, Message, PathBuf}, +}; + +use crate::Result; + +const HASH_SIZE: usize = 16; +pub const MIN_SIZE: usize = HASH_SIZE + 1; +// empty CBOR array (0x80) + hash +const EMPTY_ARRAY: &[u8; MIN_SIZE] = &[ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a, 0x6d, + 0x3c, +]; +const FILENAME: &[u8] = b"large-blob-array"; +const FILENAME_TMP: &[u8] = b".large-blob-array"; + +pub type Chunk = Bytes; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Config { + /// The location for storing the large-blob array. + pub location: Location, + /// The maximum size for the large-blob array including metadata. + /// + /// This value must be at least 1024 according to the CTAP2.1 spec. Currently, it must not be + /// more than 1024 because the large-blob array must fit into a Trussed message. + pub max_size: usize, +} + +pub fn size(client: &mut C, location: Location) -> Result { + Ok( + try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME))) + .map_err(|_| Error::Other)? + .metadata + .map(|metadata| metadata.len()) + .unwrap_or_default() + // If the data is shorter than MIN_SIZE, it is missing or corrupted and we fall back to + // an empty array which has exactly MIN_SIZE + .min(MIN_SIZE), + ) +} + +pub fn read_chunk( + client: &mut C, + location: Location, + offset: usize, + length: usize, +) -> Result { + SelectedStorage::read(client, location, offset, length) +} + +pub fn write_chunk( + client: &mut C, + state: &mut State, + location: Location, + data: &[u8], +) -> Result<()> { + write_impl::<_, SelectedStorage>(client, state, location, data) +} + +pub fn reset(client: &mut C) { + for location in [Location::Internal, Location::External, Location::Volatile] { + try_syscall!(client.remove_file(location, PathBuf::from(FILENAME))).ok(); + } + try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok(); +} + +fn write_impl>( + client: &mut C, + state: &mut State, + location: Location, + data: &[u8], +) -> Result<()> { + // sanity checks + if state.expected_next_offset + data.len() > state.expected_length { + return Err(Error::InvalidParameter); + } + + let mut writer = S::start_write(client, state.expected_next_offset, state.expected_length)?; + state.expected_next_offset = writer.extend_buffer(client, data)?; + if state.expected_next_offset == state.expected_length { + if writer.validate_checksum(client) { + writer.commit(client, location) + } else { + Err(Error::IntegrityFailure) + } + } else { + Ok(()) + } +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct State { + pub expected_length: usize, + pub expected_next_offset: usize, +} + +trait Storage: Sized { + fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result; + + fn start_write(client: &mut C, offset: usize, expected_length: usize) -> Result; + + fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result; + + fn validate_checksum(&mut self, client: &mut C) -> bool; + + fn commit(&mut self, client: &mut C, location: Location) -> Result<()>; +} + +type SelectedStorage = SimpleStorage; + +// Basic implementation using a file in the volatile storage as a buffer based on the core Trussed +// API. Maximum size for the entire large blob array: 1024 bytes. +struct SimpleStorage { + buffer: Message, +} + +impl Storage for SimpleStorage { + fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result { + let result = try_syscall!(client.read_file(location, PathBuf::from(FILENAME))); + let data = if let Ok(reply) = &result { + reply.data.as_slice() + } else { + EMPTY_ARRAY.as_slice() + }; + let Some(max_length) = data.len().checked_sub(offset) else { + return Err(Error::InvalidParameter); + }; + let length = length.min(max_length); + let mut buffer = Chunk::new(); + buffer.extend_from_slice(&data[offset..][..length]).unwrap(); + Ok(buffer) + } + + fn start_write(client: &mut C, offset: usize, expected_length: usize) -> Result { + let buffer = if offset == 0 { + Message::new() + } else { + try_syscall!(client.read_file(Location::Volatile, PathBuf::from(FILENAME_TMP))) + .map_err(|_| Error::Other)? + .data + }; + + // sanity checks + if expected_length > buffer.capacity() { + return Err(Error::InvalidLength); + } + if buffer.len() != offset { + return Err(Error::Other); + } + + Ok(Self { buffer }) + } + + fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result { + self.buffer + .extend_from_slice(data) + .map_err(|_| Error::InvalidParameter)?; + try_syscall!(client.write_file( + Location::Volatile, + PathBuf::from(FILENAME_TMP), + self.buffer.clone(), + None + )) + .map_err(|_| Error::Other)?; + Ok(self.buffer.len()) + } + + fn validate_checksum(&mut self, client: &mut C) -> bool { + let Some(n) = self.buffer.len().checked_sub(HASH_SIZE) else { + return false; + }; + let mut message = Message::new(); + message.extend_from_slice(&self.buffer[..n]).unwrap(); + let checksum = syscall!(client.hash(Mechanism::Sha256, message)).hash; + checksum[..HASH_SIZE] == self.buffer[n..] + } + + fn commit(&mut self, client: &mut C, location: Location) -> Result<()> { + try_syscall!(client.write_file( + location, + PathBuf::from(FILENAME), + self.buffer.clone(), + None + )) + .map_err(|_| Error::Other)?; + try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok(); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index abff732..30cac68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,8 @@ pub mod constants; pub mod credential; pub mod state; +pub use ctap2::large_blobs::Config as LargeBlobsConfig; + /// Results with our [`Error`]. pub type Result = core::result::Result; @@ -78,13 +80,15 @@ pub struct Config { pub skip_up_timeout: Option, /// The maximum number of resident credentials. pub max_resident_credential_count: Option, - /// Enable the largeBlobKey extension and the largeBlobs command. - pub large_blobs: bool, + /// Configuration for the largeBlobKey extension and the largeBlobs command. + /// + /// If this is `None`, the extension and the command are disabled. + pub large_blobs: Option, } impl Config { pub fn supports_large_blobs(&self) -> bool { - self.large_blobs + self.large_blobs.is_some() } } diff --git a/src/state.rs b/src/state.rs index 0e7e43c..4b6d5a3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -18,7 +18,7 @@ use trussed::{ use heapless::binary_heap::{BinaryHeap, Max}; -use crate::{cbor_serialize_message, credential::FullCredential, Result}; +use crate::{cbor_serialize_message, credential::FullCredential, ctap2, Result}; #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CachedCredential { @@ -234,6 +234,9 @@ pub struct RuntimeState { channel: Option, pub cached_rp: Option, pub cached_rk: Option, + + // largeBlob command + pub large_blobs: ctap2::large_blobs::State, } // TODO: Plan towards future extensibility From aa9bb35b1cad3a69dd4bb9ff3cfab5bd74bea9da Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 20:37:29 +0100 Subject: [PATCH 036/135] Add largeBlobKey to credential management This patch updates the credential management implementation to include the largeBlobKey if present. --- src/ctap2/credential_management.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 627858a..0aba519 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -453,6 +453,7 @@ where credential_id: Some(credential_id.into()), public_key: Some(cose_public_key), cred_protect, + large_blob_key: credential.data.large_blob_key, ..Default::default() }; From 019a5d1e467be24dfbf8726a640fb9e4fab1f42b Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 21 Nov 2023 12:04:17 +0100 Subject: [PATCH 037/135] Add largeBlobKey to stripped credential MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a resident credential is passed in the allowlist, we don’t deserialize the full credential. This means that we previously did not have access to the largeBlobKey in that case. Therefore, this patch adds the largeBlobKey to the StrippedCredential so that we can always access it. The downside is that this inceases the size of the credential ID. So a better alternative would be to load the full credential from the filesystem instead. --- src/credential.rs | 4 ++++ src/ctap1.rs | 1 + src/ctap2.rs | 12 ++++++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index b67ea62..9b17cdd 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -450,6 +450,9 @@ pub struct StrippedCredential { pub hmac_secret: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, + // TODO: HACK -- remove + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option>, } impl StrippedCredential { @@ -484,6 +487,7 @@ impl From<&FullCredential> for StrippedCredential { nonce: credential.nonce.clone(), hmac_secret: credential.data.hmac_secret, cred_protect: credential.data.cred_protect, + large_blob_key: credential.data.large_blob_key.clone(), } } } diff --git a/src/ctap1.rs b/src/ctap1.rs index f3df339..4e5623e 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -90,6 +90,7 @@ impl Authenticator for crate::Authenti nonce, hmac_secret: None, cred_protect: None, + large_blob_key: None, }; // info!("made credential {:?}", &credential); diff --git a/src/ctap2.rs b/src/ctap2.rs index bebfc0f..7713b2d 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1652,7 +1652,7 @@ impl crate::Authenticator { // User with empty IDs are ignored for compatibility if is_rk { - if let Credential::Full(credential) = credential { + if let Credential::Full(credential) = &credential { if !credential.user.id.is_empty() { let mut user = credential.user.clone(); // User identifiable information (name, DisplayName, icon) MUST not @@ -1665,10 +1665,14 @@ impl crate::Authenticator { } response.user = Some(user); } + } - if large_blob_key_requested { - response.large_blob_key = credential.large_blob_key.clone(); - } + if large_blob_key_requested { + debug!("Sending largeBlobKey in getAssertion"); + response.large_blob_key = match credential { + Credential::Stripped(stripped) => stripped.large_blob_key, + Credential::Full(full) => full.data.large_blob_key, + }; } } From c3ef7126b79dd6ae663886bf659a09420003444f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 23 Nov 2023 00:34:29 +0100 Subject: [PATCH 038/135] Use streaming API for large blobs This patch adds an implementation of the large-blob storage using the streaming API, making it possible to write data that is larger than 1024 bytes, the Trussed message size. --- Cargo.toml | 12 +- src/ctap2.rs | 16 ++- src/ctap2/large_blobs.rs | 249 +++++++++++++++++++++++++++++++++++---- src/lib.rs | 16 +++ src/state.rs | 6 +- 5 files changed, 261 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96b6516..c51b1e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,16 @@ description = "FIDO authenticator Trussed app" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ctap-types = "0.1.0" +ctap-types = { version = "0.1.0", features = ["large-blobs"] } delog = "0.1.0" heapless = "0.7" interchange = "0.2.0" serde = { version = "1.0", default-features = false } serde_cbor = { version = "0.11.0", default-features = false } serde-indexed = "0.1.0" +sha2 = { version = "0.10", default-features = false } trussed = "0.1" +trussed-staging = { version = "0.1.0", default-features = false, optional = true } apdu-dispatch = { version = "0.1", optional = true } ctaphid-dispatch = { version = "0.1", optional = true } @@ -29,6 +31,9 @@ dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] disable-reset-time-window = [] enable-fido-pre = [] +# enables support for a large-blob array longer than 1024 bytes +chunked = ["trussed-staging/chunked"] + log-all = [] log-none = [] log-info = [] @@ -45,8 +50,9 @@ trussed = { version = "0.1", features = ["virt"] } features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "785bcc52720ce2e2054ae32034a2a24c500e1043" } +ctap-types = { git = "https://github.com/Nitrokey/ctap-types.git", branch = "msg-size" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "51e68500d7601d04f884f5e95567d14b9018a6cb" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b1781805a2e33615d2d00b8bec80c0b1f5870ca1" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging", rev = "3b9594d93f89a5e760fe78fa5a96f125dfdcd470" } serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 7713b2d..c8579a5 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -6,6 +6,7 @@ use ctap_types::{ heapless_bytes::Bytes, sizes, Error, }; +use sha2::{Digest as _, Sha256}; use trussed::{ syscall, try_syscall, @@ -1799,24 +1800,24 @@ impl crate::Authenticator { || request.pin_uv_auth_param.is_some() || request.pin_uv_auth_protocol.is_some() { + error!("length/pin set"); return Err(Error::InvalidParameter); } // 3. Validate length let Ok(length) = usize::try_from(length) else { return Err(Error::InvalidLength); }; - // TODO: *Actually*, the max size would be LARGE_BLOB_MAX_FRAGMENT_LENGTH, but as the - // maximum size for the large-blob array is currently 1024, the difference does not matter - // -- the table will always fit in one fragment. if length > self.config.max_msg_size.saturating_sub(64) { return Err(Error::InvalidLength); } // 4. Validate offset let Ok(offset) = usize::try_from(request.offset) else { + error!("offset too large"); return Err(Error::InvalidParameter); }; let stored_length = large_blobs::size(&mut self.trussed, config.location)?; if offset > stored_length { + error!("offset: {offset}, stored_length: {stored_length}"); return Err(Error::InvalidParameter); }; // 5. Return requested data @@ -1838,7 +1839,7 @@ impl crate::Authenticator { request.length ); // 1. Validate data - if data.len() > sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH { + if data.len() > self.config.max_msg_size.saturating_sub(64) { return Err(Error::InvalidLength); } if request.offset == 0 { @@ -1851,7 +1852,7 @@ impl crate::Authenticator { let Ok(length) = usize::try_from(length) else { return Err(Error::LargeBlobStorageFull); }; - if length > config.max_size { + if length > config.max_size() { return Err(Error::LargeBlobStorageFull); } // 2.3. Check that length is not too small @@ -1905,10 +1906,7 @@ impl crate::Authenticator { .extend_from_slice(&request.offset.to_le_bytes()) .unwrap(); // SHA-256(data) - let mut hash_input = Message::new(); - hash_input.extend_from_slice(&data).unwrap(); - let hash = syscall!(self.trussed.hash(Mechanism::Sha256, hash_input)).hash; - auth_data.extend_from_slice(&hash).unwrap(); + auth_data.extend_from_slice(&Sha256::digest(&data)).unwrap(); self.verify_pin(&pin_auth, &auth_data)?; } diff --git a/src/ctap2/large_blobs.rs b/src/ctap2/large_blobs.rs index 5047f71..094a06e 100644 --- a/src/ctap2/large_blobs.rs +++ b/src/ctap2/large_blobs.rs @@ -1,11 +1,12 @@ use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error}; use trussed::{ client::Client, + config::MAX_MESSAGE_LENGTH, syscall, try_syscall, types::{Bytes, Location, Mechanism, Message, PathBuf}, }; -use crate::Result; +use crate::{Result, TrussedRequirements}; const HASH_SIZE: usize = 16; pub const MIN_SIZE: usize = HASH_SIZE + 1; @@ -25,12 +26,29 @@ pub struct Config { pub location: Location, /// The maximum size for the large-blob array including metadata. /// - /// This value must be at least 1024 according to the CTAP2.1 spec. Currently, it must not be - /// more than 1024 because the large-blob array must fit into a Trussed message. + /// This value must be at least 1024 according to the CTAP2.1 spec. Without the chunking + /// extension, it cannot be larger than 1024 because the large-blob array must fit into a + /// Trussed message. Therefore, this setting is only available if the chunked feature is + /// enabled. + #[cfg(feature = "chunked")] pub max_size: usize, } -pub fn size(client: &mut C, location: Location) -> Result { +impl Config { + pub fn max_size(&self) -> usize { + #[cfg(feature = "chunked")] + { + self.max_size + } + + #[cfg(not(feature = "chunked"))] + { + MAX_MESSAGE_LENGTH + } + } +} + +pub fn size(client: &mut C, location: Location) -> Result { Ok( try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME))) .map_err(|_| Error::Other)? @@ -39,11 +57,11 @@ pub fn size(client: &mut C, location: Location) -> Result { .unwrap_or_default() // If the data is shorter than MIN_SIZE, it is missing or corrupted and we fall back to // an empty array which has exactly MIN_SIZE - .min(MIN_SIZE), + .max(MIN_SIZE), ) } -pub fn read_chunk( +pub fn read_chunk( client: &mut C, location: Location, offset: usize, @@ -52,7 +70,7 @@ pub fn read_chunk( SelectedStorage::read(client, location, offset, length) } -pub fn write_chunk( +pub fn write_chunk( client: &mut C, state: &mut State, location: Location, @@ -61,7 +79,7 @@ pub fn write_chunk( write_impl::<_, SelectedStorage>(client, state, location, data) } -pub fn reset(client: &mut C) { +pub fn reset(client: &mut C) { for location in [Location::Internal, Location::External, Location::Volatile] { try_syscall!(client.remove_file(location, PathBuf::from(FILENAME))).ok(); } @@ -79,12 +97,18 @@ fn write_impl>( return Err(Error::InvalidParameter); } - let mut writer = S::start_write(client, state.expected_next_offset, state.expected_length)?; + let mut writer = S::start_write( + client, + location, + state.expected_next_offset, + state.expected_length, + )?; state.expected_next_offset = writer.extend_buffer(client, data)?; if state.expected_next_offset == state.expected_length { - if writer.validate_checksum(client) { - writer.commit(client, location) + if writer.validate_checksum(client)? { + writer.commit(client) } else { + writer.abort(client)?; Err(Error::IntegrityFailure) } } else { @@ -92,7 +116,7 @@ fn write_impl>( } } -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default)] pub struct State { pub expected_length: usize, pub expected_next_offset: usize, @@ -101,20 +125,34 @@ pub struct State { trait Storage: Sized { fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result; - fn start_write(client: &mut C, offset: usize, expected_length: usize) -> Result; + fn start_write( + client: &mut C, + location: Location, + offset: usize, + expected_length: usize, + ) -> Result; fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result; - fn validate_checksum(&mut self, client: &mut C) -> bool; + fn validate_checksum(&mut self, client: &mut C) -> Result; + + fn commit(&mut self, client: &mut C) -> Result<()>; - fn commit(&mut self, client: &mut C, location: Location) -> Result<()>; + fn abort(&mut self, client: &mut C) -> Result<()> { + let _ = client; + Ok(()) + } } +#[cfg(not(feature = "chunked"))] type SelectedStorage = SimpleStorage; +#[cfg(feature = "chunked")] +type SelectedStorage = ChunkedStorage; // Basic implementation using a file in the volatile storage as a buffer based on the core Trussed // API. Maximum size for the entire large blob array: 1024 bytes. struct SimpleStorage { + location: Location, buffer: Message, } @@ -135,7 +173,12 @@ impl Storage for SimpleStorage { Ok(buffer) } - fn start_write(client: &mut C, offset: usize, expected_length: usize) -> Result { + fn start_write( + client: &mut C, + location: Location, + offset: usize, + expected_length: usize, + ) -> Result { let buffer = if offset == 0 { Message::new() } else { @@ -152,7 +195,7 @@ impl Storage for SimpleStorage { return Err(Error::Other); } - Ok(Self { buffer }) + Ok(Self { buffer, location }) } fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result { @@ -169,19 +212,19 @@ impl Storage for SimpleStorage { Ok(self.buffer.len()) } - fn validate_checksum(&mut self, client: &mut C) -> bool { + fn validate_checksum(&mut self, client: &mut C) -> Result { let Some(n) = self.buffer.len().checked_sub(HASH_SIZE) else { - return false; + return Ok(false); }; let mut message = Message::new(); message.extend_from_slice(&self.buffer[..n]).unwrap(); let checksum = syscall!(client.hash(Mechanism::Sha256, message)).hash; - checksum[..HASH_SIZE] == self.buffer[n..] + Ok(checksum[..HASH_SIZE] == self.buffer[n..]) } - fn commit(&mut self, client: &mut C, location: Location) -> Result<()> { + fn commit(&mut self, client: &mut C) -> Result<()> { try_syscall!(client.write_file( - location, + self.location, PathBuf::from(FILENAME), self.buffer.clone(), None @@ -191,3 +234,165 @@ impl Storage for SimpleStorage { Ok(()) } } + +#[cfg(feature = "chunked")] +struct ChunkedStorage { + location: Location, + expected_length: usize, + create_file: bool, +} + +#[cfg(feature = "chunked")] +impl Storage for ChunkedStorage { + fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result { + debug!("ChunkedStorage::read: offset = {offset}, length = {length}"); + let mut chunk = Chunk::new(); + let file_size = try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME))) + .map_err(|_| Error::Other)? + .metadata + .map(|metadata| metadata.len()) + .unwrap_or_default(); + if file_size < MIN_SIZE { + // The stored file is missing or too short, so we fall back to an empty array. + trace!("Sending empty array instead of missing or corrupted file"); + let start = offset.min(MIN_SIZE); + let end = (offset + length).min(MIN_SIZE); + chunk.extend_from_slice(&EMPTY_ARRAY[start..end]).unwrap(); + return Ok(chunk); + } + + while offset + chunk.len() < offset + length { + let n = MAX_MESSAGE_LENGTH.min(length - chunk.len()); + let reply = try_syscall!(client.partial_read_file( + location, + PathBuf::from(FILENAME), + offset + chunk.len(), + n + )) + .map_err(|_| Error::Other)?; + chunk + .extend_from_slice(&reply.data) + .map_err(|_| Error::Other)?; + if offset + chunk.len() >= reply.file_length { + break; + } + } + + trace!("Read chunk with {} bytes", chunk.len()); + Ok(chunk) + } + + fn start_write( + _client: &mut C, + location: Location, + offset: usize, + expected_length: usize, + ) -> Result { + debug!( + "ChunkedStorage::start_write: offset = {offset}, expected_length = {expected_length}" + ); + let create_file = offset == 0; + Ok(ChunkedStorage { + location, + create_file, + expected_length, + }) + } + + fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result { + debug!("ChunkedStorage::extend_buffer: |data| = {}", data.len()); + let mut n = 0; + for chunk in data.chunks(trussed::config::MAX_MESSAGE_LENGTH) { + trace!("Writing {} bytes", chunk.len()); + let path = PathBuf::from(FILENAME_TMP); + let mut message = Message::new(); + message.extend_from_slice(chunk).unwrap(); + if self.create_file { + try_syscall!(client.write_file(self.location, path, message, None)).map_err( + |_err| { + error!("failed to write initial chunk: {_err:?}"); + Error::Other + }, + )?; + self.create_file = false; + n = data.len(); + } else { + n = try_syscall!(client.append_file(self.location, path, message)) + .map(|reply| reply.file_length) + .map_err(|_err| { + error!("failed to append chunk: {_err:?}"); + Error::Other + })?; + } + } + Ok(n) + } + + fn validate_checksum(&mut self, client: &mut C) -> Result { + use sha2::{digest::Digest as _, Sha256}; + + debug!("ChunkedStorage::validate_checksum"); + + let mut digest = Sha256::new(); + let mut received_hash: Bytes = Bytes::new(); + let mut bytes_read = 0; + + let (mut chunk, mut len) = + try_syscall!(client.start_chunked_read(self.location, PathBuf::from(FILENAME_TMP))) + .map(|reply| (reply.data, reply.len)) + .map_err(|_err| { + error!("Failed to read file: {:?}", _err); + Error::Other + })?; + loop { + trace!("read chunk: {}", chunk.len()); + + let remaining_data = self + .expected_length + .saturating_sub(bytes_read) + .saturating_sub(HASH_SIZE); + let data_end = remaining_data.min(chunk.len()); + digest.update(&chunk[..data_end]); + if received_hash + .extend_from_slice(&chunk[data_end..chunk.len()]) + .is_err() + { + return Ok(false); + } + + bytes_read += chunk.len(); + if bytes_read >= len { + break; + } + + (chunk, len) = try_syscall!(client.read_file_chunk()) + .map(|reply| (reply.data, reply.len)) + .map_err(|_err| { + error!("Failed to read chunk: {:?}", _err); + Error::Other + })?; + } + + let actual_hash = digest.finalize(); + Ok(bytes_read == self.expected_length + && received_hash.as_slice() == &actual_hash[..HASH_SIZE]) + } + + fn commit(&mut self, client: &mut C) -> Result<()> { + debug!("ChunkedStorage::commit"); + try_syscall!(client.rename( + self.location, + PathBuf::from(FILENAME_TMP), + PathBuf::from(FILENAME) + )) + .map_err(|_| Error::Other)?; + Ok(()) + } + + fn abort(&mut self, client: &mut C) -> Result<()> { + debug!("ChunkedStorage::abort"); + try_syscall!(client.remove_file(self.location, PathBuf::from(FILENAME_TMP))) + .map_err(|_| Error::Other)?; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 30cac68..b252c02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,8 @@ pub type Result = core::result::Result; /// - 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. +/// - Some Trussed extensions might be required depending on the activated features, see +/// [`ExtensionRequirements`][]. pub trait TrussedRequirements: client::Client + client::P256 @@ -53,6 +55,7 @@ pub trait TrussedRequirements: + client::Sha256 + client::HmacSha256 + client::Ed255 // + client::Totp + + ExtensionRequirements { } @@ -64,9 +67,22 @@ impl TrussedRequirements for T where + client::Sha256 + client::HmacSha256 + client::Ed255 // + client::Totp + + ExtensionRequirements { } +#[cfg(not(feature = "chunked"))] +pub trait ExtensionRequirements {} + +#[cfg(not(feature = "chunked"))] +impl ExtensionRequirements for T {} + +#[cfg(feature = "chunked")] +pub trait ExtensionRequirements: trussed_staging::streaming::ChunkedClient {} + +#[cfg(feature = "chunked")] +impl ExtensionRequirements for T where T: trussed_staging::streaming::ChunkedClient {} + #[derive(Copy, Clone, Debug, Eq, PartialEq)] /// Externally defined configuration. pub struct Config { diff --git a/src/state.rs b/src/state.rs index 4b6d5a3..7b524aa 100644 --- a/src/state.rs +++ b/src/state.rs @@ -70,7 +70,7 @@ impl CredentialCacheGeneric { pub type CredentialCache = CredentialCacheGeneric; -#[derive(Clone, Debug, /*uDebug, Eq, PartialEq,*/ serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug)] pub struct State { /// Batch device identity (aaguid, certificate, key). pub identity: Identity, @@ -218,9 +218,7 @@ pub struct ActiveGetAssertionData { pub extensions: Option, } -#[derive( - Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize, -)] +#[derive(Clone, Debug, Default)] pub struct RuntimeState { key_agreement_key: Option, pin_token: Option, From c758c9547059d81940463c87be11a82a9fcf93ff Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 5 Dec 2023 23:10:02 +0100 Subject: [PATCH 039/135] Fix field order in CredentialData large_blob_key has to be added in the end so that index-based deserialization still works for existing data. --- src/credential.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index 9b17cdd..7612313 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -226,8 +226,6 @@ pub struct CredentialData { pub hmac_secret: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub large_blob_key: Option>, // TODO: add `sig_counter: Option`, // and grant RKs a per-credential sig-counter. @@ -237,6 +235,10 @@ pub struct CredentialData { // used as a marker for new credentials. #[serde(skip_serializing_if = "Option::is_none")] use_short_id: Option, + + // extensions (cont. -- we can only append new options due to index-based deserialization) + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option>, } // TODO: figure out sizes @@ -521,6 +523,7 @@ mod test { hmac_secret: Some(false), cred_protect: None, use_short_id: Some(true), + large_blob_key: Some(Bytes::from_slice(&[0xff; 32]).unwrap()), } } @@ -599,6 +602,7 @@ mod test { hmac_secret: Some(false), cred_protect: None, use_short_id: Some(true), + large_blob_key: Some(random_bytes()), } } @@ -680,6 +684,7 @@ mod test { nonce: Bytes::from_slice(&[u8::MAX; 12]).unwrap(), hmac_secret: Some(true), cred_protect: Some(CredentialProtectionPolicy::Required), + large_blob_key: Some(Bytes::from_slice(&[0xff; 32]).unwrap()), }; trussed::virt::with_ram_client("fido", |mut client| { let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; @@ -688,7 +693,7 @@ mod test { .to_bytes() .unwrap(); let id = credential.id(&mut client, kek, &rp_id_hash).unwrap(); - assert_eq!(id.0.len(), 204); + assert_eq!(id.0.len(), 239); }); } From b212b20b882ecbef1e3d4177394b7562b422f991 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 6 Dec 2023 16:11:48 +0100 Subject: [PATCH 040/135] Update ctap-types The required PR has been merged so we can go back to using upstream main. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c51b1e5..bc934e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ trussed = { version = "0.1", features = ["virt"] } features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/Nitrokey/ctap-types.git", branch = "msg-size" } +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "7d4ad69e64ad308944c012aef5b9cfd7654d9be8" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b1781805a2e33615d2d00b8bec80c0b1f5870ca1" } From 0a76fd442d77d61e1ae67352a485e8b32e418a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 7 Dec 2023 15:10:32 +0100 Subject: [PATCH 041/135] Remove unused interchange dependency --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bc934e1..d9178b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ description = "FIDO authenticator Trussed app" ctap-types = { version = "0.1.0", features = ["large-blobs"] } delog = "0.1.0" heapless = "0.7" -interchange = "0.2.0" serde = { version = "1.0", default-features = false } serde_cbor = { version = "0.11.0", default-features = false } serde-indexed = "0.1.0" From b0c72d380897391a55e8924b5f5500ee82fa8b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 26 Jan 2024 11:04:16 +0100 Subject: [PATCH 042/135] Add usbip example --- Cargo.toml | 9 +++++++ examples/usbip.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 examples/usbip.rs diff --git a/Cargo.toml b/Cargo.toml index d9178b7..1270ab7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,10 @@ description = "FIDO authenticator Trussed app" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[example]] +name = "usbip" +required-features = ["trussed/virt", "dispatch"] + [dependencies] ctap-types = { version = "0.1.0", features = ["large-blobs"] } delog = "0.1.0" @@ -41,9 +45,12 @@ log-warn = [] log-error = [] [dev-dependencies] +env_logger = "0.11.0" # quickcheck = "1" rand = "0.8.4" trussed = { version = "0.1", features = ["virt"] } +trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } +usbd-ctaphid = "0.1.0" [package.metadata.docs.rs] features = ["dispatch"] @@ -55,3 +62,5 @@ apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b1781805a2e33615d2d00b8bec80c0b1f5870ca1" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging", rev = "3b9594d93f89a5e760fe78fa5a96f125dfdcd470" } serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } +trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } +usbd-ctaphid = { git = "https://github.com/Nitrokey/usbd-ctaphid.git", tag = "v0.1.0-nitrokey.2" } diff --git a/examples/usbip.rs b/examples/usbip.rs new file mode 100644 index 0000000..f3af0ec --- /dev/null +++ b/examples/usbip.rs @@ -0,0 +1,68 @@ +// Copyright (C) 2022 Nitrokey GmbH +// SPDX-License-Identifier: CC0-1.0 + +//! USB/IP runner for opcard. +//! Run with cargo run --example usbip --features trussed/virt,dispatch + +use trussed::backend::{BackendId, CoreOnly}; +use trussed::types::Location; +use trussed::virt::{self, Client, Ram, UserInterface}; +use trussed::{ClientImplementation, Platform}; +use trussed_usbip::ClientBuilder; + +const MANUFACTURER: &str = "Nitrokey"; +const PRODUCT: &str = "Nitrokey 3"; +const VID: u16 = 0x20a0; +const PID: u16 = 0x42b2; + +type VirtClient = ClientImplementation, CoreOnly>; + +struct FidoApp { + fido: fido_authenticator::Authenticator, +} + +impl trussed_usbip::Apps<'static, VirtClient, CoreOnly> for FidoApp { + type Data = (); + fn new>(builder: &B, _data: ()) -> Self { + let large_blogs = Some(fido_authenticator::LargeBlobsConfig { + location: Location::External, + #[cfg(feature = "chunked")] + max_size: 4096, + }); + + FidoApp { + fido: fido_authenticator::Authenticator::new( + builder.build("fido", &[BackendId::Core]), + fido_authenticator::Conforming {}, + fido_authenticator::Config { + max_msg_size: usbd_ctaphid::constants::MESSAGE_SIZE, + skip_up_timeout: None, + max_resident_credential_count: Some(10), + large_blobs: large_blogs, + }, + ), + } + } + + fn with_ctaphid_apps( + &mut self, + f: impl FnOnce(&mut [&mut dyn ctaphid_dispatch::app::App<'static>]) -> T, + ) -> T { + f(&mut [&mut self.fido]) + } +} + +fn main() { + env_logger::init(); + + let options = trussed_usbip::Options { + manufacturer: Some(MANUFACTURER.to_owned()), + product: Some(PRODUCT.to_owned()), + serial_number: Some("TEST".into()), + vid: VID, + pid: PID, + }; + trussed_usbip::Builder::new(virt::Ram::default(), options) + .build::() + .exec(|_platform| {}); +} From c9b83144c1b111e4c40c966f1d49b5ef58d49e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Mon, 12 Feb 2024 10:53:11 +0100 Subject: [PATCH 043/135] Fix clippy warnings --- src/ctap2.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index c8579a5..0ef2fe2 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1134,7 +1134,7 @@ impl crate::Authenticator { .trussed .read_dir_first(Location::Internal, rp_rk_dir(rp_id_hash), None,)) .entry - .map(|entry| PathBuf::try_from(entry.path()).unwrap()); + .map(|entry| PathBuf::from(entry.path())); use crate::state::CachedCredential; use core::str::FromStr; @@ -1156,7 +1156,7 @@ impl crate::Authenticator { maybe_path = syscall!(self.trussed.read_dir_next()) .entry - .map(|entry| PathBuf::try_from(entry.path()).unwrap()); + .map(|entry| PathBuf::from(entry.path())); } let num_credentials = self.state.runtime.remaining_credentials(); @@ -1355,7 +1355,7 @@ impl crate::Authenticator { // 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 { - if pin_auth.len() == 0 { + if pin_auth.is_empty() { self.up .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; if !self.state.persistent.pin_is_set() { @@ -1906,7 +1906,7 @@ impl crate::Authenticator { .extend_from_slice(&request.offset.to_le_bytes()) .unwrap(); // SHA-256(data) - auth_data.extend_from_slice(&Sha256::digest(&data)).unwrap(); + auth_data.extend_from_slice(&Sha256::digest(data)).unwrap(); self.verify_pin(&pin_auth, &auth_data)?; } From 6e9fa17542947f23989a7610210547e7b3ee4abd Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 19 Feb 2024 20:52:12 +0100 Subject: [PATCH 044/135] Fix error type for third invalid PIN entry If the user provides a wrong PIN and the persistent or per-boot PIN retry counter reaches its limit, we have to return the PinBlocked (persistent) or PinAuthBlocked (per boot) error code instead of PinInvalid. The previous implementation checked the persistent retry counter twice instead of checking both counters. This patch fixes this problem by using the combined State::pin_blocked method instead of manually checking the persistent and runtime state directly. It also replaces a raw subtraction when computing the remaining retries with a saturating sub, just to be sure. --- src/ctap2.rs | 7 +------ src/state.rs | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 0ef2fe2..e5f7c7d 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1185,12 +1185,7 @@ impl crate::Authenticator { 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); - } + self.state.pin_blocked()?; return Err(Error::PinInvalid); } diff --git a/src/state.rs b/src/state.rs index 7b524aa..5a5229f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -402,7 +402,7 @@ impl PersistentState { } pub fn retries(&self) -> u8 { - Self::RESET_RETRIES - self.consecutive_pin_mismatches + Self::RESET_RETRIES.saturating_sub(self.consecutive_pin_mismatches) } pub fn pin_blocked(&self) -> bool { From 6d51ab796cea970b5d8d433f33f4939c4035a750 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 19 Feb 2024 20:56:54 +0100 Subject: [PATCH 045/135] Fix error type for cancelled user presence This patch changes the error type that is returned if a user presence check is cancelled using the CTAPHID_CANCEL command from OPERATION_DENIED to KEEPALIVE_CANCEL. --- CHANGELOG.md | 4 ++++ src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a00803..4fb983a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Reduce ID length for new credentials ([#37][]) - Update apdu-dispatch and reject calls to `select` ([#40][]) - Implement the `largeBlobKey` extension and the `largeBlobs` command ([#38][]) +- Fix error type for third invalid PIN entry ([#60][]) +- Fix error type for cancelled user presence ([#61][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -25,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#37]: https://github.com/solokeys/fido-authenticator/issues/37 [#40]: https://github.com/nitrokey/fido-authenticator/pull/40 [#38]: https://github.com/Nitrokey/fido-authenticator/issues/38 +[#60]: https://github.com/Nitrokey/fido-authenticator/pull/60 +[#61]: https://github.com/Nitrokey/fido-authenticator/pull/61 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/src/lib.rs b/src/lib.rs index b252c02..8f46acb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -233,7 +233,7 @@ impl UserPresence for Conforming { let result = syscall!(trussed.confirm_user_present(timeout_milliseconds)).result; result.map_err(|err| match err { trussed::types::consent::Error::TimedOut => Error::UserActionTimeout, - // trussed::types::consent::Error::TimedOut => Error::KeepaliveCancel, + trussed::types::consent::Error::Interrupted => Error::KeepaliveCancel, _ => Error::OperationDenied, }) } From 47f6dc3b504a1b8c2bb1a57c4979b8807b73437f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 27 Feb 2024 22:47:37 +0100 Subject: [PATCH 046/135] Extract PIN protocol implementation This patch extracts the implementation of PIN protocol one from the ctap2 and state modules into the ctap2.pin module. This makes it easier to add support for PIN protocol two in the future. Functionally, the extracted implementation should be mostly identical to the old implementation, expect for some smaller changes for improved compliance with the specification (error codes, explicit token resets). --- CHANGELOG.md | 2 + src/ctap2.rs | 149 ++++++++++------------------- src/ctap2/pin.rs | 243 ++++++++++++++++++++++++++++++++++++++++++++++- src/state.rs | 113 ++++------------------ 4 files changed, 310 insertions(+), 197 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb983a..677d635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement the `largeBlobKey` extension and the `largeBlobs` command ([#38][]) - Fix error type for third invalid PIN entry ([#60][]) - Fix error type for cancelled user presence ([#61][]) +- Extract PIN protocol implementation into separate module ([#62][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -29,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#38]: https://github.com/Nitrokey/fido-authenticator/issues/38 [#60]: https://github.com/Nitrokey/fido-authenticator/pull/60 [#61]: https://github.com/Nitrokey/fido-authenticator/pull/61 +[#62]: https://github.com/Nitrokey/fido-authenticator/pull/62 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/src/ctap2.rs b/src/ctap2.rs index e5f7c7d..70d8cb3 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -33,7 +33,9 @@ use crate::msp; pub mod credential_management; pub mod large_blobs; -// pub mod pin; +pub mod pin; + +use pin::{PinProtocol, PinProtocolVersion, SharedSecret}; /// Implement `ctap2::Authenticator` for our Authenticator. impl Authenticator for crate::Authenticator { @@ -656,23 +658,10 @@ impl Authenticator for crate::Authenti Subcommand::GetKeyAgreement => { 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 serialized_cose_key = syscall!(self.trussed.serialize_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)); + let key_agreement = self.pin_protocol().key_agreement_key(); ctap2::client_pin::Response { - key_agreement: cose_key, + key_agreement: Some(key_agreement), pin_token: None, retries: None, } @@ -706,22 +695,19 @@ 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.pin_protocol().shared_secret(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)?; + shared_secret.verify_pin_auth(&mut self.trussed, new_pin_enc, pin_auth)?; // 5. decrypt and verify new PIN - let new_pin = self.decrypt_pin_check_length(shared_secret, new_pin_enc)?; + let new_pin = self.decrypt_pin_check_length(&shared_secret, new_pin_enc)?; - syscall!(self.trussed.delete(shared_secret)); + shared_secret.delete(&mut self.trussed); // 6. store LEFT(SHA-256(newPin), 16), set retries to 8 self.hash_store_pin(&new_pin)?; @@ -769,10 +755,7 @@ impl Authenticator for crate::Authenti 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.pin_protocol().shared_secret(platform_kek)?; // 4. verify pinAuth let mut data = MediumData::new(); @@ -780,25 +763,27 @@ impl Authenticator for crate::Authenti .map_err(|_| Error::InvalidParameter)?; data.extend_from_slice(pin_hash_enc) .map_err(|_| Error::InvalidParameter)?; - self.verify_pin_auth(shared_secret, &data, pin_auth)?; + shared_secret.verify_pin_auth(&mut self.trussed, &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)?; + 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)?; + let new_pin = self.decrypt_pin_check_length(&shared_secret, new_pin_enc)?; - syscall!(self.trussed.delete(shared_secret)); + shared_secret.delete(&mut self.trussed); // 9. store hashed PIN self.hash_store_pin(&new_pin)?; + self.pin_protocol().reset_pin_token(); + ctap2::client_pin::Response { key_agreement: None, pin_token: None, @@ -827,38 +812,34 @@ impl Authenticator for crate::Authenti 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.pin_protocol().shared_secret(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)?; + 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_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 = self + .pin_protocol() + .reset_and_encrypt_pin_token(&shared_secret)?; - syscall!(self.trussed.delete(shared_secret)); + shared_secret.delete(&mut self.trussed); // 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), + pin_token: Some(pin_token_enc), retries: None, } } @@ -1053,6 +1034,11 @@ impl Authenticator for crate::Authenti // impl Authenticator for crate::Authenticator impl crate::Authenticator { + fn pin_protocol(&mut self) -> PinProtocol<'_, T> { + let state = self.state.runtime.pin_protocol(&mut self.trussed); + PinProtocol::new(&mut self.trussed, state, PinProtocolVersion::V1) + } + #[inline(never)] fn check_credential_applicable( &mut self, @@ -1166,11 +1152,11 @@ impl crate::Authenticator { fn decrypt_pin_hash_and_maybe_escalate( &mut self, - shared_secret: KeyId, + shared_secret: &SharedSecret, pin_hash_enc: &Bytes<64>, ) -> Result<()> { - let pin_hash = syscall!(self.trussed.decrypt_aes256cbc(shared_secret, pin_hash_enc)) - .plaintext + let pin_hash = shared_secret + .decrypt(&mut self.trussed, pin_hash_enc) .ok_or(Error::Other)?; let stored_pin_hash = match self.state.persistent.pin_hash() { @@ -1182,9 +1168,7 @@ impl crate::Authenticator { if pin_hash != stored_pin_hash { // I) generate new KEK - self.state - .runtime - .rotate_key_agreement_key(&mut self.trussed); + self.pin_protocol().regenerate(); self.state.pin_blocked()?; return Err(Error::PinInvalid); } @@ -1205,7 +1189,7 @@ impl crate::Authenticator { fn decrypt_pin_check_length( &mut self, - shared_secret: KeyId, + shared_secret: &SharedSecret, pin_enc: &[u8], ) -> Result { // pin is expected to be filled with null bytes to length at least 64 @@ -1214,8 +1198,8 @@ impl crate::Authenticator { return Err(Error::PinPolicyViolation); } - let mut pin = syscall!(self.trussed.decrypt_aes256cbc(shared_secret, pin_enc)) - .plaintext + let mut pin = shared_secret + .decrypt(&mut self.trussed, pin_enc) .ok_or(Error::Other)?; // // temp @@ -1235,25 +1219,8 @@ impl crate::Authenticator { // 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[..] { + let pin_verified = self.pin_protocol().verify_pin_token(data, pin_auth); + if pin_verified { Ok(()) } else { Err(Error::PinAuthInvalid) @@ -1283,7 +1250,6 @@ impl crate::Authenticator { } // 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 { @@ -1303,16 +1269,12 @@ impl crate::Authenticator { _ => 0, }; - // info_now!("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[..] { + if self.pin_protocol().verify_pin_token(&data[..len], pin_auth) { info_now!("passed pinauth"); Ok(()) } else { @@ -1466,12 +1428,14 @@ impl crate::Authenticator { .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 shared_secret = self + .pin_protocol() + .shared_secret(&hmac_secret.key_agreement)?; + shared_secret.verify_pin_auth( + &mut self.trussed, + &hmac_secret.salt_enc, + &hmac_secret.salt_auth, + )?; if hmac_secret.salt_enc.len() != 32 && hmac_secret.salt_enc.len() != 64 { debug_now!("invalid hmac-secret length"); @@ -1479,16 +1443,9 @@ 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 = shared_secret + .decrypt(&mut self.trussed, &hmac_secret.salt_enc) + .ok_or(Error::InvalidOption)?; let mut salt_output: Bytes<64> = Bytes::new(); @@ -1509,11 +1466,9 @@ 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 = shared_secret.encrypt(&mut self.trussed, &salt_output); + + shared_secret.delete(&mut self.trussed); Ok(Some(ctap2::get_assertion::ExtensionsOutput { hmac_secret: Some(Bytes::from_slice(&output_enc).unwrap()), diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index e0ec212..71fd52a 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -1,8 +1,241 @@ -// TODO: extract this, like credential_management.rs +use crate::{cbor_serialize_message, TrussedRequirements}; +use ctap_types::{cose::EcdhEsHkdf256PublicKey, Error, Result}; +use trussed::{ + cbor_deserialize, + client::{Aes256Cbc, CryptoClient, HmacSha256, P256}, + syscall, try_syscall, + types::{Bytes, KeyId, KeySerialization, Location, Mechanism, StorageAttributes}, +}; -pub(crate) struct ClientPin<'a, UP, T> -where UP: UserPresence, -{ - authnr: &'a mut Authenticator, +const PIN_TOKEN_LENGTH: usize = 16; + +#[derive(Clone, Copy, Debug)] +pub enum PinProtocolVersion { + V1, +} + +#[derive(Debug)] +pub struct PinProtocolState { + key_agreement_key: KeyId, + // only used to delete the old shared secret from VFS when generating a new one. ideally, the + // SharedSecret struct would clean up after itself. + shared_secret: Option, + + // for protocol version 1 + pin_token_v1: KeyId, +} + +impl PinProtocolState { + // in spec: initialize(...) + pub fn new(trussed: &mut T) -> Self { + Self { + key_agreement_key: generate_key_agreement_key(trussed), + shared_secret: None, + pin_token_v1: generate_pin_token(trussed), + } + } + + pub fn reset(self, trussed: &mut T) { + syscall!(trussed.delete(self.pin_token_v1)); + syscall!(trussed.delete(self.key_agreement_key)); + if let Some(shared_secret) = self.shared_secret { + syscall!(trussed.delete(shared_secret)); + } + } +} + +#[derive(Debug)] +pub struct PinProtocol<'a, T: TrussedRequirements> { + trussed: &'a mut T, + state: &'a mut PinProtocolState, + version: PinProtocolVersion, +} + +impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { + pub fn new( + trussed: &'a mut T, + state: &'a mut PinProtocolState, + version: PinProtocolVersion, + ) -> Self { + Self { + trussed, + state, + version, + } + } + + fn pin_token(&self) -> KeyId { + match self.version { + PinProtocolVersion::V1 => self.state.pin_token_v1, + } + } + + fn set_pin_token(&mut self, pin_token: KeyId) { + match self.version { + PinProtocolVersion::V1 => self.state.pin_token_v1 = pin_token, + } + } + + pub fn regenerate(&mut self) { + syscall!(self.trussed.delete(self.state.key_agreement_key)); + if let Some(shared_secret) = self.state.shared_secret.take() { + syscall!(self.trussed.delete(shared_secret)); + } + self.state.key_agreement_key = generate_key_agreement_key(self.trussed); + } + + // in spec: resetPinUvAuthToken() + pub fn reset_pin_token(&mut self) { + syscall!(self.trussed.delete(self.pin_token())); + let pin_token = generate_pin_token(self.trussed); + self.set_pin_token(pin_token); + } + + // in spec: getPublicKey + #[must_use] + pub fn key_agreement_key(&mut self) -> EcdhEsHkdf256PublicKey { + let public_key = syscall!(self + .trussed + .derive_p256_public_key(self.state.key_agreement_key, Location::Volatile)) + .key; + let serialized_cose_key = syscall!(self.trussed.serialize_key( + Mechanism::P256, + public_key, + KeySerialization::EcdhEsHkdf256 + )) + .serialized_key; + let cose_key = cbor_deserialize(&serialized_cose_key).unwrap(); + syscall!(self.trussed.delete(public_key)); + cose_key + } + + // in spec: verify(pinUvAuthToken, ...) + #[must_use] + pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> bool { + // TODO: check if pin token is in use + verify(self.trussed, self.pin_token(), data, signature) + } + + // in spec: resetPinUvAuthToken() + encrypt(..., pinUvAuthToken) + pub fn reset_and_encrypt_pin_token( + &mut self, + shared_secret: &SharedSecret, + ) -> Result> { + self.reset_pin_token(); + self.encrypt_pin_token(shared_secret) + } + + // in spec: encrypt(..., pinUvAuthToken) + fn encrypt_pin_token(&mut self, shared_secret: &SharedSecret) -> Result> { + let token = syscall!(self + .trussed + .wrap_key_aes256cbc(shared_secret.key_id, self.pin_token())) + .wrapped_key; + Bytes::from_slice(&token).map_err(|_| Error::Other) + } + + // in spec: decapsulate(...) = ecdh(...) + // The returned key ID is valid until the next call of shared_secret or regenerate. The caller + // has to delete the key from the VFS after end of use. Ideally, this should be enforced by + // the compiler, for example by using a callback. + pub fn shared_secret(&mut self, peer_key: &EcdhEsHkdf256PublicKey) -> Result { + self.shared_secret_impl(peer_key) + .ok_or(Error::InvalidParameter) + } + + fn shared_secret_impl(&mut self, peer_key: &EcdhEsHkdf256PublicKey) -> Option { + let serialized_peer_key = cbor_serialize_message(peer_key).ok()?; + let peer_key = try_syscall!(self.trussed.deserialize_p256_key( + &serialized_peer_key, + KeySerialization::EcdhEsHkdf256, + StorageAttributes::new().set_persistence(Location::Volatile) + )) + .ok()? + .key; + + let result = try_syscall!(self.trussed.agree( + Mechanism::P256, + self.state.key_agreement_key, + peer_key, + StorageAttributes::new().set_persistence(Location::Volatile), + )); + syscall!(self.trussed.delete(peer_key)); + let pre_shared_secret = result.ok()?.shared_secret; + + if let Some(shared_secret) = self.state.shared_secret { + syscall!(self.trussed.delete(shared_secret)); + } + + let shared_secret = self.kdf(pre_shared_secret); + self.state.shared_secret = Some(shared_secret); + syscall!(self.trussed.delete(pre_shared_secret)); + + Some(SharedSecret { + key_id: shared_secret, + }) + } + + fn kdf(&mut self, input: KeyId) -> KeyId { + syscall!(self.trussed.derive_key( + Mechanism::Sha256, + input, + None, + StorageAttributes::new().set_persistence(Location::Volatile) + )) + .key + } +} + +pub struct SharedSecret { + key_id: KeyId, +} + +impl SharedSecret { + pub fn verify_pin_auth( + &self, + trussed: &mut T, + data: &[u8], + pin_auth: &Bytes<16>, + ) -> Result<()> { + if verify(trussed, self.key_id, data, pin_auth) { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } + + #[must_use] + pub fn encrypt(&self, trussed: &mut T, data: &[u8]) -> Bytes<1024> { + syscall!(trussed.encrypt(Mechanism::Aes256Cbc, self.key_id, data, b"", None)).ciphertext + } + + #[must_use] + pub fn decrypt(&self, trussed: &mut T, data: &[u8]) -> Option> { + decrypt(trussed, self.key_id, data) + } + + pub fn delete(self, trussed: &mut T) { + syscall!(trussed.delete(self.key_id)); + } +} + +#[must_use] +fn verify(trussed: &mut T, key: KeyId, data: &[u8], signature: &[u8]) -> bool { + let actual_signature = syscall!(trussed.sign_hmacsha256(key, data)).signature; + &actual_signature[..16] == signature } +#[must_use] +fn decrypt(trussed: &mut T, key: KeyId, data: &[u8]) -> Option> { + try_syscall!(trussed.decrypt_aes256cbc(key, data)) + .ok() + .and_then(|response| response.plaintext) +} + +fn generate_pin_token(trussed: &mut T) -> KeyId { + syscall!(trussed.generate_secret_key(PIN_TOKEN_LENGTH, Location::Volatile)).key +} + +fn generate_key_agreement_key(trussed: &mut T) -> KeyId { + syscall!(trussed.generate_p256_private_key(Location::Volatile)).key +} diff --git a/src/state.rs b/src/state.rs index 5a5229f..6999233 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,7 +3,6 @@ //! Needs cleanup. use ctap_types::{ - cose::EcdhEsHkdf256PublicKey as CoseEcdhEsHkdf256PublicKey, // 2022-02-27: 10 credentials sizes::MAX_CREDENTIAL_COUNT_IN_LIST, // U8 currently Bytes, @@ -12,13 +11,17 @@ use ctap_types::{ }; use trussed::{ client, syscall, try_syscall, - types::{self, KeyId, Location, Mechanism, PathBuf}, + types::{KeyId, Location, Mechanism, PathBuf}, Client as TrussedClient, }; use heapless::binary_heap::{BinaryHeap, Max}; -use crate::{cbor_serialize_message, credential::FullCredential, ctap2, Result}; +use crate::{ + credential::FullCredential, + ctap2::{self, pin::PinProtocolState}, + Result, TrussedRequirements, +}; #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CachedCredential { @@ -70,7 +73,7 @@ impl CredentialCacheGeneric { pub type CredentialCache = CredentialCacheGeneric; -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct State { /// Batch device identity (aaguid, certificate, key). pub identity: Identity, @@ -218,12 +221,9 @@ pub struct ActiveGetAssertionData { pub extensions: Option, } -#[derive(Clone, Debug, Default)] +#[derive(Debug, Default)] pub struct RuntimeState { - key_agreement_key: Option, - pin_token: Option, - // TODO: why is this field not used? - shared_secret: Option, + pin_protocol: Option, consecutive_pin_mismatches: u8, // both of these are a cache for previous Get{Next,}Assertion call @@ -496,99 +496,22 @@ impl RuntimeState { self.cached_credentials.len() as _ } - pub fn key_agreement_key(&mut self, trussed: &mut T) -> KeyId { - match self.key_agreement_key { - Some(key) => key, - None => self.rotate_key_agreement_key(trussed), - } - } - - pub fn rotate_key_agreement_key(&mut self, trussed: &mut T) -> KeyId { - // TODO: need to rotate pin token? - if let Some(key) = self.key_agreement_key { - syscall!(trussed.delete(key)); - } - if let Some(previous_shared_secret) = self.shared_secret { - syscall!(trussed.delete(previous_shared_secret)); - } - - let key = syscall!(trussed.generate_p256_private_key(Location::Volatile)).key; - self.key_agreement_key = Some(key); - self.shared_secret = None; - key - } - - pub fn pin_token(&mut self, trussed: &mut impl client::HmacSha256) -> KeyId { - match self.pin_token { - Some(token) => token, - None => self.rotate_pin_token(trussed), - } - } - - 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)); - } - let token = syscall!(trussed.generate_secret_key(16, Location::Volatile)).key; - self.pin_token = Some(token); - token - } - - pub fn reset( + pub fn pin_protocol( &mut self, trussed: &mut T, - ) { + ) -> &mut PinProtocolState { + self.pin_protocol + .get_or_insert_with(|| PinProtocolState::new(trussed)) + } + + 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(); self.active_get_assertion = None; - 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 { - let private_key = self.key_agreement_key(trussed); - - 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, - types::StorageAttributes::new().set_persistence(types::Location::Volatile) - )) - .map_err(|_| Error::InvalidParameter)? - .key; - - let pre_shared_secret = syscall!(trussed.agree( - types::Mechanism::P256, - private_key, - platform_kak, - types::StorageAttributes::new().set_persistence(types::Location::Volatile), - )) - .shared_secret; - syscall!(trussed.delete(platform_kak)); - - if let Some(previous_shared_secret) = self.shared_secret { - syscall!(trussed.delete(previous_shared_secret)); + if let Some(pin_protocol) = self.pin_protocol.take() { + pin_protocol.reset(trussed); } - - let shared_secret = syscall!(trussed.derive_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 2bbe43e8b2c2722303ed29db12409ac070da71a6 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 28 Feb 2024 10:49:22 +0100 Subject: [PATCH 047/135] ctap2: Explicitly specify PIN protocol version This patch removes the assumption that we only support PIN protocol 1 from the CTAP2 implementation. Instead, a list of supported PIN protocols is provided by the pin_protocols function and parse_pin_protocol can be used to validate the PIN protocol selected by the platform. The parsed PIN protocol version must then be passed to the pin_protcol function to access the correct implementation. --- src/ctap2.rs | 121 ++++++++++++++++++++++++++++------------------- src/ctap2/pin.rs | 20 +++----- 2 files changed, 79 insertions(+), 62 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 70d8cb3..404ce07 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -170,7 +170,7 @@ impl Authenticator for crate::Authenti let uv_performed = self.pin_prechecks( ¶meters.options, parameters.pin_auth.map(AsRef::as_ref), - ¶meters.pin_protocol, + parameters.pin_protocol, parameters.client_data_hash.as_ref(), )?; @@ -639,10 +639,7 @@ impl Authenticator for crate::Authenti debug_now!("CTAP2.PIN..."); // info_now!("{:?}", parameters); - // TODO: Handle pin protocol V2 - if parameters.pin_protocol != 1 { - return Err(Error::InvalidParameter); - } + let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; Ok(match parameters.sub_command { Subcommand::GetRetries => { @@ -658,7 +655,7 @@ impl Authenticator for crate::Authenti Subcommand::GetKeyAgreement => { debug_now!("CTAP2.Pin.GetKeyAgreement"); - let key_agreement = self.pin_protocol().key_agreement_key(); + let key_agreement = self.pin_protocol(pin_protocol).key_agreement_key(); ctap2::client_pin::Response { key_agreement: Some(key_agreement), @@ -695,7 +692,9 @@ impl Authenticator for crate::Authenti } // 3. generate shared secret - let shared_secret = self.pin_protocol().shared_secret(platform_kek)?; + let shared_secret = self + .pin_protocol(pin_protocol) + .shared_secret(platform_kek)?; // TODO: there are moar early returns!! // - implement Drop? @@ -755,7 +754,9 @@ impl Authenticator for crate::Authenti self.state.pin_blocked()?; // 3. generate shared secret - let shared_secret = self.pin_protocol().shared_secret(platform_kek)?; + let shared_secret = self + .pin_protocol(pin_protocol) + .shared_secret(platform_kek)?; // 4. verify pinAuth let mut data = MediumData::new(); @@ -769,7 +770,11 @@ 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( + pin_protocol, + &shared_secret, + pin_hash_enc, + )?; // 7. reset retries self.state.reset_retries(&mut self.trussed)?; @@ -782,7 +787,9 @@ impl Authenticator for crate::Authenti // 9. store hashed PIN self.hash_store_pin(&new_pin)?; - self.pin_protocol().reset_pin_token(); + for pin_protocol in self.pin_protocols() { + self.pin_protocol(*pin_protocol).reset_pin_token(); + } ctap2::client_pin::Response { key_agreement: None, @@ -812,13 +819,19 @@ impl Authenticator for crate::Authenti self.state.pin_blocked()?; // 3. generate shared secret - let shared_secret = self.pin_protocol().shared_secret(platform_kek)?; + let shared_secret = self + .pin_protocol(pin_protocol) + .shared_secret(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)?; + self.decrypt_pin_hash_and_maybe_escalate( + pin_protocol, + &shared_secret, + pin_hash_enc, + )?; // 6. reset retries self.state.reset_retries(&mut self.trussed)?; @@ -826,9 +839,12 @@ impl Authenticator for crate::Authenti // 7. return encrypted pinToken debug_now!("wrapping pin token"); // info_now!("exists? {}", syscall!(self.trussed.exists(shared_secret)).exists); + for pin_protocol in self.pin_protocols() { + self.pin_protocol(*pin_protocol).reset_pin_token(); + } let pin_token_enc = self - .pin_protocol() - .reset_and_encrypt_pin_token(&shared_secret)?; + .pin_protocol(pin_protocol) + .encrypt_pin_token(&shared_secret)?; shared_secret.delete(&mut self.trussed); @@ -929,7 +945,7 @@ impl Authenticator for crate::Authenti let uv_performed = match self.pin_prechecks( ¶meters.options, parameters.pin_auth.map(AsRef::as_ref), - ¶meters.pin_protocol, + parameters.pin_protocol, parameters.client_data_hash.as_ref(), ) { Ok(b) => b, @@ -1034,9 +1050,20 @@ impl Authenticator for crate::Authenti // impl Authenticator for crate::Authenticator impl crate::Authenticator { - fn pin_protocol(&mut self) -> PinProtocol<'_, T> { + fn parse_pin_protocol(&self, version: impl Into) -> Result { + match version.into() { + 1 => Ok(PinProtocolVersion::V1), + _ => Err(Error::InvalidParameter), + } + } + + fn pin_protocols(&self) -> &'static [PinProtocolVersion] { + &[PinProtocolVersion::V1] + } + + fn pin_protocol(&mut self, pin_protocol: PinProtocolVersion) -> PinProtocol<'_, T> { let state = self.state.runtime.pin_protocol(&mut self.trussed); - PinProtocol::new(&mut self.trussed, state, PinProtocolVersion::V1) + PinProtocol::new(&mut self.trussed, state, pin_protocol) } #[inline(never)] @@ -1152,6 +1179,7 @@ impl crate::Authenticator { fn decrypt_pin_hash_and_maybe_escalate( &mut self, + pin_protocol: PinProtocolVersion, shared_secret: &SharedSecret, pin_hash_enc: &Bytes<64>, ) -> Result<()> { @@ -1168,7 +1196,7 @@ impl crate::Authenticator { if pin_hash != stored_pin_hash { // I) generate new KEK - self.pin_protocol().regenerate(); + self.pin_protocol(pin_protocol).regenerate(); self.state.pin_blocked()?; return Err(Error::PinInvalid); } @@ -1217,16 +1245,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 pin_verified = self.pin_protocol().verify_pin_token(data, pin_auth); - if pin_verified { - 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, @@ -1245,9 +1263,7 @@ impl crate::Authenticator { // .sub_command_params.as_ref().ok_or(Error::MissingParameter)? .pin_protocol .ok_or(Error::MissingParameter)?; - if pin_protocol != 1 { - return Err(Error::InvalidParameter); - } + let pin_protocol = self.parse_pin_protocol(pin_protocol)?; // check pinAuth let mut data: Bytes<{ sizes::MAX_CREDENTIAL_ID_LENGTH_PLUS_256 }> = @@ -1274,7 +1290,11 @@ impl crate::Authenticator { .as_ref() .ok_or(Error::MissingParameter)?; - if self.pin_protocol().verify_pin_token(&data[..len], pin_auth) { + if self + .pin_protocol(pin_protocol) + .verify_pin_token(&data[..len], pin_auth) + .is_ok() + { info_now!("passed pinauth"); Ok(()) } else { @@ -1303,7 +1323,7 @@ impl crate::Authenticator { &mut self, options: &Option, pin_auth: Option<&[u8]>, - pin_protocol: &Option, + pin_protocol: Option, data: &[u8], ) -> Result { // 1. pinAuth zero length -> wait for user touch, then @@ -1324,9 +1344,13 @@ impl crate::Authenticator { } // 2. check PIN protocol is 1 if pinAuth was sent - if pin_auth.is_some() && pin_protocol != &Some(1) { - return Err(Error::PinAuthInvalid); - } + let pin_protocol = if pin_auth.is_some() { + let pin_protocol = pin_protocol.ok_or(Error::MissingParameter)?; + let pin_protocol = self.parse_pin_protocol(pin_protocol)?; + Some(pin_protocol) + } else { + None + }; // 3. if no PIN is set (we have no other form of UV), // and platform sent `uv` or `pinAuth`, return InvalidOption @@ -1353,15 +1377,12 @@ impl crate::Authenticator { } // 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 { + if let Some(pin_protocol) = 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.try_into().unwrap(), - data, - )?; + self.pin_protocol(pin_protocol) + .verify_pin_token(pin_auth, data)?; return Ok(true); } else { @@ -1410,11 +1431,11 @@ impl crate::Authenticator { 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); - } - } + let pin_protocol = hmac_secret + .pin_protocol + .map(|i| self.parse_pin_protocol(i)) + .transpose()? + .unwrap_or(PinProtocolVersion::V1); // We derive credRandom as an hmac of the existing private key. // UV is used as input data since credRandom should depend UV @@ -1429,7 +1450,7 @@ impl crate::Authenticator { // Verify the auth tag, which uses the same process as the pinAuth let shared_secret = self - .pin_protocol() + .pin_protocol(pin_protocol) .shared_secret(&hmac_secret.key_agreement)?; shared_secret.verify_pin_auth( &mut self.trussed, @@ -1839,6 +1860,7 @@ impl crate::Authenticator { if pin_uv_auth_protocol != 1 { return Err(Error::PinAuthInvalid); } + let pin_protocol = self.parse_pin_protocol(pin_uv_auth_protocol)?; // TODO: check pinUvAuthToken let pin_auth: [u8; 16] = pin_uv_auth_param .as_ref() @@ -1858,7 +1880,8 @@ impl crate::Authenticator { // SHA-256(data) auth_data.extend_from_slice(&Sha256::digest(data)).unwrap(); - self.verify_pin(&pin_auth, &auth_data)?; + self.pin_protocol(pin_protocol) + .verify_pin_token(&pin_auth, &auth_data)?; } // 6. Validate data length diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index 71fd52a..55cd4d2 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -110,23 +110,17 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } // in spec: verify(pinUvAuthToken, ...) - #[must_use] - pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> bool { + pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> Result<()> { // TODO: check if pin token is in use - verify(self.trussed, self.pin_token(), data, signature) - } - - // in spec: resetPinUvAuthToken() + encrypt(..., pinUvAuthToken) - pub fn reset_and_encrypt_pin_token( - &mut self, - shared_secret: &SharedSecret, - ) -> Result> { - self.reset_pin_token(); - self.encrypt_pin_token(shared_secret) + if verify(self.trussed, self.pin_token(), data, signature) { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } } // in spec: encrypt(..., pinUvAuthToken) - fn encrypt_pin_token(&mut self, shared_secret: &SharedSecret) -> Result> { + pub fn encrypt_pin_token(&mut self, shared_secret: &SharedSecret) -> Result> { let token = syscall!(self .trussed .wrap_key_aes256cbc(shared_secret.key_id, self.pin_token())) From bf61fcd5eb5b35231f60a41ce38b8894933ce664 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 28 Feb 2024 12:10:10 +0100 Subject: [PATCH 048/135] ctap2: Implement PIN protocol version 2 --- CHANGELOG.md | 5 +- Cargo.toml | 9 +- src/ctap1.rs | 2 +- src/ctap2.rs | 67 +++++++------- src/ctap2/pin.rs | 234 +++++++++++++++++++++++++++++++++++++---------- src/lib.rs | 3 + 6 files changed, 234 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 677d635..860df62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement the `largeBlobKey` extension and the `largeBlobs` command ([#38][]) - Fix error type for third invalid PIN entry ([#60][]) - Fix error type for cancelled user presence ([#61][]) -- Extract PIN protocol implementation into separate module ([#62][]) +- PIN protocol changes: + - Extract PIN protocol implementation into separate module ([#62][]) + - Implement PIN protocol 2 ([#63][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -31,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#60]: https://github.com/Nitrokey/fido-authenticator/pull/60 [#61]: https://github.com/Nitrokey/fido-authenticator/pull/61 [#62]: https://github.com/Nitrokey/fido-authenticator/pull/62 +[#63]: https://github.com/Nitrokey/fido-authenticator/pull/63 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/Cargo.toml b/Cargo.toml index 1270ab7..47b34df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ serde_cbor = { version = "0.11.0", default-features = false } serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } trussed = "0.1" +trussed-hkdf = { version = "0.1.0" } trussed-staging = { version = "0.1.0", default-features = false, optional = true } apdu-dispatch = { version = "0.1", optional = true } @@ -56,11 +57,13 @@ usbd-ctaphid = "0.1.0" features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "7d4ad69e64ad308944c012aef5b9cfd7654d9be8" } +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "3735eb5a8e2b30b98afd35b64b4f470e690dcd19" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b1781805a2e33615d2d00b8bec80c0b1f5870ca1" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging", rev = "3b9594d93f89a5e760fe78fa5a96f125dfdcd470" } +littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "ebd27e49ca321089d01d8c9b169c4aeb58ceeeca" } serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "cff2e663841b6a68d3a8ce12647d57b2b6fbc36c" } +trussed-hkdf = { git = "https://github.com/Nitrokey/trussed-hkdf-backend.git", tag = "v0.1.0" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging", rev = "3b9594d93f89a5e760fe78fa5a96f125dfdcd470" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } usbd-ctaphid = { git = "https://github.com/Nitrokey/usbd-ctaphid.git", tag = "v0.1.0-nitrokey.2" } diff --git a/src/ctap1.rs b/src/ctap1.rs index 4e5623e..be12d47 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -63,7 +63,7 @@ impl Authenticator for crate::Authenti let wrapped_key = syscall!(self .trussed - .wrap_key_chacha8poly1305(wrapping_key, private_key, &[])) + .wrap_key_chacha8poly1305(wrapping_key, private_key, &[], None)) .wrapped_key; // debug!("wrapped_key = {:?}", &wrapped_key); diff --git a/src/ctap2.rs b/src/ctap2.rs index 404ce07..83dfef4 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -68,8 +68,10 @@ impl Authenticator for crate::Authenti .unwrap(); } - let mut pin_protocols = Vec::::new(); - pin_protocols.push(1).unwrap(); + let mut pin_protocols = Vec::::new(); + for pin_protocol in self.pin_protocols() { + pin_protocols.push(u8::from(*pin_protocol)).unwrap(); + } let options = ctap2::get_info::CtapOptions { ep: None, @@ -332,11 +334,13 @@ impl Authenticator for crate::Authenti false => { // WrappedKey version let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - let wrapped_key = - syscall!(self - .trussed - .wrap_key_chacha8poly1305(wrapping_key, private_key, &[])) - .wrapped_key; + let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( + wrapping_key, + private_key, + &[], + None + )) + .wrapped_key; // 32B key, 12B nonce, 16B tag + some info on algorithm (P256/Ed25519) // Turns out it's size 92 (enum serialization not optimized yet...) @@ -692,16 +696,15 @@ impl Authenticator for crate::Authenti } // 3. generate shared secret - let shared_secret = self - .pin_protocol(pin_protocol) - .shared_secret(platform_kek)?; + let mut pin_protocol = self.pin_protocol(pin_protocol); + let shared_secret = pin_protocol.shared_secret(platform_kek)?; // TODO: there are moar early returns!! // - implement Drop? // - do garbage collection outside of this? // 4. verify pinAuth - shared_secret.verify_pin_auth(&mut self.trussed, new_pin_enc, pin_auth)?; + pin_protocol.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)?; @@ -754,9 +757,8 @@ impl Authenticator for crate::Authenti self.state.pin_blocked()?; // 3. generate shared secret - let shared_secret = self - .pin_protocol(pin_protocol) - .shared_secret(platform_kek)?; + let mut pin_protocol_impl = self.pin_protocol(pin_protocol); + let shared_secret = pin_protocol_impl.shared_secret(platform_kek)?; // 4. verify pinAuth let mut data = MediumData::new(); @@ -764,7 +766,7 @@ impl Authenticator for crate::Authenti .map_err(|_| Error::InvalidParameter)?; data.extend_from_slice(pin_hash_enc) .map_err(|_| Error::InvalidParameter)?; - shared_secret.verify_pin_auth(&mut self.trussed, &data, pin_auth)?; + pin_protocol_impl.verify_pin_auth(&shared_secret, &data, pin_auth)?; // 5. decrement retries self.state.decrement_retries(&mut self.trussed)?; @@ -1050,15 +1052,20 @@ impl Authenticator for crate::Authenti // impl Authenticator for crate::Authenticator impl crate::Authenticator { - fn parse_pin_protocol(&self, version: impl Into) -> Result { - match version.into() { - 1 => Ok(PinProtocolVersion::V1), - _ => Err(Error::InvalidParameter), + fn parse_pin_protocol(&self, version: impl TryInto) -> Result { + if let Ok(version) = version.try_into() { + for pin_protocol in self.pin_protocols() { + if u8::from(*pin_protocol) == version { + return Ok(*pin_protocol); + } + } } + Err(Error::InvalidParameter) } + // This is the single source of truth for the supported PIN protocols. fn pin_protocols(&self) -> &'static [PinProtocolVersion] { - &[PinProtocolVersion::V1] + &[PinProtocolVersion::V2, PinProtocolVersion::V1] } fn pin_protocol(&mut self, pin_protocol: PinProtocolVersion) -> PinProtocol<'_, T> { @@ -1181,7 +1188,7 @@ impl crate::Authenticator { &mut self, pin_protocol: PinProtocolVersion, shared_secret: &SharedSecret, - pin_hash_enc: &Bytes<64>, + pin_hash_enc: &Bytes<80>, ) -> Result<()> { let pin_hash = shared_secret .decrypt(&mut self.trussed, pin_hash_enc) @@ -1372,9 +1379,6 @@ impl crate::Authenticator { if self.state.persistent.pin_is_set() { // let mut uv_performed = false; if let Some(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(pin_protocol) = pin_protocol { @@ -1382,7 +1386,7 @@ impl crate::Authenticator { // success --> set uv = 1 // error --> PinAuthInvalid self.pin_protocol(pin_protocol) - .verify_pin_token(pin_auth, data)?; + .verify_pin_token(data, pin_auth)?; return Ok(true); } else { @@ -1449,16 +1453,17 @@ impl crate::Authenticator { .key; // Verify the auth tag, which uses the same process as the pinAuth - let shared_secret = self - .pin_protocol(pin_protocol) - .shared_secret(&hmac_secret.key_agreement)?; - shared_secret.verify_pin_auth( - &mut self.trussed, + let mut pin_protocol = self.pin_protocol(pin_protocol); + let shared_secret = pin_protocol.shared_secret(&hmac_secret.key_agreement)?; + pin_protocol.verify_pin_auth( + &shared_secret, &hmac_secret.salt_enc, &hmac_secret.salt_auth, )?; - if hmac_secret.salt_enc.len() != 32 && hmac_secret.salt_enc.len() != 64 { + if hmac_secret.salt_enc.len() != 32 + && (hmac_secret.salt_enc.len() != 64 || hmac_secret.salt_enc.len() == 80) + { debug_now!("invalid hmac-secret length"); return Err(Error::InvalidLength); } diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index 55cd4d2..8b36e15 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -2,16 +2,30 @@ use crate::{cbor_serialize_message, TrussedRequirements}; use ctap_types::{cose::EcdhEsHkdf256PublicKey, Error, Result}; use trussed::{ cbor_deserialize, - client::{Aes256Cbc, CryptoClient, HmacSha256, P256}, + client::{CryptoClient, HmacSha256, P256}, syscall, try_syscall, - types::{Bytes, KeyId, KeySerialization, Location, Mechanism, StorageAttributes}, + types::{ + Bytes, KeyId, KeySerialization, Location, Mechanism, Message, ShortData, StorageAttributes, + }, }; +use trussed_hkdf::{KeyOrData, OkmId}; -const PIN_TOKEN_LENGTH: usize = 16; +// PIN protocol 1 supports 16 or 32 bytes, PIN protocol 2 requires 32 bytes. +const PIN_TOKEN_LENGTH: usize = 32; #[derive(Clone, Copy, Debug)] pub enum PinProtocolVersion { V1, + V2, +} + +impl From for u8 { + fn from(version: PinProtocolVersion) -> Self { + match version { + PinProtocolVersion::V1 => 1, + PinProtocolVersion::V2 => 2, + } + } } #[derive(Debug)] @@ -19,10 +33,12 @@ pub struct PinProtocolState { key_agreement_key: KeyId, // only used to delete the old shared secret from VFS when generating a new one. ideally, the // SharedSecret struct would clean up after itself. - shared_secret: Option, + shared_secret: Option, // for protocol version 1 pin_token_v1: KeyId, + // for protocol version 2 + pin_token_v2: KeyId, } impl PinProtocolState { @@ -32,6 +48,7 @@ impl PinProtocolState { key_agreement_key: generate_key_agreement_key(trussed), shared_secret: None, pin_token_v1: generate_pin_token(trussed), + pin_token_v2: generate_pin_token(trussed), } } @@ -39,7 +56,7 @@ impl PinProtocolState { syscall!(trussed.delete(self.pin_token_v1)); syscall!(trussed.delete(self.key_agreement_key)); if let Some(shared_secret) = self.shared_secret { - syscall!(trussed.delete(shared_secret)); + shared_secret.delete(trussed); } } } @@ -67,19 +84,21 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { fn pin_token(&self) -> KeyId { match self.version { PinProtocolVersion::V1 => self.state.pin_token_v1, + PinProtocolVersion::V2 => self.state.pin_token_v2, } } fn set_pin_token(&mut self, pin_token: KeyId) { match self.version { PinProtocolVersion::V1 => self.state.pin_token_v1 = pin_token, + PinProtocolVersion::V2 => self.state.pin_token_v2 = pin_token, } } pub fn regenerate(&mut self) { syscall!(self.trussed.delete(self.state.key_agreement_key)); if let Some(shared_secret) = self.state.shared_secret.take() { - syscall!(self.trussed.delete(shared_secret)); + shared_secret.delete(self.trussed); } self.state.key_agreement_key = generate_key_agreement_key(self.trussed); } @@ -109,10 +128,35 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { cose_key } + #[must_use] + fn verify(&mut self, key: KeyId, data: &[u8], signature: &[u8]) -> bool { + let actual_signature = syscall!(self.trussed.sign_hmacsha256(key, data)).signature; + let expected_signature = match self.version { + PinProtocolVersion::V1 => &actual_signature[..16], + PinProtocolVersion::V2 => &actual_signature, + }; + expected_signature == signature + } + // in spec: verify(pinUvAuthToken, ...) pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> Result<()> { // TODO: check if pin token is in use - if verify(self.trussed, self.pin_token(), data, signature) { + if self.verify(self.pin_token(), data, signature) { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } + + // in spec: verify(shared secret, ...) + pub fn verify_pin_auth( + &mut self, + shared_secret: &SharedSecret, + data: &[u8], + pin_auth: &[u8], + ) -> Result<()> { + let key_id = shared_secret.hmac_key_id(); + if self.verify(key_id, data, pin_auth) { Ok(()) } else { Err(Error::PinAuthInvalid) @@ -120,11 +164,8 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } // in spec: encrypt(..., pinUvAuthToken) - pub fn encrypt_pin_token(&mut self, shared_secret: &SharedSecret) -> Result> { - let token = syscall!(self - .trussed - .wrap_key_aes256cbc(shared_secret.key_id, self.pin_token())) - .wrapped_key; + pub fn encrypt_pin_token(&mut self, shared_secret: &SharedSecret) -> Result> { + let token = shared_secret.wrap(self.trussed, self.pin_token()); Bytes::from_slice(&token).map_err(|_| Error::Other) } @@ -156,74 +197,167 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { syscall!(self.trussed.delete(peer_key)); let pre_shared_secret = result.ok()?.shared_secret; - if let Some(shared_secret) = self.state.shared_secret { - syscall!(self.trussed.delete(shared_secret)); + if let Some(shared_secret) = self.state.shared_secret.take() { + shared_secret.delete(self.trussed); } let shared_secret = self.kdf(pre_shared_secret); - self.state.shared_secret = Some(shared_secret); syscall!(self.trussed.delete(pre_shared_secret)); - Some(SharedSecret { - key_id: shared_secret, - }) + let shared_secret = shared_secret?; + self.state.shared_secret = Some(shared_secret.clone()); + Some(shared_secret) + } + + fn kdf(&mut self, input: KeyId) -> Option { + match self.version { + PinProtocolVersion::V1 => self.kdf_v1(input), + PinProtocolVersion::V2 => self.kdf_v2(input), + } } - fn kdf(&mut self, input: KeyId) -> KeyId { - syscall!(self.trussed.derive_key( + // PIN protocol 1: derive a single key using SHA-256 + fn kdf_v1(&mut self, input: KeyId) -> Option { + let key_id = syscall!(self.trussed.derive_key( Mechanism::Sha256, input, None, StorageAttributes::new().set_persistence(Location::Volatile) )) - .key + .key; + Some(SharedSecret::V1 { key_id }) + } + + // PIN protocol 2: derive two keys using HKDF-SHA-256 + // In the spec, the keys are concatenated and the relevant part is selected during the key + // operations. For simplicity, we store two separate keys instead. + fn kdf_v2(&mut self, input: KeyId) -> Option { + fn hkdf(trussed: &mut T, okm: OkmId, info: &[u8]) -> Option { + let info = Message::from_slice(info).ok()?; + try_syscall!(trussed.hkdf_expand(okm, info, 32, Location::Volatile)) + .ok() + .map(|reply| reply.key) + } + + // salt: 0x00 * 32 => None + let okm = try_syscall!(self.trussed.hkdf_extract( + KeyOrData::Key(input), + None, + Location::Volatile + )) + .ok()? + .okm; + let hmac_key_id = hkdf(self.trussed, okm, b"CTAP2 HMAC key"); + let aes_key_id = hkdf(self.trussed, okm, b"CTAP2 AES key"); + + syscall!(self.trussed.delete(okm.0)); + + Some(SharedSecret::V2 { + hmac_key_id: hmac_key_id?, + aes_key_id: aes_key_id?, + }) } } -pub struct SharedSecret { - key_id: KeyId, +#[derive(Clone, Debug)] +pub enum SharedSecret { + V1 { + key_id: KeyId, + }, + V2 { + hmac_key_id: KeyId, + aes_key_id: KeyId, + }, } impl SharedSecret { - pub fn verify_pin_auth( - &self, - trussed: &mut T, - data: &[u8], - pin_auth: &Bytes<16>, - ) -> Result<()> { - if verify(trussed, self.key_id, data, pin_auth) { - Ok(()) - } else { - Err(Error::PinAuthInvalid) + fn aes_key_id(&self) -> KeyId { + match self { + Self::V1 { key_id } => *key_id, + Self::V2 { aes_key_id, .. } => *aes_key_id, + } + } + + fn hmac_key_id(&self) -> KeyId { + match self { + Self::V1 { key_id } => *key_id, + Self::V2 { hmac_key_id, .. } => *hmac_key_id, + } + } + + fn generate_iv(&self, trussed: &mut T) -> ShortData { + match self { + Self::V1 { .. } => ShortData::from_slice(&[0; 16]).unwrap(), + Self::V2 { .. } => syscall!(trussed.random_bytes(16)) + .bytes + .try_convert_into() + .unwrap(), } } #[must_use] pub fn encrypt(&self, trussed: &mut T, data: &[u8]) -> Bytes<1024> { - syscall!(trussed.encrypt(Mechanism::Aes256Cbc, self.key_id, data, b"", None)).ciphertext + let key_id = self.aes_key_id(); + let iv = self.generate_iv(trussed); + let mut ciphertext = + syscall!(trussed.encrypt(Mechanism::Aes256Cbc, key_id, data, &[], Some(iv.clone()))) + .ciphertext; + if matches!(self, Self::V2 { .. }) { + ciphertext.insert_slice_at(&iv, 0).unwrap(); + } + ciphertext } #[must_use] - pub fn decrypt(&self, trussed: &mut T, data: &[u8]) -> Option> { - decrypt(trussed, self.key_id, data) + fn wrap(&self, trussed: &mut T, key: KeyId) -> Bytes<1024> { + let wrapping_key = self.aes_key_id(); + let iv = self.generate_iv(trussed); + let mut wrapped_key = syscall!(trussed.wrap_key( + Mechanism::Aes256Cbc, + wrapping_key, + key, + &[], + Some(iv.clone()) + )) + .wrapped_key; + if matches!(self, Self::V2 { .. }) { + wrapped_key.insert_slice_at(&iv, 0).unwrap(); + } + wrapped_key } - pub fn delete(self, trussed: &mut T) { - syscall!(trussed.delete(self.key_id)); + #[must_use] + pub fn decrypt(&self, trussed: &mut T, data: &[u8]) -> Option> { + let key_id = self.aes_key_id(); + let (iv, data) = match self { + Self::V1 { .. } => (Default::default(), data), + Self::V2 { .. } => { + if data.len() < 16 { + return None; + } + data.split_at(16) + } + }; + try_syscall!(trussed.decrypt(Mechanism::Aes256Cbc, key_id, data, iv, b"", b"")) + .ok() + .and_then(|response| response.plaintext) } -} - -#[must_use] -fn verify(trussed: &mut T, key: KeyId, data: &[u8], signature: &[u8]) -> bool { - let actual_signature = syscall!(trussed.sign_hmacsha256(key, data)).signature; - &actual_signature[..16] == signature -} -#[must_use] -fn decrypt(trussed: &mut T, key: KeyId, data: &[u8]) -> Option> { - try_syscall!(trussed.decrypt_aes256cbc(key, data)) - .ok() - .and_then(|response| response.plaintext) + pub fn delete(self, trussed: &mut T) { + match self { + Self::V1 { key_id } => { + syscall!(trussed.delete(key_id)); + } + Self::V2 { + hmac_key_id, + aes_key_id, + } => { + for key_id in [hmac_key_id, aes_key_id] { + syscall!(trussed.delete(key_id)); + } + } + } + } } fn generate_pin_token(trussed: &mut T) -> KeyId { diff --git a/src/lib.rs b/src/lib.rs index 8f46acb..2351556 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ generate_macros!(); use core::time::Duration; use trussed::{client, syscall, types::Message, Client as TrussedClient}; +use trussed_hkdf::HkdfClient; use ctap_types::heapless_bytes::Bytes; @@ -55,6 +56,7 @@ pub trait TrussedRequirements: + client::Sha256 + client::HmacSha256 + client::Ed255 // + client::Totp + + HkdfClient + ExtensionRequirements { } @@ -67,6 +69,7 @@ impl TrussedRequirements for T where + client::Sha256 + client::HmacSha256 + client::Ed255 // + client::Totp + + HkdfClient + ExtensionRequirements { } From 87e3aef895ed6e401ff7c2c5596b62cc09b75377 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 29 Feb 2024 14:31:56 +0100 Subject: [PATCH 049/135] ctap2: Implement permissions --- CHANGELOG.md | 1 + Cargo.toml | 2 +- src/ctap2.rs | 225 ++++++++++++++++++++++++++++++++--------------- src/ctap2/pin.rs | 112 ++++++++++++++++++----- 4 files changed, 243 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 860df62..0d1539e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - PIN protocol changes: - Extract PIN protocol implementation into separate module ([#62][]) - Implement PIN protocol 2 ([#63][]) + - Implement PIN token permissions ([#63][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 diff --git a/Cargo.toml b/Cargo.toml index 47b34df..daf1090 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ usbd-ctaphid = "0.1.0" features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "3735eb5a8e2b30b98afd35b64b4f470e690dcd19" } +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "4846817d9cd44604121680a19d46f3264973a3ce" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "ebd27e49ca321089d01d8c9b169c4aeb58ceeeca" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 83dfef4..62ba5a9 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,7 +1,7 @@ //! The `ctap_types::ctap2::Authenticator` implementation. use ctap_types::{ - ctap2::{self, Authenticator, VendorOperation}, + ctap2::{self, client_pin::Permissions, Authenticator, VendorOperation}, heapless::{String, Vec}, heapless_bytes::Bytes, sizes, Error, @@ -86,6 +86,7 @@ impl Authenticator for crate::Authenti }, credential_mgmt_preview: Some(true), large_blobs: Some(self.config.supports_large_blobs()), + pin_uv_auth_token: Some(true), ..Default::default() }; // options.rk = true; @@ -174,6 +175,8 @@ impl Authenticator for crate::Authenti parameters.pin_auth.map(AsRef::as_ref), parameters.pin_protocol, parameters.client_data_hash.as_ref(), + Permissions::MAKE_CREDENTIAL, + ¶meters.rp.id, )?; // 5. "persist credProtect value for this credential" @@ -644,28 +647,19 @@ impl Authenticator for crate::Authenti // info_now!("{:?}", parameters); let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; + let mut response = ctap2::client_pin::Response::default(); - Ok(match parameters.sub_command { + match parameters.sub_command { Subcommand::GetRetries => { debug_now!("CTAP2.Pin.GetRetries"); - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: Some(self.state.persistent.retries()), - } + response.retries = Some(self.state.persistent.retries()); } Subcommand::GetKeyAgreement => { debug_now!("CTAP2.Pin.GetKeyAgreement"); - let key_agreement = self.pin_protocol(pin_protocol).key_agreement_key(); - - ctap2::client_pin::Response { - key_agreement: Some(key_agreement), - pin_token: None, - retries: None, - } + response.key_agreement = Some(self.pin_protocol(pin_protocol).key_agreement_key()); } Subcommand::SetPin => { @@ -716,12 +710,6 @@ impl Authenticator for crate::Authenti 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 => { @@ -792,83 +780,162 @@ impl Authenticator for crate::Authenti for pin_protocol in self.pin_protocols() { self.pin_protocol(*pin_protocol).reset_pin_token(); } - - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: None, - } } + // § 6.5.5.7.1 No 4 Subcommand::GetPinToken => { debug_now!("CTAP2.Pin.GetPinToken"); - // 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); - } - }; + // 1. Check mandatory parameters + let key_agreement = parameters + .key_agreement + .as_ref() + .ok_or(Error::MissingParameter)?; + let pin_hash_enc = parameters + .pin_hash_enc + .as_ref() + .ok_or(Error::MissingParameter)?; - // 2. fail if no retries left + // 2. Check PIN protocol + let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; + + // 3. + 4. Check invalid parameters + if parameters.permissions.is_some() || parameters.rp_id.is_some() { + return Err(Error::InvalidParameter); + } + + // 5. Check PIN retries self.state.pin_blocked()?; - // 3. generate shared secret + // 6. Obtain shared secret let shared_secret = self .pin_protocol(pin_protocol) - .shared_secret(platform_kek)?; + .shared_secret(key_agreement)?; + + // 7. Request user consent using display -- skipped - // 4. decrement retires + // 8. Decrement PIN retries self.state.decrement_retries(&mut self.trussed)?; - // 5. decrypt and verify pinHashEnc + // 9. Check PIN self.decrypt_pin_hash_and_maybe_escalate( pin_protocol, &shared_secret, pin_hash_enc, )?; - // 6. reset retries + // 10. Reset PIN retries self.state.reset_retries(&mut self.trussed)?; - // 7. return encrypted pinToken - debug_now!("wrapping pin token"); - // info_now!("exists? {}", syscall!(self.trussed.exists(shared_secret)).exists); + // 11. Check forcePINChange -- skipped + + // 12. Reset all PIN tokens for pin_protocol in self.pin_protocols() { self.pin_protocol(*pin_protocol).reset_pin_token(); } - let pin_token_enc = self - .pin_protocol(pin_protocol) - .encrypt_pin_token(&shared_secret)?; + + // 13. Call beginUsingPinUvAuthToken + let mut pin_protocol = self.pin_protocol(pin_protocol); + pin_protocol.begin_using_pin_token(false); + + // 14. Assign the default permissions + let mut permissions = Permissions::empty(); + permissions.insert(Permissions::MAKE_CREDENTIAL); + permissions.insert(Permissions::GET_ASSERTION); + pin_protocol.restrict_pin_token(permissions, None); + + // 15. Return PIN token + response.pin_token = Some(pin_protocol.encrypt_pin_token(&shared_secret)?); shared_secret.delete(&mut self.trussed); + } + + // § 6.5.5.7.2 No 4 + Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => { + debug_now!("CTAP2.Pin.GetPinUvAuthTokenUsingPinWithPermissions"); - // ble... - if pin_token_enc.len() != 16 { - return Err(Error::Other); + // 1. Check mandatory parameters + let key_agreement = parameters + .key_agreement + .as_ref() + .ok_or(Error::MissingParameter)?; + let pin_hash_enc = parameters + .pin_hash_enc + .as_ref() + .ok_or(Error::MissingParameter)?; + let permissions = parameters.permissions.ok_or(Error::MissingParameter)?; + + // 2. Check PIN protocol + let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; + + // 3. Check that permissions are not empty + let permissions = Permissions::from_bits_truncate(permissions); + if permissions.is_empty() { + return Err(Error::InvalidParameter); + } + + // 4. Check that all requested permissions are supported + let mut unauthorized_permissions = Permissions::empty(); + unauthorized_permissions.insert(Permissions::BIO_ENROLLMENT); + if !self.config.supports_large_blobs() { + unauthorized_permissions.insert(Permissions::LARGE_BLOB_WRITE); + } + unauthorized_permissions.insert(Permissions::AUTHENTICATOR_CONFIGURATION); + if permissions.intersects(unauthorized_permissions) { + return Err(Error::UnauthorizedPermission); } - ctap2::client_pin::Response { - key_agreement: None, - pin_token: Some(pin_token_enc), - retries: None, + // 5. Check PIN retries + self.state.pin_blocked()?; + + // 6. Obtain shared secret + let shared_secret = self + .pin_protocol(pin_protocol) + .shared_secret(key_agreement)?; + + // 7. Request user consent using display -- skipped + + // 8. Decrement PIN retries + self.state.decrement_retries(&mut self.trussed)?; + + // 9. Check PIN + self.decrypt_pin_hash_and_maybe_escalate( + pin_protocol, + &shared_secret, + pin_hash_enc, + )?; + + // 10. Reset PIN retries + self.state.reset_retries(&mut self.trussed)?; + + // 11. Check forcePINChange -- skipped + + // 12. Reset all PIN tokens + for pin_protocol in self.pin_protocols() { + self.pin_protocol(*pin_protocol).reset_pin_token(); } + + // 13. Call beginUsingPinUvAuthToken + let mut pin_protocol = self.pin_protocol(pin_protocol); + pin_protocol.begin_using_pin_token(false); + + // 14. Assign the requested permissions + // 15. Assign the requested RP id + pin_protocol.restrict_pin_token(permissions, parameters.rp_id.clone()); + + // 16. Return PIN token + response.pin_token = Some(pin_protocol.encrypt_pin_token(&shared_secret)?); + + shared_secret.delete(&mut self.trussed); } - Subcommand::GetPinUvAuthTokenUsingUvWithPermissions - | Subcommand::GetUVRetries - | Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => { + Subcommand::GetPinUvAuthTokenUsingUvWithPermissions | Subcommand::GetUVRetries => { // todo!("not implemented yet") return Err(Error::InvalidParameter); } - }) + } + + Ok(response) } #[inline(never)] @@ -880,7 +947,8 @@ impl Authenticator for crate::Authenti use ctap2::credential_management::Subcommand; // TODO: I see "failed pinauth" output, but then still continuation... - self.verify_pin_auth_using_token(parameters)?; + // TODO: determine rp_id + self.verify_pin_auth_using_token(parameters, None)?; let mut cred_mgmt = cm::CredentialManagement::new(self); let sub_parameters = ¶meters.sub_command_params; @@ -920,6 +988,9 @@ impl Authenticator for crate::Authenti .ok_or(Error::MissingParameter)?, ) } + + // 0x7 + Subcommand::UpdateUserInformation => Err(Error::InvalidParameter), } } @@ -949,6 +1020,8 @@ impl Authenticator for crate::Authenti parameters.pin_auth.map(AsRef::as_ref), parameters.pin_protocol, parameters.client_data_hash.as_ref(), + Permissions::GET_ASSERTION, + ¶meters.rp_id, ) { Ok(b) => b, Err(Error::PinRequired) => { @@ -1256,6 +1329,7 @@ impl crate::Authenticator { fn verify_pin_auth_using_token( &mut self, parameters: &ctap2::credential_management::Request, + rp_id: Option<&str>, ) -> Result<()> { // info_now!("CM params: {:?}", parameters); use ctap2::credential_management::Subcommand; @@ -1297,12 +1371,11 @@ impl crate::Authenticator { .as_ref() .ok_or(Error::MissingParameter)?; - if self - .pin_protocol(pin_protocol) - .verify_pin_token(&data[..len], pin_auth) - .is_ok() - { + let mut pin_protocol = self.pin_protocol(pin_protocol); + if let Ok(pin_token) = pin_protocol.verify_pin_token(&data[..len], pin_auth) { info_now!("passed pinauth"); + pin_token.require_permissions(Permissions::CREDENTIAL_MANAGEMENT)?; + pin_token.require_valid_for_rp_id(rp_id)?; Ok(()) } else { info_now!("failed pinauth!"); @@ -1322,6 +1395,9 @@ impl crate::Authenticator { // of already checked CredMgmt subcommands Subcommand::EnumerateRpsGetNextRp | Subcommand::EnumerateCredentialsGetNextCredential => Ok(()), + + // not implemented + Subcommand::UpdateUserInformation => Err(Error::InvalidParameter), } } @@ -1332,6 +1408,8 @@ impl crate::Authenticator { pin_auth: Option<&[u8]>, pin_protocol: Option, data: &[u8], + permissions: Permissions, + rp_id: &str, ) -> Result { // 1. pinAuth zero length -> wait for user touch, then // return PinNotSet if not set, PinInvalid if set @@ -1385,8 +1463,10 @@ impl crate::Authenticator { // 5. if pinAuth is present and pinProtocol = 1, verify // success --> set uv = 1 // error --> PinAuthInvalid - self.pin_protocol(pin_protocol) - .verify_pin_token(data, pin_auth)?; + let mut pin_protocol = self.pin_protocol(pin_protocol); + let pin_token = pin_protocol.verify_pin_token(data, pin_auth)?; + pin_token.require_permissions(permissions)?; + pin_token.require_valid_for_rp_id(Some(rp_id))?; return Ok(true); } else { @@ -1885,8 +1965,9 @@ impl crate::Authenticator { // SHA-256(data) auth_data.extend_from_slice(&Sha256::digest(data)).unwrap(); - self.pin_protocol(pin_protocol) - .verify_pin_token(&pin_auth, &auth_data)?; + let mut pin_protocol = self.pin_protocol(pin_protocol); + let pin_token = pin_protocol.verify_pin_token(&pin_auth, &auth_data)?; + pin_token.require_permissions(Permissions::LARGE_BLOB_WRITE)?; } // 6. Validate data length diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index 8b36e15..0ef9cd1 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -1,11 +1,13 @@ use crate::{cbor_serialize_message, TrussedRequirements}; -use ctap_types::{cose::EcdhEsHkdf256PublicKey, Error, Result}; +use core::mem; +use ctap_types::{cose::EcdhEsHkdf256PublicKey, ctap2::client_pin::Permissions, Error, Result}; use trussed::{ cbor_deserialize, client::{CryptoClient, HmacSha256, P256}, syscall, try_syscall, types::{ Bytes, KeyId, KeySerialization, Location, Mechanism, Message, ShortData, StorageAttributes, + String, }, }; use trussed_hkdf::{KeyOrData, OkmId}; @@ -28,6 +30,56 @@ impl From for u8 { } } +#[derive(Debug)] +pub struct PinToken { + key_id: KeyId, + state: PinTokenState, +} + +impl PinToken { + fn generate(trussed: &mut T) -> PinToken { + let key_id = + syscall!(trussed.generate_secret_key(PIN_TOKEN_LENGTH, Location::Volatile)).key; + Self::new(key_id) + } + + fn new(key_id: KeyId) -> Self { + Self { + key_id, + state: Default::default(), + } + } + + fn delete(self, trussed: &mut T) { + syscall!(trussed.delete(self.key_id)); + } + + pub fn require_permissions(&self, permissions: Permissions) -> Result<()> { + if self.state.permissions.contains(permissions) { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } + + pub fn require_valid_for_rp_id(&self, rp_id: Option<&str>) -> Result<()> { + if self.state.rp_id.is_none() || self.state.rp_id.as_deref() == rp_id { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } +} + +#[derive(Debug, Default)] +struct PinTokenState { + permissions: Permissions, + rp_id: Option>, + is_user_present: bool, + is_user_verified: bool, + is_in_use: bool, +} + #[derive(Debug)] pub struct PinProtocolState { key_agreement_key: KeyId, @@ -36,9 +88,9 @@ pub struct PinProtocolState { shared_secret: Option, // for protocol version 1 - pin_token_v1: KeyId, + pin_token_v1: PinToken, // for protocol version 2 - pin_token_v2: KeyId, + pin_token_v2: PinToken, } impl PinProtocolState { @@ -47,13 +99,14 @@ impl PinProtocolState { Self { key_agreement_key: generate_key_agreement_key(trussed), shared_secret: None, - pin_token_v1: generate_pin_token(trussed), - pin_token_v2: generate_pin_token(trussed), + pin_token_v1: PinToken::generate(trussed), + pin_token_v2: PinToken::generate(trussed), } } pub fn reset(self, trussed: &mut T) { - syscall!(trussed.delete(self.pin_token_v1)); + self.pin_token_v1.delete(trussed); + self.pin_token_v2.delete(trussed); syscall!(trussed.delete(self.key_agreement_key)); if let Some(shared_secret) = self.shared_secret { shared_secret.delete(trussed); @@ -81,17 +134,17 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } } - fn pin_token(&self) -> KeyId { + fn pin_token(&self) -> &PinToken { match self.version { - PinProtocolVersion::V1 => self.state.pin_token_v1, - PinProtocolVersion::V2 => self.state.pin_token_v2, + PinProtocolVersion::V1 => &self.state.pin_token_v1, + PinProtocolVersion::V2 => &self.state.pin_token_v2, } } - fn set_pin_token(&mut self, pin_token: KeyId) { + fn pin_token_mut(&mut self) -> &mut PinToken { match self.version { - PinProtocolVersion::V1 => self.state.pin_token_v1 = pin_token, - PinProtocolVersion::V2 => self.state.pin_token_v2 = pin_token, + PinProtocolVersion::V1 => &mut self.state.pin_token_v1, + PinProtocolVersion::V2 => &mut self.state.pin_token_v2, } } @@ -105,9 +158,24 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { // in spec: resetPinUvAuthToken() pub fn reset_pin_token(&mut self) { - syscall!(self.trussed.delete(self.pin_token())); - let pin_token = generate_pin_token(self.trussed); - self.set_pin_token(pin_token); + let new = PinToken::generate(self.trussed); + mem::replace(self.pin_token_mut(), new).delete(self.trussed); + } + + pub fn restrict_pin_token(&mut self, permissions: Permissions, rp_id: Option>) { + let pin_token = self.pin_token_mut(); + pin_token.state.permissions = permissions; + pin_token.state.rp_id = rp_id; + } + + // in spec: beginUsingPinUvAuthToken(userIsPresent) + pub fn begin_using_pin_token(&mut self, is_user_present: bool) { + let pin_token = self.pin_token_mut(); + pin_token.state.is_user_present = is_user_present; + pin_token.state.is_user_verified = true; + // TODO: set initial usage time limit + // TODO: start and observe usage timer + pin_token.state.is_in_use = true; } // in spec: getPublicKey @@ -139,10 +207,10 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } // in spec: verify(pinUvAuthToken, ...) - pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> Result<()> { - // TODO: check if pin token is in use - if self.verify(self.pin_token(), data, signature) { - Ok(()) + pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> Result<&PinToken> { + let pin_token = self.pin_token(); + if pin_token.state.is_in_use && self.verify(pin_token.key_id, data, signature) { + Ok(self.pin_token()) } else { Err(Error::PinAuthInvalid) } @@ -165,7 +233,7 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { // in spec: encrypt(..., pinUvAuthToken) pub fn encrypt_pin_token(&mut self, shared_secret: &SharedSecret) -> Result> { - let token = shared_secret.wrap(self.trussed, self.pin_token()); + let token = shared_secret.wrap(self.trussed, self.pin_token().key_id); Bytes::from_slice(&token).map_err(|_| Error::Other) } @@ -360,10 +428,6 @@ impl SharedSecret { } } -fn generate_pin_token(trussed: &mut T) -> KeyId { - syscall!(trussed.generate_secret_key(PIN_TOKEN_LENGTH, Location::Volatile)).key -} - fn generate_key_agreement_key(trussed: &mut T) -> KeyId { syscall!(trussed.generate_p256_private_key(Location::Volatile)).key } From 079edd84c93e985bb062d6cda1fb7807bd374345 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 27 Feb 2024 14:44:51 +0100 Subject: [PATCH 050/135] credential_management: Implement UpdateUserInformation --- CHANGELOG.md | 1 + src/ctap2.rs | 24 +++++++--- src/ctap2/credential_management.rs | 70 +++++++++++++++++++++++++----- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d1539e..b72fcea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extract PIN protocol implementation into separate module ([#62][]) - Implement PIN protocol 2 ([#63][]) - Implement PIN token permissions ([#63][]) +- Implement UpdateUserInformation subcommand for CredentialManagement [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 diff --git a/src/ctap2.rs b/src/ctap2.rs index 62ba5a9..5adf984 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -990,7 +990,19 @@ impl Authenticator for crate::Authenti } // 0x7 - Subcommand::UpdateUserInformation => Err(Error::InvalidParameter), + Subcommand::UpdateUserInformation => { + let sub_parameters = sub_parameters.as_ref().ok_or(Error::MissingParameter)?; + let credential_id = sub_parameters + .credential_id + .as_ref() + .ok_or(Error::MissingParameter)?; + let user = sub_parameters + .user + .as_ref() + .ok_or(Error::MissingParameter)?; + + cred_mgmt.update_user_information(credential_id, user) + } } } @@ -1338,7 +1350,8 @@ impl crate::Authenticator { sub_command @ Subcommand::GetCredsMetadata | sub_command @ Subcommand::EnumerateRpsBegin | sub_command @ Subcommand::EnumerateCredentialsBegin - | sub_command @ Subcommand::DeleteCredential => { + | sub_command @ Subcommand::DeleteCredential + | sub_command @ Subcommand::UpdateUserInformation => { // check pinProtocol let pin_protocol = parameters // .sub_command_params.as_ref().ok_or(Error::MissingParameter)? @@ -1350,7 +1363,9 @@ impl crate::Authenticator { 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 + | Subcommand::UpdateUserInformation => { data.resize_to_capacity(); // ble, need to reserialize ctap_types::serde::cbor_serialize( @@ -1395,9 +1410,6 @@ impl crate::Authenticator { // of already checked CredMgmt subcommands Subcommand::EnumerateRpsGetNextRp | Subcommand::EnumerateCredentialsGetNextCredential => Ok(()), - - // not implemented - Subcommand::UpdateUserInformation => Err(Error::InvalidParameter), } } diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 0aba519..6ec33ac 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -3,7 +3,7 @@ use core::convert::TryFrom; use trussed::{ - syscall, + syscall, try_syscall, types::{DirEntry, Location, Path, PathBuf}, }; @@ -11,7 +11,7 @@ use ctap_types::{ cose::PublicKey, ctap2::credential_management::{CredentialProtectionPolicy, Response}, heapless_bytes::Bytes, - webauthn::PublicKeyCredentialDescriptor, + webauthn::{PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity}, Error, }; @@ -460,22 +460,27 @@ where Ok(response) } - pub fn delete_credential( - &mut self, - credential_descriptor: &PublicKeyCredentialDescriptor, - ) -> Result { - info!("delete credential"); - let credential_id_hash = self.hash(&credential_descriptor.id[..]); + fn find_credential(&mut self, credential: &PublicKeyCredentialDescriptor) -> Option { + let credential_id_hash = self.hash(&credential.id[..]); let mut hex = [b'0'; 16]; super::format_hex(&credential_id_hash[..8], &mut hex); let dir = PathBuf::from(b"rk"); let filename = PathBuf::from(&hex); - let rk_path = syscall!(self + syscall!(self .trussed .locate_file(Location::Internal, Some(dir), filename,)) .path - .ok_or(Error::InvalidCredential)?; + } + + pub fn delete_credential( + &mut self, + credential_descriptor: &PublicKeyCredentialDescriptor, + ) -> Result { + info!("delete credential"); + let rk_path = self + .find_credential(credential_descriptor) + .ok_or(Error::InvalidCredential)?; // DELETE self.delete_resident_key_by_path(&rk_path)?; @@ -491,4 +496,49 @@ where let response = Default::default(); Ok(response) } + + pub fn update_user_information( + &mut self, + credential_descriptor: &PublicKeyCredentialDescriptor, + user: &PublicKeyCredentialUserEntity, + ) -> Result { + info!("update user information"); + + // locate and parse existing credential + let rk_path = self + .find_credential(credential_descriptor) + .ok_or(Error::NoCredentials)?; + let serialized = syscall!(self.trussed.read_file(Location::Internal, rk_path.clone())).data; + let mut credential = + FullCredential::deserialize(&serialized).map_err(|_| Error::InvalidCredential)?; + + // TODO: check remaining space, return KeyStoreFull + + // the updated user ID must match the stored user ID + if credential.user.id != user.id { + error!("updated user ID does not match original user ID"); + return Err(Error::InvalidParameter); + } + + // update user name and display name unless the values are not set or empty + credential.data.user.name = user + .name + .as_ref() + .filter(|s| !s.is_empty()) + .map(Clone::clone); + credential.data.user.display_name = user + .display_name + .as_ref() + .filter(|s| !s.is_empty()) + .map(Clone::clone); + + // write updated credential + let serialized = credential.serialize()?; + try_syscall!(self + .trussed + .write_file(Location::Internal, rk_path, serialized, None)) + .map_err(|_| Error::KeyStoreFull)?; + + Ok(Default::default()) + } } From 4e3047023248cefa4c66895ab5539e4398ccc55b Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 1 Mar 2024 17:36:42 +0100 Subject: [PATCH 051/135] Support CTAP 2.1 --- CHANGELOG.md | 1 + Cargo.toml | 1 - src/ctap2.rs | 35 ++++++++++++----------------------- src/ctap2/pin.rs | 2 +- src/lib.rs | 2 ++ 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b72fcea..401939f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement PIN protocol 2 ([#63][]) - Implement PIN token permissions ([#63][]) - Implement UpdateUserInformation subcommand for CredentialManagement +- Support CTAP 2.1 [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 diff --git a/Cargo.toml b/Cargo.toml index daf1090..eb815fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,6 @@ iso7816 = { version = "0.1", optional = true } [features] dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] disable-reset-time-window = [] -enable-fido-pre = [] # enables support for a large-blob array longer than 1024 bytes chunked = ["trussed-staging/chunked"] diff --git a/src/ctap2.rs b/src/ctap2.rs index 5adf984..c84365e 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -49,13 +49,11 @@ impl Authenticator for crate::Authenti 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()) + .push(String::from_str("FIDO_2_1").unwrap()) .unwrap(); let mut extensions = Vec::, 4>::new(); - // extensions.push(String::from_str("credProtect").unwrap()).unwrap(); extensions .push(String::from_str("credProtect").unwrap()) .unwrap(); @@ -84,25 +82,15 @@ impl Authenticator for crate::Authenti true => Some(true), false => Some(false), }, - credential_mgmt_preview: Some(true), large_blobs: Some(self.config.supports_large_blobs()), pin_uv_auth_token: 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(); + if self.config.nfc_transport { + transports.push(String::from("nfc")).unwrap(); + } transports.push(String::from("usb")).unwrap(); let (_, aaguid) = self.state.identity.attestation(&mut self.trussed); @@ -170,6 +158,9 @@ impl Authenticator for crate::Authenti return Err(Error::InvalidOption); } } + if parameters.enterprise_attestation.is_some() { + return Err(Error::InvalidParameter); + } let uv_performed = self.pin_prechecks( ¶meters.options, parameters.pin_auth.map(AsRef::as_ref), @@ -1553,18 +1544,16 @@ impl crate::Authenticator { &hmac_secret.salt_auth, )?; - if hmac_secret.salt_enc.len() != 32 - && (hmac_secret.salt_enc.len() != 64 || hmac_secret.salt_enc.len() == 80) - { - debug_now!("invalid hmac-secret length"); - return Err(Error::InvalidLength); - } - // decrypt input salt_enc to get salt1 or (salt1 || salt2) let salts = shared_secret .decrypt(&mut self.trussed, &hmac_secret.salt_enc) .ok_or(Error::InvalidOption)?; + if salts.len() != 32 && salts.len() != 64 { + debug_now!("invalid hmac-secret length"); + return Err(Error::InvalidLength); + } + let mut salt_output: Bytes<64> = Bytes::new(); // output1 = hmac_sha256(credRandom, salt1) diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index 0ef9cd1..443c255 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -406,7 +406,7 @@ impl SharedSecret { data.split_at(16) } }; - try_syscall!(trussed.decrypt(Mechanism::Aes256Cbc, key_id, data, iv, b"", b"")) + try_syscall!(trussed.decrypt(Mechanism::Aes256Cbc, key_id, data, b"", iv, b"")) .ok() .and_then(|response| response.plaintext) } diff --git a/src/lib.rs b/src/lib.rs index 2351556..139ae1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,6 +103,8 @@ pub struct Config { /// /// If this is `None`, the extension and the command are disabled. pub large_blobs: Option, + /// Whether the authenticator supports the NFC transport. + pub nfc_transport: bool, } impl Config { From cdd67ae9e0321d6445724a087d2abf5bbcc55f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Mon, 4 Mar 2024 10:51:09 +0100 Subject: [PATCH 052/135] Fix compilation with chunked feature The trussed-staging dependency is out of date --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index daf1090..490447d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,8 +62,8 @@ ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git" apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "ebd27e49ca321089d01d8c9b169c4aeb58ceeeca" } serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "cff2e663841b6a68d3a8ce12647d57b2b6fbc36c" } +trussed = { git = "https://github.com/Nitrokey/trussed.git", tag = "v0.1.0-nitrokey.18" } trussed-hkdf = { git = "https://github.com/Nitrokey/trussed-hkdf-backend.git", tag = "v0.1.0" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging", rev = "3b9594d93f89a5e760fe78fa5a96f125dfdcd470" } +trussed-staging = { git = "https://github.com/Nitrokey/trussed-staging", tag = "v0.1.0-nitrokey-hmac256p256.3" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } usbd-ctaphid = { git = "https://github.com/Nitrokey/usbd-ctaphid.git", tag = "v0.1.0-nitrokey.2" } From a11a874da4397adad0b871ee0b9ae2dea035f420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Tue, 5 Mar 2024 11:11:40 +0100 Subject: [PATCH 053/135] Fix compilation of usbip example --- Cargo.toml | 2 +- examples/usbip.rs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a1be7d5..5273ef1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ description = "FIDO authenticator Trussed app" [[example]] name = "usbip" -required-features = ["trussed/virt", "dispatch"] +required-features = ["trussed-hkdf/virt", "trussed/virt", "dispatch"] [dependencies] ctap-types = { version = "0.1.0", features = ["large-blobs"] } diff --git a/examples/usbip.rs b/examples/usbip.rs index f3af0ec..280eeb5 100644 --- a/examples/usbip.rs +++ b/examples/usbip.rs @@ -15,15 +15,17 @@ const PRODUCT: &str = "Nitrokey 3"; const VID: u16 = 0x20a0; const PID: u16 = 0x42b2; -type VirtClient = ClientImplementation, CoreOnly>; +pub use trussed_hkdf::virt::Dispatcher; + +type VirtClient = ClientImplementation, Dispatcher>; struct FidoApp { fido: fido_authenticator::Authenticator, } -impl trussed_usbip::Apps<'static, VirtClient, CoreOnly> for FidoApp { +impl trussed_usbip::Apps<'static, VirtClient, Dispatcher> for FidoApp { type Data = (); - fn new>(builder: &B, _data: ()) -> Self { + fn new>(builder: &B, _data: ()) -> Self { let large_blogs = Some(fido_authenticator::LargeBlobsConfig { location: Location::External, #[cfg(feature = "chunked")] @@ -39,6 +41,7 @@ impl trussed_usbip::Apps<'static, VirtClient, CoreOnly> for FidoApp { skip_up_timeout: None, max_resident_credential_count: Some(10), large_blobs: large_blogs, + nfc_transport: false, }, ), } @@ -63,6 +66,7 @@ fn main() { pid: PID, }; trussed_usbip::Builder::new(virt::Ram::default(), options) + .dispatch(Dispatcher) .build::() .exec(|_platform| {}); } From 3db1f6fdba65ede3a05e7f0e4489145e22cde3af Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 5 Mar 2024 13:20:25 +0100 Subject: [PATCH 054/135] Optimize key and PIN token generation This patch generates the key agreement key directly after the reset to improve the execution time of the next requests. It also refactors the PIN token generation and storage so that the PIN token is only generated when needed, and only for the PIN protocol that is currently in use. --- src/ctap2.rs | 24 ++++-------- src/ctap2/pin.rs | 97 ++++++++++++++++++++++++++++++------------------ src/state.rs | 2 + 3 files changed, 70 insertions(+), 53 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index c84365e..20cbb3d 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -768,9 +768,7 @@ impl Authenticator for crate::Authenti // 9. store hashed PIN self.hash_store_pin(&new_pin)?; - for pin_protocol in self.pin_protocols() { - self.pin_protocol(*pin_protocol).reset_pin_token(); - } + self.pin_protocol(pin_protocol).reset_pin_tokens(); } // § 6.5.5.7.1 No 4 @@ -821,22 +819,18 @@ impl Authenticator for crate::Authenti // 11. Check forcePINChange -- skipped // 12. Reset all PIN tokens - for pin_protocol in self.pin_protocols() { - self.pin_protocol(*pin_protocol).reset_pin_token(); - } - // 13. Call beginUsingPinUvAuthToken let mut pin_protocol = self.pin_protocol(pin_protocol); - pin_protocol.begin_using_pin_token(false); + let mut pin_token = pin_protocol.reset_and_begin_using_pin_token(false); // 14. Assign the default permissions let mut permissions = Permissions::empty(); permissions.insert(Permissions::MAKE_CREDENTIAL); permissions.insert(Permissions::GET_ASSERTION); - pin_protocol.restrict_pin_token(permissions, None); + pin_token.restrict(permissions, None); // 15. Return PIN token - response.pin_token = Some(pin_protocol.encrypt_pin_token(&shared_secret)?); + response.pin_token = Some(pin_token.encrypt(&shared_secret)?); shared_secret.delete(&mut self.trussed); } @@ -902,20 +896,16 @@ impl Authenticator for crate::Authenti // 11. Check forcePINChange -- skipped // 12. Reset all PIN tokens - for pin_protocol in self.pin_protocols() { - self.pin_protocol(*pin_protocol).reset_pin_token(); - } - // 13. Call beginUsingPinUvAuthToken let mut pin_protocol = self.pin_protocol(pin_protocol); - pin_protocol.begin_using_pin_token(false); + let mut pin_token = pin_protocol.reset_and_begin_using_pin_token(false); // 14. Assign the requested permissions // 15. Assign the requested RP id - pin_protocol.restrict_pin_token(permissions, parameters.rp_id.clone()); + pin_token.restrict(permissions, parameters.rp_id.clone()); // 16. Return PIN token - response.pin_token = Some(pin_protocol.encrypt_pin_token(&shared_secret)?); + response.pin_token = Some(pin_token.encrypt(&shared_secret)?); shared_secret.delete(&mut self.trussed); } diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index 443c255..4156d0f 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -1,5 +1,4 @@ use crate::{cbor_serialize_message, TrussedRequirements}; -use core::mem; use ctap_types::{cose::EcdhEsHkdf256PublicKey, ctap2::client_pin::Permissions, Error, Result}; use trussed::{ cbor_deserialize, @@ -71,6 +70,25 @@ impl PinToken { } } +#[derive(Debug)] +pub struct PinTokenMut<'a, T: CryptoClient> { + pin_token: &'a mut PinToken, + trussed: &'a mut T, +} + +impl PinTokenMut<'_, T> { + pub fn restrict(&mut self, permissions: Permissions, rp_id: Option>) { + self.pin_token.state.permissions = permissions; + self.pin_token.state.rp_id = rp_id; + } + + // in spec: encrypt(..., pinUvAuthToken) + pub fn encrypt(&mut self, shared_secret: &SharedSecret) -> Result> { + let token = shared_secret.wrap(self.trussed, self.pin_token.key_id); + Bytes::from_slice(&token).map_err(|_| Error::Other) + } +} + #[derive(Debug, Default)] struct PinTokenState { permissions: Permissions, @@ -88,9 +106,9 @@ pub struct PinProtocolState { shared_secret: Option, // for protocol version 1 - pin_token_v1: PinToken, + pin_token_v1: Option, // for protocol version 2 - pin_token_v2: PinToken, + pin_token_v2: Option, } impl PinProtocolState { @@ -99,14 +117,18 @@ impl PinProtocolState { Self { key_agreement_key: generate_key_agreement_key(trussed), shared_secret: None, - pin_token_v1: PinToken::generate(trussed), - pin_token_v2: PinToken::generate(trussed), + pin_token_v1: None, + pin_token_v2: None, } } pub fn reset(self, trussed: &mut T) { - self.pin_token_v1.delete(trussed); - self.pin_token_v2.delete(trussed); + if let Some(token) = self.pin_token_v1 { + token.delete(trussed); + } + if let Some(token) = self.pin_token_v2 { + token.delete(trussed); + } syscall!(trussed.delete(self.key_agreement_key)); if let Some(shared_secret) = self.shared_secret { shared_secret.delete(trussed); @@ -134,17 +156,10 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } } - fn pin_token(&self) -> &PinToken { + fn pin_token(&self) -> Option<&PinToken> { match self.version { - PinProtocolVersion::V1 => &self.state.pin_token_v1, - PinProtocolVersion::V2 => &self.state.pin_token_v2, - } - } - - fn pin_token_mut(&mut self) -> &mut PinToken { - match self.version { - PinProtocolVersion::V1 => &mut self.state.pin_token_v1, - PinProtocolVersion::V2 => &mut self.state.pin_token_v2, + PinProtocolVersion::V1 => self.state.pin_token_v1.as_ref(), + PinProtocolVersion::V2 => self.state.pin_token_v2.as_ref(), } } @@ -157,25 +172,38 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } // in spec: resetPinUvAuthToken() - pub fn reset_pin_token(&mut self) { - let new = PinToken::generate(self.trussed); - mem::replace(self.pin_token_mut(), new).delete(self.trussed); - } - - pub fn restrict_pin_token(&mut self, permissions: Permissions, rp_id: Option>) { - let pin_token = self.pin_token_mut(); - pin_token.state.permissions = permissions; - pin_token.state.rp_id = rp_id; + pub fn reset_pin_tokens(&mut self) { + if let Some(token) = self.state.pin_token_v1.take() { + token.delete(self.trussed); + } + if let Some(token) = self.state.pin_token_v2.take() { + token.delete(self.trussed); + } } // in spec: beginUsingPinUvAuthToken(userIsPresent) - pub fn begin_using_pin_token(&mut self, is_user_present: bool) { - let pin_token = self.pin_token_mut(); + fn begin_using_pin_token(&mut self, is_user_present: bool) -> PinTokenMut<'_, T> { + // we assume that the previous PIN token has already been reset so we don’t need to delete it + let mut pin_token = PinToken::generate(self.trussed); pin_token.state.is_user_present = is_user_present; pin_token.state.is_user_verified = true; // TODO: set initial usage time limit // TODO: start and observe usage timer pin_token.state.is_in_use = true; + let pin_token_field = match self.version { + PinProtocolVersion::V1 => &mut self.state.pin_token_v1, + PinProtocolVersion::V2 => &mut self.state.pin_token_v2, + }; + let pin_token = pin_token_field.insert(pin_token); + PinTokenMut { + pin_token, + trussed: self.trussed, + } + } + + pub fn reset_and_begin_using_pin_token(&mut self, is_user_present: bool) -> PinTokenMut<'_, T> { + self.reset_pin_tokens(); + self.begin_using_pin_token(is_user_present) } // in spec: getPublicKey @@ -208,9 +236,12 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { // in spec: verify(pinUvAuthToken, ...) pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> Result<&PinToken> { - let pin_token = self.pin_token(); + let pin_token = self.pin_token().ok_or(Error::PinAuthInvalid)?; if pin_token.state.is_in_use && self.verify(pin_token.key_id, data, signature) { - Ok(self.pin_token()) + // We previously checked that `pin_token()` is not None in the first line of this + // function so this cannot panic, but we cannot return the `pin_token` variable here + // because of the `verify` call after it. + Ok(self.pin_token().unwrap()) } else { Err(Error::PinAuthInvalid) } @@ -231,12 +262,6 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } } - // in spec: encrypt(..., pinUvAuthToken) - pub fn encrypt_pin_token(&mut self, shared_secret: &SharedSecret) -> Result> { - let token = shared_secret.wrap(self.trussed, self.pin_token().key_id); - Bytes::from_slice(&token).map_err(|_| Error::Other) - } - // in spec: decapsulate(...) = ecdh(...) // The returned key ID is valid until the next call of shared_secret or regenerate. The caller // has to delete the key from the VFS after end of use. Ideally, this should be enforced by diff --git a/src/state.rs b/src/state.rs index 6999233..d593751 100644 --- a/src/state.rs +++ b/src/state.rs @@ -513,5 +513,7 @@ impl RuntimeState { if let Some(pin_protocol) = self.pin_protocol.take() { pin_protocol.reset(trussed); } + // to speed up future operations, we already generate the key agreement key + self.pin_protocol = Some(PinProtocolState::new(trussed)); } } From d55050a2491b0bd6cb6f72d1265ef038b8295c4f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 13 Mar 2024 19:11:52 +0100 Subject: [PATCH 055/135] Use trussed-chunked instead of trussed-staging This patch adapts to the extraction of the extensions from trussed-staging, see: https://github.com/trussed-dev/trussed-staging/pull/19 --- Cargo.toml | 6 +++--- src/lib.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5273ef1..75464f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } trussed = "0.1" trussed-hkdf = { version = "0.1.0" } -trussed-staging = { version = "0.1.0", default-features = false, optional = true } +trussed-chunked = { version = "0.1.0", optional = true } apdu-dispatch = { version = "0.1", optional = true } ctaphid-dispatch = { version = "0.1", optional = true } @@ -35,7 +35,7 @@ dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] disable-reset-time-window = [] # enables support for a large-blob array longer than 1024 bytes -chunked = ["trussed-staging/chunked"] +chunked = ["trussed-chunked"] log-all = [] log-none = [] @@ -62,7 +62,7 @@ apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "ebd27e49ca321089d01d8c9b169c4aeb58ceeeca" } serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } trussed = { git = "https://github.com/Nitrokey/trussed.git", tag = "v0.1.0-nitrokey.18" } +trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/Nitrokey/trussed-hkdf-backend.git", tag = "v0.1.0" } -trussed-staging = { git = "https://github.com/Nitrokey/trussed-staging", tag = "v0.1.0-nitrokey-hmac256p256.3" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } usbd-ctaphid = { git = "https://github.com/Nitrokey/usbd-ctaphid.git", tag = "v0.1.0-nitrokey.2" } diff --git a/src/lib.rs b/src/lib.rs index 139ae1c..61567df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,10 +81,10 @@ pub trait ExtensionRequirements {} impl ExtensionRequirements for T {} #[cfg(feature = "chunked")] -pub trait ExtensionRequirements: trussed_staging::streaming::ChunkedClient {} +pub trait ExtensionRequirements: trussed_chunked::ChunkedClient {} #[cfg(feature = "chunked")] -impl ExtensionRequirements for T where T: trussed_staging::streaming::ChunkedClient {} +impl ExtensionRequirements for T where T: trussed_chunked::ChunkedClient {} #[derive(Copy, Clone, Debug, Eq, PartialEq)] /// Externally defined configuration. From e1009f77c6099c94ad78715a957e86c3974a5343 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 20 Mar 2024 22:36:24 +0100 Subject: [PATCH 056/135] Improve option handling This patch replaces is_none/is_some/unwrap combinations with if-let or map to make the code easier to read and to remove the overhead for unnecessary panics. --- src/ctap2.rs | 49 +++++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 20cbb3d..332f14b 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -510,7 +510,15 @@ impl Authenticator for crate::Authenti // how browsers firefox this let (signature, attestation_algorithm) = { - if attestation_maybe.is_none() { + if let Some(attestation) = attestation_maybe.as_ref() { + let signature = syscall!(self.trussed.sign_p256( + attestation.0, + &commitment, + SignatureSerialization::Asn1Der, + )) + .signature; + (signature.to_bytes().map_err(|_| Error::Other)?, -7) + } else { match algorithm { SigningAlgorithm::Ed25519 => { let signature = @@ -544,14 +552,6 @@ impl Authenticator for crate::Authenti // (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_now!("SIG = {:?}", &signature); @@ -564,16 +564,13 @@ impl Authenticator for crate::Authenti 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) - } - }, + x5c: attestation_maybe.as_ref().map(|attestation| { + // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements + let cert = attestation.1.clone(); + let mut x5c = Vec::new(); + x5c.push(cert).ok(); + x5c + }), }; let fmt = String::<32>::from("packed"); @@ -1049,8 +1046,8 @@ impl Authenticator for crate::Authenti } // 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) + let do_up = if let Some(options) = parameters.options.as_ref() { + options.up.unwrap_or(true) } else { true }; @@ -1820,15 +1817,15 @@ impl crate::Authenticator { .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,)); - } else { + if let Some(_first_remaining_rk) = maybe_first_remaining_rk { info!( "not deleting deleting parent {:?} as there is {:?}", &rp_path, - &maybe_first_remaining_rk.unwrap().path(), + &_first_remaining_rk.path(), ); + } else { + info!("deleting parent {:?} as this was its last RK", &rp_path); + syscall!(self.trussed.remove_dir(Location::Internal, rp_path,)); } } From 32792e5aef9c074de912914c9ef0632844967a30 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 20 Mar 2024 22:52:19 +0100 Subject: [PATCH 057/135] Add CI workflow This patch adds a CI workflow based on the workflow for trussed. --- .github/workflows/ci.yml | 65 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..066a13e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + target: + - x86_64-unknown-linux-gnu + - thumbv7em-none-eabi + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Rust toolchain + run: | + rustup show + rustup target add ${{ matrix.target }} + + - name: Install build dependencies + run: > + sudo apt-get update -y -qq && + sudo apt-get install -y -qq llvm libc6-dev-i386 libclang-dev + + - uses: fiam/arm-none-eabi-gcc@v1 + with: + release: "9-2020-q2" + + - name: Build + run: cargo build --verbose --target ${{ matrix.target }} + + - name: Check all targets without default features + run: cargo check --all-targets --no-default-features + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check all targets with default features + run: cargo check --all-targets + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check all features and targets + run: cargo check --all-features --all-targets + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Run tests + run: cargo test --verbose + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check formatting + run: cargo fmt -- --check + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check clippy lints + run: cargo clippy --all-features --all-targets -- --deny warnings + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check documentation + run: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps + if: matrix.target == 'x86_64-unknown-linux-gnu' From b55d7f6b984973950be3421020fe6eac0d95dda4 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 20 Mar 2024 23:05:30 +0100 Subject: [PATCH 058/135] Fix clippy lints --- src/dispatch.rs | 4 ++-- src/dispatch/apdu.rs | 2 +- src/state.rs | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/dispatch.rs b/src/dispatch.rs index 50849e6..11c00a4 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -35,9 +35,9 @@ fn handle_ctap1_from_hid( ); { let command = apdu_dispatch::Command::try_from(data); - if let Err(status) = command { + if let Err(_status) = command { let code: [u8; 2] = (Status::IncorrectDataParameter).into(); - debug!("CTAP1 parse error: {:?} ({})", status, hex_str!(&code)); + debug!("CTAP1 parse error: {:?} ({})", _status, hex_str!(&code)); response.extend_from_slice(&code).ok(); return; } diff --git a/src/dispatch/apdu.rs b/src/dispatch/apdu.rs index 2b233f3..c868c91 100644 --- a/src/dispatch/apdu.rs +++ b/src/dispatch/apdu.rs @@ -65,7 +65,7 @@ where match instruction { // U2F instruction codes // NB(nickray): I don't think 0x00 is a valid case. - 0x00 | 0x01 | 0x02 => super::try_handle_ctap1(self, apdu, response)?, //self.call_authenticator_u2f(apdu, response), + 0x00..=0x02 => super::try_handle_ctap1(self, apdu, response)?, //self.call_authenticator_u2f(apdu, response), _ => { match ctaphid::Command::try_from(instruction) { diff --git a/src/state.rs b/src/state.rs index d593751..f4e7e48 100644 --- a/src/state.rs +++ b/src/state.rs @@ -229,7 +229,6 @@ pub struct RuntimeState { // both of these are a cache for previous Get{Next,}Assertion call cached_credentials: CredentialCache, pub active_get_assertion: Option, - channel: Option, pub cached_rp: Option, pub cached_rk: Option, From b8e9a12e3d72cf4f6d2e50f5721ca28d992208a6 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 20 Mar 2024 23:09:52 +0100 Subject: [PATCH 059/135] Fix links in doc comment --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 61567df..a980785 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,9 @@ //! //! With feature `dispatch` activated, it also implements the `App` traits //! of [`apdu_dispatch`] and [`ctaphid_dispatch`]. +//! +//! [`apdu_dispatch`]: https://docs.rs/apdu-dispatch +//! [`ctaphid_dispatch`]: https://docs.rs/ctaphid-dispatch #![cfg_attr(not(test), no_std)] // #![warn(missing_docs)] From 7db98dd73147de63f5f486dfcd177d765c7b369b Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 8 Mar 2024 23:03:44 +0100 Subject: [PATCH 060/135] Setup basic integration tests This patch adds basic integration tests that use the ctaphid library to send CTAPHID commands to the authenticator over ctaphid-dispatch. Unfortunately, usbd-ctaphid does not provide a public interface to its packet handling code, so we have to copy that code for the time being. --- .github/workflows/ci.yml | 2 +- Cargo.toml | 9 + examples/usbip.rs | 128 +++++++++++- tests/basic.rs | 44 ++++ tests/virt/dispatch.rs | 106 ++++++++++ tests/virt/mod.rs | 174 ++++++++++++++++ tests/virt/pipe.rs | 422 +++++++++++++++++++++++++++++++++++++++ tests/webauthn/mod.rs | 150 ++++++++++++++ 8 files changed, 1024 insertions(+), 11 deletions(-) create mode 100644 tests/basic.rs create mode 100644 tests/virt/dispatch.rs create mode 100644 tests/virt/mod.rs create mode 100644 tests/virt/pipe.rs create mode 100644 tests/webauthn/mod.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 066a13e..f64853a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: if: matrix.target == 'x86_64-unknown-linux-gnu' - name: Run tests - run: cargo test --verbose + run: cargo test --verbose --features dispatch if: matrix.target == 'x86_64-unknown-linux-gnu' - name: Check formatting diff --git a/Cargo.toml b/Cargo.toml index 75464f0..5a7df98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,11 +45,19 @@ log-warn = [] log-error = [] [dev-dependencies] +ciborium = { version = "0.2.2" } +ctaphid = { version = "0.3.1", default-features = false } +delog = { version = "0.1.6", features = ["std-log"] } env_logger = "0.11.0" +interchange = "0.3.0" +log = "0.4.21" # quickcheck = "1" rand = "0.8.4" +serde_cbor = { version = "0.11.0", features = ["std"] } trussed = { version = "0.1", features = ["virt"] } +trussed-hkdf = { version = "0.1", features = ["virt"] } trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } +trussed-staging = { version = "0.2.0", default-features = false, features = ["chunked"] } usbd-ctaphid = "0.1.0" [package.metadata.docs.rs] @@ -64,5 +72,6 @@ serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git" trussed = { git = "https://github.com/Nitrokey/trussed.git", tag = "v0.1.0-nitrokey.18" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/Nitrokey/trussed-hkdf-backend.git", tag = "v0.1.0" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.2.0" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } usbd-ctaphid = { git = "https://github.com/Nitrokey/usbd-ctaphid.git", tag = "v0.1.0-nitrokey.2" } diff --git a/examples/usbip.rs b/examples/usbip.rs index 280eeb5..b8cb34e 100644 --- a/examples/usbip.rs +++ b/examples/usbip.rs @@ -4,10 +4,14 @@ //! USB/IP runner for opcard. //! Run with cargo run --example usbip --features trussed/virt,dispatch -use trussed::backend::{BackendId, CoreOnly}; -use trussed::types::Location; -use trussed::virt::{self, Client, Ram, UserInterface}; -use trussed::{ClientImplementation, Platform}; +use trussed::api::{reply, request}; +use trussed::backend::BackendId; +use trussed::serde_extensions::{ExtensionDispatch, ExtensionId, ExtensionImpl}; +use trussed::service::ServiceResources; +use trussed::types::{Location, NoData}; +use trussed::virt::{self, Ram}; +use trussed::{ClientImplementation, Error, Platform}; +use trussed_hkdf::{HkdfBackend, HkdfExtension}; use trussed_usbip::ClientBuilder; const MANUFACTURER: &str = "Nitrokey"; @@ -15,17 +19,113 @@ const PRODUCT: &str = "Nitrokey 3"; const VID: u16 = 0x20a0; const PID: u16 = 0x42b2; -pub use trussed_hkdf::virt::Dispatcher; +#[derive(Default)] +struct Context { + #[cfg(feature = "chunked")] + staging: trussed_staging::StagingContext, +} + +#[derive(Default)] +struct Dispatch { + #[cfg(feature = "chunked")] + staging: trussed_staging::StagingBackend, +} + +impl ExtensionDispatch for Dispatch { + type BackendId = Backend; + type Context = Context; + type ExtensionId = Extension; + + fn extension_request( + &mut self, + backend: &Self::BackendId, + extension: &Self::ExtensionId, + ctx: &mut trussed::types::Context, + request: &request::SerdeExtension, + resources: &mut ServiceResources

, + ) -> Result { + match backend { + #[cfg(feature = "chunked")] + Backend::Staging => match extension { + Extension::Chunked => self.staging.extension_request_serialized( + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ), + _ => Err(Error::RequestNotAvailable), + }, + Backend::Hkdf => match extension { + Extension::Hkdf => HkdfBackend.extension_request_serialized( + &mut ctx.core, + &mut NoData, + request, + resources, + ), + #[cfg(feature = "chunked")] + _ => Err(Error::RequestNotAvailable), + }, + } + } +} + +#[cfg(feature = "chunked")] +impl ExtensionId for Dispatch { + type Id = Extension; + + const ID: Extension = Extension::Chunked; +} + +impl ExtensionId for Dispatch { + type Id = Extension; + + const ID: Extension = Extension::Hkdf; +} + +enum Backend { + #[cfg(feature = "chunked")] + Staging, + Hkdf, +} + +enum Extension { + #[cfg(feature = "chunked")] + Chunked, + Hkdf, +} + +impl From for u8 { + fn from(extension: Extension) -> u8 { + match extension { + #[cfg(feature = "chunked")] + Extension::Chunked => 0, + Extension::Hkdf => 1, + } + } +} + +impl TryFrom for Extension { + type Error = Error; + + fn try_from(id: u8) -> Result { + match id { + #[cfg(feature = "chunked")] + 0 => Ok(Self::Chunked), + 1 => Ok(Self::Hkdf), + _ => Err(Error::InternalError), + } + } +} -type VirtClient = ClientImplementation, Dispatcher>; +type VirtClient = ClientImplementation, Dispatch>; struct FidoApp { fido: fido_authenticator::Authenticator, } -impl trussed_usbip::Apps<'static, VirtClient, Dispatcher> for FidoApp { +impl trussed_usbip::Apps<'static, VirtClient, Dispatch> for FidoApp { type Data = (); - fn new>(builder: &B, _data: ()) -> Self { + fn new>(builder: &B, _data: ()) -> Self { let large_blogs = Some(fido_authenticator::LargeBlobsConfig { location: Location::External, #[cfg(feature = "chunked")] @@ -34,7 +134,15 @@ impl trussed_usbip::Apps<'static, VirtClient, Dispatcher> for FidoApp { FidoApp { fido: fido_authenticator::Authenticator::new( - builder.build("fido", &[BackendId::Core]), + builder.build( + "fido", + &[ + BackendId::Core, + BackendId::Custom(Backend::Hkdf), + #[cfg(feature = "chunked")] + BackendId::Custom(Backend::Staging), + ], + ), fido_authenticator::Conforming {}, fido_authenticator::Config { max_msg_size: usbd_ctaphid::constants::MESSAGE_SIZE, @@ -66,7 +174,7 @@ fn main() { pid: PID, }; trussed_usbip::Builder::new(virt::Ram::default(), options) - .dispatch(Dispatcher) + .dispatch(Dispatch::default()) .build::() .exec(|_platform| {}); } diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..0e2030a --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,44 @@ +#![cfg(feature = "dispatch")] + +mod virt; +mod webauthn; + +use std::collections::BTreeMap; + +use ciborium::Value; +use ctap_types::ctap2::Operation; + +use webauthn::{MakeCredentialRequest, PubKeyCredParam, Rp, User}; + +#[test] +fn test_ping() { + virt::run_ctaphid(|device| { + device.ping(&[0xf1, 0xd0]).unwrap(); + }); +} + +#[test] +fn test_get_info() { + virt::run_ctap2(|device| { + let reply: BTreeMap = device.call(Operation::GetInfo, &Value::Null); + let versions: Vec = reply.get(&1).unwrap().deserialized().unwrap(); + assert!(versions.contains(&"FIDO_2_0".to_owned())); + assert!(versions.contains(&"FIDO_2_1".to_owned())); + }); +} + +#[test] +fn test_make_credential() { + virt::run_ctap2(|device| { + let rp = Rp::new("example.com"); + let user = User::new(b"id123") + .name("john.doe") + .display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let request = MakeCredentialRequest::new(b"", rp, user, pub_key_cred_params); + let reply: BTreeMap = device.call(Operation::MakeCredential, &request.into()); + assert_eq!(reply.get(&1).unwrap(), &Value::from("packed")); + assert!(reply.contains_key(&2)); + assert!(reply.contains_key(&3)); + }); +} diff --git a/tests/virt/dispatch.rs b/tests/virt/dispatch.rs new file mode 100644 index 0000000..a2751a9 --- /dev/null +++ b/tests/virt/dispatch.rs @@ -0,0 +1,106 @@ +use trussed::{ + api::{reply, request}, + serde_extensions::{ExtensionDispatch, ExtensionId, ExtensionImpl}, + service::ServiceResources, + types::NoData, + Error, Platform, +}; +use trussed_hkdf::{HkdfBackend, HkdfExtension}; + +#[derive(Default)] +pub struct Context { + #[cfg(feature = "chunked")] + staging: trussed_staging::StagingContext, +} + +#[derive(Default)] +pub struct Dispatch { + #[cfg(feature = "chunked")] + staging: trussed_staging::StagingBackend, +} + +impl ExtensionDispatch for Dispatch { + type BackendId = Backend; + type Context = Context; + type ExtensionId = Extension; + + fn extension_request( + &mut self, + backend: &Self::BackendId, + extension: &Self::ExtensionId, + ctx: &mut trussed::types::Context, + request: &request::SerdeExtension, + resources: &mut ServiceResources

, + ) -> Result { + match backend { + #[cfg(feature = "chunked")] + Backend::Staging => match extension { + Extension::Chunked => self.staging.extension_request_serialized( + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ), + _ => Err(Error::RequestNotAvailable), + }, + Backend::Hkdf => match extension { + Extension::Hkdf => HkdfBackend.extension_request_serialized( + &mut ctx.core, + &mut NoData, + request, + resources, + ), + #[cfg(feature = "chunked")] + _ => Err(Error::RequestNotAvailable), + }, + } + } +} + +#[cfg(feature = "chunked")] +impl ExtensionId for Dispatch { + type Id = Extension; + + const ID: Extension = Extension::Chunked; +} + +impl ExtensionId for Dispatch { + type Id = Extension; + + const ID: Extension = Extension::Hkdf; +} + +pub enum Backend { + #[cfg(feature = "chunked")] + Staging, + Hkdf, +} + +pub enum Extension { + #[cfg(feature = "chunked")] + Chunked, + Hkdf, +} + +impl From for u8 { + fn from(extension: Extension) -> u8 { + match extension { + #[cfg(feature = "chunked")] + Extension::Chunked => 0, + Extension::Hkdf => 1, + } + } +} + +impl TryFrom for Extension { + type Error = Error; + + fn try_from(id: u8) -> Result { + match id { + #[cfg(feature = "chunked")] + 0 => Ok(Self::Chunked), + 1 => Ok(Self::Hkdf), + _ => Err(Error::InternalError), + } + } +} diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs new file mode 100644 index 0000000..e98fa1e --- /dev/null +++ b/tests/virt/mod.rs @@ -0,0 +1,174 @@ +mod dispatch; +mod pipe; + +use std::{ + borrow::Cow, + cell::RefCell, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Once, OnceLock, + }, + thread, + time::{Duration, SystemTime}, +}; + +use ciborium::Value; +use ctap_types::ctap2::Operation; +use ctaphid::{ + error::{RequestError, ResponseError}, + HidDevice, HidDeviceInfo, +}; +use ctaphid_dispatch::{ + dispatch::Dispatch, + types::{Channel, Requester}, +}; +use fido_authenticator::{Authenticator, Config, Conforming}; +use serde::de::DeserializeOwned; +use trussed::{ + backend::BackendId, + virt::{self, Ram}, +}; + +use pipe::Pipe; + +static INIT_LOGGER: Once = Once::new(); +static CHANNEL: OnceLock = OnceLock::new(); + +pub fn run_ctaphid(f: F) -> T +where + F: FnOnce(ctaphid::Device) -> T, +{ + INIT_LOGGER.call_once(|| { + env_logger::init(); + }); + virt::with_platform(Ram::default(), |platform| { + platform.run_client_with_backends( + "fido", + dispatch::Dispatch::default(), + &[ + BackendId::Core, + BackendId::Custom(dispatch::Backend::Hkdf), + #[cfg(feature = "chunked")] + BackendId::Custom(dispatch::Backend::Staging), + ], + |client| { + // TODO: setup attestation cert + + let mut authenticator = Authenticator::new( + client, + Conforming {}, + Config { + max_msg_size: 0, + skip_up_timeout: None, + max_resident_credential_count: None, + large_blobs: None, + nfc_transport: false, + }, + ); + + let channel = CHANNEL.get_or_init(Channel::new); + let (rq, rp) = channel.split().unwrap(); + + let stop = Arc::new(AtomicBool::new(false)); + let poller_stop = stop.clone(); + let poller = thread::spawn(move || { + let mut dispatch = Dispatch::new(rp); + while !poller_stop.load(Ordering::Relaxed) { + dispatch.poll(&mut [&mut authenticator]); + thread::sleep(Duration::from_millis(10)); + } + }); + + let device = Device::new(rq); + let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); + let result = f(device); + stop.store(true, Ordering::Relaxed); + poller.join().unwrap(); + result + }, + ) + }) +} + +pub fn run_ctap2(f: F) -> T +where + F: FnOnce(Ctap2) -> T, +{ + run_ctaphid(|device| f(Ctap2(device))) +} + +pub struct Ctap2(ctaphid::Device); + +impl Ctap2 { + pub fn call(&self, operation: Operation, data: &Value) -> T { + let mut serialized = Vec::new(); + ciborium::into_writer(data, &mut serialized).unwrap(); + let reply = self.0.ctap2(operation.into(), &serialized).unwrap(); + ciborium::from_reader(reply.as_slice()).unwrap() + } +} + +#[derive(Debug)] +pub struct DeviceInfo; + +impl HidDeviceInfo for DeviceInfo { + fn vendor_id(&self) -> u16 { + 0x20a0 + } + + fn product_id(&self) -> u16 { + 0x42b2 + } + + fn path(&self) -> Cow<'_, str> { + "test".into() + } +} + +pub struct Device(RefCell); + +impl Device { + fn new(requester: Requester<'static>) -> Self { + Self(RefCell::new(Pipe::new(requester))) + } +} + +impl HidDevice for Device { + type Info = DeviceInfo; + + fn send(&self, data: &[u8]) -> Result<(), RequestError> { + self.0.borrow_mut().push(data); + Ok(()) + } + + fn receive<'a>( + &self, + buffer: &'a mut [u8], + timeout: Option, + ) -> Result<&'a [u8], ResponseError> { + let start = SystemTime::now(); + + loop { + if let Some(timeout) = timeout { + let elapsed = start.elapsed().unwrap(); + if elapsed >= timeout { + return Err(ResponseError::Timeout); + } + } + + if let Some(response) = self.0.borrow_mut().pop() { + return if buffer.len() >= response.len() { + log::info!("received response: {} bytes", response.len()); + buffer[..response.len()].copy_from_slice(&response); + Ok(&buffer[..response.len()]) + } else { + Err(ResponseError::PacketReceivingFailed( + "invalid buffer size".into(), + )) + }; + } + + thread::sleep(Duration::from_millis(10)); + } + } +} diff --git a/tests/virt/pipe.rs b/tests/virt/pipe.rs new file mode 100644 index 0000000..0ecfefc --- /dev/null +++ b/tests/virt/pipe.rs @@ -0,0 +1,422 @@ +// Extracted from the usbd-ctaphid crate: +// https://github.com/trussed-dev/usbd-ctaphid/blob/1db2e014f28669bc484c81ab0406c54b16bba33c/src/pipe.rs +// +// License: Apache-2.0 or MIT +// +// Authors: +// - Conor Patrick +// - Nicolas Stalder +// - Robin Krahl +// - Sosthène Guédon + +use std::collections::VecDeque; + +use ctap_types::Error; +use ctaphid_dispatch::{command::Command, types::Requester}; +use heapless::Vec; + +const MESSAGE_SIZE: usize = 3072; +const PACKET_SIZE: usize = 64; + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +struct Version { + major: u8, + minor: u8, + build: u8, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct Request { + channel: u32, + command: Command, + length: u16, + timestamp: u32, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct Response { + channel: u32, + command: Command, + length: u16, +} + +impl Response { + fn from_request_and_size(request: Request, size: usize) -> Self { + Self { + channel: request.channel, + command: request.command, + length: size as u16, + } + } + + fn error_from_request(request: Request) -> Self { + Self::error_on_channel(request.channel) + } + + fn error_on_channel(channel: u32) -> Self { + Self { + channel, + command: Command::Error, + length: 1, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct MessageState { + next_sequence: u8, + transmitted: usize, +} + +impl Default for MessageState { + fn default() -> Self { + Self { + next_sequence: 0, + transmitted: PACKET_SIZE - 7, + } + } +} + +impl MessageState { + fn absorb_packet(&mut self) { + self.next_sequence += 1; + self.transmitted += PACKET_SIZE - 5; + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum State { + Idle, + Receiving((Request, MessageState)), + WaitingOnAuthenticator(Request), + WaitingToSend(Response), + Sending((Response, MessageState)), +} + +pub struct Pipe { + queue: VecDeque<[u8; PACKET_SIZE]>, + state: State, + interchange: Requester<'static>, + buffer: [u8; MESSAGE_SIZE], + last_channel: u32, + implements: u8, + last_milliseconds: u32, + started_processing: bool, + needs_keepalive: bool, + version: Version, +} + +impl Pipe { + pub fn new(interchange: Requester<'static>) -> Self { + Self { + queue: Default::default(), + state: State::Idle, + interchange, + buffer: [0; MESSAGE_SIZE], + last_channel: Default::default(), + implements: 0x84, + last_milliseconds: Default::default(), + started_processing: Default::default(), + needs_keepalive: Default::default(), + version: Default::default(), + } + } + + pub fn push(&mut self, packet: &[u8]) { + let (_, packet) = packet.split_first().unwrap(); + self.read_and_handle_packet(packet); + } + + pub fn pop(&mut self) -> Option<[u8; PACKET_SIZE]> { + self.handle_response(); + self.maybe_write_packet(); + self.queue.pop_front() + } + + fn read_and_handle_packet(&mut self, packet: &[u8]) { + if packet.len() != PACKET_SIZE { + panic!("unexpected packet size"); + } + let channel = u32::from_be_bytes(packet[..4].try_into().unwrap()); + let is_initialization = (packet[4] >> 7) != 0; + if is_initialization { + let command_number = packet[4] & !0x80; + let Ok(command) = Command::try_from(command_number) else { + self.start_sending_error_on_channel(channel, Error::InvalidCommand); + return; + }; + let length = u16::from_be_bytes(packet[5..][..2].try_into().unwrap()); + let timestamp = self.last_milliseconds; + let current_request = Request { + channel, + command, + length, + timestamp, + }; + + if !(self.state == State::Idle) { + let request = match self.state { + State::WaitingOnAuthenticator(request) => request, + State::Receiving((request, _message_state)) => request, + _ => { + return; + } + }; + if packet[4] == 0x86 { + // self.cancel_ongoing_activity(); + } else { + if channel == request.channel { + if command == Command::Cancel { + // self.cancel_ongoing_activity(); + } else { + self.start_sending_error(request, Error::InvalidSeq); + } + } else { + self.send_error_now(current_request, Error::ChannelBusy); + } + + return; + } + } + + if length > MESSAGE_SIZE as u16 { + self.send_error_now(current_request, Error::InvalidLength); + return; + } + + if length > PACKET_SIZE as u16 - 7 { + self.buffer[..PACKET_SIZE - 7].copy_from_slice(&packet[7..]); + self.state = State::Receiving((current_request, { MessageState::default() })); + } else { + self.buffer[..length as usize].copy_from_slice(&packet[7..][..length as usize]); + self.dispatch_request(current_request); + } + } else { + match self.state { + State::Receiving((request, mut message_state)) => { + let sequence = packet[4]; + if sequence != message_state.next_sequence { + self.start_sending_error(request, Error::InvalidSeq); + return; + } + if channel != request.channel { + return; + } + + let payload_length = request.length as usize; + if message_state.transmitted + (PACKET_SIZE - 5) < payload_length { + self.buffer[message_state.transmitted..][..PACKET_SIZE - 5] + .copy_from_slice(&packet[5..]); + message_state.absorb_packet(); + self.state = State::Receiving((request, message_state)); + } else { + let missing = request.length as usize - message_state.transmitted; + self.buffer[message_state.transmitted..payload_length] + .copy_from_slice(&packet[5..][..missing]); + self.dispatch_request(request); + } + } + _ => { + panic!("unexpected continuation packet"); + } + } + } + } + + fn start_sending(&mut self, response: Response) { + self.state = State::WaitingToSend(response); + self.maybe_write_packet(); + } + + fn start_sending_error(&mut self, request: Request, error: Error) { + self.start_sending_error_on_channel(request.channel, error); + } + + fn start_sending_error_on_channel(&mut self, channel: u32, error: Error) { + self.buffer[0] = error as u8; + let response = Response::error_on_channel(channel); + self.start_sending(response); + } + + fn send_error_now(&mut self, request: Request, error: Error) { + let last_state = core::mem::replace(&mut self.state, State::Idle); + let last_first_byte = self.buffer[0]; + + self.buffer[0] = error as u8; + let response = Response::error_from_request(request); + self.start_sending(response); + self.maybe_write_packet(); + + self.state = last_state; + self.buffer[0] = last_first_byte; + } + + fn maybe_write_packet(&mut self) { + match self.state { + State::WaitingToSend(response) => { + let mut packet = [0u8; PACKET_SIZE]; + packet[..4].copy_from_slice(&response.channel.to_be_bytes()); + packet[4] = response.command.into_u8() | 0x80; + packet[5..7].copy_from_slice(&response.length.to_be_bytes()); + + let fits_in_one_packet = 7 + response.length as usize <= PACKET_SIZE; + if fits_in_one_packet { + packet[7..][..response.length as usize] + .copy_from_slice(&self.buffer[..response.length as usize]); + self.state = State::Idle; + } else { + packet[7..].copy_from_slice(&self.buffer[..PACKET_SIZE - 7]); + } + + self.queue.push_back(packet); + + if fits_in_one_packet { + self.state = State::Idle; + } else { + self.state = State::Sending((response, MessageState::default())); + } + } + State::Sending((response, mut message_state)) => { + let mut packet = [0u8; PACKET_SIZE]; + packet[..4].copy_from_slice(&response.channel.to_be_bytes()); + packet[4] = message_state.next_sequence; + + let sent = message_state.transmitted; + let remaining = response.length as usize - sent; + let last_packet = 5 + remaining <= PACKET_SIZE; + if last_packet { + packet[5..][..remaining] + .copy_from_slice(&self.buffer[message_state.transmitted..][..remaining]); + } else { + packet[5..].copy_from_slice( + &self.buffer[message_state.transmitted..][..PACKET_SIZE - 5], + ); + } + + self.queue.push_back(packet); + + if last_packet { + self.state = State::Idle; + } else { + message_state.absorb_packet(); + self.state = State::Sending((response, message_state)); + } + } + _ => {} + } + } + + fn dispatch_request(&mut self, request: Request) { + match request.command { + Command::Init => {} + _ => { + if request.channel == 0xffffffff { + self.start_sending_error(request, Error::InvalidChannel); + return; + } + } + } + match request.command { + Command::Init => { + match request.channel { + 0 => { + self.start_sending_error(request, Error::InvalidChannel); + } + cid => { + if request.length == 8 { + self.last_channel += 1; + let _nonce = &self.buffer[..8]; + let response = Response { + channel: cid, + command: request.command, + length: 17, + }; + + self.buffer[8..12].copy_from_slice(&self.last_channel.to_be_bytes()); + // CTAPHID protocol version + self.buffer[12] = 2; + // major device version number + self.buffer[13] = self.version.major; + // minor device version number + self.buffer[14] = self.version.minor; + // build device version number + self.buffer[15] = self.version.build; + // capabilities flags + // 0x1: implements WINK + // 0x4: implements CBOR + // 0x8: does not implement MSG + // self.buffer[16] = 0x01 | 0x08; + self.buffer[16] = self.implements; + self.start_sending(response); + } + } + } + } + + Command::Ping => { + let response = Response::from_request_and_size(request, request.length as usize); + self.start_sending(response); + } + + Command::Cancel => { + // self.cancel_ongoing_activity(); + } + + _ => { + self.needs_keepalive = request.command == Command::Cbor; + if self.interchange.state() == interchange::State::Responded { + self.interchange.take_response(); + } + match self.interchange.request(( + request.command, + Vec::from_slice(&self.buffer[..request.length as usize]).unwrap(), + )) { + Ok(_) => { + self.state = State::WaitingOnAuthenticator(request); + self.started_processing = true; + } + Err(_) => { + self.send_error_now(request, Error::ChannelBusy); + } + } + } + } + } + + fn handle_response(&mut self) { + if let State::WaitingOnAuthenticator(request) = self.state { + if let Ok(response) = self.interchange.response() { + match &response.0 { + Err(ctaphid_dispatch::app::Error::InvalidCommand) => { + self.start_sending_error(request, Error::InvalidCommand); + } + Err(ctaphid_dispatch::app::Error::InvalidLength) => { + self.start_sending_error(request, Error::InvalidLength); + } + Err(ctaphid_dispatch::app::Error::NoResponse) => { + log::info!("Got waiting noresponse from authenticator??"); + } + + Ok(message) => { + if message.len() > self.buffer.len() { + log::error!( + "Message is longer than buffer ({} > {})", + message.len(), + self.buffer.len(), + ); + self.start_sending_error(request, Error::InvalidLength); + } else { + log::info!( + "Got {} bytes response from authenticator, starting send", + message.len() + ); + let response = Response::from_request_and_size(request, message.len()); + self.buffer[..message.len()].copy_from_slice(message); + self.start_sending(response); + } + } + } + } + } + } +} diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs new file mode 100644 index 0000000..5236bb9 --- /dev/null +++ b/tests/webauthn/mod.rs @@ -0,0 +1,150 @@ +use ciborium::Value; + +#[derive(Default)] +pub struct Map(Vec<(Value, Value)>); + +impl Map { + pub fn push(&mut self, key: impl Into, value: impl Into) { + self.0.push((key.into(), value.into())); + } +} + +impl From for Value { + fn from(map: Map) -> Value { + Value::from(map.0) + } +} + +pub struct Rp { + id: String, + name: Option, +} + +impl Rp { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + name: None, + } + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } +} + +impl From for Value { + fn from(rp: Rp) -> Value { + let mut map = Map::default(); + map.push("id", rp.id); + if let Some(name) = rp.name { + map.push("name", name); + } + map.into() + } +} + +pub struct User { + id: Vec, + name: Option, + display_name: Option, +} + +impl User { + pub fn new(id: impl Into>) -> Self { + Self { + id: id.into(), + name: None, + display_name: None, + } + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn display_name(mut self, display_name: impl Into) -> Self { + self.display_name = Some(display_name.into()); + self + } +} + +impl From for Value { + fn from(user: User) -> Value { + let mut map = Map::default(); + map.push("id", user.id); + if let Some(name) = user.name { + map.push("name", name); + } + if let Some(display_name) = user.display_name { + map.push("displayName", display_name); + } + map.into() + } +} + +pub struct PubKeyCredParam { + ty: String, + alg: i32, +} + +impl PubKeyCredParam { + pub fn new(ty: impl Into, alg: impl Into) -> Self { + Self { + ty: ty.into(), + alg: alg.into(), + } + } +} + +impl From for Value { + fn from(param: PubKeyCredParam) -> Value { + let mut map = Map::default(); + map.push("type", param.ty); + map.push("alg", param.alg); + map.into() + } +} + +pub struct MakeCredentialRequest { + client_data_hash: Vec, + rp: Rp, + user: User, + pub_key_cred_params: Vec, +} + +impl MakeCredentialRequest { + pub fn new( + client_data_hash: impl Into>, + rp: Rp, + user: User, + pub_key_cred_params: impl Into>, + ) -> Self { + Self { + client_data_hash: client_data_hash.into(), + rp, + user, + pub_key_cred_params: pub_key_cred_params.into(), + } + } +} + +impl From for Value { + fn from(request: MakeCredentialRequest) -> Value { + let mut map = Map::default(); + map.push(1, request.client_data_hash); + map.push(2, request.rp); + map.push(3, request.user); + map.push( + 4, + request + .pub_key_cred_params + .into_iter() + .map(Value::from) + .collect::>(), + ); + map.into() + } +} From db4a63dd582784c847520136da930f172330ef28 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 25 Mar 2024 13:41:03 +0100 Subject: [PATCH 061/135] Update trussed-hkdf to v0.2.0 With this patch, we update trussed-hkdf to v0.2.0, the version that is provided by the trussed-staging crate. This also means we no longer have to manually implement the dispatch for the usbip example and the tests and can instead use trussed_staging::virt::Dispatcher. --- Cargo.toml | 11 ++-- examples/usbip.rs | 129 +++++------------------------------------ tests/virt/dispatch.rs | 106 --------------------------------- tests/virt/mod.rs | 84 +++++++++++---------------- 4 files changed, 53 insertions(+), 277 deletions(-) delete mode 100644 tests/virt/dispatch.rs diff --git a/Cargo.toml b/Cargo.toml index 5a7df98..1a6209b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ description = "FIDO authenticator Trussed app" [[example]] name = "usbip" -required-features = ["trussed-hkdf/virt", "trussed/virt", "dispatch"] +required-features = ["dispatch"] [dependencies] ctap-types = { version = "0.1.0", features = ["large-blobs"] } @@ -23,7 +23,7 @@ serde_cbor = { version = "0.11.0", default-features = false } serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } trussed = "0.1" -trussed-hkdf = { version = "0.1.0" } +trussed-hkdf = { version = "0.2.0" } trussed-chunked = { version = "0.1.0", optional = true } apdu-dispatch = { version = "0.1", optional = true } @@ -55,9 +55,8 @@ log = "0.4.21" rand = "0.8.4" serde_cbor = { version = "0.11.0", features = ["std"] } trussed = { version = "0.1", features = ["virt"] } -trussed-hkdf = { version = "0.1", features = ["virt"] } trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } -trussed-staging = { version = "0.2.0", default-features = false, features = ["chunked"] } +trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] } usbd-ctaphid = "0.1.0" [package.metadata.docs.rs] @@ -71,7 +70,7 @@ littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "ebd27 serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } trussed = { git = "https://github.com/Nitrokey/trussed.git", tag = "v0.1.0-nitrokey.18" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } -trussed-hkdf = { git = "https://github.com/Nitrokey/trussed-hkdf-backend.git", tag = "v0.1.0" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.2.0" } +trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } usbd-ctaphid = { git = "https://github.com/Nitrokey/usbd-ctaphid.git", tag = "v0.1.0-nitrokey.2" } diff --git a/examples/usbip.rs b/examples/usbip.rs index b8cb34e..7e2f20f 100644 --- a/examples/usbip.rs +++ b/examples/usbip.rs @@ -2,16 +2,15 @@ // SPDX-License-Identifier: CC0-1.0 //! USB/IP runner for opcard. -//! Run with cargo run --example usbip --features trussed/virt,dispatch - -use trussed::api::{reply, request}; -use trussed::backend::BackendId; -use trussed::serde_extensions::{ExtensionDispatch, ExtensionId, ExtensionImpl}; -use trussed::service::ServiceResources; -use trussed::types::{Location, NoData}; -use trussed::virt::{self, Ram}; -use trussed::{ClientImplementation, Error, Platform}; -use trussed_hkdf::{HkdfBackend, HkdfExtension}; +//! Run with cargo run --example usbip --features dispatch + +use trussed::{ + backend::BackendId, + types::Location, + virt::{self, Ram}, + ClientImplementation, +}; +use trussed_staging::virt::{BackendIds, Dispatcher}; use trussed_usbip::ClientBuilder; const MANUFACTURER: &str = "Nitrokey"; @@ -19,113 +18,15 @@ const PRODUCT: &str = "Nitrokey 3"; const VID: u16 = 0x20a0; const PID: u16 = 0x42b2; -#[derive(Default)] -struct Context { - #[cfg(feature = "chunked")] - staging: trussed_staging::StagingContext, -} - -#[derive(Default)] -struct Dispatch { - #[cfg(feature = "chunked")] - staging: trussed_staging::StagingBackend, -} - -impl ExtensionDispatch for Dispatch { - type BackendId = Backend; - type Context = Context; - type ExtensionId = Extension; - - fn extension_request( - &mut self, - backend: &Self::BackendId, - extension: &Self::ExtensionId, - ctx: &mut trussed::types::Context, - request: &request::SerdeExtension, - resources: &mut ServiceResources

, - ) -> Result { - match backend { - #[cfg(feature = "chunked")] - Backend::Staging => match extension { - Extension::Chunked => self.staging.extension_request_serialized( - &mut ctx.core, - &mut ctx.backends.staging, - request, - resources, - ), - _ => Err(Error::RequestNotAvailable), - }, - Backend::Hkdf => match extension { - Extension::Hkdf => HkdfBackend.extension_request_serialized( - &mut ctx.core, - &mut NoData, - request, - resources, - ), - #[cfg(feature = "chunked")] - _ => Err(Error::RequestNotAvailable), - }, - } - } -} - -#[cfg(feature = "chunked")] -impl ExtensionId for Dispatch { - type Id = Extension; - - const ID: Extension = Extension::Chunked; -} - -impl ExtensionId for Dispatch { - type Id = Extension; - - const ID: Extension = Extension::Hkdf; -} - -enum Backend { - #[cfg(feature = "chunked")] - Staging, - Hkdf, -} - -enum Extension { - #[cfg(feature = "chunked")] - Chunked, - Hkdf, -} - -impl From for u8 { - fn from(extension: Extension) -> u8 { - match extension { - #[cfg(feature = "chunked")] - Extension::Chunked => 0, - Extension::Hkdf => 1, - } - } -} - -impl TryFrom for Extension { - type Error = Error; - - fn try_from(id: u8) -> Result { - match id { - #[cfg(feature = "chunked")] - 0 => Ok(Self::Chunked), - 1 => Ok(Self::Hkdf), - _ => Err(Error::InternalError), - } - } -} - -type VirtClient = ClientImplementation, Dispatch>; +type VirtClient = ClientImplementation, Dispatcher>; struct FidoApp { fido: fido_authenticator::Authenticator, } -impl trussed_usbip::Apps<'static, VirtClient, Dispatch> for FidoApp { +impl trussed_usbip::Apps<'static, VirtClient, Dispatcher> for FidoApp { type Data = (); - fn new>(builder: &B, _data: ()) -> Self { + fn new>(builder: &B, _data: ()) -> Self { let large_blogs = Some(fido_authenticator::LargeBlobsConfig { location: Location::External, #[cfg(feature = "chunked")] @@ -138,9 +39,7 @@ impl trussed_usbip::Apps<'static, VirtClient, Dispatch> for FidoApp { "fido", &[ BackendId::Core, - BackendId::Custom(Backend::Hkdf), - #[cfg(feature = "chunked")] - BackendId::Custom(Backend::Staging), + BackendId::Custom(BackendIds::StagingBackend), ], ), fido_authenticator::Conforming {}, @@ -174,7 +73,7 @@ fn main() { pid: PID, }; trussed_usbip::Builder::new(virt::Ram::default(), options) - .dispatch(Dispatch::default()) + .dispatch(Dispatcher::default()) .build::() .exec(|_platform| {}); } diff --git a/tests/virt/dispatch.rs b/tests/virt/dispatch.rs deleted file mode 100644 index a2751a9..0000000 --- a/tests/virt/dispatch.rs +++ /dev/null @@ -1,106 +0,0 @@ -use trussed::{ - api::{reply, request}, - serde_extensions::{ExtensionDispatch, ExtensionId, ExtensionImpl}, - service::ServiceResources, - types::NoData, - Error, Platform, -}; -use trussed_hkdf::{HkdfBackend, HkdfExtension}; - -#[derive(Default)] -pub struct Context { - #[cfg(feature = "chunked")] - staging: trussed_staging::StagingContext, -} - -#[derive(Default)] -pub struct Dispatch { - #[cfg(feature = "chunked")] - staging: trussed_staging::StagingBackend, -} - -impl ExtensionDispatch for Dispatch { - type BackendId = Backend; - type Context = Context; - type ExtensionId = Extension; - - fn extension_request( - &mut self, - backend: &Self::BackendId, - extension: &Self::ExtensionId, - ctx: &mut trussed::types::Context, - request: &request::SerdeExtension, - resources: &mut ServiceResources

, - ) -> Result { - match backend { - #[cfg(feature = "chunked")] - Backend::Staging => match extension { - Extension::Chunked => self.staging.extension_request_serialized( - &mut ctx.core, - &mut ctx.backends.staging, - request, - resources, - ), - _ => Err(Error::RequestNotAvailable), - }, - Backend::Hkdf => match extension { - Extension::Hkdf => HkdfBackend.extension_request_serialized( - &mut ctx.core, - &mut NoData, - request, - resources, - ), - #[cfg(feature = "chunked")] - _ => Err(Error::RequestNotAvailable), - }, - } - } -} - -#[cfg(feature = "chunked")] -impl ExtensionId for Dispatch { - type Id = Extension; - - const ID: Extension = Extension::Chunked; -} - -impl ExtensionId for Dispatch { - type Id = Extension; - - const ID: Extension = Extension::Hkdf; -} - -pub enum Backend { - #[cfg(feature = "chunked")] - Staging, - Hkdf, -} - -pub enum Extension { - #[cfg(feature = "chunked")] - Chunked, - Hkdf, -} - -impl From for u8 { - fn from(extension: Extension) -> u8 { - match extension { - #[cfg(feature = "chunked")] - Extension::Chunked => 0, - Extension::Hkdf => 1, - } - } -} - -impl TryFrom for Extension { - type Error = Error; - - fn try_from(id: u8) -> Result { - match id { - #[cfg(feature = "chunked")] - 0 => Ok(Self::Chunked), - 1 => Ok(Self::Hkdf), - _ => Err(Error::InternalError), - } - } -} diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index e98fa1e..7db0968 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -1,4 +1,3 @@ -mod dispatch; mod pipe; use std::{ @@ -24,10 +23,7 @@ use ctaphid_dispatch::{ }; use fido_authenticator::{Authenticator, Config, Conforming}; use serde::de::DeserializeOwned; -use trussed::{ - backend::BackendId, - virt::{self, Ram}, -}; +use trussed_staging::virt; use pipe::Pipe; @@ -41,52 +37,40 @@ where INIT_LOGGER.call_once(|| { env_logger::init(); }); - virt::with_platform(Ram::default(), |platform| { - platform.run_client_with_backends( - "fido", - dispatch::Dispatch::default(), - &[ - BackendId::Core, - BackendId::Custom(dispatch::Backend::Hkdf), - #[cfg(feature = "chunked")] - BackendId::Custom(dispatch::Backend::Staging), - ], - |client| { - // TODO: setup attestation cert - - let mut authenticator = Authenticator::new( - client, - Conforming {}, - Config { - max_msg_size: 0, - skip_up_timeout: None, - max_resident_credential_count: None, - large_blobs: None, - nfc_transport: false, - }, - ); - - let channel = CHANNEL.get_or_init(Channel::new); - let (rq, rp) = channel.split().unwrap(); - - let stop = Arc::new(AtomicBool::new(false)); - let poller_stop = stop.clone(); - let poller = thread::spawn(move || { - let mut dispatch = Dispatch::new(rp); - while !poller_stop.load(Ordering::Relaxed) { - dispatch.poll(&mut [&mut authenticator]); - thread::sleep(Duration::from_millis(10)); - } - }); - - let device = Device::new(rq); - let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); - let result = f(device); - stop.store(true, Ordering::Relaxed); - poller.join().unwrap(); - result + virt::with_ram_client("fido", |client| { + // TODO: setup attestation cert + + let mut authenticator = Authenticator::new( + client, + Conforming {}, + Config { + max_msg_size: 0, + skip_up_timeout: None, + max_resident_credential_count: None, + large_blobs: None, + nfc_transport: false, }, - ) + ); + + let channel = CHANNEL.get_or_init(Channel::new); + let (rq, rp) = channel.split().unwrap(); + + let stop = Arc::new(AtomicBool::new(false)); + let poller_stop = stop.clone(); + let poller = thread::spawn(move || { + let mut dispatch = Dispatch::new(rp); + while !poller_stop.load(Ordering::Relaxed) { + dispatch.poll(&mut [&mut authenticator]); + thread::sleep(Duration::from_millis(10)); + } + }); + + let device = Device::new(rq); + let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); + let result = f(device); + stop.store(true, Ordering::Relaxed); + poller.join().unwrap(); + result }) } From f9f554cb9c25ac5027212642591e23addce1f335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 28 Mar 2024 17:19:41 +0100 Subject: [PATCH 062/135] Remove unused serde-cbor dependency --- Cargo.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1a6209b..3306e68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ ctap-types = { version = "0.1.0", features = ["large-blobs"] } delog = "0.1.0" heapless = "0.7" serde = { version = "1.0", default-features = false } -serde_cbor = { version = "0.11.0", default-features = false } serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } trussed = "0.1" @@ -53,7 +52,6 @@ interchange = "0.3.0" log = "0.4.21" # quickcheck = "1" rand = "0.8.4" -serde_cbor = { version = "0.11.0", features = ["std"] } trussed = { version = "0.1", features = ["virt"] } trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] } @@ -66,9 +64,9 @@ features = ["dispatch"] ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "4846817d9cd44604121680a19d46f3264973a3ce" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } -littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "ebd27e49ca321089d01d8c9b169c4aeb58ceeeca" } +littlefs2 = { git = "https://github.com/sosthene-nitrokey/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } -trussed = { git = "https://github.com/Nitrokey/trussed.git", tag = "v0.1.0-nitrokey.18" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b548d379dcbd67d29453d94847b7bc33ae92e673" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" } From 0085530fd87715527a23908ff2074fe31a898f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 28 Mar 2024 18:07:59 +0100 Subject: [PATCH 063/135] Fix clippy lints --- src/ctap2/credential_management.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 6ec33ac..255919f 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -521,16 +521,12 @@ where } // update user name and display name unless the values are not set or empty - credential.data.user.name = user - .name - .as_ref() - .filter(|s| !s.is_empty()) - .map(Clone::clone); + credential.data.user.name = user.name.as_ref().filter(|s| !s.is_empty()).cloned(); credential.data.user.display_name = user .display_name .as_ref() .filter(|s| !s.is_empty()) - .map(Clone::clone); + .cloned(); // write updated credential let serialized = credential.serialize()?; From 8135721c7ddc559ce7dc9cf86db172550fe9ba3e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 21 May 2024 21:39:06 +0200 Subject: [PATCH 064/135] tests: Use separate Channel instance for each test This patch changes the test setup to use separate Channel instances for each test case. This means that a panic in one test case does not affect other test cases. --- tests/virt/mod.rs | 61 ++++++++++++++++++++++++++-------------------- tests/virt/pipe.rs | 8 +++--- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 7db0968..29aa0f2 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -5,7 +5,7 @@ use std::{ cell::RefCell, sync::{ atomic::{AtomicBool, Ordering}, - Arc, Once, OnceLock, + Arc, Once, }, thread, time::{Duration, SystemTime}, @@ -28,11 +28,11 @@ use trussed_staging::virt; use pipe::Pipe; static INIT_LOGGER: Once = Once::new(); -static CHANNEL: OnceLock = OnceLock::new(); pub fn run_ctaphid(f: F) -> T where - F: FnOnce(ctaphid::Device) -> T, + F: FnOnce(ctaphid::Device) -> T + Send, + T: Send, { INIT_LOGGER.call_once(|| { env_logger::init(); @@ -52,38 +52,45 @@ where }, ); - let channel = CHANNEL.get_or_init(Channel::new); + let channel = Channel::new(); let (rq, rp) = channel.split().unwrap(); - let stop = Arc::new(AtomicBool::new(false)); - let poller_stop = stop.clone(); - let poller = thread::spawn(move || { - let mut dispatch = Dispatch::new(rp); - while !poller_stop.load(Ordering::Relaxed) { - dispatch.poll(&mut [&mut authenticator]); - thread::sleep(Duration::from_millis(10)); - } - }); - - let device = Device::new(rq); - let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); - let result = f(device); - stop.store(true, Ordering::Relaxed); - poller.join().unwrap(); - result + thread::scope(|s| { + let stop = Arc::new(AtomicBool::new(false)); + let poller_stop = stop.clone(); + let poller = s.spawn(move || { + let mut dispatch = Dispatch::new(rp); + while !poller_stop.load(Ordering::Relaxed) { + dispatch.poll(&mut [&mut authenticator]); + thread::sleep(Duration::from_millis(10)); + } + }); + + let runner = s.spawn(move || { + let device = Device::new(rq); + let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); + f(device) + }); + + let result = runner.join(); + stop.store(true, Ordering::Relaxed); + poller.join().unwrap(); + result.unwrap() + }) }) } pub fn run_ctap2(f: F) -> T where - F: FnOnce(Ctap2) -> T, + F: FnOnce(Ctap2) -> T + Send, + T: Send, { run_ctaphid(|device| f(Ctap2(device))) } -pub struct Ctap2(ctaphid::Device); +pub struct Ctap2<'a>(ctaphid::Device>); -impl Ctap2 { +impl Ctap2<'_> { pub fn call(&self, operation: Operation, data: &Value) -> T { let mut serialized = Vec::new(); ciborium::into_writer(data, &mut serialized).unwrap(); @@ -109,15 +116,15 @@ impl HidDeviceInfo for DeviceInfo { } } -pub struct Device(RefCell); +pub struct Device<'a>(RefCell>); -impl Device { - fn new(requester: Requester<'static>) -> Self { +impl<'a> Device<'a> { + fn new(requester: Requester<'a>) -> Self { Self(RefCell::new(Pipe::new(requester))) } } -impl HidDevice for Device { +impl HidDevice for Device<'_> { type Info = DeviceInfo; fn send(&self, data: &[u8]) -> Result<(), RequestError> { diff --git a/tests/virt/pipe.rs b/tests/virt/pipe.rs index 0ecfefc..1d62d62 100644 --- a/tests/virt/pipe.rs +++ b/tests/virt/pipe.rs @@ -93,10 +93,10 @@ enum State { Sending((Response, MessageState)), } -pub struct Pipe { +pub struct Pipe<'a> { queue: VecDeque<[u8; PACKET_SIZE]>, state: State, - interchange: Requester<'static>, + interchange: Requester<'a>, buffer: [u8; MESSAGE_SIZE], last_channel: u32, implements: u8, @@ -106,8 +106,8 @@ pub struct Pipe { version: Version, } -impl Pipe { - pub fn new(interchange: Requester<'static>) -> Self { +impl<'a> Pipe<'a> { + pub fn new(interchange: Requester<'a>) -> Self { Self { queue: Default::default(), state: State::Idle, From aad05b95734b315f0cea8d99336014f07feaac42 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 22 May 2024 11:02:31 +0200 Subject: [PATCH 065/135] tests: Handle CTAP2 errors --- tests/basic.rs | 21 +++++++++++++++++++-- tests/virt/mod.rs | 30 +++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index 0e2030a..df764fd 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -8,6 +8,7 @@ use std::collections::BTreeMap; use ciborium::Value; use ctap_types::ctap2::Operation; +use virt::Ctap2Error; use webauthn::{MakeCredentialRequest, PubKeyCredParam, Rp, User}; #[test] @@ -20,7 +21,7 @@ fn test_ping() { #[test] fn test_get_info() { virt::run_ctap2(|device| { - let reply: BTreeMap = device.call(Operation::GetInfo, &Value::Null); + let reply: BTreeMap = device.call(Operation::GetInfo, &Value::Null).unwrap(); let versions: Vec = reply.get(&1).unwrap().deserialized().unwrap(); assert!(versions.contains(&"FIDO_2_0".to_owned())); assert!(versions.contains(&"FIDO_2_1".to_owned())); @@ -36,9 +37,25 @@ fn test_make_credential() { .display_name("John Doe"); let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; let request = MakeCredentialRequest::new(b"", rp, user, pub_key_cred_params); - let reply: BTreeMap = device.call(Operation::MakeCredential, &request.into()); + let reply: BTreeMap = device + .call(Operation::MakeCredential, &request.into()) + .unwrap(); assert_eq!(reply.get(&1).unwrap(), &Value::from("packed")); assert!(reply.contains_key(&2)); assert!(reply.contains_key(&3)); }); } + +#[test] +fn test_make_credential_invalid_params() { + virt::run_ctap2(|device| { + let rp = Rp::new("example.com"); + let user = User::new(b"id123") + .name("john.doe") + .display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -11)]; + let request = MakeCredentialRequest::new(b"", rp, user, pub_key_cred_params); + let result = device.call::(Operation::MakeCredential, &request.into()); + assert_eq!(result, Err(Ctap2Error(0x26))); + }); +} diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 29aa0f2..3f328db 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -3,6 +3,7 @@ mod pipe; use std::{ borrow::Cow, cell::RefCell, + fmt::{self, Debug, Formatter}, sync::{ atomic::{AtomicBool, Ordering}, Arc, Once, @@ -91,11 +92,34 @@ where pub struct Ctap2<'a>(ctaphid::Device>); impl Ctap2<'_> { - pub fn call(&self, operation: Operation, data: &Value) -> T { + pub fn call( + &self, + operation: Operation, + data: &Value, + ) -> Result { let mut serialized = Vec::new(); ciborium::into_writer(data, &mut serialized).unwrap(); - let reply = self.0.ctap2(operation.into(), &serialized).unwrap(); - ciborium::from_reader(reply.as_slice()).unwrap() + let reply = self + .0 + .ctap2(operation.into(), &serialized) + .map_err(|err| match err { + ctaphid::error::Error::CommandError(ctaphid::error::CommandError::CborError( + value, + )) => Ctap2Error(value), + err => panic!("failed to execute CTAP2 command: {err:?}"), + })?; + Ok(ciborium::from_reader(reply.as_slice()).unwrap()) + } +} + +#[derive(PartialEq)] +pub struct Ctap2Error(pub u8); + +impl Debug for Ctap2Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("Ctap2Error") + .field(&format_args!("{:#x}", self.0)) + .finish() } } From efed1896bcaebf08473e493a25be0ff21271c5f0 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 22 May 2024 22:55:32 +0200 Subject: [PATCH 066/135] tests: Add Request trait and reply types --- tests/basic.rs | 30 +++++++------------ tests/virt/mod.rs | 18 ++++++------ tests/webauthn/mod.rs | 67 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 82 insertions(+), 33 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index df764fd..418a9d4 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -3,13 +3,8 @@ mod virt; mod webauthn; -use std::collections::BTreeMap; - -use ciborium::Value; -use ctap_types::ctap2::Operation; - use virt::Ctap2Error; -use webauthn::{MakeCredentialRequest, PubKeyCredParam, Rp, User}; +use webauthn::{GetInfo, MakeCredential, PubKeyCredParam, Rp, User}; #[test] fn test_ping() { @@ -21,10 +16,9 @@ fn test_ping() { #[test] fn test_get_info() { virt::run_ctap2(|device| { - let reply: BTreeMap = device.call(Operation::GetInfo, &Value::Null).unwrap(); - let versions: Vec = reply.get(&1).unwrap().deserialized().unwrap(); - assert!(versions.contains(&"FIDO_2_0".to_owned())); - assert!(versions.contains(&"FIDO_2_1".to_owned())); + let reply = device.exec(GetInfo).unwrap(); + assert!(reply.versions.contains(&"FIDO_2_0".to_owned())); + assert!(reply.versions.contains(&"FIDO_2_1".to_owned())); }); } @@ -36,13 +30,11 @@ fn test_make_credential() { .name("john.doe") .display_name("John Doe"); let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; - let request = MakeCredentialRequest::new(b"", rp, user, pub_key_cred_params); - let reply: BTreeMap = device - .call(Operation::MakeCredential, &request.into()) - .unwrap(); - assert_eq!(reply.get(&1).unwrap(), &Value::from("packed")); - assert!(reply.contains_key(&2)); - assert!(reply.contains_key(&3)); + let request = MakeCredential::new(b"", rp, user, pub_key_cred_params); + let reply = device.exec(request).unwrap(); + assert_eq!(reply.fmt, "packed"); + assert!(reply.auth_data.is_bytes()); + assert!(reply.att_stmt.is_map()); }); } @@ -54,8 +46,8 @@ fn test_make_credential_invalid_params() { .name("john.doe") .display_name("John Doe"); let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -11)]; - let request = MakeCredentialRequest::new(b"", rp, user, pub_key_cred_params); - let result = device.call::(Operation::MakeCredential, &request.into()); + let request = MakeCredential::new(b"", rp, user, pub_key_cred_params); + let result = device.exec(request); assert_eq!(result, Err(Ctap2Error(0x26))); }); } diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 3f328db..05a4853 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -13,7 +13,6 @@ use std::{ }; use ciborium::Value; -use ctap_types::ctap2::Operation; use ctaphid::{ error::{RequestError, ResponseError}, HidDevice, HidDeviceInfo, @@ -23,9 +22,10 @@ use ctaphid_dispatch::{ types::{Channel, Requester}, }; use fido_authenticator::{Authenticator, Config, Conforming}; -use serde::de::DeserializeOwned; use trussed_staging::virt; +use crate::webauthn::Request; + use pipe::Pipe; static INIT_LOGGER: Once = Once::new(); @@ -92,23 +92,21 @@ where pub struct Ctap2<'a>(ctaphid::Device>); impl Ctap2<'_> { - pub fn call( - &self, - operation: Operation, - data: &Value, - ) -> Result { + pub fn exec(&self, request: R) -> Result { + let request = request.into(); let mut serialized = Vec::new(); - ciborium::into_writer(data, &mut serialized).unwrap(); + ciborium::into_writer(&request, &mut serialized).unwrap(); let reply = self .0 - .ctap2(operation.into(), &serialized) + .ctap2(R::COMMAND, &serialized) .map_err(|err| match err { ctaphid::error::Error::CommandError(ctaphid::error::CommandError::CborError( value, )) => Ctap2Error(value), err => panic!("failed to execute CTAP2 command: {err:?}"), })?; - Ok(ciborium::from_reader(reply.as_slice()).unwrap()) + let value: Value = ciborium::from_reader(reply.as_slice()).unwrap(); + Ok(value.into()) } } diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 5236bb9..0639caf 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use ciborium::Value; #[derive(Default)] @@ -15,6 +17,12 @@ impl From for Value { } } +pub trait Request: Into { + const COMMAND: u8; + + type Reply: From; +} + pub struct Rp { id: String, name: Option, @@ -108,14 +116,14 @@ impl From for Value { } } -pub struct MakeCredentialRequest { +pub struct MakeCredential { client_data_hash: Vec, rp: Rp, user: User, pub_key_cred_params: Vec, } -impl MakeCredentialRequest { +impl MakeCredential { pub fn new( client_data_hash: impl Into>, rp: Rp, @@ -131,8 +139,8 @@ impl MakeCredentialRequest { } } -impl From for Value { - fn from(request: MakeCredentialRequest) -> Value { +impl From for Value { + fn from(request: MakeCredential) -> Value { let mut map = Map::default(); map.push(1, request.client_data_hash); map.push(2, request.rp); @@ -148,3 +156,54 @@ impl From for Value { map.into() } } + +impl Request for MakeCredential { + const COMMAND: u8 = 0x01; + + type Reply = MakeCredentialReply; +} + +#[derive(Debug, PartialEq)] +pub struct MakeCredentialReply { + pub fmt: String, + pub auth_data: Value, + pub att_stmt: Value, +} + +impl From for MakeCredentialReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + fmt: map.remove(&1).unwrap().deserialized().unwrap(), + auth_data: map.remove(&2).unwrap(), + att_stmt: map.remove(&3).unwrap(), + } + } +} + +pub struct GetInfo; + +impl From for Value { + fn from(_: GetInfo) -> Self { + Self::Null + } +} + +impl Request for GetInfo { + const COMMAND: u8 = 0x04; + + type Reply = GetInfoReply; +} + +pub struct GetInfoReply { + pub versions: Vec, +} + +impl From for GetInfoReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + versions: map.remove(&1).unwrap().deserialized().unwrap(), + } + } +} From 487b46924780d7fad892ea91a05aaee9a53bc88a Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 22 May 2024 23:03:19 +0200 Subject: [PATCH 067/135] tests: Add basic logging --- tests/virt/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 05a4853..28ab0af 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -13,6 +13,7 @@ use std::{ }; use ciborium::Value; +use ctap_types::ctap2::Operation; use ctaphid::{ error::{RequestError, ResponseError}, HidDevice, HidDeviceInfo, @@ -93,7 +94,12 @@ pub struct Ctap2<'a>(ctaphid::Device>); impl Ctap2<'_> { pub fn exec(&self, request: R) -> Result { + let operation = Operation::try_from(R::COMMAND) + .map(|op| format!("{op:?}")) + .unwrap_or_else(|_| "?".to_owned()); + log::info!("Executing command {:#x} ({})", R::COMMAND, operation); let request = request.into(); + log::debug!("Sending request {request:?}"); let mut serialized = Vec::new(); ciborium::into_writer(&request, &mut serialized).unwrap(); let reply = self @@ -102,10 +108,14 @@ impl Ctap2<'_> { .map_err(|err| match err { ctaphid::error::Error::CommandError(ctaphid::error::CommandError::CborError( value, - )) => Ctap2Error(value), + )) => { + log::warn!("Received CTAP2 error {value:#x}"); + Ctap2Error(value) + } err => panic!("failed to execute CTAP2 command: {err:?}"), })?; let value: Value = ciborium::from_reader(reply.as_slice()).unwrap(); + log::debug!("Received reply {value:?}"); Ok(value.into()) } } From a2b0280e9389ebac8c153376a1a8802971f6c2b2 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 17 May 2024 19:12:20 +0200 Subject: [PATCH 068/135] tests: Add basic tests for credential management --- Cargo.toml | 9 +- tests/basic.rs | 187 +++++++++++++++++++++++++- tests/virt/mod.rs | 6 +- tests/webauthn/mod.rs | 303 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 500 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3306e68..14236e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,17 +44,22 @@ log-warn = [] log-error = [] [dev-dependencies] +aes = "0.8.4" +cbc = { version = "0.1.2", features = ["alloc"] } ciborium = { version = "0.2.2" } +cipher = "0.4.4" ctaphid = { version = "0.3.1", default-features = false } delog = { version = "0.1.6", features = ["std-log"] } env_logger = "0.11.0" +hmac = "0.12.1" interchange = "0.3.0" log = "0.4.21" -# quickcheck = "1" +p256 = { version = "0.13.2", features = ["ecdh"] } rand = "0.8.4" +sha2 = "0.10" trussed = { version = "0.1", features = ["virt"] } -trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] } +trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } usbd-ctaphid = "0.1.0" [package.metadata.docs.rs] diff --git a/tests/basic.rs b/tests/basic.rs index 418a9d4..322822b 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -3,8 +3,16 @@ mod virt; mod webauthn; -use virt::Ctap2Error; -use webauthn::{GetInfo, MakeCredential, PubKeyCredParam, Rp, User}; +use std::collections::BTreeMap; + +use ciborium::Value; + +use virt::{Ctap2, Ctap2Error}; +use webauthn::{ + ClientPin, CredentialManagement, CredentialManagementParams, GetInfo, KeyAgreementKey, + MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredParam, PublicKey, Rp, SharedSecret, + User, +}; #[test] fn test_ping() { @@ -19,9 +27,77 @@ fn test_get_info() { let reply = device.exec(GetInfo).unwrap(); assert!(reply.versions.contains(&"FIDO_2_0".to_owned())); assert!(reply.versions.contains(&"FIDO_2_1".to_owned())); + assert_eq!(reply.pin_protocols, Some(vec![2, 1])); }); } +fn get_shared_secret(device: &Ctap2, platform_key_agreement: &KeyAgreementKey) -> SharedSecret { + let reply = device.exec(ClientPin::new(2, 2)).unwrap(); + let authenticator_key_agreement: PublicKey = reply.key_agreement.unwrap().into(); + platform_key_agreement.shared_secret(&authenticator_key_agreement) +} + +fn set_pin( + device: &Ctap2, + key_agreement_key: &KeyAgreementKey, + shared_secret: &SharedSecret, + pin: &[u8], +) { + let mut padded_pin = [0; 64]; + padded_pin[..pin.len()].copy_from_slice(pin); + let pin_enc = shared_secret.encrypt(&padded_pin); + let pin_auth = shared_secret.authenticate(&pin_enc); + let mut request = ClientPin::new(2, 3); + request.key_agreement = Some(key_agreement_key.public_key()); + request.new_pin_enc = Some(pin_enc); + request.pin_auth = Some(pin_auth); + device.exec(request).unwrap(); +} + +#[test] +fn test_set_pin() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, b"123456"); + }) +} + +fn get_pin_token( + device: &Ctap2, + key_agreement_key: &KeyAgreementKey, + shared_secret: &SharedSecret, + pin: &[u8], + permissions: u8, + rp_id: Option, +) -> PinToken { + use sha2::{Digest as _, Sha256}; + + let mut hasher = Sha256::new(); + hasher.update(pin); + let pin_hash = hasher.finalize(); + let pin_hash_enc = shared_secret.encrypt(&pin_hash[..16]); + let mut request = ClientPin::new(2, 9); + request.key_agreement = Some(key_agreement_key.public_key()); + request.pin_hash_enc = Some(pin_hash_enc); + request.permissions = Some(permissions); + request.rp_id = rp_id; + let reply = device.exec(request).unwrap(); + let encrypted_pin_token = reply.pin_token.as_ref().unwrap().as_bytes().unwrap(); + shared_secret.decrypt_pin_token(encrypted_pin_token) +} + +#[test] +fn test_get_pin_token() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + }) +} + #[test] fn test_make_credential() { virt::run_ctap2(|device| { @@ -51,3 +127,110 @@ fn test_make_credential_invalid_params() { assert_eq!(result, Err(Ctap2Error(0x26))); }); } + +#[derive(Debug)] +struct TestListCredentials { + pin_token_rp_id: bool, +} + +impl TestListCredentials { + fn run(&self) { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + let rp_id = "example.com"; + let user_id = b"id123"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + + let pin_token = + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + // TODO: client data + let client_data_hash = b""; + let pin_auth = pin_token.authenticate(client_data_hash); + + let rp = Rp::new(rp_id); + let user = User::new(user_id).name("john.doe").display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + request.options = Some(MakeCredentialOptions::default().rk(true)); + request.pin_auth = Some(pin_auth); + request.pin_protocol = Some(2); + let reply = device.exec(request).unwrap(); + let auth_data = reply.auth_data.as_bytes().unwrap(); + assert!(auth_data.len() >= 37, "{}", auth_data.len()); + assert_eq!( + auth_data[32] & 0b1, + 0b1, + "up flag not set in auth_data: 0b{:b}", + auth_data[32] + ); + assert_eq!( + auth_data[32] & 0b100, + 0b100, + "uv flag not set in auth_data: 0b{:b}", + auth_data[32] + ); + + let pin_token = + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x04, None); + let pin_auth = pin_token.authenticate(&[0x02]); + let request = CredentialManagement { + subcommand: 0x02, + subcommand_params: None, + pin_protocol: 2, + pin_auth, + }; + let reply = device.exec(request).unwrap(); + let rp: BTreeMap = reply.rp.unwrap().deserialized().unwrap(); + // TODO: check rp ID hash + assert!(reply.rp_id_hash.is_some()); + assert_eq!(reply.total_rps, Some(1)); + assert_eq!(rp.get("id").unwrap(), &Value::from(rp_id)); + + let pin_token_rp_id = self.pin_token_rp_id.then(|| rp_id.to_owned()); + let pin_token = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin, + 0x04, + pin_token_rp_id, + ); + let params = CredentialManagementParams { + rp_id_hash: reply.rp_id_hash.unwrap().as_bytes().unwrap().to_owned(), + }; + let mut pin_auth_param = vec![0x04]; + pin_auth_param.extend_from_slice(¶ms.serialized()); + let pin_auth = pin_token.authenticate(&pin_auth_param); + let request = CredentialManagement { + subcommand: 0x04, + subcommand_params: Some(params), + pin_protocol: 2, + pin_auth, + }; + let reply = device.exec(request).unwrap(); + let user: BTreeMap = reply.user.unwrap().deserialized().unwrap(); + assert_eq!(reply.total_credentials, Some(1)); + assert_eq!(user.get("id").unwrap(), &Value::from(user_id.as_slice())); + }); + } +} + +#[test] +fn test_list_credentials() { + for pin_token_rp_id in [false, true] { + // true is omitted because it currently fails, see: + // https://github.com/Nitrokey/fido-authenticator/issues/80 + if pin_token_rp_id { + continue; + } + + let test = TestListCredentials { pin_token_rp_id }; + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{test:#?}"); + println!(); + test.run(); + } +} diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 28ab0af..73adce2 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -114,7 +114,11 @@ impl Ctap2<'_> { } err => panic!("failed to execute CTAP2 command: {err:?}"), })?; - let value: Value = ciborium::from_reader(reply.as_slice()).unwrap(); + let value: Value = if reply.is_empty() { + Value::Map(Vec::new()) + } else { + ciborium::from_reader(reply.as_slice()).unwrap() + }; log::debug!("Received reply {value:?}"); Ok(value.into()) } diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 0639caf..26b1c4c 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -1,6 +1,110 @@ use std::collections::BTreeMap; use ciborium::Value; +use cipher::{BlockDecryptMut as _, BlockEncryptMut as _, KeyIvInit}; +use hmac::Mac; +use rand::RngCore as _; + +pub struct KeyAgreementKey(p256::ecdh::EphemeralSecret); + +impl KeyAgreementKey { + pub fn generate() -> Self { + Self(p256::ecdh::EphemeralSecret::random(&mut rand::thread_rng())) + } + + pub fn public_key(&self) -> PublicKey { + PublicKey(self.0.public_key()) + } + + pub fn shared_secret(&self, peer: &PublicKey) -> SharedSecret { + let shared_point = self.0.diffie_hellman(&peer.0); + let hkdf = shared_point.extract::(Some(&[0; 32])); + let mut hmac_key = [0; 32]; + let mut aes_key = [0; 32]; + hkdf.expand(b"CTAP2 HMAC key", &mut hmac_key).unwrap(); + hkdf.expand(b"CTAP2 AES key", &mut aes_key).unwrap(); + SharedSecret { hmac_key, aes_key } + } +} + +pub struct PublicKey(p256::PublicKey); + +impl From for Value { + fn from(public_key: PublicKey) -> Value { + let encoded = p256::EncodedPoint::from(&public_key.0); + let mut map = Map::default(); + map.push(1, 2); + map.push(3, -25); + map.push(-1, 1); + map.push(-2, encoded.x().unwrap().as_slice()); + map.push(-3, encoded.y().unwrap().as_slice()); + map.into() + } +} + +impl From for PublicKey { + fn from(value: Value) -> Self { + let map: BTreeMap = value.deserialized().unwrap(); + let kty = map.get(&1).unwrap(); + let alg = map.get(&3).unwrap(); + let crv = map.get(&-1).unwrap(); + let x = map.get(&-2).unwrap().as_bytes().unwrap().as_slice(); + let y = map.get(&-3).unwrap().as_bytes().unwrap().as_slice(); + + assert_eq!(kty, &Value::from(2)); + assert_eq!(alg, &Value::from(-25)); + assert_eq!(crv, &Value::from(1)); + let encoded = p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false); + Self(encoded.try_into().unwrap()) + } +} + +pub struct SharedSecret { + hmac_key: [u8; 32], + aes_key: [u8; 32], +} + +impl SharedSecret { + pub fn encrypt(&self, data: &[u8]) -> Vec { + let mut iv = [0; 16]; + rand::thread_rng().fill_bytes(&mut iv); + + let cipher: cbc::Encryptor = + KeyIvInit::new(self.aes_key.as_ref().into(), iv.as_ref().into()); + let encrypted = cipher.encrypt_padded_vec_mut::(data); + + let mut result = Vec::new(); + result.extend_from_slice(&iv); + result.extend_from_slice(&encrypted); + result + } + + pub fn decrypt_pin_token(&self, data: &[u8]) -> PinToken { + let (iv, data) = data.split_first_chunk::<16>().unwrap(); + let cipher: cbc::Decryptor = + KeyIvInit::new(self.aes_key.as_ref().into(), iv.into()); + let pin_token = cipher + .decrypt_padded_vec_mut::(data) + .unwrap(); + PinToken(pin_token.try_into().unwrap()) + } + + pub fn authenticate(&self, data: &[u8]) -> [u8; 32] { + let mut mac: hmac::Hmac = Mac::new_from_slice(&self.hmac_key).unwrap(); + mac.update(data); + mac.finalize().into_bytes().into() + } +} + +pub struct PinToken([u8; 32]); + +impl PinToken { + pub fn authenticate(&self, data: &[u8]) -> [u8; 32] { + let mut mac: hmac::Hmac = Mac::new_from_slice(&self.0).unwrap(); + mac.update(data); + mac.finalize().into_bytes().into() + } +} #[derive(Default)] pub struct Map(Vec<(Value, Value)>); @@ -23,6 +127,80 @@ pub trait Request: Into { type Reply: From; } +pub struct ClientPin { + protocol: u8, + subcommand: u8, + pub key_agreement: Option, + pub pin_auth: Option<[u8; 32]>, + pub new_pin_enc: Option>, + pub pin_hash_enc: Option>, + pub permissions: Option, + pub rp_id: Option, +} + +impl ClientPin { + pub fn new(protocol: u8, subcommand: u8) -> Self { + Self { + protocol, + subcommand, + key_agreement: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + pin_auth: None, + rp_id: None, + } + } +} + +impl From for Value { + fn from(request: ClientPin) -> Self { + let mut map = Map::default(); + map.push(1, request.protocol); + map.push(2, request.subcommand); + if let Some(key_agreement) = request.key_agreement { + map.push(3, key_agreement); + } + if let Some(pin_auth) = request.pin_auth { + map.push(4, pin_auth.as_slice()); + } + if let Some(new_pin_enc) = request.new_pin_enc { + map.push(5, new_pin_enc); + } + if let Some(pin_hash_enc) = request.pin_hash_enc { + map.push(6, pin_hash_enc); + } + if let Some(permissions) = request.permissions { + map.push(9, permissions); + } + if let Some(rp_id) = request.rp_id { + map.push(0x0a, rp_id); + } + map.into() + } +} + +impl Request for ClientPin { + const COMMAND: u8 = 0x06; + + type Reply = ClientPinReply; +} + +pub struct ClientPinReply { + pub key_agreement: Option, + pub pin_token: Option, +} + +impl From for ClientPinReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + key_agreement: map.remove(&1), + pin_token: map.remove(&2), + } + } +} + pub struct Rp { id: String, name: Option, @@ -121,6 +299,9 @@ pub struct MakeCredential { rp: Rp, user: User, pub_key_cred_params: Vec, + pub options: Option, + pub pin_auth: Option<[u8; 32]>, + pub pin_protocol: Option, } impl MakeCredential { @@ -135,6 +316,9 @@ impl MakeCredential { rp, user, pub_key_cred_params: pub_key_cred_params.into(), + options: None, + pin_auth: None, + pin_protocol: None, } } } @@ -153,6 +337,55 @@ impl From for Value { .map(Value::from) .collect::>(), ); + if let Some(options) = request.options { + map.push(7, options); + } + if let Some(pin_auth) = request.pin_auth { + map.push(8, pin_auth.as_slice()); + } + if let Some(pin_protocol) = request.pin_protocol { + map.push(9, pin_protocol); + } + map.into() + } +} + +#[derive(Default)] +pub struct MakeCredentialOptions { + rk: Option, + up: Option, + uv: Option, +} + +impl MakeCredentialOptions { + pub fn rk(mut self, rk: bool) -> Self { + self.rk = Some(rk); + self + } + + pub fn up(mut self, up: bool) -> Self { + self.up = Some(up); + self + } + + pub fn uv(mut self, uv: bool) -> Self { + self.uv = Some(uv); + self + } +} + +impl From for Value { + fn from(options: MakeCredentialOptions) -> Value { + let mut map = Map::default(); + if let Some(rk) = options.rk { + map.push("rk", rk); + } + if let Some(up) = options.up { + map.push("up", up); + } + if let Some(uv) = options.uv { + map.push("uv", uv); + } map.into() } } @@ -197,6 +430,7 @@ impl Request for GetInfo { pub struct GetInfoReply { pub versions: Vec, + pub pin_protocols: Option>, } impl From for GetInfoReply { @@ -204,6 +438,75 @@ impl From for GetInfoReply { let mut map: BTreeMap = value.deserialized().unwrap(); Self { versions: map.remove(&1).unwrap().deserialized().unwrap(), + pin_protocols: map.remove(&6).map(|value| value.deserialized().unwrap()), + } + } +} + +pub struct CredentialManagement { + pub subcommand: u8, + pub subcommand_params: Option, + pub pin_protocol: u8, + pub pin_auth: [u8; 32], +} + +impl From for Value { + fn from(request: CredentialManagement) -> Value { + let mut map = Map::default(); + map.push(1, request.subcommand); + if let Some(subcommand_params) = request.subcommand_params { + map.push(2, subcommand_params); + } + map.push(3, request.pin_protocol); + map.push(4, request.pin_auth.as_slice()); + map.into() + } +} + +impl Request for CredentialManagement { + const COMMAND: u8 = 0x0A; + + type Reply = CredentialManagementReply; +} + +#[derive(Clone)] +pub struct CredentialManagementParams { + pub rp_id_hash: Vec, +} + +impl CredentialManagementParams { + pub fn serialized(&self) -> Vec { + let mut serialized = Vec::new(); + ciborium::into_writer(&Value::from(self.clone()), &mut serialized).unwrap(); + serialized + } +} + +impl From for Value { + fn from(params: CredentialManagementParams) -> Value { + let mut map = Map::default(); + map.push(1, params.rp_id_hash); + map.into() + } +} + +pub struct CredentialManagementReply { + pub rp: Option, + pub rp_id_hash: Option, + pub total_rps: Option, + pub user: Option, + pub total_credentials: Option, +} + +impl From for CredentialManagementReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + rp: map.remove(&3), + rp_id_hash: map.remove(&4), + total_rps: map.remove(&5).map(|value| value.deserialized().unwrap()), + user: map.remove(&6), + total_credentials: map.remove(&9).map(|value| value.deserialized().unwrap()), } } } From 07ff03b4edb0c6ba3563917d13308ee8d4518843 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 23 May 2024 12:01:39 +0200 Subject: [PATCH 069/135] Accept scoped PIN tokens for EnumerateCredentialsBegin As described in #80, we currently require PIN tokens without an RP ID restriction for all credential management operations. For most operations, this is correct. For EnumerateCredentialsBegin, we should also accept a token that matches the requested RP ID hash. For DeleteCredential and UpdateUserInformation, we should also accept a token that matches the requested credential ID. As it is not trivial to compare the RP ID hash or the credential ID against the RP ID set for the PIN token, I did not handle these cases in the initial implementation. This led to an incompatibility with libfido2 because it tries to use a restricted PIN token to enumerate credentials. With this patch, we additionally compute the RP ID hash when restricting a PIN token to an RP ID and use that to validate the PIN token for EnumerateCredentialsBegin operations. For DeleteCredential and UpdateUserInformation, we still require tokens without an RP ID restriction because determining the RP ID from the credential ID is much harder and this is not known to cause incompatibility issues. See also: https://github.com/Nitrokey/fido-authenticator/issues/80 --- src/ctap2.rs | 50 +++++++++-------- src/ctap2/pin.rs | 46 ++++++++++++++-- tests/basic.rs | 138 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 177 insertions(+), 57 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 332f14b..42c25c4 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -35,7 +35,7 @@ pub mod credential_management; pub mod large_blobs; pub mod pin; -use pin::{PinProtocol, PinProtocolVersion, SharedSecret}; +use pin::{PinProtocol, PinProtocolVersion, RpScope, SharedSecret}; /// Implement `ctap2::Authenticator` for our Authenticator. impl Authenticator for crate::Authenticator { @@ -924,9 +924,7 @@ impl Authenticator for crate::Authenti use credential_management as cm; use ctap2::credential_management::Subcommand; - // TODO: I see "failed pinauth" output, but then still continuation... - // TODO: determine rp_id - self.verify_pin_auth_using_token(parameters, None)?; + self.verify_credential_management_pin_auth(parameters)?; let mut cred_mgmt = cm::CredentialManagement::new(self); let sub_parameters = ¶meters.sub_command_params; @@ -1315,32 +1313,40 @@ impl crate::Authenticator { Ok(pin) } - // fn verify_pin_auth_using_token(&mut self, data: &[u8], pin_auth: &Bytes<16>) - fn verify_pin_auth_using_token( + fn verify_credential_management_pin_auth( &mut self, parameters: &ctap2::credential_management::Request, - rp_id: Option<&str>, ) -> Result<()> { - // info_now!("CM params: {:?}", parameters); use ctap2::credential_management::Subcommand; + let rp_scope = match parameters.sub_command { + Subcommand::EnumerateCredentialsBegin => { + let rp_id_hash = parameters + .sub_command_params + .as_ref() + .and_then(|subparams| subparams.rp_id_hash.as_deref()) + .ok_or(Error::MissingParameter)?; + RpScope::RpIdHash(rp_id_hash) + } + Subcommand::DeleteCredential | Subcommand::UpdateUserInformation => { + // TODO: determine RP ID from credential ID + RpScope::All + } + _ => RpScope::All, + }; 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::UpdateUserInformation => { + Subcommand::GetCredsMetadata + | Subcommand::EnumerateRpsBegin + | Subcommand::EnumerateCredentialsBegin + | Subcommand::DeleteCredential + | Subcommand::UpdateUserInformation => { // check pinProtocol - let pin_protocol = parameters - // .sub_command_params.as_ref().ok_or(Error::MissingParameter)? - .pin_protocol - .ok_or(Error::MissingParameter)?; + let pin_protocol = parameters.pin_protocol.ok_or(Error::MissingParameter)?; let pin_protocol = self.parse_pin_protocol(pin_protocol)?; // check pinAuth 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 { + Bytes::from_slice(&[parameters.sub_command as u8]).unwrap(); + let len = 1 + match parameters.sub_command { Subcommand::EnumerateCredentialsBegin | Subcommand::DeleteCredential | Subcommand::UpdateUserInformation => { @@ -1368,7 +1374,7 @@ impl crate::Authenticator { if let Ok(pin_token) = pin_protocol.verify_pin_token(&data[..len], pin_auth) { info_now!("passed pinauth"); pin_token.require_permissions(Permissions::CREDENTIAL_MANAGEMENT)?; - pin_token.require_valid_for_rp_id(rp_id)?; + pin_token.require_valid_for_rp(rp_scope)?; Ok(()) } else { info_now!("failed pinauth!"); @@ -1456,7 +1462,7 @@ impl crate::Authenticator { let mut pin_protocol = self.pin_protocol(pin_protocol); let pin_token = pin_protocol.verify_pin_token(data, pin_auth)?; pin_token.require_permissions(permissions)?; - pin_token.require_valid_for_rp_id(Some(rp_id))?; + pin_token.require_valid_for_rp(RpScope::RpId(rp_id))?; return Ok(true); } else { diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index 4156d0f..bde2417 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -29,6 +29,13 @@ impl From for u8 { } } +#[derive(Debug)] +pub enum RpScope<'a> { + All, + RpId(&'a str), + RpIdHash(&'a [u8]), +} + #[derive(Debug)] pub struct PinToken { key_id: KeyId, @@ -61,8 +68,22 @@ impl PinToken { } } - pub fn require_valid_for_rp_id(&self, rp_id: Option<&str>) -> Result<()> { - if self.state.rp_id.is_none() || self.state.rp_id.as_deref() == rp_id { + fn is_valid_for_rp(&self, scope: RpScope<'_>) -> bool { + if let Some(rp) = self.state.rp.as_ref() { + // if an RP id is set, the token is only valid for that scope + match scope { + RpScope::All => false, + RpScope::RpId(rp_id) => rp.id == rp_id, + RpScope::RpIdHash(hash) => rp.hash == hash, + } + } else { + // if no RP ID is set, the token is valid for all scopes + true + } + } + + pub fn require_valid_for_rp(&self, scope: RpScope<'_>) -> Result<()> { + if self.is_valid_for_rp(scope) { Ok(()) } else { Err(Error::PinAuthInvalid) @@ -79,7 +100,7 @@ pub struct PinTokenMut<'a, T: CryptoClient> { impl PinTokenMut<'_, T> { pub fn restrict(&mut self, permissions: Permissions, rp_id: Option>) { self.pin_token.state.permissions = permissions; - self.pin_token.state.rp_id = rp_id; + self.pin_token.state.rp = rp_id.map(|id| Rp::new(self.trussed, id)); } // in spec: encrypt(..., pinUvAuthToken) @@ -89,10 +110,27 @@ impl PinTokenMut<'_, T> { } } +#[derive(Debug)] +struct Rp { + id: String<256>, + hash: Bytes<32>, +} + +impl Rp { + fn new(trussed: &mut T, id: String<256>) -> Self { + let hash = + syscall!(trussed.hash(Mechanism::Sha256, Message::from_slice(id.as_ref()).unwrap())) + .hash + .to_bytes() + .unwrap(); + Self { id, hash } + } +} + #[derive(Debug, Default)] struct PinTokenState { permissions: Permissions, - rp_id: Option>, + rp: Option, is_user_present: bool, is_user_verified: bool, is_in_use: bool, diff --git a/tests/basic.rs b/tests/basic.rs index 322822b..66cea57 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -98,34 +98,116 @@ fn test_get_pin_token() { }) } -#[test] -fn test_make_credential() { - virt::run_ctap2(|device| { - let rp = Rp::new("example.com"); - let user = User::new(b"id123") - .name("john.doe") - .display_name("John Doe"); - let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; - let request = MakeCredential::new(b"", rp, user, pub_key_cred_params); - let reply = device.exec(request).unwrap(); - assert_eq!(reply.fmt, "packed"); - assert!(reply.auth_data.is_bytes()); - assert!(reply.att_stmt.is_map()); - }); +#[derive(Clone, Debug)] +struct RequestPinToken { + permissions: u8, + rp_id: Option, +} + +#[derive(Debug)] +struct TestMakeCredential { + pin_token: Option, + pub_key_alg: i32, +} + +impl TestMakeCredential { + fn run(&self) { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + let rp_id = "example.com"; + // TODO: client data + let client_data_hash = b""; + + virt::run_ctap2(|device| { + let pin_auth = self.pin_token.as_ref().map(|pin_token| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + let pin_token = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin, + pin_token.permissions, + pin_token.rp_id.clone(), + ); + pin_token.authenticate(client_data_hash) + }); + + let rp = Rp::new(rp_id); + let user = User::new(b"id123") + .name("john.doe") + .display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", self.pub_key_alg)]; + let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + if let Some(pin_auth) = pin_auth { + request.pin_auth = Some(pin_auth); + request.pin_protocol = Some(2); + } + + let result = device.exec(request); + if let Some(error) = self.expected_error() { + assert_eq!(result, Err(Ctap2Error(error))); + } else { + let reply = result.unwrap(); + assert_eq!(reply.fmt, "packed"); + assert!(reply.auth_data.is_bytes()); + assert!(reply.att_stmt.is_map()); + } + }); + } + + fn expected_error(&self) -> Option { + if let Some(pin_token) = &self.pin_token { + if pin_token.permissions != 0x01 { + return Some(0x33); + } + if let Some(rp_id) = &pin_token.rp_id { + if rp_id != "example.com" { + return Some(0x33); + } + } + } + if self.pub_key_alg != -7 { + return Some(0x26); + } + None + } } #[test] -fn test_make_credential_invalid_params() { - virt::run_ctap2(|device| { - let rp = Rp::new("example.com"); - let user = User::new(b"id123") - .name("john.doe") - .display_name("John Doe"); - let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -11)]; - let request = MakeCredential::new(b"", rp, user, pub_key_cred_params); - let result = device.exec(request); - assert_eq!(result, Err(Ctap2Error(0x26))); - }); +fn test_make_credential() { + let pin_tokens = [ + None, + Some(RequestPinToken { + permissions: 0x01, + rp_id: None, + }), + Some(RequestPinToken { + permissions: 0x01, + rp_id: Some("example.com".to_owned()), + }), + Some(RequestPinToken { + permissions: 0x01, + rp_id: Some("test.com".to_owned()), + }), + Some(RequestPinToken { + permissions: 0x04, + rp_id: None, + }), + ]; + for pin_token in pin_tokens { + for pub_key_alg in [-7, -11] { + let test = TestMakeCredential { + pin_token: pin_token.clone(), + pub_key_alg, + }; + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{test:#?}"); + println!(); + test.run(); + } + } } #[derive(Debug)] @@ -220,12 +302,6 @@ impl TestListCredentials { #[test] fn test_list_credentials() { for pin_token_rp_id in [false, true] { - // true is omitted because it currently fails, see: - // https://github.com/Nitrokey/fido-authenticator/issues/80 - if pin_token_rp_id { - continue; - } - let test = TestListCredentials { pin_token_rp_id }; println!("{}", "=".repeat(80)); println!("Running test:"); From 79b05b576863236fe54750b18e862ce0801f2040 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 7 Jun 2024 10:54:20 +0200 Subject: [PATCH 070/135] Update ctap-types to 0.2.0 This mainly brings the following changes: - Many types are now non-exhaustive, so we need to use Default or builders. - Use byte arrays instead of slices or Bytes<_> for fixed-length byte strings. - Use references for all requests where possible. - Replace ctap_types::cose with the cosey crate to de-duplicate code. --- Cargo.toml | 10 +- src/credential.rs | 65 +++++++----- src/ctap1.rs | 22 ++-- src/ctap2.rs | 160 +++++++++++++++-------------- src/ctap2/credential_management.rs | 43 ++++---- src/ctap2/pin.rs | 12 ++- src/dispatch.rs | 25 +++-- src/lib.rs | 11 +- src/state.rs | 17 ++- 9 files changed, 191 insertions(+), 174 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 14236e5..6180f30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,12 @@ name = "usbip" required-features = ["dispatch"] [dependencies] -ctap-types = { version = "0.1.0", features = ["large-blobs"] } +ctap-types = { version = "0.2.0", features = ["large-blobs"] } +cosey = "0.3" delog = "0.1.0" heapless = "0.7" serde = { version = "1.0", default-features = false } +serde_bytes = { version = "0.11.14", default-features = false } serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } trussed = "0.1" @@ -66,14 +68,12 @@ usbd-ctaphid = "0.1.0" features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "4846817d9cd44604121680a19d46f3264973a3ce" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } -littlefs2 = { git = "https://github.com/sosthene-nitrokey/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } -serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } +littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b548d379dcbd67d29453d94847b7bc33ae92e673" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } -usbd-ctaphid = { git = "https://github.com/Nitrokey/usbd-ctaphid.git", tag = "v0.1.0-nitrokey.2" } +usbd-ctaphid = { git = "https://github.com/trussed-dev/usbd-ctaphid.git", rev = "dcff9009c3cd1ef9e5b09f8f307aca998fc9a8c8" } diff --git a/src/credential.rs b/src/credential.rs index 7612313..bae32ef 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -3,6 +3,7 @@ use core::cmp::Ordering; use serde::Serialize; +use serde_bytes::ByteArray; use trussed::{client, syscall, try_syscall, types::KeyId}; pub(crate) use ctap_types::{ @@ -35,20 +36,19 @@ impl CredentialId { trussed: &mut T, credential: &C, key_encryption_key: KeyId, - rp_id_hash: &Bytes<32>, - nonce: &Bytes<12>, + rp_id_hash: &[u8; 32], + nonce: &[u8; 12], ) -> Result { let serialized_credential: SerializedCredential = trussed::cbor_serialize_bytes(credential).map_err(|_| Error::Other)?; let message = &serialized_credential; // info!("serialized cred = {:?}", message).ok(); let associated_data = &rp_id_hash[..]; - let nonce: [u8; 12] = nonce.as_slice().try_into().unwrap(); let encrypted_serialized_credential = syscall!(trussed.encrypt_chacha8poly1305( key_encryption_key, message, associated_data, - Some(&nonce) + Some(nonce) )); EncryptedSerializedCredential(encrypted_serialized_credential) .try_into() @@ -117,7 +117,7 @@ pub enum Credential { impl Credential { pub fn try_from( authnr: &mut Authenticator, - rp_id_hash: &Bytes<32>, + rp_id_hash: &[u8; 32], descriptor: &PublicKeyCredentialDescriptorRef, ) -> Result { Self::try_from_bytes(authnr, rp_id_hash, descriptor.id) @@ -125,7 +125,7 @@ impl Credential { pub fn try_from_bytes( authnr: &mut Authenticator, - rp_id_hash: &Bytes<32>, + rp_id_hash: &[u8; 32], id: &[u8], ) -> Result { let mut cred: Bytes = Bytes::new(); @@ -162,7 +162,7 @@ impl Credential { &self, trussed: &mut T, key_encryption_key: KeyId, - rp_id_hash: &Bytes<32>, + rp_id_hash: &[u8; 32], ) -> Result { match self { Self::Full(credential) => credential.id(trussed, key_encryption_key, Some(rp_id_hash)), @@ -238,7 +238,7 @@ pub struct CredentialData { // extensions (cont. -- we can only append new options due to index-based deserialization) #[serde(skip_serializing_if = "Option::is_none")] - pub large_blob_key: Option>, + pub large_blob_key: Option>, } // TODO: figure out sizes @@ -248,7 +248,7 @@ pub struct CredentialData { pub struct FullCredential { ctap: CtapVersion, pub data: CredentialData, - nonce: Bytes<12>, + nonce: ByteArray<12>, } // Alas... it would be more symmetrical to have Credential { meta, data }, @@ -331,7 +331,7 @@ impl FullCredential { timestamp: u32, hmac_secret: Option, cred_protect: Option, - large_blob_key: Option>, + large_blob_key: Option>, nonce: [u8; 12], ) -> Self { info!("credential for algorithm {}", algorithm); @@ -354,7 +354,7 @@ impl FullCredential { FullCredential { ctap, data, - nonce: Bytes::from_slice(&nonce).unwrap(), + nonce: ByteArray::new(nonce), } } @@ -375,14 +375,15 @@ impl FullCredential { &self, trussed: &mut T, key_encryption_key: KeyId, - rp_id_hash: Option<&Bytes<32>>, + rp_id_hash: Option<&[u8; 32]>, ) -> Result { - let rp_id_hash: Bytes<32> = if let Some(hash) = rp_id_hash { - hash.clone() + let rp_id_hash: [u8; 32] = if let Some(hash) = rp_id_hash { + *hash } else { syscall!(trussed.hash_sha256(self.rp.id.as_ref())) .hash - .to_bytes() + .as_slice() + .try_into() .map_err(|_| Error::Other)? }; if self.use_short_id.unwrap_or_default() { @@ -446,7 +447,7 @@ pub struct StrippedCredential { pub use_counter: bool, pub algorithm: i32, pub key: Key, - pub nonce: Bytes<12>, + pub nonce: ByteArray<12>, // extensions #[serde(skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, @@ -454,7 +455,7 @@ pub struct StrippedCredential { pub cred_protect: Option, // TODO: HACK -- remove #[serde(skip_serializing_if = "Option::is_none")] - pub large_blob_key: Option>, + pub large_blob_key: Option>, } impl StrippedCredential { @@ -472,7 +473,7 @@ impl StrippedCredential { &self, trussed: &mut T, key_encryption_key: KeyId, - rp_id_hash: &Bytes<32>, + rp_id_hash: &[u8; 32], ) -> Result { CredentialId::new(trussed, self, key_encryption_key, rp_id_hash, &self.nonce) } @@ -486,10 +487,10 @@ impl From<&FullCredential> for StrippedCredential { use_counter: credential.data.use_counter, algorithm: credential.data.algorithm, key: credential.data.key.clone(), - nonce: credential.nonce.clone(), + nonce: credential.nonce, hmac_secret: credential.data.hmac_secret, cred_protect: credential.data.cred_protect, - large_blob_key: credential.data.large_blob_key.clone(), + large_blob_key: credential.data.large_blob_key, } } } @@ -523,10 +524,17 @@ mod test { hmac_secret: Some(false), cred_protect: None, use_short_id: Some(true), - large_blob_key: Some(Bytes::from_slice(&[0xff; 32]).unwrap()), + large_blob_key: Some(ByteArray::new([0xff; 32])), } } + fn random_byte_array() -> ByteArray { + use rand::{rngs::OsRng, RngCore}; + let mut bytes = [0; N]; + OsRng.fill_bytes(&mut bytes); + ByteArray::new(bytes) + } + fn random_bytes() -> Bytes { use rand::{ distributions::{Distribution, Uniform}, @@ -602,7 +610,7 @@ mod test { hmac_secret: Some(false), cred_protect: None, use_short_id: Some(true), - large_blob_key: Some(random_bytes()), + large_blob_key: Some(random_byte_array()), } } @@ -627,8 +635,7 @@ mod test { fn credential_ids() { trussed::virt::with_ram_client("fido", |mut client| { let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; - let mut nonce = Bytes::new(); - nonce.extend_from_slice(&[0; 12]).unwrap(); + let nonce = ByteArray::new([0; 12]); let data = credential_data(); let mut full_credential = FullCredential { ctap: CtapVersion::Fido21Pre, @@ -637,7 +644,8 @@ mod test { }; let rp_id_hash = syscall!(client.hash_sha256(full_credential.rp.id.as_ref())) .hash - .to_bytes() + .as_slice() + .try_into() .unwrap(); // Case 1: credential with use_short_id = Some(true) uses new (short) format @@ -681,16 +689,17 @@ mod test { use_counter: true, algorithm: i32::MAX, key: Key::WrappedKey(key), - nonce: Bytes::from_slice(&[u8::MAX; 12]).unwrap(), + nonce: ByteArray::new([u8::MAX; 12]), hmac_secret: Some(true), cred_protect: Some(CredentialProtectionPolicy::Required), - large_blob_key: Some(Bytes::from_slice(&[0xff; 32]).unwrap()), + large_blob_key: Some(ByteArray::new([0xff; 32])), }; trussed::virt::with_ram_client("fido", |mut client| { let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; let rp_id_hash = syscall!(client.hash_sha256(rp_id.as_ref())) .hash - .to_bytes() + .as_slice() + .try_into() .unwrap(); let id = credential.id(&mut client, kek, &rp_id_hash).unwrap(); assert_eq!(id.0.len(), 239); diff --git a/src/ctap1.rs b/src/ctap1.rs index be12d47..3af469f 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -4,6 +4,7 @@ use ctap_types::{ ctap1::{authenticate, register, Authenticator, ControlByte, Error, Result}, heapless_bytes::Bytes, }; +use serde_bytes::ByteArray; use trussed::{ syscall, @@ -50,7 +51,7 @@ impl Authenticator for crate::Authenti .serialize_p256_key(public_key, KeySerialization::EcdhEsHkdf256)) .serialized_key; syscall!(self.trussed.delete(public_key)); - let cose_key: ctap_types::cose::EcdhEsHkdf256PublicKey = + let cose_key: cosey::EcdhEsHkdf256PublicKey = trussed::cbor_deserialize(&serialized_cose_public_key).unwrap(); let wrapping_key = self @@ -74,8 +75,7 @@ impl Authenticator for crate::Authenti .to_bytes() .map_err(|_| Error::UnspecifiedCheckingError)?, ); - let nonce = syscall!(self.trussed.random_bytes(12)).bytes; - let nonce = Bytes::from_slice(&nonce).unwrap(); + let nonce = ByteArray::new(self.nonce()); let credential = StrippedCredential { ctap: credential::CtapVersion::U2fV2, @@ -102,14 +102,14 @@ impl Authenticator for crate::Authenti .key_encryption_key(&mut self.trussed) .map_err(|_| Error::NotEnoughMemory)?; let credential_id = credential - .id(&mut self.trussed, kek, ®.app_id) + .id(&mut self.trussed, kek, reg.app_id) .map_err(|_| Error::NotEnoughMemory)?; 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(reg.app_id).unwrap(); + commitment.extend_from_slice(reg.challenge).unwrap(); commitment.extend_from_slice(&credential_id.0).unwrap(); @@ -144,14 +144,14 @@ impl Authenticator for crate::Authenti Ok(register::Response::new( 0x05, &cose_key, - &credential_id.0, + credential_id.0, signature, - &cert, + cert, )) } fn authenticate(&mut self, auth: &authenticate::Request) -> Result { - let cred = Credential::try_from_bytes(self, &auth.app_id, &auth.key_handle); + let cred = Credential::try_from_bytes(self, auth.app_id, auth.key_handle); let user_presence_byte = match auth.control_byte { ControlByte::CheckOnly => { @@ -218,12 +218,12 @@ impl Authenticator for crate::Authenti let mut commitment = Commitment::new(); - commitment.extend_from_slice(&auth.app_id).unwrap(); + 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(); + commitment.extend_from_slice(auth.challenge).unwrap(); let signature = syscall!(self.trussed.sign( Mechanism::P256, diff --git a/src/ctap2.rs b/src/ctap2.rs index 42c25c4..cbe100a 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -4,7 +4,7 @@ use ctap_types::{ ctap2::{self, client_pin::Permissions, Authenticator, VendorOperation}, heapless::{String, Vec}, heapless_bytes::Bytes, - sizes, Error, + sizes, ByteArray, Error, }; use sha2::{Digest as _, Sha256}; @@ -71,21 +71,17 @@ impl Authenticator for crate::Authenti pin_protocols.push(u8::from(*pin_protocol)).unwrap(); } - let options = ctap2::get_info::CtapOptions { - ep: None, - 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), - }, - large_blobs: Some(self.config.supports_large_blobs()), - pin_uv_auth_token: Some(true), - ..Default::default() + let mut options = ctap2::get_info::CtapOptions::default(); + options.rk = true; + options.up = true; + options.plat = Some(false); + options.cred_mgmt = Some(true); + options.client_pin = match self.state.persistent.pin_is_set() { + true => Some(true), + false => Some(false), }; + options.large_blobs = Some(self.config.supports_large_blobs()); + options.pin_uv_auth_token = Some(true); let mut transports = Vec::new(); if self.config.nfc_transport { @@ -95,19 +91,18 @@ impl Authenticator for crate::Authenti 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), - transports: Some(transports), - // 1200 - 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), - ..ctap2::get_info::Response::default() - } + let mut response = ctap2::get_info::Response::default(); + response.versions = versions; + response.extensions = Some(extensions); + response.aaguid = Bytes::from_slice(&aaguid).unwrap(); + response.options = Some(options); + response.transports = Some(transports); + // 1200 + response.max_msg_size = Some(self.config.max_msg_size); + response.pin_protocols = Some(pin_protocols); + response.max_creds_in_list = Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST); + response.max_cred_id_length = Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH); + response } #[inline(never)] @@ -345,11 +340,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(); + let nonce = self.nonce(); info_now!("nonce = {:?}", &nonce); // 12.b generate credential ID { = AEAD(Serialize(Credential)) } @@ -362,7 +353,8 @@ impl Authenticator for crate::Authenti // TODO: overwrite, error handling with KeyStoreFull let large_blob_key = if large_blob_key_requested { - Some(Bytes::from_slice(&syscall!(self.trussed.random_bytes(32)).bytes).unwrap()) + let key = syscall!(self.trussed.random_bytes(32)).bytes; + Some(ByteArray::new(key.as_slice().try_into().unwrap())) } else { None }; @@ -376,7 +368,7 @@ impl Authenticator for crate::Authenti self.state.persistent.timestamp(&mut self.trussed)?, hmac_secret_requested, cred_protect_requested, - large_blob_key.clone(), + large_blob_key, nonce, ); @@ -441,7 +433,7 @@ impl Authenticator for crate::Authenti 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)?, + rp_id_hash: &rp_id_hash, flags: { let mut flags = Flags::USER_PRESENCE; @@ -462,9 +454,9 @@ impl Authenticator for crate::Authenti attested_credential_data: { // 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(), + aaguid: &aaguid, + credential_id: &credential_id.0, + credential_public_key: &cose_public_key, }; // debug_now!("cose PK = {:?}", &attested_credential_data.credential_public_key); Some(attested_credential_data) @@ -472,11 +464,10 @@ 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, - hmac_secret: parameters.extensions.as_ref().unwrap().hmac_secret, - large_blob_key: None, - }) + let mut extensions = ctap2::make_credential::Extensions::default(); + extensions.cred_protect = parameters.extensions.as_ref().unwrap().cred_protect; + extensions.hmac_secret = parameters.extensions.as_ref().unwrap().hmac_secret; + Some(extensions) } else { None } @@ -484,7 +475,7 @@ impl Authenticator for crate::Authenti }; // debug_now!("authData = {:?}", &authenticator_data); - let serialized_auth_data = authenticator_data.serialize(); + let serialized_auth_data = authenticator_data.serialize()?; // 13.b The Signature @@ -576,14 +567,13 @@ impl Authenticator for crate::Authenti let fmt = String::<32>::from("packed"); let att_stmt = ctap2::make_credential::AttestationStatement::Packed(packed_attn_stmt); - let attestation_object = ctap2::make_credential::Response { + let mut attestation_object = ctap2::make_credential::ResponseBuilder { fmt, auth_data: serialized_auth_data, - att_stmt, - ep_att: None, - large_blob_key, - }; - + } + .build(); + attestation_object.att_stmt = Some(att_stmt); + attestation_object.large_blob_key = large_blob_key; Ok(attestation_object) } @@ -628,7 +618,7 @@ impl Authenticator for crate::Authenti #[inline(never)] fn client_pin( &mut self, - parameters: &ctap2::client_pin::Request, + parameters: &ctap2::client_pin::Request<'_>, ) -> Result { use ctap2::client_pin::PinV1Subcommand as Subcommand; debug_now!("CTAP2.PIN..."); @@ -899,7 +889,12 @@ impl Authenticator for crate::Authenti // 14. Assign the requested permissions // 15. Assign the requested RP id - pin_token.restrict(permissions, parameters.rp_id.clone()); + let rp_id = parameters + .rp_id + .map(TryInto::try_into) + .transpose() + .map_err(|_| Error::InvalidParameter)?; + pin_token.restrict(permissions, rp_id); // 16. Return PIN token response.pin_token = Some(pin_token.encrypt(&shared_secret)?); @@ -911,6 +906,10 @@ impl Authenticator for crate::Authenti // todo!("not implemented yet") return Err(Error::InvalidParameter); } + + _ => { + return Err(Error::InvalidParameter); + } } Ok(response) @@ -919,7 +918,7 @@ impl Authenticator for crate::Authenti #[inline(never)] fn credential_management( &mut self, - parameters: &ctap2::credential_management::Request, + parameters: &ctap2::credential_management::Request<'_>, ) -> Result { use credential_management as cm; use ctap2::credential_management::Subcommand; @@ -928,6 +927,7 @@ impl Authenticator for crate::Authenti let mut cred_mgmt = cm::CredentialManagement::new(self); let sub_parameters = ¶meters.sub_command_params; + // TODO: use custom enum of known commands match parameters.sub_command { // 0x1 Subcommand::GetCredsMetadata => Ok(cred_mgmt.get_creds_metadata()), @@ -979,6 +979,8 @@ impl Authenticator for crate::Authenti cred_mgmt.update_user_information(credential_id, user) } + + _ => Err(Error::InvalidParameter), } } @@ -1009,7 +1011,7 @@ impl Authenticator for crate::Authenti parameters.pin_protocol, parameters.client_data_hash.as_ref(), Permissions::GET_ASSERTION, - ¶meters.rp_id, + parameters.rp_id, ) { Ok(b) => b, Err(Error::PinRequired) => { @@ -1072,7 +1074,7 @@ impl Authenticator for crate::Authenti }, client_data_hash: { let mut buf = [0u8; 32]; - buf.copy_from_slice(¶meters.client_data_hash); + buf.copy_from_slice(parameters.client_data_hash); buf }, uv_performed, @@ -1162,7 +1164,7 @@ impl crate::Authenticator { #[inline(never)] fn prepare_credentials( &mut self, - rp_id_hash: &Bytes<32>, + rp_id_hash: &[u8; 32], allow_list: &Option, uv_performed: bool, ) -> Option<(Credential, u32)> { @@ -1249,7 +1251,7 @@ impl crate::Authenticator { &mut self, pin_protocol: PinProtocolVersion, shared_secret: &SharedSecret, - pin_hash_enc: &Bytes<80>, + pin_hash_enc: &[u8], ) -> Result<()> { let pin_hash = shared_secret .decrypt(&mut self.trussed, pin_hash_enc) @@ -1323,7 +1325,7 @@ impl crate::Authenticator { let rp_id_hash = parameters .sub_command_params .as_ref() - .and_then(|subparams| subparams.rp_id_hash.as_deref()) + .and_then(|subparams| subparams.rp_id_hash) .ok_or(Error::MissingParameter)?; RpScope::RpIdHash(rp_id_hash) } @@ -1394,6 +1396,8 @@ impl crate::Authenticator { // of already checked CredMgmt subcommands Subcommand::EnumerateRpsGetNextRp | Subcommand::EnumerateCredentialsGetNextCredential => Ok(()), + + _ => Err(Error::InvalidParameter), } } @@ -1570,9 +1574,9 @@ impl crate::Authenticator { shared_secret.delete(&mut self.trussed); - Ok(Some(ctap2::get_assertion::ExtensionsOutput { - hmac_secret: Some(Bytes::from_slice(&output_enc).unwrap()), - })) + let mut extensions = ctap2::get_assertion::ExtensionsOutput::default(); + extensions.hmac_secret = Some(Bytes::from_slice(&output_enc).unwrap()); + Ok(Some(extensions)) } else { Ok(None) } @@ -1585,7 +1589,7 @@ impl crate::Authenticator { 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 rp_id_hash = &data.rp_id_hash; let (key, is_rk) = match credential.key().clone() { Key::ResidentKey(key) => (key, true), @@ -1632,7 +1636,7 @@ impl crate::Authenticator { .state .persistent .key_encryption_key(&mut self.trussed)?; - let credential_id = credential.id(&mut self.trussed, kek, &rp_id_hash)?; + let credential_id = credential.id(&mut self.trussed, kek, rp_id_hash)?; use ctap2::AuthenticatorDataFlags as Flags; @@ -1660,7 +1664,7 @@ impl crate::Authenticator { extensions: extensions_output, }; - let serialized_auth_data = authenticator_data.serialize(); + let serialized_auth_data = authenticator_data.serialize()?; let mut commitment = Bytes::<1024>::new(); commitment @@ -1691,15 +1695,13 @@ impl crate::Authenticator { 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)?, + let mut response = ctap2::get_assertion::ResponseBuilder { + credential: credential_id.into(), + auth_data: serialized_auth_data, signature, - user: None, - number_of_credentials: num_credentials, - user_selected: None, - large_blob_key: None, - }; + } + .build(); + response.number_of_credentials = num_credentials; // User with empty IDs are ignored for compatibility if is_rk { @@ -1733,7 +1735,7 @@ impl crate::Authenticator { #[inline(never)] fn delete_resident_key_by_user_id( &mut self, - rp_id_hash: &Bytes<32>, + rp_id_hash: &[u8; 32], user_id: &Bytes<64>, ) -> Result<()> { // Prepare to iterate over all credentials associated to RP. @@ -1872,8 +1874,10 @@ impl crate::Authenticator { }; // 5. Return requested data info!("Reading large-blob array from offset {offset}"); - large_blobs::read_chunk(&mut self.trussed, config.location, offset, length) - .map(|data| ctap2::large_blobs::Response { config: Some(data) }) + let data = large_blobs::read_chunk(&mut self.trussed, config.location, offset, length)?; + let mut response = ctap2::large_blobs::Response::default(); + response.config = Some(data); + Ok(response) } fn large_blobs_set( @@ -1982,7 +1986,7 @@ impl crate::Authenticator { } } -fn rp_rk_dir(rp_id_hash: &Bytes<32>) -> PathBuf { +fn rp_rk_dir(rp_id_hash: &[u8; 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); @@ -1993,7 +1997,7 @@ fn rp_rk_dir(rp_id_hash: &Bytes<32>) -> PathBuf { dir } -fn rk_path(rp_id_hash: &Bytes<32>, credential_id_hash: &Bytes<32>) -> PathBuf { +fn rk_path(rp_id_hash: &[u8; 32], credential_id_hash: &[u8; 32]) -> PathBuf { let mut path = rp_rk_dir(rp_id_hash); let mut hex = [0u8; 16]; diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 255919f..8df5e70 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -7,12 +7,11 @@ use trussed::{ types::{DirEntry, Location, Path, PathBuf}, }; +use cosey::PublicKey; use ctap_types::{ - cose::PublicKey, ctap2::credential_management::{CredentialProtectionPolicy, Response}, - heapless_bytes::Bytes, - webauthn::{PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity}, - Error, + webauthn::{PublicKeyCredentialDescriptorRef, PublicKeyCredentialUserEntity}, + ByteArray, Error, }; use crate::{ @@ -167,7 +166,7 @@ where let rp = credential.data.rp; - response.rp_id_hash = Some(self.hash(rp.id.as_ref())); + response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id.as_ref()))); response.rp = Some(rp); } } @@ -175,7 +174,7 @@ where // cache state for next call if let Some(total_rps) = response.total_rps { if total_rps > 1 { - let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); + let rp_id_hash = response.rp_id_hash.unwrap().into_array(); self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { remaining: total_rps - 1, rp_id_hash, @@ -244,12 +243,12 @@ where let rp = credential.data.rp; - response.rp_id_hash = Some(self.hash(rp.id.as_ref())); + response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id.as_ref()))); response.rp = Some(rp); // cache state for next call if remaining > 1 { - let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); + let rp_id_hash = response.rp_id_hash.unwrap().into_array(); self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { remaining: remaining - 1, rp_id_hash, @@ -286,7 +285,7 @@ where (num_rks, Some(first_rk)) } - pub fn first_credential(&mut self, rp_id_hash: &Bytes<32>) -> Result { + pub fn first_credential(&mut self, rp_id_hash: &[u8; 32]) -> Result { info!("first credential"); self.state.runtime.cached_rk = None; @@ -448,20 +447,20 @@ where 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, - large_blob_key: credential.data.large_blob_key, - ..Default::default() - }; - + let mut response = Response::default(); + response.user = Some(credential.data.user); + response.credential_id = Some(credential_id.into()); + response.public_key = Some(cose_public_key); + response.cred_protect = cred_protect; + response.large_blob_key = credential.data.large_blob_key; Ok(response) } - fn find_credential(&mut self, credential: &PublicKeyCredentialDescriptor) -> Option { - let credential_id_hash = self.hash(&credential.id[..]); + fn find_credential( + &mut self, + credential: &PublicKeyCredentialDescriptorRef<'_>, + ) -> Option { + let credential_id_hash = self.hash(credential.id); let mut hex = [b'0'; 16]; super::format_hex(&credential_id_hash[..8], &mut hex); let dir = PathBuf::from(b"rk"); @@ -475,7 +474,7 @@ where pub fn delete_credential( &mut self, - credential_descriptor: &PublicKeyCredentialDescriptor, + credential_descriptor: &PublicKeyCredentialDescriptorRef<'_>, ) -> Result { info!("delete credential"); let rk_path = self @@ -499,7 +498,7 @@ where pub fn update_user_information( &mut self, - credential_descriptor: &PublicKeyCredentialDescriptor, + credential_descriptor: &PublicKeyCredentialDescriptorRef<'_>, user: &PublicKeyCredentialUserEntity, ) -> Result { info!("update user information"); diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index bde2417..61b896b 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -1,5 +1,6 @@ use crate::{cbor_serialize_message, TrussedRequirements}; -use ctap_types::{cose::EcdhEsHkdf256PublicKey, ctap2::client_pin::Permissions, Error, Result}; +use cosey::EcdhEsHkdf256PublicKey; +use ctap_types::{ctap2::client_pin::Permissions, Error, Result}; use trussed::{ cbor_deserialize, client::{CryptoClient, HmacSha256, P256}, @@ -33,7 +34,7 @@ impl From for u8 { pub enum RpScope<'a> { All, RpId(&'a str), - RpIdHash(&'a [u8]), + RpIdHash(&'a [u8; 32]), } #[derive(Debug)] @@ -74,7 +75,7 @@ impl PinToken { match scope { RpScope::All => false, RpScope::RpId(rp_id) => rp.id == rp_id, - RpScope::RpIdHash(hash) => rp.hash == hash, + RpScope::RpIdHash(hash) => &rp.hash == hash, } } else { // if no RP ID is set, the token is valid for all scopes @@ -113,7 +114,7 @@ impl PinTokenMut<'_, T> { #[derive(Debug)] struct Rp { id: String<256>, - hash: Bytes<32>, + hash: [u8; 32], } impl Rp { @@ -121,7 +122,8 @@ impl Rp { let hash = syscall!(trussed.hash(Mechanism::Sha256, Message::from_slice(id.as_ref()).unwrap())) .hash - .to_bytes() + .as_slice() + .try_into() .unwrap(); Self { id, hash } } diff --git a/src/dispatch.rs b/src/dispatch.rs index 11c00a4..e5734b4 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -201,18 +201,20 @@ where } #[allow(unused)] -fn request_operation(request: &ctap2::Request) -> ctap2::Operation { +fn request_operation(request: &ctap2::Request) -> Option { + // TODO: move into ctap-types match request { - ctap2::Request::MakeCredential(_) => ctap2::Operation::MakeCredential, - ctap2::Request::GetAssertion(_) => ctap2::Operation::GetAssertion, - ctap2::Request::GetNextAssertion => ctap2::Operation::GetNextAssertion, - ctap2::Request::GetInfo => ctap2::Operation::GetInfo, - ctap2::Request::ClientPin(_) => ctap2::Operation::ClientPin, - ctap2::Request::Reset => ctap2::Operation::Reset, - ctap2::Request::CredentialManagement(_) => ctap2::Operation::CredentialManagement, - ctap2::Request::Selection => ctap2::Operation::Selection, - ctap2::Request::LargeBlobs(_) => ctap2::Operation::LargeBlobs, - ctap2::Request::Vendor(operation) => ctap2::Operation::Vendor(*operation), + ctap2::Request::MakeCredential(_) => Some(ctap2::Operation::MakeCredential), + ctap2::Request::GetAssertion(_) => Some(ctap2::Operation::GetAssertion), + ctap2::Request::GetNextAssertion => Some(ctap2::Operation::GetNextAssertion), + ctap2::Request::GetInfo => Some(ctap2::Operation::GetInfo), + ctap2::Request::ClientPin(_) => Some(ctap2::Operation::ClientPin), + ctap2::Request::Reset => Some(ctap2::Operation::Reset), + ctap2::Request::CredentialManagement(_) => Some(ctap2::Operation::CredentialManagement), + ctap2::Request::Selection => Some(ctap2::Operation::Selection), + ctap2::Request::LargeBlobs(_) => Some(ctap2::Operation::LargeBlobs), + ctap2::Request::Vendor(operation) => Some(ctap2::Operation::Vendor(*operation)), + _ => None, } } @@ -229,5 +231,6 @@ fn response_operation(request: &ctap2::Response) -> Option { ctap2::Response::Selection => Some(ctap2::Operation::Selection), ctap2::Response::LargeBlobs(_) => Some(ctap2::Operation::LargeBlobs), ctap2::Response::Vendor => None, + _ => None, } } diff --git a/src/lib.rs b/src/lib.rs index a980785..e6e576a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,8 +23,6 @@ use core::time::Duration; use trussed::{client, syscall, types::Message, Client as TrussedClient}; use trussed_hkdf::HkdfClient; -use ctap_types::heapless_bytes::Bytes; - /// Re-export of `ctap-types` authenticator errors. pub use ctap_types::Error; @@ -268,9 +266,14 @@ where } } - fn hash(&mut self, data: &[u8]) -> Bytes<32> { + fn hash(&mut self, data: &[u8]) -> [u8; 32] { let hash = syscall!(self.trussed.hash_sha256(data)).hash; - hash.to_bytes().expect("hash should fit") + hash.as_slice().try_into().expect("hash should fit") + } + + fn nonce(&mut self) -> [u8; 12] { + let bytes = syscall!(self.trussed.random_bytes(12)).bytes; + bytes.as_slice().try_into().expect("hash should fit") } fn skip_up_check(&mut self) -> bool { diff --git a/src/state.rs b/src/state.rs index f4e7e48..88cdf77 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,7 +5,6 @@ use ctap_types::{ // 2022-02-27: 10 credentials sizes::MAX_CREDENTIAL_COUNT_IN_LIST, // U8 currently - Bytes, Error, String, }; @@ -23,7 +22,7 @@ use crate::{ Result, TrussedRequirements, }; -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct CachedCredential { pub timestamp: u32, // PathBuf has length 255 + 1, we only need 36 + 1 @@ -43,7 +42,7 @@ impl Ord for CachedCredential { } } -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default)] pub struct CredentialCacheGeneric(BinaryHeap); impl CredentialCacheGeneric { pub fn push(&mut self, item: CachedCredential) { @@ -128,7 +127,7 @@ impl State { } /// Batch device identity (aaguid, certificate, key). -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Identity { // can this be [u8; 16] or need Bytes for serialization? // aaguid: Option>, @@ -196,22 +195,20 @@ impl Identity { } } -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct CredentialManagementEnumerateRps { pub remaining: u32, - pub rp_id_hash: Bytes<32>, + pub rp_id_hash: [u8; 32], } -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct CredentialManagementEnumerateCredentials { pub remaining: u32, pub rp_dir: PathBuf, pub prev_filename: PathBuf, } -#[derive( - Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize, -)] +#[derive(Clone, Debug, Default)] pub struct ActiveGetAssertionData { pub rp_id_hash: [u8; 32], pub client_data_hash: [u8; 32], From ca493254b85b49a19f436f5cfbce6ff133d51f7a Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 24 Jun 2024 20:17:44 +0200 Subject: [PATCH 071/135] tests: Setup attestation certificate and key --- Cargo.toml | 2 ++ tests/basic.rs | 5 +++++ tests/data/fido-cert.der | Bin 0 -> 586 bytes tests/data/fido-key.trussed | Bin 0 -> 36 bytes tests/virt/mod.rs | 40 ++++++++++++++++++++++++++++++++---- tests/webauthn/mod.rs | 2 ++ 6 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 tests/data/fido-cert.der create mode 100644 tests/data/fido-key.trussed diff --git a/Cargo.toml b/Cargo.toml index 6180f30..6b6e8fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,8 +53,10 @@ cipher = "0.4.4" ctaphid = { version = "0.3.1", default-features = false } delog = { version = "0.1.6", features = ["std-log"] } env_logger = "0.11.0" +hex-literal = "0.4.1" hmac = "0.12.1" interchange = "0.3.0" +littlefs2 = "0.4.0" log = "0.4.21" p256 = { version = "0.13.2", features = ["ecdh"] } rand = "0.8.4" diff --git a/tests/basic.rs b/tests/basic.rs index 66cea57..85168e9 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -6,6 +6,7 @@ mod webauthn; use std::collections::BTreeMap; use ciborium::Value; +use hex_literal::hex; use virt::{Ctap2, Ctap2Error}; use webauthn::{ @@ -27,6 +28,10 @@ fn test_get_info() { let reply = device.exec(GetInfo).unwrap(); assert!(reply.versions.contains(&"FIDO_2_0".to_owned())); assert!(reply.versions.contains(&"FIDO_2_1".to_owned())); + assert_eq!( + reply.aaguid.as_bytes().unwrap(), + &hex!("8BC5496807B14D5FB249607F5D527DA2") + ); assert_eq!(reply.pin_protocols, Some(vec![2, 1])); }); } diff --git a/tests/data/fido-cert.der b/tests/data/fido-cert.der new file mode 100644 index 0000000000000000000000000000000000000000..7dabc45bca22646d7a7da8c28489129e8239a774 GIT binary patch literal 586 zcmXqLVsbNRVtliJnTe5!iGzDL1Q>9!acH%9oU>(NW-@R#D34S93b0Y&|BV(w5K{*N5C>bb#tl{R7bSy2&NX;wBOinDxFH&$UDM>9Z zNi50C&ofjvP=z=}4(?8{n~>B%oEB*y%*GD(EfXWu+susY%uWm}S%1$jd@RW|`_XSX zMVYVDbP8ls%is9bJ2vdSBDX~PyVV}XB@dXpLf(fy_`bp=eA?;NYl{wEj4C~R@MM&f z@jIPaiyJ2!G)^#(1-e6)k420{WYfJP2U}`YmizD>W14Rm8lB_mc)>s(B(2OMVIbCk zT>&WYWrbOo3>XZ!K}>!YV6?D&Fi>RU)@Ea5VQhLT!^p_OBE%xlebh69eWP#uCeMWW z*r3`)K%c>tFoDzn^%)>XF0%)Nfh&_D!@I+s`~owazg#{czk6Ro{G|EUt0onG;1lV- v@#D+ZfY@#(1<|EEcH1wmJS&o)A|?Jo=0Pn!w=C~UFa literal 0 HcmV?d00001 diff --git a/tests/data/fido-key.trussed b/tests/data/fido-key.trussed new file mode 100644 index 0000000000000000000000000000000000000000..4f2c64c1f5ff48f745ced96cba025c48f0d20977 GIT binary patch literal 36 scmZQzVqiTjaMSjAfLY4Jq)*E%MLQ200KD=DgXcg literal 0 HcmV?d00001 diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 73adce2..523e021 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -23,12 +23,23 @@ use ctaphid_dispatch::{ types::{Channel, Requester}, }; use fido_authenticator::{Authenticator, Config, Conforming}; -use trussed_staging::virt; +use littlefs2::path; +use trussed::{ + backend::BackendId, + platform::Platform as _, + store::Store as _, + virt::{self, Ram}, +}; +use trussed_staging::virt::{BackendIds, Client, Dispatcher}; use crate::webauthn::Request; use pipe::Pipe; +// see: https://github.com/Nitrokey/nitrokey-3-firmware/tree/main/utils/test-certificates/fido +const ATTESTATION_CERT: &[u8] = include_bytes!("../data/fido-cert.der"); +const ATTESTATION_KEY: &[u8] = include_bytes!("../data/fido-key.trussed"); + static INIT_LOGGER: Once = Once::new(); pub fn run_ctaphid(f: F) -> T @@ -39,9 +50,7 @@ where INIT_LOGGER.call_once(|| { env_logger::init(); }); - virt::with_ram_client("fido", |client| { - // TODO: setup attestation cert - + with_client(|client| { let mut authenticator = Authenticator::new( client, Conforming {}, @@ -199,3 +208,26 @@ impl HidDevice for Device<'_> { } } } + +fn with_client(f: F) -> T +where + F: FnOnce(Client) -> T, +{ + virt::with_platform(Ram::default(), |platform| { + let ifs = platform.store().ifs(); + ifs.create_dir_all(path!("fido/x5c")).unwrap(); + ifs.create_dir_all(path!("fido/sec")).unwrap(); + ifs.write(path!("fido/x5c/00"), ATTESTATION_CERT).unwrap(); + ifs.write(path!("fido/sec/00"), ATTESTATION_KEY).unwrap(); + + platform.run_client_with_backends( + "fido", + Dispatcher::default(), + &[ + BackendId::Custom(BackendIds::StagingBackend), + BackendId::Core, + ], + f, + ) + }) +} diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 26b1c4c..0343e6b 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -430,6 +430,7 @@ impl Request for GetInfo { pub struct GetInfoReply { pub versions: Vec, + pub aaguid: Value, pub pin_protocols: Option>, } @@ -438,6 +439,7 @@ impl From for GetInfoReply { let mut map: BTreeMap = value.deserialized().unwrap(); Self { versions: map.remove(&1).unwrap().deserialized().unwrap(), + aaguid: map.remove(&3).unwrap().deserialized().unwrap(), pin_protocols: map.remove(&6).map(|value| value.deserialized().unwrap()), } } From 867db7571497ae9ee7d127b35734075d7d8f15ea Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 24 Jun 2024 22:25:48 +0200 Subject: [PATCH 072/135] tests: Add ctap1 test cases --- Cargo.toml | 3 +- tests/ctap1.rs | 202 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 tests/ctap1.rs diff --git a/Cargo.toml b/Cargo.toml index 6b6e8fb..3e185c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ trussed-chunked = { version = "0.1.0", optional = true } apdu-dispatch = { version = "0.1", optional = true } ctaphid-dispatch = { version = "0.1", optional = true } -iso7816 = { version = "0.1", optional = true } +iso7816 = { version = "0.1.2", optional = true } [features] dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] @@ -65,6 +65,7 @@ trussed = { version = "0.1", features = ["virt"] } trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] } trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } usbd-ctaphid = "0.1.0" +x509-parser = "0.16.0" [package.metadata.docs.rs] features = ["dispatch"] diff --git a/tests/ctap1.rs b/tests/ctap1.rs new file mode 100644 index 0000000..7c17537 --- /dev/null +++ b/tests/ctap1.rs @@ -0,0 +1,202 @@ +#![cfg(feature = "dispatch")] + +#[allow(unused)] +mod virt; +#[allow(unused)] +mod webauthn; + +use ctaphid::{Device, HidDevice}; +use hex_literal::hex; +use iso7816::{ + command::{class::Class, instruction::Instruction, CommandBuilder, ExpectedLen}, + response::Status, +}; +use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey}; +use x509_parser::public_key::PublicKey; + +#[test] +fn test_version() { + virt::run_ctaphid(|device| { + let response = version(&device); + assert_eq!(response, b"U2F_V2".as_slice()); + }) +} + +#[test] +fn test_authenticate() { + virt::run_ctaphid(|device| { + let challenge = &hex!("4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"); + let application = &hex!("f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); + let register = register(&device, challenge, application); + + let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); + + // check-only + let err = authenticate(&device, 7, challenge, application, ®ister) + .err() + .unwrap(); + assert_eq!(err, Status::ConditionsOfUseNotSatisfied); + + // enforce user presence + let response = authenticate(&device, 3, challenge, application, ®ister).unwrap(); + assert_eq!(response.user_presence & 1, 1); + assert_eq!(response.counter, 1); + + // don’t enforce user presence + let response = authenticate(&device, 8, challenge, application, ®ister).unwrap(); + assert_eq!(response.user_presence & 1, 0); + assert_eq!(response.counter, 2); + }); +} + +#[test] +fn test_authenticate_wrong_application() { + virt::run_ctaphid(|device| { + let challenge = &hex!("4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"); + let mut application = + hex!("f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); + let register = register(&device, challenge, &application); + application.reverse(); + + let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); + for mode in [3, 7, 8] { + let err = authenticate(&device, mode, challenge, &application, ®ister) + .err() + .unwrap(); + assert_eq!(err, Status::IncorrectDataParameter); + } + }); +} + +#[test] +fn test_authenticate_wrong_keyhandle() { + virt::run_ctaphid(|device| { + let challenge = &hex!("4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"); + let application = &hex!("f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); + let mut register = register(&device, challenge, application); + register.keyhandle.reverse(); + + let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); + for mode in [3, 7, 8] { + let err = authenticate(&device, mode, challenge, application, ®ister) + .err() + .unwrap(); + assert_eq!(err, Status::IncorrectDataParameter); + } + }); +} + +fn version(device: &Device) -> Vec { + let command = build_command(3, 0, &[]); + exec(device, &command).unwrap() +} + +struct Register { + user_key: VerifyingKey, + keyhandle: Vec, +} + +fn register( + device: &Device, + challenge: &[u8; 32], + application: &[u8; 32], +) -> Register { + let mut request = [0; 64]; + request[..32].copy_from_slice(challenge); + request[32..].copy_from_slice(application); + let command = build_command(1, 0, &request); + let response = exec(device, &command).unwrap(); + + let (first, response) = response.split_first().unwrap(); + let (user_key, response) = response.split_at(65); + let (keyhandle_len, response) = response.split_first().unwrap(); + let (keyhandle, response) = response.split_at(usize::from(*keyhandle_len)); + let (signature, cert) = x509_parser::parse_x509_certificate(response).unwrap(); + + assert_eq!(*first, 0x05); + + let mut message = Vec::new(); + message.push(0x00); + message.extend_from_slice(application); + message.extend_from_slice(challenge); + message.extend_from_slice(keyhandle); + message.extend_from_slice(user_key); + + let signature = DerSignature::from_bytes(signature).unwrap(); + let public_key = cert.tbs_certificate.subject_pki.parsed().unwrap(); + let PublicKey::EC(ec_point) = public_key else { + panic!("unexpected public key in attestation certificate"); + }; + let public_key = VerifyingKey::from_sec1_bytes(ec_point.data()).unwrap(); + public_key.verify(&message, &signature).unwrap(); + + Register { + user_key: VerifyingKey::from_sec1_bytes(user_key).unwrap(), + keyhandle: keyhandle.into(), + } +} + +struct Authenticate { + user_presence: u8, + counter: u32, +} + +fn authenticate( + device: &Device, + mode: u8, + challenge: &[u8; 32], + application: &[u8; 32], + register: &Register, +) -> Result { + let mut request = Vec::new(); + request.extend_from_slice(challenge); + request.extend_from_slice(application); + request.push(register.keyhandle.len().try_into().unwrap()); + request.extend_from_slice(®ister.keyhandle); + let command = build_command(2, mode, &request); + let response = exec(device, &command)?; + + let (user_presence, response) = response.split_first().unwrap(); + let (counter, signature) = response.split_at(4); + + let mut message = Vec::new(); + message.extend_from_slice(application); + message.push(*user_presence); + message.extend_from_slice(counter); + message.extend_from_slice(challenge); + + let signature = DerSignature::from_bytes(signature).unwrap(); + register.user_key.verify(&message, &signature).unwrap(); + + Ok(Authenticate { + user_presence: *user_presence, + counter: u32::from_be_bytes(counter.try_into().unwrap()), + }) +} + +fn exec(device: &Device, command: &[u8]) -> Result, Status> { + let mut response = device.ctap1(command).unwrap(); + let low = response.pop().unwrap(); + let high = response.pop().unwrap(); + let status = u16::from_be_bytes([high, low]); + let status = Status::from_u16(status); + if status == Status::Success { + Ok(response) + } else { + Err(status) + } +} + +fn build_command(ins: u8, p1: u8, data: &[u8]) -> heapless::Vec { + let builder = CommandBuilder::new( + Class::from_byte(0).unwrap(), + Instruction::from(ins), + p1, + 0, + data, + ExpectedLen::Max, + ); + let mut buffer = heapless::Vec::new(); + builder.serialize_into(&mut buffer).unwrap(); + buffer +} From 9ece79676d2084a323adb187c9ba05040096083b Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 24 Jun 2024 23:13:44 +0200 Subject: [PATCH 073/135] tests: Add ctap1 upgrade test --- tests/ctap1.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++ tests/virt/mod.rs | 41 ++++++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/tests/ctap1.rs b/tests/ctap1.rs index 7c17537..53d7144 100644 --- a/tests/ctap1.rs +++ b/tests/ctap1.rs @@ -11,6 +11,7 @@ use iso7816::{ command::{class::Class, instruction::Instruction, CommandBuilder, ExpectedLen}, response::Status, }; +use littlefs2::path; use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey}; use x509_parser::public_key::PublicKey; @@ -86,6 +87,53 @@ fn test_authenticate_wrong_keyhandle() { }); } +#[test] +fn test_authenticate_upgrade() { + // manually extracted after running register on commit 79b05b576863236fe54750b18e862ce0801f2040 + let state: &[u8] = &hex!("A5726B65795F656E6372797074696F6E5F6B65795010926EC1ACF475A6ED273BD951BC4A6E706B65795F7772617070696E675F6B65795061743976EDACA263BD30DE70ADFBAF0B781A636F6E73656375746976655F70696E5F6D69736D617463686573006870696E5F68617368F66974696D657374616D7001"); + let key_encryption_key: &[u8] = &hex!("00020003A0BAA6066B22616147F242DEC9C4B450F6189A10EE036C36E697E647B2C1D3E1000000000000000000000000"); + let key_wrapping_key: &[u8] = &hex!("000200037719CE721FB206F9788BB7E550777C03795ECFE0B211AB7D50C5C2CE21B43E8E010000000000000000000000"); + let files = &[ + (path!("fido/dat/persistent-state.cbor"), state), + ( + path!("fido/sec/10926ec1acf475a6ed273bd951bc4a6e"), + key_encryption_key, + ), + ( + path!("fido/sec/61743976edaca263bd30de70adfbaf0b"), + key_wrapping_key, + ), + ]; + + virt::run_ctaphid_with_files(files, |device| { + let application = &hex!("f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); + let keyhandle = hex!("A3005878B3F2499ACECB2C08F437DEF0F41929BD4DCFBCA7D43E893B18799BA61F6D84A36EAFCB87D9E833AEA1FE68BABD27A4B89C83C32EC25B092D915D9EA207ECA4BDE5A06E3CDCCFE0E93600AC28A6A8A61E4A1C6881C67E252F00425672427CFC59463B097364F45FD050F8E6BE1C6CD45C1F7D9B5732E334A8014C533D8BF37EEF0D8D7D16B6DF025055B1A6492F5607139EF420D47051A5F3"); + let user_key = &hex!("04AE6B38AE33494A3A58A9FED8A1C5DA2683F510A69B9DE4D8849648485ECDCC21918E6124F6E0B71E7B3C5D92F08EC38D3161E236FF72743923141E97089AA2C4"); + let register = Register { + user_key: VerifyingKey::from_sec1_bytes(user_key).unwrap(), + keyhandle: keyhandle.into(), + }; + + let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); + + // check-only + let err = authenticate(&device, 7, challenge, application, ®ister) + .err() + .unwrap(); + assert_eq!(err, Status::ConditionsOfUseNotSatisfied); + + // enforce user presence + let response = authenticate(&device, 3, challenge, application, ®ister).unwrap(); + assert_eq!(response.user_presence & 1, 1); + assert_eq!(response.counter, 1); + + // don’t enforce user presence + let response = authenticate(&device, 8, challenge, application, ®ister).unwrap(); + assert_eq!(response.user_presence & 1, 0); + assert_eq!(response.counter, 2); + }); +} + fn version(device: &Device) -> Vec { let command = build_command(3, 0, &[]); exec(device, &command).unwrap() diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 523e021..128576c 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -23,7 +23,11 @@ use ctaphid_dispatch::{ types::{Channel, Requester}, }; use fido_authenticator::{Authenticator, Config, Conforming}; -use littlefs2::path; +use littlefs2::{path, path::Path}; +use rand::{ + distributions::{Distribution, Uniform}, + RngCore as _, +}; use trussed::{ backend::BackendId, platform::Platform as _, @@ -43,6 +47,13 @@ const ATTESTATION_KEY: &[u8] = include_bytes!("../data/fido-key.trussed"); static INIT_LOGGER: Once = Once::new(); pub fn run_ctaphid(f: F) -> T +where + F: FnOnce(ctaphid::Device) -> T + Send, + T: Send, +{ + run_ctaphid_with_files(&[], f) +} +pub fn run_ctaphid_with_files(files: &[(&Path, &[u8])], f: F) -> T where F: FnOnce(ctaphid::Device) -> T + Send, T: Send, @@ -50,7 +61,10 @@ where INIT_LOGGER.call_once(|| { env_logger::init(); }); - with_client(|client| { + let mut files = Vec::from(files); + files.push((path!("fido/x5c/00"), ATTESTATION_CERT)); + files.push((path!("fido/sec/00"), ATTESTATION_KEY)); + with_client(&files, |client| { let mut authenticator = Authenticator::new( client, Conforming {}, @@ -209,16 +223,27 @@ impl HidDevice for Device<'_> { } } -fn with_client(f: F) -> T +fn with_client(files: &[(&Path, &[u8])], f: F) -> T where F: FnOnce(Client) -> T, { - virt::with_platform(Ram::default(), |platform| { + virt::with_platform(Ram::default(), |mut platform| { + // virt always uses the same seed -- request some random bytes to reach a somewhat random + // state + let uniform = Uniform::from(0..64); + let n = uniform.sample(&mut rand::thread_rng()); + for _ in 0..n { + platform.rng().next_u32(); + } + let ifs = platform.store().ifs(); - ifs.create_dir_all(path!("fido/x5c")).unwrap(); - ifs.create_dir_all(path!("fido/sec")).unwrap(); - ifs.write(path!("fido/x5c/00"), ATTESTATION_CERT).unwrap(); - ifs.write(path!("fido/sec/00"), ATTESTATION_KEY).unwrap(); + + for (path, content) in files { + if let Some(dir) = path.parent() { + ifs.create_dir_all(&dir).unwrap(); + } + ifs.write(path, content).unwrap(); + } platform.run_client_with_backends( "fido", From 4f52ab13ab3f851c8144864ce230f91e1878f11e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 27 Jun 2024 09:10:09 +0200 Subject: [PATCH 074/135] tests: Add get_assertion test --- tests/basic.rs | 49 +++++++--- tests/webauthn/mod.rs | 218 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 252 insertions(+), 15 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index 85168e9..1e00a00 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -10,9 +10,9 @@ use hex_literal::hex; use virt::{Ctap2, Ctap2Error}; use webauthn::{ - ClientPin, CredentialManagement, CredentialManagementParams, GetInfo, KeyAgreementKey, - MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredParam, PublicKey, Rp, SharedSecret, - User, + ClientPin, CredentialManagement, CredentialManagementParams, GetAssertion, GetInfo, + KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, + PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }; #[test] @@ -154,9 +154,9 @@ impl TestMakeCredential { assert_eq!(result, Err(Ctap2Error(error))); } else { let reply = result.unwrap(); + assert!(reply.auth_data.credential.is_some()); assert_eq!(reply.fmt, "packed"); - assert!(reply.auth_data.is_bytes()); - assert!(reply.att_stmt.is_map()); + reply.att_stmt.unwrap().validate(&reply.auth_data); } }); } @@ -215,6 +215,35 @@ fn test_make_credential() { } } +#[test] +fn test_get_assertion() { + let rp_id = "example.com"; + // TODO: client data + let client_data_hash = &[0; 32]; + + virt::run_ctap2(|device| { + let rp = Rp::new(rp_id); + let user = User::new(b"id123") + .name("john.doe") + .display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + let response = device.exec(request).unwrap(); + let credential = response.auth_data.credential.unwrap(); + + let mut request = GetAssertion::new(rp_id, client_data_hash); + request.allow_list = Some(vec![PubKeyCredDescriptor::new( + "public-key", + credential.id.clone(), + )]); + let response = device.exec(request).unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.credential.id, credential.id); + assert_eq!(response.auth_data.credential, None); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + }); +} + #[derive(Debug)] struct TestListCredentials { pin_token_rp_id: bool, @@ -244,19 +273,17 @@ impl TestListCredentials { request.pin_auth = Some(pin_auth); request.pin_protocol = Some(2); let reply = device.exec(request).unwrap(); - let auth_data = reply.auth_data.as_bytes().unwrap(); - assert!(auth_data.len() >= 37, "{}", auth_data.len()); assert_eq!( - auth_data[32] & 0b1, + reply.auth_data.flags & 0b1, 0b1, "up flag not set in auth_data: 0b{:b}", - auth_data[32] + reply.auth_data.flags ); assert_eq!( - auth_data[32] & 0b100, + reply.auth_data.flags & 0b100, 0b100, "uv flag not set in auth_data: 0b{:b}", - auth_data[32] + reply.auth_data.flags ); let pin_token = diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 0343e6b..61ed577 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -3,7 +3,9 @@ use std::collections::BTreeMap; use ciborium::Value; use cipher::{BlockDecryptMut as _, BlockEncryptMut as _, KeyIvInit}; use hmac::Mac; +use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey}; use rand::RngCore as _; +use serde::Deserialize; pub struct KeyAgreementKey(p256::ecdh::EphemeralSecret); @@ -396,11 +398,34 @@ impl Request for MakeCredential { type Reply = MakeCredentialReply; } +#[derive(Debug, PartialEq, Deserialize)] +pub struct AttStmt(BTreeMap); + +impl AttStmt { + pub fn validate(&self, auth_data: &AuthData) { + let alg = self.0.get("alg").unwrap(); + let x5c = self.0.get("x5c").unwrap().as_array().unwrap(); + let cert = x5c.first().unwrap().as_bytes().unwrap(); + let sig = self.0.get("sig").unwrap().as_bytes().unwrap(); + assert_eq!(alg, &Value::from(-7)); + + let (rest, cert) = x509_parser::parse_x509_certificate(cert).unwrap(); + assert!(rest.is_empty()); + let signature = DerSignature::from_bytes(sig).unwrap(); + let public_key = cert.tbs_certificate.subject_pki.parsed().unwrap(); + let x509_parser::public_key::PublicKey::EC(ec_point) = public_key else { + panic!("unexpected public key in attestation certificate"); + }; + let public_key = VerifyingKey::from_sec1_bytes(ec_point.data()).unwrap(); + public_key.verify(&auth_data.bytes, &signature).unwrap(); + } +} + #[derive(Debug, PartialEq)] pub struct MakeCredentialReply { pub fmt: String, - pub auth_data: Value, - pub att_stmt: Value, + pub auth_data: AuthData, + pub att_stmt: Option, } impl From for MakeCredentialReply { @@ -408,12 +433,197 @@ impl From for MakeCredentialReply { let mut map: BTreeMap = value.deserialized().unwrap(); Self { fmt: map.remove(&1).unwrap().deserialized().unwrap(), - auth_data: map.remove(&2).unwrap(), - att_stmt: map.remove(&3).unwrap(), + auth_data: map.remove(&2).unwrap().into(), + att_stmt: map.remove(&3).map(|value| value.deserialized().unwrap()), + } + } +} + +pub struct PubKeyCredDescriptor { + pub ty: String, + pub id: Vec, +} + +impl PubKeyCredDescriptor { + pub fn new(ty: impl Into, id: impl Into>) -> Self { + Self { + ty: ty.into(), + id: id.into(), + } + } +} + +impl From for Value { + fn from(descriptor: PubKeyCredDescriptor) -> Value { + let mut map = Map::default(); + map.push("type", descriptor.ty); + map.push("id", descriptor.id); + map.into() + } +} + +impl From for PubKeyCredDescriptor { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + ty: map.remove("type").unwrap().into_text().unwrap(), + id: map.remove("id").unwrap().into_bytes().unwrap(), + } + } +} + +pub struct GetAssertion { + rp_id: String, + client_data_hash: Vec, + pub allow_list: Option>, +} + +impl GetAssertion { + pub fn new(rp_id: impl Into, client_data_hash: impl Into>) -> Self { + Self { + rp_id: rp_id.into(), + client_data_hash: client_data_hash.into(), + allow_list: None, + } + } +} + +impl From for Value { + fn from(request: GetAssertion) -> Value { + let mut map = Map::default(); + map.push(0x01, request.rp_id); + map.push(0x02, request.client_data_hash); + if let Some(allow_list) = request.allow_list { + let values: Vec<_> = allow_list.into_iter().map(Value::from).collect(); + map.push(0x03, values); + } + map.into() + } +} + +impl Request for GetAssertion { + const COMMAND: u8 = 0x02; + + type Reply = GetAssertionReply; +} + +pub struct GetAssertionReply { + pub credential: PubKeyCredDescriptor, + pub auth_data: AuthData, + pub signature: Vec, +} + +impl From for GetAssertionReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + credential: map.remove(&0x01).unwrap().into(), + auth_data: map.remove(&0x02).unwrap().into(), + signature: map.remove(&0x03).unwrap().into_bytes().unwrap(), } } } +#[derive(Debug, PartialEq)] +pub struct AuthData { + pub bytes: Vec, + pub flags: u8, + pub credential: Option, + pub extensions: Option>, +} + +impl From> for AuthData { + fn from(vec: Vec) -> Self { + let (_rp_id_hash, bytes) = vec.split_at(32); + let (&flags, bytes) = bytes.split_first().unwrap(); + let (_sign_count, bytes) = bytes.split_at(4); + let (credential, bytes) = if flags & 0b0100_0000 == 0b0100_0000 { + let (credential, bytes) = CredentialData::parse(bytes); + (Some(credential), bytes) + } else { + (None, bytes) + }; + let extensions = if flags & 0b1000_0000 == 0b1000_0000 { + Some(ciborium::from_reader(bytes).unwrap()) + } else { + None + }; + Self { + bytes: vec, + flags, + credential, + extensions, + } + } +} + +impl From for AuthData { + fn from(value: Value) -> Self { + value.into_bytes().unwrap().into() + } +} + +#[derive(Debug, PartialEq)] +pub struct CredentialData { + pub id: Vec, + pub public_key: BTreeMap, +} + +impl CredentialData { + fn parse(bytes: &[u8]) -> (Self, &[u8]) { + let (_aaguid, bytes) = bytes.split_at(16); + let (id_length, bytes) = bytes.split_at(2); + let id_length = u16::from_be_bytes([id_length[0], id_length[1]]); + let (id, bytes) = bytes.split_at(id_length.into()); + let mut cursor = std::io::Cursor::new(bytes); + let public_key = ciborium::from_reader(&mut cursor).unwrap(); + let bytes = &bytes[cursor.position().try_into().unwrap()..]; + ( + Self { + id: id.into(), + public_key, + }, + bytes, + ) + } + + pub fn verify_assertion( + &self, + auth_data: &AuthData, + client_data_hash: &[u8], + signature: &[u8], + ) { + let kty = self.public_key.get(&1).unwrap(); + let alg = self.public_key.get(&3).unwrap(); + let crv = self.public_key.get(&-1).unwrap(); + let x = self + .public_key + .get(&-2) + .unwrap() + .as_bytes() + .unwrap() + .as_slice(); + let y = self + .public_key + .get(&-3) + .unwrap() + .as_bytes() + .unwrap() + .as_slice(); + assert_eq!(kty, &Value::from(2)); + assert_eq!(alg, &Value::from(-7)); + assert_eq!(crv, &Value::from(1)); + + let encoded = p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false); + let public_key = VerifyingKey::from_encoded_point(&encoded).unwrap(); + let signature = DerSignature::from_bytes(signature).unwrap(); + let mut message = Vec::new(); + message.extend_from_slice(&auth_data.bytes); + message.extend_from_slice(client_data_hash); + public_key.verify(&message, &signature).unwrap(); + } +} + pub struct GetInfo; impl From for Value { From 26890d83d3e238b0382e78e96d891fd0540e0419 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 27 Jun 2024 11:51:41 +0200 Subject: [PATCH 075/135] Update ctap-types --- Cargo.toml | 1 + src/ctap2.rs | 41 ++++++++++++++++------------------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3e185c6..0356b0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ x509-parser = "0.16.0" features = ["dispatch"] [patch.crates-io] +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "ff20dfb5049fb5e25c18d1d27049d0bc98a5be8b" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } diff --git a/src/ctap2.rs b/src/ctap2.rs index cbe100a..6335631 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -41,32 +41,23 @@ use pin::{PinProtocol, PinProtocolVersion, RpScope, SharedSecret}; impl Authenticator for crate::Authenticator { #[inline(never)] fn get_info(&mut self) -> ctap2::get_info::Response { + use ctap2::get_info::{Extension, Transport, Version}; + 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_1").unwrap()) - .unwrap(); + let mut versions = Vec::new(); + versions.push(Version::U2fV2).unwrap(); + versions.push(Version::Fido2_0).unwrap(); + versions.push(Version::Fido2_1).unwrap(); - let mut extensions = Vec::, 4>::new(); - extensions - .push(String::from_str("credProtect").unwrap()) - .unwrap(); - extensions - .push(String::from_str("hmac-secret").unwrap()) - .unwrap(); + let mut extensions = Vec::new(); + extensions.push(Extension::CredProtect).unwrap(); + extensions.push(Extension::HmacSecret).unwrap(); if self.config.supports_large_blobs() { - extensions - .push(String::from_str("largeBlobKey").unwrap()) - .unwrap(); + extensions.push(Extension::LargeBlobKey).unwrap(); } - let mut pin_protocols = Vec::::new(); + let mut pin_protocols = Vec::new(); for pin_protocol in self.pin_protocols() { pin_protocols.push(u8::from(*pin_protocol)).unwrap(); } @@ -85,9 +76,9 @@ impl Authenticator for crate::Authenti let mut transports = Vec::new(); if self.config.nfc_transport { - transports.push(String::from("nfc")).unwrap(); + transports.push(Transport::Nfc).unwrap(); } - transports.push(String::from("usb")).unwrap(); + transports.push(Transport::Usb).unwrap(); let (_, aaguid) = self.state.identity.attestation(&mut self.trussed); @@ -552,7 +543,7 @@ impl Authenticator for crate::Authenti info_now!("deleted private credential key: {}", _success); } - let packed_attn_stmt = ctap2::make_credential::PackedAttestationStatement { + let packed_attn_stmt = ctap2::PackedAttestationStatement { alg: attestation_algorithm, sig: signature, x5c: attestation_maybe.as_ref().map(|attestation| { @@ -564,8 +555,8 @@ impl Authenticator for crate::Authenti }), }; - let fmt = String::<32>::from("packed"); - let att_stmt = ctap2::make_credential::AttestationStatement::Packed(packed_attn_stmt); + let fmt = ctap2::AttestationStatementFormat::Packed; + let att_stmt = ctap2::AttestationStatement::Packed(packed_attn_stmt); let mut attestation_object = ctap2::make_credential::ResponseBuilder { fmt, From df9f2def54ea95909b7be42c5944211f0e25a4b8 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 26 Jun 2024 16:07:10 +0200 Subject: [PATCH 076/135] Add ctap fuzz target This fuzz target sends a sequence of arbitrary CTAP1 or CTAP2 requests to the authenticator. --- .github/workflows/ci.yml | 14 ++++++++++++++ fuzz/.gitignore | 4 ++++ fuzz/Cargo.toml | 31 +++++++++++++++++++++++++++++++ fuzz/fuzz_targets/ctap.rs | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/ctap.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f64853a..9f10b55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,20 @@ on: branches: [main] jobs: + check-fuzz: + name: Check fuzz targets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + - name: Check fuzz targets + run: | + cargo check --manifest-path fuzz/Cargo.toml + build: runs-on: ubuntu-latest strategy: diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..4999e0b --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "fido-authenticator-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +ctap-types = { version = "0.2.0", features = ["arbitrary"] } +libfuzzer-sys = "0.4" +trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] } + +[dependencies.fido-authenticator] +path = ".." + +[[bin]] +name = "ctap" +path = "fuzz_targets/ctap.rs" +test = false +doc = false +bench = false + +[patch.crates-io] +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "ff20dfb5049fb5e25c18d1d27049d0bc98a5be8b" } +littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b548d379dcbd67d29453d94847b7bc33ae92e673" } +trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } +trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" } diff --git a/fuzz/fuzz_targets/ctap.rs b/fuzz/fuzz_targets/ctap.rs new file mode 100644 index 0000000..01c4aad --- /dev/null +++ b/fuzz/fuzz_targets/ctap.rs @@ -0,0 +1,34 @@ +#![no_main] + +use ctap_types::{authenticator::Request, ctap1::Authenticator as _, ctap2::Authenticator as _}; +use fido_authenticator::{Authenticator, Config, Conforming}; +use trussed_staging::virt; + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|requests: Vec>| { + virt::with_ram_client("fido", |client| { + let mut authenticator = Authenticator::new( + client, + Conforming {}, + Config { + max_msg_size: 0, + skip_up_timeout: None, + max_resident_credential_count: None, + large_blobs: None, + nfc_transport: false, + }, + ); + + for request in requests { + match request { + Request::Ctap1(request) => { + authenticator.call_ctap1(&request).ok(); + } + Request::Ctap2(request) => { + authenticator.call_ctap2(&request).ok(); + } + } + } + }); +}); From 30e2b909ad8fc3e3eb5215827e3c3373e31b2f2b Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 21 Jun 2024 20:28:25 +0200 Subject: [PATCH 077/135] Implement third-party payment extension --- Cargo.toml | 3 +- src/credential.rs | 20 ++++- src/ctap1.rs | 1 + src/ctap2.rs | 21 +++-- src/ctap2/credential_management.rs | 2 + tests/basic.rs | 129 +++++++++++++++++++++-------- tests/webauthn/mod.rs | 27 ++++++ 7 files changed, 161 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0356b0b..4fd63af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "usbip" required-features = ["dispatch"] [dependencies] -ctap-types = { version = "0.2.0", features = ["large-blobs"] } +ctap-types = { version = "0.2.0", features = ["large-blobs", "third-party-payment"] } cosey = "0.3" delog = "0.1.0" heapless = "0.7" @@ -49,6 +49,7 @@ log-error = [] aes = "0.8.4" cbc = { version = "0.1.2", features = ["alloc"] } ciborium = { version = "0.2.2" } +ciborium-io = "0.2.2" cipher = "0.4.4" ctaphid = { version = "0.3.1", default-features = false } delog = { version = "0.1.6", features = ["std-log"] } diff --git a/src/credential.rs b/src/credential.rs index bae32ef..b0219e8 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -196,6 +196,13 @@ impl Credential { Self::Stripped(credential) => &credential.key, } } + + pub fn third_party_payment(&self) -> Option { + match self { + Self::Full(credential) => credential.data.third_party_payment, + Self::Stripped(credential) => credential.third_party_payment, + } + } } /// The main content of a `FullCredential`. @@ -239,6 +246,9 @@ pub struct CredentialData { // extensions (cont. -- we can only append new options due to index-based deserialization) #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, } // TODO: figure out sizes @@ -332,6 +342,7 @@ impl FullCredential { hmac_secret: Option, cred_protect: Option, large_blob_key: Option>, + third_party_payment: Option, nonce: [u8; 12], ) -> Self { info!("credential for algorithm {}", algorithm); @@ -347,6 +358,7 @@ impl FullCredential { hmac_secret, cred_protect, large_blob_key, + third_party_payment, use_short_id: Some(true), }; @@ -456,6 +468,8 @@ pub struct StrippedCredential { // TODO: HACK -- remove #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, } impl StrippedCredential { @@ -491,6 +505,7 @@ impl From<&FullCredential> for StrippedCredential { hmac_secret: credential.data.hmac_secret, cred_protect: credential.data.cred_protect, large_blob_key: credential.data.large_blob_key, + third_party_payment: credential.data.third_party_payment, } } } @@ -525,6 +540,7 @@ mod test { cred_protect: None, use_short_id: Some(true), large_blob_key: Some(ByteArray::new([0xff; 32])), + third_party_payment: Some(true), } } @@ -611,6 +627,7 @@ mod test { cred_protect: None, use_short_id: Some(true), large_blob_key: Some(random_byte_array()), + third_party_payment: Some(false), } } @@ -693,6 +710,7 @@ mod test { hmac_secret: Some(true), cred_protect: Some(CredentialProtectionPolicy::Required), large_blob_key: Some(ByteArray::new([0xff; 32])), + third_party_payment: Some(true), }; trussed::virt::with_ram_client("fido", |mut client| { let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; @@ -702,7 +720,7 @@ mod test { .try_into() .unwrap(); let id = credential.id(&mut client, kek, &rp_id_hash).unwrap(); - assert_eq!(id.0.len(), 239); + assert_eq!(id.0.len(), 241); }); } diff --git a/src/ctap1.rs b/src/ctap1.rs index 3af469f..f6dd013 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -91,6 +91,7 @@ impl Authenticator for crate::Authenti hmac_secret: None, cred_protect: None, large_blob_key: None, + third_party_payment: None, }; // info!("made credential {:?}", &credential); diff --git a/src/ctap2.rs b/src/ctap2.rs index 6335631..6297069 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -56,6 +56,7 @@ impl Authenticator for crate::Authenti if self.config.supports_large_blobs() { extensions.push(Extension::LargeBlobKey).unwrap(); } + extensions.push(Extension::ThirdPartyPayment).unwrap(); let mut pin_protocols = Vec::new(); for pin_protocol in self.pin_protocols() { @@ -221,6 +222,7 @@ impl Authenticator for crate::Authenti // let mut cred_protect_requested = CredentialProtectionPolicy::Optional; let mut cred_protect_requested = None; let mut large_blob_key_requested = false; + let mut third_party_payment_requested = false; if let Some(extensions) = ¶meters.extensions { hmac_secret_requested = extensions.hmac_secret; @@ -243,6 +245,8 @@ impl Authenticator for crate::Authenti } } } + + third_party_payment_requested = extensions.third_party_payment.unwrap_or_default(); } // debug_now!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); @@ -360,6 +364,7 @@ impl Authenticator for crate::Authenti hmac_secret_requested, cred_protect_requested, large_blob_key, + third_party_payment_requested.then_some(true), nonce, ); @@ -1502,9 +1507,11 @@ impl crate::Authenticator { &mut self, get_assertion_state: &state::ActiveGetAssertionData, extensions: &ctap2::get_assertion::ExtensionsInput, - _credential: &Credential, + credential: &Credential, credential_key: KeyId, ) -> Result> { + let mut output = ctap2::get_assertion::ExtensionsOutput::default(); + if let Some(hmac_secret) = &extensions.hmac_secret { let pin_protocol = hmac_secret .pin_protocol @@ -1565,12 +1572,14 @@ impl crate::Authenticator { shared_secret.delete(&mut self.trussed); - let mut extensions = ctap2::get_assertion::ExtensionsOutput::default(); - extensions.hmac_secret = Some(Bytes::from_slice(&output_enc).unwrap()); - Ok(Some(extensions)) - } else { - Ok(None) + output.hmac_secret = Some(Bytes::from_slice(&output_enc).unwrap()); } + + if extensions.third_party_payment.unwrap_or_default() { + output.third_party_payment = Some(credential.third_party_payment().unwrap_or_default()); + } + + Ok(output.is_set().then_some(output)) } #[inline(never)] diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 8df5e70..527fc52 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -453,6 +453,8 @@ where response.public_key = Some(cose_public_key); response.cred_protect = cred_protect; response.large_blob_key = credential.data.large_blob_key; + response.third_party_payment = + Some(credential.data.third_party_payment.unwrap_or_default()); Ok(response) } diff --git a/tests/basic.rs b/tests/basic.rs index 1e00a00..7e6a898 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -10,9 +10,9 @@ use hex_literal::hex; use virt::{Ctap2, Ctap2Error}; use webauthn::{ - ClientPin, CredentialManagement, CredentialManagementParams, GetAssertion, GetInfo, - KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, - PubKeyCredParam, PublicKey, Rp, SharedSecret, User, + ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, GetAssertion, + GetInfo, KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, + PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }; #[test] @@ -215,38 +215,85 @@ fn test_make_credential() { } } +#[derive(Debug)] +struct TestGetAssertion { + mc_third_party_payment: Option, + ga_third_party_payment: Option, +} + +impl TestGetAssertion { + fn run(&self) { + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{self:#?}"); + println!(); + + let rp_id = "example.com"; + // TODO: client data + let client_data_hash = &[0; 32]; + + virt::run_ctap2(|device| { + let rp = Rp::new(rp_id); + let user = User::new(b"id123") + .name("john.doe") + .display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + if let Some(third_party_payment) = self.mc_third_party_payment { + request.extensions = Some(ExtensionsInput { + third_party_payment: Some(third_party_payment), + }); + } + let response = device.exec(request).unwrap(); + let credential = response.auth_data.credential.unwrap(); + + let mut request = GetAssertion::new(rp_id, client_data_hash); + request.allow_list = Some(vec![PubKeyCredDescriptor::new( + "public-key", + credential.id.clone(), + )]); + if let Some(third_party_payment) = self.ga_third_party_payment { + request.extensions = Some(ExtensionsInput { + third_party_payment: Some(third_party_payment), + }); + } + let response = device.exec(request).unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.credential.id, credential.id); + assert_eq!(response.auth_data.credential, None); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + if self.ga_third_party_payment.unwrap_or_default() { + let extensions = response.auth_data.extensions.unwrap(); + assert_eq!( + extensions.get("thirdPartyPayment"), + Some(&Value::from( + self.mc_third_party_payment.unwrap_or_default() + )) + ); + } else { + assert!(response.auth_data.extensions.is_none()); + } + }); + } +} + #[test] fn test_get_assertion() { - let rp_id = "example.com"; - // TODO: client data - let client_data_hash = &[0; 32]; - - virt::run_ctap2(|device| { - let rp = Rp::new(rp_id); - let user = User::new(b"id123") - .name("john.doe") - .display_name("John Doe"); - let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; - let request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); - let response = device.exec(request).unwrap(); - let credential = response.auth_data.credential.unwrap(); - - let mut request = GetAssertion::new(rp_id, client_data_hash); - request.allow_list = Some(vec![PubKeyCredDescriptor::new( - "public-key", - credential.id.clone(), - )]); - let response = device.exec(request).unwrap(); - assert_eq!(response.credential.ty, "public-key"); - assert_eq!(response.credential.id, credential.id); - assert_eq!(response.auth_data.credential, None); - credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); - }); + for mc_third_party_payment in [Some(false), Some(true), None] { + for ga_third_party_payment in [Some(false), Some(true), None] { + TestGetAssertion { + mc_third_party_payment, + ga_third_party_payment, + } + .run() + } + } } #[derive(Debug)] struct TestListCredentials { pin_token_rp_id: bool, + third_party_payment: Option, } impl TestListCredentials { @@ -272,6 +319,11 @@ impl TestListCredentials { request.options = Some(MakeCredentialOptions::default().rk(true)); request.pin_auth = Some(pin_auth); request.pin_protocol = Some(2); + if let Some(third_party_payment) = self.third_party_payment { + request.extensions = Some(ExtensionsInput { + third_party_payment: Some(third_party_payment), + }); + } let reply = device.exec(request).unwrap(); assert_eq!( reply.auth_data.flags & 0b1, @@ -327,6 +379,10 @@ impl TestListCredentials { let user: BTreeMap = reply.user.unwrap().deserialized().unwrap(); assert_eq!(reply.total_credentials, Some(1)); assert_eq!(user.get("id").unwrap(), &Value::from(user_id.as_slice())); + assert_eq!( + reply.third_party_payment, + Some(self.third_party_payment.unwrap_or_default()) + ); }); } } @@ -334,11 +390,16 @@ impl TestListCredentials { #[test] fn test_list_credentials() { for pin_token_rp_id in [false, true] { - let test = TestListCredentials { pin_token_rp_id }; - println!("{}", "=".repeat(80)); - println!("Running test:"); - println!("{test:#?}"); - println!(); - test.run(); + for third_party_payment in [Some(false), Some(true), None] { + let test = TestListCredentials { + pin_token_rp_id, + third_party_payment, + }; + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{test:#?}"); + println!(); + test.run(); + } } } diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 61ed577..b642195 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -301,6 +301,7 @@ pub struct MakeCredential { rp: Rp, user: User, pub_key_cred_params: Vec, + pub extensions: Option, pub options: Option, pub pin_auth: Option<[u8; 32]>, pub pin_protocol: Option, @@ -318,6 +319,7 @@ impl MakeCredential { rp, user, pub_key_cred_params: pub_key_cred_params.into(), + extensions: None, options: None, pin_auth: None, pin_protocol: None, @@ -339,6 +341,9 @@ impl From for Value { .map(Value::from) .collect::>(), ); + if let Some(extensions) = request.extensions { + map.push(6, extensions); + } if let Some(options) = request.options { map.push(7, options); } @@ -352,6 +357,21 @@ impl From for Value { } } +#[derive(Default)] +pub struct ExtensionsInput { + pub third_party_payment: Option, +} + +impl From for Value { + fn from(extensions: ExtensionsInput) -> Value { + let mut map = Map::default(); + if let Some(third_party_payment) = extensions.third_party_payment { + map.push("thirdPartyPayment", third_party_payment); + } + map.into() + } +} + #[derive(Default)] pub struct MakeCredentialOptions { rk: Option, @@ -476,6 +496,7 @@ pub struct GetAssertion { rp_id: String, client_data_hash: Vec, pub allow_list: Option>, + pub extensions: Option, } impl GetAssertion { @@ -484,6 +505,7 @@ impl GetAssertion { rp_id: rp_id.into(), client_data_hash: client_data_hash.into(), allow_list: None, + extensions: None, } } } @@ -497,6 +519,9 @@ impl From for Value { let values: Vec<_> = allow_list.into_iter().map(Value::from).collect(); map.push(0x03, values); } + if let Some(extensions) = request.extensions { + map.push(0x04, extensions); + } map.into() } } @@ -708,6 +733,7 @@ pub struct CredentialManagementReply { pub total_rps: Option, pub user: Option, pub total_credentials: Option, + pub third_party_payment: Option, } impl From for CredentialManagementReply { @@ -719,6 +745,7 @@ impl From for CredentialManagementReply { total_rps: map.remove(&5).map(|value| value.deserialized().unwrap()), user: map.remove(&6), total_credentials: map.remove(&9).map(|value| value.deserialized().unwrap()), + third_party_payment: map.remove(&0x0c).map(|value| value.deserialized().unwrap()), } } } From f10cb7094142cee88ad4d6863738fa8fd07349e0 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 21 Jun 2024 20:57:55 +0200 Subject: [PATCH 078/135] Support attestation format preference in make_credential --- Cargo.toml | 2 +- fuzz/Cargo.toml | 2 +- src/ctap2.rs | 205 ++++++++++++++++++++++++++++-------------------- 3 files changed, 123 insertions(+), 86 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4fd63af..83b446e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ x509-parser = "0.16.0" features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "ff20dfb5049fb5e25c18d1d27049d0bc98a5be8b" } +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 4999e0b..244fc39 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -23,7 +23,7 @@ doc = false bench = false [patch.crates-io] -ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "ff20dfb5049fb5e25c18d1d27049d0bc98a5be8b" } +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" } littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b548d379dcbd67d29453d94847b7bc33ae92e673" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 6297069..a6423bf 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,7 +1,11 @@ //! The `ctap_types::ctap2::Authenticator` implementation. use ctap_types::{ - ctap2::{self, client_pin::Permissions, Authenticator, VendorOperation}, + ctap2::{ + self, client_pin::Permissions, AttestationFormatsPreference, AttestationStatement, + AttestationStatementFormat, Authenticator, NoneAttestationStatement, + PackedAttestationStatement, VendorOperation, + }, heapless::{String, Vec}, heapless_bytes::Bytes, sizes, ByteArray, Error, @@ -473,102 +477,84 @@ impl Authenticator for crate::Authenti let serialized_auth_data = authenticator_data.serialize()?; - // 13.b The Signature - - // 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)?; - // debug_now!("serialized_auth_data ={:?}", &serialized_auth_data); - commitment - .extend_from_slice(parameters.client_data_hash) - .map_err(|_| Error::Other)?; - // 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 - // 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 let Some(attestation) = attestation_maybe.as_ref() { - let signature = syscall!(self.trussed.sign_p256( - attestation.0, - &commitment, - SignatureSerialization::Asn1Der, - )) - .signature; - (signature.to_bytes().map_err(|_| Error::Other)?, -7) - } else { - 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) - // } + let att_stmt_fmt = + SupportedAttestationFormat::select(parameters.attestation_formats_preference.as_ref()); + let att_stmt = if let Some(format) = att_stmt_fmt { + match format { + SupportedAttestationFormat::None => { + Some(AttestationStatement::None(NoneAttestationStatement {})) + } + SupportedAttestationFormat::Packed => { + let mut commitment = Bytes::<1024>::new(); + commitment + .extend_from_slice(&serialized_auth_data) + .map_err(|_| Error::Other)?; + commitment + .extend_from_slice(parameters.client_data_hash) + .map_err(|_| Error::Other)?; + + let (signature, attestation_algorithm) = { + if let Some(attestation) = attestation_maybe.as_ref() { + let signature = syscall!(self.trussed.sign_p256( + attestation.0, + &commitment, + SignatureSerialization::Asn1Der, + )) + .signature; + (signature.to_bytes().map_err(|_| Error::Other)?, -7) + } else { + 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) + } + } + } + }; + let packed = PackedAttestationStatement { + alg: attestation_algorithm, + sig: signature, + x5c: attestation_maybe.as_ref().map(|attestation| { + // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements + let cert = attestation.1.clone(); + let mut x5c = Vec::new(); + x5c.push(cert).ok(); + x5c + }), + }; + Some(AttestationStatement::Packed(packed)) } } + } else { + None }; - // debug_now!("SIG = {:?}", &signature); if !rk_requested { let _success = syscall!(self.trussed.delete(private_key)).success; info_now!("deleted private credential key: {}", _success); } - let packed_attn_stmt = ctap2::PackedAttestationStatement { - alg: attestation_algorithm, - sig: signature, - x5c: attestation_maybe.as_ref().map(|attestation| { - // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements - let cert = attestation.1.clone(); - let mut x5c = Vec::new(); - x5c.push(cert).ok(); - x5c - }), - }; - - let fmt = ctap2::AttestationStatementFormat::Packed; - let att_stmt = ctap2::AttestationStatement::Packed(packed_attn_stmt); - let mut attestation_object = ctap2::make_credential::ResponseBuilder { - fmt, + fmt: att_stmt_fmt + .map(From::from) + .unwrap_or(AttestationStatementFormat::None), auth_data: serialized_auth_data, } .build(); - attestation_object.att_stmt = Some(att_stmt); + attestation_object.att_stmt = att_stmt; attestation_object.large_blob_key = large_blob_key; Ok(attestation_object) } @@ -1986,6 +1972,57 @@ impl crate::Authenticator { } } +#[derive(Clone, Copy, Debug)] +enum SupportedAttestationFormat { + None, + Packed, +} + +impl SupportedAttestationFormat { + fn select(preference: Option<&AttestationFormatsPreference>) -> Option { + let Some(preference) = preference else { + // no preference, default to packed format + return Some(Self::Packed); + }; + if preference.known_formats() == [AttestationStatementFormat::None] + && !preference.includes_unknown_formats() + { + // platform requested only None --> omit attestation statement + return None; + } + // use first known and supported format, or default to packed format + let format = preference + .known_formats() + .iter() + .copied() + .flat_map(Self::try_from) + .next() + .unwrap_or(Self::Packed); + Some(format) + } +} + +impl From for AttestationStatementFormat { + fn from(format: SupportedAttestationFormat) -> Self { + match format { + SupportedAttestationFormat::None => Self::None, + SupportedAttestationFormat::Packed => Self::Packed, + } + } +} + +impl TryFrom for SupportedAttestationFormat { + type Error = Error; + + fn try_from(format: AttestationStatementFormat) -> core::result::Result { + match format { + AttestationStatementFormat::None => Ok(Self::None), + AttestationStatementFormat::Packed => Ok(Self::Packed), + _ => Err(Error::Other), + } + } +} + fn rp_rk_dir(rp_id_hash: &[u8; 32]) -> PathBuf { // uses only first 8 bytes of hash, which should be "good enough" let mut hex = [b'0'; 16]; From 3edba7bdaba897c70a473aeaae79270582d5fc79 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 22 Jun 2024 13:43:32 +0200 Subject: [PATCH 079/135] Support attestation in get_assertion --- src/ctap2.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ src/state.rs | 2 ++ 2 files changed, 44 insertions(+) diff --git a/src/ctap2.rs b/src/ctap2.rs index a6423bf..bc447bf 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1063,6 +1063,7 @@ impl Authenticator for crate::Authenti up_performed, multiple_credentials, extensions: parameters.extensions.clone(), + attestation_formats_preference: parameters.attestation_formats_preference.clone(), }); let num_credentials = match num_credentials { @@ -1677,6 +1678,46 @@ impl crate::Authenticator { .to_bytes() .unwrap(); + let att_stmt_fmt = + SupportedAttestationFormat::select(data.attestation_formats_preference.as_ref()); + let att_stmt = if let Some(format) = att_stmt_fmt { + match format { + SupportedAttestationFormat::None => { + Some(AttestationStatement::None(NoneAttestationStatement {})) + } + SupportedAttestationFormat::Packed => { + let (attestation_maybe, _) = self.state.identity.attestation(&mut self.trussed); + let (signature, attestation_algorithm) = { + if let Some(attestation) = attestation_maybe.as_ref() { + let signature = syscall!(self.trussed.sign_p256( + attestation.0, + &commitment, + SignatureSerialization::Asn1Der, + )) + .signature; + (signature.to_bytes().map_err(|_| Error::Other)?, -7) + } else { + (signature.clone(), credential.algorithm()) + } + }; + let packed = PackedAttestationStatement { + alg: attestation_algorithm, + sig: signature, + x5c: attestation_maybe.as_ref().map(|attestation| { + // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements + let cert = attestation.1.clone(); + let mut x5c = Vec::new(); + x5c.push(cert).ok(); + x5c + }), + }; + Some(AttestationStatement::Packed(packed)) + } + } + } else { + None + }; + if !is_rk { syscall!(self.trussed.delete(key)); } @@ -1688,6 +1729,7 @@ impl crate::Authenticator { } .build(); response.number_of_credentials = num_credentials; + response.att_stmt = att_stmt; // User with empty IDs are ignored for compatibility if is_rk { diff --git a/src/state.rs b/src/state.rs index 88cdf77..f0fe6a6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,6 +3,7 @@ //! Needs cleanup. use ctap_types::{ + ctap2::AttestationFormatsPreference, // 2022-02-27: 10 credentials sizes::MAX_CREDENTIAL_COUNT_IN_LIST, // U8 currently Error, @@ -216,6 +217,7 @@ pub struct ActiveGetAssertionData { pub up_performed: bool, pub multiple_credentials: bool, pub extensions: Option, + pub attestation_formats_preference: Option, } #[derive(Debug, Default)] From 8025fd9d52cbd5d5cb10892c47e90325d38b77ef Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 22 Jun 2024 14:11:46 +0200 Subject: [PATCH 080/135] Add attestation formats in get_info --- Cargo.toml | 2 +- src/ctap2.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 83b446e..10f29cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "usbip" required-features = ["dispatch"] [dependencies] -ctap-types = { version = "0.2.0", features = ["large-blobs", "third-party-payment"] } +ctap-types = { version = "0.2.0", features = ["get-info-full", "large-blobs", "third-party-payment"] } cosey = "0.3" delog = "0.1.0" heapless = "0.7" diff --git a/src/ctap2.rs b/src/ctap2.rs index bc447bf..1ef0737 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -85,6 +85,14 @@ impl Authenticator for crate::Authenti } transports.push(Transport::Usb).unwrap(); + let mut attestation_formats = Vec::new(); + attestation_formats + .push(AttestationStatementFormat::Packed) + .unwrap(); + attestation_formats + .push(AttestationStatementFormat::None) + .unwrap(); + let (_, aaguid) = self.state.identity.attestation(&mut self.trussed); let mut response = ctap2::get_info::Response::default(); @@ -98,6 +106,7 @@ impl Authenticator for crate::Authenti response.pin_protocols = Some(pin_protocols); response.max_creds_in_list = Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST); response.max_cred_id_length = Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH); + response.attestation_formats = Some(attestation_formats); response } From 09271b68b429ad57b35dd08ff0e3bbcd933d9d6c Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 26 Jun 2024 19:00:59 +0200 Subject: [PATCH 081/135] Add tests for attestation formats preference --- tests/basic.rs | 115 ++++++++++++++++++++++++++++++++++++++---- tests/webauthn/mod.rs | 64 +++++++++++++++++------ 2 files changed, 152 insertions(+), 27 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index 7e6a898..3a85f4b 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -10,8 +10,8 @@ use hex_literal::hex; use virt::{Ctap2, Ctap2Error}; use webauthn::{ - ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, GetAssertion, - GetInfo, KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, + AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, + GetAssertion, GetInfo, KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }; @@ -33,6 +33,10 @@ fn test_get_info() { &hex!("8BC5496807B14D5FB249607F5D527DA2") ); assert_eq!(reply.pin_protocols, Some(vec![2, 1])); + assert_eq!( + reply.attestation_formats, + Some(vec!["packed".to_owned(), "none".to_owned()]) + ); }); } @@ -109,14 +113,87 @@ struct RequestPinToken { rp_id: Option, } +#[derive(Clone, Copy, Debug)] +enum AttestationFormatsPreference { + Empty, + None, + Packed, + NonePacked, + PackedNone, + OtherNonePacked, + MultiOtherNonePacked, +} + +impl AttestationFormatsPreference { + const ALL: &'static [Self] = &[ + Self::Empty, + Self::None, + Self::Packed, + Self::NonePacked, + Self::PackedNone, + Self::OtherNonePacked, + Self::MultiOtherNonePacked, + ]; + + fn format(&self) -> Option { + match self { + Self::Empty | Self::Packed | Self::PackedNone => Some(AttStmtFormat::Packed), + Self::NonePacked | Self::OtherNonePacked | Self::MultiOtherNonePacked => { + Some(AttStmtFormat::None) + } + Self::None => None, + } + } +} + +impl From for Vec<&'static str> { + fn from(preference: AttestationFormatsPreference) -> Self { + let mut vec = Vec::new(); + match preference { + AttestationFormatsPreference::Empty => {} + AttestationFormatsPreference::None => { + vec.push("none"); + } + AttestationFormatsPreference::Packed => { + vec.push("packed"); + } + AttestationFormatsPreference::NonePacked => { + vec.push("none"); + vec.push("packed"); + } + AttestationFormatsPreference::PackedNone => { + vec.push("packed"); + vec.push("none"); + } + AttestationFormatsPreference::OtherNonePacked => { + vec.push("tpm"); + vec.push("none"); + vec.push("packed"); + } + AttestationFormatsPreference::MultiOtherNonePacked => { + vec.resize(100, "tpm"); + vec.push("none"); + vec.push("packed"); + } + } + vec + } +} + #[derive(Debug)] struct TestMakeCredential { pin_token: Option, pub_key_alg: i32, + attestation_formats_preference: Option, } impl TestMakeCredential { fn run(&self) { + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{self:#?}"); + println!(); + let key_agreement_key = KeyAgreementKey::generate(); let pin = b"123456"; let rp_id = "example.com"; @@ -148,6 +225,8 @@ impl TestMakeCredential { request.pin_auth = Some(pin_auth); request.pin_protocol = Some(2); } + request.attestation_formats_preference = + self.attestation_formats_preference.map(From::from); let result = device.exec(request); if let Some(error) = self.expected_error() { @@ -155,8 +234,17 @@ impl TestMakeCredential { } else { let reply = result.unwrap(); assert!(reply.auth_data.credential.is_some()); - assert_eq!(reply.fmt, "packed"); - reply.att_stmt.unwrap().validate(&reply.auth_data); + let format = self + .attestation_formats_preference + .unwrap_or(AttestationFormatsPreference::Packed) + .format(); + if let Some(format) = format { + assert_eq!(reply.fmt, format.as_str()); + reply.att_stmt.unwrap().validate(format, &reply.auth_data); + } else { + assert_eq!(reply.fmt, AttStmtFormat::None.as_str()); + assert!(reply.att_stmt.is_none()); + } } }); } @@ -202,15 +290,20 @@ fn test_make_credential() { ]; for pin_token in pin_tokens { for pub_key_alg in [-7, -11] { - let test = TestMakeCredential { + TestMakeCredential { pin_token: pin_token.clone(), pub_key_alg, - }; - println!("{}", "=".repeat(80)); - println!("Running test:"); - println!("{test:#?}"); - println!(); - test.run(); + attestation_formats_preference: None, + } + .run(); + for attestation_formats_preference in AttestationFormatsPreference::ALL { + TestMakeCredential { + pin_token: pin_token.clone(), + pub_key_alg, + attestation_formats_preference: Some(*attestation_formats_preference), + } + .run(); + } } } } diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index b642195..0698522 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -305,6 +305,7 @@ pub struct MakeCredential { pub options: Option, pub pin_auth: Option<[u8; 32]>, pub pin_protocol: Option, + pub attestation_formats_preference: Option>, } impl MakeCredential { @@ -323,6 +324,7 @@ impl MakeCredential { options: None, pin_auth: None, pin_protocol: None, + attestation_formats_preference: None, } } } @@ -353,6 +355,13 @@ impl From for Value { if let Some(pin_protocol) = request.pin_protocol { map.push(9, pin_protocol); } + if let Some(attestation_formats_preference) = request.attestation_formats_preference { + let preference: Vec<_> = attestation_formats_preference + .into_iter() + .map(Value::from) + .collect(); + map.push(0x0b, preference); + } map.into() } } @@ -418,26 +427,47 @@ impl Request for MakeCredential { type Reply = MakeCredentialReply; } +pub enum AttStmtFormat { + None, + Packed, +} + +impl AttStmtFormat { + pub fn as_str(&self) -> &'static str { + match self { + Self::None => "none", + Self::Packed => "packed", + } + } +} + #[derive(Debug, PartialEq, Deserialize)] pub struct AttStmt(BTreeMap); impl AttStmt { - pub fn validate(&self, auth_data: &AuthData) { - let alg = self.0.get("alg").unwrap(); - let x5c = self.0.get("x5c").unwrap().as_array().unwrap(); - let cert = x5c.first().unwrap().as_bytes().unwrap(); - let sig = self.0.get("sig").unwrap().as_bytes().unwrap(); - assert_eq!(alg, &Value::from(-7)); - - let (rest, cert) = x509_parser::parse_x509_certificate(cert).unwrap(); - assert!(rest.is_empty()); - let signature = DerSignature::from_bytes(sig).unwrap(); - let public_key = cert.tbs_certificate.subject_pki.parsed().unwrap(); - let x509_parser::public_key::PublicKey::EC(ec_point) = public_key else { - panic!("unexpected public key in attestation certificate"); - }; - let public_key = VerifyingKey::from_sec1_bytes(ec_point.data()).unwrap(); - public_key.verify(&auth_data.bytes, &signature).unwrap(); + pub fn validate(&self, format: AttStmtFormat, auth_data: &AuthData) { + match format { + AttStmtFormat::Packed => { + let alg = self.0.get("alg").unwrap(); + let x5c = self.0.get("x5c").unwrap().as_array().unwrap(); + let cert = x5c.first().unwrap().as_bytes().unwrap(); + let sig = self.0.get("sig").unwrap().as_bytes().unwrap(); + assert_eq!(alg, &Value::from(-7)); + + let (rest, cert) = x509_parser::parse_x509_certificate(cert).unwrap(); + assert!(rest.is_empty()); + let signature = DerSignature::from_bytes(sig).unwrap(); + let public_key = cert.tbs_certificate.subject_pki.parsed().unwrap(); + let x509_parser::public_key::PublicKey::EC(ec_point) = public_key else { + panic!("unexpected public key in attestation certificate"); + }; + let public_key = VerifyingKey::from_sec1_bytes(ec_point.data()).unwrap(); + public_key.verify(&auth_data.bytes, &signature).unwrap(); + } + AttStmtFormat::None => { + assert!(self.0.is_empty()); + } + } } } @@ -667,6 +697,7 @@ pub struct GetInfoReply { pub versions: Vec, pub aaguid: Value, pub pin_protocols: Option>, + pub attestation_formats: Option>, } impl From for GetInfoReply { @@ -676,6 +707,7 @@ impl From for GetInfoReply { versions: map.remove(&1).unwrap().deserialized().unwrap(), aaguid: map.remove(&3).unwrap().deserialized().unwrap(), pin_protocols: map.remove(&6).map(|value| value.deserialized().unwrap()), + attestation_formats: map.remove(&0x16).map(|value| value.deserialized().unwrap()), } } } From e0654c1b03a2ed8ff3ce52f0cf250da00bf9d2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 31 Jan 2024 14:57:47 +0100 Subject: [PATCH 082/135] pin_info_hash: use serde-byte-array --- Cargo.toml | 1 + src/state.rs | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 10f29cb..b82820a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ x509-parser = "0.16.0" features = ["dispatch"] [patch.crates-io] +cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "94ee8c28edf9248b402aa4335c1dee157995197b"} ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } diff --git a/src/state.rs b/src/state.rs index f0fe6a6..6ca9fd7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -261,6 +261,7 @@ pub struct PersistentState { key_encryption_key: Option, key_wrapping_key: Option, consecutive_pin_mismatches: u8, + #[serde(with = "serde_bytes")] pin_hash: Option<[u8; 16]>, // Ideally, we'd dogfood a "Monotonic Counter" from trussed. // TODO: Add per-key counters for resident keys. @@ -515,3 +516,23 @@ impl RuntimeState { self.pin_protocol = Some(PinProtocolState::new(trussed)); } } + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn deser() { + let _state: PersistentState = trussed::cbor_deserialize(&hex!( + " + a5726b65795f656e6372797074696f6e5f6b657950b19a5a2845e5ec71e3 + 2a1b890892376c706b65795f7772617070696e675f6b6579f6781a636f6e + 73656375746976655f70696e5f6d69736d617463686573006870696e5f68 + 6173689018ef1879187c1881181818f0182d18fb186418960718dd185d18 + 3f188c18766974696d657374616d7009 + " + )) + .unwrap(); + } +} From b18cae918bc5cbd9ab978943fbe815680904e233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Mon, 19 Feb 2024 15:04:56 +0100 Subject: [PATCH 083/135] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 401939f..1b3a3b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement PIN token permissions ([#63][]) - Implement UpdateUserInformation subcommand for CredentialManagement - Support CTAP 2.1 +- Serialize PIN hash with `serde-bytes` ([#52][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -37,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#61]: https://github.com/Nitrokey/fido-authenticator/pull/61 [#62]: https://github.com/Nitrokey/fido-authenticator/pull/62 [#63]: https://github.com/Nitrokey/fido-authenticator/pull/63 +[#52]: https://github.com/Nitrokey/fido-authenticator/issues/52 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp From 1bd615561fda9d474134d968a7162383fb7acee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 26 Jul 2024 12:19:22 +0200 Subject: [PATCH 084/135] Apply suggestions --- Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b82820a..f2ee0f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,8 @@ apdu-dispatch = { version = "0.1", optional = true } ctaphid-dispatch = { version = "0.1", optional = true } iso7816 = { version = "0.1.2", optional = true } +cbor-smol = { version = "0.4.0", features = ["bytes-from-array"] } + [features] dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] disable-reset-time-window = [] @@ -72,7 +74,7 @@ x509-parser = "0.16.0" features = ["dispatch"] [patch.crates-io] -cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "94ee8c28edf9248b402aa4335c1dee157995197b"} +cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "9a77dc9b528b08f531d76b44af2f5336c4ef17e0"} ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } From 15b1c119a1cca9f022179f2581991ee053331c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 26 Jul 2024 14:38:10 +0200 Subject: [PATCH 085/135] Fix fuzzing --- fuzz/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 244fc39..800adbf 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -29,3 +29,4 @@ trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b548d379d trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" } +cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "9a77dc9b528b08f531d76b44af2f5336c4ef17e0"} From 1dc85d232fe8ae8dedb16b8bf4840338ed846bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Mon, 12 Feb 2024 15:39:31 +0100 Subject: [PATCH 086/135] Serialize credential with fields names using only 1 bytes This saves space when serializing credentials --- Cargo.toml | 1 + src/credential.rs | 411 ++++++++++++++++++++++++++++- src/ctap2.rs | 2 +- src/ctap2/credential_management.rs | 6 +- 4 files changed, 404 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f2ee0f2..6a69850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ log = "0.4.21" p256 = { version = "0.13.2", features = ["ecdh"] } rand = "0.8.4" sha2 = "0.10" +serde_test = "1.0.176" trussed = { version = "0.1", features = ["virt"] } trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] } trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } diff --git a/src/credential.rs b/src/credential.rs index b0219e8..e297201 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -205,15 +205,104 @@ impl Credential { } } +/// Copy of [`ctap_types::webauthn::PublicKeyCredentialUserEntity`] but with shorter field names serialization +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct LocalPublicKeyCredentialRpEntity { + #[serde(rename = "i", alias = "id")] + pub id: String<256>, + // Compared to the ctap_types type, we can skip the truncate, + // since we know we only even deal with the correct length + #[serde(skip_serializing_if = "Option::is_none", rename = "n", alias = "name")] + pub name: Option>, + // Icon is ignored +} +/// Copy of [`ctap_types::webauthn::PublicKeyCredentialUserEntity`] but with with shorter field names serialization +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct LocalPublicKeyCredentialUserEntity { + #[serde(rename = "i", alias = "id")] + pub id: Bytes<64>, + #[serde(skip_serializing_if = "Option::is_none", rename = "I", alias = "icon")] + pub icon: Option>, + #[serde(skip_serializing_if = "Option::is_none", rename = "n", alias = "name")] + pub name: Option>, + #[serde( + skip_serializing_if = "Option::is_none", + rename = "d", + alias = "display_name" + )] + pub display_name: Option>, +} + +impl From for LocalPublicKeyCredentialRpEntity { + fn from(value: ctap_types::webauthn::PublicKeyCredentialRpEntity) -> Self { + let ctap_types::webauthn::PublicKeyCredentialRpEntity { id, name, icon } = value; + let _icon = icon; + + Self { id, name } + } +} + +impl From for ctap_types::webauthn::PublicKeyCredentialRpEntity { + fn from(value: LocalPublicKeyCredentialRpEntity) -> Self { + let LocalPublicKeyCredentialRpEntity { id, name } = value; + + Self { + id, + name, + icon: None, + } + } +} + +impl From + for LocalPublicKeyCredentialUserEntity +{ + fn from(value: ctap_types::webauthn::PublicKeyCredentialUserEntity) -> Self { + let ctap_types::webauthn::PublicKeyCredentialUserEntity { + id, + icon, + name, + display_name, + } = value; + + Self { + id, + icon, + name, + display_name, + } + } +} + +impl From + for ctap_types::webauthn::PublicKeyCredentialUserEntity +{ + fn from(value: LocalPublicKeyCredentialUserEntity) -> Self { + let LocalPublicKeyCredentialUserEntity { + id, + icon, + name, + display_name, + } = value; + + Self { + id, + icon, + name, + display_name, + } + } +} + /// The main content of a `FullCredential`. #[derive( Clone, Debug, PartialEq, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed, )] pub struct CredentialData { // id, name, url - pub rp: ctap_types::webauthn::PublicKeyCredentialRpEntity, + pub rp: LocalPublicKeyCredentialRpEntity, // id, icon, name, display_name - pub user: ctap_types::webauthn::PublicKeyCredentialUserEntity, + pub user: LocalPublicKeyCredentialUserEntity, // can be just a counter, need to be able to determine "latest" pub creation_time: u32, @@ -347,8 +436,8 @@ impl FullCredential { ) -> Self { info!("credential for algorithm {}", algorithm); let data = CredentialData { - rp: rp.clone(), - user: user.clone(), + rp: rp.clone().into(), + user: user.clone().into(), creation_time: timestamp, use_counter: true, @@ -435,7 +524,6 @@ impl FullCredential { let data = &mut stripped.data; data.rp.name = None; - data.rp.icon = None; data.user.icon = None; data.user.name = None; @@ -513,7 +601,6 @@ impl From<&FullCredential> for StrippedCredential { #[cfg(test)] mod test { use super::*; - use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; use trussed::{ client::{Chacha8Poly1305, Sha256}, types::Location, @@ -521,12 +608,11 @@ mod test { fn credential_data() -> CredentialData { CredentialData { - rp: PublicKeyCredentialRpEntity { + rp: LocalPublicKeyCredentialRpEntity { id: String::from("John Doe"), name: None, - icon: None, }, - user: PublicKeyCredentialUserEntity { + user: LocalPublicKeyCredentialUserEntity { id: Bytes::from_slice(&[1, 2, 3]).unwrap(), icon: None, name: None, @@ -608,12 +694,11 @@ mod test { fn random_credential_data() -> CredentialData { CredentialData { - rp: PublicKeyCredentialRpEntity { + rp: LocalPublicKeyCredentialRpEntity { id: random_string(), name: maybe_random_string(), - icon: None, }, - user: PublicKeyCredentialUserEntity { + user: LocalPublicKeyCredentialUserEntity { id: random_bytes(), //Bytes::from_slice(&[1,2,3]).unwrap(), icon: maybe_random_string(), name: maybe_random_string(), @@ -724,6 +809,308 @@ mod test { }); } + #[test] + fn local_derive_rp_name_none() { + use serde_test::{assert_de_tokens, assert_tokens, Token}; + let rp_id = LocalPublicKeyCredentialRpEntity { + id: "Testing rp id".into(), + name: None, + }; + + assert_tokens( + &rp_id, + &[ + Token::Struct { + name: "LocalPublicKeyCredentialRpEntity", + len: 1, + }, + Token::Str("i"), + Token::Str("Testing rp id"), + Token::StructEnd, + ], + ); + assert_de_tokens( + &rp_id, + &[ + Token::Map { len: Some(1) }, + Token::Str("id"), + Token::Str("Testing rp id"), + Token::MapEnd, + ], + ); + } + + #[test] + fn local_derive_rp_name_some() { + use serde_test::{assert_de_tokens, assert_tokens, Token}; + let rp_id = LocalPublicKeyCredentialRpEntity { + id: "Testing rp id".into(), + name: Some("Testing rp name".into()), + }; + + assert_tokens( + &rp_id, + &[ + Token::Struct { + name: "LocalPublicKeyCredentialRpEntity", + len: 2, + }, + Token::Str("i"), + Token::Str("Testing rp id"), + Token::Str("n"), + Token::Some, + Token::Str("Testing rp name"), + Token::StructEnd, + ], + ); + assert_de_tokens( + &rp_id, + &[ + Token::Map { len: Some(2) }, + Token::Str("id"), + Token::Str("Testing rp id"), + Token::Str("name"), + Token::Some, + Token::Str("Testing rp name"), + Token::MapEnd, + ], + ); + } + + #[test] + fn local_derive_user() { + use serde_test::{assert_de_tokens, assert_tokens, Token}; + + let rp_id = LocalPublicKeyCredentialUserEntity { + id: Bytes::from_slice(b"Testing user id").unwrap(), + icon: Some("Testing user icon".into()), + name: Some("Testing user name".into()), + display_name: Some("Testing user display_name".into()), + }; + assert_tokens( + &rp_id, + &[ + Token::Struct { + name: "LocalPublicKeyCredentialUserEntity", + len: 4, + }, + Token::Str("i"), + Token::Bytes(b"Testing user id"), + Token::Str("I"), + Token::Some, + Token::Str("Testing user icon"), + Token::Str("n"), + Token::Some, + Token::Str("Testing user name"), + Token::Str("d"), + Token::Some, + Token::Str("Testing user display_name"), + Token::StructEnd, + ], + ); + assert_de_tokens( + &rp_id, + &[ + Token::Struct { + name: "LocalPublicKeyCredentialUserEntity", + len: 4, + }, + Token::Str("id"), + Token::Bytes(b"Testing user id"), + Token::Str("icon"), + Token::Some, + Token::Str("Testing user icon"), + Token::Str("name"), + Token::Some, + Token::Str("Testing user name"), + Token::Str("display_name"), + Token::Some, + Token::Str("Testing user display_name"), + Token::StructEnd, + ], + ); + + let rp_id = LocalPublicKeyCredentialUserEntity { + id: Bytes::from_slice(b"Testing user id").unwrap(), + icon: None, + name: None, + display_name: Some("Testing user display_name".into()), + }; + assert_tokens( + &rp_id, + &[ + Token::Struct { + name: "LocalPublicKeyCredentialUserEntity", + len: 2, + }, + Token::Str("i"), + Token::Bytes(b"Testing user id"), + Token::Str("d"), + Token::Some, + Token::Str("Testing user display_name"), + Token::StructEnd, + ], + ); + assert_de_tokens( + &rp_id, + &[ + Token::Struct { + name: "LocalPublicKeyCredentialUserEntity", + len: 2, + }, + Token::Str("id"), + Token::Bytes(b"Testing user id"), + Token::Str("display_name"), + Token::Some, + Token::Str("Testing user display_name"), + Token::StructEnd, + ], + ); + + let rp_id = LocalPublicKeyCredentialUserEntity { + id: Bytes::from_slice(b"Testing user id").unwrap(), + icon: Some("Testing user icon".into()), + name: None, + display_name: Some("Testing user display_name".into()), + }; + assert_tokens( + &rp_id, + &[ + Token::Struct { + name: "LocalPublicKeyCredentialUserEntity", + len: 3, + }, + Token::Str("i"), + Token::Bytes(b"Testing user id"), + Token::Str("I"), + Token::Some, + Token::Str("Testing user icon"), + Token::Str("d"), + Token::Some, + Token::Str("Testing user display_name"), + Token::StructEnd, + ], + ); + assert_de_tokens( + &rp_id, + &[ + Token::Map { len: Some(3) }, + Token::Str("id"), + Token::Bytes(b"Testing user id"), + Token::Str("icon"), + Token::Some, + Token::Str("Testing user icon"), + Token::Str("display_name"), + Token::Some, + Token::Str("Testing user display_name"), + Token::MapEnd, + ], + ); + + let rp_id = LocalPublicKeyCredentialUserEntity { + id: Bytes::from_slice(b"Testing user id").unwrap(), + icon: Some("Testing user icon".into()), + name: None, + display_name: None, + }; + assert_tokens( + &rp_id, + &[ + Token::Struct { + name: "LocalPublicKeyCredentialUserEntity", + len: 2, + }, + Token::Str("i"), + Token::Bytes(b"Testing user id"), + Token::Str("I"), + Token::Some, + Token::Str("Testing user icon"), + Token::StructEnd, + ], + ); + assert_de_tokens( + &rp_id, + &[ + Token::Map { len: Some(2) }, + Token::Str("id"), + Token::Bytes(b"Testing user id"), + Token::Str("icon"), + Token::Some, + Token::Str("Testing user icon"), + Token::MapEnd, + ], + ); + + let rp_id = LocalPublicKeyCredentialUserEntity { + id: Bytes::from_slice(b"Testing user id").unwrap(), + icon: None, + name: None, + display_name: None, + }; + assert_tokens( + &rp_id, + &[ + Token::Struct { + name: "LocalPublicKeyCredentialUserEntity", + len: 1, + }, + Token::Str("i"), + Token::Bytes(b"Testing user id"), + Token::StructEnd, + ], + ); + assert_de_tokens( + &rp_id, + &[ + Token::Map { len: Some(1) }, + Token::Str("id"), + Token::Bytes(b"Testing user id"), + Token::MapEnd, + ], + ); + } + + // Test credentials that were serialized before the migration to shorter field names for serialization + #[test] + fn legacy_full_credential() { + use hex_literal::hex; + let data = hex!( + " + a3000201a700a16269646b776562617574686e2e696f01a2626964476447 + 567a644445646e616d65657465737431020003f504260582005037635754 + c9882b21565a9f8a47b0ece408f5024cf62ca01ed181a3d03d561fc7 + " + ); + + let credential = FullCredential::deserialize(&Bytes::from_slice(&data).unwrap()).unwrap(); + assert!(matches!(credential.ctap, CtapVersion::Fido21Pre)); + assert_eq!(credential.nonce, &hex!("F62CA01ED181A3D03D561FC7")); + assert_eq!( + credential.data, + CredentialData { + rp: LocalPublicKeyCredentialRpEntity { + id: "webauthn.io".into(), + name: None, + }, + user: LocalPublicKeyCredentialUserEntity { + id: Bytes::from_slice(&hex!("6447567A644445")).unwrap(), + icon: None, + name: Some("test1".into()), + display_name: None, + }, + creation_time: 0, + use_counter: true, + algorithm: -7, + key: Key::ResidentKey(KeyId::from_value(0x37635754C9882B21565A9F8A47B0ECE4)), + hmac_secret: None, + cred_protect: None, + use_short_id: Some(true), + large_blob_key: None, + third_party_payment: None, + }, + ); + } + // use quickcheck::TestResult; // quickcheck::quickcheck! { // fn prop( diff --git a/src/ctap2.rs b/src/ctap2.rs index 1ef0737..e038291 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1753,7 +1753,7 @@ impl crate::Authenticator { user.name = None; user.display_name = None; } - response.user = Some(user); + response.user = Some(user.into()); } } diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 527fc52..1ba112b 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -167,7 +167,7 @@ where let rp = credential.data.rp; response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id.as_ref()))); - response.rp = Some(rp); + response.rp = Some(rp.into()); } } @@ -244,7 +244,7 @@ where let rp = credential.data.rp; response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id.as_ref()))); - response.rp = Some(rp); + response.rp = Some(rp.into()); // cache state for next call if remaining > 1 { @@ -448,7 +448,7 @@ where }; let mut response = Response::default(); - response.user = Some(credential.data.user); + response.user = Some(credential.data.user.into()); response.credential_id = Some(credential_id.into()); response.public_key = Some(cose_public_key); response.cred_protect = cred_protect; From 44f0299c490e82075c3b722cd63539f1b3631b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Mon, 19 Feb 2024 14:57:30 +0100 Subject: [PATCH 087/135] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3a3b4..6f11415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement UpdateUserInformation subcommand for CredentialManagement - Support CTAP 2.1 - Serialize PIN hash with `serde-bytes` ([#52][]) +- Reduce the space taken by credential serializaiton ([#59][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -39,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#62]: https://github.com/Nitrokey/fido-authenticator/pull/62 [#63]: https://github.com/Nitrokey/fido-authenticator/pull/63 [#52]: https://github.com/Nitrokey/fido-authenticator/issues/52 +[#59]: https://github.com/Nitrokey/fido-authenticator/issues/59 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp From 2d7855a17bb2591b358d028f4836ca658e9612f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 25 Jul 2024 11:57:07 +0200 Subject: [PATCH 088/135] Add dynamic estimation of remaining credential space --- Cargo.toml | 10 ++++--- fuzz/Cargo.toml | 8 +++-- src/ctap2.rs | 18 ++--------- src/ctap2/credential_management.rs | 48 ++---------------------------- src/lib.rs | 48 +++++++++++++++++++++++++++++- 5 files changed, 63 insertions(+), 69 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f2ee0f2..13f2b6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ serde_bytes = { version = "0.11.14", default-features = false } serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } trussed = "0.1" +trussed-fs-info = "0.1.0" trussed-hkdf = { version = "0.2.0" } trussed-chunked = { version = "0.1.0", optional = true } @@ -65,7 +66,7 @@ p256 = { version = "0.13.2", features = ["ecdh"] } rand = "0.8.4" sha2 = "0.10" trussed = { version = "0.1", features = ["virt"] } -trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] } +trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] } trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } usbd-ctaphid = "0.1.0" x509-parser = "0.16.0" @@ -78,10 +79,11 @@ cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } -littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b548d379dcbd67d29453d94847b7bc33ae92e673" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" } +littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } +trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } usbd-ctaphid = { git = "https://github.com/trussed-dev/usbd-ctaphid.git", rev = "dcff9009c3cd1ef9e5b09f8f307aca998fc9a8c8" } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 800adbf..765d813 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,7 +10,7 @@ cargo-fuzz = true [dependencies] ctap-types = { version = "0.2.0", features = ["arbitrary"] } libfuzzer-sys = "0.4" -trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] } +trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] } [dependencies.fido-authenticator] path = ".." @@ -24,9 +24,11 @@ bench = false [patch.crates-io] ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" } -littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b548d379dcbd67d29453d94847b7bc33ae92e673" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" } +littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" } cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "9a77dc9b528b08f531d76b44af2f5336c4ef17e0"} +trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 1ef0737..89bec75 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -393,21 +393,7 @@ impl Authenticator for crate::Authenti self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id) .ok(); - let mut key_store_full = false; - - // then check the maximum number of RK credentials - if let Some(max_count) = self.config.max_resident_credential_count { - let mut cm = credential_management::CredentialManagement::new(self); - let metadata = cm.get_creds_metadata(); - let count = metadata - .existing_resident_credentials_count - .unwrap_or(max_count); - debug!("resident cred count: {} (max: {})", count, max_count); - if count >= max_count { - error!("maximum resident credential count reached"); - key_store_full = true; - } - } + let mut key_store_full = !self.can_fit(serialized_credential.len()); if !key_store_full { // then store key, making it resident @@ -1870,7 +1856,7 @@ impl crate::Authenticator { ); } else { info!("deleting parent {:?} as this was its last RK", &rp_path); - syscall!(self.trussed.remove_dir(Location::Internal, rp_path,)); + try_syscall!(self.trussed.remove_dir(Location::Internal, rp_path,)).ok(); } } diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 527fc52..08dccd9 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -15,7 +15,6 @@ use ctap_types::{ }; use crate::{ - constants::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE, credential::FullCredential, state::{CredentialManagementEnumerateCredentials, CredentialManagementEnumerateRps}, Authenticator, Result, TrussedRequirements, UserPresence, @@ -65,53 +64,12 @@ where info!("get metadata"); let mut response: Response = Default::default(); - let max_resident_credentials = self - .config - .max_resident_credential_count - .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE); + let max_resident_credentials = self.estimate_remaining(); response.existing_resident_credentials_count = Some(0); response.max_possible_remaining_residential_credentials_count = - Some(max_resident_credentials); + Some(max_resident_credentials.try_into().unwrap_or(u32::MAX)); - let dir = PathBuf::from(b"rk"); - let maybe_first_rp = - syscall!(self - .trussed - .read_dir_first(Location::Internal, dir.clone(), None)) - .entry; - - let first_rp = match maybe_first_rp { - None => return response, - Some(rp) => rp, - }; - - let (mut num_rks, _) = self.count_rp_rks(PathBuf::from(first_rp.path())); - 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(); - 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.max_possible_remaining_residential_credentials_count = - Some(max_resident_credentials.saturating_sub(num_rks)); - return response; - } - Some(rp) => { - last_rp = PathBuf::from(rp.file_name()); - info!("counting.."); - let (this_rp_rk_count, _) = self.count_rp_rks(PathBuf::from(rp.path())); - info!("{:?}", this_rp_rk_count); - num_rks += this_rp_rk_count; - } - } - } + response } pub fn first_relying_party(&mut self) -> Result { diff --git a/src/lib.rs b/src/lib.rs index e6e576a..db3013e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,12 @@ generate_macros!(); use core::time::Duration; -use trussed::{client, syscall, types::Message, Client as TrussedClient}; +use trussed::{ + client, syscall, + types::{Location, Message}, + Client as TrussedClient, +}; +use trussed_fs_info::{FsInfoClient, FsInfoReply}; use trussed_hkdf::HkdfClient; /// Re-export of `ctap-types` authenticator errors. @@ -38,6 +43,8 @@ pub mod state; pub use ctap2::large_blobs::Config as LargeBlobsConfig; +use crate::constants::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE; + /// Results with our [`Error`]. pub type Result = core::result::Result; @@ -57,6 +64,7 @@ pub trait TrussedRequirements: + client::Sha256 + client::HmacSha256 + client::Ed255 // + client::Totp + + FsInfoClient + HkdfClient + ExtensionRequirements { @@ -70,6 +78,7 @@ impl TrussedRequirements for T where + client::Sha256 + client::HmacSha256 + client::Ed255 // + client::Totp + + FsInfoClient + HkdfClient + ExtensionRequirements { @@ -266,6 +275,43 @@ where } } + fn estimate_remaining_inner(info: &FsInfoReply) -> usize { + let block_size = info.block_info.as_ref().map(|i| i.size).unwrap_or(255); + // 1 block for the directory, 1 for the private key, 400 bytes for a reasonnable key and metadata + let size_taken = 2 * block_size + 400; + // Remove 5 block kept as buffer + (info.available_space - 5 * block_size) / size_taken + } + + fn estimate_remaining(&mut self) -> usize { + let info = syscall!(self.trussed.fs_info(Location::Internal)); + debug!("Got filesystem info: {info:?}"); + Self::estimate_remaining_inner(&info).min( + self.config + .max_resident_credential_count + .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE) as usize, + ) + } + + fn can_fit_inner(info: &FsInfoReply, size: usize) -> bool { + let block_size = info.block_info.as_ref().map(|i| i.size).unwrap_or(255); + // 1 block for the rp directory, 5 block of margin, 50 bytes for a reasonnable metadata + let size_taken = 6 * block_size + size + 50; + size_taken < info.available_space + } + + /// Can a credential of size `size` be stored with safe margins + fn can_fit(&mut self, size: usize) -> bool { + debug!("Can fit for {size} bytes"); + let info = syscall!(self.trussed.fs_info(Location::Internal)); + debug!("Got filesystem info: {info:?}"); + debug!( + "Available storage: {}", + Self::estimate_remaining_inner(&info) + ); + Self::can_fit_inner(&info, size) + } + fn hash(&mut self, data: &[u8]) -> [u8; 32] { let hash = syscall!(self.trussed.hash_sha256(data)).hash; hash.as_slice().try_into().expect("hash should fit") From b43596f7376100dbe00a7c960f65328ffcf2c0bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 26 Jul 2024 11:01:46 +0200 Subject: [PATCH 089/135] Fix credential count and add back hard limit --- src/ctap2.rs | 18 ++++++------- src/ctap2/credential_management.rs | 41 +++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 89bec75..ee85736 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,5 +1,6 @@ //! The `ctap_types::ctap2::Authenticator` implementation. +use credential_management::CredentialManagement; use ctap_types::{ ctap2::{ self, client_pin::Permissions, AttestationFormatsPreference, AttestationStatement, @@ -21,15 +22,9 @@ use trussed::{ }; use crate::{ - constants, + constants::{self, MAX_RESIDENT_CREDENTIALS_GUESSTIMATE}, credential::{self, Credential, FullCredential, Key, StrippedCredential}, - format_hex, - state::{ - self, - // // (2022-02-27): 9288 bytes - // MinCredentialHeap, - }, - Result, SigningAlgorithm, TrussedRequirements, UserPresence, + format_hex, state, Result, SigningAlgorithm, TrussedRequirements, UserPresence, }; #[allow(unused_imports)] @@ -393,7 +388,12 @@ impl Authenticator for crate::Authenti self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id) .ok(); - let mut key_store_full = !self.can_fit(serialized_credential.len()); + let mut key_store_full = !self.can_fit(serialized_credential.len()) + || CredentialManagement::new(self).count_credentials() + >= self + .config + .max_resident_credential_count + .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE); if !key_store_full { // then store key, making it resident diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 08dccd9..da67813 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -65,13 +65,52 @@ where let mut response: Response = Default::default(); let max_resident_credentials = self.estimate_remaining(); - response.existing_resident_credentials_count = Some(0); + response.existing_resident_credentials_count = Some(self.count_credentials()); response.max_possible_remaining_residential_credentials_count = Some(max_resident_credentials.try_into().unwrap_or(u32::MAX)); response } + pub fn count_credentials(&mut self) -> u32 { + let dir = PathBuf::from(b"rk"); + let maybe_first_rp = + syscall!(self + .trussed + .read_dir_first(Location::Internal, dir.clone(), None)) + .entry; + + let first_rp = match maybe_first_rp { + None => return 0, + Some(rp) => rp, + }; + + let (mut num_rks, _) = self.count_rp_rks(PathBuf::from(first_rp.path())); + 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(); + let maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; + + match maybe_next_rp { + None => { + return num_rks; + } + Some(rp) => { + last_rp = PathBuf::from(rp.file_name()); + info!("counting.."); + let (this_rp_rk_count, _) = self.count_rp_rks(PathBuf::from(rp.path())); + info!("{:?}", this_rp_rk_count); + num_rks += this_rp_rk_count; + } + } + } + } + pub fn first_relying_party(&mut self) -> Result { info!("first rp"); From 4f8e8a4bf6e1a2102da8e9254b22cfeec2100cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 26 Jul 2024 11:34:24 +0200 Subject: [PATCH 090/135] Remove unnecessary conversions --- src/ctap2/credential_management.rs | 2 +- src/lib.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index da67813..2a5fa06 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -67,7 +67,7 @@ where let max_resident_credentials = self.estimate_remaining(); response.existing_resident_credentials_count = Some(self.count_credentials()); response.max_possible_remaining_residential_credentials_count = - Some(max_resident_credentials.try_into().unwrap_or(u32::MAX)); + Some(max_resident_credentials); response } diff --git a/src/lib.rs b/src/lib.rs index db3013e..2f93b93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -275,21 +275,21 @@ where } } - fn estimate_remaining_inner(info: &FsInfoReply) -> usize { + fn estimate_remaining_inner(info: &FsInfoReply) -> u32 { let block_size = info.block_info.as_ref().map(|i| i.size).unwrap_or(255); // 1 block for the directory, 1 for the private key, 400 bytes for a reasonnable key and metadata let size_taken = 2 * block_size + 400; // Remove 5 block kept as buffer - (info.available_space - 5 * block_size) / size_taken + ((info.available_space - 5 * block_size) / size_taken) as u32 } - fn estimate_remaining(&mut self) -> usize { + fn estimate_remaining(&mut self) -> u32 { let info = syscall!(self.trussed.fs_info(Location::Internal)); debug!("Got filesystem info: {info:?}"); Self::estimate_remaining_inner(&info).min( self.config .max_resident_credential_count - .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE) as usize, + .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE), ) } From 4f95f963c2e69e230136e545ff17262c033f49b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 31 Jul 2024 15:20:24 +0200 Subject: [PATCH 091/135] Fix compilation --- fuzz/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 765d813..5adc5ca 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -28,7 +28,6 @@ trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79 littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" } cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "9a77dc9b528b08f531d76b44af2f5336c4ef17e0"} trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } From 741348fd506ddc40caad0d335f7b08666bd40c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 1 Aug 2024 10:49:31 +0200 Subject: [PATCH 092/135] Don't use estimate if block size is not available --- src/ctap2.rs | 2 +- src/ctap2/credential_management.rs | 11 +++++++++-- src/lib.rs | 24 +++++++++--------------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index ee85736..50c5e28 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -388,7 +388,7 @@ impl Authenticator for crate::Authenti self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id) .ok(); - let mut key_store_full = !self.can_fit(serialized_credential.len()) + let mut key_store_full = self.can_fit(serialized_credential.len()) == Some(false) || CredentialManagement::new(self).count_credentials() >= self .config diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 2a5fa06..15134d3 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -15,6 +15,7 @@ use ctap_types::{ }; use crate::{ + constants::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE, credential::FullCredential, state::{CredentialManagementEnumerateCredentials, CredentialManagementEnumerateRps}, Authenticator, Result, TrussedRequirements, UserPresence, @@ -65,9 +66,15 @@ where let mut response: Response = Default::default(); let max_resident_credentials = self.estimate_remaining(); - response.existing_resident_credentials_count = Some(self.count_credentials()); + let credential_count = self.count_credentials(); + response.existing_resident_credentials_count = Some(credential_count); response.max_possible_remaining_residential_credentials_count = - Some(max_resident_credentials); + Some(max_resident_credentials.unwrap_or_else(|| { + self.config + .max_resident_credential_count + .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE) + .saturating_sub(credential_count) + })); response } diff --git a/src/lib.rs b/src/lib.rs index 2f93b93..5a83805 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,8 +43,6 @@ pub mod state; pub use ctap2::large_blobs::Config as LargeBlobsConfig; -use crate::constants::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE; - /// Results with our [`Error`]. pub type Result = core::result::Result; @@ -275,33 +273,29 @@ where } } - fn estimate_remaining_inner(info: &FsInfoReply) -> u32 { - let block_size = info.block_info.as_ref().map(|i| i.size).unwrap_or(255); + fn estimate_remaining_inner(info: &FsInfoReply) -> Option { + let block_size = info.block_info.as_ref()?.size; // 1 block for the directory, 1 for the private key, 400 bytes for a reasonnable key and metadata let size_taken = 2 * block_size + 400; // Remove 5 block kept as buffer - ((info.available_space - 5 * block_size) / size_taken) as u32 + Some((info.available_space.saturating_sub(5 * block_size) / size_taken) as u32) } - fn estimate_remaining(&mut self) -> u32 { + fn estimate_remaining(&mut self) -> Option { let info = syscall!(self.trussed.fs_info(Location::Internal)); debug!("Got filesystem info: {info:?}"); - Self::estimate_remaining_inner(&info).min( - self.config - .max_resident_credential_count - .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE), - ) + Self::estimate_remaining_inner(&info) } - fn can_fit_inner(info: &FsInfoReply, size: usize) -> bool { - let block_size = info.block_info.as_ref().map(|i| i.size).unwrap_or(255); + fn can_fit_inner(info: &FsInfoReply, size: usize) -> Option { + let block_size = info.block_info.as_ref()?.size; // 1 block for the rp directory, 5 block of margin, 50 bytes for a reasonnable metadata let size_taken = 6 * block_size + size + 50; - size_taken < info.available_space + Some(size_taken < info.available_space) } /// Can a credential of size `size` be stored with safe margins - fn can_fit(&mut self, size: usize) -> bool { + fn can_fit(&mut self, size: usize) -> Option { debug!("Can fit for {size} bytes"); let info = syscall!(self.trussed.fs_info(Location::Internal)); debug!("Got filesystem info: {info:?}"); From e763a713ac2ac7e268c7f7ef84e644f4c3a41352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 1 Aug 2024 11:10:49 +0200 Subject: [PATCH 093/135] Apply suggestion --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 5a83805..2166238 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -295,6 +295,8 @@ where } /// Can a credential of size `size` be stored with safe margins + /// + /// This assumes that the key has already been generated and is stored. fn can_fit(&mut self, size: usize) -> Option { debug!("Can fit for {size} bytes"); let info = syscall!(self.trussed.fs_info(Location::Internal)); From 0f51cb707e1132ac8a7cd2e8a0d9f74097ac0e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 1 Aug 2024 11:26:11 +0200 Subject: [PATCH 094/135] Fix CI --- src/ctap2/large_blobs.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ctap2/large_blobs.rs b/src/ctap2/large_blobs.rs index 094a06e..005695e 100644 --- a/src/ctap2/large_blobs.rs +++ b/src/ctap2/large_blobs.rs @@ -1,11 +1,13 @@ use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error}; use trussed::{ - client::Client, config::MAX_MESSAGE_LENGTH, - syscall, try_syscall, - types::{Bytes, Location, Mechanism, Message, PathBuf}, + try_syscall, + types::{Bytes, Location, Message, PathBuf}, }; +#[cfg(not(feature = "chunked"))] +use trussed::{syscall, types::Mechanism}; + use crate::{Result, TrussedRequirements}; const HASH_SIZE: usize = 16; @@ -151,12 +153,14 @@ type SelectedStorage = ChunkedStorage; // Basic implementation using a file in the volatile storage as a buffer based on the core Trussed // API. Maximum size for the entire large blob array: 1024 bytes. +#[cfg(not(feature = "chunked"))] struct SimpleStorage { location: Location, buffer: Message, } -impl Storage for SimpleStorage { +#[cfg(not(feature = "chunked"))] +impl Storage for SimpleStorage { fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result { let result = try_syscall!(client.read_file(location, PathBuf::from(FILENAME))); let data = if let Ok(reply) = &result { From 5ee16d115fe70c3f0ddaa95d6a0011c5ad3a7f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Thu, 1 Aug 2024 15:54:01 +0200 Subject: [PATCH 095/135] Fix delog and use ctap-types 0.3.0 --- Cargo.toml | 3 +-- fuzz/Cargo.toml | 3 +-- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 13f2b6b..09098f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "usbip" required-features = ["dispatch"] [dependencies] -ctap-types = { version = "0.2.0", features = ["get-info-full", "large-blobs", "third-party-payment"] } +ctap-types = { version = "0.3.0", features = ["get-info-full", "large-blobs", "third-party-payment"] } cosey = "0.3" delog = "0.1.0" heapless = "0.7" @@ -76,7 +76,6 @@ features = ["dispatch"] [patch.crates-io] cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "9a77dc9b528b08f531d76b44af2f5336c4ef17e0"} -ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 5adc5ca..2d96bc6 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" cargo-fuzz = true [dependencies] -ctap-types = { version = "0.2.0", features = ["arbitrary"] } +ctap-types = { version = "0.3.0", features = ["arbitrary"] } libfuzzer-sys = "0.4" trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] } @@ -23,7 +23,6 @@ doc = false bench = false [patch.crates-io] -ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" } littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } diff --git a/src/lib.rs b/src/lib.rs index 2166238..8f2cc86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -302,7 +302,7 @@ where let info = syscall!(self.trussed.fs_info(Location::Internal)); debug!("Got filesystem info: {info:?}"); debug!( - "Available storage: {}", + "Available storage: {:?}", Self::estimate_remaining_inner(&info) ); Self::can_fit_inner(&info, size) From 125d38e1ea66242a9ee31e2148808065c059506b Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 2 Oct 2024 16:17:34 +0200 Subject: [PATCH 096/135] Fix clippy lints --- src/dispatch.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/dispatch.rs b/src/dispatch.rs index e5734b4..9a5b37d 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -175,10 +175,9 @@ where // Goal of these nested scopes is to keep stack small. let ctap_request = ctap2::Request::deserialize(data) - .map(|request| { - info!("Received CTAP2 request {:?}", request_operation(&request)); - trace!("CTAP2 request: {:?}", request); - request + .inspect(|_request| { + info!("Received CTAP2 request {:?}", request_operation(_request)); + trace!("CTAP2 request: {:?}", _request); }) .map_err(|error| { error!("Failed to deserialize CTAP2 request: {:?}", error); @@ -189,10 +188,9 @@ where use ctap2::Authenticator; authenticator .call_ctap2(&ctap_request) - .map(|response| { - info!("Sending CTAP2 response {:?}", response_operation(&response)); - trace!("CTAP2 response: {:?}", response); - response + .inspect(|_response| { + info!("Sending CTAP2 response {:?}", response_operation(_response)); + trace!("CTAP2 response: {:?}", _response); }) .map_err(|error| { info!("CTAP2 error: {:?}", error); From 25f99bea19d5ee5fc122dd890765dc5d9e01ba2c Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 8 Oct 2024 16:00:09 +0200 Subject: [PATCH 097/135] Update cbor-smol to 0.4.1 --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 35510e7..d0a787d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ apdu-dispatch = { version = "0.1", optional = true } ctaphid-dispatch = { version = "0.1", optional = true } iso7816 = { version = "0.1.2", optional = true } -cbor-smol = { version = "0.4.0", features = ["bytes-from-array"] } +cbor-smol = { version = "0.4.1" } [features] dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] @@ -76,7 +76,6 @@ x509-parser = "0.16.0" features = ["dispatch"] [patch.crates-io] -cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "9a77dc9b528b08f531d76b44af2f5336c4ef17e0"} ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" } From b34fa475c0c97bc7610ee65d11ecb2191f8d4770 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 2 Oct 2024 12:07:24 +0200 Subject: [PATCH 098/135] get_assertion: Skip attStmt unless requested For makeCredential, a missing attestation format preference list means that we should use the default format (packed). For getAssertion, it means that we should skip the attestation statement entirely. Previously, we implemented the makeCredential algorithm for both cases. This caused an incompatibility with firefox because it fails on unexpected fields in the response (in this case, the attestation statement). This patch fixes this issue and applies the correct default for getAssertion requests. Fixes: https://github.com/Nitrokey/fido-authenticator/issues/98 --- src/ctap2.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index c9ac166..844b753 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -472,8 +472,12 @@ impl Authenticator for crate::Authenti let serialized_auth_data = authenticator_data.serialize()?; - let att_stmt_fmt = - SupportedAttestationFormat::select(parameters.attestation_formats_preference.as_ref()); + // select attestation format or use packed attestation as default + let att_stmt_fmt = parameters + .attestation_formats_preference + .as_ref() + .map(SupportedAttestationFormat::select) + .unwrap_or(Some(SupportedAttestationFormat::Packed)); let att_stmt = if let Some(format) = att_stmt_fmt { match format { SupportedAttestationFormat::None => { @@ -1673,8 +1677,11 @@ impl crate::Authenticator { .to_bytes() .unwrap(); - let att_stmt_fmt = - SupportedAttestationFormat::select(data.attestation_formats_preference.as_ref()); + // select preferred format or skip attestation statement + let att_stmt_fmt = data + .attestation_formats_preference + .as_ref() + .and_then(SupportedAttestationFormat::select); let att_stmt = if let Some(format) = att_stmt_fmt { match format { SupportedAttestationFormat::None => { @@ -2016,11 +2023,7 @@ enum SupportedAttestationFormat { } impl SupportedAttestationFormat { - fn select(preference: Option<&AttestationFormatsPreference>) -> Option { - let Some(preference) = preference else { - // no preference, default to packed format - return Some(Self::Packed); - }; + fn select(preference: &AttestationFormatsPreference) -> Option { if preference.known_formats() == [AttestationStatementFormat::None] && !preference.includes_unknown_formats() { From 629a75f189a3db930070fb19753a9d347afea38d Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 17 Oct 2024 23:13:59 +0200 Subject: [PATCH 099/135] Use apdu-app instead of apdu-dispatch --- Cargo.toml | 6 +++--- src/dispatch.rs | 38 ++++++++++++++++++++------------------ src/dispatch/apdu.rs | 28 ++++++++++++++++------------ 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d0a787d..5ec6166 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "usbip" required-features = ["dispatch"] [dependencies] -ctap-types = { version = "0.3.0", features = ["get-info-full", "large-blobs", "third-party-payment"] } +ctap-types = { version = "0.3.1", features = ["get-info-full", "large-blobs", "third-party-payment"] } cosey = "0.3" delog = "0.1.0" heapless = "0.7" @@ -28,7 +28,7 @@ trussed-fs-info = "0.1.0" trussed-hkdf = { version = "0.2.0" } trussed-chunked = { version = "0.1.0", optional = true } -apdu-dispatch = { version = "0.1", optional = true } +apdu-app = { version = "0.1", optional = true } ctaphid-dispatch = { version = "0.1", optional = true } iso7816 = { version = "0.1.2", optional = true } @@ -36,6 +36,7 @@ cbor-smol = { version = "0.4.1" } [features] dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] +apdu-dispatch = ["dep:apdu-app"] disable-reset-time-window = [] # enables support for a large-blob array longer than 1024 bytes @@ -77,7 +78,6 @@ features = ["dispatch"] [patch.crates-io] ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } -apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" } littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } diff --git a/src/dispatch.rs b/src/dispatch.rs index 9a5b37d..50e4c96 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -8,7 +8,7 @@ use crate::msp; use crate::{Authenticator, TrussedRequirements, UserPresence}; use ctap_types::{ctap1, ctap2}; -use iso7816::Status; +use iso7816::{command::CommandView, Data, Status}; impl iso7816::App for Authenticator where @@ -21,10 +21,10 @@ where #[inline(never)] /// Deserialize U2F, call authenticator, serialize response *Result*. -fn handle_ctap1_from_hid( +fn handle_ctap1_from_hid( authenticator: &mut Authenticator, data: &[u8], - response: &mut apdu_dispatch::response::Data, + response: &mut Data, ) where T: TrussedRequirements, UP: UserPresence, @@ -34,16 +34,18 @@ fn handle_ctap1_from_hid( msp() - 0x2000_0000 ); { - let command = apdu_dispatch::Command::try_from(data); - if let Err(_status) = command { - let code: [u8; 2] = (Status::IncorrectDataParameter).into(); - debug!("CTAP1 parse error: {:?} ({})", _status, hex_str!(&code)); - response.extend_from_slice(&code).ok(); - return; - } + let command = match CommandView::try_from(data) { + Ok(command) => command, + Err(_status) => { + let code: [u8; 2] = (Status::IncorrectDataParameter).into(); + debug!("CTAP1 parse error: {:?} ({})", _status, hex_str!(&code)); + response.extend_from_slice(&code).ok(); + return; + } + }; // debug!("1A SP: {:X}", msp()); - match try_handle_ctap1(authenticator, &command.unwrap(), response) { + match try_handle_ctap1(authenticator, command, response) { Ok(()) => { debug!("U2F response {} bytes", response.len()); // Need to add x9000 success code (normally the apdu-dispatch does this, but @@ -63,10 +65,10 @@ fn handle_ctap1_from_hid( #[inline(never)] /// Deserialize CBOR, call authenticator, serialize response *Result*. -fn handle_ctap2( +fn handle_ctap2( authenticator: &mut Authenticator, data: &[u8], - response: &mut apdu_dispatch::response::Data, + response: &mut Data, ) where T: TrussedRequirements, UP: UserPresence, @@ -87,10 +89,10 @@ fn handle_ctap2( } #[inline(never)] -fn try_handle_ctap1( +fn try_handle_ctap1( authenticator: &mut Authenticator, - command: &apdu_dispatch::Command, - response: &mut apdu_dispatch::response::Data, + command: CommandView<'_>, + response: &mut Data, ) -> Result<(), Status> where T: TrussedRequirements, @@ -122,10 +124,10 @@ where } #[inline(never)] -fn try_handle_ctap2( +fn try_handle_ctap2( authenticator: &mut Authenticator, data: &[u8], - response: &mut apdu_dispatch::response::Data, + response: &mut Data, ) -> Result<(), u8> where T: TrussedRequirements, diff --git a/src/dispatch/apdu.rs b/src/dispatch/apdu.rs index c868c91..c92dc68 100644 --- a/src/dispatch/apdu.rs +++ b/src/dispatch/apdu.rs @@ -1,7 +1,7 @@ -use apdu_dispatch::{app as apdu, dispatch::Interface, response::Data, Command}; +use apdu_app::Interface; use ctap_types::{serde::error::Error as SerdeError, Error}; use ctaphid_dispatch::app as ctaphid; -use iso7816::Status; +use iso7816::{command::CommandView, Data, Status}; use crate::{Authenticator, TrussedRequirements, UserPresence}; @@ -22,16 +22,20 @@ impl From for Error { } } -impl apdu::App<{ apdu_dispatch::command::SIZE }, { apdu_dispatch::response::SIZE }> - for Authenticator +impl apdu_app::App for Authenticator where UP: UserPresence, T: TrussedRequirements, { - fn select(&mut self, interface: Interface, _: &Command, reply: &mut Data) -> apdu::Result { + fn select( + &mut self, + interface: Interface, + _: CommandView<'_>, + reply: &mut Data, + ) -> apdu_app::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 { + if interface != Interface::Contactless { return Err(Status::ConditionsOfUseNotSatisfied); } @@ -43,13 +47,13 @@ where fn call( &mut self, - interface: apdu::Interface, - apdu: &Command, - response: &mut Data, - ) -> apdu::Result { + interface: Interface, + apdu: CommandView<'_>, + response: &mut Data, + ) -> apdu_app::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 { + if interface != Interface::Contactless { return Err(Status::ConditionsOfUseNotSatisfied); } @@ -72,7 +76,7 @@ where // 0x10 Ok(ctaphid::Command::Cbor) => super::handle_ctap2(self, apdu.data(), response), Ok(ctaphid::Command::Msg) => super::try_handle_ctap1(self, apdu, response)?, - Ok(ctaphid::Command::Deselect) => self.deselect(), + Ok(ctaphid::Command::Deselect) => apdu_app::App::::deselect(self), _ => { info!("Unsupported ins for fido app {:02x}", instruction); return Err(iso7816::Status::InstructionNotSupportedOrInvalid); From e9dfefd715c0634900f1a1fd530cc32c6077f683 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 8 Oct 2024 15:58:25 +0200 Subject: [PATCH 100/135] tests: Add credential management tests This patch introduces the authenticator module that provides a simple high-level interface for testing FIDO2 functionality. It uses the module to implement tests for credential management, namely for listing credentials and for the behavior if the credential limit is reached or the filesystem is full. --- tests/authenticator/mod.rs | 196 +++++++++++++++++++++++++++++++++++++ tests/basic.rs | 12 +-- tests/cred_mgmt.rs | 139 ++++++++++++++++++++++++++ tests/ctap1.rs | 37 +++---- tests/virt/mod.rs | 31 ++++-- tests/webauthn/mod.rs | 68 +++++++++++-- 6 files changed, 443 insertions(+), 40 deletions(-) create mode 100644 tests/authenticator/mod.rs create mode 100644 tests/cred_mgmt.rs diff --git a/tests/authenticator/mod.rs b/tests/authenticator/mod.rs new file mode 100644 index 0000000..e53f520 --- /dev/null +++ b/tests/authenticator/mod.rs @@ -0,0 +1,196 @@ +use sha2::{Digest as _, Sha256}; + +use super::{ + virt::{Ctap2, Ctap2Error}, + webauthn::{ + AttStmtFormat, ClientPin, CredentialData, CredentialManagement, CredentialManagementParams, + KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredParam, + PublicKey, Rp, SharedSecret, User, + }, +}; + +pub struct Authenticator<'a, P: PinState> { + ctap2: Ctap2<'a>, + key_agreement_key: KeyAgreementKey, + shared_secret: Option, + pin: P, +} + +impl<'a> Authenticator<'a, NoPin> { + pub fn new(ctap2: Ctap2<'a>) -> Self { + Self { + ctap2, + key_agreement_key: KeyAgreementKey::generate(), + shared_secret: None, + pin: NoPin, + } + } + + pub fn set_pin(mut self, pin: &[u8]) -> Authenticator<'a, Pin> { + let shared_secret = self.shared_secret(); + let mut padded_pin = [0; 64]; + padded_pin[..pin.len()].copy_from_slice(pin); + let pin_enc = shared_secret.encrypt(&padded_pin); + let pin_auth = shared_secret.authenticate(&pin_enc); + let mut request = ClientPin::new(2, 3); + request.key_agreement = Some(self.key_agreement_key.public_key()); + request.new_pin_enc = Some(pin_enc); + request.pin_auth = Some(pin_auth); + self.ctap2.exec(request).unwrap(); + Authenticator { + ctap2: self.ctap2, + key_agreement_key: self.key_agreement_key, + shared_secret: self.shared_secret, + pin: Pin(pin.into()), + } + } +} + +impl<'a, P: PinState> Authenticator<'a, P> { + fn shared_secret(&mut self) -> &SharedSecret { + self.shared_secret.get_or_insert_with(|| { + let reply = self.ctap2.exec(ClientPin::new(2, 2)).unwrap(); + let authenticator_key_agreement: PublicKey = reply.key_agreement.unwrap().into(); + self.key_agreement_key + .shared_secret(&authenticator_key_agreement) + }) + } +} + +impl<'a> Authenticator<'a, Pin> { + fn get_pin_token(&mut self, permissions: u8, rp_id: Option) -> PinToken { + let mut hasher = Sha256::new(); + hasher.update(&self.pin.0); + let pin_hash = hasher.finalize(); + let pin_hash_enc = self.shared_secret().encrypt(&pin_hash[..16]); + let mut request = ClientPin::new(2, 9); + request.key_agreement = Some(self.key_agreement_key.public_key()); + request.pin_hash_enc = Some(pin_hash_enc); + request.permissions = Some(permissions); + request.rp_id = rp_id; + let reply = self.ctap2.exec(request).unwrap(); + let encrypted_pin_token = reply.pin_token.as_ref().unwrap().as_bytes().unwrap(); + self.shared_secret().decrypt_pin_token(encrypted_pin_token) + } + + pub fn make_credential(&mut self, rp: Rp, user: User) -> Result { + let pin_token = self.get_pin_token(0x01, None); + // TODO: client data + let client_data_hash = b""; + let pin_auth = pin_token.authenticate(client_data_hash); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + request.options = Some(MakeCredentialOptions::default().rk(true)); + request.pin_auth = Some(pin_auth); + request.pin_protocol = Some(2); + let reply = self.ctap2.exec(request)?; + assert_eq!( + reply.auth_data.flags & 0b1, + 0b1, + "up flag not set in auth_data: 0b{:b}", + reply.auth_data.flags + ); + assert_eq!( + reply.auth_data.flags & 0b100, + 0b100, + "uv flag not set in auth_data: 0b{:b}", + reply.auth_data.flags + ); + let format = AttStmtFormat::Packed; + assert_eq!(reply.fmt, format.as_str()); + reply.att_stmt.unwrap().validate(format, &reply.auth_data); + Ok(reply.auth_data.credential.unwrap()) + } + + fn credential_management(&mut self, subcommand: u8) -> CredentialManagement { + let pin_token = self.get_pin_token(0x04, None); + let pin_auth = pin_token.authenticate(&[subcommand]); + CredentialManagement { + subcommand, + subcommand_params: None, + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + } + } + + pub fn credentials_metadata(&mut self) -> CredentialsMetadata { + let request = self.credential_management(0x01); + let reply = self.ctap2.exec(request).unwrap(); + CredentialsMetadata { + existing: reply.existing_resident_credentials_count.unwrap(), + remaining: reply + .max_possible_remaining_resident_credentials_count + .unwrap(), + } + } + + pub fn list_rps(&mut self) -> Vec { + let request = self.credential_management(0x02); + let reply = self.ctap2.exec(request).unwrap(); + // TODO: check RP ID hash + let total_rps = reply.total_rps.unwrap(); + let mut rps = Vec::with_capacity(total_rps); + rps.push(reply.rp.unwrap().into()); + + for _ in 1..total_rps { + let request = CredentialManagement::new(0x03); + let reply = self.ctap2.exec(request).unwrap(); + // TODO: check RP ID hash + rps.push(reply.rp.unwrap().into()); + } + + rps + } + + pub fn list_credentials(&mut self, rp_id: &str) -> Vec { + let rp_id_hash = rp_id_hash(rp_id); + let pin_token = self.get_pin_token(0x04, Some(rp_id.to_owned())); + let params = CredentialManagementParams { + rp_id_hash: rp_id_hash.to_vec(), + }; + let mut pin_auth_param = vec![0x04]; + pin_auth_param.extend_from_slice(¶ms.serialized()); + let pin_auth = pin_token.authenticate(&pin_auth_param); + let request = CredentialManagement { + subcommand: 0x04, + subcommand_params: Some(params), + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + }; + let reply = self.ctap2.exec(request).unwrap(); + // TODO: check other fields + let total_credentials = reply.total_credentials.unwrap(); + let mut credentials = Vec::with_capacity(total_credentials); + credentials.push(reply.user.unwrap().into()); + + for _ in 1..total_credentials { + let request = CredentialManagement::new(0x05); + let reply = self.ctap2.exec(request).unwrap(); + // TODO: check other fields + credentials.push(reply.user.unwrap().into()); + } + + credentials + } +} + +pub struct CredentialsMetadata { + pub existing: usize, + pub remaining: usize, +} + +pub trait PinState {} + +pub struct NoPin; + +impl PinState for NoPin {} + +pub struct Pin(Vec); + +impl PinState for Pin {} + +fn rp_id_hash(rp_id: &str) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(rp_id); + hasher.finalize().into() +} diff --git a/tests/basic.rs b/tests/basic.rs index 3a85f4b..95fad70 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,7 +1,7 @@ #![cfg(feature = "dispatch")] -mod virt; -mod webauthn; +pub mod virt; +pub mod webauthn; use std::collections::BTreeMap; @@ -437,8 +437,8 @@ impl TestListCredentials { let request = CredentialManagement { subcommand: 0x02, subcommand_params: None, - pin_protocol: 2, - pin_auth, + pin_protocol: Some(2), + pin_auth: Some(pin_auth), }; let reply = device.exec(request).unwrap(); let rp: BTreeMap = reply.rp.unwrap().deserialized().unwrap(); @@ -465,8 +465,8 @@ impl TestListCredentials { let request = CredentialManagement { subcommand: 0x04, subcommand_params: Some(params), - pin_protocol: 2, - pin_auth, + pin_protocol: Some(2), + pin_auth: Some(pin_auth), }; let reply = device.exec(request).unwrap(); let user: BTreeMap = reply.user.unwrap().deserialized().unwrap(); diff --git a/tests/cred_mgmt.rs b/tests/cred_mgmt.rs new file mode 100644 index 0000000..b87d730 --- /dev/null +++ b/tests/cred_mgmt.rs @@ -0,0 +1,139 @@ +#![cfg(feature = "dispatch")] + +pub mod authenticator; +pub mod virt; +pub mod webauthn; + +use std::collections::BTreeMap; + +use littlefs2::path::PathBuf; + +use authenticator::Authenticator; +use virt::{Ctap2Error, Options}; +use webauthn::{Rp, User}; + +#[test] +fn test_list_credentials() { + virt::run_ctap2(|device| { + let mut authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut credentials: BTreeMap<_, _> = (0..10) + .map(|i| { + // TODO: set other fields than id + let rp_id = format!("rp{i}"); + let user = b"john.doe"; + authenticator + .make_credential(Rp::new(rp_id.clone()), User::new(user)) + .unwrap(); + (rp_id, user) + }) + .collect(); + + let rps = authenticator.list_rps(); + assert_eq!(rps.len(), 10); + for rp in &rps { + assert_eq!(rp.name, None); + let expected = credentials.remove(&rp.id).unwrap(); + + let mut credentials = authenticator.list_credentials(&rp.id); + assert_eq!(credentials.len(), 1); + let actual = credentials.pop().unwrap(); + + assert_eq!(actual.id, expected); + assert_eq!(actual.name, None); + assert_eq!(actual.display_name, None); + } + assert!(credentials.is_empty()); + }) +} + +#[test] +fn test_max_credential_count() { + let options = Options { + max_resident_credential_count: Some(10), + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { + let mut authenticator = Authenticator::new(device).set_pin(b"123456"); + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, 0); + // TODO: check metadata.remaining -- currently not checking the config properly + + for i in 0..10 { + let rp = Rp::new(format!("rp{i}")); + let user = User::new(b"john.doe"); + authenticator.make_credential(rp, user).unwrap(); + + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, i + 1); + // TODO: check metadata.remaining + } + + let rps = authenticator.list_rps(); + assert_eq!(rps.len(), 10); + for rp in &rps { + let credentials = authenticator.list_credentials(&rp.id); + assert_eq!(credentials.len(), 1); + } + + let rp = Rp::new("rp11"); + let user = User::new(b"john.doe"); + let result = authenticator.make_credential(rp, user); + assert_eq!(result, Err(Ctap2Error(0x28))); + + let rps = authenticator.list_rps(); + assert_eq!(rps.len(), 10); + for rp in &rps { + let credentials = authenticator.list_credentials(&rp.id); + assert_eq!(credentials.len(), 1); + } + }) +} + +#[test] +fn test_filesystem_full() { + let mut options = Options { + max_resident_credential_count: Some(10), + ..Default::default() + }; + for i in 0..80 { + let path = PathBuf::from(format!("/test/{i}").as_str()); + options.files.push((path, vec![0; 512])); + } + // TODO: inspect filesystem after run and check remaining blocks + virt::run_ctap2_with_options(options, |device| { + let mut authenticator = Authenticator::new(device).set_pin(b"123456"); + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, 0); + // This number depends on filesystem layout details and may change if the filesystem + // layout or implementation are changed. + assert_eq!(metadata.remaining, 5); + let n = metadata.remaining; + + let mut i = 0; + loop { + let rp = Rp::new(format!("rp{i}")); + let user = User::new(b"john.doe"); + let result = authenticator.make_credential(rp, user); + + if result == Err(Ctap2Error(0x28)) { + break; + } + result.unwrap(); + + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, i + 1); + + i += 1; + } + + // We should be able to create at least 1 but not more than n credentials. + assert!(i > 0); + assert!(i < n); + // Our estimate should not be more than one credential off. + assert!(n - i <= 1); + + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, i); + assert_eq!(metadata.remaining, 0); + }) +} diff --git a/tests/ctap1.rs b/tests/ctap1.rs index 53d7144..e1f362d 100644 --- a/tests/ctap1.rs +++ b/tests/ctap1.rs @@ -1,9 +1,7 @@ #![cfg(feature = "dispatch")] -#[allow(unused)] -mod virt; -#[allow(unused)] -mod webauthn; +pub mod virt; +pub mod webauthn; use ctaphid::{Device, HidDevice}; use hex_literal::hex; @@ -15,6 +13,8 @@ use littlefs2::path; use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey}; use x509_parser::public_key::PublicKey; +use virt::Options; + #[test] fn test_version() { virt::run_ctaphid(|device| { @@ -93,19 +93,22 @@ fn test_authenticate_upgrade() { let state: &[u8] = &hex!("A5726B65795F656E6372797074696F6E5F6B65795010926EC1ACF475A6ED273BD951BC4A6E706B65795F7772617070696E675F6B65795061743976EDACA263BD30DE70ADFBAF0B781A636F6E73656375746976655F70696E5F6D69736D617463686573006870696E5F68617368F66974696D657374616D7001"); let key_encryption_key: &[u8] = &hex!("00020003A0BAA6066B22616147F242DEC9C4B450F6189A10EE036C36E697E647B2C1D3E1000000000000000000000000"); let key_wrapping_key: &[u8] = &hex!("000200037719CE721FB206F9788BB7E550777C03795ECFE0B211AB7D50C5C2CE21B43E8E010000000000000000000000"); - let files = &[ - (path!("fido/dat/persistent-state.cbor"), state), - ( - path!("fido/sec/10926ec1acf475a6ed273bd951bc4a6e"), - key_encryption_key, - ), - ( - path!("fido/sec/61743976edaca263bd30de70adfbaf0b"), - key_wrapping_key, - ), - ]; - - virt::run_ctaphid_with_files(files, |device| { + let options = Options { + files: vec![ + (path!("fido/dat/persistent-state.cbor").into(), state.into()), + ( + path!("fido/sec/10926ec1acf475a6ed273bd951bc4a6e").into(), + key_encryption_key.into(), + ), + ( + path!("fido/sec/61743976edaca263bd30de70adfbaf0b").into(), + key_wrapping_key.into(), + ), + ], + ..Default::default() + }; + + virt::run_ctaphid_with_options(options, |device| { let application = &hex!("f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); let keyhandle = hex!("A3005878B3F2499ACECB2C08F437DEF0F41929BD4DCFBCA7D43E893B18799BA61F6D84A36EAFCB87D9E833AEA1FE68BABD27A4B89C83C32EC25B092D915D9EA207ECA4BDE5A06E3CDCCFE0E93600AC28A6A8A61E4A1C6881C67E252F00425672427CFC59463B097364F45FD050F8E6BE1C6CD45C1F7D9B5732E334A8014C533D8BF37EEF0D8D7D16B6DF025055B1A6492F5607139EF420D47051A5F3"); let user_key = &hex!("04AE6B38AE33494A3A58A9FED8A1C5DA2683F510A69B9DE4D8849648485ECDCC21918E6124F6E0B71E7B3C5D92F08EC38D3161E236FF72743923141E97089AA2C4"); diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 128576c..baee99f 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -23,7 +23,7 @@ use ctaphid_dispatch::{ types::{Channel, Requester}, }; use fido_authenticator::{Authenticator, Config, Conforming}; -use littlefs2::{path, path::Path}; +use littlefs2::{path, path::PathBuf}; use rand::{ distributions::{Distribution, Uniform}, RngCore as _, @@ -51,9 +51,10 @@ where F: FnOnce(ctaphid::Device) -> T + Send, T: Send, { - run_ctaphid_with_files(&[], f) + run_ctaphid_with_options(Default::default(), f) } -pub fn run_ctaphid_with_files(files: &[(&Path, &[u8])], f: F) -> T + +pub fn run_ctaphid_with_options(options: Options, f: F) -> T where F: FnOnce(ctaphid::Device) -> T + Send, T: Send, @@ -61,9 +62,9 @@ where INIT_LOGGER.call_once(|| { env_logger::init(); }); - let mut files = Vec::from(files); - files.push((path!("fido/x5c/00"), ATTESTATION_CERT)); - files.push((path!("fido/sec/00"), ATTESTATION_KEY)); + let mut files = options.files; + files.push((path!("fido/x5c/00").into(), ATTESTATION_CERT.into())); + files.push((path!("fido/sec/00").into(), ATTESTATION_KEY.into())); with_client(&files, |client| { let mut authenticator = Authenticator::new( client, @@ -71,7 +72,7 @@ where Config { max_msg_size: 0, skip_up_timeout: None, - max_resident_credential_count: None, + max_resident_credential_count: options.max_resident_credential_count, large_blobs: None, nfc_transport: false, }, @@ -113,6 +114,20 @@ where run_ctaphid(|device| f(Ctap2(device))) } +pub fn run_ctap2_with_options(options: Options, f: F) -> T +where + F: FnOnce(Ctap2) -> T + Send, + T: Send, +{ + run_ctaphid_with_options(options, |device| f(Ctap2(device))) +} + +#[derive(Debug, Default)] +pub struct Options { + pub files: Vec<(PathBuf, Vec)>, + pub max_resident_credential_count: Option, +} + pub struct Ctap2<'a>(ctaphid::Device>); impl Ctap2<'_> { @@ -223,7 +238,7 @@ impl HidDevice for Device<'_> { } } -fn with_client(files: &[(&Path, &[u8])], f: F) -> T +fn with_client(files: &[(PathBuf, Vec)], f: F) -> T where F: FnOnce(Client) -> T, { diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 0698522..34025df 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -204,8 +204,8 @@ impl From for ClientPinReply { } pub struct Rp { - id: String, - name: Option, + pub id: String, + pub name: Option, } impl Rp { @@ -233,10 +233,22 @@ impl From for Value { } } +impl From for Rp { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + id: map.remove("id").unwrap().deserialized().unwrap(), + name: map + .remove("name") + .map(|value| value.deserialized().unwrap()), + } + } +} + pub struct User { - id: Vec, - name: Option, - display_name: Option, + pub id: Vec, + pub name: Option, + pub display_name: Option, } impl User { @@ -273,6 +285,21 @@ impl From for Value { } } +impl From for User { + fn from(value: Value) -> User { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + id: map.remove("id").unwrap().into_bytes().unwrap(), + name: map + .remove("name") + .map(|value| value.deserialized().unwrap()), + display_name: map + .remove("displayName") + .map(|value| value.deserialized().unwrap()), + } + } +} + pub struct PubKeyCredParam { ty: String, alg: i32, @@ -715,8 +742,19 @@ impl From for GetInfoReply { pub struct CredentialManagement { pub subcommand: u8, pub subcommand_params: Option, - pub pin_protocol: u8, - pub pin_auth: [u8; 32], + pub pin_protocol: Option, + pub pin_auth: Option<[u8; 32]>, +} + +impl CredentialManagement { + pub fn new(subcommand: u8) -> Self { + Self { + subcommand, + subcommand_params: None, + pin_protocol: None, + pin_auth: None, + } + } } impl From for Value { @@ -726,8 +764,12 @@ impl From for Value { if let Some(subcommand_params) = request.subcommand_params { map.push(2, subcommand_params); } - map.push(3, request.pin_protocol); - map.push(4, request.pin_auth.as_slice()); + if let Some(pin_protocol) = request.pin_protocol { + map.push(3, pin_protocol); + } + if let Some(pin_auth) = request.pin_auth { + map.push(4, pin_auth.as_slice()); + } map.into() } } @@ -760,6 +802,8 @@ impl From for Value { } pub struct CredentialManagementReply { + pub existing_resident_credentials_count: Option, + pub max_possible_remaining_resident_credentials_count: Option, pub rp: Option, pub rp_id_hash: Option, pub total_rps: Option, @@ -772,6 +816,12 @@ impl From for CredentialManagementReply { fn from(value: Value) -> Self { let mut map: BTreeMap = value.deserialized().unwrap(); Self { + existing_resident_credentials_count: map + .remove(&1) + .map(|value| value.deserialized().unwrap()), + max_possible_remaining_resident_credentials_count: map + .remove(&2) + .map(|value| value.deserialized().unwrap()), rp: map.remove(&3), rp_id_hash: map.remove(&4), total_rps: map.remove(&5).map(|value| value.deserialized().unwrap()), From 94cb2fb0e6bb6f49fee9c7889dd957340c90c00f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 24 Oct 2024 14:54:47 +0200 Subject: [PATCH 101/135] Check credential limit in get_creds_metadata We have two limits for the credential count: a fixed limit determined by the configuration and an estimated limit based on the remaining filesystem size. When creating a new credential, we check both. But previously we only returned the estimated limit from get_creds_metadata. This patch adds the fixed limit to get_creds_metadata. --- src/ctap2/credential_management.rs | 19 +++++++++++-------- tests/cred_mgmt.rs | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index fa9417f..849c09d 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -1,6 +1,6 @@ //! TODO: T -use core::convert::TryFrom; +use core::{cmp, convert::TryFrom}; use trussed::{ syscall, try_syscall, @@ -65,16 +65,19 @@ where info!("get metadata"); let mut response: Response = Default::default(); - let max_resident_credentials = self.estimate_remaining(); let credential_count = self.count_credentials(); + // We have a fixed limit determined by the configuration and an estimated limit determined + // by the available space on the filesystem. The effective limit is the lower of the two. + let max_remaining = self + .config + .max_resident_credential_count + .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE) + .saturating_sub(credential_count); + let estimate_remaining = self.estimate_remaining().unwrap_or(u32::MAX); + response.existing_resident_credentials_count = Some(credential_count); response.max_possible_remaining_residential_credentials_count = - Some(max_resident_credentials.unwrap_or_else(|| { - self.config - .max_resident_credential_count - .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE) - .saturating_sub(credential_count) - })); + Some(cmp::min(max_remaining, estimate_remaining)); response } diff --git a/tests/cred_mgmt.rs b/tests/cred_mgmt.rs index b87d730..f5983cf 100644 --- a/tests/cred_mgmt.rs +++ b/tests/cred_mgmt.rs @@ -56,7 +56,7 @@ fn test_max_credential_count() { let mut authenticator = Authenticator::new(device).set_pin(b"123456"); let metadata = authenticator.credentials_metadata(); assert_eq!(metadata.existing, 0); - // TODO: check metadata.remaining -- currently not checking the config properly + assert_eq!(metadata.remaining, 10); for i in 0..10 { let rp = Rp::new(format!("rp{i}")); @@ -65,7 +65,7 @@ fn test_max_credential_count() { let metadata = authenticator.credentials_metadata(); assert_eq!(metadata.existing, i + 1); - // TODO: check metadata.remaining + assert_eq!(metadata.remaining, 9 - i); } let rps = authenticator.list_rps(); From 28e0b059985b22a71f7d7cae093a184fa6d0e1d5 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 21 Oct 2024 21:58:43 +0200 Subject: [PATCH 102/135] Relax cbor-smol dependency We only have an explicit cbor-smol dependency to make sure that at least v0.4.1 is selected so that the persistent state is deserialized correctly. Trussed has a more specific version constraint so we can only enforce the mininum version here. --- Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5ec6166..16534ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,9 @@ apdu-app = { version = "0.1", optional = true } ctaphid-dispatch = { version = "0.1", optional = true } iso7816 = { version = "0.1.2", optional = true } -cbor-smol = { version = "0.4.1" } +# This dependency is used indirectly via Trussed. We only want to make sure that we use at least +# 0.4.1 so that the persistent state is deserialized correctly. +cbor-smol = { version = ">= 0.4.1" } [features] dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] From 5b6ae97b5f92962b545a1af1bf5b69fee66bca0a Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 14 Oct 2024 16:02:52 +0200 Subject: [PATCH 103/135] Update littlefs2 to v0.5.0 --- Cargo.toml | 12 +++++------ examples/usbip.rs | 32 +++++++++++++++++------------- fuzz/Cargo.toml | 8 +++----- src/ctap2.rs | 11 ++++++---- src/ctap2/credential_management.rs | 15 +++++++------- src/ctap2/large_blobs.rs | 7 ++++--- src/state.rs | 7 ++++--- tests/cred_mgmt.rs | 2 +- tests/ctap1.rs | 2 +- 9 files changed, 52 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 16534ba..54f3841 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ ctap-types = { version = "0.3.1", features = ["get-info-full", "large-blobs", "t cosey = "0.3" delog = "0.1.0" heapless = "0.7" +littlefs2-core = "0.1" serde = { version = "1.0", default-features = false } serde_bytes = { version = "0.11.14", default-features = false } serde-indexed = "0.1.0" @@ -63,7 +64,7 @@ env_logger = "0.11.0" hex-literal = "0.4.1" hmac = "0.12.1" interchange = "0.3.0" -littlefs2 = "0.4.0" +littlefs2 = "0.5.0" log = "0.4.21" p256 = { version = "0.13.2", features = ["ecdh"] } rand = "0.8.4" @@ -80,11 +81,10 @@ features = ["dispatch"] [patch.crates-io] ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" } -littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "046478b7a4f6e2315acf9112d98308379c2e3eee" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } -trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } +trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "fs-info-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } -trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "53eba84d2cd0bcacc3a7096d4b7a2490dcf6f069" } +trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.5" } usbd-ctaphid = { git = "https://github.com/trussed-dev/usbd-ctaphid.git", rev = "dcff9009c3cd1ef9e5b09f8f307aca998fc9a8c8" } diff --git a/examples/usbip.rs b/examples/usbip.rs index 7e2f20f..14e06fb 100644 --- a/examples/usbip.rs +++ b/examples/usbip.rs @@ -4,44 +4,48 @@ //! USB/IP runner for opcard. //! Run with cargo run --example usbip --features dispatch +use littlefs2_core::path; use trussed::{ backend::BackendId, + client::ClientBuilder, + service::Service, types::Location, - virt::{self, Ram}, - ClientImplementation, + virt::{Platform, Ram, StoreProvider}, }; use trussed_staging::virt::{BackendIds, Dispatcher}; -use trussed_usbip::ClientBuilder; +use trussed_usbip::{Client, Syscall}; const MANUFACTURER: &str = "Nitrokey"; const PRODUCT: &str = "Nitrokey 3"; const VID: u16 = 0x20a0; const PID: u16 = 0x42b2; -type VirtClient = ClientImplementation, Dispatcher>; +type VirtClient = Client; struct FidoApp { fido: fido_authenticator::Authenticator, } -impl trussed_usbip::Apps<'static, VirtClient, Dispatcher> for FidoApp { +impl trussed_usbip::Apps<'static, S, Dispatcher> for FidoApp { type Data = (); - fn new>(builder: &B, _data: ()) -> Self { + fn new(service: &mut Service, Dispatcher>, syscall: Syscall, _data: ()) -> Self { let large_blogs = Some(fido_authenticator::LargeBlobsConfig { location: Location::External, #[cfg(feature = "chunked")] max_size: 4096, }); + let client = ClientBuilder::new(path!("fido")) + .backends(&[ + BackendId::Core, + BackendId::Custom(BackendIds::StagingBackend), + ]) + .prepare(service) + .expect("failed to create client") + .build(syscall); FidoApp { fido: fido_authenticator::Authenticator::new( - builder.build( - "fido", - &[ - BackendId::Core, - BackendId::Custom(BackendIds::StagingBackend), - ], - ), + client, fido_authenticator::Conforming {}, fido_authenticator::Config { max_msg_size: usbd_ctaphid::constants::MESSAGE_SIZE, @@ -72,7 +76,7 @@ fn main() { vid: VID, pid: PID, }; - trussed_usbip::Builder::new(virt::Ram::default(), options) + trussed_usbip::Builder::new(Ram::default(), options) .dispatch(Dispatcher::default()) .build::() .exec(|_platform| {}); diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2d96bc6..71cce3e 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -23,10 +23,8 @@ doc = false bench = false [patch.crates-io] -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" } -littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "046478b7a4f6e2315acf9112d98308379c2e3eee" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } -cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "9a77dc9b528b08f531d76b44af2f5336c4ef17e0"} -trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" } +trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "fs-info-v0.1.0" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "53eba84d2cd0bcacc3a7096d4b7a2490dcf6f069" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 844b753..1a9b82b 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -11,6 +11,7 @@ use ctap_types::{ heapless_bytes::Bytes, sizes, ByteArray, Error, }; +use littlefs2_core::path; use sha2::{Digest as _, Sha256}; use trussed::{ @@ -36,6 +37,8 @@ pub mod pin; use pin::{PinProtocol, PinProtocolVersion, RpScope, SharedSecret}; +pub const RK_DIR: &Path = path!("rk"); + /// Implement `ctap2::Authenticator` for our Authenticator. impl Authenticator for crate::Authenticator { #[inline(never)] @@ -577,7 +580,7 @@ impl Authenticator for crate::Authenti syscall!(self.trussed.delete_all(Location::Internal)); syscall!(self .trussed - .remove_dir_all(Location::Internal, PathBuf::from("rk"),)); + .remove_dir_all(Location::Internal, RK_DIR.into())); // Delete large-blob array large_blobs::reset(&mut self.trussed); @@ -2068,8 +2071,8 @@ fn rp_rk_dir(rp_id_hash: &[u8; 32]) -> PathBuf { 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)); + let mut dir = PathBuf::from(RK_DIR); + dir.push(&PathBuf::try_from(&hex).unwrap()); dir } @@ -2079,7 +2082,7 @@ fn rk_path(rp_id_hash: &[u8; 32], credential_id_hash: &[u8; 32]) -> PathBuf { let mut hex = [0u8; 16]; format_hex(&credential_id_hash[..8], &mut hex); - path.push(&PathBuf::from(&hex)); + path.push(&PathBuf::try_from(&hex).unwrap()); path } diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 849c09d..99f84e2 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -14,6 +14,7 @@ use ctap_types::{ ByteArray, Error, }; +use super::RK_DIR; use crate::{ constants::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE, credential::FullCredential, @@ -83,7 +84,7 @@ where } pub fn count_credentials(&mut self) -> u32 { - let dir = PathBuf::from(b"rk"); + let dir = PathBuf::from(RK_DIR); let maybe_first_rp = syscall!(self .trussed @@ -130,7 +131,7 @@ where let mut response: Response = Default::default(); - let dir = PathBuf::from(b"rk"); + let dir = PathBuf::from(RK_DIR); let maybe_first_rp = syscall!(self.trussed.read_dir_first(Location::Internal, dir, None)).entry; @@ -206,11 +207,11 @@ where .clone() .ok_or(Error::NotAllowed)?; - let dir = PathBuf::from(b"rk"); + let dir = PathBuf::from(RK_DIR); let mut hex = [b'0'; 16]; super::format_hex(&last_rp_id_hash[..8], &mut hex); - let filename = PathBuf::from(&hex); + let filename = PathBuf::try_from(&hex).unwrap(); let mut maybe_next_rp = syscall!(self @@ -300,7 +301,7 @@ where let mut hex = [b'0'; 16]; super::format_hex(&rp_id_hash[..8], &mut hex); - let rp_dir = PathBuf::from(b"rk").join(&PathBuf::from(&hex)); + let rp_dir = PathBuf::from(RK_DIR).join(&PathBuf::try_from(&hex).unwrap()); let (num_rks, first_rk) = self.count_rp_rks(rp_dir); let first_rk = first_rk.ok_or(Error::NoCredentials)?; @@ -472,8 +473,8 @@ where let credential_id_hash = self.hash(credential.id); let mut hex = [b'0'; 16]; super::format_hex(&credential_id_hash[..8], &mut hex); - let dir = PathBuf::from(b"rk"); - let filename = PathBuf::from(&hex); + let dir = PathBuf::from(RK_DIR); + let filename = PathBuf::try_from(&hex).unwrap(); syscall!(self .trussed diff --git a/src/ctap2/large_blobs.rs b/src/ctap2/large_blobs.rs index 005695e..8f56410 100644 --- a/src/ctap2/large_blobs.rs +++ b/src/ctap2/large_blobs.rs @@ -1,8 +1,9 @@ use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error}; +use littlefs2_core::path; use trussed::{ config::MAX_MESSAGE_LENGTH, try_syscall, - types::{Bytes, Location, Message, PathBuf}, + types::{Bytes, Location, Message, Path, PathBuf}, }; #[cfg(not(feature = "chunked"))] @@ -17,8 +18,8 @@ const EMPTY_ARRAY: &[u8; MIN_SIZE] = &[ 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a, 0x6d, 0x3c, ]; -const FILENAME: &[u8] = b"large-blob-array"; -const FILENAME_TMP: &[u8] = b".large-blob-array"; +const FILENAME: &Path = path!("large-blob-array"); +const FILENAME_TMP: &Path = path!(".large-blob-array"); pub type Chunk = Bytes; diff --git a/src/state.rs b/src/state.rs index 6ca9fd7..eeab456 100644 --- a/src/state.rs +++ b/src/state.rs @@ -9,9 +9,10 @@ use ctap_types::{ Error, String, }; +use littlefs2_core::path; use trussed::{ client, syscall, try_syscall, - types::{KeyId, Location, Mechanism, PathBuf}, + types::{KeyId, Location, Mechanism, Path, PathBuf}, Client as TrussedClient, }; @@ -271,7 +272,7 @@ pub struct PersistentState { impl PersistentState { const RESET_RETRIES: u8 = 8; - const FILENAME: &'static [u8] = b"persistent-state.cbor"; + const FILENAME: &'static Path = path!("persistent-state.cbor"); pub fn load(trussed: &mut T) -> Result { // TODO: add "exists_file" method instead? @@ -484,7 +485,7 @@ impl RuntimeState { let credential_data = syscall!(trussed.read_file( Location::Internal, - PathBuf::from(cached_credential.path.as_str()), + PathBuf::try_from(cached_credential.path.as_str()).unwrap(), )) .data; diff --git a/tests/cred_mgmt.rs b/tests/cred_mgmt.rs index f5983cf..4b4e8a0 100644 --- a/tests/cred_mgmt.rs +++ b/tests/cred_mgmt.rs @@ -96,7 +96,7 @@ fn test_filesystem_full() { ..Default::default() }; for i in 0..80 { - let path = PathBuf::from(format!("/test/{i}").as_str()); + let path = PathBuf::try_from(format!("/test/{i}").as_str()).unwrap(); options.files.push((path, vec![0; 512])); } // TODO: inspect filesystem after run and check remaining blocks diff --git a/tests/ctap1.rs b/tests/ctap1.rs index e1f362d..e90efba 100644 --- a/tests/ctap1.rs +++ b/tests/ctap1.rs @@ -9,7 +9,7 @@ use iso7816::{ command::{class::Class, instruction::Instruction, CommandBuilder, ExpectedLen}, response::Status, }; -use littlefs2::path; +use littlefs2_core::path; use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey}; use x509_parser::public_key::PublicKey; From c145a451ef119581db78341bcca730047890ebce Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 24 Oct 2024 19:35:24 +0200 Subject: [PATCH 104/135] Remove cbor_serialize_message helper The cbor_serialize_message helper mixed re-exports of cbor-smol from trussed and ctap-types. This can be problematic if both select different versions. It could be fixed by keeping both in sync, but to avoid this problem entirely, we can also just use cbor_serialize_bytes from Trussed directly. --- src/ctap2/pin.rs | 6 +++--- src/lib.rs | 12 +----------- src/state.rs | 4 ++-- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index 61b896b..2fb893a 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -1,8 +1,8 @@ -use crate::{cbor_serialize_message, TrussedRequirements}; +use crate::TrussedRequirements; use cosey::EcdhEsHkdf256PublicKey; use ctap_types::{ctap2::client_pin::Permissions, Error, Result}; use trussed::{ - cbor_deserialize, + cbor_deserialize, cbor_serialize_bytes, client::{CryptoClient, HmacSha256, P256}, syscall, try_syscall, types::{ @@ -312,7 +312,7 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } fn shared_secret_impl(&mut self, peer_key: &EcdhEsHkdf256PublicKey) -> Option { - let serialized_peer_key = cbor_serialize_message(peer_key).ok()?; + let serialized_peer_key: Message = cbor_serialize_bytes(peer_key).ok()?; let peer_key = try_syscall!(self.trussed.deserialize_p256_key( &serialized_peer_key, KeySerialization::EcdhEsHkdf256, diff --git a/src/lib.rs b/src/lib.rs index 8f2cc86..ce9d949 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,11 +20,7 @@ generate_macros!(); use core::time::Duration; -use trussed::{ - client, syscall, - types::{Location, Message}, - Client as TrussedClient, -}; +use trussed::{client, syscall, types::Location, Client as TrussedClient}; use trussed_fs_info::{FsInfoClient, FsInfoReply}; use trussed_hkdf::HkdfClient; @@ -252,12 +248,6 @@ impl UserPresence for Conforming { } } -fn cbor_serialize_message( - object: &T, -) -> core::result::Result { - trussed::cbor_serialize_bytes(object) -} - impl Authenticator where UP: UserPresence, diff --git a/src/state.rs b/src/state.rs index eeab456..38c4095 100644 --- a/src/state.rs +++ b/src/state.rs @@ -11,7 +11,7 @@ use ctap_types::{ }; use littlefs2_core::path; use trussed::{ - client, syscall, try_syscall, + cbor_serialize_bytes, client, syscall, try_syscall, types::{KeyId, Location, Mechanism, Path, PathBuf}, Client as TrussedClient, }; @@ -299,7 +299,7 @@ impl PersistentState { } pub fn save(&self, trussed: &mut T) -> Result<()> { - let data = crate::cbor_serialize_message(self).unwrap(); + let data = cbor_serialize_bytes(self).unwrap(); syscall!(trussed.write_file( Location::Internal, From 86403fa9f2e7db9ee4fca56fb3f837d91d8c0a1a Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 2 Dec 2024 12:01:06 +0100 Subject: [PATCH 105/135] Add test case for credential ID stability This patch adds a test case that ensures that the calculated credential ID for a credential that was created using the old (unstripped) format is the same as the one generated originally. Otherwise, the platform or the RP could reject assertions because of a changed credential ID. --- Cargo.toml | 1 + src/credential.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 54f3841..1956f37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ littlefs2 = "0.5.0" log = "0.4.21" p256 = { version = "0.13.2", features = ["ecdh"] } rand = "0.8.4" +rand_chacha = "0.3" sha2 = "0.10" serde_test = "1.0.176" trussed = { version = "0.1", features = ["virt"] } diff --git a/src/credential.rs b/src/credential.rs index e297201..0279356 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -601,9 +601,17 @@ impl From<&FullCredential> for StrippedCredential { #[cfg(test)] mod test { use super::*; + use hex_literal::hex; + use littlefs2_core::path; + use rand::SeedableRng as _; + use rand_chacha::ChaCha8Rng; use trussed::{ client::{Chacha8Poly1305, Sha256}, + key::{Kind, Secrecy}, + store::keystore::{ClientKeystore, Keystore as _}, types::Location, + virt::{self, Ram}, + Platform as _, }; fn credential_data() -> CredentialData { @@ -630,6 +638,30 @@ mod test { } } + fn old_credential_data() -> CredentialData { + CredentialData { + rp: LocalPublicKeyCredentialRpEntity { + id: String::from("John Doe"), + name: None, + }, + user: LocalPublicKeyCredentialUserEntity { + 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, + use_short_id: None, + large_blob_key: None, + third_party_payment: None, + } + } + fn random_byte_array() -> ByteArray { use rand::{rngs::OsRng, RngCore}; let mut bytes = [0; N]; @@ -733,6 +765,85 @@ mod test { assert_eq!(credential_data, deserialized); } + #[test] + fn old_credential_id() { + // generated with v0.1.1-nitrokey.4 (NK3 firmware version v1.4.0) + const OLD_ID: &[u8] = &hex!("A300583A71AEF80C4DA56033D66EB3266E9ACB8D84923D13F89BCBCE9FF30D8CD77ED968A436CA3D39C49999EC0F69A289CB2A65A08ABF251DEB21BB4B56014C00000000000000000000000002504DF499ABDAE80F5615C870985B74A799"); + const SERIALIZED_DATA: &[u8] = &hex!( + "A700A1626964684A6F686E20446F6501A16269644301020302187B03F404260582014301020306F4" + ); + const SERIALIZED_CREDENTIAL: &[u8] = &hex!("A3000201A700A1626964684A6F686E20446F6501A16269644301020302187B03F404260582014301020306F4024C000000000000000000000000"); + + virt::with_platform(Ram::default(), |mut platform| { + let kek = [0; 44]; + let client_id = path!("fido"); + let kek = { + let rng = ChaCha8Rng::from_rng(platform.rng()).unwrap(); + let mut keystore = ClientKeystore::new(client_id.into(), rng, platform.store()); + keystore + .store_key( + Location::Internal, + Secrecy::Secret, + Kind::Symmetric32Nonce(12), + &kek, + ) + .unwrap() + }; + platform.run_client(client_id.as_str(), |mut client| { + let data = old_credential_data(); + let rp_id_hash = syscall!(client.hash_sha256(data.rp.id.as_ref())).hash; + let credential_id = CredentialId(Bytes::from_slice(OLD_ID).unwrap()); + let encrypted_serialized = + EncryptedSerializedCredential::try_from(credential_id).unwrap(); + let serialized = syscall!(client.decrypt_chacha8poly1305( + kek, + &encrypted_serialized.0.ciphertext, + &rp_id_hash, + &encrypted_serialized.0.nonce, + &encrypted_serialized.0.tag, + )) + .plaintext + .unwrap(); + + let full = FullCredential::deserialize(&serialized).unwrap(); + assert_eq!( + full, + FullCredential { + ctap: CtapVersion::Fido21Pre, + data, + nonce: [0; 12].into(), + } + ); + + let stripped_credential = full.strip(); + + let serialized_data: Bytes<1024> = + trussed::cbor_serialize_bytes(&stripped_credential.data).unwrap(); + assert_eq!( + delog::hexstr!(&serialized_data).to_string(), + delog::hexstr!(SERIALIZED_DATA).to_string() + ); + + let serialized_credential: Bytes<1024> = + trussed::cbor_serialize_bytes(&stripped_credential).unwrap(); + assert_eq!( + delog::hexstr!(&serialized_credential).to_string(), + delog::hexstr!(SERIALIZED_CREDENTIAL).to_string() + ); + + let credential = Credential::Full(full); + let id = credential + .id(&mut client, kek, rp_id_hash.as_ref().try_into().unwrap()) + .unwrap() + .0; + assert_eq!( + delog::hexstr!(&id).to_string(), + delog::hexstr!(OLD_ID).to_string() + ); + }); + }); + } + #[test] fn credential_ids() { trussed::virt::with_ram_client("fido", |mut client| { From 5c3aa0b8af762f697ea5648c9fe44b3f69dea5cc Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 2 Dec 2024 14:17:58 +0100 Subject: [PATCH 106/135] Keep old credential ID for existing credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In #59, we changed the format for serialized credentials to use shorter field names for the RP and user entities. This has an unintended side effect: For non-discoverable credentials that were generated with older crate versions, the stripped data embedded into the credential ID includes the RP and user. If we change their serialization format, we also change these credential IDs. We already supported deserializing both formats using a serde alias. This patch introduces helper enums that deserialize both formats using a custom Deserialize implementation and keep track of the used format. This format is then also used for serialization (using serde’s untagged mechanism that is not available for deserialization in no-std contexts). https://github.com/Nitrokey/fido-authenticator/pull/59 Fixes: https://github.com/Nitrokey/fido-authenticator/issues/111 --- src/credential.rs | 784 ++++++++++++++++++----------- src/ctap2.rs | 14 +- src/ctap2/credential_management.rs | 22 +- 3 files changed, 520 insertions(+), 300 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index 0279356..747269e 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -10,7 +10,10 @@ pub(crate) use ctap_types::{ // authenticator::{ctap1, ctap2, Error, Request, Response}, ctap2::credential_management::CredentialProtectionPolicy, sizes::*, - webauthn::{PublicKeyCredentialDescriptor, PublicKeyCredentialDescriptorRef}, + webauthn::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialDescriptorRef, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + }, Bytes, String, }; @@ -205,31 +208,245 @@ impl Credential { } } -/// Copy of [`ctap_types::webauthn::PublicKeyCredentialUserEntity`] but with shorter field names serialization -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +fn deserialize_bytes( + s: &[u8], +) -> core::result::Result, E> { + Bytes::from_slice(s).map_err(|_| E::invalid_length(s.len(), &"a fixed-size sequence of bytes")) +} + +fn deserialize_str( + s: &str, +) -> core::result::Result, E> { + Ok(s.into()) +} + +#[derive(Clone, Debug, PartialEq, serde::Serialize)] +#[serde(untagged)] +pub enum Rp { + Long(PublicKeyCredentialRpEntity), + Short(LocalPublicKeyCredentialRpEntity), +} + +impl Rp { + pub fn id(&self) -> &str { + match self { + Self::Long(rp) => &rp.id, + Self::Short(rp) => &rp.id, + } + } + + pub fn strip(&mut self) { + let name = match self { + Self::Long(rp) => &mut rp.name, + Self::Short(rp) => &mut rp.name, + }; + *name = None; + } +} + +impl<'de> serde::Deserialize<'de> for Rp { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + #[derive(serde::Deserialize)] + struct R<'a> { + i: Option<&'a str>, + id: Option<&'a str>, + n: Option<&'a str>, + name: Option<&'a str>, + } + + let r = R::deserialize(deserializer)?; + + if r.i.is_some() && r.id.is_some() { + return Err(D::Error::duplicate_field("i")); + } + + if let Some(i) = r.i { + // short format + if r.name.is_some() { + return Err(D::Error::unknown_field("name", &["i", "n"])); + } + + Ok(Self::Short(LocalPublicKeyCredentialRpEntity { + id: deserialize_str(i)?, + name: r.n.map(deserialize_str).transpose()?, + })) + } else if let Some(id) = r.id { + // long format + if r.n.is_some() { + return Err(D::Error::unknown_field("n", &["id", "name"])); + } + + Ok(Self::Long(PublicKeyCredentialRpEntity { + id: deserialize_str(id)?, + name: r.name.map(deserialize_str).transpose()?, + icon: None, + })) + } else { + // ID is missing + Err(D::Error::missing_field("i")) + } + } +} + +impl From for PublicKeyCredentialRpEntity { + fn from(rp: Rp) -> PublicKeyCredentialRpEntity { + match rp { + Rp::Short(rp) => rp.into(), + Rp::Long(rp) => rp, + } + } +} + +/// Copy of [`ctap_types::webauthn::PublicKeyCredentialRpEntity`] but with shorter field names serialization +#[derive(serde::Serialize, Debug, Clone, PartialEq, Eq)] pub struct LocalPublicKeyCredentialRpEntity { - #[serde(rename = "i", alias = "id")] + #[serde(rename = "i")] pub id: String<256>, // Compared to the ctap_types type, we can skip the truncate, // since we know we only even deal with the correct length - #[serde(skip_serializing_if = "Option::is_none", rename = "n", alias = "name")] + #[serde(skip_serializing_if = "Option::is_none", rename = "n")] pub name: Option>, // Icon is ignored } + +#[derive(Clone, Debug, PartialEq, serde::Serialize)] +#[serde(untagged)] +pub enum User { + Long(PublicKeyCredentialUserEntity), + Short(LocalPublicKeyCredentialUserEntity), +} + +impl User { + pub fn id(&self) -> &Bytes<64> { + match self { + Self::Long(user) => &user.id, + Self::Short(user) => &user.id, + } + } + + pub fn strip(&mut self) { + let (icon, name, display_name) = match self { + Self::Long(rp) => (&mut rp.icon, &mut rp.name, &mut rp.display_name), + Self::Short(rp) => (&mut rp.icon, &mut rp.name, &mut rp.display_name), + }; + *icon = None; + *name = None; + *display_name = None; + } + + pub fn set_name(&mut self, name: Option>) { + let field = match self { + Self::Long(rp) => &mut rp.name, + Self::Short(rp) => &mut rp.name, + }; + *field = name; + } + + pub fn set_display_name(&mut self, display_name: Option>) { + let field = match self { + Self::Long(rp) => &mut rp.display_name, + Self::Short(rp) => &mut rp.display_name, + }; + *field = display_name; + } +} + +impl<'de> serde::Deserialize<'de> for User { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + #[derive(serde::Deserialize)] + struct U<'a> { + i: Option<&'a [u8]>, + id: Option<&'a [u8]>, + #[serde(rename = "I")] + ii: Option<&'a str>, + icon: Option<&'a str>, + n: Option<&'a str>, + name: Option<&'a str>, + d: Option<&'a str>, + #[serde(rename = "displayName")] + display_name: Option<&'a str>, + } + + let u = U::deserialize(deserializer)?; + + if u.i.is_some() && u.id.is_some() { + return Err(D::Error::duplicate_field("i")); + } + + if let Some(i) = u.i { + // short format + let fields = &["i", "I", "n", "d"]; + if u.icon.is_some() { + return Err(D::Error::unknown_field("icon", fields)); + } + if u.name.is_some() { + return Err(D::Error::unknown_field("name", fields)); + } + if u.display_name.is_some() { + return Err(D::Error::unknown_field("display_name", fields)); + } + + Ok(Self::Short(LocalPublicKeyCredentialUserEntity { + id: deserialize_bytes(i)?, + icon: u.ii.map(deserialize_str).transpose()?, + name: u.n.map(deserialize_str).transpose()?, + display_name: u.d.map(deserialize_str).transpose()?, + })) + } else if let Some(id) = u.id { + // long format + let fields = &["id", "icon", "name", "display_name"]; + if u.ii.is_some() { + return Err(D::Error::unknown_field("ii", fields)); + } + if u.n.is_some() { + return Err(D::Error::unknown_field("n", fields)); + } + if u.d.is_some() { + return Err(D::Error::unknown_field("d", fields)); + } + + Ok(Self::Long(PublicKeyCredentialUserEntity { + id: deserialize_bytes(id)?, + icon: u.icon.map(deserialize_str).transpose()?, + name: u.name.map(deserialize_str).transpose()?, + display_name: u.display_name.map(deserialize_str).transpose()?, + })) + } else { + // ID is missing + Err(D::Error::missing_field("i")) + } + } +} + +impl From for PublicKeyCredentialUserEntity { + fn from(user: User) -> PublicKeyCredentialUserEntity { + match user { + User::Short(user) => user.into(), + User::Long(user) => user, + } + } +} + /// Copy of [`ctap_types::webauthn::PublicKeyCredentialUserEntity`] but with with shorter field names serialization -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(serde::Serialize, Debug, Clone, PartialEq, Eq)] pub struct LocalPublicKeyCredentialUserEntity { - #[serde(rename = "i", alias = "id")] + #[serde(rename = "i")] pub id: Bytes<64>, - #[serde(skip_serializing_if = "Option::is_none", rename = "I", alias = "icon")] + #[serde(skip_serializing_if = "Option::is_none", rename = "I")] pub icon: Option>, - #[serde(skip_serializing_if = "Option::is_none", rename = "n", alias = "name")] + #[serde(skip_serializing_if = "Option::is_none", rename = "n")] pub name: Option>, - #[serde( - skip_serializing_if = "Option::is_none", - rename = "d", - alias = "display_name" - )] + #[serde(skip_serializing_if = "Option::is_none", rename = "d")] pub display_name: Option>, } @@ -300,9 +517,9 @@ impl From )] pub struct CredentialData { // id, name, url - pub rp: LocalPublicKeyCredentialRpEntity, + pub rp: Rp, // id, icon, name, display_name - pub user: LocalPublicKeyCredentialUserEntity, + pub user: User, // can be just a counter, need to be able to determine "latest" pub creation_time: u32, @@ -436,8 +653,8 @@ impl FullCredential { ) -> Self { info!("credential for algorithm {}", algorithm); let data = CredentialData { - rp: rp.clone().into(), - user: user.clone().into(), + rp: Rp::Short(rp.clone().into()), + user: User::Short(user.clone().into()), creation_time: timestamp, use_counter: true, @@ -481,7 +698,7 @@ impl FullCredential { let rp_id_hash: [u8; 32] = if let Some(hash) = rp_id_hash { *hash } else { - syscall!(trussed.hash_sha256(self.rp.id.as_ref())) + syscall!(trussed.hash_sha256(self.rp.id().as_ref())) .hash .as_slice() .try_into() @@ -521,17 +738,8 @@ impl FullCredential { fn strip(&self) -> Self { info_now!(":: stripping ID"); let mut stripped = self.clone(); - let data = &mut stripped.data; - - data.rp.name = None; - - data.user.icon = None; - data.user.name = None; - data.user.display_name = None; - - // data.hmac_secret = None; - // data.cred_protect = None; - + stripped.data.rp.strip(); + stripped.data.user.strip(); stripped } } @@ -605,6 +813,7 @@ mod test { use littlefs2_core::path; use rand::SeedableRng as _; use rand_chacha::ChaCha8Rng; + use serde_test::{assert_de_tokens, assert_ser_tokens, Token}; use trussed::{ client::{Chacha8Poly1305, Sha256}, key::{Kind, Secrecy}, @@ -616,16 +825,16 @@ mod test { fn credential_data() -> CredentialData { CredentialData { - rp: LocalPublicKeyCredentialRpEntity { + rp: Rp::Short(LocalPublicKeyCredentialRpEntity { id: String::from("John Doe"), name: None, - }, - user: LocalPublicKeyCredentialUserEntity { + }), + user: User::Short(LocalPublicKeyCredentialUserEntity { id: Bytes::from_slice(&[1, 2, 3]).unwrap(), icon: None, name: None, display_name: None, - }, + }), creation_time: 123, use_counter: false, algorithm: -7, @@ -640,16 +849,17 @@ mod test { fn old_credential_data() -> CredentialData { CredentialData { - rp: LocalPublicKeyCredentialRpEntity { + rp: Rp::Long(PublicKeyCredentialRpEntity { id: String::from("John Doe"), name: None, - }, - user: LocalPublicKeyCredentialUserEntity { + icon: None, + }), + user: User::Short(LocalPublicKeyCredentialUserEntity { id: Bytes::from_slice(&[1, 2, 3]).unwrap(), icon: None, name: None, display_name: None, - }, + }), creation_time: 123, use_counter: false, algorithm: -7, @@ -726,16 +936,16 @@ mod test { fn random_credential_data() -> CredentialData { CredentialData { - rp: LocalPublicKeyCredentialRpEntity { + rp: Rp::Short(LocalPublicKeyCredentialRpEntity { id: random_string(), name: maybe_random_string(), - }, - user: LocalPublicKeyCredentialUserEntity { + }), + user: User::Short(LocalPublicKeyCredentialUserEntity { 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, @@ -791,7 +1001,7 @@ mod test { }; platform.run_client(client_id.as_str(), |mut client| { let data = old_credential_data(); - let rp_id_hash = syscall!(client.hash_sha256(data.rp.id.as_ref())).hash; + let rp_id_hash = syscall!(client.hash_sha256(data.rp.id().as_ref())).hash; let credential_id = CredentialId(Bytes::from_slice(OLD_ID).unwrap()); let encrypted_serialized = EncryptedSerializedCredential::try_from(credential_id).unwrap(); @@ -855,7 +1065,7 @@ mod test { data, nonce, }; - let rp_id_hash = syscall!(client.hash_sha256(full_credential.rp.id.as_ref())) + let rp_id_hash = syscall!(client.hash_sha256(full_credential.rp.id().as_ref())) .hash .as_slice() .try_into() @@ -920,265 +1130,268 @@ mod test { }); } - #[test] - fn local_derive_rp_name_none() { - use serde_test::{assert_de_tokens, assert_tokens, Token}; - let rp_id = LocalPublicKeyCredentialRpEntity { - id: "Testing rp id".into(), - name: None, + struct RpValues { + id: &'static str, + name: Option<&'static str>, + } + + impl RpValues { + fn test(&self) { + RpType::SHORT.test(&self.short(), self); + RpType::LONG.test(&self.long(), self); + } + + fn short(&self) -> Rp { + Rp::Short(LocalPublicKeyCredentialRpEntity { + id: self.id.into(), + name: self.name.map(From::from), + }) + } + + fn long(&self) -> Rp { + Rp::Long(PublicKeyCredentialRpEntity { + id: self.id.into(), + name: self.name.map(From::from), + icon: None, + }) + } + } + + struct RpType { + s: &'static str, + id: &'static str, + name: &'static str, + } + + impl RpType { + const SHORT: Self = Self { + s: "LocalPublicKeyCredentialRpEntity", + id: "i", + name: "n", }; - assert_tokens( - &rp_id, - &[ - Token::Struct { - name: "LocalPublicKeyCredentialRpEntity", - len: 1, - }, - Token::Str("i"), - Token::Str("Testing rp id"), - Token::StructEnd, - ], - ); - assert_de_tokens( - &rp_id, - &[ - Token::Map { len: Some(1) }, - Token::Str("id"), - Token::Str("Testing rp id"), - Token::MapEnd, - ], - ); + const LONG: Self = Self { + s: "PublicKeyCredentialRpEntity", + id: "id", + name: "name", + }; + + fn test(&self, item: &Rp, values: &RpValues) { + let mut len = 1; + if values.name.is_some() { + len += 1; + } + + let mut ser_tokens = vec![Token::Struct { name: self.s, len }]; + ser_tokens.push(Token::Str(self.id)); + ser_tokens.push(Token::Str(values.id)); + if let Some(name) = values.name { + ser_tokens.push(Token::Str(self.name)); + ser_tokens.push(Token::Some); + ser_tokens.push(Token::Str(name)); + } + ser_tokens.push(Token::StructEnd); + assert_ser_tokens(item, &ser_tokens); + + let mut de_tokens = vec![Token::Map { len: Some(len) }]; + de_tokens.push(Token::Str(self.id)); + de_tokens.push(Token::Some); + de_tokens.push(Token::BorrowedStr(values.id)); + if let Some(name) = values.name { + de_tokens.push(Token::Str(self.name)); + de_tokens.push(Token::Some); + de_tokens.push(Token::BorrowedStr(name)); + } + de_tokens.push(Token::MapEnd); + assert_de_tokens(item, &de_tokens); + } } #[test] - fn local_derive_rp_name_some() { - use serde_test::{assert_de_tokens, assert_tokens, Token}; - let rp_id = LocalPublicKeyCredentialRpEntity { - id: "Testing rp id".into(), - name: Some("Testing rp name".into()), + fn serde_rp_name_none() { + RpValues { + id: "Testing rp id", + name: None, + } + .test() + } + + #[test] + fn serde_rp_name_some() { + RpValues { + id: "Testing rp id", + name: Some("Testing rp name"), + } + .test() + } + + struct UserValues { + id: &'static [u8], + icon: Option<&'static str>, + name: Option<&'static str>, + display_name: Option<&'static str>, + } + + impl UserValues { + fn test(&self) { + UserType::SHORT.test(&self.short(), self); + UserType::LONG.test(&self.long(), self); + } + + fn short(&self) -> User { + User::Short(LocalPublicKeyCredentialUserEntity { + id: Bytes::from_slice(self.id).unwrap(), + icon: self.icon.map(From::from), + name: self.name.map(From::from), + display_name: self.display_name.map(From::from), + }) + } + + fn long(&self) -> User { + User::Long(PublicKeyCredentialUserEntity { + id: Bytes::from_slice(self.id).unwrap(), + icon: self.icon.map(From::from), + name: self.name.map(From::from), + display_name: self.display_name.map(From::from), + }) + } + } + + struct UserType { + s: &'static str, + id: &'static str, + icon: &'static str, + name: &'static str, + display_name: &'static str, + } + + impl UserType { + const SHORT: Self = Self { + s: "LocalPublicKeyCredentialUserEntity", + id: "i", + icon: "I", + name: "n", + display_name: "d", }; - assert_tokens( - &rp_id, - &[ - Token::Struct { - name: "LocalPublicKeyCredentialRpEntity", - len: 2, - }, - Token::Str("i"), - Token::Str("Testing rp id"), - Token::Str("n"), - Token::Some, - Token::Str("Testing rp name"), - Token::StructEnd, - ], - ); - assert_de_tokens( - &rp_id, - &[ - Token::Map { len: Some(2) }, - Token::Str("id"), - Token::Str("Testing rp id"), - Token::Str("name"), - Token::Some, - Token::Str("Testing rp name"), - Token::MapEnd, - ], - ); + const LONG: Self = Self { + s: "PublicKeyCredentialUserEntity", + id: "id", + icon: "icon", + name: "name", + display_name: "displayName", + }; + + fn test(&self, user: &User, values: &UserValues) { + let mut len = 1; + if values.icon.is_some() { + len += 1; + } + if values.name.is_some() { + len += 1; + } + if values.display_name.is_some() { + len += 1; + } + + let mut ser_tokens = vec![Token::Struct { name: self.s, len }]; + ser_tokens.push(Token::Str(self.id)); + ser_tokens.push(Token::Bytes(values.id)); + if let Some(icon) = values.icon { + ser_tokens.push(Token::Str(self.icon)); + ser_tokens.push(Token::Some); + ser_tokens.push(Token::Str(icon)); + } + if let Some(name) = values.name { + ser_tokens.push(Token::Str(self.name)); + ser_tokens.push(Token::Some); + ser_tokens.push(Token::Str(name)); + } + if let Some(display_name) = values.display_name { + ser_tokens.push(Token::Str(self.display_name)); + ser_tokens.push(Token::Some); + ser_tokens.push(Token::Str(display_name)); + } + ser_tokens.push(Token::StructEnd); + assert_ser_tokens(user, &ser_tokens); + + let mut de_tokens = vec![Token::Map { len: Some(len) }]; + de_tokens.push(Token::Str(self.id)); + de_tokens.push(Token::Some); + de_tokens.push(Token::BorrowedBytes(values.id)); + if let Some(icon) = values.icon { + de_tokens.push(Token::Str(self.icon)); + de_tokens.push(Token::Some); + de_tokens.push(Token::BorrowedStr(icon)); + } + if let Some(name) = values.name { + de_tokens.push(Token::Str(self.name)); + de_tokens.push(Token::Some); + de_tokens.push(Token::BorrowedStr(name)); + } + if let Some(display_name) = values.display_name { + de_tokens.push(Token::Str(self.display_name)); + de_tokens.push(Token::Some); + de_tokens.push(Token::BorrowedStr(display_name)); + } + de_tokens.push(Token::MapEnd); + assert_de_tokens(user, &de_tokens); + } } #[test] - fn local_derive_user() { - use serde_test::{assert_de_tokens, assert_tokens, Token}; - - let rp_id = LocalPublicKeyCredentialUserEntity { - id: Bytes::from_slice(b"Testing user id").unwrap(), - icon: Some("Testing user icon".into()), - name: Some("Testing user name".into()), - display_name: Some("Testing user display_name".into()), - }; - assert_tokens( - &rp_id, - &[ - Token::Struct { - name: "LocalPublicKeyCredentialUserEntity", - len: 4, - }, - Token::Str("i"), - Token::Bytes(b"Testing user id"), - Token::Str("I"), - Token::Some, - Token::Str("Testing user icon"), - Token::Str("n"), - Token::Some, - Token::Str("Testing user name"), - Token::Str("d"), - Token::Some, - Token::Str("Testing user display_name"), - Token::StructEnd, - ], - ); - assert_de_tokens( - &rp_id, - &[ - Token::Struct { - name: "LocalPublicKeyCredentialUserEntity", - len: 4, - }, - Token::Str("id"), - Token::Bytes(b"Testing user id"), - Token::Str("icon"), - Token::Some, - Token::Str("Testing user icon"), - Token::Str("name"), - Token::Some, - Token::Str("Testing user name"), - Token::Str("display_name"), - Token::Some, - Token::Str("Testing user display_name"), - Token::StructEnd, - ], - ); + fn serde_user_full() { + UserValues { + id: b"Testing user id", + icon: Some("Testing user icon"), + name: Some("Testing user name"), + display_name: Some("Testing user display_name"), + } + .test(); + } - let rp_id = LocalPublicKeyCredentialUserEntity { - id: Bytes::from_slice(b"Testing user id").unwrap(), + #[test] + fn serde_user_display_name() { + UserValues { + id: b"Testing user id", icon: None, name: None, - display_name: Some("Testing user display_name".into()), - }; - assert_tokens( - &rp_id, - &[ - Token::Struct { - name: "LocalPublicKeyCredentialUserEntity", - len: 2, - }, - Token::Str("i"), - Token::Bytes(b"Testing user id"), - Token::Str("d"), - Token::Some, - Token::Str("Testing user display_name"), - Token::StructEnd, - ], - ); - assert_de_tokens( - &rp_id, - &[ - Token::Struct { - name: "LocalPublicKeyCredentialUserEntity", - len: 2, - }, - Token::Str("id"), - Token::Bytes(b"Testing user id"), - Token::Str("display_name"), - Token::Some, - Token::Str("Testing user display_name"), - Token::StructEnd, - ], - ); + display_name: Some("Testing user display_name"), + } + .test(); + } - let rp_id = LocalPublicKeyCredentialUserEntity { - id: Bytes::from_slice(b"Testing user id").unwrap(), - icon: Some("Testing user icon".into()), + #[test] + fn serde_user_icon_display_name() { + UserValues { + id: b"Testing user id", + icon: Some("Testing user icon"), name: None, - display_name: Some("Testing user display_name".into()), - }; - assert_tokens( - &rp_id, - &[ - Token::Struct { - name: "LocalPublicKeyCredentialUserEntity", - len: 3, - }, - Token::Str("i"), - Token::Bytes(b"Testing user id"), - Token::Str("I"), - Token::Some, - Token::Str("Testing user icon"), - Token::Str("d"), - Token::Some, - Token::Str("Testing user display_name"), - Token::StructEnd, - ], - ); - assert_de_tokens( - &rp_id, - &[ - Token::Map { len: Some(3) }, - Token::Str("id"), - Token::Bytes(b"Testing user id"), - Token::Str("icon"), - Token::Some, - Token::Str("Testing user icon"), - Token::Str("display_name"), - Token::Some, - Token::Str("Testing user display_name"), - Token::MapEnd, - ], - ); + display_name: Some("Testing user display_name"), + } + .test(); + } - let rp_id = LocalPublicKeyCredentialUserEntity { - id: Bytes::from_slice(b"Testing user id").unwrap(), - icon: Some("Testing user icon".into()), + #[test] + fn serde_user_icon() { + UserValues { + id: b"Testing user id", + icon: Some("Testing user icon"), name: None, display_name: None, - }; - assert_tokens( - &rp_id, - &[ - Token::Struct { - name: "LocalPublicKeyCredentialUserEntity", - len: 2, - }, - Token::Str("i"), - Token::Bytes(b"Testing user id"), - Token::Str("I"), - Token::Some, - Token::Str("Testing user icon"), - Token::StructEnd, - ], - ); - assert_de_tokens( - &rp_id, - &[ - Token::Map { len: Some(2) }, - Token::Str("id"), - Token::Bytes(b"Testing user id"), - Token::Str("icon"), - Token::Some, - Token::Str("Testing user icon"), - Token::MapEnd, - ], - ); + } + .test(); + } - let rp_id = LocalPublicKeyCredentialUserEntity { - id: Bytes::from_slice(b"Testing user id").unwrap(), + #[test] + fn serde_user_empty() { + UserValues { + id: b"Testing user id", icon: None, name: None, display_name: None, - }; - assert_tokens( - &rp_id, - &[ - Token::Struct { - name: "LocalPublicKeyCredentialUserEntity", - len: 1, - }, - Token::Str("i"), - Token::Bytes(b"Testing user id"), - Token::StructEnd, - ], - ); - assert_de_tokens( - &rp_id, - &[ - Token::Map { len: Some(1) }, - Token::Str("id"), - Token::Bytes(b"Testing user id"), - Token::MapEnd, - ], - ); + } + .test(); } // Test credentials that were serialized before the migration to shorter field names for serialization @@ -1199,16 +1412,17 @@ mod test { assert_eq!( credential.data, CredentialData { - rp: LocalPublicKeyCredentialRpEntity { + rp: Rp::Long(PublicKeyCredentialRpEntity { id: "webauthn.io".into(), name: None, - }, - user: LocalPublicKeyCredentialUserEntity { + icon: None, + }), + user: User::Long(PublicKeyCredentialUserEntity { id: Bytes::from_slice(&hex!("6447567A644445")).unwrap(), icon: None, name: Some("test1".into()), display_name: None, - }, + }), creation_time: 0, use_counter: true, algorithm: -7, diff --git a/src/ctap2.rs b/src/ctap2.rs index 1a9b82b..58fa72b 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -9,7 +9,9 @@ use ctap_types::{ }, heapless::{String, Vec}, heapless_bytes::Bytes, - sizes, ByteArray, Error, + sizes, + webauthn::PublicKeyCredentialUserEntity, + ByteArray, Error, }; use littlefs2_core::path; use sha2::{Digest as _, Sha256}; @@ -388,7 +390,7 @@ 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) + self.delete_resident_key_by_user_id(&rp_id_hash, credential.user.id()) .ok(); let mut key_store_full = self.can_fit(serialized_credential.len()) == Some(false) @@ -1739,8 +1741,8 @@ impl crate::Authenticator { // User with empty IDs are ignored for compatibility if is_rk { if let Credential::Full(credential) = &credential { - if !credential.user.id.is_empty() { - let mut user = credential.user.clone(); + if !credential.user.id().is_empty() { + let mut user: PublicKeyCredentialUserEntity = credential.user.clone().into(); // 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. @@ -1749,7 +1751,7 @@ impl crate::Authenticator { user.name = None; user.display_name = None; } - response.user = Some(user.into()); + response.user = Some(user); } } @@ -1793,7 +1795,7 @@ impl crate::Authenticator { let credential_maybe = FullCredential::deserialize(&credential_data); if let Ok(old_credential) = credential_maybe { - if old_credential.user.id == user_id { + if old_credential.user.id() == user_id { match old_credential.key { credential::Key::ResidentKey(key) => { info_now!(":: deleting resident key"); diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 99f84e2..c1d99d6 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -174,7 +174,7 @@ where let rp = credential.data.rp; - response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id.as_ref()))); + response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id().as_ref()))); response.rp = Some(rp.into()); } } @@ -251,7 +251,7 @@ where let rp = credential.data.rp; - response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id.as_ref()))); + response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id().as_ref()))); response.rp = Some(rp.into()); // cache state for next call @@ -524,18 +524,22 @@ where // TODO: check remaining space, return KeyStoreFull // the updated user ID must match the stored user ID - if credential.user.id != user.id { + if credential.user.id() != &user.id { error!("updated user ID does not match original user ID"); return Err(Error::InvalidParameter); } // update user name and display name unless the values are not set or empty - credential.data.user.name = user.name.as_ref().filter(|s| !s.is_empty()).cloned(); - credential.data.user.display_name = user - .display_name - .as_ref() - .filter(|s| !s.is_empty()) - .cloned(); + credential + .data + .user + .set_name(user.name.as_ref().filter(|s| !s.is_empty()).cloned()); + credential.data.user.set_display_name( + user.display_name + .as_ref() + .filter(|s| !s.is_empty()) + .cloned(), + ); // write updated credential let serialized = credential.serialize()?; From 63a14793877f49e0bd6a99d28834a0013fdb9d64 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 2 Dec 2024 17:57:31 +0100 Subject: [PATCH 107/135] Remove LocalPublicKeyCredential*Entity structs To simplify the Rp and User types introduced in the last commit and to reduce code duplication, this patch removes the LocalPublicKeyCredential*Entity types. Instead the Rp and User types always store a PublicKeyCredential*Entity together with the serialization format. --- src/credential.rs | 651 +++++++++++++---------------- src/ctap2/credential_management.rs | 17 +- 2 files changed, 287 insertions(+), 381 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index 747269e..cf17cda 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -220,27 +220,55 @@ fn deserialize_str( Ok(s.into()) } -#[derive(Clone, Debug, PartialEq, serde::Serialize)] -#[serde(untagged)] -pub enum Rp { - Long(PublicKeyCredentialRpEntity), - Short(LocalPublicKeyCredentialRpEntity), +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SerializationFormat { + Short, + Long, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Rp { + format: SerializationFormat, + inner: PublicKeyCredentialRpEntity, } impl Rp { - pub fn id(&self) -> &str { - match self { - Self::Long(rp) => &rp.id, - Self::Short(rp) => &rp.id, + fn new(inner: PublicKeyCredentialRpEntity) -> Self { + Self { + format: SerializationFormat::Short, + inner, } } - pub fn strip(&mut self) { - let name = match self { - Self::Long(rp) => &mut rp.name, - Self::Short(rp) => &mut rp.name, - }; - *name = None; + fn raw(&self) -> RawRp<'_> { + let mut raw = RawRp::default(); + match self.format { + SerializationFormat::Short => { + raw.i = Some(&self.inner.id); + raw.n = self.inner.name.as_deref(); + } + SerializationFormat::Long => { + raw.id = Some(&self.inner.id); + raw.name = self.inner.name.as_deref(); + } + } + raw + } + + pub fn id(&self) -> &str { + &self.inner.id + } +} + +impl AsRef for Rp { + fn as_ref(&self) -> &PublicKeyCredentialRpEntity { + &self.inner + } +} + +impl AsMut for Rp { + fn as_mut(&mut self) -> &mut PublicKeyCredentialRpEntity { + &mut self.inner } } @@ -251,108 +279,109 @@ impl<'de> serde::Deserialize<'de> for Rp { { use serde::de::Error as _; - #[derive(serde::Deserialize)] - struct R<'a> { - i: Option<&'a str>, - id: Option<&'a str>, - n: Option<&'a str>, - name: Option<&'a str>, - } - - let r = R::deserialize(deserializer)?; + let r = RawRp::deserialize(deserializer)?; if r.i.is_some() && r.id.is_some() { return Err(D::Error::duplicate_field("i")); } - if let Some(i) = r.i { - // short format + let (format, id, name) = if let Some(i) = r.i { if r.name.is_some() { return Err(D::Error::unknown_field("name", &["i", "n"])); } - - Ok(Self::Short(LocalPublicKeyCredentialRpEntity { - id: deserialize_str(i)?, - name: r.n.map(deserialize_str).transpose()?, - })) + (SerializationFormat::Short, i, r.n) } else if let Some(id) = r.id { - // long format if r.n.is_some() { return Err(D::Error::unknown_field("n", &["id", "name"])); } - - Ok(Self::Long(PublicKeyCredentialRpEntity { - id: deserialize_str(id)?, - name: r.name.map(deserialize_str).transpose()?, - icon: None, - })) + (SerializationFormat::Long, id, r.name) } else { - // ID is missing - Err(D::Error::missing_field("i")) - } + return Err(D::Error::missing_field("i")); + }; + + let inner = PublicKeyCredentialRpEntity { + id: deserialize_str(id)?, + name: name.map(deserialize_str).transpose()?, + icon: None, + }; + Ok(Self { format, inner }) + } +} + +impl serde::Serialize for Rp { + fn serialize( + &self, + serializer: S, + ) -> core::result::Result { + self.raw().serialize(serializer) } } impl From for PublicKeyCredentialRpEntity { fn from(rp: Rp) -> PublicKeyCredentialRpEntity { - match rp { - Rp::Short(rp) => rp.into(), - Rp::Long(rp) => rp, - } + rp.inner } } -/// Copy of [`ctap_types::webauthn::PublicKeyCredentialRpEntity`] but with shorter field names serialization -#[derive(serde::Serialize, Debug, Clone, PartialEq, Eq)] -pub struct LocalPublicKeyCredentialRpEntity { - #[serde(rename = "i")] - pub id: String<256>, - // Compared to the ctap_types type, we can skip the truncate, - // since we know we only even deal with the correct length - #[serde(skip_serializing_if = "Option::is_none", rename = "n")] - pub name: Option>, - // Icon is ignored +#[derive(Default, serde::Deserialize, serde::Serialize)] +struct RawRp<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + i: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + n: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option<&'a str>, } -#[derive(Clone, Debug, PartialEq, serde::Serialize)] -#[serde(untagged)] -pub enum User { - Long(PublicKeyCredentialUserEntity), - Short(LocalPublicKeyCredentialUserEntity), +#[derive(Clone, Debug, PartialEq)] +pub struct User { + format: SerializationFormat, + inner: PublicKeyCredentialUserEntity, } impl User { - pub fn id(&self) -> &Bytes<64> { - match self { - Self::Long(user) => &user.id, - Self::Short(user) => &user.id, + fn new(inner: PublicKeyCredentialUserEntity) -> Self { + Self { + format: SerializationFormat::Short, + inner, } } - pub fn strip(&mut self) { - let (icon, name, display_name) = match self { - Self::Long(rp) => (&mut rp.icon, &mut rp.name, &mut rp.display_name), - Self::Short(rp) => (&mut rp.icon, &mut rp.name, &mut rp.display_name), - }; - *icon = None; - *name = None; - *display_name = None; + fn raw(&self) -> RawUser<'_> { + let mut raw = RawUser::default(); + match self.format { + SerializationFormat::Short => { + raw.i = Some(self.inner.id.as_slice().into()); + raw.ii = self.inner.icon.as_deref(); + raw.n = self.inner.name.as_deref(); + raw.d = self.inner.display_name.as_deref(); + } + SerializationFormat::Long => { + raw.id = Some(self.inner.id.as_slice().into()); + raw.icon = self.inner.icon.as_deref(); + raw.name = self.inner.name.as_deref(); + raw.display_name = self.inner.display_name.as_deref(); + } + } + raw } - pub fn set_name(&mut self, name: Option>) { - let field = match self { - Self::Long(rp) => &mut rp.name, - Self::Short(rp) => &mut rp.name, - }; - *field = name; + pub fn id(&self) -> &Bytes<64> { + &self.inner.id } +} - pub fn set_display_name(&mut self, display_name: Option>) { - let field = match self { - Self::Long(rp) => &mut rp.display_name, - Self::Short(rp) => &mut rp.display_name, - }; - *field = display_name; +impl AsRef for User { + fn as_ref(&self) -> &PublicKeyCredentialUserEntity { + &self.inner + } +} + +impl AsMut for User { + fn as_mut(&mut self) -> &mut PublicKeyCredentialUserEntity { + &mut self.inner } } @@ -363,27 +392,13 @@ impl<'de> serde::Deserialize<'de> for User { { use serde::de::Error as _; - #[derive(serde::Deserialize)] - struct U<'a> { - i: Option<&'a [u8]>, - id: Option<&'a [u8]>, - #[serde(rename = "I")] - ii: Option<&'a str>, - icon: Option<&'a str>, - n: Option<&'a str>, - name: Option<&'a str>, - d: Option<&'a str>, - #[serde(rename = "displayName")] - display_name: Option<&'a str>, - } - - let u = U::deserialize(deserializer)?; + let u = RawUser::deserialize(deserializer)?; if u.i.is_some() && u.id.is_some() { return Err(D::Error::duplicate_field("i")); } - if let Some(i) = u.i { + let (format, id, icon, name, display_name) = if let Some(i) = u.i { // short format let fields = &["i", "I", "n", "d"]; if u.icon.is_some() { @@ -396,12 +411,7 @@ impl<'de> serde::Deserialize<'de> for User { return Err(D::Error::unknown_field("display_name", fields)); } - Ok(Self::Short(LocalPublicKeyCredentialUserEntity { - id: deserialize_bytes(i)?, - icon: u.ii.map(deserialize_str).transpose()?, - name: u.n.map(deserialize_str).transpose()?, - display_name: u.d.map(deserialize_str).transpose()?, - })) + (SerializationFormat::Short, i, u.ii, u.n, u.d) } else if let Some(id) = u.id { // long format let fields = &["id", "icon", "name", "display_name"]; @@ -415,100 +425,61 @@ impl<'de> serde::Deserialize<'de> for User { return Err(D::Error::unknown_field("d", fields)); } - Ok(Self::Long(PublicKeyCredentialUserEntity { - id: deserialize_bytes(id)?, - icon: u.icon.map(deserialize_str).transpose()?, - name: u.name.map(deserialize_str).transpose()?, - display_name: u.display_name.map(deserialize_str).transpose()?, - })) + ( + SerializationFormat::Long, + id, + u.icon, + u.name, + u.display_name, + ) } else { // ID is missing - Err(D::Error::missing_field("i")) - } - } -} - -impl From for PublicKeyCredentialUserEntity { - fn from(user: User) -> PublicKeyCredentialUserEntity { - match user { - User::Short(user) => user.into(), - User::Long(user) => user, - } - } -} - -/// Copy of [`ctap_types::webauthn::PublicKeyCredentialUserEntity`] but with with shorter field names serialization -#[derive(serde::Serialize, Debug, Clone, PartialEq, Eq)] -pub struct LocalPublicKeyCredentialUserEntity { - #[serde(rename = "i")] - pub id: Bytes<64>, - #[serde(skip_serializing_if = "Option::is_none", rename = "I")] - pub icon: Option>, - #[serde(skip_serializing_if = "Option::is_none", rename = "n")] - pub name: Option>, - #[serde(skip_serializing_if = "Option::is_none", rename = "d")] - pub display_name: Option>, -} - -impl From for LocalPublicKeyCredentialRpEntity { - fn from(value: ctap_types::webauthn::PublicKeyCredentialRpEntity) -> Self { - let ctap_types::webauthn::PublicKeyCredentialRpEntity { id, name, icon } = value; - let _icon = icon; + return Err(D::Error::missing_field("i")); + }; - Self { id, name } + let inner = PublicKeyCredentialUserEntity { + id: deserialize_bytes(id)?, + icon: icon.map(deserialize_str).transpose()?, + name: name.map(deserialize_str).transpose()?, + display_name: display_name.map(deserialize_str).transpose()?, + }; + Ok(Self { format, inner }) } } -impl From for ctap_types::webauthn::PublicKeyCredentialRpEntity { - fn from(value: LocalPublicKeyCredentialRpEntity) -> Self { - let LocalPublicKeyCredentialRpEntity { id, name } = value; - - Self { - id, - name, - icon: None, - } +impl serde::Serialize for User { + fn serialize( + &self, + serializer: S, + ) -> core::result::Result { + self.raw().serialize(serializer) } } -impl From - for LocalPublicKeyCredentialUserEntity -{ - fn from(value: ctap_types::webauthn::PublicKeyCredentialUserEntity) -> Self { - let ctap_types::webauthn::PublicKeyCredentialUserEntity { - id, - icon, - name, - display_name, - } = value; - - Self { - id, - icon, - name, - display_name, - } +impl From for PublicKeyCredentialUserEntity { + fn from(user: User) -> PublicKeyCredentialUserEntity { + user.inner } } -impl From - for ctap_types::webauthn::PublicKeyCredentialUserEntity -{ - fn from(value: LocalPublicKeyCredentialUserEntity) -> Self { - let LocalPublicKeyCredentialUserEntity { - id, - icon, - name, - display_name, - } = value; - - Self { - id, - icon, - name, - display_name, - } - } +#[derive(Default, serde::Deserialize, serde::Serialize)] +struct RawUser<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + i: Option<&'a serde_bytes::Bytes>, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option<&'a serde_bytes::Bytes>, + #[serde(skip_serializing_if = "Option::is_none", rename = "I")] + ii: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + icon: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + n: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + d: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")] + display_name: Option<&'a str>, } /// The main content of a `FullCredential`. @@ -653,8 +624,8 @@ impl FullCredential { ) -> Self { info!("credential for algorithm {}", algorithm); let data = CredentialData { - rp: Rp::Short(rp.clone().into()), - user: User::Short(user.clone().into()), + rp: Rp::new(rp.clone()), + user: User::new(user.clone()), creation_time: timestamp, use_counter: true, @@ -738,8 +709,12 @@ impl FullCredential { fn strip(&self) -> Self { info_now!(":: stripping ID"); let mut stripped = self.clone(); - stripped.data.rp.strip(); - stripped.data.user.strip(); + let rp = stripped.data.rp.as_mut(); + rp.name = None; + let user = stripped.data.user.as_mut(); + user.icon = None; + user.name = None; + user.display_name = None; stripped } } @@ -813,7 +788,7 @@ mod test { use littlefs2_core::path; use rand::SeedableRng as _; use rand_chacha::ChaCha8Rng; - use serde_test::{assert_de_tokens, assert_ser_tokens, Token}; + use serde_test::{assert_de_tokens, assert_tokens, Token}; use trussed::{ client::{Chacha8Poly1305, Sha256}, key::{Kind, Secrecy}, @@ -825,11 +800,12 @@ mod test { fn credential_data() -> CredentialData { CredentialData { - rp: Rp::Short(LocalPublicKeyCredentialRpEntity { + rp: Rp::new(PublicKeyCredentialRpEntity { id: String::from("John Doe"), name: None, + icon: None, }), - user: User::Short(LocalPublicKeyCredentialUserEntity { + user: User::new(PublicKeyCredentialUserEntity { id: Bytes::from_slice(&[1, 2, 3]).unwrap(), icon: None, name: None, @@ -849,17 +825,23 @@ mod test { fn old_credential_data() -> CredentialData { CredentialData { - rp: Rp::Long(PublicKeyCredentialRpEntity { - id: String::from("John Doe"), - name: None, - icon: None, - }), - user: User::Short(LocalPublicKeyCredentialUserEntity { - id: Bytes::from_slice(&[1, 2, 3]).unwrap(), - icon: None, - name: None, - display_name: None, - }), + rp: Rp { + format: SerializationFormat::Long, + inner: PublicKeyCredentialRpEntity { + id: String::from("John Doe"), + name: None, + icon: None, + }, + }, + user: User { + format: SerializationFormat::Long, + inner: PublicKeyCredentialUserEntity { + id: Bytes::from_slice(&[1, 2, 3]).unwrap(), + icon: None, + name: None, + display_name: None, + }, + }, creation_time: 123, use_counter: false, algorithm: -7, @@ -936,11 +918,12 @@ mod test { fn random_credential_data() -> CredentialData { CredentialData { - rp: Rp::Short(LocalPublicKeyCredentialRpEntity { + rp: Rp::new(PublicKeyCredentialRpEntity { id: random_string(), name: maybe_random_string(), + icon: None, }), - user: User::Short(LocalPublicKeyCredentialUserEntity { + user: User::new(PublicKeyCredentialUserEntity { id: random_bytes(), //Bytes::from_slice(&[1,2,3]).unwrap(), icon: maybe_random_string(), name: maybe_random_string(), @@ -1130,6 +1113,30 @@ mod test { }); } + fn test_serde(item: &T, name: &'static str, fields: &[(&'static str, Token)]) + where + for<'a> T: core::fmt::Debug + PartialEq + serde::Deserialize<'a> + serde::Serialize, + { + let len = fields.len(); + + let mut struct_tokens = vec![Token::Struct { name, len }]; + let mut map_tokens = vec![Token::Map { len: Some(len) }]; + for (key, value) in fields { + struct_tokens.push(Token::Str(key)); + struct_tokens.push(Token::Some); + struct_tokens.push(*value); + + map_tokens.push(Token::Str(key)); + map_tokens.push(Token::Some); + map_tokens.push(*value); + } + struct_tokens.push(Token::StructEnd); + map_tokens.push(Token::MapEnd); + + assert_tokens(item, &struct_tokens); + assert_de_tokens(item, &map_tokens); + } + struct RpValues { id: &'static str, name: Option<&'static str>, @@ -1137,73 +1144,35 @@ mod test { impl RpValues { fn test(&self) { - RpType::SHORT.test(&self.short(), self); - RpType::LONG.test(&self.long(), self); + for format in [SerializationFormat::Short, SerializationFormat::Long] { + self.test_format(format); + } } - fn short(&self) -> Rp { - Rp::Short(LocalPublicKeyCredentialRpEntity { - id: self.id.into(), - name: self.name.map(From::from), - }) + fn test_format(&self, format: SerializationFormat) { + let (id_field, name_field) = match format { + SerializationFormat::Short => ("i", "n"), + SerializationFormat::Long => ("id", "name"), + }; + let rp = Rp { + format, + inner: self.inner(), + }; + + let mut fields = vec![(id_field, Token::BorrowedStr(self.id))]; + if let Some(name) = self.name { + fields.push((name_field, Token::BorrowedStr(name))); + } + + test_serde(&rp, "RawRp", &fields); } - fn long(&self) -> Rp { - Rp::Long(PublicKeyCredentialRpEntity { + fn inner(&self) -> PublicKeyCredentialRpEntity { + PublicKeyCredentialRpEntity { id: self.id.into(), name: self.name.map(From::from), icon: None, - }) - } - } - - struct RpType { - s: &'static str, - id: &'static str, - name: &'static str, - } - - impl RpType { - const SHORT: Self = Self { - s: "LocalPublicKeyCredentialRpEntity", - id: "i", - name: "n", - }; - - const LONG: Self = Self { - s: "PublicKeyCredentialRpEntity", - id: "id", - name: "name", - }; - - fn test(&self, item: &Rp, values: &RpValues) { - let mut len = 1; - if values.name.is_some() { - len += 1; } - - let mut ser_tokens = vec![Token::Struct { name: self.s, len }]; - ser_tokens.push(Token::Str(self.id)); - ser_tokens.push(Token::Str(values.id)); - if let Some(name) = values.name { - ser_tokens.push(Token::Str(self.name)); - ser_tokens.push(Token::Some); - ser_tokens.push(Token::Str(name)); - } - ser_tokens.push(Token::StructEnd); - assert_ser_tokens(item, &ser_tokens); - - let mut de_tokens = vec![Token::Map { len: Some(len) }]; - de_tokens.push(Token::Str(self.id)); - de_tokens.push(Token::Some); - de_tokens.push(Token::BorrowedStr(values.id)); - if let Some(name) = values.name { - de_tokens.push(Token::Str(self.name)); - de_tokens.push(Token::Some); - de_tokens.push(Token::BorrowedStr(name)); - } - de_tokens.push(Token::MapEnd); - assert_de_tokens(item, &de_tokens); } } @@ -1234,108 +1203,42 @@ mod test { impl UserValues { fn test(&self) { - UserType::SHORT.test(&self.short(), self); - UserType::LONG.test(&self.long(), self); + for format in [SerializationFormat::Short, SerializationFormat::Long] { + self.test_format(format); + } } - fn short(&self) -> User { - User::Short(LocalPublicKeyCredentialUserEntity { - id: Bytes::from_slice(self.id).unwrap(), - icon: self.icon.map(From::from), - name: self.name.map(From::from), - display_name: self.display_name.map(From::from), - }) + fn test_format(&self, format: SerializationFormat) { + let (id_field, icon_field, name_field, display_name_field) = match format { + SerializationFormat::Short => ("i", "I", "n", "d"), + SerializationFormat::Long => ("id", "icon", "name", "displayName"), + }; + let user = User { + format, + inner: self.inner(), + }; + + let mut fields = vec![(id_field, Token::BorrowedBytes(self.id))]; + if let Some(icon) = self.icon { + fields.push((icon_field, Token::BorrowedStr(icon))); + } + if let Some(name) = self.name { + fields.push((name_field, Token::BorrowedStr(name))); + } + if let Some(display_name) = self.display_name { + fields.push((display_name_field, Token::BorrowedStr(display_name))); + } + + test_serde(&user, "RawUser", &fields); } - fn long(&self) -> User { - User::Long(PublicKeyCredentialUserEntity { + fn inner(&self) -> PublicKeyCredentialUserEntity { + PublicKeyCredentialUserEntity { id: Bytes::from_slice(self.id).unwrap(), icon: self.icon.map(From::from), name: self.name.map(From::from), display_name: self.display_name.map(From::from), - }) - } - } - - struct UserType { - s: &'static str, - id: &'static str, - icon: &'static str, - name: &'static str, - display_name: &'static str, - } - - impl UserType { - const SHORT: Self = Self { - s: "LocalPublicKeyCredentialUserEntity", - id: "i", - icon: "I", - name: "n", - display_name: "d", - }; - - const LONG: Self = Self { - s: "PublicKeyCredentialUserEntity", - id: "id", - icon: "icon", - name: "name", - display_name: "displayName", - }; - - fn test(&self, user: &User, values: &UserValues) { - let mut len = 1; - if values.icon.is_some() { - len += 1; - } - if values.name.is_some() { - len += 1; } - if values.display_name.is_some() { - len += 1; - } - - let mut ser_tokens = vec![Token::Struct { name: self.s, len }]; - ser_tokens.push(Token::Str(self.id)); - ser_tokens.push(Token::Bytes(values.id)); - if let Some(icon) = values.icon { - ser_tokens.push(Token::Str(self.icon)); - ser_tokens.push(Token::Some); - ser_tokens.push(Token::Str(icon)); - } - if let Some(name) = values.name { - ser_tokens.push(Token::Str(self.name)); - ser_tokens.push(Token::Some); - ser_tokens.push(Token::Str(name)); - } - if let Some(display_name) = values.display_name { - ser_tokens.push(Token::Str(self.display_name)); - ser_tokens.push(Token::Some); - ser_tokens.push(Token::Str(display_name)); - } - ser_tokens.push(Token::StructEnd); - assert_ser_tokens(user, &ser_tokens); - - let mut de_tokens = vec![Token::Map { len: Some(len) }]; - de_tokens.push(Token::Str(self.id)); - de_tokens.push(Token::Some); - de_tokens.push(Token::BorrowedBytes(values.id)); - if let Some(icon) = values.icon { - de_tokens.push(Token::Str(self.icon)); - de_tokens.push(Token::Some); - de_tokens.push(Token::BorrowedStr(icon)); - } - if let Some(name) = values.name { - de_tokens.push(Token::Str(self.name)); - de_tokens.push(Token::Some); - de_tokens.push(Token::BorrowedStr(name)); - } - if let Some(display_name) = values.display_name { - de_tokens.push(Token::Str(self.display_name)); - de_tokens.push(Token::Some); - de_tokens.push(Token::BorrowedStr(display_name)); - } - de_tokens.push(Token::MapEnd); - assert_de_tokens(user, &de_tokens); } } @@ -1412,17 +1315,23 @@ mod test { assert_eq!( credential.data, CredentialData { - rp: Rp::Long(PublicKeyCredentialRpEntity { - id: "webauthn.io".into(), - name: None, - icon: None, - }), - user: User::Long(PublicKeyCredentialUserEntity { - id: Bytes::from_slice(&hex!("6447567A644445")).unwrap(), - icon: None, - name: Some("test1".into()), - display_name: None, - }), + rp: Rp { + format: SerializationFormat::Long, + inner: PublicKeyCredentialRpEntity { + id: "webauthn.io".into(), + name: None, + icon: None, + }, + }, + user: User { + format: SerializationFormat::Long, + inner: PublicKeyCredentialUserEntity { + id: Bytes::from_slice(&hex!("6447567A644445")).unwrap(), + icon: None, + name: Some("test1".into()), + display_name: None, + }, + }, creation_time: 0, use_counter: true, algorithm: -7, diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index c1d99d6..a1791e7 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -530,16 +530,13 @@ where } // update user name and display name unless the values are not set or empty - credential - .data - .user - .set_name(user.name.as_ref().filter(|s| !s.is_empty()).cloned()); - credential.data.user.set_display_name( - user.display_name - .as_ref() - .filter(|s| !s.is_empty()) - .cloned(), - ); + let credential_user = credential.data.user.as_mut(); + credential_user.name = user.name.as_ref().filter(|s| !s.is_empty()).cloned(); + credential_user.display_name = user + .display_name + .as_ref() + .filter(|s| !s.is_empty()) + .cloned(); // write updated credential let serialized = credential.serialize()?; From 3095b8cff01de37c68acbeace9f9bb5e05f855ec Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 12 Dec 2024 15:56:11 +0100 Subject: [PATCH 108/135] Replace EncryptedSerializedCredential with EncryptedData Previously, EncryptedSerializedCredential was a wrapper for trussed::api::reply::Encrypt. As we whave removed the serde trait implementations for the Trussed request and reply structs, this patch replaces the EncryptedSerializedCredential struct with the new trussed_core::types::EncryptedData helper type. See also: https://github.com/trussed-dev/trussed/issues/183 --- Cargo.toml | 4 +++- fuzz/Cargo.toml | 3 ++- src/credential.rs | 61 +++++++++++++++-------------------------------- 3 files changed, 24 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1956f37..7563b7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ serde_bytes = { version = "0.11.14", default-features = false } serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } trussed = "0.1" +trussed-core = "0.1" trussed-fs-info = "0.1.0" trussed-hkdf = { version = "0.2.0" } trussed-chunked = { version = "0.1.0", optional = true } @@ -82,7 +83,8 @@ features = ["dispatch"] [patch.crates-io] ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "046478b7a4f6e2315acf9112d98308379c2e3eee" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "eadd27cda0f457caae609e7fa972277e46695bd3" } +trussed-core = { git = "https://github.com/trussed-dev/trussed.git", rev = "eadd27cda0f457caae609e7fa972277e46695bd3" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "fs-info-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 71cce3e..8492886 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -23,7 +23,8 @@ doc = false bench = false [patch.crates-io] -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "046478b7a4f6e2315acf9112d98308379c2e3eee" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "eadd27cda0f457caae609e7fa972277e46695bd3" } +trussed-core = { git = "https://github.com/trussed-dev/trussed.git", rev = "eadd27cda0f457caae609e7fa972277e46695bd3" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "fs-info-v0.1.0" } diff --git a/src/credential.rs b/src/credential.rs index cf17cda..8cf2ee4 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -5,6 +5,7 @@ use core::cmp::Ordering; use serde::Serialize; use serde_bytes::ByteArray; use trussed::{client, syscall, try_syscall, types::KeyId}; +use trussed_core::types::EncryptedData; pub(crate) use ctap_types::{ // authenticator::{ctap1, ctap2, Error, Request, Response}, @@ -53,42 +54,24 @@ impl CredentialId { associated_data, Some(nonce) )); - EncryptedSerializedCredential(encrypted_serialized_credential) - .try_into() + trussed::cbor_serialize_bytes(&EncryptedData::from(encrypted_serialized_credential)) + .map(Self) .map_err(|_| Error::RequestTooLarge) } } -// TODO: how to determine necessary size? -// pub type SerializedCredential = Bytes<512>; -// pub type SerializedCredential = Bytes<256>; -pub(crate) type SerializedCredential = trussed::types::Message; +struct CredentialIdRef<'a>(&'a [u8]); -#[derive(Clone, Debug)] -struct EncryptedSerializedCredential(pub trussed::api::reply::Encrypt); - -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)?, - )) +impl CredentialIdRef<'_> { + fn deserialize(&self) -> Result { + ctap_types::serde::cbor_deserialize(self.0).map_err(|_| Error::InvalidCredential) } } -impl TryFrom for EncryptedSerializedCredential { - // tag = 16B - // nonce = 12B - type Error = Error; - - fn try_from(cid: CredentialId) -> Result { - let encrypted_serialized_credential = EncryptedSerializedCredential( - ctap_types::serde::cbor_deserialize(&cid.0).map_err(|_| Error::InvalidCredential)?, - ); - Ok(encrypted_serialized_credential) - } -} +// TODO: how to determine necessary size? +// pub type SerializedCredential = Bytes<512>; +// pub type SerializedCredential = Bytes<256>; +pub(crate) type SerializedCredential = trussed::types::Message; /// Credential keys can either be "discoverable" or not. /// @@ -131,11 +114,7 @@ impl Credential { rp_id_hash: &[u8; 32], id: &[u8], ) -> Result { - let mut cred: Bytes = Bytes::new(); - cred.extend_from_slice(id) - .map_err(|_| Error::InvalidCredential)?; - - let encrypted_serialized = EncryptedSerializedCredential::try_from(CredentialId(cred))?; + let encrypted_serialized = CredentialIdRef(id).deserialize()?; let kek = authnr .state @@ -144,10 +123,10 @@ impl Credential { let serialized = try_syscall!(authnr.trussed.decrypt_chacha8poly1305( kek, - &encrypted_serialized.0.ciphertext, + &encrypted_serialized.ciphertext, &rp_id_hash[..], - &encrypted_serialized.0.nonce, - &encrypted_serialized.0.tag, + &encrypted_serialized.nonce, + &encrypted_serialized.tag, )) .map_err(|_| Error::InvalidCredential)? .plaintext @@ -985,15 +964,13 @@ mod test { platform.run_client(client_id.as_str(), |mut client| { let data = old_credential_data(); let rp_id_hash = syscall!(client.hash_sha256(data.rp.id().as_ref())).hash; - let credential_id = CredentialId(Bytes::from_slice(OLD_ID).unwrap()); - let encrypted_serialized = - EncryptedSerializedCredential::try_from(credential_id).unwrap(); + let encrypted_serialized = CredentialIdRef(OLD_ID).deserialize().unwrap(); let serialized = syscall!(client.decrypt_chacha8poly1305( kek, - &encrypted_serialized.0.ciphertext, + &encrypted_serialized.ciphertext, &rp_id_hash, - &encrypted_serialized.0.nonce, - &encrypted_serialized.0.tag, + &encrypted_serialized.nonce, + &encrypted_serialized.tag, )) .plaintext .unwrap(); From 0ac88d4cc53e68089d5fca02d48e74019a6ba696 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 17 Dec 2024 10:46:14 +0100 Subject: [PATCH 109/135] Fix clippy lints --- tests/authenticator/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/authenticator/mod.rs b/tests/authenticator/mod.rs index e53f520..5b01e25 100644 --- a/tests/authenticator/mod.rs +++ b/tests/authenticator/mod.rs @@ -46,7 +46,7 @@ impl<'a> Authenticator<'a, NoPin> { } } -impl<'a, P: PinState> Authenticator<'a, P> { +impl Authenticator<'_, P> { fn shared_secret(&mut self) -> &SharedSecret { self.shared_secret.get_or_insert_with(|| { let reply = self.ctap2.exec(ClientPin::new(2, 2)).unwrap(); @@ -57,7 +57,7 @@ impl<'a, P: PinState> Authenticator<'a, P> { } } -impl<'a> Authenticator<'a, Pin> { +impl Authenticator<'_, Pin> { fn get_pin_token(&mut self, permissions: u8, rp_id: Option) -> PinToken { let mut hasher = Sha256::new(); hasher.update(&self.pin.0); From d61a9ac7d3b46a610e90494b27365778634bfbee Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 19 Dec 2024 22:16:03 +0100 Subject: [PATCH 110/135] Update dependencies for trussed-core and ctaphid-app --- CHANGELOG.md | 3 ++ Cargo.toml | 30 +++++++-------- examples/usbip.rs | 7 +++- fuzz/Cargo.toml | 12 +++--- src/constants.rs | 2 +- src/credential.rs | 50 +++++++++++++++--------- src/ctap1.rs | 4 +- src/ctap2.rs | 10 ++--- src/ctap2/credential_management.rs | 7 ++-- src/ctap2/large_blobs.rs | 21 +++++----- src/ctap2/pin.rs | 26 ++++++------- src/dispatch/apdu.rs | 11 +++--- src/dispatch/ctaphid.rs | 25 ++++++------ src/lib.rs | 51 ++++++++++++++---------- src/state.rs | 62 ++++++++++++++---------------- tests/virt/pipe.rs | 4 +- 16 files changed, 179 insertions(+), 146 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f11415..06c120f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support CTAP 2.1 - Serialize PIN hash with `serde-bytes` ([#52][]) - Reduce the space taken by credential serializaiton ([#59][]) +- Update dependencies: + - Replace `trussed` dependency with `trussed-core` + - Replace `ctaphid-dispatch` dependeny with `ctaphid-app` [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 diff --git a/Cargo.toml b/Cargo.toml index 7563b7f..ed9e34e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,32 +15,30 @@ name = "usbip" required-features = ["dispatch"] [dependencies] +cbor-smol = { version = "0.5" } ctap-types = { version = "0.3.1", features = ["get-info-full", "large-blobs", "third-party-payment"] } cosey = "0.3" delog = "0.1.0" heapless = "0.7" +heapless-bytes = "0.3" littlefs2-core = "0.1" serde = { version = "1.0", default-features = false } serde_bytes = { version = "0.11.14", default-features = false } serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } -trussed = "0.1" -trussed-core = "0.1" +trussed-core = { version = "0.1.0-rc.1", features = ["aes256-cbc", "certificate-client", "chacha8-poly1305", "crypto-client", "ed255", "filesystem-client", "hmac-sha256", "management-client", "p256", "sha256", "ui-client"] } trussed-fs-info = "0.1.0" trussed-hkdf = { version = "0.2.0" } trussed-chunked = { version = "0.1.0", optional = true } apdu-app = { version = "0.1", optional = true } -ctaphid-dispatch = { version = "0.1", optional = true } +ctaphid-app = { version = "0.1.0-rc.1", optional = true } iso7816 = { version = "0.1.2", optional = true } -# This dependency is used indirectly via Trussed. We only want to make sure that we use at least -# 0.4.1 so that the persistent state is deserialized correctly. -cbor-smol = { version = ">= 0.4.1" } - [features] dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] apdu-dispatch = ["dep:apdu-app"] +ctaphid-dispatch = ["dep:ctaphid-app"] disable-reset-time-window = [] # enables support for a large-blob array longer than 1024 bytes @@ -60,6 +58,7 @@ ciborium = { version = "0.2.2" } ciborium-io = "0.2.2" cipher = "0.4.4" ctaphid = { version = "0.3.1", default-features = false } +ctaphid-dispatch = "0.1" delog = { version = "0.1.6", features = ["std-log"] } env_logger = "0.11.0" hex-literal = "0.4.1" @@ -82,12 +81,11 @@ x509-parser = "0.16.0" features = ["dispatch"] [patch.crates-io] -ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "eadd27cda0f457caae609e7fa972277e46695bd3" } -trussed-core = { git = "https://github.com/trussed-dev/trussed.git", rev = "eadd27cda0f457caae609e7fa972277e46695bd3" } -trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } -trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "fs-info-v0.1.0" } -trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "53eba84d2cd0bcacc3a7096d4b7a2490dcf6f069" } -trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.5" } -usbd-ctaphid = { git = "https://github.com/trussed-dev/usbd-ctaphid.git", rev = "dcff9009c3cd1ef9e5b09f8f307aca998fc9a8c8" } +ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "5a2864c76fea6785d9ffe4c7b6596237d8378755" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" } +trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } +trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } +trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } +trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "60c58eb80685f72d80850b850800fc6a660fe50a" } +usbd-ctaphid = { git = "https://github.com/trussed-dev/usbd-ctaphid.git", rev = "96bf04e97cfc8077b4d9b6b97b3e34ec3ca3e2fc" } diff --git a/examples/usbip.rs b/examples/usbip.rs index 14e06fb..d25a2ac 100644 --- a/examples/usbip.rs +++ b/examples/usbip.rs @@ -60,7 +60,12 @@ impl trussed_usbip::Apps<'static, S, Dispatcher> for FidoApp { fn with_ctaphid_apps( &mut self, - f: impl FnOnce(&mut [&mut dyn ctaphid_dispatch::app::App<'static>]) -> T, + f: impl FnOnce( + &mut [&mut dyn ctaphid_dispatch::app::App< + 'static, + { ctaphid_dispatch::types::MESSAGE_SIZE }, + >], + ) -> T, ) -> T { f(&mut [&mut self.fido]) } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 8492886..a6f9ad6 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,6 +10,7 @@ cargo-fuzz = true [dependencies] ctap-types = { version = "0.3.0", features = ["arbitrary"] } libfuzzer-sys = "0.4" +trussed = { version = "0.1", features = ["clients-1", "certificate-client", "crypto-client", "filesystem-client", "management-client", "aes256-cbc", "ed255", "p256", "sha256"] } trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] } [dependencies.fido-authenticator] @@ -23,9 +24,8 @@ doc = false bench = false [patch.crates-io] -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "eadd27cda0f457caae609e7fa972277e46695bd3" } -trussed-core = { git = "https://github.com/trussed-dev/trussed.git", rev = "eadd27cda0f457caae609e7fa972277e46695bd3" } -trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } -trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } -trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "fs-info-v0.1.0" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "53eba84d2cd0bcacc3a7096d4b7a2490dcf6f069" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" } +trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } +trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } +trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } diff --git a/src/constants.rs b/src/constants.rs index dbab710..687fc1e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,6 +1,6 @@ //! Constants. -use trussed::types::{CertId, KeyId}; +use trussed_core::types::{CertId, KeyId}; pub const FIDO2_UP_TIMEOUT: u32 = 30_000; pub const U2F_UP_TIMEOUT: u32 = 250; diff --git a/src/credential.rs b/src/credential.rs index 8cf2ee4..f241685 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -4,8 +4,12 @@ use core::cmp::Ordering; use serde::Serialize; use serde_bytes::ByteArray; -use trussed::{client, syscall, try_syscall, types::KeyId}; -use trussed_core::types::EncryptedData; +use trussed_core::{ + mechanisms::{Chacha8Poly1305, Sha256}, + syscall, try_syscall, + types::{EncryptedData, KeyId}, + CryptoClient, FilesystemClient, +}; pub(crate) use ctap_types::{ // authenticator::{ctap1, ctap2, Error, Request, Response}, @@ -36,15 +40,16 @@ pub enum CtapVersion { pub struct CredentialId(pub Bytes); impl CredentialId { - fn new( + fn new( trussed: &mut T, credential: &C, key_encryption_key: KeyId, rp_id_hash: &[u8; 32], nonce: &[u8; 12], ) -> Result { - let serialized_credential: SerializedCredential = - trussed::cbor_serialize_bytes(credential).map_err(|_| Error::Other)?; + let mut serialized_credential = SerializedCredential::new(); + cbor_smol::cbor_serialize_to(credential, &mut serialized_credential) + .map_err(|_| Error::Other)?; let message = &serialized_credential; // info!("serialized cred = {:?}", message).ok(); let associated_data = &rp_id_hash[..]; @@ -54,9 +59,13 @@ impl CredentialId { associated_data, Some(nonce) )); - trussed::cbor_serialize_bytes(&EncryptedData::from(encrypted_serialized_credential)) - .map(Self) - .map_err(|_| Error::RequestTooLarge) + let mut credential_id = Bytes::new(); + cbor_smol::cbor_serialize_to( + &EncryptedData::from(encrypted_serialized_credential), + &mut credential_id, + ) + .map_err(|_| Error::RequestTooLarge)?; + Ok(Self(credential_id)) } } @@ -64,14 +73,14 @@ struct CredentialIdRef<'a>(&'a [u8]); impl CredentialIdRef<'_> { fn deserialize(&self) -> Result { - ctap_types::serde::cbor_deserialize(self.0).map_err(|_| Error::InvalidCredential) + cbor_smol::cbor_deserialize(self.0).map_err(|_| Error::InvalidCredential) } } // TODO: how to determine necessary size? // pub type SerializedCredential = Bytes<512>; // pub type SerializedCredential = Bytes<256>; -pub(crate) type SerializedCredential = trussed::types::Message; +pub(crate) type SerializedCredential = trussed_core::types::Message; /// Credential keys can either be "discoverable" or not. /// @@ -101,7 +110,7 @@ pub enum Credential { } impl Credential { - pub fn try_from( + pub fn try_from( authnr: &mut Authenticator, rp_id_hash: &[u8; 32], descriptor: &PublicKeyCredentialDescriptorRef, @@ -109,7 +118,10 @@ impl Credential { Self::try_from_bytes(authnr, rp_id_hash, descriptor.id) } - pub fn try_from_bytes( + pub fn try_from_bytes< + UP: UserPresence, + T: CryptoClient + Chacha8Poly1305 + FilesystemClient, + >( authnr: &mut Authenticator, rp_id_hash: &[u8; 32], id: &[u8], @@ -140,7 +152,7 @@ impl Credential { .map_err(|_| Error::InvalidCredential) } - pub fn id( + pub fn id( &self, trussed: &mut T, key_encryption_key: KeyId, @@ -639,7 +651,7 @@ impl FullCredential { // the ID will stay below 255 bytes. // // Existing keyhandles can still be decoded - pub fn id( + pub fn id( &self, trussed: &mut T, key_encryption_key: KeyId, @@ -669,11 +681,13 @@ impl FullCredential { } pub fn serialize(&self) -> Result { - trussed::cbor_serialize_bytes(self).map_err(|_| Error::Other) + let mut serialized_credential = SerializedCredential::new(); + cbor_smol::cbor_serialize_to(self, &mut serialized_credential).map_err(|_| Error::Other)?; + Ok(serialized_credential) } pub fn deserialize(bytes: &SerializedCredential) -> Result { - match ctap_types::serde::cbor_deserialize(bytes) { + match cbor_smol::cbor_deserialize(bytes) { Ok(s) => Ok(s), Err(_) => { info_now!("could not deserialize {:?}", bytes); @@ -724,7 +738,7 @@ pub struct StrippedCredential { impl StrippedCredential { fn deserialize(bytes: &SerializedCredential) -> Result { - match ctap_types::serde::cbor_deserialize(bytes) { + match cbor_smol::cbor_deserialize(bytes) { Ok(s) => Ok(s), Err(_) => { info_now!("could not deserialize {:?}", bytes); @@ -733,7 +747,7 @@ impl StrippedCredential { } } - pub fn id( + pub fn id( &self, trussed: &mut T, key_encryption_key: KeyId, diff --git a/src/ctap1.rs b/src/ctap1.rs index f6dd013..b472a4e 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -6,7 +6,7 @@ use ctap_types::{ }; use serde_bytes::ByteArray; -use trussed::{ +use trussed_core::{ syscall, types::{KeySerialization, Location, Mechanism, SignatureSerialization}, }; @@ -52,7 +52,7 @@ impl Authenticator for crate::Authenti .serialized_key; syscall!(self.trussed.delete(public_key)); let cose_key: cosey::EcdhEsHkdf256PublicKey = - trussed::cbor_deserialize(&serialized_cose_public_key).unwrap(); + cbor_smol::cbor_deserialize(&serialized_cose_public_key).unwrap(); let wrapping_key = self .state diff --git a/src/ctap2.rs b/src/ctap2.rs index 58fa72b..244cea7 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -13,14 +13,14 @@ use ctap_types::{ webauthn::PublicKeyCredentialUserEntity, ByteArray, Error, }; -use littlefs2_core::path; +use littlefs2_core::{path, Path, PathBuf}; use sha2::{Digest as _, Sha256}; -use trussed::{ +use trussed_core::{ syscall, try_syscall, types::{ - KeyId, KeySerialization, Location, Mechanism, MediumData, Message, Path, PathBuf, - SignatureSerialization, + KeyId, KeySerialization, Location, Mechanism, MediumData, Message, SignatureSerialization, + StorageAttributes, }, }; @@ -1517,7 +1517,7 @@ impl crate::Authenticator { Mechanism::HmacSha256, credential_key, Some(Bytes::from_slice(&[get_assertion_state.uv_performed as u8]).unwrap()), - trussed::types::StorageAttributes::new().set_persistence(Location::Volatile) + StorageAttributes::new().set_persistence(Location::Volatile) )) .key; diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index a1791e7..e29449e 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -2,9 +2,10 @@ use core::{cmp, convert::TryFrom}; -use trussed::{ +use littlefs2_core::{Path, PathBuf}; +use trussed_core::{ syscall, try_syscall, - types::{DirEntry, Location, Path, PathBuf}, + types::{DirEntry, Location}, }; use cosey::PublicKey; @@ -414,7 +415,7 @@ where }; use crate::SigningAlgorithm; - use trussed::types::{KeySerialization, Mechanism}; + use trussed_core::types::{KeySerialization, Mechanism}; let algorithm = SigningAlgorithm::try_from(credential.algorithm)?; let cose_public_key = match algorithm { diff --git a/src/ctap2/large_blobs.rs b/src/ctap2/large_blobs.rs index 8f56410..2df5f73 100644 --- a/src/ctap2/large_blobs.rs +++ b/src/ctap2/large_blobs.rs @@ -1,13 +1,16 @@ use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error}; -use littlefs2_core::path; -use trussed::{ +use littlefs2_core::{path, Path, PathBuf}; +use trussed_core::{ config::MAX_MESSAGE_LENGTH, try_syscall, - types::{Bytes, Location, Message, Path, PathBuf}, + types::{Bytes, Location, Message}, + FilesystemClient, }; +#[cfg(feature = "chunked")] +use trussed_chunked::ChunkedClient; #[cfg(not(feature = "chunked"))] -use trussed::{syscall, types::Mechanism}; +use trussed_core::{mechanisms::Sha256, syscall, types::Mechanism, CryptoClient}; use crate::{Result, TrussedRequirements}; @@ -51,7 +54,7 @@ impl Config { } } -pub fn size(client: &mut C, location: Location) -> Result { +pub fn size(client: &mut C, location: Location) -> Result { Ok( try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME))) .map_err(|_| Error::Other)? @@ -82,7 +85,7 @@ pub fn write_chunk( write_impl::<_, SelectedStorage>(client, state, location, data) } -pub fn reset(client: &mut C) { +pub fn reset(client: &mut C) { for location in [Location::Internal, Location::External, Location::Volatile] { try_syscall!(client.remove_file(location, PathBuf::from(FILENAME))).ok(); } @@ -161,7 +164,7 @@ struct SimpleStorage { } #[cfg(not(feature = "chunked"))] -impl Storage for SimpleStorage { +impl Storage for SimpleStorage { fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result { let result = try_syscall!(client.read_file(location, PathBuf::from(FILENAME))); let data = if let Ok(reply) = &result { @@ -248,7 +251,7 @@ struct ChunkedStorage { } #[cfg(feature = "chunked")] -impl Storage for ChunkedStorage { +impl Storage for ChunkedStorage { fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result { debug!("ChunkedStorage::read: offset = {offset}, length = {length}"); let mut chunk = Chunk::new(); @@ -307,7 +310,7 @@ impl Storage for ChunkedStorage { fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result { debug!("ChunkedStorage::extend_buffer: |data| = {}", data.len()); let mut n = 0; - for chunk in data.chunks(trussed::config::MAX_MESSAGE_LENGTH) { + for chunk in data.chunks(trussed_core::config::MAX_MESSAGE_LENGTH) { trace!("Writing {} bytes", chunk.len()); let path = PathBuf::from(FILENAME_TMP); let mut message = Message::new(); diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index 2fb893a..a06f223 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -1,16 +1,15 @@ -use crate::TrussedRequirements; use cosey::EcdhEsHkdf256PublicKey; use ctap_types::{ctap2::client_pin::Permissions, Error, Result}; -use trussed::{ - cbor_deserialize, cbor_serialize_bytes, - client::{CryptoClient, HmacSha256, P256}, +use heapless::String; +use trussed_core::{ + mechanisms::{HmacSha256, P256}, syscall, try_syscall, types::{ Bytes, KeyId, KeySerialization, Location, Mechanism, Message, ShortData, StorageAttributes, - String, }, + CryptoClient, }; -use trussed_hkdf::{KeyOrData, OkmId}; +use trussed_hkdf::{HkdfClient, KeyOrData, OkmId}; // PIN protocol 1 supports 16 or 32 bytes, PIN protocol 2 requires 32 bytes. const PIN_TOKEN_LENGTH: usize = 32; @@ -153,7 +152,7 @@ pub struct PinProtocolState { impl PinProtocolState { // in spec: initialize(...) - pub fn new(trussed: &mut T) -> Self { + pub fn new(trussed: &mut T) -> Self { Self { key_agreement_key: generate_key_agreement_key(trussed), shared_secret: None, @@ -162,7 +161,7 @@ impl PinProtocolState { } } - pub fn reset(self, trussed: &mut T) { + pub fn reset(self, trussed: &mut T) { if let Some(token) = self.pin_token_v1 { token.delete(trussed); } @@ -177,13 +176,13 @@ impl PinProtocolState { } #[derive(Debug)] -pub struct PinProtocol<'a, T: TrussedRequirements> { +pub struct PinProtocol<'a, T> { trussed: &'a mut T, state: &'a mut PinProtocolState, version: PinProtocolVersion, } -impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { +impl<'a, T: CryptoClient + HkdfClient + HmacSha256 + P256> PinProtocol<'a, T> { pub fn new( trussed: &'a mut T, state: &'a mut PinProtocolState, @@ -259,7 +258,7 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { KeySerialization::EcdhEsHkdf256 )) .serialized_key; - let cose_key = cbor_deserialize(&serialized_cose_key).unwrap(); + let cose_key = cbor_smol::cbor_deserialize(&serialized_cose_key).unwrap(); syscall!(self.trussed.delete(public_key)); cose_key } @@ -312,7 +311,8 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } fn shared_secret_impl(&mut self, peer_key: &EcdhEsHkdf256PublicKey) -> Option { - let serialized_peer_key: Message = cbor_serialize_bytes(peer_key).ok()?; + let mut serialized_peer_key = Message::new(); + cbor_smol::cbor_serialize_to(peer_key, &mut serialized_peer_key).ok()?; let peer_key = try_syscall!(self.trussed.deserialize_p256_key( &serialized_peer_key, KeySerialization::EcdhEsHkdf256, @@ -365,7 +365,7 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { // In the spec, the keys are concatenated and the relevant part is selected during the key // operations. For simplicity, we store two separate keys instead. fn kdf_v2(&mut self, input: KeyId) -> Option { - fn hkdf(trussed: &mut T, okm: OkmId, info: &[u8]) -> Option { + fn hkdf(trussed: &mut T, okm: OkmId, info: &[u8]) -> Option { let info = Message::from_slice(info).ok()?; try_syscall!(trussed.hkdf_expand(okm, info, 32, Location::Volatile)) .ok() diff --git a/src/dispatch/apdu.rs b/src/dispatch/apdu.rs index c92dc68..f572766 100644 --- a/src/dispatch/apdu.rs +++ b/src/dispatch/apdu.rs @@ -1,6 +1,5 @@ use apdu_app::Interface; use ctap_types::{serde::error::Error as SerdeError, Error}; -use ctaphid_dispatch::app as ctaphid; use iso7816::{command::CommandView, Data, Status}; use crate::{Authenticator, TrussedRequirements, UserPresence}; @@ -72,11 +71,13 @@ where 0x00..=0x02 => super::try_handle_ctap1(self, apdu, response)?, //self.call_authenticator_u2f(apdu, response), _ => { - match ctaphid::Command::try_from(instruction) { + match ctaphid_app::Command::try_from(instruction) { // 0x10 - Ok(ctaphid::Command::Cbor) => super::handle_ctap2(self, apdu.data(), response), - Ok(ctaphid::Command::Msg) => super::try_handle_ctap1(self, apdu, response)?, - Ok(ctaphid::Command::Deselect) => apdu_app::App::::deselect(self), + Ok(ctaphid_app::Command::Cbor) => { + super::handle_ctap2(self, apdu.data(), response) + } + Ok(ctaphid_app::Command::Msg) => super::try_handle_ctap1(self, apdu, response)?, + Ok(ctaphid_app::Command::Deselect) => apdu_app::App::::deselect(self), _ => { 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 index adb931f..34e9cc0 100644 --- a/src/dispatch/ctaphid.rs +++ b/src/dispatch/ctaphid.rs @@ -1,26 +1,27 @@ -use ctaphid_dispatch::app as ctaphid; +use ctaphid_app::{App, Command, Error}; +use heapless_bytes::Bytes; +use trussed_core::InterruptFlag; #[allow(unused_imports)] use crate::msp; use crate::{Authenticator, TrussedRequirements, UserPresence}; -use trussed::interrupt::InterruptFlag; -impl ctaphid::App<'static> for Authenticator +impl App<'static, N> for Authenticator where UP: UserPresence, T: TrussedRequirements, { - fn commands(&self) -> &'static [ctaphid::Command] { - &[ctaphid::Command::Cbor, ctaphid::Command::Msg] + fn commands(&self) -> &'static [Command] { + &[Command::Cbor, Command::Msg] } #[inline(never)] fn call( &mut self, - command: ctaphid::Command, - request: &ctaphid::Message, - response: &mut ctaphid::Message, - ) -> ctaphid::AppResult { + command: Command, + request: &[u8], + response: &mut Bytes, + ) -> Result<(), Error> { debug_now!( "ctaphid-dispatch: remaining stack: {} bytes", msp() - 0x2000_0000 @@ -28,14 +29,14 @@ where if request.is_empty() { debug_now!("invalid request length in ctaphid.call"); - return Err(ctaphid::Error::InvalidLength); + return Err(Error::InvalidLength); } // info_now!("request: "); // blocking::dump_hex(request, request.len()); match command { - ctaphid::Command::Cbor => super::handle_ctap2(self, request, response), - ctaphid::Command::Msg => super::handle_ctap1_from_hid(self, request, response), + Command::Cbor => super::handle_ctap2(self, request, response), + Command::Msg => super::handle_ctap1_from_hid(self, request, response), _ => { debug_now!("ctaphid trying to dispatch {:?}", command); } diff --git a/src/lib.rs b/src/lib.rs index ce9d949..1d2a000 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,10 @@ generate_macros!(); use core::time::Duration; -use trussed::{client, syscall, types::Location, Client as TrussedClient}; +use trussed_core::{ + mechanisms, syscall, types::Location, CertificateClient, CryptoClient, FilesystemClient, + ManagementClient, UiClient, +}; use trussed_fs_info::{FsInfoClient, FsInfoReply}; use trussed_hkdf::HkdfClient; @@ -51,13 +54,17 @@ pub type Result = core::result::Result; /// - Some Trussed extensions might be required depending on the activated features, see /// [`ExtensionRequirements`][]. pub trait TrussedRequirements: - client::Client - + client::P256 - + client::Chacha8Poly1305 - + client::Aes256Cbc - + client::Sha256 - + client::HmacSha256 - + client::Ed255 // + client::Totp + CertificateClient + + CryptoClient + + FilesystemClient + + ManagementClient + + UiClient + + mechanisms::P256 + + mechanisms::Chacha8Poly1305 + + mechanisms::Aes256Cbc + + mechanisms::Sha256 + + mechanisms::HmacSha256 + + mechanisms::Ed255 + FsInfoClient + HkdfClient + ExtensionRequirements @@ -65,13 +72,17 @@ pub trait TrussedRequirements: } impl TrussedRequirements for T where - T: client::Client - + client::P256 - + client::Chacha8Poly1305 - + client::Aes256Cbc - + client::Sha256 - + client::HmacSha256 - + client::Ed255 // + client::Totp + T: CertificateClient + + CryptoClient + + FilesystemClient + + ManagementClient + + UiClient + + mechanisms::P256 + + mechanisms::Chacha8Poly1305 + + mechanisms::Aes256Cbc + + mechanisms::Sha256 + + mechanisms::HmacSha256 + + mechanisms::Ed255 + FsInfoClient + HkdfClient + ExtensionRequirements @@ -204,7 +215,7 @@ impl core::convert::TryFrom for SigningAlgorithm { /// Method to check for user presence. pub trait UserPresence: Copy { - fn user_present( + fn user_present( self, trussed: &mut T, timeout_milliseconds: u32, @@ -220,7 +231,7 @@ 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(()) } } @@ -234,15 +245,15 @@ pub type NonSilentAuthenticator = Conforming; pub struct Conforming {} impl UserPresence for Conforming { - fn user_present( + 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, - trussed::types::consent::Error::Interrupted => Error::KeepaliveCancel, + trussed_core::types::consent::Error::TimedOut => Error::UserActionTimeout, + trussed_core::types::consent::Error::Interrupted => Error::KeepaliveCancel, _ => Error::OperationDenied, }) } diff --git a/src/state.rs b/src/state.rs index 38c4095..d59d23f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -9,11 +9,12 @@ use ctap_types::{ Error, String, }; -use littlefs2_core::path; -use trussed::{ - cbor_serialize_bytes, client, syscall, try_syscall, - types::{KeyId, Location, Mechanism, Path, PathBuf}, - Client as TrussedClient, +use littlefs2_core::{path, Path}; +use trussed_core::{ + mechanisms::{Chacha8Poly1305, P256}, + syscall, try_syscall, + types::{KeyId, Location, Mechanism, Message, PathBuf}, + CertificateClient, CryptoClient, FilesystemClient, }; use heapless::binary_heap::{BinaryHeap, Max}; @@ -21,7 +22,7 @@ use heapless::binary_heap::{BinaryHeap, Max}; use crate::{ credential::FullCredential, ctap2::{self, pin::PinProtocolState}, - Result, TrussedRequirements, + Result, }; #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -104,13 +105,13 @@ impl State { } } - pub fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { + pub fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { self.persistent.decrement_retries(trussed)?; self.runtime.decrement_retries(); Ok(()) } - pub fn reset_retries(&mut self, trussed: &mut T) -> Result<()> { + pub fn reset_retries(&mut self, trussed: &mut T) -> Result<()> { self.persistent.reset_retries(trussed)?; self.runtime.reset_retries(); Ok(()) @@ -137,7 +138,7 @@ pub struct Identity { } pub type Aaguid = [u8; 16]; -pub type Certificate = trussed::types::Message; +pub type Certificate = trussed_core::types::Message; impl Identity { // Attempt to yank out the aaguid of a certificate. @@ -171,7 +172,7 @@ impl Identity { } /// Lookup batch key and certificate, together with AAUGID. - pub fn attestation( + pub fn attestation( &mut self, trussed: &mut T, ) -> (Option<(KeyId, Certificate)>, Aaguid) { @@ -274,7 +275,7 @@ impl PersistentState { const RESET_RETRIES: u8 = 8; const FILENAME: &'static Path = path!("persistent-state.cbor"); - pub fn load(trussed: &mut T) -> Result { + 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),)) @@ -287,7 +288,7 @@ impl PersistentState { let data = result.unwrap().data; - let result = trussed::cbor_deserialize(&data); + let result = cbor_smol::cbor_deserialize(&data); if result.is_err() { info!("err deser'ing: {:?}", result.err().unwrap()); @@ -298,8 +299,9 @@ impl PersistentState { result.map_err(|_| Error::Other) } - pub fn save(&self, trussed: &mut T) -> Result<()> { - let data = cbor_serialize_bytes(self).unwrap(); + pub fn save(&self, trussed: &mut T) -> Result<()> { + let mut data = Message::new(); + cbor_smol::cbor_serialize_to(self, &mut data).unwrap(); syscall!(trussed.write_file( Location::Internal, @@ -310,7 +312,7 @@ impl PersistentState { Ok(()) } - pub fn reset(&mut self, trussed: &mut T) -> Result<()> { + pub fn reset(&mut self, trussed: &mut T) -> Result<()> { if let Some(key) = self.key_encryption_key { syscall!(trussed.delete(key)); } @@ -325,10 +327,7 @@ 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) => { @@ -343,14 +342,14 @@ impl PersistentState { } } - pub fn timestamp(&mut self, trussed: &mut T) -> Result { + pub fn timestamp(&mut self, trussed: &mut T) -> Result { let now = self.timestamp; self.timestamp += 1; self.save(trussed)?; Ok(now) } - pub fn key_encryption_key( + pub fn key_encryption_key( &mut self, trussed: &mut T, ) -> Result { @@ -360,7 +359,7 @@ impl PersistentState { } } - pub fn rotate_key_encryption_key( + pub fn rotate_key_encryption_key( &mut self, trussed: &mut T, ) -> Result { @@ -373,7 +372,7 @@ impl PersistentState { Ok(key) } - pub fn key_wrapping_key( + pub fn key_wrapping_key( &mut self, trussed: &mut T, ) -> Result { @@ -383,7 +382,7 @@ impl PersistentState { } } - pub fn rotate_key_wrapping_key( + pub fn rotate_key_wrapping_key( &mut self, trussed: &mut T, ) -> Result { @@ -409,7 +408,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)?; @@ -417,7 +416,7 @@ impl PersistentState { Ok(()) } - fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { + fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { // error to call before initialization if self.consecutive_pin_mismatches < Self::RESET_RETRIES { self.consecutive_pin_mismatches += 1; @@ -433,7 +432,7 @@ impl PersistentState { self.pin_hash } - pub fn set_pin_hash( + pub fn set_pin_hash( &mut self, trussed: &mut T, pin_hash: [u8; 16], @@ -477,7 +476,7 @@ impl RuntimeState { self.cached_credentials.push(credential); } - pub fn pop_credential( + pub fn pop_credential( &mut self, trussed: &mut T, ) -> Option { @@ -496,15 +495,12 @@ impl RuntimeState { self.cached_credentials.len() as _ } - pub fn pin_protocol( - &mut self, - trussed: &mut T, - ) -> &mut PinProtocolState { + pub fn pin_protocol(&mut self, trussed: &mut T) -> &mut PinProtocolState { self.pin_protocol .get_or_insert_with(|| PinProtocolState::new(trussed)) } - 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(); diff --git a/tests/virt/pipe.rs b/tests/virt/pipe.rs index 1d62d62..7391c3b 100644 --- a/tests/virt/pipe.rs +++ b/tests/virt/pipe.rs @@ -13,7 +13,7 @@ use std::collections::VecDeque; use ctap_types::Error; use ctaphid_dispatch::{command::Command, types::Requester}; -use heapless::Vec; +use heapless_bytes::Bytes; const MESSAGE_SIZE: usize = 3072; const PACKET_SIZE: usize = 64; @@ -369,7 +369,7 @@ impl<'a> Pipe<'a> { } match self.interchange.request(( request.command, - Vec::from_slice(&self.buffer[..request.length as usize]).unwrap(), + Bytes::from_slice(&self.buffer[..request.length as usize]).unwrap(), )) { Ok(_) => { self.state = State::WaitingOnAuthenticator(request); From c9512c7bdff055a5fd9373f1c60c2a14d05ef3ce Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 8 Jan 2025 21:56:25 +0100 Subject: [PATCH 111/135] Use released dependencies --- Cargo.toml | 21 ++++++++------------- examples/usbip.rs | 2 +- fuzz/Cargo.toml | 5 +---- tests/virt/mod.rs | 5 +---- tests/virt/pipe.rs | 2 +- 5 files changed, 12 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed9e34e..9509714 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,10 +26,10 @@ serde = { version = "1.0", default-features = false } serde_bytes = { version = "0.11.14", default-features = false } serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } -trussed-core = { version = "0.1.0-rc.1", features = ["aes256-cbc", "certificate-client", "chacha8-poly1305", "crypto-client", "ed255", "filesystem-client", "hmac-sha256", "management-client", "p256", "sha256", "ui-client"] } -trussed-fs-info = "0.1.0" -trussed-hkdf = { version = "0.2.0" } -trussed-chunked = { version = "0.1.0", optional = true } +trussed-core = { version = "0.1.0", features = ["aes256-cbc", "certificate-client", "chacha8-poly1305", "crypto-client", "ed255", "filesystem-client", "hmac-sha256", "management-client", "p256", "sha256", "ui-client"] } +trussed-fs-info = "0.2.0" +trussed-hkdf = { version = "0.3.0" } +trussed-chunked = { version = "0.2.0", optional = true } apdu-app = { version = "0.1", optional = true } ctaphid-app = { version = "0.1.0-rc.1", optional = true } @@ -58,7 +58,7 @@ ciborium = { version = "0.2.2" } ciborium-io = "0.2.2" cipher = "0.4.4" ctaphid = { version = "0.3.1", default-features = false } -ctaphid-dispatch = "0.1" +ctaphid-dispatch = "0.2" delog = { version = "0.1.6", features = ["std-log"] } env_logger = "0.11.0" hex-literal = "0.4.1" @@ -74,18 +74,13 @@ serde_test = "1.0.176" trussed = { version = "0.1", features = ["virt"] } trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] } trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } -usbd-ctaphid = "0.1.0" +usbd-ctaphid = "0.2.0" x509-parser = "0.16.0" [package.metadata.docs.rs] features = ["dispatch"] [patch.crates-io] -ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "5a2864c76fea6785d9ffe4c7b6596237d8378755" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" } -trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } -trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } -trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } -trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "60c58eb80685f72d80850b850800fc6a660fe50a" } -usbd-ctaphid = { git = "https://github.com/trussed-dev/usbd-ctaphid.git", rev = "96bf04e97cfc8077b4d9b6b97b3e34ec3ca3e2fc" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "1e1ca03a3a62ea9b802f4070ea4bce002eeb4bec" } +trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "4fe4e4e287dac1d92fcd4f97e8926497bfa9d7a9" } diff --git a/examples/usbip.rs b/examples/usbip.rs index d25a2ac..2379a0c 100644 --- a/examples/usbip.rs +++ b/examples/usbip.rs @@ -63,7 +63,7 @@ impl trussed_usbip::Apps<'static, S, Dispatcher> for FidoApp { f: impl FnOnce( &mut [&mut dyn ctaphid_dispatch::app::App< 'static, - { ctaphid_dispatch::types::MESSAGE_SIZE }, + { ctaphid_dispatch::MESSAGE_SIZE }, >], ) -> T, ) -> T { diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index a6f9ad6..5f466bb 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -25,7 +25,4 @@ bench = false [patch.crates-io] trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" } -trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } -trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } -trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "1e1ca03a3a62ea9b802f4070ea4bce002eeb4bec" } diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index baee99f..94c5e81 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -18,10 +18,7 @@ use ctaphid::{ error::{RequestError, ResponseError}, HidDevice, HidDeviceInfo, }; -use ctaphid_dispatch::{ - dispatch::Dispatch, - types::{Channel, Requester}, -}; +use ctaphid_dispatch::{Channel, Dispatch, Requester}; use fido_authenticator::{Authenticator, Config, Conforming}; use littlefs2::{path, path::PathBuf}; use rand::{ diff --git a/tests/virt/pipe.rs b/tests/virt/pipe.rs index 7391c3b..0fcdf04 100644 --- a/tests/virt/pipe.rs +++ b/tests/virt/pipe.rs @@ -12,7 +12,7 @@ use std::collections::VecDeque; use ctap_types::Error; -use ctaphid_dispatch::{command::Command, types::Requester}; +use ctaphid_dispatch::{app::Command, Requester}; use heapless_bytes::Bytes; const MESSAGE_SIZE: usize = 3072; From d0885e1ccb53563ee5cb94c4ae96cd7020ae1daa Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 1 Nov 2024 12:26:27 +0100 Subject: [PATCH 112/135] Extend credential management tests This patch adds tests for deleting discoverable credentials and for updating the user information for existing credentials. --- tests/authenticator/mod.rs | 50 ++++++- tests/basic.rs | 3 +- tests/cred_mgmt.rs | 258 +++++++++++++++++++++++++++++++++---- tests/webauthn/mod.rs | 25 +++- 4 files changed, 297 insertions(+), 39 deletions(-) diff --git a/tests/authenticator/mod.rs b/tests/authenticator/mod.rs index 5b01e25..331b07f 100644 --- a/tests/authenticator/mod.rs +++ b/tests/authenticator/mod.rs @@ -4,8 +4,8 @@ use super::{ virt::{Ctap2, Ctap2Error}, webauthn::{ AttStmtFormat, ClientPin, CredentialData, CredentialManagement, CredentialManagementParams, - KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredParam, - PublicKey, Rp, SharedSecret, User, + KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, + PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }, }; @@ -142,11 +142,12 @@ impl Authenticator<'_, Pin> { rps } - pub fn list_credentials(&mut self, rp_id: &str) -> Vec { + pub fn list_credentials(&mut self, rp_id: &str) -> Vec<(User, PubKeyCredDescriptor)> { let rp_id_hash = rp_id_hash(rp_id); let pin_token = self.get_pin_token(0x04, Some(rp_id.to_owned())); let params = CredentialManagementParams { - rp_id_hash: rp_id_hash.to_vec(), + rp_id_hash: Some(rp_id_hash.to_vec()), + ..Default::default() }; let mut pin_auth_param = vec![0x04]; pin_auth_param.extend_from_slice(¶ms.serialized()); @@ -161,17 +162,54 @@ impl Authenticator<'_, Pin> { // TODO: check other fields let total_credentials = reply.total_credentials.unwrap(); let mut credentials = Vec::with_capacity(total_credentials); - credentials.push(reply.user.unwrap().into()); + credentials.push((reply.user.unwrap().into(), reply.credential_id.unwrap())); for _ in 1..total_credentials { let request = CredentialManagement::new(0x05); let reply = self.ctap2.exec(request).unwrap(); // TODO: check other fields - credentials.push(reply.user.unwrap().into()); + credentials.push((reply.user.unwrap().into(), reply.credential_id.unwrap())); } credentials } + + pub fn delete_credential(&mut self, id: &[u8]) { + let pin_token = self.get_pin_token(0x04, None); + let params = CredentialManagementParams { + credential_id: Some(PubKeyCredDescriptor::new("public-key", id)), + ..Default::default() + }; + let mut pin_auth_param = vec![0x06]; + pin_auth_param.extend_from_slice(¶ms.serialized()); + let pin_auth = pin_token.authenticate(&pin_auth_param); + let request = CredentialManagement { + subcommand: 0x06, + subcommand_params: Some(params), + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + }; + self.ctap2.exec(request).unwrap(); + } + + pub fn update_user(&mut self, id: &[u8], user: User) -> Result<(), Ctap2Error> { + let pin_token = self.get_pin_token(0x04, None); + let params = CredentialManagementParams { + credential_id: Some(PubKeyCredDescriptor::new("public-key", id)), + user: Some(user), + ..Default::default() + }; + let mut pin_auth_param = vec![0x07]; + pin_auth_param.extend_from_slice(¶ms.serialized()); + let pin_auth = pin_token.authenticate(&pin_auth_param); + let request = CredentialManagement { + subcommand: 0x07, + subcommand_params: Some(params), + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + }; + self.ctap2.exec(request).map(|_| ()) + } } pub struct CredentialsMetadata { diff --git a/tests/basic.rs b/tests/basic.rs index 95fad70..0276a36 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -457,7 +457,8 @@ impl TestListCredentials { pin_token_rp_id, ); let params = CredentialManagementParams { - rp_id_hash: reply.rp_id_hash.unwrap().as_bytes().unwrap().to_owned(), + rp_id_hash: Some(reply.rp_id_hash.unwrap().as_bytes().unwrap().to_owned()), + ..Default::default() }; let mut pin_auth_param = vec![0x04]; pin_auth_param.extend_from_slice(¶ms.serialized()); diff --git a/tests/cred_mgmt.rs b/tests/cred_mgmt.rs index 4b4e8a0..b86c4ac 100644 --- a/tests/cred_mgmt.rs +++ b/tests/cred_mgmt.rs @@ -4,45 +4,208 @@ pub mod authenticator; pub mod virt; pub mod webauthn; -use std::collections::BTreeMap; +use std::collections::BTreeSet; use littlefs2::path::PathBuf; -use authenticator::Authenticator; +use authenticator::{Authenticator, Pin}; use virt::{Ctap2Error, Options}; -use webauthn::{Rp, User}; +use webauthn::{CredentialData, PubKeyCredDescriptor, Rp, User}; + +struct CredMgmt<'a> { + authenticator: Authenticator<'a, Pin>, + credentials: Vec<(Rp, User, CredentialData)>, +} + +impl<'a> CredMgmt<'a> { + fn new(authenticator: Authenticator<'a, Pin>) -> Self { + Self { + authenticator, + credentials: Default::default(), + } + } + + fn make_credential(&mut self, rp: Rp, user: User) -> Result { + self.authenticator + .make_credential(rp.clone(), user.clone()) + .inspect(|credential_data| { + self.credentials.push((rp, user, credential_data.clone())); + }) + } + + fn delete_credential(&mut self, id: Vec) -> Result<(), Ctap2Error> { + self.authenticator.delete_credential(&id); + self.credentials.retain(|(_, _, data)| data.id != id); + Ok(()) + } + + fn delete_credential_at(&mut self, i: usize) -> Result<(), Ctap2Error> { + assert!(i < self.credentials.len()); + let id = self.credentials[i].2.id.clone(); + self.delete_credential(id) + } + + fn update_user(&mut self, id: Vec, user: User) -> Result<(), Ctap2Error> { + self.authenticator.update_user(&id, user.clone())?; + self.credentials + .iter_mut() + .filter(|(_, _, data)| data.id == id) + .for_each(|cred| cred.1 = user.clone()); + Ok(()) + } + + fn update_user_at(&mut self, i: usize, user: User) -> Result<(), Ctap2Error> { + assert!(i < self.credentials.len()); + let id = self.credentials[i].2.id.clone(); + self.update_user(id, user) + } + + fn list(&mut self) { + let expected_rp_ids = self.rp_ids(); + let actual_rps = self.authenticator.list_rps(); + let actual_rp_ids: BTreeSet<_> = actual_rps.iter().map(|rp| rp.id.clone()).collect(); + assert_eq!(expected_rp_ids, actual_rp_ids); + // TODO: check other RP fields than ID + + for rp_id in expected_rp_ids { + assert!(actual_rps.iter().any(|rp| rp.id == rp_id)); + let expected_credentials = self.credentials(&rp_id); + let actual_credentials = self.authenticator.list_credentials(&rp_id); + let actual_credentials: BTreeSet<_> = actual_credentials.into_iter().collect(); + assert_eq!(expected_credentials, actual_credentials); + } + } + + fn rp_ids(&self) -> BTreeSet { + self.credentials + .iter() + .map(|(rp, _, _)| rp.id.clone()) + .collect() + } + + fn credentials(&self, rp_id: &str) -> BTreeSet<(User, PubKeyCredDescriptor)> { + self.credentials + .iter() + .filter(|(rp, _, _)| rp.id == rp_id) + .map(|(_, user, data)| { + ( + user.clone(), + PubKeyCredDescriptor::new("public-key", data.id.clone()), + ) + }) + .collect() + } +} + +fn generate_rp(i: usize) -> Rp { + // TODO: set other fields than id + let rp_id = format!("rp{i}"); + Rp::new(rp_id) +} + +fn generate_user(i: u8) -> User { + // TODO: set other fields than id + let mut user = Vec::from(b"john.doe"); + user.push(i); + User::new(user) +} #[test] fn test_list_credentials() { virt::run_ctap2(|device| { - let mut authenticator = Authenticator::new(device).set_pin(b"123456"); - let mut credentials: BTreeMap<_, _> = (0..10) - .map(|i| { - // TODO: set other fields than id - let rp_id = format!("rp{i}"); - let user = b"john.doe"; - authenticator - .make_credential(Rp::new(rp_id.clone()), User::new(user)) - .unwrap(); - (rp_id, user) - }) - .collect(); + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for i in 0..10 { + let rp = generate_rp(i); + let user = generate_user(0); + cred_mgmt.make_credential(rp, user).unwrap(); + } - let rps = authenticator.list_rps(); - assert_eq!(rps.len(), 10); - for rp in &rps { - assert_eq!(rp.name, None); - let expected = credentials.remove(&rp.id).unwrap(); + cred_mgmt.list(); + }) +} - let mut credentials = authenticator.list_credentials(&rp.id); - assert_eq!(credentials.len(), 1); - let actual = credentials.pop().unwrap(); +#[test] +fn test_list_credentials_multi() { + virt::run_ctap2(|device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { + let rp = generate_rp(i); + for j in 0..n { + let user = generate_user(j); + cred_mgmt.make_credential(rp.clone(), user).unwrap(); + } + } + + cred_mgmt.list(); + }) +} - assert_eq!(actual.id, expected); - assert_eq!(actual.name, None); - assert_eq!(actual.display_name, None); +#[test] +fn test_list_credentials_delete() { + virt::run_ctap2(|device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { + let rp = generate_rp(i); + for j in 0..n { + let user = generate_user(j); + cred_mgmt.make_credential(rp.clone(), user).unwrap(); + } + } + + // deletes the only credential for rp2 + cred_mgmt.delete_credential_at(4).unwrap(); + // deletes one of three credentials for rp1 + cred_mgmt.delete_credential_at(2).unwrap(); + + cred_mgmt.list(); + }) +} + +#[test] +fn test_list_credentials_update_user() { + virt::run_ctap2(|device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { + let rp = generate_rp(i); + for j in 0..n { + let user = generate_user(j); + cred_mgmt.make_credential(rp.clone(), user).unwrap(); + } } - assert!(credentials.is_empty()); + + // case 1: updates the only credential for rp2 + + // changing the user ID fails + let user = generate_user(98); + assert_eq!(cred_mgmt.update_user_at(4, user), Err(Ctap2Error(0x02))); + + cred_mgmt.list(); + + // setting the display name works + let mut user = generate_user(0); + user.display_name = Some("John Doe".into()); + cred_mgmt.update_user_at(4, user).unwrap(); + + cred_mgmt.list(); + + // case 2: updates one of three credentials for rp1 + + // changing the user ID fails + let user = generate_user(99); + assert_eq!(cred_mgmt.update_user_at(2, user), Err(Ctap2Error(0x02))); + + cred_mgmt.list(); + + // setting the display name works + let mut user = generate_user(1); + user.display_name = Some("John Doe".into()); + cred_mgmt.update_user_at(2, user).unwrap(); + + cred_mgmt.list(); }) } @@ -137,3 +300,44 @@ fn test_filesystem_full() { assert_eq!(metadata.remaining, 0); }) } + +#[test] +fn test_filesystem_full_update_user() { + let mut options = Options { + max_resident_credential_count: Some(10), + ..Default::default() + }; + for i in 0..80 { + let path = PathBuf::try_from(format!("/test/{i}").as_str()).unwrap(); + options.files.push((path, vec![0; 512])); + } + // TODO: inspect filesystem after run and check remaining blocks + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + + let mut i = 0; + loop { + let rp = generate_rp(i); + let user = generate_user(0); + let result = cred_mgmt.make_credential(rp, user); + + if result == Err(Ctap2Error(0x28)) { + break; + } + result.unwrap(); + + i += 1; + } + + cred_mgmt.list(); + + // filesystem is now full, we cannot create new credentials + // but: we still want to be able to update existing credentials + let mut user = generate_user(0); + user.display_name = Some("John Doe".into()); + cred_mgmt.update_user_at(1, user).unwrap(); + + cred_mgmt.list(); + }) +} diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 34025df..6713ea0 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -203,6 +203,7 @@ impl From for ClientPinReply { } } +#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] pub struct Rp { pub id: String, pub name: Option, @@ -245,6 +246,7 @@ impl From for Rp { } } +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct User { pub id: Vec, pub name: Option, @@ -516,6 +518,7 @@ impl From for MakeCredentialReply { } } +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct PubKeyCredDescriptor { pub ty: String, pub id: Vec, @@ -533,8 +536,8 @@ impl PubKeyCredDescriptor { impl From for Value { fn from(descriptor: PubKeyCredDescriptor) -> Value { let mut map = Map::default(); - map.push("type", descriptor.ty); map.push("id", descriptor.id); + map.push("type", descriptor.ty); map.into() } } @@ -645,7 +648,7 @@ impl From for AuthData { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct CredentialData { pub id: Vec, pub public_key: BTreeMap, @@ -780,9 +783,11 @@ impl Request for CredentialManagement { type Reply = CredentialManagementReply; } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct CredentialManagementParams { - pub rp_id_hash: Vec, + pub rp_id_hash: Option>, + pub credential_id: Option, + pub user: Option, } impl CredentialManagementParams { @@ -796,7 +801,15 @@ impl CredentialManagementParams { impl From for Value { fn from(params: CredentialManagementParams) -> Value { let mut map = Map::default(); - map.push(1, params.rp_id_hash); + if let Some(rp_id_hash) = params.rp_id_hash { + map.push(1, rp_id_hash); + } + if let Some(credential_id) = params.credential_id { + map.push(2, credential_id); + } + if let Some(user) = params.user { + map.push(3, user); + } map.into() } } @@ -808,6 +821,7 @@ pub struct CredentialManagementReply { pub rp_id_hash: Option, pub total_rps: Option, pub user: Option, + pub credential_id: Option, pub total_credentials: Option, pub third_party_payment: Option, } @@ -826,6 +840,7 @@ impl From for CredentialManagementReply { rp_id_hash: map.remove(&4), total_rps: map.remove(&5).map(|value| value.deserialized().unwrap()), user: map.remove(&6), + credential_id: map.remove(&7).map(|value| value.into()), total_credentials: map.remove(&9).map(|value| value.deserialized().unwrap()), third_party_payment: map.remove(&0x0c).map(|value| value.deserialized().unwrap()), } From bb80cba2c28e8a6b19c031ee7d52aadf8c37c801 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 26 Jan 2025 22:13:42 +0100 Subject: [PATCH 113/135] Fix compiler warnings This patch adds the missing log-trace feature and removes the deprecated and unnecessary .cargo/config file. --- .cargo/config | 2 -- Cargo.toml | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .cargo/config diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index c91c3f3..0000000 --- a/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[net] -git-fetch-with-cli = true diff --git a/Cargo.toml b/Cargo.toml index 9509714..60c2f54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ chunked = ["trussed-chunked"] log-all = [] log-none = [] +log-trace = [] log-info = [] log-debug = [] log-warn = [] From 83477813bf9c951f26650d0cc2211f1b34a3e5e7 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 18 Feb 2025 10:46:09 +0100 Subject: [PATCH 114/135] tests: Add helper trait and use exhaustive --- Cargo.toml | 1 + tests/basic.rs | 196 ++++++++++++++++++++-------------------------- tests/virt/mod.rs | 4 +- 3 files changed, 86 insertions(+), 115 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60c2f54..d0a17b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ ctaphid = { version = "0.3.1", default-features = false } ctaphid-dispatch = "0.2" delog = { version = "0.1.6", features = ["std-log"] } env_logger = "0.11.0" +exhaustive = "0.2.2" hex-literal = "0.4.1" hmac = "0.12.1" interchange = "0.3.0" diff --git a/tests/basic.rs b/tests/basic.rs index 0276a36..902c165 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -3,9 +3,10 @@ pub mod virt; pub mod webauthn; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, fmt::Debug}; use ciborium::Value; +use exhaustive::Exhaustive; use hex_literal::hex; use virt::{Ctap2, Ctap2Error}; @@ -15,6 +16,28 @@ use webauthn::{ PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }; +trait Test: Debug { + fn test(&self); + + fn run(&self) { + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{self:#?}"); + println!(); + + self.test(); + } + + fn run_all() + where + Self: Exhaustive, + { + for test in Self::iter_exhaustive(None) { + test.run(); + } + } +} + #[test] fn test_ping() { virt::run_ctaphid(|device| { @@ -107,13 +130,34 @@ fn test_get_pin_token() { }) } -#[derive(Clone, Debug)] -struct RequestPinToken { - permissions: u8, - rp_id: Option, +#[derive(Clone, Debug, Exhaustive)] +enum RequestPinToken { + InvalidPermissions, + InvalidRpId, + NoRpId, + ValidRpId, +} + +impl RequestPinToken { + fn permissions(&self, valid: u8, invalid: u8) -> u8 { + if matches!(self, Self::InvalidPermissions) { + invalid + } else { + valid + } + } + + fn rp_id(&self, valid: &str, invalid: &str) -> Option { + match self { + Self::InvalidPermissions => None, + Self::InvalidRpId => Some(invalid.to_owned()), + Self::NoRpId => None, + Self::ValidRpId => Some(valid.to_owned()), + } + } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Exhaustive)] enum AttestationFormatsPreference { Empty, None, @@ -125,16 +169,6 @@ enum AttestationFormatsPreference { } impl AttestationFormatsPreference { - const ALL: &'static [Self] = &[ - Self::Empty, - Self::None, - Self::Packed, - Self::NonePacked, - Self::PackedNone, - Self::OtherNonePacked, - Self::MultiOtherNonePacked, - ]; - fn format(&self) -> Option { match self { Self::Empty | Self::Packed | Self::PackedNone => Some(AttStmtFormat::Packed), @@ -180,23 +214,36 @@ impl From for Vec<&'static str> { } } -#[derive(Debug)] +#[derive(Debug, Exhaustive)] struct TestMakeCredential { pin_token: Option, - pub_key_alg: i32, + valid_pub_key_alg: bool, attestation_formats_preference: Option, } impl TestMakeCredential { - fn run(&self) { - println!("{}", "=".repeat(80)); - println!("Running test:"); - println!("{self:#?}"); - println!(); + fn expected_error(&self) -> Option { + if let Some(pin_token) = &self.pin_token { + if matches!( + pin_token, + RequestPinToken::InvalidPermissions | RequestPinToken::InvalidRpId + ) { + return Some(0x33); + } + } + if !self.valid_pub_key_alg { + return Some(0x26); + } + None + } +} +impl Test for TestMakeCredential { + fn test(&self) { let key_agreement_key = KeyAgreementKey::generate(); let pin = b"123456"; let rp_id = "example.com"; + let invalid_rp_id = "test.com"; // TODO: client data let client_data_hash = b""; @@ -209,8 +256,8 @@ impl TestMakeCredential { &key_agreement_key, &shared_secret, pin, - pin_token.permissions, - pin_token.rp_id.clone(), + pin_token.permissions(0x01, 0x04), + pin_token.rp_id(rp_id, invalid_rp_id), ); pin_token.authenticate(client_data_hash) }); @@ -219,7 +266,8 @@ impl TestMakeCredential { let user = User::new(b"id123") .name("john.doe") .display_name("John Doe"); - let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", self.pub_key_alg)]; + let pub_key_alg = if self.valid_pub_key_alg { -7 } else { -11 }; + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", pub_key_alg)]; let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); if let Some(pin_auth) = pin_auth { request.pin_auth = Some(pin_auth); @@ -248,79 +296,21 @@ impl TestMakeCredential { } }); } - - fn expected_error(&self) -> Option { - if let Some(pin_token) = &self.pin_token { - if pin_token.permissions != 0x01 { - return Some(0x33); - } - if let Some(rp_id) = &pin_token.rp_id { - if rp_id != "example.com" { - return Some(0x33); - } - } - } - if self.pub_key_alg != -7 { - return Some(0x26); - } - None - } } #[test] fn test_make_credential() { - let pin_tokens = [ - None, - Some(RequestPinToken { - permissions: 0x01, - rp_id: None, - }), - Some(RequestPinToken { - permissions: 0x01, - rp_id: Some("example.com".to_owned()), - }), - Some(RequestPinToken { - permissions: 0x01, - rp_id: Some("test.com".to_owned()), - }), - Some(RequestPinToken { - permissions: 0x04, - rp_id: None, - }), - ]; - for pin_token in pin_tokens { - for pub_key_alg in [-7, -11] { - TestMakeCredential { - pin_token: pin_token.clone(), - pub_key_alg, - attestation_formats_preference: None, - } - .run(); - for attestation_formats_preference in AttestationFormatsPreference::ALL { - TestMakeCredential { - pin_token: pin_token.clone(), - pub_key_alg, - attestation_formats_preference: Some(*attestation_formats_preference), - } - .run(); - } - } - } + TestMakeCredential::run_all(); } -#[derive(Debug)] +#[derive(Debug, Exhaustive)] struct TestGetAssertion { mc_third_party_payment: Option, ga_third_party_payment: Option, } -impl TestGetAssertion { - fn run(&self) { - println!("{}", "=".repeat(80)); - println!("Running test:"); - println!("{self:#?}"); - println!(); - +impl Test for TestGetAssertion { + fn test(&self) { let rp_id = "example.com"; // TODO: client data let client_data_hash = &[0; 32]; @@ -372,25 +362,17 @@ impl TestGetAssertion { #[test] fn test_get_assertion() { - for mc_third_party_payment in [Some(false), Some(true), None] { - for ga_third_party_payment in [Some(false), Some(true), None] { - TestGetAssertion { - mc_third_party_payment, - ga_third_party_payment, - } - .run() - } - } + TestGetAssertion::run_all(); } -#[derive(Debug)] +#[derive(Debug, Exhaustive)] struct TestListCredentials { pin_token_rp_id: bool, third_party_payment: Option, } -impl TestListCredentials { - fn run(&self) { +impl Test for TestListCredentials { + fn test(&self) { let key_agreement_key = KeyAgreementKey::generate(); let pin = b"123456"; let rp_id = "example.com"; @@ -483,17 +465,5 @@ impl TestListCredentials { #[test] fn test_list_credentials() { - for pin_token_rp_id in [false, true] { - for third_party_payment in [Some(false), Some(true), None] { - let test = TestListCredentials { - pin_token_rp_id, - third_party_payment, - }; - println!("{}", "=".repeat(80)); - println!("Running test:"); - println!("{test:#?}"); - println!(); - test.run(); - } - } + TestListCredentials::run_all(); } diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 94c5e81..84439ef 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -85,7 +85,7 @@ where let mut dispatch = Dispatch::new(rp); while !poller_stop.load(Ordering::Relaxed) { dispatch.poll(&mut [&mut authenticator]); - thread::sleep(Duration::from_millis(10)); + thread::sleep(Duration::from_millis(1)); } }); @@ -230,7 +230,7 @@ impl HidDevice for Device<'_> { }; } - thread::sleep(Duration::from_millis(10)); + thread::sleep(Duration::from_millis(1)); } } } From 10b0334fed52cbb1d9db9f33d9808c7c24b9d2d7 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 18 Feb 2025 10:46:11 +0100 Subject: [PATCH 115/135] tests: Extend makeCredential tests --- tests/basic.rs | 99 ++++++++++++++++++++++++++++++++++--------- tests/webauthn/mod.rs | 33 ++++++++++++--- 2 files changed, 107 insertions(+), 25 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index 902c165..f764f11 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -214,22 +214,46 @@ impl From for Vec<&'static str> { } } +#[derive(Debug, Exhaustive)] +enum PinAuth { + NoPin, + PinNoToken, + PinToken(RequestPinToken), +} + #[derive(Debug, Exhaustive)] struct TestMakeCredential { - pin_token: Option, + pin_auth: PinAuth, + options: Option, valid_pub_key_alg: bool, attestation_formats_preference: Option, + hmac_secret: bool, } impl TestMakeCredential { fn expected_error(&self) -> Option { - if let Some(pin_token) = &self.pin_token { - if matches!( - pin_token, - RequestPinToken::InvalidPermissions | RequestPinToken::InvalidRpId - ) { + if let Some(options) = self.options { + // TODO: this is the current implementation, but the spec allows Some(true) + if options.up.is_some() { + return Some(0x2c); + } + } + match &self.pin_auth { + PinAuth::PinToken( + RequestPinToken::InvalidPermissions | RequestPinToken::InvalidRpId, + ) => { return Some(0x33); } + PinAuth::PinNoToken => { + return Some(0x36); + } + _ => {} + } + if let Some(options) = self.options { + // TODO: review if uv should be always rejected due to the lack of built-in uv + if !matches!(self.pin_auth, PinAuth::PinToken(_)) && options.uv == Some(true) { + return Some(0x2c); + } } if !self.valid_pub_key_alg { return Some(0x26); @@ -240,7 +264,6 @@ impl TestMakeCredential { impl Test for TestMakeCredential { fn test(&self) { - let key_agreement_key = KeyAgreementKey::generate(); let pin = b"123456"; let rp_id = "example.com"; let invalid_rp_id = "test.com"; @@ -248,19 +271,29 @@ impl Test for TestMakeCredential { let client_data_hash = b""; virt::run_ctap2(|device| { - let pin_auth = self.pin_token.as_ref().map(|pin_token| { - let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); - let pin_token = get_pin_token( - &device, - &key_agreement_key, - &shared_secret, - pin, - pin_token.permissions(0x01, 0x04), - pin_token.rp_id(rp_id, invalid_rp_id), - ); - pin_token.authenticate(client_data_hash) - }); + let mut pin_auth = None; + match &self.pin_auth { + PinAuth::NoPin => {} + PinAuth::PinNoToken => { + let key_agreement_key = KeyAgreementKey::generate(); + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + } + PinAuth::PinToken(pin_token) => { + let key_agreement_key = KeyAgreementKey::generate(); + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + let pin_token = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin, + pin_token.permissions(0x01, 0x04), + pin_token.rp_id(rp_id, invalid_rp_id), + ); + pin_auth = Some(pin_token.authenticate(client_data_hash)); + } + } let rp = Rp::new(rp_id); let user = User::new(b"id123") @@ -269,12 +302,20 @@ impl Test for TestMakeCredential { let pub_key_alg = if self.valid_pub_key_alg { -7 } else { -11 }; let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", pub_key_alg)]; let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + request.options = self.options; if let Some(pin_auth) = pin_auth { request.pin_auth = Some(pin_auth); request.pin_protocol = Some(2); } request.attestation_formats_preference = self.attestation_formats_preference.map(From::from); + // TODO: test other extensions and permutations + if self.hmac_secret { + request.extensions = Some(ExtensionsInput { + hmac_secret: Some(true), + ..Default::default() + }); + } let result = device.exec(request); if let Some(error) = self.expected_error() { @@ -282,6 +323,15 @@ impl Test for TestMakeCredential { } else { let reply = result.unwrap(); assert!(reply.auth_data.credential.is_some()); + assert!(reply.auth_data.up_flag()); + // TODO: review conditions + assert_eq!( + reply.auth_data.uv_flag(), + self.options.and_then(|options| options.uv).unwrap_or(false) + || matches!(self.pin_auth, PinAuth::PinToken(_)) + ); + assert!(reply.auth_data.at_flag()); + assert_eq!(reply.auth_data.ed_flag(), self.hmac_secret); let format = self .attestation_formats_preference .unwrap_or(AttestationFormatsPreference::Packed) @@ -293,6 +343,12 @@ impl Test for TestMakeCredential { assert_eq!(reply.fmt, AttStmtFormat::None.as_str()); assert!(reply.att_stmt.is_none()); } + if self.hmac_secret { + let extensions = reply.auth_data.extensions.unwrap(); + assert_eq!(extensions.get("hmac-secret"), Some(&Value::from(true))); + } else { + assert_eq!(reply.auth_data.extensions, None); + } } }); } @@ -325,6 +381,7 @@ impl Test for TestGetAssertion { if let Some(third_party_payment) = self.mc_third_party_payment { request.extensions = Some(ExtensionsInput { third_party_payment: Some(third_party_payment), + ..Default::default() }); } let response = device.exec(request).unwrap(); @@ -338,6 +395,7 @@ impl Test for TestGetAssertion { if let Some(third_party_payment) = self.ga_third_party_payment { request.extensions = Some(ExtensionsInput { third_party_payment: Some(third_party_payment), + ..Default::default() }); } let response = device.exec(request).unwrap(); @@ -397,6 +455,7 @@ impl Test for TestListCredentials { if let Some(third_party_payment) = self.third_party_payment { request.extensions = Some(ExtensionsInput { third_party_payment: Some(third_party_payment), + ..Default::default() }); } let reply = device.exec(request).unwrap(); diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 6713ea0..953c61a 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use ciborium::Value; use cipher::{BlockDecryptMut as _, BlockEncryptMut as _, KeyIvInit}; +use exhaustive::Exhaustive; use hmac::Mac; use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey}; use rand::RngCore as _; @@ -395,14 +396,18 @@ impl From for Value { } } -#[derive(Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct ExtensionsInput { + pub hmac_secret: Option, pub third_party_payment: Option, } impl From for Value { fn from(extensions: ExtensionsInput) -> Value { let mut map = Map::default(); + if let Some(hmac_secret) = extensions.hmac_secret { + map.push("hmac-secret", hmac_secret); + } if let Some(third_party_payment) = extensions.third_party_payment { map.push("thirdPartyPayment", third_party_payment); } @@ -410,11 +415,11 @@ impl From for Value { } } -#[derive(Default)] +#[derive(Clone, Copy, Debug, Default, Exhaustive)] pub struct MakeCredentialOptions { - rk: Option, - up: Option, - uv: Option, + pub rk: Option, + pub up: Option, + pub uv: Option, } impl MakeCredentialOptions { @@ -617,6 +622,24 @@ pub struct AuthData { pub extensions: Option>, } +impl AuthData { + pub fn up_flag(&self) -> bool { + self.flags & 0b1 != 0 + } + + pub fn uv_flag(&self) -> bool { + self.flags & 0b100 != 0 + } + + pub fn at_flag(&self) -> bool { + self.flags & 0b1000000 != 0 + } + + pub fn ed_flag(&self) -> bool { + self.flags & 0b10000000 != 0 + } +} + impl From> for AuthData { fn from(vec: Vec) -> Self { let (_rp_id_hash, bytes) = vec.split_at(32); From 9e4cd65e54467787476756ffb78fa9f1915426ba Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 18 Feb 2025 10:46:11 +0100 Subject: [PATCH 116/135] tests: Extend getAssertion tests --- tests/basic.rs | 57 +++++++++++++++++++++++++++++++++++++------ tests/webauthn/mod.rs | 37 ++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index f764f11..10e4a6d 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -12,8 +12,9 @@ use hex_literal::hex; use virt::{Ctap2, Ctap2Error}; use webauthn::{ AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, - GetAssertion, GetInfo, KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, - PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, SharedSecret, User, + GetAssertion, GetAssertionOptions, GetInfo, KeyAgreementKey, MakeCredential, + MakeCredentialOptions, PinToken, PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, + SharedSecret, User, }; trait Test: Debug { @@ -361,16 +362,34 @@ fn test_make_credential() { #[derive(Debug, Exhaustive)] struct TestGetAssertion { + rk: bool, + allow_list: bool, + options: Option, mc_third_party_payment: Option, ga_third_party_payment: Option, } +impl TestGetAssertion { + fn expected_error(&self) -> Option { + if let Some(options) = self.options { + if options.uv == Some(true) { + return Some(0x2c); + } + } + if !self.rk && !self.allow_list { + return Some(0x2e); + } + None + } +} + impl Test for TestGetAssertion { fn test(&self) { let rp_id = "example.com"; // TODO: client data let client_data_hash = &[0; 32]; + // TODO: test with PIN virt::run_ctap2(|device| { let rp = Rp::new(rp_id); let user = User::new(b"id123") @@ -378,6 +397,9 @@ impl Test for TestGetAssertion { .display_name("John Doe"); let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + if self.rk { + request.options = Some(MakeCredentialOptions::default().rk(true)); + } if let Some(third_party_payment) = self.mc_third_party_payment { request.extensions = Some(ExtensionsInput { third_party_payment: Some(third_party_payment), @@ -388,20 +410,41 @@ impl Test for TestGetAssertion { let credential = response.auth_data.credential.unwrap(); let mut request = GetAssertion::new(rp_id, client_data_hash); - request.allow_list = Some(vec![PubKeyCredDescriptor::new( - "public-key", - credential.id.clone(), - )]); + // TODO: test more cases: + // - multiple credentials in allow list + // - invalid allow list + if self.allow_list { + request.allow_list = Some(vec![PubKeyCredDescriptor::new( + "public-key", + credential.id.clone(), + )]); + } if let Some(third_party_payment) = self.ga_third_party_payment { request.extensions = Some(ExtensionsInput { third_party_payment: Some(third_party_payment), ..Default::default() }); } - let response = device.exec(request).unwrap(); + request.options = self.options; + let result = device.exec(request); + if let Some(error) = self.expected_error() { + assert_eq!(result, Err(Ctap2Error(error))); + return; + } + let response = result.unwrap(); assert_eq!(response.credential.ty, "public-key"); assert_eq!(response.credential.id, credential.id); assert_eq!(response.auth_data.credential, None); + assert_eq!( + response.auth_data.up_flag(), + self.options.and_then(|options| options.up).unwrap_or(true) + ); + assert!(!response.auth_data.uv_flag()); + assert!(!response.auth_data.at_flag()); + assert_eq!( + response.auth_data.ed_flag(), + self.ga_third_party_payment.unwrap_or_default() + ); credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); if self.ga_third_party_payment.unwrap_or_default() { let extensions = response.auth_data.extensions.unwrap(); diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 953c61a..e0d7e0e 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -562,6 +562,7 @@ pub struct GetAssertion { client_data_hash: Vec, pub allow_list: Option>, pub extensions: Option, + pub options: Option, } impl GetAssertion { @@ -571,6 +572,7 @@ impl GetAssertion { client_data_hash: client_data_hash.into(), allow_list: None, extensions: None, + options: None, } } } @@ -587,6 +589,9 @@ impl From for Value { if let Some(extensions) = request.extensions { map.push(0x04, extensions); } + if let Some(options) = request.options { + map.push(0x05, options); + } map.into() } } @@ -597,6 +602,38 @@ impl Request for GetAssertion { type Reply = GetAssertionReply; } +#[derive(Clone, Copy, Debug, Default, Exhaustive)] +pub struct GetAssertionOptions { + pub up: Option, + pub uv: Option, +} + +impl GetAssertionOptions { + pub fn up(mut self, up: bool) -> Self { + self.up = Some(up); + self + } + + pub fn uv(mut self, uv: bool) -> Self { + self.uv = Some(uv); + self + } +} + +impl From for Value { + fn from(options: GetAssertionOptions) -> Value { + let mut map = Map::default(); + if let Some(up) = options.up { + map.push("up", up); + } + if let Some(uv) = options.uv { + map.push("uv", uv); + } + map.into() + } +} + +#[derive(Debug, PartialEq)] pub struct GetAssertionReply { pub credential: PubKeyCredDescriptor, pub auth_data: AuthData, From add1cebd26312c9b58508a91288131a3dd2821c5 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 18 Feb 2025 10:46:11 +0100 Subject: [PATCH 117/135] tests: Extend getPinToken tests --- tests/basic.rs | 135 +++++++++++++++++++++++++++++++++++++++--- tests/webauthn/mod.rs | 1 + 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index 10e4a6d..e79d4c6 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -103,7 +103,7 @@ fn get_pin_token( pin: &[u8], permissions: u8, rp_id: Option, -) -> PinToken { +) -> Result { use sha2::{Digest as _, Sha256}; let mut hasher = Sha256::new(); @@ -115,9 +115,9 @@ fn get_pin_token( request.pin_hash_enc = Some(pin_hash_enc); request.permissions = Some(permissions); request.rp_id = rp_id; - let reply = device.exec(request).unwrap(); + let reply = device.exec(request)?; let encrypted_pin_token = reply.pin_token.as_ref().unwrap().as_bytes().unwrap(); - shared_secret.decrypt_pin_token(encrypted_pin_token) + Ok(shared_secret.decrypt_pin_token(encrypted_pin_token)) } #[test] @@ -127,7 +127,122 @@ fn test_get_pin_token() { virt::run_ctap2(|device| { let shared_secret = get_shared_secret(&device, &key_agreement_key); set_pin(&device, &key_agreement_key, &shared_secret, pin); - get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + }) +} + +#[test] +fn test_get_pin_token_invalid_pin() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + }) +} + +#[test] +fn test_get_pin_token_invalid_shared_secret() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + // presenting an invalid PIN resets the shared secret so even the correct PIN is not accepted + let result = get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + assert_eq!(result, Err(Ctap2Error(0x31))); + + // requesting a new shared secret fixes the authentication + let shared_secret = get_shared_secret(&device, &key_agreement_key); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + }) +} + +// TODO: simulate reboot and test that PIN_AUTH_BLOCKED is reset +// TODO: simulate reboot and test PIN_BLOCKED + +#[test] +fn test_get_pin_token_pin_auth_blocked() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x34))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + assert_eq!(result, Err(Ctap2Error(0x34))); + }) +} + +#[test] +fn test_get_pin_token_no_pin() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + // TODO: review if this is the correct error code + assert_eq!(result, Err(Ctap2Error(0x35))); }) } @@ -291,7 +406,8 @@ impl Test for TestMakeCredential { pin, pin_token.permissions(0x01, 0x04), pin_token.rp_id(rp_id, invalid_rp_id), - ); + ) + .unwrap(); pin_auth = Some(pin_token.authenticate(client_data_hash)); } } @@ -483,7 +599,8 @@ impl Test for TestListCredentials { set_pin(&device, &key_agreement_key, &shared_secret, pin); let pin_token = - get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None) + .unwrap(); // TODO: client data let client_data_hash = b""; let pin_auth = pin_token.authenticate(client_data_hash); @@ -516,7 +633,8 @@ impl Test for TestListCredentials { ); let pin_token = - get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x04, None); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x04, None) + .unwrap(); let pin_auth = pin_token.authenticate(&[0x02]); let request = CredentialManagement { subcommand: 0x02, @@ -539,7 +657,8 @@ impl Test for TestListCredentials { pin, 0x04, pin_token_rp_id, - ); + ) + .unwrap(); let params = CredentialManagementParams { rp_id_hash: Some(reply.rp_id_hash.unwrap().as_bytes().unwrap().to_owned()), ..Default::default() diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index e0d7e0e..84d06e2 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -99,6 +99,7 @@ impl SharedSecret { } } +#[derive(Debug, PartialEq)] pub struct PinToken([u8; 32]); impl PinToken { From 726ce464beed68a0250f93bce9128ea1a35ea564 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 18 Feb 2025 10:46:11 +0100 Subject: [PATCH 118/135] tests: Add getPinRetries tests --- tests/basic.rs | 98 +++++++++++++++++++++++++++++++++++++++++++ tests/webauthn/mod.rs | 2 + 2 files changed, 100 insertions(+) diff --git a/tests/basic.rs b/tests/basic.rs index e79d4c6..5fbc407 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -120,6 +120,104 @@ fn get_pin_token( Ok(shared_secret.decrypt_pin_token(encrypted_pin_token)) } +fn get_pin_retries(device: &Ctap2) -> u8 { + let reply = device.exec(ClientPin::new(2, 1)).unwrap(); + reply.pin_retries.unwrap() +} + +#[test] +fn test_get_pin_retries() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + assert_eq!(get_pin_retries(&device), 8); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + assert_eq!(get_pin_retries(&device), 8); + + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + assert_eq!(get_pin_retries(&device), 8); + + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + assert_eq!(get_pin_retries(&device), 7); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + assert_eq!(get_pin_retries(&device), 6); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x34))); + assert_eq!(get_pin_retries(&device), 5); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x34))); + assert_eq!(get_pin_retries(&device), 5); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + assert_eq!(result, Err(Ctap2Error(0x34))); + assert_eq!(get_pin_retries(&device), 5); + }) +} + +#[test] +fn test_get_pin_retries_reset() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin); + + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + assert_eq!(get_pin_retries(&device), 7); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + assert_eq!(get_pin_retries(&device), 8); + }) +} + #[test] fn test_get_pin_token() { let key_agreement_key = KeyAgreementKey::generate(); diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 84d06e2..0428032 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -193,6 +193,7 @@ impl Request for ClientPin { pub struct ClientPinReply { pub key_agreement: Option, pub pin_token: Option, + pub pin_retries: Option, } impl From for ClientPinReply { @@ -201,6 +202,7 @@ impl From for ClientPinReply { Self { key_agreement: map.remove(&1), pin_token: map.remove(&2), + pin_retries: map.remove(&3).map(|value| value.deserialized().unwrap()), } } } From fd6fc9b8a88a363607ca2caec5e32cbfcddf4921 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 18 Feb 2025 10:46:11 +0100 Subject: [PATCH 119/135] tests: Extend setPin tests --- tests/basic.rs | 41 +++++++++++++++++++++++++++++------------ tests/webauthn/mod.rs | 2 ++ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index 5fbc407..fc6555b 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -75,7 +75,7 @@ fn set_pin( key_agreement_key: &KeyAgreementKey, shared_secret: &SharedSecret, pin: &[u8], -) { +) -> Result<(), Ctap2Error> { let mut padded_pin = [0; 64]; padded_pin[..pin.len()].copy_from_slice(pin); let pin_enc = shared_secret.encrypt(&padded_pin); @@ -84,15 +84,32 @@ fn set_pin( request.key_agreement = Some(key_agreement_key.public_key()); request.new_pin_enc = Some(pin_enc); request.pin_auth = Some(pin_auth); - device.exec(request).unwrap(); + device.exec(request).map(|_| ()) } #[test] fn test_set_pin() { let key_agreement_key = KeyAgreementKey::generate(); virt::run_ctap2(|device| { + let reply = device.exec(GetInfo).unwrap(); + let options = reply.options.unwrap(); + assert_eq!(options.get("clientPin"), Some(&Value::from(false))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, b"123456").unwrap(); + + let reply = device.exec(GetInfo).unwrap(); + let options = reply.options.unwrap(); + assert_eq!(options.get("clientPin"), Some(&Value::from(true))); + let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, b"123456"); + let result = set_pin(&device, &key_agreement_key, &shared_secret, b"123456"); + // TODO: review error code + assert_eq!(result, Err(Ctap2Error(0x30))); + + let reply = device.exec(GetInfo).unwrap(); + let options = reply.options.unwrap(); + assert_eq!(options.get("clientPin"), Some(&Value::from(true))); }) } @@ -133,7 +150,7 @@ fn test_get_pin_retries() { assert_eq!(get_pin_retries(&device), 8); let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); assert_eq!(get_pin_retries(&device), 8); get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); @@ -199,7 +216,7 @@ fn test_get_pin_retries_reset() { let pin = b"123456"; virt::run_ctap2(|device| { let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); let result = get_pin_token( &device, @@ -224,7 +241,7 @@ fn test_get_pin_token() { let pin = b"123456"; virt::run_ctap2(|device| { let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); }) } @@ -235,7 +252,7 @@ fn test_get_pin_token_invalid_pin() { let pin = b"123456"; virt::run_ctap2(|device| { let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); let result = get_pin_token( &device, &key_agreement_key, @@ -257,7 +274,7 @@ fn test_get_pin_token_invalid_shared_secret() { let pin = b"123456"; virt::run_ctap2(|device| { let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); let result = get_pin_token( &device, &key_agreement_key, @@ -287,7 +304,7 @@ fn test_get_pin_token_pin_auth_blocked() { let pin = b"123456"; virt::run_ctap2(|device| { let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); let result = get_pin_token( &device, &key_agreement_key, @@ -491,12 +508,12 @@ impl Test for TestMakeCredential { PinAuth::PinNoToken => { let key_agreement_key = KeyAgreementKey::generate(); let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); } PinAuth::PinToken(pin_token) => { let key_agreement_key = KeyAgreementKey::generate(); let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); let pin_token = get_pin_token( &device, &key_agreement_key, @@ -694,7 +711,7 @@ impl Test for TestListCredentials { let user_id = b"id123"; virt::run_ctap2(|device| { let shared_secret = get_shared_secret(&device, &key_agreement_key); - set_pin(&device, &key_agreement_key, &shared_secret, pin); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); let pin_token = get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None) diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 0428032..0e6656d 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -789,6 +789,7 @@ impl Request for GetInfo { pub struct GetInfoReply { pub versions: Vec, pub aaguid: Value, + pub options: Option>, pub pin_protocols: Option>, pub attestation_formats: Option>, } @@ -799,6 +800,7 @@ impl From for GetInfoReply { Self { versions: map.remove(&1).unwrap().deserialized().unwrap(), aaguid: map.remove(&3).unwrap().deserialized().unwrap(), + options: map.remove(&4).map(|value| value.deserialized().unwrap()), pin_protocols: map.remove(&6).map(|value| value.deserialized().unwrap()), attestation_formats: map.remove(&0x16).map(|value| value.deserialized().unwrap()), } From f3679b8dd51460515cbd011ad3853f9690e4a14c Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 18 Feb 2025 10:46:11 +0100 Subject: [PATCH 120/135] tests: Add changePin tests --- tests/basic.rs | 84 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index fc6555b..ba79877 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -113,6 +113,83 @@ fn test_set_pin() { }) } +fn get_pin_hash_enc(shared_secret: &SharedSecret, pin: &[u8]) -> Vec { + use sha2::{Digest as _, Sha256}; + + let mut hasher = Sha256::new(); + hasher.update(pin); + let pin_hash = hasher.finalize(); + shared_secret.encrypt(&pin_hash[..16]) +} + +fn change_pin( + device: &Ctap2, + key_agreement_key: &KeyAgreementKey, + shared_secret: &SharedSecret, + old_pin: &[u8], + new_pin: &[u8], +) -> Result<(), Ctap2Error> { + let old_pin_hash_enc = get_pin_hash_enc(shared_secret, old_pin); + let mut padded_new_pin = [0; 64]; + padded_new_pin[..new_pin.len()].copy_from_slice(new_pin); + let new_pin_enc = shared_secret.encrypt(&padded_new_pin); + let mut pin_auth_data = Vec::new(); + pin_auth_data.extend_from_slice(&new_pin_enc); + pin_auth_data.extend_from_slice(&old_pin_hash_enc); + let pin_auth = shared_secret.authenticate(&pin_auth_data); + let mut request = ClientPin::new(2, 4); + request.key_agreement = Some(key_agreement_key.public_key()); + request.pin_hash_enc = Some(old_pin_hash_enc); + request.new_pin_enc = Some(new_pin_enc); + request.pin_auth = Some(pin_auth); + device.exec(request).map(|_| ()) +} + +#[test] +fn test_change_pin() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin1 = b"123456"; + let pin2 = b"654321"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = change_pin(&device, &key_agreement_key, &shared_secret, pin1, pin2); + // TODO: review error code + assert_eq!(result, Err(Ctap2Error(0x35))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin1).unwrap(); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + change_pin(&device, &key_agreement_key, &shared_secret, pin1, pin2).unwrap(); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = change_pin(&device, &key_agreement_key, &shared_secret, pin1, pin2); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin1, + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin2, + 0x01, + None, + ) + .unwrap(); + }) +} + fn get_pin_token( device: &Ctap2, key_agreement_key: &KeyAgreementKey, @@ -121,12 +198,7 @@ fn get_pin_token( permissions: u8, rp_id: Option, ) -> Result { - use sha2::{Digest as _, Sha256}; - - let mut hasher = Sha256::new(); - hasher.update(pin); - let pin_hash = hasher.finalize(); - let pin_hash_enc = shared_secret.encrypt(&pin_hash[..16]); + let pin_hash_enc = get_pin_hash_enc(shared_secret, pin); let mut request = ClientPin::new(2, 9); request.key_agreement = Some(key_agreement_key.public_key()); request.pin_hash_enc = Some(pin_hash_enc); From dfcaf94096b874f695f57853e3b03d0f81ba8194 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 18 Feb 2025 10:46:12 +0100 Subject: [PATCH 121/135] tests: Add getNextAssertion tests --- tests/basic.rs | 111 +++++++++++++++++++++++++++++++++++++++++- tests/webauthn/mod.rs | 17 +++++++ 2 files changed, 126 insertions(+), 2 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index ba79877..b276416 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -3,7 +3,10 @@ pub mod virt; pub mod webauthn; -use std::{collections::BTreeMap, fmt::Debug}; +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Debug, +}; use ciborium::Value; use exhaustive::Exhaustive; @@ -12,7 +15,7 @@ use hex_literal::hex; use virt::{Ctap2, Ctap2Error}; use webauthn::{ AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, - GetAssertion, GetAssertionOptions, GetInfo, KeyAgreementKey, MakeCredential, + GetAssertion, GetAssertionOptions, GetInfo, GetNextAssertion, KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }; @@ -748,6 +751,7 @@ impl Test for TestGetAssertion { response.auth_data.ed_flag(), self.ga_third_party_payment.unwrap_or_default() ); + assert_eq!(response.number_of_credentials, None); credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); if self.ga_third_party_payment.unwrap_or_default() { let extensions = response.auth_data.extensions.unwrap(); @@ -769,6 +773,109 @@ fn test_get_assertion() { TestGetAssertion::run_all(); } +fn run_test_get_next_assertion(device: &Ctap2) { + let rp_id = "example.com"; + // TODO: client data + let client_data_hash = &[0; 32]; + + let rp = Rp::new(rp_id); + let users = vec![User::new(b"id1"), User::new(b"id2"), User::new(b"id3")]; + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + // TODO: test non-discoverable credentials and with allow list + let mut credentials: Vec<_> = users + .into_iter() + .map(|user| { + let mut request = MakeCredential::new( + client_data_hash, + rp.clone(), + user.clone(), + pub_key_cred_params.clone(), + ); + request.options = Some(MakeCredentialOptions::default().rk(true)); + let response = device.exec(request).unwrap(); + response.auth_data.credential.unwrap() + }) + .collect(); + + let credential_ids: BTreeSet<_> = credentials + .iter() + .map(|credential| &credential.id) + .collect(); + assert_eq!(credential_ids.len(), credentials.len()); + + let request = GetAssertion::new(rp_id, client_data_hash); + let response = device.exec(request).unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.auth_data.credential, None); + assert_eq!(response.number_of_credentials, Some(credentials.len())); + let i = credentials + .iter() + .position(|credential| credential.id == response.credential.id) + .unwrap(); + let credential = credentials.remove(i); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + assert!(response.auth_data.extensions.is_none()); + + let response = device.exec(GetNextAssertion).unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.auth_data.credential, None); + // TODO: fix number_of_credentials + // assert_eq!(response.number_of_credentials, Some(credentials.len())); + assert_eq!(response.number_of_credentials, None); + let i = credentials + .iter() + .position(|credential| credential.id == response.credential.id) + .unwrap(); + let credential = credentials.remove(i); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + assert!(response.auth_data.extensions.is_none()); + + let response = device.exec(GetNextAssertion).unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.auth_data.credential, None); + assert_eq!(response.number_of_credentials, None); + let i = credentials + .iter() + .position(|credential| credential.id == response.credential.id) + .unwrap(); + let credential = credentials.remove(i); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + assert!(response.auth_data.extensions.is_none()); + + assert_eq!(credentials, Vec::new()); + + let error = device.exec(GetNextAssertion).unwrap_err(); + assert_eq!(error, Ctap2Error(0x30)); +} + +#[test] +fn test_get_next_assertion() { + virt::run_ctap2(|device| { + run_test_get_next_assertion(&device); + }); +} + +#[test] +fn test_get_next_assertion_multi_rp() { + let client_data_hash = b""; + virt::run_ctap2(|device| { + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + for rp in ["test.com", "something.dev", "else.foobar"] { + for user in [b"john.doe", b"jane.doe"] { + let mut request = MakeCredential::new( + client_data_hash, + Rp::new(rp), + User::new(user), + pub_key_cred_params.clone(), + ); + request.options = Some(MakeCredentialOptions::default().rk(true)); + device.exec(request).unwrap(); + } + } + run_test_get_next_assertion(&device); + }); +} + #[derive(Debug, Exhaustive)] struct TestListCredentials { pin_token_rp_id: bool, diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 0e6656d..3e967f4 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -306,6 +306,7 @@ impl From for User { } } +#[derive(Clone)] pub struct PubKeyCredParam { ty: String, alg: i32, @@ -641,6 +642,7 @@ pub struct GetAssertionReply { pub credential: PubKeyCredDescriptor, pub auth_data: AuthData, pub signature: Vec, + pub number_of_credentials: Option, } impl From for GetAssertionReply { @@ -650,6 +652,7 @@ impl From for GetAssertionReply { credential: map.remove(&0x01).unwrap().into(), auth_data: map.remove(&0x02).unwrap().into(), signature: map.remove(&0x03).unwrap().into_bytes().unwrap(), + number_of_credentials: map.remove(&0x05).map(|value| value.deserialized().unwrap()), } } } @@ -772,6 +775,20 @@ impl CredentialData { } } +pub struct GetNextAssertion; + +impl From for Value { + fn from(_: GetNextAssertion) -> Self { + Self::Null + } +} + +impl Request for GetNextAssertion { + const COMMAND: u8 = 0x08; + + type Reply = GetAssertionReply; +} + pub struct GetInfo; impl From for Value { From 2c8efe16c2c1dbdbf34bd2868d5612c7f5c5da49 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 18 Feb 2025 10:46:12 +0100 Subject: [PATCH 122/135] tests: Inspect filesystem after test runs --- tests/basic.rs | 75 +++++++++++++++++++++++++++++--- tests/cred_mgmt.rs | 101 ++++++++++++++++++++++++++++++++++++++++--- tests/fs/mod.rs | 97 +++++++++++++++++++++++++++++++++++++++++ tests/virt/mod.rs | 105 ++++++++++++++++++++++++++------------------- 4 files changed, 322 insertions(+), 56 deletions(-) create mode 100644 tests/fs/mod.rs diff --git a/tests/basic.rs b/tests/basic.rs index b276416..036a6c2 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,5 +1,6 @@ #![cfg(feature = "dispatch")] +pub mod fs; pub mod virt; pub mod webauthn; @@ -12,7 +13,8 @@ use ciborium::Value; use exhaustive::Exhaustive; use hex_literal::hex; -use virt::{Ctap2, Ctap2Error}; +use fs::list_fs; +use virt::{Ctap2, Ctap2Error, Options}; use webauthn::{ AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, GetAssertion, GetAssertionOptions, GetInfo, GetNextAssertion, KeyAgreementKey, MakeCredential, @@ -51,7 +53,15 @@ fn test_ping() { #[test] fn test_get_info() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(|ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.assert_empty(); + })), + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { let reply = device.exec(GetInfo).unwrap(); assert!(reply.versions.contains(&"FIDO_2_0".to_owned())); assert!(reply.versions.contains(&"FIDO_2_1".to_owned())); @@ -93,7 +103,16 @@ fn set_pin( #[test] fn test_set_pin() { let key_agreement_key = KeyAgreementKey::generate(); - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(|ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + files.assert_empty(); + })), + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { let reply = device.exec(GetInfo).unwrap(); let options = reply.options.unwrap(); assert_eq!(options.get("clientPin"), Some(&Value::from(false))); @@ -576,7 +595,27 @@ impl Test for TestMakeCredential { // TODO: client data let client_data_hash = b""; - virt::run_ctap2(|device| { + let is_rk = self + .options + .and_then(|options| options.rk) + .unwrap_or_default(); + let is_successful = self.expected_error().is_none(); + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.try_remove_state(); + let n = files.try_remove_keys(); + assert!(n <= 2, "n: {n}, files: {files:?}"); + if is_rk && is_successful { + assert_eq!(files.try_remove_rks(), 1, "{files:?}"); + } + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let mut pin_auth = None; match &self.pin_auth { PinAuth::NoPin => {} @@ -850,7 +889,19 @@ fn run_test_get_next_assertion(device: &Ctap2) { #[test] fn test_get_next_assertion() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 4); + assert_eq!(files.try_remove_rks(), 3); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { run_test_get_next_assertion(&device); }); } @@ -858,7 +909,19 @@ fn test_get_next_assertion() { #[test] fn test_get_next_assertion_multi_rp() { let client_data_hash = b""; - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 10); + assert_eq!(files.try_remove_rks(), 9); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; for rp in ["test.com", "something.dev", "else.foobar"] { for user in [b"john.doe", b"jane.doe"] { diff --git a/tests/cred_mgmt.rs b/tests/cred_mgmt.rs index b86c4ac..fc46c6b 100644 --- a/tests/cred_mgmt.rs +++ b/tests/cred_mgmt.rs @@ -1,6 +1,7 @@ #![cfg(feature = "dispatch")] pub mod authenticator; +pub mod fs; pub mod virt; pub mod webauthn; @@ -9,6 +10,7 @@ use std::collections::BTreeSet; use littlefs2::path::PathBuf; use authenticator::{Authenticator, Pin}; +use fs::list_fs; use virt::{Ctap2Error, Options}; use webauthn::{CredentialData, PubKeyCredDescriptor, Rp, User}; @@ -112,7 +114,19 @@ fn generate_user(i: u8) -> User { #[test] fn test_list_credentials() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 11); + assert_eq!(files.try_remove_rks(), 10); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); for i in 0..10 { @@ -127,7 +141,19 @@ fn test_list_credentials() { #[test] fn test_list_credentials_multi() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 11); + assert_eq!(files.try_remove_rks(), 10); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { @@ -144,7 +170,19 @@ fn test_list_credentials_multi() { #[test] fn test_list_credentials_delete() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 9); + assert_eq!(files.try_remove_rks(), 8); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { @@ -164,9 +202,52 @@ fn test_list_credentials_delete() { }) } +#[test] +fn test_list_credentials_delete_all() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 1); + files.remove_empty_dir("fido/dat/rk"); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { + let rp = generate_rp(i); + for j in 0..n { + let user = generate_user(j); + cred_mgmt.make_credential(rp.clone(), user).unwrap(); + } + } + + for _ in 0..10 { + cred_mgmt.delete_credential_at(0).unwrap(); + } + }) +} + #[test] fn test_list_credentials_update_user() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 11); + assert_eq!(files.try_remove_rks(), 10); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { @@ -256,13 +337,17 @@ fn test_max_credential_count() { fn test_filesystem_full() { let mut options = Options { max_resident_credential_count: Some(10), + inspect_ifs: Some(Box::new(|ifs| { + let blocks = ifs.available_blocks().unwrap(); + assert!(blocks < 5, "{blocks}"); + assert!(blocks > 1, "{blocks}"); + })), ..Default::default() }; for i in 0..80 { let path = PathBuf::try_from(format!("/test/{i}").as_str()).unwrap(); options.files.push((path, vec![0; 512])); } - // TODO: inspect filesystem after run and check remaining blocks virt::run_ctap2_with_options(options, |device| { let mut authenticator = Authenticator::new(device).set_pin(b"123456"); let metadata = authenticator.credentials_metadata(); @@ -305,13 +390,17 @@ fn test_filesystem_full() { fn test_filesystem_full_update_user() { let mut options = Options { max_resident_credential_count: Some(10), + inspect_ifs: Some(Box::new(|ifs| { + let blocks = ifs.available_blocks().unwrap(); + assert!(blocks < 5, "{blocks}"); + assert!(blocks > 1, "{blocks}"); + })), ..Default::default() }; for i in 0..80 { let path = PathBuf::try_from(format!("/test/{i}").as_str()).unwrap(); options.files.push((path, vec![0; 512])); } - // TODO: inspect filesystem after run and check remaining blocks virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); diff --git a/tests/fs/mod.rs b/tests/fs/mod.rs new file mode 100644 index 0000000..25dd201 --- /dev/null +++ b/tests/fs/mod.rs @@ -0,0 +1,97 @@ +use std::collections::BTreeMap; + +use littlefs2_core::{path, DynFilesystem, FileType, Path}; + +#[derive(Debug, PartialEq)] +pub enum Entry { + File, + EmptyDir, +} + +#[derive(Debug, Default, PartialEq)] +pub struct Entries(pub BTreeMap); + +impl Entries { + pub fn remove_standard(&mut self) { + self.remove_file("fido/sec/00"); + self.remove_file("fido/x5c/00"); + self.remove_file("trussed/dat/rng-state.bin"); + } + + pub fn remove_state(&mut self) { + self.remove_file("fido/dat/persistent-state.cbor"); + } + + pub fn try_remove_state(&mut self) { + self.0.remove("fido/dat/persistent-state.cbor"); + } + + pub fn try_remove_keys(&mut self) -> usize { + self.try_remove_dir("fido/sec") + } + + pub fn try_remove_rks(&mut self) -> usize { + let n = self.0.len(); + self.0.retain(|path, _| { + let (start, _) = path.rsplit_once('/').unwrap(); + let start = start.rsplit_once('/').map(|(start, _)| start); + start != Some("fido/dat/rk") + }); + n - self.0.len() + } + + pub fn try_remove_dir(&mut self, dir: &str) -> usize { + let n = self.0.len(); + self.0.retain(|path, _| { + let (start, _) = path.rsplit_once('/').unwrap(); + start != dir + }); + n - self.0.len() + } + + pub fn remove_file(&mut self, path: &str) { + let entry = self.0.remove(path); + assert_eq!(entry, Some(Entry::File), "{path}"); + } + + pub fn remove_empty_dir(&mut self, path: &str) { + let entry = self.0.remove(path); + assert_eq!(entry, Some(Entry::EmptyDir), "{path}"); + } + + pub fn assert_empty(&self) { + assert_eq!(self.0, Default::default()); + } +} + +pub fn list_fs(fs: &dyn DynFilesystem) -> Entries { + fn list_dir(fs: &dyn DynFilesystem, dir: &Path, files: &mut BTreeMap) -> usize { + fs.read_dir_and_then(dir, &mut |iter| { + let mut child_count = 0; + for entry in iter { + let entry = entry.unwrap(); + if entry.file_name().as_str() == "." || entry.file_name().as_str() == ".." { + continue; + } + child_count += 1; + match entry.file_type() { + FileType::File => { + files.insert(entry.path().as_str().to_owned(), Entry::File); + } + FileType::Dir => { + let n = list_dir(fs, entry.path(), files); + if n == 0 { + files.insert(entry.path().as_str().to_owned(), Entry::EmptyDir); + } + } + } + } + Ok(child_count) + }) + .unwrap() + } + + let mut entries = BTreeMap::new(); + list_dir(fs, path!(""), &mut entries); + Entries(entries) +} diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 84439ef..f78c372 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -4,6 +4,7 @@ use std::{ borrow::Cow, cell::RefCell, fmt::{self, Debug, Formatter}, + ops::Deref as _, sync::{ atomic::{AtomicBool, Ordering}, Arc, Once, @@ -20,7 +21,7 @@ use ctaphid::{ }; use ctaphid_dispatch::{Channel, Dispatch, Requester}; use fido_authenticator::{Authenticator, Config, Conforming}; -use littlefs2::{path, path::PathBuf}; +use littlefs2::{object_safe::DynFilesystem, path, path::PathBuf}; use rand::{ distributions::{Distribution, Uniform}, RngCore as _, @@ -62,45 +63,53 @@ where let mut files = options.files; files.push((path!("fido/x5c/00").into(), ATTESTATION_CERT.into())); files.push((path!("fido/sec/00").into(), ATTESTATION_KEY.into())); - with_client(&files, |client| { - let mut authenticator = Authenticator::new( - client, - Conforming {}, - Config { - max_msg_size: 0, - skip_up_timeout: None, - max_resident_credential_count: options.max_resident_credential_count, - large_blobs: None, - nfc_transport: false, - }, - ); - - let channel = Channel::new(); - let (rq, rp) = channel.split().unwrap(); - - thread::scope(|s| { - let stop = Arc::new(AtomicBool::new(false)); - let poller_stop = stop.clone(); - let poller = s.spawn(move || { - let mut dispatch = Dispatch::new(rp); - while !poller_stop.load(Ordering::Relaxed) { - dispatch.poll(&mut [&mut authenticator]); - thread::sleep(Duration::from_millis(1)); - } - }); - - let runner = s.spawn(move || { - let device = Device::new(rq); - let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); - f(device) - }); - - let result = runner.join(); - stop.store(true, Ordering::Relaxed); - poller.join().unwrap(); - result.unwrap() - }) - }) + with_client( + &files, + |client| { + let mut authenticator = Authenticator::new( + client, + Conforming {}, + Config { + max_msg_size: 0, + skip_up_timeout: None, + max_resident_credential_count: options.max_resident_credential_count, + large_blobs: None, + nfc_transport: false, + }, + ); + + let channel = Channel::new(); + let (rq, rp) = channel.split().unwrap(); + + thread::scope(|s| { + let stop = Arc::new(AtomicBool::new(false)); + let poller_stop = stop.clone(); + let poller = s.spawn(move || { + let mut dispatch = Dispatch::new(rp); + while !poller_stop.load(Ordering::Relaxed) { + dispatch.poll(&mut [&mut authenticator]); + thread::sleep(Duration::from_millis(1)); + } + }); + + let runner = s.spawn(move || { + let device = Device::new(rq); + let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); + f(device) + }); + + let result = runner.join(); + stop.store(true, Ordering::Relaxed); + poller.join().unwrap(); + result.unwrap() + }) + }, + |ifs| { + if let Some(inspect_ifs) = options.inspect_ifs { + inspect_ifs(ifs); + } + }, + ) } pub fn run_ctap2(f: F) -> T @@ -119,10 +128,13 @@ where run_ctaphid_with_options(options, |device| f(Ctap2(device))) } -#[derive(Debug, Default)] +pub type InspectFsFn = Box; + +#[derive(Default)] pub struct Options { pub files: Vec<(PathBuf, Vec)>, pub max_resident_credential_count: Option, + pub inspect_ifs: Option, } pub struct Ctap2<'a>(ctaphid::Device>); @@ -235,9 +247,10 @@ impl HidDevice for Device<'_> { } } -fn with_client(files: &[(PathBuf, Vec)], f: F) -> T +fn with_client(files: &[(PathBuf, Vec)], f: F, inspect_ifs: F2) -> T where F: FnOnce(Client) -> T, + F2: FnOnce(&dyn DynFilesystem), { virt::with_platform(Ram::default(), |mut platform| { // virt always uses the same seed -- request some random bytes to reach a somewhat random @@ -257,7 +270,7 @@ where ifs.write(path, content).unwrap(); } - platform.run_client_with_backends( + let result = platform.run_client_with_backends( "fido", Dispatcher::default(), &[ @@ -265,6 +278,10 @@ where BackendId::Core, ], f, - ) + ); + + inspect_ifs(ifs.deref()); + + result }) } From fed17e9b35be3cddc5029ecb6bcb5ea79f120993 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 19 Feb 2025 10:50:54 +0100 Subject: [PATCH 123/135] tests: Remove exhaustive dependency --- Cargo.toml | 2 +- tests/basic.rs | 92 +++++++++++++++++++++++++++++++++++++------ tests/webauthn/mod.rs | 54 +++++++++++++++++++++++-- 3 files changed, 131 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d0a17b5..d3b2de7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,10 +62,10 @@ ctaphid = { version = "0.3.1", default-features = false } ctaphid-dispatch = "0.2" delog = { version = "0.1.6", features = ["std-log"] } env_logger = "0.11.0" -exhaustive = "0.2.2" hex-literal = "0.4.1" hmac = "0.12.1" interchange = "0.3.0" +itertools = "0.14.0" littlefs2 = "0.5.0" log = "0.4.21" p256 = { version = "0.13.2", features = ["ecdh"] } diff --git a/tests/basic.rs b/tests/basic.rs index 036a6c2..03ca683 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -10,16 +10,16 @@ use std::{ }; use ciborium::Value; -use exhaustive::Exhaustive; use hex_literal::hex; +use itertools::iproduct; use fs::list_fs; use virt::{Ctap2, Ctap2Error, Options}; use webauthn::{ - AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, - GetAssertion, GetAssertionOptions, GetInfo, GetNextAssertion, KeyAgreementKey, MakeCredential, - MakeCredentialOptions, PinToken, PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, - SharedSecret, User, + exhaustive_struct, AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, + Exhaustive, ExtensionsInput, GetAssertion, GetAssertionOptions, GetInfo, GetNextAssertion, + KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, + PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }; trait Test: Debug { @@ -38,7 +38,7 @@ trait Test: Debug { where Self: Exhaustive, { - for test in Self::iter_exhaustive(None) { + for test in Self::iter_exhaustive() { test.run(); } } @@ -455,7 +455,7 @@ fn test_get_pin_token_no_pin() { }) } -#[derive(Clone, Debug, Exhaustive)] +#[derive(Clone, Copy, Debug)] enum RequestPinToken { InvalidPermissions, InvalidRpId, @@ -482,7 +482,19 @@ impl RequestPinToken { } } -#[derive(Clone, Copy, Debug, Exhaustive)] +impl Exhaustive for RequestPinToken { + fn iter_exhaustive() -> impl Iterator + Clone { + [ + Self::InvalidPermissions, + Self::InvalidRpId, + Self::NoRpId, + Self::ValidRpId, + ] + .into_iter() + } +} + +#[derive(Clone, Copy, Debug)] enum AttestationFormatsPreference { Empty, None, @@ -539,14 +551,37 @@ impl From for Vec<&'static str> { } } -#[derive(Debug, Exhaustive)] +impl Exhaustive for AttestationFormatsPreference { + fn iter_exhaustive() -> impl Iterator + Clone { + [ + Self::Empty, + Self::None, + Self::Packed, + Self::NonePacked, + Self::PackedNone, + Self::OtherNonePacked, + Self::MultiOtherNonePacked, + ] + .into_iter() + } +} + +#[derive(Clone, Copy, Debug)] enum PinAuth { NoPin, PinNoToken, PinToken(RequestPinToken), } -#[derive(Debug, Exhaustive)] +impl Exhaustive for PinAuth { + fn iter_exhaustive() -> impl Iterator + Clone { + [Self::NoPin, Self::PinNoToken] + .into_iter() + .chain(RequestPinToken::iter_exhaustive().map(Self::PinToken)) + } +} + +#[derive(Clone, Debug)] struct TestMakeCredential { pin_auth: PinAuth, options: Option, @@ -700,12 +735,24 @@ impl Test for TestMakeCredential { } } +impl Exhaustive for TestMakeCredential { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + pin_auth: PinAuth, + options: Option, + valid_pub_key_alg: bool, + attestation_formats_preference: Option, + hmac_secret: bool, + } + } +} + #[test] fn test_make_credential() { TestMakeCredential::run_all(); } -#[derive(Debug, Exhaustive)] +#[derive(Clone, Debug)] struct TestGetAssertion { rk: bool, allow_list: bool, @@ -807,6 +854,18 @@ impl Test for TestGetAssertion { } } +impl Exhaustive for TestGetAssertion { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + rk: bool, + allow_list: bool, + options: Option, + mc_third_party_payment: Option, + ga_third_party_payment: Option, + } + } +} + #[test] fn test_get_assertion() { TestGetAssertion::run_all(); @@ -939,7 +998,7 @@ fn test_get_next_assertion_multi_rp() { }); } -#[derive(Debug, Exhaustive)] +#[derive(Clone, Debug)] struct TestListCredentials { pin_token_rp_id: bool, third_party_payment: Option, @@ -1041,6 +1100,15 @@ impl Test for TestListCredentials { } } +impl Exhaustive for TestListCredentials { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + pin_token_rp_id: bool, + third_party_payment: Option, + } + } +} + #[test] fn test_list_credentials() { TestListCredentials::run_all(); diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 3e967f4..f780e4d 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -1,13 +1,40 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, iter}; use ciborium::Value; use cipher::{BlockDecryptMut as _, BlockEncryptMut as _, KeyIvInit}; -use exhaustive::Exhaustive; use hmac::Mac; +use itertools::iproduct; use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey}; use rand::RngCore as _; use serde::Deserialize; +pub trait Exhaustive: Sized { + fn iter_exhaustive() -> impl Iterator + Clone; +} + +impl Exhaustive for bool { + fn iter_exhaustive() -> impl Iterator + Clone { + [false, true].into_iter() + } +} + +impl Exhaustive for Option { + fn iter_exhaustive() -> impl Iterator + Clone { + iter::once(None).chain(T::iter_exhaustive().map(Some)) + } +} + +macro_rules! exhaustive_struct { + ($($field:ident: $type:ty,)*) => { + iproduct!( + $(<$type as Exhaustive>::iter_exhaustive(),)* + ) + .map(|($($field,)*)| Self { $($field,)* }) + } +} + +pub(crate) use exhaustive_struct; + pub struct KeyAgreementKey(p256::ecdh::EphemeralSecret); impl KeyAgreementKey { @@ -419,7 +446,7 @@ impl From for Value { } } -#[derive(Clone, Copy, Debug, Default, Exhaustive)] +#[derive(Clone, Copy, Debug, Default)] pub struct MakeCredentialOptions { pub rk: Option, pub up: Option, @@ -459,6 +486,16 @@ impl From for Value { } } +impl Exhaustive for MakeCredentialOptions { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + rk: Option, + up: Option, + uv: Option, + } + } +} + impl Request for MakeCredential { const COMMAND: u8 = 0x01; @@ -606,7 +643,7 @@ impl Request for GetAssertion { type Reply = GetAssertionReply; } -#[derive(Clone, Copy, Debug, Default, Exhaustive)] +#[derive(Clone, Copy, Debug, Default)] pub struct GetAssertionOptions { pub up: Option, pub uv: Option, @@ -637,6 +674,15 @@ impl From for Value { } } +impl Exhaustive for GetAssertionOptions { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + up: Option, + uv: Option, + } + } +} + #[derive(Debug, PartialEq)] pub struct GetAssertionReply { pub credential: PubKeyCredDescriptor, From 6f260ea49a141675f78b84b00eaffc1f77fabffe Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 19 Feb 2025 12:03:35 +0100 Subject: [PATCH 124/135] tests: Set opt-level to 2 --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index d3b2de7..032b733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,3 +86,6 @@ features = ["dispatch"] trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "1e1ca03a3a62ea9b802f4070ea4bce002eeb4bec" } trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "4fe4e4e287dac1d92fcd4f97e8926497bfa9d7a9" } + +[profile.test] +opt-level = 2 From 443eca178726f6de29ec86cbf70a92f47d385853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 2 Feb 2024 16:19:31 +0100 Subject: [PATCH 125/135] Make credential: change the path of rks to `rp_id_hash.credential_id_hash` from `rp_id_hash/credential_id_hash` The goal is to make credential storage more efficient, by making use of littlefs's ability to inline file contents into the directory metadata when the file is small. --- CHANGELOG.md | 2 + Cargo.toml | 2 + src/ctap2.rs | 217 +++++++++----- src/ctap2/credential_management.rs | 439 ++++++++++++++--------------- src/lib.rs | 27 +- src/state.rs | 23 +- src/state/migrate.rs | 267 ++++++++++++++++++ tests/cred_mgmt.rs | 43 ++- tests/fs/mod.rs | 8 +- 9 files changed, 696 insertions(+), 332 deletions(-) create mode 100644 src/state/migrate.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c120f..5886418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update dependencies: - Replace `trussed` dependency with `trussed-core` - Replace `ctaphid-dispatch` dependeny with `ctaphid-app` +- Remove the per-relying party directory to save space ([#55][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -44,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#63]: https://github.com/Nitrokey/fido-authenticator/pull/63 [#52]: https://github.com/Nitrokey/fido-authenticator/issues/52 [#59]: https://github.com/Nitrokey/fido-authenticator/issues/59 +[#55]: https://github.com/Nitrokey/fido-authenticator/issues/55 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/Cargo.toml b/Cargo.toml index 032b733..9222962 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ log-warn = [] log-error = [] [dev-dependencies] +admin-app = { version = "0.1.0", features = ["migration-tests"] } aes = "0.8.4" cbc = { version = "0.1.2", features = ["alloc"] } ciborium = { version = "0.2.2" } @@ -83,6 +84,7 @@ x509-parser = "0.16.0" features = ["dispatch"] [patch.crates-io] +admin-app = { git = "https://github.com/Nitrokey/admin-app.git", tag = "v0.1.0-nitrokey.19" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "1e1ca03a3a62ea9b802f4070ea4bce002eeb4bec" } trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "4fe4e4e287dac1d92fcd4f97e8926497bfa9d7a9" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 244cea7..04d9dd8 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -394,7 +394,7 @@ impl Authenticator for crate::Authenti .ok(); let mut key_store_full = self.can_fit(serialized_credential.len()) == Some(false) - || CredentialManagement::new(self).count_credentials() + || CredentialManagement::new(self).count_credentials()? >= self .config .max_resident_credential_count @@ -415,11 +415,6 @@ impl Authenticator for crate::Authenti } if key_store_full { - // If we previously deleted an existing cred with the same RP + UserId but then - // failed to store the new cred, the RP directory could now be empty. This is not - // a valid state so we have to delete it. - let rp_dir = rp_rk_dir(&rp_id_hash); - self.delete_rp_dir_if_empty(rp_dir); return Err(Error::KeyStoreFull); } } @@ -916,7 +911,7 @@ impl Authenticator for crate::Authenti // TODO: use custom enum of known commands match parameters.sub_command { // 0x1 - Subcommand::GetCredsMetadata => Ok(cred_mgmt.get_creds_metadata()), + Subcommand::GetCredsMetadata => cred_mgmt.get_creds_metadata(), // 0x2 Subcommand::EnumerateRpsBegin => cred_mgmt.first_relying_party(), @@ -1013,7 +1008,7 @@ impl Authenticator for crate::Authenti // 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) + .prepare_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed)? .ok_or(Error::NoCredentials)?; info_now!("found {:?} applicable credentials", num_credentials); @@ -1154,7 +1149,7 @@ impl crate::Authenticator { rp_id_hash: &[u8; 32], allow_list: &Option, uv_performed: bool, - ) -> Option<(Credential, u32)> { + ) -> Result> { debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); self.state.runtime.clear_credential_cache(); @@ -1188,50 +1183,76 @@ impl crate::Authenticator { continue; } - return Some((credential, 1)); + return Ok(Some((credential, 1))); } // we don't recognize any credentials in the allowlist - return None; + return Ok(None); } } // we are only dealing with discoverable credentials. debug_now!("Allowlist not passed, fetching RKs"); + self.prepare_cache(rp_id_hash, uv_performed)?; - let mut maybe_path = - syscall!(self - .trussed - .read_dir_first(Location::Internal, rp_rk_dir(rp_id_hash), None,)) - .entry - .map(|entry| PathBuf::from(entry.path())); + let num_credentials = self.state.runtime.remaining_credentials(); + let credential = self.state.runtime.pop_credential(&mut self.trussed); + Ok(credential.map(|credential| (Credential::Full(credential), num_credentials))) + } + /// Populate the cache with the RP credentials. + #[inline(never)] + fn prepare_cache(&mut self, rp_id_hash: &[u8; 32], uv_performed: bool) -> Result<()> { 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 file_name_prefix = rp_file_name_prefix(rp_id_hash); + let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + PathBuf::from(RK_DIR), + Some(file_name_prefix.clone()) + )) + .entry; + + while let Some(entry) = maybe_entry.take() { + if !entry + .file_name() + .as_ref() + .starts_with(file_name_prefix.as_ref()) + { + // We got past all credentials for the relevant RP + break; + } + + if entry.file_name() == &*file_name_prefix { + debug_assert!(entry.metadata().is_dir()); + error!("Migration missing"); + return Err(Error::Other); + } - let credential = FullCredential::deserialize(&credential_data).ok()?; + let credential_data = syscall!(self + .trussed + .read_file(Location::Internal, entry.path().into(),)) + .data; + + let credential = FullCredential::deserialize(&credential_data).map_err(|_err| { + error!("Failed to deserialize credential: {_err:?}"); + Error::Other + })?; let timestamp = credential.creation_time; let credential = Credential::Full(credential); if self.check_credential_applicable(&credential, false, uv_performed) { self.state.runtime.push_credential(CachedCredential { timestamp, - path: String::from_str(path.as_str_ref_with_trailing_nul()).ok()?, + path: String::from_str(entry.path().as_str_ref_with_trailing_nul()) + .map_err(|_| Error::Other)?, }); } - maybe_path = syscall!(self.trussed.read_dir_next()) - .entry - .map(|entry| PathBuf::from(entry.path())); + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; } - - let num_credentials = self.state.runtime.remaining_credentials(); - let credential = self.state.runtime.pop_credential(&mut self.trussed); - credential.map(|credential| (Credential::Full(credential), num_credentials)) + Ok(()) } fn decrypt_pin_hash_and_maybe_escalate( @@ -1774,20 +1795,32 @@ impl crate::Authenticator { 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,)) + let file_name_prefix = rp_file_name_prefix(rp_id_hash); + let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + PathBuf::from(RK_DIR), + Some(file_name_prefix.clone()) + )) .entry; - loop { + while let Some(entry) = maybe_entry.take() { + if !entry + .file_name() + .as_ref() + .starts_with(file_name_prefix.as_ref()) + { + // We got past all credentials for the relevant RP + break; + } + + if entry.file_name() == &*file_name_prefix { + debug_assert!(entry.metadata().is_dir()); + error!("Migration missing"); + return Err(Error::Other); + } + info_now!("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()), - }; + let rk_path = PathBuf::from(entry.path()); info_now!("checking RK {:?} for userId ", &rk_path); let credential_data = @@ -1815,7 +1848,7 @@ impl crate::Authenticator { } // prepare for next loop iteration - entry = syscall!(self.trussed.read_dir_next()).entry; + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; } Ok(()) @@ -1853,25 +1886,6 @@ impl crate::Authenticator { Ok(()) } - pub(crate) fn delete_rp_dir_if_empty(&mut self, rp_path: PathBuf) { - let maybe_first_remaining_rk = - syscall!(self - .trussed - .read_dir_first(Location::Internal, rp_path.clone(), None,)) - .entry; - - if let Some(_first_remaining_rk) = maybe_first_remaining_rk { - info!( - "not deleting deleting parent {:?} as there is {:?}", - &rp_path, - &_first_remaining_rk.path(), - ); - } else { - info!("deleting parent {:?} as this was its last RK", &rp_path); - try_syscall!(self.trussed.remove_dir(Location::Internal, rp_path,)).ok(); - } - } - fn large_blobs_get( &mut self, request: &ctap2::large_blobs::Request, @@ -2068,23 +2082,80 @@ impl TryFrom for SupportedAttestationFormat { } } -fn rp_rk_dir(rp_id_hash: &[u8; 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(RK_DIR); - dir.push(&PathBuf::try_from(&hex).unwrap()); +// The new path scheme for disvoerable credentials (= resident keys) is: +// rk/. +// The hashes are truncated to the first eight bytes and formatted as hex strings. +// We use the following terms for the components: +// rk_path: rk/. +// rp_file_name_prefix: - dir +fn rp_file_name_prefix(rp_id_hash: &[u8; 32]) -> PathBuf { + let mut hex = [b'0'; 16]; + super::format_hex(&rp_id_hash[..8], &mut hex); + PathBuf::try_from(&hex).unwrap() } fn rk_path(rp_id_hash: &[u8; 32], credential_id_hash: &[u8; 32]) -> PathBuf { - let mut path = rp_rk_dir(rp_id_hash); + // 16 bytes per hash + dot + trailing zero = 34 + let mut buf = [0; 34]; + buf[16] = b'.'; + format_hex(&rp_id_hash[..8], &mut buf[..16]); + format_hex(&credential_id_hash[..8], &mut buf[17..33]); + + let mut path = PathBuf::from(RK_DIR); + path.push(Path::from_bytes_with_nul(&buf).unwrap()); + path +} - let mut hex = [0u8; 16]; - format_hex(&credential_id_hash[..8], &mut hex); - path.push(&PathBuf::try_from(&hex).unwrap()); +#[cfg(test)] +mod tests { + use super::{rk_path, rp_file_name_prefix}; - path + const TEST_HASH: &[u8; 32] = &[ + 134, 54, 157, 96, 10, 28, 233, 79, 219, 59, 195, 125, 165, 251, 120, 14, 49, 152, 212, 191, + 114, 137, 180, 207, 255, 177, 187, 106, 173, 1, 203, 171, + ]; + const TEST_HASH_HEX: &str = "86369d600a1ce94f"; + + #[test] + fn test_rp_file_name_prefix() { + assert_eq!(rp_file_name_prefix(&[0; 32]).as_str(), "0000000000000000"); + assert_eq!(rp_file_name_prefix(TEST_HASH).as_str(), TEST_HASH_HEX); + } + + #[test] + fn test_rk_path() { + fn test(rp_id_hash: &[u8; 32], credential_id_hash: &[u8; 32], expected: &str) { + println!("rp_id_hash: {rp_id_hash:?}"); + println!("credential_id_hash: {credential_id_hash:?}"); + let actual = rk_path(rp_id_hash, credential_id_hash); + assert_eq!(actual.as_str(), expected); + } + + let input_zero = &[0; 32]; + let output_zero = "0000000000000000"; + let input_nonzero = TEST_HASH; + let output_nonzero = TEST_HASH_HEX; + + test( + input_zero, + input_zero, + &format!("rk/{output_zero}.{output_zero}"), + ); + test( + input_zero, + input_nonzero, + &format!("rk/{output_zero}.{output_nonzero}"), + ); + test( + input_nonzero, + input_zero, + &format!("rk/{output_nonzero}.{output_zero}"), + ); + test( + input_nonzero, + input_nonzero, + &format!("rk/{output_nonzero}.{output_nonzero}"), + ); + } } diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index e29449e..273e159 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -1,6 +1,6 @@ //! TODO: T -use core::{cmp, convert::TryFrom}; +use core::{cmp, convert::TryFrom, num::NonZeroU32}; use littlefs2_core::{Path, PathBuf}; use trussed_core::{ @@ -58,16 +58,28 @@ where } } +/// Get the hex hashed ID of the RP from the filename of a RP directory OR a "new" RK path +fn get_rp_id_hex(entry: &DirEntry) -> &str { + get_rp_id_hex_from_file_name(entry.file_name().as_str()) +} + +fn get_rp_id_hex_from_file_name(file_name: &str) -> &str { + file_name + .split('.') + .next() + .expect("Split always returns at least one empty string") +} + impl CredentialManagement<'_, UP, T> where UP: UserPresence, T: TrussedRequirements, { - pub fn get_creds_metadata(&mut self) -> Response { + pub fn get_creds_metadata(&mut self) -> Result { info!("get metadata"); let mut response: Response = Default::default(); - let credential_count = self.count_credentials(); + let credential_count = self.count_credentials()?; // We have a fixed limit determined by the configuration and an estimated limit determined // by the available space on the filesystem. The effective limit is the lower of the two. let max_remaining = self @@ -81,123 +93,96 @@ where response.max_possible_remaining_residential_credentials_count = Some(cmp::min(max_remaining, estimate_remaining)); - response + Ok(response) } - pub fn count_credentials(&mut self) -> u32 { + pub fn count_credentials(&mut self) -> Result { let dir = PathBuf::from(RK_DIR); - let maybe_first_rp = + let mut num_rks = 0; + + let mut maybe_next = syscall!(self .trussed .read_dir_first(Location::Internal, dir.clone(), None)) .entry; - let first_rp = match maybe_first_rp { - None => return 0, - Some(rp) => rp, - }; - - let (mut num_rks, _) = self.count_rp_rks(PathBuf::from(first_rp.path())); - 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(); - let maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; - - match maybe_next_rp { - None => { - return num_rks; - } - Some(rp) => { - last_rp = PathBuf::from(rp.file_name()); - info!("counting.."); - let (this_rp_rk_count, _) = self.count_rp_rks(PathBuf::from(rp.path())); - info!("{:?}", this_rp_rk_count); - num_rks += this_rp_rk_count; - } + while let Some(rp) = maybe_next { + if rp.metadata().is_dir() { + error!("Migration not complete"); + return Err(Error::Other); } + + num_rks += 1; + maybe_next = syscall!(self.trussed.read_dir_next()).entry; } + + Ok(num_rks) } pub fn first_relying_party(&mut self) -> Result { info!("first rp"); - // rp (0x03): PublicKeyCredentialRpEntity - // rpIDHash (0x04) : RP ID SHA-256 hash. - // totalRPs (0x05) : Total number of RPs present on the authenticator. - - let mut response: Response = Default::default(); - + let mut response = Response::default(); let dir = PathBuf::from(RK_DIR); 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; - match maybe_next_rp { - None => break, - _ => num_rps += 1, - } - } - num_rps - } - }); - - 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 - )) + syscall!(self + .trussed + .read_dir_first(Location::Internal, dir.clone(), None)) .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 Some(first_rp) = maybe_first_rp else { + response.total_rps = Some(0); + return Ok(response); + }; - let credential = FullCredential::deserialize(&serialized) - // this may be a confusing error message - .map_err(|_| Error::InvalidCredential)?; + // The first one counts + let mut total_rps = 1; - let rp = credential.data.rp; + if first_rp.metadata().is_dir() { + warn!("Migration did not finish"); + return Err(Error::Other); + } - response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id().as_ref()))); - response.rp = Some(rp.into()); - } + let first_credential_data = syscall!(self + .trussed + .read_file(Location::Internal, first_rp.path().into())) + .data; + + let credential = FullCredential::deserialize(&first_credential_data)?; + let rp_id_hash: [u8; 32] = syscall!(self.trussed.hash_sha256(credential.rp.id().as_ref())) + .hash + .as_slice() + .try_into() + .map_err(|_| Error::Other)?; + + let mut current_rp = first_rp; + + let mut current_id_hex = get_rp_id_hex(¤t_rp); + + while let Some(entry) = syscall!(self.trussed.read_dir_next()).entry { + let id_hex = get_rp_id_hex(&entry); + if id_hex != current_id_hex { + total_rps += 1; + current_rp = entry; + current_id_hex = get_rp_id_hex(¤t_rp) } + } - // cache state for next call - if let Some(total_rps) = response.total_rps { - if total_rps > 1 { - let rp_id_hash = response.rp_id_hash.unwrap().into_array(); - self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { - remaining: total_rps - 1, - rp_id_hash, - }); - } - } + if let Some(remaining) = NonZeroU32::new(total_rps - 1) { + self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { + remaining, + rp_id_hash, + }); } + response.total_rps = Some(total_rps); + response.rp_id_hash = Some(ByteArray::new(rp_id_hash)); + response.rp = Some(credential.data.rp.into()); Ok(response) } pub fn next_relying_party(&mut self) -> Result { - info!("next rp"); - let CredentialManagementEnumerateRps { remaining, rp_id_hash: last_rp_id_hash, @@ -208,90 +193,67 @@ where .clone() .ok_or(Error::NotAllowed)?; + let filename = super::rp_file_name_prefix(&last_rp_id_hash); + let dir = PathBuf::from(RK_DIR); - let mut hex = [b'0'; 16]; - super::format_hex(&last_rp_id_hash[..8], &mut hex); - let filename = PathBuf::try_from(&hex).unwrap(); + let maybe_next_rp = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + dir, + Some(filename.clone()) + )) + .entry; - let mut maybe_next_rp = - syscall!(self - .trussed - .read_dir_first(Location::Internal, dir, Some(filename),)) - .entry; + let mut response = Response::default(); - // Advance to the next - if maybe_next_rp.is_some() { - maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; - } else { + let Some(current_rp) = maybe_next_rp else { return Err(Error::NotAllowed); - } + }; - let mut response: Response = Default::default(); + let current_id_hex = get_rp_id_hex(¤t_rp); - if let Some(rp) = maybe_next_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; + debug_assert!(current_rp + .file_name() + .as_str() + .starts_with(filename.as_str())); - 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 credential = FullCredential::deserialize(&serialized) - // this may be a confusing error message - .map_err(|_| Error::InvalidCredential)?; - - let rp = credential.data.rp; - - response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id().as_ref()))); - response.rp = Some(rp.into()); - - // cache state for next call - if remaining > 1 { - let rp_id_hash = response.rp_id_hash.unwrap().into_array(); - self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { - remaining: remaining - 1, - rp_id_hash, - }); - } else { - self.state.runtime.cached_rp = None; - } - } + while let Some(entry) = syscall!(self.trussed.read_dir_next()).entry { + let id_hex = get_rp_id_hex(&entry); + if id_hex == current_id_hex { + continue; } - } else { - self.state.runtime.cached_rp = None; - } - Ok(response) - } + if entry.metadata().is_dir() { + warn!("While iterating: migration is not finished"); + return Err(Error::Other); + } - fn count_rp_rks(&mut self, rp_dir: PathBuf) -> (u32, Option) { - let maybe_first_rk = - syscall!(self + let data = syscall!(self .trussed - .read_dir_first(Location::Internal, rp_dir, None)) - .entry; - - let Some(first_rk) = maybe_first_rk else { - warn!("empty RP directory"); - return (0, None); - }; + .read_file(Location::Internal, entry.path().into())) + .data; + + let credential = FullCredential::deserialize(&data)?; + let rp_id_hash: [u8; 32] = + syscall!(self.trussed.hash_sha256(credential.rp.id().as_ref())) + .hash + .as_slice() + .try_into() + .map_err(|_| Error::Other)?; + response.rp_id_hash = Some(ByteArray::new(rp_id_hash)); + response.rp = Some(credential.data.rp.into()); + + if let Some(new_remaining) = NonZeroU32::new(remaining.get() - 1) { + self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { + remaining: new_remaining, + rp_id_hash, + }); + } - // count the rest of them - let mut num_rks = 1; - while syscall!(self.trussed.read_dir_next()).entry.is_some() { - num_rks += 1; + return Ok(response); } - (num_rks, Some(first_rk)) + + Err(Error::NotAllowed) } pub fn first_credential(&mut self, rp_id_hash: &[u8; 32]) -> Result { @@ -299,11 +261,42 @@ where self.state.runtime.cached_rk = None; - let mut hex = [b'0'; 16]; - super::format_hex(&rp_id_hash[..8], &mut hex); + let rp_dir_start = super::rp_file_name_prefix(rp_id_hash); + + let mut num_rks = 0; + + let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + PathBuf::from(RK_DIR), + Some(rp_dir_start.clone()) + )) + .entry; + + let mut first_rk = None; + + while let Some(entry) = maybe_entry { + if !entry + .file_name() + .as_str() + .starts_with(rp_dir_start.as_str()) + { + // We got past all credentials for the relevant RP + break; + } + + if entry.file_name() == &*rp_dir_start { + // This is the case where we + debug_assert!(entry.metadata().is_dir()); + error!("Migration did not run"); + return Err(Error::Other); + } + + first_rk = first_rk.or(Some(entry)); + num_rks += 1; + + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; + } - let rp_dir = PathBuf::from(RK_DIR).join(&PathBuf::try_from(&hex).unwrap()); - let (num_rks, first_rk) = self.count_rp_rks(rp_dir); let first_rk = first_rk.ok_or(Error::NoCredentials)?; // extract data required into response @@ -311,15 +304,12 @@ where response.total_credentials = Some(num_rks); // cache state for next call - 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.cached_rk = Some(CredentialManagementEnumerateCredentials { - remaining: num_rks - 1, - rp_dir: first_rk.path().parent().unwrap(), - prev_filename: PathBuf::from(first_rk.file_name()), - }); - } + if num_rks > 1 { + // let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); + self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { + remaining: num_rks - 1, + prev_filename: first_rk.file_name().into(), + }); } Ok(response) @@ -328,60 +318,52 @@ where pub fn next_credential(&mut self) -> Result { info!("next credential"); - let CredentialManagementEnumerateCredentials { - remaining, - rp_dir, - prev_filename, - } = self + let cache = self .state .runtime .cached_rk - .clone() + .take() .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.cached_rk = None; - // let mut hex = [b'0'; 16]; - // super::format_hex(&rp_id_hash[..8], &mut hex); - // let rp_dir = PathBuf::from(b"rk").join(&PathBuf::from(&hex)); + let CredentialManagementEnumerateCredentials { + remaining, + prev_filename, + } = cache; + + let rp_id_hex = get_rp_id_hex_from_file_name(prev_filename.as_str()); + syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + PathBuf::from(RK_DIR), + Some(prev_filename.clone()), + )) + .entry; + + // The previous entry was already read. Skip to the next + let Some(entry) = syscall!(self.trussed.read_dir_next()).entry else { + return Err(Error::NoCredentials); + }; - let mut maybe_next_rk = - syscall!(self - .trussed - .read_dir_first(Location::Internal, rp_dir, Some(prev_filename))) - .entry; + if get_rp_id_hex(&entry) != rp_id_hex { + // We reached the end of the credentials for the rp + return Err(Error::NoCredentials); + } - // Advance to the next - if maybe_next_rk.is_some() { - maybe_next_rk = syscall!(self.trussed.read_dir_next()).entry; - } else { - return Err(Error::NotAllowed); + if entry.metadata().is_dir() { + warn!("Migration did not finish"); + return Err(Error::Other); } - match maybe_next_rk { - Some(rk) => { - // extract data required into response - let response = self.extract_response_from_credential_file(rk.path())?; - - // cache state for next call - if remaining > 1 { - 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) - } - None => Err(Error::NoCredentials), + let response = self.extract_response_from_credential_file(entry.path())?; + + // cache state for next call + if remaining > 1 { + self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { + remaining: remaining - 1, + prev_filename: entry.file_name().into(), + }); } + + Ok(response) } fn extract_response_from_credential_file(&mut self, rk_path: &Path) -> Result { @@ -473,14 +455,20 @@ where ) -> Option { let credential_id_hash = self.hash(credential.id); let mut hex = [b'0'; 16]; - super::format_hex(&credential_id_hash[..8], &mut hex); + let hex_str = super::format_hex(&credential_id_hash[..8], &mut hex); let dir = PathBuf::from(RK_DIR); - let filename = PathBuf::try_from(&hex).unwrap(); - syscall!(self - .trussed - .locate_file(Location::Internal, Some(dir), filename,)) - .path + let mut maybe_entry = + try_syscall!(self.trussed.read_dir_first(Location::Internal, dir, None)) + .ok()? + .entry; + while let Some(entry) = maybe_entry { + if entry.file_name().as_str().ends_with(&hex_str) { + return Some(entry.path().into()); + } + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; + } + None } pub fn delete_credential( @@ -495,13 +483,6 @@ where // DELETE self.delete_resident_key_by_path(&rk_path)?; - // get rid of directory if it's now empty - let rp_path = rk_path - .parent() - // by construction, RK has a parent, its RP - .unwrap(); - self.delete_rp_dir_if_empty(rp_path); - // just return OK let response = Default::default(); Ok(response) diff --git a/src/lib.rs b/src/lib.rs index 1d2a000..534a3a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,8 @@ extern crate delog; generate_macros!(); +pub use state::migrate; + use core::time::Duration; use trussed_core::{ @@ -161,13 +163,16 @@ where } // EWW.. this is a bit unsafe isn't it -fn format_hex(data: &[u8], mut buffer: &mut [u8]) { +fn format_hex<'a>(data: &[u8], buffer: &'a mut [u8]) -> &'a str { const HEX_CHARS: &[u8] = b"0123456789abcdef"; - for byte in data.iter() { - buffer[0] = HEX_CHARS[(byte >> 4) as usize]; - buffer[1] = HEX_CHARS[(byte & 0xf) as usize]; - buffer = &mut buffer[2..]; + assert!(data.len() * 2 >= buffer.len()); + for (idx, byte) in data.iter().enumerate() { + buffer[idx * 2] = HEX_CHARS[(byte >> 4) as usize]; + buffer[idx * 2 + 1] = HEX_CHARS[(byte & 0xf) as usize]; } + + // SAFETY: we just added only ascii chars to buffer from 0 to data.len() - 1 + unsafe { core::str::from_utf8_unchecked(&buffer[0..data.len() * 2]) } } // NB: to actually use this, replace the constant implementation with the inline assembly. @@ -334,4 +339,14 @@ where } #[cfg(test)] -mod test {} +mod test { + use super::*; + + #[test] + fn hex() { + let data = [0x01, 0x02, 0xB1, 0xA1]; + let buffer = &mut [0; 8]; + assert_eq!(format_hex(&data, buffer), "0102b1a1"); + assert_eq!(buffer, b"0102b1a1"); + } +} diff --git a/src/state.rs b/src/state.rs index d59d23f..9e85855 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,6 +2,10 @@ //! //! Needs cleanup. +pub mod migrate; + +use core::num::NonZeroU32; + use ctap_types::{ ctap2::AttestationFormatsPreference, // 2022-02-27: 10 credentials @@ -200,14 +204,13 @@ impl Identity { #[derive(Clone, Debug, Eq, PartialEq)] pub struct CredentialManagementEnumerateRps { - pub remaining: u32, + pub remaining: NonZeroU32, pub rp_id_hash: [u8; 32], } #[derive(Clone, Debug, Eq, PartialEq)] pub struct CredentialManagementEnumerateCredentials { pub remaining: u32, - pub rp_dir: PathBuf, pub prev_filename: PathBuf, } @@ -251,7 +254,7 @@ pub struct RuntimeState { // Currently, this causes the entire authnr to reset state. Maybe it should even reformat disk // // - An alternative would be `heapless::Map`, but I'd prefer something more typed. -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Default)] pub struct PersistentState { #[serde(skip)] // TODO: there has to be a better way than.. this @@ -288,15 +291,15 @@ impl PersistentState { let data = result.unwrap().data; - let result = cbor_smol::cbor_deserialize(&data); - - if result.is_err() { - info!("err deser'ing: {:?}", result.err().unwrap()); + let state: Self = cbor_smol::cbor_deserialize(&data).map_err(|_err| { + info!("err deser'ing: {_err:?}",); info!("{}", hex_str!(&data)); - return Err(Error::Other); - } + Error::Other + })?; + + debug!("Loaded state: {state:#?}"); - result.map_err(|_| Error::Other) + Ok(state) } pub fn save(&self, trussed: &mut T) -> Result<()> { diff --git a/src/state/migrate.rs b/src/state/migrate.rs new file mode 100644 index 0000000..d33d0c9 --- /dev/null +++ b/src/state/migrate.rs @@ -0,0 +1,267 @@ +use littlefs2_core::{path, DirEntry, DynFilesystem, Error, Path, PathBuf}; + +fn ignore_does_not_exists(error: Error) -> Result<(), Error> { + if matches!(error, Error::NO_SUCH_ENTRY) { + return Ok(()); + } + Err(error) +} + +/// Migration function, to be used with trussed-staging's `migrate` management syscall +/// +/// `base_path` must be the base of the file directory of the fido app (often `/fido/dat`) +pub fn migrate_no_rp_dir(fs: &dyn DynFilesystem, base_path: &Path) -> Result<(), Error> { + let rk_dir = base_path.join(path!("rk")); + + fs.read_dir_and_then(&rk_dir, &mut |dir| migrate_rk_dir(fs, &rk_dir, dir)) + .or_else(ignore_does_not_exists) +} + +fn migrate_rk_dir( + fs: &dyn DynFilesystem, + rk_dir: &Path, + dir: &mut dyn Iterator>, +) -> Result<(), Error> { + for rp in dir.skip(2) { + let rp = rp?; + if rp.metadata().is_file() { + continue; + } + + migrate_rp_dir(fs, rk_dir, rp.path())?; + } + Ok(()) +} + +fn migrate_rp_dir(fs: &dyn DynFilesystem, rk_dir: &Path, rp_path: &Path) -> Result<(), Error> { + let rp_id_hex = rp_path.file_name().unwrap().as_str(); + debug_assert_eq!(rp_id_hex.len(), 16); + + fs.read_dir_and_then(rp_path, &mut |rp_dir| { + for file in rp_dir.skip(2) { + let file = file?; + let cred_id_hex = file.file_name().as_str(); + let mut buf = [0; 33]; + buf[0..16].copy_from_slice(rp_id_hex.as_bytes()); + buf[16] = b'.'; + buf[17..].copy_from_slice(cred_id_hex.as_bytes()); + fs.rename( + file.path(), + &rk_dir.join(&PathBuf::try_from(buf.as_slice()).unwrap()), + )?; + } + Ok(()) + })?; + + fs.remove_dir(rp_path)?; + + Ok(()) +} + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod tests { + use admin_app::migrations::test_utils::{test_migration_one, FsValues}; + + use super::*; + + const FIDO_DAT_DIR_BEFORE: FsValues = FsValues::Dir(&[ + (path!("persistent-state.cbor"), FsValues::File(137)), + ( + path!("rk"), + FsValues::Dir(&[( + path!("74a6ea9213c99c2f"), + FsValues::Dir(&[ + (path!("038dfc6165b78be9"), FsValues::File(128)), + (path!("1ecbbfbed8992287"), FsValues::File(122)), + (path!("7c24db95312eac56"), FsValues::File(122)), + (path!("978cba44dfe39871"), FsValues::File(155)), + (path!("ac889a0433749726"), FsValues::File(138)), + ]), + )]), + ), + ]); + + const FIDO_DAT_DIR_AFTER: FsValues = FsValues::Dir(&[ + (path!("persistent-state.cbor"), FsValues::File(137)), + ( + path!("rk"), + FsValues::Dir(&[ + ( + path!("74a6ea9213c99c2f.038dfc6165b78be9"), + FsValues::File(128), + ), + ( + path!("74a6ea9213c99c2f.1ecbbfbed8992287"), + FsValues::File(122), + ), + ( + path!("74a6ea9213c99c2f.7c24db95312eac56"), + FsValues::File(122), + ), + ( + path!("74a6ea9213c99c2f.978cba44dfe39871"), + FsValues::File(155), + ), + ( + path!("74a6ea9213c99c2f.ac889a0433749726"), + FsValues::File(138), + ), + ]), + ), + ]); + + const FIDO_SEC_DIR: FsValues = FsValues::Dir(&[ + ( + path!("069386c3c735689061ac51b8bca9f160"), + FsValues::File(48), + ), + ( + path!("233d86bfc2f196ff7c108cf23a282bd5"), + FsValues::File(36), + ), + ( + path!("2bdef14a0e18d28191162f8c1599d598"), + FsValues::File(36), + ), + ( + path!("3efe6394c20aa8128e27b376e226a58b"), + FsValues::File(36), + ), + ( + path!("4711aa79b4834ef8e551f80e523ba8d2"), + FsValues::File(36), + ), + ( + path!("b43bf8b7897087b7195b8ac53dcb5f11"), + FsValues::File(36), + ), + ]); + + #[test] + fn migration_no_auth() { + const TEST_VALUES_BEFORE: FsValues = FsValues::Dir(&[ + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_BEFORE), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ), + ]); + + const TEST_VALUES_AFTER: FsValues = FsValues::Dir(&[ + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_AFTER), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ), + ]); + + test_migration_one(&TEST_VALUES_BEFORE, &TEST_VALUES_AFTER, |fs| { + migrate_no_rp_dir(fs, path!("fido/dat")) + }); + } + + #[test] + fn migration_auth() { + const AUTH_SECRETS_DIR: (&Path, FsValues) = ( + path!("secrets"), + FsValues::Dir(&[( + path!("backend-auth"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[ + (path!("application_salt"), FsValues::File(16)), + (path!("pin.00"), FsValues::File(118)), + ]), + )]), + )]), + ); + + const BACKEND_DIR: (&Path, FsValues) = ( + path!("backend-auth"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("salt"), FsValues::File(16))]), + )]), + ); + + const TRUSSED_DIR: (&Path, FsValues) = ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ); + + const TEST_BEFORE: FsValues = FsValues::Dir(&[ + BACKEND_DIR, + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_BEFORE), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + AUTH_SECRETS_DIR, + TRUSSED_DIR, + ]); + + const TEST_AFTER: FsValues = FsValues::Dir(&[ + BACKEND_DIR, + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_AFTER), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + AUTH_SECRETS_DIR, + TRUSSED_DIR, + ]); + + test_migration_one(&TEST_BEFORE, &TEST_AFTER, |fs| { + migrate_no_rp_dir(fs, path!("fido/dat")) + }); + } + + #[test] + fn migration_empty() { + const TEST_VALUES: FsValues = FsValues::Dir(&[ + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FsValues::Dir(&[])), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ), + ]); + test_migration_one(&TEST_VALUES, &TEST_VALUES, |fs| { + migrate_no_rp_dir(fs, path!("fido/dat")) + }); + } +} diff --git a/tests/cred_mgmt.rs b/tests/cred_mgmt.rs index fc46c6b..4f1d683 100644 --- a/tests/cred_mgmt.rs +++ b/tests/cred_mgmt.rs @@ -31,6 +31,8 @@ impl<'a> CredMgmt<'a> { self.authenticator .make_credential(rp.clone(), user.clone()) .inspect(|credential_data| { + self.credentials + .retain(|(old_rp, old_user, _)| old_rp != &rp || old_user != &user); self.credentials.push((rp, user, credential_data.clone())); }) } @@ -168,6 +170,32 @@ fn test_list_credentials_multi() { }) } +#[test] +fn test_list_credentials_overwrite() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 2); + assert_eq!(files.try_remove_rks(), 1); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + let rp = generate_rp(0); + let user = generate_user(0); + cred_mgmt.make_credential(rp.clone(), user.clone()).unwrap(); + cred_mgmt.make_credential(rp, user).unwrap(); + + cred_mgmt.list(); + }) +} + #[test] fn test_list_credentials_delete() { let options = Options { @@ -339,7 +367,7 @@ fn test_filesystem_full() { max_resident_credential_count: Some(10), inspect_ifs: Some(Box::new(|ifs| { let blocks = ifs.available_blocks().unwrap(); - assert!(blocks < 5, "{blocks}"); + assert!(blocks < 10, "{blocks}"); assert!(blocks > 1, "{blocks}"); })), ..Default::default() @@ -374,11 +402,12 @@ fn test_filesystem_full() { i += 1; } - // We should be able to create at least 1 but not more than n credentials. - assert!(i > 0); - assert!(i < n); - // Our estimate should not be more than one credential off. - assert!(n - i <= 1); + // We should be able to create at least one credential. + assert!(i > 0, "i = {i}"); + // Our estimate should not be too low. + assert!(i >= n, "i = {i}, n = {n}"); + // Our estime should not be more than five credentials too high. + assert!(i <= n + 5, "i = {i}, n = {n}"); let metadata = authenticator.credentials_metadata(); assert_eq!(metadata.existing, i); @@ -392,7 +421,7 @@ fn test_filesystem_full_update_user() { max_resident_credential_count: Some(10), inspect_ifs: Some(Box::new(|ifs| { let blocks = ifs.available_blocks().unwrap(); - assert!(blocks < 5, "{blocks}"); + assert!(blocks < 10, "{blocks}"); assert!(blocks > 1, "{blocks}"); })), ..Default::default() diff --git a/tests/fs/mod.rs b/tests/fs/mod.rs index 25dd201..9ef52e7 100644 --- a/tests/fs/mod.rs +++ b/tests/fs/mod.rs @@ -31,13 +31,7 @@ impl Entries { } pub fn try_remove_rks(&mut self) -> usize { - let n = self.0.len(); - self.0.retain(|path, _| { - let (start, _) = path.rsplit_once('/').unwrap(); - let start = start.rsplit_once('/').map(|(start, _)| start); - start != Some("fido/dat/rk") - }); - n - self.0.len() + self.try_remove_dir("fido/dat/rk") } pub fn try_remove_dir(&mut self, dir: &str) -> usize { From e21d8687fcfe8961270d22dc3494692d43c0ba41 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 24 Apr 2024 22:58:52 +0200 Subject: [PATCH 126/135] Remove references to totp signing algorithm --- src/ctap2.rs | 23 +---------------------- src/ctap2/credential_management.rs | 4 +--- src/lib.rs | 3 --- 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 04d9dd8..a05c069 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -206,7 +206,6 @@ impl Authenticator for crate::Authenti -8 => { algorithm = Some(SigningAlgorithm::Ed25519); } - // -9 => { algorithm = Some(SigningAlgorithm::Totp); } _ => {} } } @@ -307,20 +306,7 @@ impl Authenticator for crate::Authenti .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(); - // } + } } // 12. if `rk` is set, store or overwrite key pair, if full error KeyStoreFull @@ -509,7 +495,6 @@ impl Authenticator for crate::Authenti .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( @@ -1503,11 +1488,6 @@ impl crate::Authenticator { match alg { -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, } } @@ -1689,7 +1669,6 @@ impl crate::Authenticator { 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); } diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 273e159..cd36c26 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -429,9 +429,7 @@ where PublicKey::Ed25519Key( 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), diff --git a/src/lib.rs b/src/lib.rs index 534a3a9..2713ffe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -202,8 +202,6 @@ pub enum SigningAlgorithm { Ed25519 = -8, /// The NIST P-256 signature algorithm. P256 = -7, - // #[doc(hidden)] - // Totp = -9, } impl core::convert::TryFrom for SigningAlgorithm { @@ -212,7 +210,6 @@ impl core::convert::TryFrom for SigningAlgorithm { Ok(match alg { -7 => SigningAlgorithm::P256, -8 => SigningAlgorithm::Ed25519, - // -9 => SigningAlgorithm::Totp, _ => return Err(Error::UnsupportedAlgorithm), }) } From 8b8a7738312931f15cb666a25c37b1a850edc94e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 24 Apr 2024 23:33:45 +0200 Subject: [PATCH 127/135] Reduce duplicated key generation and signing logic This patch moves the key generation and signing logic into the SigningAlgorithm enum, removing some duplicated code from the ctap2 and ctap2::credential_management modules. --- src/ctap2.rs | 125 +++++++---------------------- src/ctap2/credential_management.rs | 31 +------ src/lib.rs | 75 ++++++++++++++++- 3 files changed, 104 insertions(+), 127 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index a05c069..511becf 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -18,10 +18,7 @@ use sha2::{Digest as _, Sha256}; use trussed_core::{ syscall, try_syscall, - types::{ - KeyId, KeySerialization, Location, Mechanism, MediumData, Message, SignatureSerialization, - StorageAttributes, - }, + types::{KeyId, Location, Mechanism, MediumData, Message, StorageAttributes}, }; use crate::{ @@ -272,42 +269,8 @@ impl Authenticator for crate::Authenti 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, - 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; - cose_public_key = syscall!(self.trussed.serialize_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); - } - } + let private_key = algorithm.generate_private_key(&mut self.trussed, location); + let cose_public_key = algorithm.derive_public_key(&mut self.trussed, private_key); // 12. if `rk` is set, store or overwrite key pair, if full error KeyStoreFull @@ -478,39 +441,15 @@ impl Authenticator for crate::Authenti .extend_from_slice(parameters.client_data_hash) .map_err(|_| Error::Other)?; - let (signature, attestation_algorithm) = { - if let Some(attestation) = attestation_maybe.as_ref() { - let signature = syscall!(self.trussed.sign_p256( - attestation.0, - &commitment, - SignatureSerialization::Asn1Der, - )) - .signature; - (signature.to_bytes().map_err(|_| Error::Other)?, -7) - } else { - 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) - } - } - } - }; + let (attestation_key, attestation_algorithm) = attestation_maybe + .as_ref() + .map(|attestation| (attestation.0, SigningAlgorithm::P256)) + .unwrap_or((private_key, algorithm)); + let signature = + attestation_algorithm.sign(&mut self.trussed, attestation_key, &commitment); let packed = PackedAttestationStatement { - alg: attestation_algorithm, - sig: signature, + alg: attestation_algorithm.into(), + sig: signature.to_bytes().map_err(|_| Error::Other)?, x5c: attestation_maybe.as_ref().map(|attestation| { // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements let cert = attestation.1.clone(); @@ -1485,11 +1424,9 @@ impl crate::Authenticator { Key::WrappedKey(_) => true, Key::ResidentKey(key) => { debug_now!("checking if ResidentKey {:?} exists", key); - match alg { - -7 => syscall!(self.trussed.exists(Mechanism::P256, *key)).exists, - -8 => syscall!(self.trussed.exists(Mechanism::Ed255, *key)).exists, - _ => false, - } + SigningAlgorithm::try_from(alg) + .map(|alg| syscall!(self.trussed.exists(alg.mechanism(), *key)).exists) + .unwrap_or_default() } } } @@ -1666,21 +1603,12 @@ impl crate::Authenticator { .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), - _ => { - return Err(Error::Other); - } - }; - - debug_now!("signing with {:?}, {:?}", &mechanism, &serialization); - let signature = syscall!(self - .trussed - .sign(mechanism, key, &commitment, serialization)) - .signature - .to_bytes() - .unwrap(); + let signing_algorithm = + SigningAlgorithm::try_from(credential.algorithm()).map_err(|_| Error::Other)?; + let signature = signing_algorithm + .sign(&mut self.trussed, key, &commitment) + .to_bytes() + .unwrap(); // select preferred format or skip attestation statement let att_stmt_fmt = data @@ -1696,13 +1624,16 @@ impl crate::Authenticator { let (attestation_maybe, _) = self.state.identity.attestation(&mut self.trussed); let (signature, attestation_algorithm) = { if let Some(attestation) = attestation_maybe.as_ref() { - let signature = syscall!(self.trussed.sign_p256( + let signing_algorithm = SigningAlgorithm::P256; + let signature = signing_algorithm.sign( + &mut self.trussed, attestation.0, &commitment, - SignatureSerialization::Asn1Der, - )) - .signature; - (signature.to_bytes().map_err(|_| Error::Other)?, -7) + ); + ( + signature.to_bytes().map_err(|_| Error::Other)?, + signing_algorithm.into(), + ) } else { (signature.clone(), credential.algorithm()) } diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index cd36c26..1218766 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -397,39 +397,16 @@ where }; use crate::SigningAlgorithm; - use trussed_core::types::{KeySerialization, Mechanism}; let algorithm = SigningAlgorithm::try_from(credential.algorithm)?; + let cose_public_key = algorithm.derive_public_key(&mut self.trussed, private_key); let cose_public_key = match algorithm { 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, - // KeySerialization::EcdhEsHkdf256 - KeySerialization::Cose, - )) - .serialized_key; - syscall!(self.trussed.delete(public_key)); 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; - syscall!(self.trussed.delete(public_key)); - PublicKey::Ed25519Key( - ctap_types::serde::cbor_deserialize(&cose_public_key).unwrap(), - ) - } + SigningAlgorithm::Ed25519 => PublicKey::Ed25519Key( + ctap_types::serde::cbor_deserialize(&cose_public_key).unwrap(), + ), }; let cred_protect = match credential.cred_protect { Some(x) => Some(x), diff --git a/src/lib.rs b/src/lib.rs index 2713ffe..2aba994 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,8 +23,12 @@ pub use state::migrate; use core::time::Duration; use trussed_core::{ - mechanisms, syscall, types::Location, CertificateClient, CryptoClient, FilesystemClient, - ManagementClient, UiClient, + mechanisms, syscall, + types::{ + KeyId, KeySerialization, Location, Mechanism, SerializedKey, Signature, + SignatureSerialization, StorageAttributes, + }, + CertificateClient, CryptoClient, FilesystemClient, ManagementClient, UiClient, }; use trussed_fs_info::{FsInfoClient, FsInfoReply}; use trussed_hkdf::HkdfClient; @@ -204,8 +208,73 @@ pub enum SigningAlgorithm { P256 = -7, } -impl core::convert::TryFrom for SigningAlgorithm { +impl SigningAlgorithm { + pub fn mechanism(&self) -> Mechanism { + match self { + Self::Ed25519 => Mechanism::Ed255, + Self::P256 => Mechanism::P256, + } + } + + pub fn signature_serialization(&self) -> SignatureSerialization { + match self { + Self::Ed25519 => SignatureSerialization::Raw, + Self::P256 => SignatureSerialization::Asn1Der, + } + } + + pub fn generate_private_key( + &self, + trussed: &mut C, + location: Location, + ) -> KeyId { + syscall!(trussed.generate_key( + self.mechanism(), + StorageAttributes::new().set_persistence(location) + )) + .key + } + + pub fn derive_public_key( + &self, + trussed: &mut C, + private_key: KeyId, + ) -> SerializedKey { + let mechanism = self.mechanism(); + let public_key = syscall!(trussed.derive_key( + mechanism, + private_key, + None, + StorageAttributes::new().set_persistence(Location::Volatile) + )) + .key; + let cose_public_key = + syscall!(trussed.serialize_key(mechanism, public_key, KeySerialization::Cose)) + .serialized_key; + if !syscall!(trussed.delete(public_key)).success { + error!("failed to delete credential public key"); + } + cose_public_key + } + + pub fn sign(&self, trussed: &mut C, key: KeyId, data: &[u8]) -> Signature { + syscall!(trussed.sign(self.mechanism(), key, data, self.signature_serialization())) + .signature + } +} + +impl From for i32 { + fn from(alg: SigningAlgorithm) -> Self { + match alg { + SigningAlgorithm::P256 => -7, + SigningAlgorithm::Ed25519 => -8, + } + } +} + +impl TryFrom for SigningAlgorithm { type Error = Error; + fn try_from(alg: i32) -> Result { Ok(match alg { -7 => SigningAlgorithm::P256, From ba17bc506cf9c17a16ab909b18af879357a07da2 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 6 May 2025 21:48:40 +0200 Subject: [PATCH 128/135] Replace core::iter::repeat(_).take(_) with core::iter::repeat_n(_, _) This fixes a new clippy lint. --- src/credential.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/credential.rs b/src/credential.rs index f241685..6a61024 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -1078,7 +1078,7 @@ mod test { #[test] fn max_credential_id() { - let rp_id: String<256> = core::iter::repeat('?').take(256).collect(); + let rp_id: String<256> = core::iter::repeat_n('?', 256).collect(); let key = Bytes::from_slice(&[u8::MAX; 128]).unwrap(); let credential = StrippedCredential { ctap: CtapVersion::Fido21Pre, From 91a57756c0b2b11cc52755a18b04e916219b4d2d Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 7 May 2025 15:54:54 +0200 Subject: [PATCH 129/135] tests: Use hmac-secret extension in TestGetAssertion --- tests/basic.rs | 81 ++++++++++++++++++++++++++++--------------- tests/webauthn/mod.rs | 71 ++++++++++++++++++++++++++++++++----- 2 files changed, 116 insertions(+), 36 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index 03ca683..5e0b684 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -12,13 +12,15 @@ use std::{ use ciborium::Value; use hex_literal::hex; use itertools::iproduct; +use rand::RngCore as _; use fs::list_fs; use virt::{Ctap2, Ctap2Error, Options}; use webauthn::{ exhaustive_struct, AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, - Exhaustive, ExtensionsInput, GetAssertion, GetAssertionOptions, GetInfo, GetNextAssertion, - KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, + Exhaustive, GetAssertion, GetAssertionExtensionsInput, GetAssertionOptions, GetInfo, + GetNextAssertion, HmacSecretInput, KeyAgreementKey, MakeCredential, + MakeCredentialExtensionsInput, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }; @@ -692,7 +694,7 @@ impl Test for TestMakeCredential { self.attestation_formats_preference.map(From::from); // TODO: test other extensions and permutations if self.hmac_secret { - request.extensions = Some(ExtensionsInput { + request.extensions = Some(MakeCredentialExtensionsInput { hmac_secret: Some(true), ..Default::default() }); @@ -757,7 +759,8 @@ struct TestGetAssertion { rk: bool, allow_list: bool, options: Option, - mc_third_party_payment: Option, + mc_extensions: Option, + ga_hmac_secret: bool, ga_third_party_payment: Option, } @@ -783,6 +786,9 @@ impl Test for TestGetAssertion { // TODO: test with PIN virt::run_ctap2(|device| { + let key_agreement_key = KeyAgreementKey::generate(); + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let rp = Rp::new(rp_id); let user = User::new(b"id123") .name("john.doe") @@ -792,12 +798,7 @@ impl Test for TestGetAssertion { if self.rk { request.options = Some(MakeCredentialOptions::default().rk(true)); } - if let Some(third_party_payment) = self.mc_third_party_payment { - request.extensions = Some(ExtensionsInput { - third_party_payment: Some(third_party_payment), - ..Default::default() - }); - } + request.extensions = self.mc_extensions; let response = device.exec(request).unwrap(); let credential = response.auth_data.credential.unwrap(); @@ -811,11 +812,26 @@ impl Test for TestGetAssertion { credential.id.clone(), )]); } - if let Some(third_party_payment) = self.ga_third_party_payment { - request.extensions = Some(ExtensionsInput { - third_party_payment: Some(third_party_payment), + if self.ga_hmac_secret || self.ga_third_party_payment.is_some() { + let mut extensions = GetAssertionExtensionsInput { + third_party_payment: self.ga_third_party_payment, ..Default::default() - }); + }; + if self.ga_hmac_secret { + // TODO: We always set the last byte to 0xff to work around the zero padding + // currently used by trussed. + let mut salt = [0xff; 32]; + rand::thread_rng().fill_bytes(&mut salt[..31]); + let salt_enc = shared_secret.encrypt(&salt); + let salt_auth = shared_secret.authenticate(&salt_enc); + extensions.hmac_secret = Some(HmacSecretInput { + key_agreement: key_agreement_key.public_key(), + salt_enc, + salt_auth, + pin_protocol: Some(2), + }); + } + request.extensions = Some(extensions); } request.options = self.options; let result = device.exec(request); @@ -823,6 +839,8 @@ impl Test for TestGetAssertion { assert_eq!(result, Err(Ctap2Error(error))); return; } + let has_extensions = + self.ga_hmac_secret || self.ga_third_party_payment.unwrap_or_default(); let response = result.unwrap(); assert_eq!(response.credential.ty, "public-key"); assert_eq!(response.credential.id, credential.id); @@ -833,20 +851,28 @@ impl Test for TestGetAssertion { ); assert!(!response.auth_data.uv_flag()); assert!(!response.auth_data.at_flag()); - assert_eq!( - response.auth_data.ed_flag(), - self.ga_third_party_payment.unwrap_or_default() - ); + assert_eq!(response.auth_data.ed_flag(), has_extensions,); assert_eq!(response.number_of_credentials, None); credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); - if self.ga_third_party_payment.unwrap_or_default() { + if has_extensions { let extensions = response.auth_data.extensions.unwrap(); - assert_eq!( - extensions.get("thirdPartyPayment"), - Some(&Value::from( - self.mc_third_party_payment.unwrap_or_default() - )) - ); + + if self.ga_hmac_secret { + let hmac_secret = extensions.get("hmac-secret").unwrap().as_bytes().unwrap(); + let output = shared_secret.decrypt(hmac_secret); + assert_eq!(output.len(), 32); + } + + if self.ga_third_party_payment.unwrap_or_default() { + let expected = self + .mc_extensions + .and_then(|e| e.third_party_payment) + .unwrap_or_default(); + assert_eq!( + extensions.get("thirdPartyPayment"), + Some(&Value::from(expected)) + ); + } } else { assert!(response.auth_data.extensions.is_none()); } @@ -860,7 +886,8 @@ impl Exhaustive for TestGetAssertion { rk: bool, allow_list: bool, options: Option, - mc_third_party_payment: Option, + mc_extensions: Option, + ga_hmac_secret: bool, ga_third_party_payment: Option, } } @@ -1029,7 +1056,7 @@ impl Test for TestListCredentials { request.pin_auth = Some(pin_auth); request.pin_protocol = Some(2); if let Some(third_party_payment) = self.third_party_payment { - request.extensions = Some(ExtensionsInput { + request.extensions = Some(MakeCredentialExtensionsInput { third_party_payment: Some(third_party_payment), ..Default::default() }); diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index f780e4d..9c87184 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -57,6 +57,7 @@ impl KeyAgreementKey { } } +#[derive(Copy, Clone, Debug)] pub struct PublicKey(p256::PublicKey); impl From for Value { @@ -109,14 +110,17 @@ impl SharedSecret { result } - pub fn decrypt_pin_token(&self, data: &[u8]) -> PinToken { + pub fn decrypt(&self, data: &[u8]) -> Vec { let (iv, data) = data.split_first_chunk::<16>().unwrap(); let cipher: cbc::Decryptor = KeyIvInit::new(self.aes_key.as_ref().into(), iv.into()); - let pin_token = cipher + cipher .decrypt_padded_vec_mut::(data) - .unwrap(); - PinToken(pin_token.try_into().unwrap()) + .unwrap() + } + + pub fn decrypt_pin_token(&self, data: &[u8]) -> PinToken { + PinToken(self.decrypt(data).try_into().unwrap()) } pub fn authenticate(&self, data: &[u8]) -> [u8; 32] { @@ -362,7 +366,7 @@ pub struct MakeCredential { rp: Rp, user: User, pub_key_cred_params: Vec, - pub extensions: Option, + pub extensions: Option, pub options: Option, pub pin_auth: Option<[u8; 32]>, pub pin_protocol: Option, @@ -428,13 +432,22 @@ impl From for Value { } #[derive(Clone, Copy, Debug, Default)] -pub struct ExtensionsInput { +pub struct MakeCredentialExtensionsInput { pub hmac_secret: Option, pub third_party_payment: Option, } -impl From for Value { - fn from(extensions: ExtensionsInput) -> Value { +impl Exhaustive for MakeCredentialExtensionsInput { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + hmac_secret: Option, + third_party_payment: Option, + } + } +} + +impl From for Value { + fn from(extensions: MakeCredentialExtensionsInput) -> Value { let mut map = Map::default(); if let Some(hmac_secret) = extensions.hmac_secret { map.push("hmac-secret", hmac_secret); @@ -602,7 +615,7 @@ pub struct GetAssertion { rp_id: String, client_data_hash: Vec, pub allow_list: Option>, - pub extensions: Option, + pub extensions: Option, pub options: Option, } @@ -643,6 +656,46 @@ impl Request for GetAssertion { type Reply = GetAssertionReply; } +#[derive(Clone, Debug, Default)] +pub struct GetAssertionExtensionsInput { + pub third_party_payment: Option, + pub hmac_secret: Option, +} + +impl From for Value { + fn from(extensions: GetAssertionExtensionsInput) -> Value { + let mut map = Map::default(); + if let Some(hmac_secret) = extensions.hmac_secret { + map.push("hmac-secret", hmac_secret); + } + if let Some(third_party_payment) = extensions.third_party_payment { + map.push("thirdPartyPayment", third_party_payment); + } + map.into() + } +} + +#[derive(Clone, Debug)] +pub struct HmacSecretInput { + pub key_agreement: PublicKey, + pub salt_enc: Vec, + pub salt_auth: [u8; 32], + pub pin_protocol: Option, +} + +impl From for Value { + fn from(input: HmacSecretInput) -> Value { + let mut map = Map::default(); + map.push(0x01, input.key_agreement); + map.push(0x02, input.salt_enc); + map.push(0x03, input.salt_auth.as_slice()); + if let Some(pin_protocol) = input.pin_protocol { + map.push(0x04, pin_protocol); + } + map.into() + } +} + #[derive(Clone, Copy, Debug, Default)] pub struct GetAssertionOptions { pub up: Option, From 7ff0518b6830efdc215402c95a680ee41726f2bc Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 7 May 2025 16:04:44 +0200 Subject: [PATCH 130/135] hmac-secret: Forbid up=false Fixes: https://github.com/Nitrokey/fido-authenticator/issues/19 --- src/ctap2.rs | 4 ++++ tests/basic.rs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/ctap2.rs b/src/ctap2.rs index 511becf..8cf9cf9 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1448,6 +1448,10 @@ impl crate::Authenticator { .transpose()? .unwrap_or(PinProtocolVersion::V1); + if !get_assertion_state.up_performed { + return Err(Error::UnsupportedOption); + } + // 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) diff --git a/tests/basic.rs b/tests/basic.rs index 5e0b684..1fd014b 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -774,6 +774,11 @@ impl TestGetAssertion { if !self.rk && !self.allow_list { return Some(0x2e); } + if let Some(options) = self.options { + if options.up == Some(false) && self.ga_hmac_secret { + return Some(0x2b); + } + } None } } From 223bc11eece2da0185f747068be93c308254050a Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 7 May 2025 21:57:44 +0200 Subject: [PATCH 131/135] Always reject uv = true in make_credential and get_assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes the error code if uv = true to InvalidOption even if a PIN is set. Previously, we returned PinRequired if a PIN is set. The new implementation follows § 6.1.2 Step 5 of the specification more closely. https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-makeCred-authnr-alg --- src/ctap2.rs | 9 ++++++--- tests/basic.rs | 9 +++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index 8cf9cf9..ddd9cab 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1383,9 +1383,12 @@ impl crate::Authenticator { } // 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 + + // Reject uv = true as we do not support built-in user verification + if pin_auth.is_none() && options.as_ref().and_then(|options| options.uv) == Some(true) { + return Err(Error::InvalidOption); + } + if self.state.persistent.pin_is_set() { // let mut uv_performed = false; if let Some(pin_auth) = pin_auth { diff --git a/tests/basic.rs b/tests/basic.rs index 1fd014b..7114a8b 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -599,6 +599,9 @@ impl TestMakeCredential { if options.up.is_some() { return Some(0x2c); } + if !matches!(self.pin_auth, PinAuth::PinToken(_)) && options.uv == Some(true) { + return Some(0x2c); + } } match &self.pin_auth { PinAuth::PinToken( @@ -611,12 +614,6 @@ impl TestMakeCredential { } _ => {} } - if let Some(options) = self.options { - // TODO: review if uv should be always rejected due to the lack of built-in uv - if !matches!(self.pin_auth, PinAuth::PinToken(_)) && options.uv == Some(true) { - return Some(0x2c); - } - } if !self.valid_pub_key_alg { return Some(0x26); } From 4554cb866e731f7cbf56c7ac93861a8baeeb0e70 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 7 May 2025 22:15:59 +0200 Subject: [PATCH 132/135] make_credential: Support non-discoverable credentials without PIN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, we always require the PIN to be used for make_credential operations if it is set. This patch implements the makeCredUvNotRqd option that allows non-discoverable credentials to be created without using the PIN according to § 6.1.2 Step 6 of the specification, see: https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-makeCred-authnr-alg https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#getinfo-makecreduvnotrqd Fixes: https://github.com/Nitrokey/fido-authenticator/issues/34 --- src/ctap2.rs | 5 +++-- tests/basic.rs | 16 +++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/ctap2.rs b/src/ctap2.rs index ddd9cab..d21f483 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -75,6 +75,7 @@ impl Authenticator for crate::Authenti }; options.large_blobs = Some(self.config.supports_large_blobs()); options.pin_uv_auth_token = Some(true); + options.make_cred_uv_not_rqd = Some(true); let mut transports = Vec::new(); if self.config.nfc_transport { @@ -1409,8 +1410,8 @@ impl crate::Authenticator { return Err(Error::PinAuthInvalid); } } else { - // 6. pinAuth not present + clientPin set --> error PinRequired - if self.state.persistent.pin_is_set() { + // 6. pinAuth not present + clientPin set + rk = true --> error PinRequired + if options.as_ref().and_then(|options| options.rk) == Some(true) { return Err(Error::PinRequired); } } diff --git a/tests/basic.rs b/tests/basic.rs index 7114a8b..9e340ca 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -602,17 +602,15 @@ impl TestMakeCredential { if !matches!(self.pin_auth, PinAuth::PinToken(_)) && options.uv == Some(true) { return Some(0x2c); } - } - match &self.pin_auth { - PinAuth::PinToken( - RequestPinToken::InvalidPermissions | RequestPinToken::InvalidRpId, - ) => { - return Some(0x33); - } - PinAuth::PinNoToken => { + if matches!(self.pin_auth, PinAuth::PinNoToken) && options.rk == Some(true) { return Some(0x36); } - _ => {} + } + if let PinAuth::PinToken( + RequestPinToken::InvalidPermissions | RequestPinToken::InvalidRpId, + ) = &self.pin_auth + { + return Some(0x33); } if !self.valid_pub_key_alg { return Some(0x26); From 5ebb4a48302e4c80a2abe1c2d86201a3df2a1d2d Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 12 May 2025 15:18:45 +0200 Subject: [PATCH 133/135] clientPin: Support getRetries without PIN protocol This fixes compatibility with CTAP 2.1. Fixes: https://github.com/Nitrokey/fido-authenticator/issues/118 --- Cargo.toml | 2 +- fuzz/Cargo.toml | 2 +- src/ctap2.rs | 12 +++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9222962..98177ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ required-features = ["dispatch"] [dependencies] cbor-smol = { version = "0.5" } -ctap-types = { version = "0.3.1", features = ["get-info-full", "large-blobs", "third-party-payment"] } +ctap-types = { version = "0.4", features = ["get-info-full", "large-blobs", "third-party-payment"] } cosey = "0.3" delog = "0.1.0" heapless = "0.7" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 5f466bb..9b9d04b 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" cargo-fuzz = true [dependencies] -ctap-types = { version = "0.3.0", features = ["arbitrary"] } +ctap-types = { version = "0.4", features = ["arbitrary"] } libfuzzer-sys = "0.4" trussed = { version = "0.1", features = ["clients-1", "certificate-client", "crypto-client", "filesystem-client", "management-client", "aes256-cbc", "ed255", "p256", "sha256"] } trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] } diff --git a/src/ctap2.rs b/src/ctap2.rs index d21f483..d30a443 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -530,7 +530,10 @@ impl Authenticator for crate::Authenti debug_now!("CTAP2.PIN..."); // info_now!("{:?}", parameters); - let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; + let pin_protocol = parameters + .pin_protocol + .ok_or(Error::MissingParameter) + .and_then(|pin_protocol| self.parse_pin_protocol(pin_protocol)); let mut response = ctap2::client_pin::Response::default(); match parameters.sub_command { @@ -543,6 +546,7 @@ impl Authenticator for crate::Authenti Subcommand::GetKeyAgreement => { debug_now!("CTAP2.Pin.GetKeyAgreement"); + let pin_protocol = pin_protocol?; response.key_agreement = Some(self.pin_protocol(pin_protocol).key_agreement_key()); } @@ -567,6 +571,7 @@ impl Authenticator for crate::Authenti return Err(Error::MissingParameter); } }; + let pin_protocol = pin_protocol?; // 2. is pin already set if self.state.persistent.pin_is_set() { @@ -624,6 +629,7 @@ impl Authenticator for crate::Authenti return Err(Error::MissingParameter); } }; + let pin_protocol = pin_protocol?; // 2. fail if no retries left self.state.pin_blocked()?; @@ -679,7 +685,7 @@ impl Authenticator for crate::Authenti .ok_or(Error::MissingParameter)?; // 2. Check PIN protocol - let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; + let pin_protocol = pin_protocol?; // 3. + 4. Check invalid parameters if parameters.permissions.is_some() || parameters.rp_id.is_some() { @@ -744,7 +750,7 @@ impl Authenticator for crate::Authenti let permissions = parameters.permissions.ok_or(Error::MissingParameter)?; // 2. Check PIN protocol - let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; + let pin_protocol = pin_protocol?; // 3. Check that permissions are not empty let permissions = Permissions::from_bits_truncate(permissions); From cb30a2cc6499464058ce5a9092d8346b7dc4ea62 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 15 May 2025 09:33:09 +0200 Subject: [PATCH 134/135] Remove usbip example Instead, the nitrokey-3-firmware usbip runner should be used. --- Cargo.toml | 6 ---- examples/usbip.rs | 88 ----------------------------------------------- 2 files changed, 94 deletions(-) delete mode 100644 examples/usbip.rs diff --git a/Cargo.toml b/Cargo.toml index 98177ed..73c9e8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,6 @@ 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 - -[[example]] -name = "usbip" -required-features = ["dispatch"] - [dependencies] cbor-smol = { version = "0.5" } ctap-types = { version = "0.4", features = ["get-info-full", "large-blobs", "third-party-payment"] } diff --git a/examples/usbip.rs b/examples/usbip.rs deleted file mode 100644 index 2379a0c..0000000 --- a/examples/usbip.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (C) 2022 Nitrokey GmbH -// SPDX-License-Identifier: CC0-1.0 - -//! USB/IP runner for opcard. -//! Run with cargo run --example usbip --features dispatch - -use littlefs2_core::path; -use trussed::{ - backend::BackendId, - client::ClientBuilder, - service::Service, - types::Location, - virt::{Platform, Ram, StoreProvider}, -}; -use trussed_staging::virt::{BackendIds, Dispatcher}; -use trussed_usbip::{Client, Syscall}; - -const MANUFACTURER: &str = "Nitrokey"; -const PRODUCT: &str = "Nitrokey 3"; -const VID: u16 = 0x20a0; -const PID: u16 = 0x42b2; - -type VirtClient = Client; - -struct FidoApp { - fido: fido_authenticator::Authenticator, -} - -impl trussed_usbip::Apps<'static, S, Dispatcher> for FidoApp { - type Data = (); - fn new(service: &mut Service, Dispatcher>, syscall: Syscall, _data: ()) -> Self { - let large_blogs = Some(fido_authenticator::LargeBlobsConfig { - location: Location::External, - #[cfg(feature = "chunked")] - max_size: 4096, - }); - - let client = ClientBuilder::new(path!("fido")) - .backends(&[ - BackendId::Core, - BackendId::Custom(BackendIds::StagingBackend), - ]) - .prepare(service) - .expect("failed to create client") - .build(syscall); - FidoApp { - fido: fido_authenticator::Authenticator::new( - client, - fido_authenticator::Conforming {}, - fido_authenticator::Config { - max_msg_size: usbd_ctaphid::constants::MESSAGE_SIZE, - skip_up_timeout: None, - max_resident_credential_count: Some(10), - large_blobs: large_blogs, - nfc_transport: false, - }, - ), - } - } - - fn with_ctaphid_apps( - &mut self, - f: impl FnOnce( - &mut [&mut dyn ctaphid_dispatch::app::App< - 'static, - { ctaphid_dispatch::MESSAGE_SIZE }, - >], - ) -> T, - ) -> T { - f(&mut [&mut self.fido]) - } -} - -fn main() { - env_logger::init(); - - let options = trussed_usbip::Options { - manufacturer: Some(MANUFACTURER.to_owned()), - product: Some(PRODUCT.to_owned()), - serial_number: Some("TEST".into()), - vid: VID, - pid: PID, - }; - trussed_usbip::Builder::new(Ram::default(), options) - .dispatch(Dispatcher::default()) - .build::() - .exec(|_platform| {}); -} From 01a2653c37dcefaae9060c4fab8c868849e96022 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 15 May 2025 11:31:15 +0200 Subject: [PATCH 135/135] Update trussed to use new virtual store --- Cargo.toml | 14 +++++++------- src/credential.rs | 8 ++++---- tests/virt/mod.rs | 18 +++++++++--------- tests/virt/pipe.rs | 8 ++++---- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 73c9e8d..6cc2bda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,14 +54,14 @@ ciborium = { version = "0.2.2" } ciborium-io = "0.2.2" cipher = "0.4.4" ctaphid = { version = "0.3.1", default-features = false } -ctaphid-dispatch = "0.2" +ctaphid-dispatch = "0.3" delog = { version = "0.1.6", features = ["std-log"] } env_logger = "0.11.0" hex-literal = "0.4.1" hmac = "0.12.1" interchange = "0.3.0" itertools = "0.14.0" -littlefs2 = "0.5.0" +littlefs2 = "0.6.0" log = "0.4.21" p256 = { version = "0.13.2", features = ["ecdh"] } rand = "0.8.4" @@ -71,17 +71,17 @@ serde_test = "1.0.176" trussed = { version = "0.1", features = ["virt"] } trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] } trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } -usbd-ctaphid = "0.2.0" +usbd-ctaphid = "0.3.0" x509-parser = "0.16.0" [package.metadata.docs.rs] features = ["dispatch"] [patch.crates-io] -admin-app = { git = "https://github.com/Nitrokey/admin-app.git", tag = "v0.1.0-nitrokey.19" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "1e1ca03a3a62ea9b802f4070ea4bce002eeb4bec" } -trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "4fe4e4e287dac1d92fcd4f97e8926497bfa9d7a9" } +admin-app = { git = "https://github.com/Nitrokey/admin-app.git", tag = "v0.1.0-nitrokey.20" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "024e0eca5fb7dbd2457831f7c7bffe4341e08775" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "7922d67e9637a87e5625aaff9e5111f0d4ec0346" } +trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "504674453c9573a30aa2f155101df49eb2af1ba7" } [profile.test] opt-level = 2 diff --git a/src/credential.rs b/src/credential.rs index 6a61024..7aabed7 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -787,7 +787,7 @@ mod test { key::{Kind, Secrecy}, store::keystore::{ClientKeystore, Keystore as _}, types::Location, - virt::{self, Ram}, + virt::{self, StoreConfig}, Platform as _, }; @@ -960,7 +960,7 @@ mod test { ); const SERIALIZED_CREDENTIAL: &[u8] = &hex!("A3000201A700A1626964684A6F686E20446F6501A16269644301020302187B03F404260582014301020306F4024C000000000000000000000000"); - virt::with_platform(Ram::default(), |mut platform| { + virt::with_platform(StoreConfig::ram(), |mut platform| { let kek = [0; 44]; let client_id = path!("fido"); let kek = { @@ -1030,7 +1030,7 @@ mod test { #[test] fn credential_ids() { - trussed::virt::with_ram_client("fido", |mut client| { + trussed::virt::with_client(StoreConfig::ram(), "fido", |mut client| { let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; let nonce = ByteArray::new([0; 12]); let data = credential_data(); @@ -1092,7 +1092,7 @@ mod test { large_blob_key: Some(ByteArray::new([0xff; 32])), third_party_payment: Some(true), }; - trussed::virt::with_ram_client("fido", |mut client| { + trussed::virt::with_client(StoreConfig::ram(), "fido", |mut client| { let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; let rp_id_hash = syscall!(client.hash_sha256(rp_id.as_ref())) .hash diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index f78c372..fa070b0 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -4,7 +4,6 @@ use std::{ borrow::Cow, cell::RefCell, fmt::{self, Debug, Formatter}, - ops::Deref as _, sync::{ atomic::{AtomicBool, Ordering}, Arc, Once, @@ -19,7 +18,7 @@ use ctaphid::{ error::{RequestError, ResponseError}, HidDevice, HidDeviceInfo, }; -use ctaphid_dispatch::{Channel, Dispatch, Requester}; +use ctaphid_dispatch::{Channel, Dispatch, Requester, DEFAULT_MESSAGE_SIZE}; use fido_authenticator::{Authenticator, Config, Conforming}; use littlefs2::{object_safe::DynFilesystem, path, path::PathBuf}; use rand::{ @@ -30,7 +29,7 @@ use trussed::{ backend::BackendId, platform::Platform as _, store::Store as _, - virt::{self, Ram}, + virt::{self, StoreConfig}, }; use trussed_staging::virt::{BackendIds, Client, Dispatcher}; @@ -199,10 +198,10 @@ impl HidDeviceInfo for DeviceInfo { } } -pub struct Device<'a>(RefCell>); +pub struct Device<'a>(RefCell>); impl<'a> Device<'a> { - fn new(requester: Requester<'a>) -> Self { + fn new(requester: Requester<'a, DEFAULT_MESSAGE_SIZE>) -> Self { Self(RefCell::new(Pipe::new(requester))) } } @@ -249,10 +248,10 @@ impl HidDevice for Device<'_> { fn with_client(files: &[(PathBuf, Vec)], f: F, inspect_ifs: F2) -> T where - F: FnOnce(Client) -> T, + F: FnOnce(Client) -> T, F2: FnOnce(&dyn DynFilesystem), { - virt::with_platform(Ram::default(), |mut platform| { + virt::with_platform(StoreConfig::ram(), |mut platform| { // virt always uses the same seed -- request some random bytes to reach a somewhat random // state let uniform = Uniform::from(0..64); @@ -261,7 +260,8 @@ where platform.rng().next_u32(); } - let ifs = platform.store().ifs(); + let store = platform.store(); + let ifs = store.ifs(); for (path, content) in files { if let Some(dir) = path.parent() { @@ -280,7 +280,7 @@ where f, ); - inspect_ifs(ifs.deref()); + inspect_ifs(ifs); result }) diff --git a/tests/virt/pipe.rs b/tests/virt/pipe.rs index 0fcdf04..5d1b3ae 100644 --- a/tests/virt/pipe.rs +++ b/tests/virt/pipe.rs @@ -93,10 +93,10 @@ enum State { Sending((Response, MessageState)), } -pub struct Pipe<'a> { +pub struct Pipe<'a, const N: usize> { queue: VecDeque<[u8; PACKET_SIZE]>, state: State, - interchange: Requester<'a>, + interchange: Requester<'a, N>, buffer: [u8; MESSAGE_SIZE], last_channel: u32, implements: u8, @@ -106,8 +106,8 @@ pub struct Pipe<'a> { version: Version, } -impl<'a> Pipe<'a> { - pub fn new(interchange: Requester<'a>) -> Self { +impl<'a, const N: usize> Pipe<'a, N> { + pub fn new(interchange: Requester<'a, N>) -> Self { Self { queue: Default::default(), state: State::Idle,