From 67cc015f51f3a846f8a38a636e2628a32b2e6538 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Thu, 1 Feb 2024 08:10:01 +0100 Subject: [PATCH 1/6] Add credBlob-extension --- examples/ctap2_discoverable_creds.rs | 44 ++++++++++++++++++-------- src/ctap2/attestation.rs | 8 ++++- src/ctap2/commands/get_assertion.rs | 22 +++++++++++-- src/ctap2/commands/make_credentials.rs | 20 +++++++++--- src/ctap2/server.rs | 35 ++++++++++++++++++++ 5 files changed, 108 insertions(+), 21 deletions(-) diff --git a/examples/ctap2_discoverable_creds.rs b/examples/ctap2_discoverable_creds.rs index 4667dcf5..09089fa8 100644 --- a/examples/ctap2_discoverable_creds.rs +++ b/examples/ctap2_discoverable_creds.rs @@ -6,18 +6,19 @@ use authenticator::{ authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, crypto::COSEAlgorithm, ctap2::server::{ - AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, - ResidentKeyRequirement, Transport, UserVerificationRequirement, + AuthenticationExtensionsClientInputs, AuthenticatorExtensionsCredBlob, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport, + UserVerificationRequirement, }, statecallback::StateCallback, Pin, StatusPinUv, StatusUpdate, }; -use getopts::Options; +use getopts::{Matches, Options}; use sha2::{Digest, Sha256}; +use std::io::Write; use std::sync::mpsc::{channel, RecvError}; use std::{env, io, thread}; -use std::io::Write; fn print_usage(program: &str, opts: Options) { println!("------------------------------------------------------------------------"); @@ -60,7 +61,12 @@ fn ask_user_choice(choices: &[PublicKeyCredentialUserEntity]) -> Option { } } -fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: u64) { +fn register_user( + manager: &mut AuthenticatorService, + username: &str, + timeout_ms: u64, + matches: &Matches, +) { println!(); println!("*********************************************************************"); println!("Asking a security key to register now with user: {username}"); @@ -168,6 +174,9 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: resident_key_req: ResidentKeyRequirement::Required, extensions: AuthenticationExtensionsClientInputs { cred_props: Some(true), + cred_blob: matches.opt_present("cred_blob").then(|| { + AuthenticatorExtensionsCredBlob::AsBytes("My short credBlob".as_bytes().to_vec()) + }), ..Default::default() }, pin: None, @@ -214,10 +223,8 @@ fn main() { "timeout in seconds", "SEC", ); - opts.optflag( - "s", - "skip_reg", - "Skip registration"); + opts.optflag("s", "skip_reg", "Skip registration"); + opts.optflag("b", "cred_blob", "With credBlob"); opts.optflag("h", "help", "print this help menu"); let matches = match opts.parse(&args[1..]) { @@ -247,7 +254,7 @@ fn main() { if !matches.opt_present("skip_reg") { for username in &["A. User", "A. Nother", "Dr. Who"] { - register_user(&mut manager, username, timeout_ms) + register_user(&mut manager, username, timeout_ms, &matches) } } @@ -337,7 +344,12 @@ fn main() { allow_list, user_verification_req: UserVerificationRequirement::Required, user_presence_req: true, - extensions: Default::default(), + extensions: AuthenticationExtensionsClientInputs { + cred_blob: matches + .opt_present("cred_blob") + .then_some(AuthenticatorExtensionsCredBlob::AsBool(true)), + ..Default::default() + }, pin: None, use_ctap1_fallback: false, }; @@ -364,7 +376,13 @@ fn main() { println!("Found credentials:"); println!( "{:?}", - assertion_object.assertion.user.clone().unwrap().name.unwrap() // Unwrapping here, as these shouldn't fail + assertion_object + .assertion + .user + .clone() + .unwrap() + .name + .unwrap() // Unwrapping here, as these shouldn't fail ); println!("-----------------------------------------------------------------"); println!("Done."); diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index ad5da1b0..7626420d 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1,3 +1,4 @@ +use super::server::AuthenticatorExtensionsCredBlob; use super::utils::{from_slice_stream, read_be_u16, read_be_u32, read_byte}; use crate::crypto::{COSEAlgorithm, CryptoError, SharedSecret}; use crate::ctap2::server::{CredentialProtectionPolicy, HMACGetSecretOutput, RpIdHash}; @@ -119,11 +120,16 @@ pub struct Extension { pub hmac_secret: Option, #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, + #[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, } impl Extension { pub fn has_some(&self) -> bool { - self.min_pin_length.is_some() || self.hmac_secret.is_some() || self.cred_protect.is_some() + self.min_pin_length.is_some() + || self.hmac_secret.is_some() + || self.cred_protect.is_some() + || self.cred_blob.is_some() } } diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index f0708543..989308d8 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -15,8 +15,8 @@ use crate::ctap2::commands::make_credentials::UserVerification; use crate::ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, - PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, - UserVerificationRequirement, + AuthenticatorExtensionsCredBlob, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, + RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_be_u32, read_byte}; use crate::errors::AuthenticatorError; @@ -253,6 +253,8 @@ pub struct GetAssertionExtensions { skip_serializing_if = "HmacGetSecretOrPrf::skip_serializing" )] pub hmac_secret: Option, + #[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, } impl From for GetAssertionExtensions { @@ -271,13 +273,17 @@ impl From for GetAssertionExtensions { .or_else( || prf.map(HmacGetSecretOrPrf::PrfUninitialized), // Cannot calculate hmac-secret inputs here because we don't yet know which eval or evalByCredential entry to use ), + cred_blob: match input.cred_blob { + Some(AuthenticatorExtensionsCredBlob::AsBool(x)) => Some(x), + _ => None, + }, } } } impl GetAssertionExtensions { fn has_content(&self) -> bool { - self.hmac_secret.is_some() + self.hmac_secret.is_some() || self.cred_blob.is_some() } } @@ -432,6 +438,11 @@ impl GetAssertion { } None => {} } + + // 3. credBlob + // The extension returns a flag in the authenticator data which we need to mirror as a + // client output. + result.extensions.cred_blob = result.assertion.auth_data.extensions.cred_blob.clone(); } } @@ -1052,6 +1063,7 @@ pub mod test { None, ), )), + cred_blob: None, }, options: GetAssertionOptions { user_presence: Some(true), @@ -1109,6 +1121,7 @@ pub mod test { Some(2), ), )), + cred_blob: None, }, options: GetAssertionOptions { user_presence: None, @@ -1154,6 +1167,7 @@ pub mod test { eval_by_credential: None, }, )), + cred_blob: None, }, options: GetAssertionOptions { user_presence: None, @@ -1175,6 +1189,7 @@ pub mod test { extensions: GetAssertionExtensions { app_id: None, hmac_secret: Some(HmacGetSecretOrPrf::PrfUnmatched), + cred_blob: None, }, options: GetAssertionOptions { user_presence: None, @@ -2832,6 +2847,7 @@ pub mod test { cred_protect: None, hmac_secret: hmac_secret_response, min_pin_length: None, + cred_blob: None, }, }, signature: vec![], diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index f0a08b33..5f772632 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -15,9 +15,9 @@ use crate::ctap2::attestation::{ use crate::ctap2::client_data::ClientDataHash; use crate::ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, - AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, CredentialProtectionPolicy, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, - RelyingParty, RpIdHash, UserVerificationRequirement, + AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, AuthenticatorExtensionsCredBlob, + CredentialProtectionPolicy, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_byte, serde_parse_err}; use crate::errors::AuthenticatorError; @@ -242,6 +242,8 @@ pub struct MakeCredentialsExtensions { pub hmac_secret: Option, #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, + #[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, } #[derive(Debug, Clone)] @@ -264,7 +266,10 @@ impl Serialize for HmacCreateSecretOrPrf { impl MakeCredentialsExtensions { fn has_content(&self) -> bool { - self.cred_protect.is_some() || self.hmac_secret.is_some() || self.min_pin_length.is_some() + self.cred_protect.is_some() + || self.hmac_secret.is_some() + || self.min_pin_length.is_some() + || self.cred_blob.is_some() } } @@ -281,6 +286,7 @@ impl From for MakeCredentialsExtensions { } }, min_pin_length: input.min_pin_length, + cred_blob: input.cred_blob, } } } @@ -409,6 +415,11 @@ impl MakeCredentials { } None | Some(HmacCreateSecretOrPrf::HmacCreateSecret(false)) => {} } + + // 3. credBlob + // The extension returns a flag in the authenticator data which we need to mirror as a + // client output. + result.extensions.cred_blob = result.att_obj.auth_data.extensions.cred_blob.clone(); } } @@ -763,6 +774,7 @@ pub mod test { ), hmac_secret: Some(HmacCreateSecretOrPrf::HmacCreateSecret(true)), min_pin_length: Some(true), + cred_blob: None, }, options: MakeCredentialsOptions { resident_key: Some(true), diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 29ed7c1b..b9ce8cbc 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -354,6 +354,35 @@ impl<'de> Deserialize<'de> for CredentialProtectionPolicy { } } +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AuthenticatorExtensionsCredBlob { + /// Used in GetAssertion-requests to request the stored blob, + /// and in MakeCredential-responses to signify if the + /// storing worked. + AsBool(bool), + /// Used in MakeCredential-requests to store a new credBlob, + /// and in GetAssertion-responses when retrieving the + /// stored blob. + #[serde(serialize_with = "vec_to_bytebuf", deserialize_with = "bytebuf_to_vec")] + AsBytes(Vec), +} + +fn vec_to_bytebuf(data: &[u8], s: S) -> Result +where + S: Serializer, +{ + ByteBuf::from(data).serialize(s) +} + +fn bytebuf_to_vec<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let bytes = ::deserialize(deserializer)?; + Ok(bytes.to_vec()) +} + #[derive(Clone, Debug, Default)] pub struct AuthenticationExtensionsClientInputs { pub app_id: Option, @@ -364,6 +393,9 @@ pub struct AuthenticationExtensionsClientInputs { pub hmac_get_secret: Option, pub min_pin_length: Option, pub prf: Option, + /// MakeCredential-requests use AsBytes + /// GetAssertion-requests use AsBool + pub cred_blob: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -495,6 +527,9 @@ pub struct AuthenticationExtensionsClientOutputs { pub hmac_create_secret: Option, pub hmac_get_secret: Option, pub prf: Option, + /// MakeCredential-responses use AsBool + /// GetAssertion-responses use AsBytes + pub cred_blob: Option, } #[derive(Clone, Debug, PartialEq, Eq)] From db728df63a47f64c85a1a83a10b9af4a7c56c827 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Thu, 1 Feb 2024 16:14:28 +0100 Subject: [PATCH 2/6] Add largeBlobKeys-extension --- Cargo.toml | 2 + examples/ctap2.rs | 3 + examples/ctap2_discoverable_creds.rs | 90 ++++- examples/interactive_management.rs | 3 + examples/set_pin.rs | 3 + examples/test_exclude_list.rs | 3 + src/ctap2/commands/get_assertion.rs | 64 +++- src/ctap2/commands/large_blobs.rs | 451 +++++++++++++++++++++++++ src/ctap2/commands/make_credentials.rs | 31 ++ src/ctap2/commands/mod.rs | 2 + src/ctap2/mod.rs | 65 +++- src/ctap2/preflight.rs | 3 + src/ctap2/server.rs | 1 + src/errors.rs | 1 + src/statemachine.rs | 4 - src/status_update.rs | 4 + 16 files changed, 706 insertions(+), 24 deletions(-) create mode 100644 src/ctap2/commands/large_blobs.rs diff --git a/Cargo.toml b/Cargo.toml index 7b2b4dad..7bbd4e74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,8 @@ env_logger = "^0.6" getopts = "^0.2" assert_matches = "1.2" rpassword = "5.0" +flate3 = "1" +aes-gcm = "0.10" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } diff --git a/examples/ctap2.rs b/examples/ctap2.rs index be742a5e..78701bdc 100644 --- a/examples/ctap2.rs +++ b/examples/ctap2.rs @@ -136,6 +136,9 @@ fn main() { Ok(StatusUpdate::SelectResultNotice(_, _)) => { panic!("Unexpected select device notice") } + Ok(StatusUpdate::LargeBlobData(_, _)) => { + panic!("Unexpected large blob data request") + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/examples/ctap2_discoverable_creds.rs b/examples/ctap2_discoverable_creds.rs index 09089fa8..30c1b393 100644 --- a/examples/ctap2_discoverable_creds.rs +++ b/examples/ctap2_discoverable_creds.rs @@ -2,22 +2,29 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use aes_gcm::{ + aead::{Aead, AeadCore, KeyInit, OsRng, Payload}, + Aes256Gcm, Key, +}; use authenticator::{ authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, crypto::COSEAlgorithm, - ctap2::server::{ - AuthenticationExtensionsClientInputs, AuthenticatorExtensionsCredBlob, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, - PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport, - UserVerificationRequirement, + ctap2::{ + commands::large_blobs::LargeBlobArrayElement, + server::{ + AuthenticationExtensionsClientInputs, AuthenticatorExtensionsCredBlob, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport, + UserVerificationRequirement, + }, }, statecallback::StateCallback, Pin, StatusPinUv, StatusUpdate, }; use getopts::{Matches, Options}; use sha2::{Digest, Sha256}; -use std::io::Write; use std::sync::mpsc::{channel, RecvError}; +use std::{convert::TryInto, io::Write}; use std::{env, io, thread}; fn print_usage(program: &str, opts: Options) { @@ -82,6 +89,8 @@ fn register_user( ); let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); + let has_large_blob = matches.opt_present("large_blob_key"); + let name = username.to_string(); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { match status_rx.recv() { @@ -137,6 +146,37 @@ fn register_user( Ok(StatusUpdate::SelectResultNotice(_, _)) => { panic!("Unexpected select result notice") } + Ok(StatusUpdate::LargeBlobData(tx, key)) => { + if has_large_blob { + // Let origData equal the opaque large-blob data. + let orig_data = format!("This is the large blob for {name}").into_bytes(); + // Let origSize be the length, in bytes, of origData. + let orig_size = orig_data.len() as u64; + // Let plaintext equal origData after compression with DEFLATE [RFC1951]. + let plaintext = flate3::deflate(&orig_data); + // Let nonce be a fresh, random, 12-byte value. + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + // Let ciphertext be the AEAD_AES_256_GCM authenticated encryption of plaintext using key, nonce, and the associated data as specified above. + let gcm_key = Key::::from_slice(&key); + let cipher = Aes256Gcm::new(gcm_key); + let mut payload = Payload::from(plaintext.as_ref()); + // Associated data: The value 0x626c6f62 ("blob") || uint64LittleEndian(origSize). + let mut aad = b"blob".to_vec(); + aad.extend_from_slice(&orig_size.to_le_bytes()); + payload.aad = &aad; + let ciphertext = cipher + .encrypt(&nonce, payload) + .expect("Failed to encrypt plaintext large blob"); + let elem = LargeBlobArrayElement { + ciphertext, + nonce: nonce.to_vec().try_into().unwrap(), + orig_size, + }; + tx.send(elem).expect("Failed to send large blob element"); + } else { + panic!("Unexpected large blob data request"); + } + } Err(RecvError) => { println!("STATUS: end"); return; @@ -177,6 +217,7 @@ fn register_user( cred_blob: matches.opt_present("cred_blob").then(|| { AuthenticatorExtensionsCredBlob::AsBytes("My short credBlob".as_bytes().to_vec()) }), + large_blob_key: matches.opt_present("large_blob_key").then_some(true), ..Default::default() }, pin: None, @@ -208,6 +249,30 @@ fn register_user( } println!("Register result: {:?}", &attestation_object); + + if matches.opt_present("large_blob_key") { + println!("Adding large blob key"); + } +} + +fn extract_associated_large_blobs(key: Vec, array: Vec) -> Vec { + let valid_elements = array + .iter() + .filter_map(|e| { + let gcm_key = Key::::from_slice(&key); + let cipher = Aes256Gcm::new(gcm_key); + let mut payload = Payload::from(e.ciphertext.as_slice()); + // Associated data: The value 0x626c6f62 ("blob") || uint64LittleEndian(origSize). + let mut aad = b"blob".to_vec(); + aad.extend_from_slice(&e.orig_size.to_le_bytes()); + payload.aad = &aad; + let plaintext = cipher.decrypt(e.nonce.as_slice().into(), payload).ok(); + plaintext + }) + .map(|d| flate3::inflate(&d)) + .map(|d| String::from_utf8_lossy(&d).to_string()) + .collect(); + valid_elements } fn main() { @@ -225,7 +290,7 @@ fn main() { ); opts.optflag("s", "skip_reg", "Skip registration"); opts.optflag("b", "cred_blob", "With credBlob"); - + opts.optflag("l", "large_blob_key", "With largeBlobKey-extension"); opts.optflag("h", "help", "print this help menu"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, @@ -329,6 +394,9 @@ fn main() { let idx = ask_user_choice(&users); index_sender.send(idx).expect("Failed to send choice"); } + Ok(StatusUpdate::LargeBlobData(..)) => { + panic!("Unexpected large blob data request") + } Err(RecvError) => { println!("STATUS: end"); return; @@ -348,6 +416,7 @@ fn main() { cred_blob: matches .opt_present("cred_blob") .then_some(AuthenticatorExtensionsCredBlob::AsBool(true)), + large_blob_key: matches.opt_present("large_blob_key").then_some(true), ..Default::default() }, pin: None, @@ -385,6 +454,13 @@ fn main() { .unwrap() // Unwrapping here, as these shouldn't fail ); println!("-----------------------------------------------------------------"); + if matches.opt_present("large_blob_key") { + let large_blobs = extract_associated_large_blobs( + assertion_object.large_blob_key.unwrap(), + assertion_object.large_blob_array.unwrap(), + ); + println!("Associated large blobs: {large_blobs:?}"); + } println!("Done."); break; } diff --git a/examples/interactive_management.rs b/examples/interactive_management.rs index 6ef0e9dc..c0dbe4bd 100644 --- a/examples/interactive_management.rs +++ b/examples/interactive_management.rs @@ -730,6 +730,9 @@ fn interactive_status_callback(status_rx: Receiver) { Ok(StatusUpdate::SelectResultNotice(_, _)) => { panic!("Unexpected select device notice") } + Ok(StatusUpdate::LargeBlobData(_, _)) => { + panic!("Unexpected large blob data request") + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/examples/set_pin.rs b/examples/set_pin.rs index 5534ca08..98cf8343 100644 --- a/examples/set_pin.rs +++ b/examples/set_pin.rs @@ -117,6 +117,9 @@ fn main() { Ok(StatusUpdate::SelectResultNotice(_, _)) => { panic!("Unexpected select device notice") } + Ok(StatusUpdate::LargeBlobData(_, _)) => { + panic!("Unexpected large blob data request") + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/examples/test_exclude_list.rs b/examples/test_exclude_list.rs index 958d66b1..fa7716b9 100644 --- a/examples/test_exclude_list.rs +++ b/examples/test_exclude_list.rs @@ -129,6 +129,9 @@ fn main() { Ok(StatusUpdate::SelectResultNotice(_, _)) => { panic!("Unexpected select device notice") } + Ok(StatusUpdate::LargeBlobData(_, _)) => { + panic!("Unexpected large blob data request") + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 989308d8..daa491ee 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -1,4 +1,5 @@ use super::get_info::AuthenticatorInfo; +use super::large_blobs::LargeBlobArrayElement; use super::{ Command, CommandError, CtapResponse, PinUvAuthCommand, PinUvAuthResult, RequestCtap1, RequestCtap2, Retryable, StatusCode, @@ -255,6 +256,8 @@ pub struct GetAssertionExtensions { pub hmac_secret: Option, #[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")] pub cred_blob: Option, + #[serde(rename = "largeBlobKey", skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option, } impl From for GetAssertionExtensions { @@ -277,13 +280,14 @@ impl From for GetAssertionExtensions { Some(AuthenticatorExtensionsCredBlob::AsBool(x)) => Some(x), _ => None, }, + large_blob_key: input.large_blob_key, } } } impl GetAssertionExtensions { fn has_content(&self) -> bool { - self.hmac_secret.is_some() || self.cred_blob.is_some() + self.hmac_secret.is_some() || self.cred_blob.is_some() || self.large_blob_key.is_some() } } @@ -624,22 +628,31 @@ impl RequestCtap2 for GetAssertion { let assertion: GetAssertionResponse = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; let number_of_credentials = assertion.number_of_credentials.unwrap_or(1); - + let user_selected = assertion.user_selected; + let large_blob_key = assertion.large_blob_key.clone(); let mut results = Vec::with_capacity(number_of_credentials); results.push(GetAssertionResult { assertion: assertion.into(), attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + user_selected, + large_blob_key, + large_blob_array: None, }); let msg = GetNextAssertion; // We already have one, so skipping 0 for _ in 1..number_of_credentials { let assertion = dev.send_cbor(&msg)?; + let user_selected = assertion.user_selected; + let large_blob_key = assertion.large_blob_key.clone(); results.push(GetAssertionResult { assertion: assertion.into(), attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + user_selected, + large_blob_key, + large_blob_array: None, }); } @@ -690,6 +703,9 @@ pub struct GetAssertionResult { pub assertion: Assertion, pub attachment: AuthenticatorAttachment, pub extensions: AuthenticationExtensionsClientOutputs, + pub user_selected: Option, + pub large_blob_key: Option>, + pub large_blob_array: Option>, } impl GetAssertionResult { @@ -727,6 +743,9 @@ impl GetAssertionResult { assertion, attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + user_selected: None, + large_blob_key: None, + large_blob_array: None, }) } } @@ -738,6 +757,8 @@ pub struct GetAssertionResponse { pub signature: Vec, pub user: Option, pub number_of_credentials: Option, + pub user_selected: Option, + pub large_blob_key: Option>, } impl CtapResponse for GetAssertionResponse {} @@ -765,41 +786,55 @@ impl<'de> Deserialize<'de> for GetAssertionResponse { let mut signature = None; let mut user = None; let mut number_of_credentials = None; + let mut user_selected = None; + let mut large_blob_key = None; while let Some(key) = map.next_key()? { match key { - 1 => { + 0x01 => { if credentials.is_some() { return Err(M::Error::duplicate_field("credentials")); } credentials = Some(map.next_value()?); } - 2 => { + 0x02 => { if auth_data.is_some() { return Err(M::Error::duplicate_field("auth_data")); } auth_data = Some(map.next_value()?); } - 3 => { + 0x03 => { if signature.is_some() { return Err(M::Error::duplicate_field("signature")); } let signature_bytes: ByteBuf = map.next_value()?; - let signature_bytes: Vec = signature_bytes.into_vec(); - signature = Some(signature_bytes); + signature = Some(signature_bytes.into_vec()); } - 4 => { + 0x04 => { if user.is_some() { return Err(M::Error::duplicate_field("user")); } user = map.next_value()?; } - 5 => { + 0x05 => { if number_of_credentials.is_some() { return Err(M::Error::duplicate_field("number_of_credentials")); } number_of_credentials = Some(map.next_value()?); } + 0x06 => { + if user_selected.is_some() { + return Err(M::Error::duplicate_field("user_selected")); + } + user_selected = Some(map.next_value()?); + } + 0x07 => { + if large_blob_key.is_some() { + return Err(M::Error::duplicate_field("large_blob_key")); + } + let large_blob_key_bytes: ByteBuf = map.next_value()?; + large_blob_key = Some(large_blob_key_bytes.into_vec()); + } k => return Err(M::Error::custom(format!("unexpected key: {k:?}"))), } } @@ -813,6 +848,8 @@ impl<'de> Deserialize<'de> for GetAssertionResponse { signature, user, number_of_credentials, + user_selected, + large_blob_key, }) } } @@ -1015,6 +1052,9 @@ pub mod test { assertion: expected_assertion, attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + user_selected: None, + large_blob_key: None, + large_blob_array: None, }]; let response = device.send_cbor(&assertion).unwrap(); assert_eq!(response, expected); @@ -1339,6 +1379,9 @@ pub mod test { assertion: expected_assertion, attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + user_selected: None, + large_blob_key: None, + large_blob_array: None, }]; assert_eq!(response, expected); } @@ -1481,6 +1524,9 @@ pub mod test { assertion: expected_assertion, attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + user_selected: None, + large_blob_key: None, + large_blob_array: None, }]; assert_eq!(response, expected); } diff --git a/src/ctap2/commands/large_blobs.rs b/src/ctap2/commands/large_blobs.rs new file mode 100644 index 00000000..24e9c654 --- /dev/null +++ b/src/ctap2/commands/large_blobs.rs @@ -0,0 +1,451 @@ +use crate::{ + crypto::{PinUvAuthParam, PinUvAuthToken}, + ctap2::server::UserVerificationRequirement, + errors::AuthenticatorError, + transport::errors::HIDError, + FidoDevice, +}; +use serde::{ + de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor}, + ser::{Error as SerError, SerializeMap}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::{from_slice, to_vec, Value}; +use sha2::{Digest, Sha256}; +use std::fmt; + +use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode}; + +#[derive(Debug)] +pub(crate) struct LargeBlobs { + get: Option, // (0x01) Unsigned integer Optional The number of bytes requested to read. MUST NOT be present if set is present. + set: Option, // (0x02) Byte String Optional A fragment to write. MUST NOT be present if get is present. + offset: u64, // (0x03) Unsigned integer Required The byte offset at which to read/write. + length: Option, // (0x04) Unsigned integer Optional The total length of a write operation. Present if, and only if, set is present and offset is zero. + pin_uv_auth_param: Option, // (0x05) authenticate(pinUvAuthToken, 32×0xff || h’0c00' || uint32LittleEndian(offset) || SHA-256(contents of set byte string, i.e. not including an outer CBOR tag with major type two)) +} + +impl PinUvAuthCommand for LargeBlobs { + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + // pinUvAuthParam (0x05): the result of calling + // authenticate(pinUvAuthToken, 32×0xff || h’0c00' || uint32LittleEndian(offset) || SHA-256(contents of set byte string, i.e. not including an outer CBOR tag with major type two)) + let mut data = vec![0xff; 32]; + data.extend([0x0c, 0x00]); + data.extend((self.offset as u32).to_le_bytes()); + if let Some(ref set) = self.set { + let mut hasher = Sha256::new(); + hasher.update(set.as_slice()); + data.extend(hasher.finalize().as_slice()); + } + param = Some(token.derive(&data).map_err(CommandError::Crypto)?); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn can_skip_user_verification( + &mut self, + _info: &crate::AuthenticatorInfo, + _uv: UserVerificationRequirement, + ) -> bool { + // "discouraged" does not exist for LargeBlobs + false + } + + fn set_uv_option(&mut self, _uv: Option) { + /* No-op */ + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } + + fn get_rp_id(&self) -> Option<&String> { + None + } +} + +impl Serialize for LargeBlobs { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.set.is_none() && self.get.is_none() { + return Err(SerError::custom("Either set or get has to be set")); + } + let mut map_len = 2; // get/set and offset + if self.length.is_some() { + map_len += 1; + } + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + if let Some(ref get) = self.get { + map.serialize_entry(&0x01, get)?; + } + if let Some(ref set) = self.set { + map.serialize_entry(&0x02, set)?; + } + map.serialize_entry(&0x03, &self.offset)?; + if let Some(ref length) = self.length { + map.serialize_entry(&0x04, length)?; + } + if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param { + map.serialize_entry(&0x05, pin_uv_auth_param)?; + map.serialize_entry(&0x06, &pin_uv_auth_param.pin_protocol.id())?; + } + map.end() + } +} + +impl RequestCtap2 for LargeBlobs { + type Output = LargeBlobSegment; + + fn command(&self) -> Command { + Command::LargeBlobs + } + + fn wire_format(&self) -> Result, HIDError> { + let output = to_vec(&self).map_err(CommandError::Serializing)?; + trace!("client subcommmand: {:04X?}", &output); + Ok(output) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: FidoDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + let payload = &input[1..]; + if status.is_ok() { + if payload.len() > 1 { + Ok(payload.to_vec()) + } else { + // Some subcommands return only an OK-status without any data + Ok(Vec::new()) + } + } else { + let data: Option = if input.len() > 1 { + Some(from_slice(payload).map_err(CommandError::Deserializing)?) + } else { + None + }; + Err(CommandError::StatusCode(status, data).into()) + } + } + + fn send_to_virtual_device( + &self, + _dev: &mut Dev, + ) -> Result { + unimplemented!() + } +} + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct LargeBlobArrayElement { + /// AEAD_AES_256_GCM ciphertext, implicitly including the AEAD “authentication tag” at the end. + pub ciphertext: Vec, + /// AEAD_AES_256_GCM nonce. MUST be exactly 12 bytes long. + pub nonce: [u8; 12], + /// Contains the length, in bytes, of the uncompressed data. + pub orig_size: u64, +} + +impl Serialize for LargeBlobArrayElement { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let map_len = 3; // The Array is the only one + let mut map = serializer.serialize_map(Some(map_len))?; + map.serialize_entry(&0x01, &self.ciphertext)?; + map.serialize_entry(&0x02, &self.nonce)?; + map.serialize_entry(&0x03, &self.orig_size)?; + map.end() + } +} + +impl<'de> Deserialize<'de> for LargeBlobArrayElement { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct LargeBlobArrayElementVisitor; + + impl<'de> Visitor<'de> for LargeBlobArrayElementVisitor { + type Value = LargeBlobArrayElement; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut ciphertext = None; // Sub-level (0x01) + let mut nonce = None; // Sub-level (0x02) + let mut orig_size = None; // Sub-level (0x03) + + // Parsing out the top-level "large-blob array" + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if ciphertext.is_some() { + return Err(SerdeError::duplicate_field("ciphertext")); + } + ciphertext = Some(map.next_value()?); + } + 0x02 => { + if nonce.is_some() { + return Err(SerdeError::duplicate_field("nonce")); + } + nonce = Some(map.next_value()?); + } + 0x03 => { + if orig_size.is_some() { + return Err(SerdeError::duplicate_field("orig_size")); + } + orig_size = Some(map.next_value()?); + } + k => { + warn!("LargeBlobArray: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + + let ciphertext = ciphertext.ok_or_else(|| M::Error::missing_field("ciphertext"))?; + let nonce = nonce.ok_or_else(|| M::Error::missing_field("nonce"))?; + let orig_size = orig_size.ok_or_else(|| M::Error::missing_field("orig_size"))?; + + Ok(LargeBlobArrayElement { + ciphertext, + nonce, + orig_size, + }) + } + } + deserializer.deserialize_bytes(LargeBlobArrayElementVisitor) + } +} + +#[derive(Default, Debug)] +pub struct LargeBlobsResponse { + pub(crate) large_blob_array: Vec, + /// Truncated SHA-256 hash of the preceding bytes + pub(crate) hash: [u8; 16], + pub(crate) byte_len: u64, +} + +impl<'de> Deserialize<'de> for LargeBlobsResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct LargeBlobsResponseVisitor; + + impl<'de> Visitor<'de> for LargeBlobsResponseVisitor { + type Value = LargeBlobsResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + // this data is serialized as a CBOR-encoded array (called the large-blob array) of large-blob maps, concatenated with 16 following bytes. Those final 16 bytes are the truncated SHA-256 hash of the preceding bytes. + let mut response = None; // Top-level 0x01 + + // Parsing out the top-level "large-blob array" + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if response.is_some() { + return Err(SerdeError::duplicate_field("response")); + } + let payload: ByteBuf = map.next_value()?; + // Note: the minimum length of a serialized large-blob array is 17 bytes. Omitting 16 bytes for the trailing SHA-256 hash, this leaves just one byte. This is the size of an empty CBOR array. + if payload.len() < 17 { + return Err(SerdeError::invalid_length( + payload.len(), + &">= 17 bytes", + )); + } + // split off trailing hash-bytes + let (mut large_blob, hash_slice) = payload.split_at(payload.len() - 16); + + let mut hasher = Sha256::new(); + hasher.update(large_blob); + let expected_hash = hasher.finalize(); + // The initial serialized large-blob array is the value of the serialized large-blob array on a fresh authenticator, as well as immediately after a reset. It is the byte string h'8076be8b528d0075f7aae98d6fa57a6d3c', which is an empty CBOR array (80) followed by LEFT(SHA-256(h'80'), 16). + let default_large_blob = [ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, + 0x8d, 0x6f, 0xa5, 0x7a, 0x6d, 0x3c, + ]; + // Once complete, the platform MUST confirm that the embedded SHA-256 hash is correct, based on the definition above. If not, the configuration is corrupt and the platform MUST discard it and act as if the initial serialized large-blob array was received. + if &expected_hash.as_slice()[0..16] != hash_slice { + warn!("Large blob array hash doesn't match with the expected value! Assuming an empty array."); + large_blob = &default_large_blob; + } + + let byte_len = large_blob.len() as u64; + let large_blob_array: Vec = + from_slice(large_blob).unwrap(); + let mut hash = [0u8; 16]; + hash.copy_from_slice(hash_slice); + response = Some(LargeBlobsResponse { + large_blob_array, + hash, + byte_len, + }); + } + k => { + warn!("LargeBlobsResponse: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + let response = + response.ok_or_else(|| M::Error::missing_field("large_blob_bytes"))?; + + Ok(response) + } + } + deserializer.deserialize_bytes(LargeBlobsResponseVisitor) + } +} + +pub type LargeBlobSegment = Vec; + +impl CtapResponse for LargeBlobSegment {} + +pub fn read_large_blob_array( + dev: &mut Dev, + keep_alive: &dyn Fn() -> bool, +) -> Result +where + Dev: FidoDevice, +{ + // Spec: + // A per-authenticator constant, maxFragmentLength, is here defined as the value of maxMsgSize (from the authenticatorGetInfo response) minus 64. + // If no maxMsgSize is given in the authenticatorGetInfo response) then it defaults to 1024, leaving maxFragmentLength to default to 960. + let max_fragment_length = dev + .get_authenticator_info() + .and_then(|i| i.max_msg_size) + .unwrap_or(1024) + - 64; + let mut bytes = vec![]; + let mut offset = 0; + loop { + let cmd = LargeBlobs { + get: Some(max_fragment_length as u64), + set: None, + offset, + length: None, + pin_uv_auth_param: None, + }; + let mut segment = dev.send_cbor_cancellable(&cmd, keep_alive)?; + let segment_len = segment.len(); + bytes.append(&mut segment); + // Spec: + // If the length of the response is equal to the value of get then more data may be + // available and the platform SHOULD repeatedly issue requests, each time updating offset + // to equal the amount of data received so far. It stops once a short (or empty) + // fragment is returned. + if segment_len < max_fragment_length { + // The last segment was smaller than the max-size + // so we have read all there is to read. + break; + } else { + // There is still more data. So set the offset and repeat + offset += segment_len as u64; + continue; + } + } + let response: LargeBlobsResponse = from_slice(&bytes).map_err(CommandError::Deserializing)?; + Ok(response) +} + +pub fn write_large_blob_segment( + dev: &mut Dev, + keep_alive: &dyn Fn() -> bool, + bytes: &[u8], + initial_offset: u64, + pin_uv_auth_token: Option, +) -> Result<(), AuthenticatorError> +where + Dev: FidoDevice, +{ + // Spec: + // A per-authenticator constant, maxFragmentLength, is here defined as the value of maxMsgSize (from the authenticatorGetInfo response) minus 64. + // If no maxMsgSize is given in the authenticatorGetInfo response) then it defaults to 1024, leaving maxFragmentLength to default to 960. + let max_fragment_length = dev + .get_authenticator_info() + .and_then(|i| i.max_msg_size) + .unwrap_or(1024) + - 64; + let total_length = bytes.len(); + let mut offset = initial_offset; + for chunk in bytes.chunks(max_fragment_length) { + let chunk_len = chunk.len(); + let mut cmd = LargeBlobs { + get: None, + set: Some(ByteBuf::from(chunk)), + offset, + length: if offset == 0 { + Some(total_length as u64) + } else { + None + }, + pin_uv_auth_param: None, + }; + cmd.set_pin_uv_auth_param(pin_uv_auth_token.clone())?; + dev.send_cbor_cancellable(&cmd, keep_alive)?; + offset += chunk_len as u64; + } + Ok(()) +} + +pub fn add_large_blob( + dev: &mut Dev, + keep_alive: &dyn Fn() -> bool, + blob: LargeBlobArrayElement, + pin_uv_auth_token: Option, +) -> Result<(), AuthenticatorError> +where + Dev: FidoDevice, +{ + let mut array = read_large_blob_array(dev, keep_alive)?; + // Adding it + array.large_blob_array.push(blob); + // Then rewriting the whole array + let mut bytes = to_vec(&array.large_blob_array).map_err(CommandError::Serializing)?; + + let mut hasher = Sha256::new(); + hasher.update(&bytes); + let hash = hasher.finalize(); + bytes.extend_from_slice(&hash[..16]); + write_large_blob_segment(dev, keep_alive, &bytes, 0, pin_uv_auth_token) +} diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 5f772632..97248500 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -28,6 +28,7 @@ use serde::{ de::{Error as DesError, MapAccess, Unexpected, Visitor}, Deserialize, Deserializer, Serialize, Serializer, }; +use serde_bytes::ByteBuf; use serde_cbor::{self, de::from_slice, ser, Value}; use std::fmt; use std::io::{Cursor, Read}; @@ -37,6 +38,8 @@ pub struct MakeCredentialsResult { pub att_obj: AttestationObject, pub attachment: AuthenticatorAttachment, pub extensions: AuthenticationExtensionsClientOutputs, + pub ep_attestation: Option, + pub large_blob_key: Option>, } impl MakeCredentialsResult { @@ -106,6 +109,8 @@ impl MakeCredentialsResult { att_obj, attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + ep_attestation: None, + large_blob_key: None, }) } } @@ -131,6 +136,8 @@ impl<'de> Deserialize<'de> for MakeCredentialsResult { let mut format: Option<&str> = None; let mut auth_data: Option = None; let mut att_stmt: Option = None; + let mut ep_attestation: Option = None; + let mut large_blob_key: Option> = None; while let Some(key) = map.next_key()? { match key { @@ -175,6 +182,20 @@ impl<'de> Deserialize<'de> for MakeCredentialsResult { } } } + 4 => { + if ep_attestation.is_some() { + return Err(M::Error::duplicate_field("ep_attestation")); + } + let ep_attestation_val: bool = map.next_value()?; + ep_attestation = Some(ep_attestation_val); + } + 5 => { + if large_blob_key.is_some() { + return Err(M::Error::duplicate_field("large_blob_key")); + } + let large_blob_key_bytes: ByteBuf = map.next_value()?; + large_blob_key = Some(large_blob_key_bytes.into_vec()); + } _ => continue, } } @@ -191,6 +212,8 @@ impl<'de> Deserialize<'de> for MakeCredentialsResult { }, attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + ep_attestation, + large_blob_key, }) } } @@ -244,6 +267,8 @@ pub struct MakeCredentialsExtensions { pub min_pin_length: Option, #[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")] pub cred_blob: Option, + #[serde(rename = "largeBlobKey", skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option, } #[derive(Debug, Clone)] @@ -270,6 +295,7 @@ impl MakeCredentialsExtensions { || self.hmac_secret.is_some() || self.min_pin_length.is_some() || self.cred_blob.is_some() + || self.large_blob_key.is_some() } } @@ -287,6 +313,7 @@ impl From for MakeCredentialsExtensions { }, min_pin_length: input.min_pin_length, cred_blob: input.cred_blob, + large_blob_key: input.large_blob_key, } } } @@ -727,6 +754,8 @@ pub mod test { att_obj: create_attestation_obj(), attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + ep_attestation: None, + large_blob_key: None, }; assert_eq!(make_cred_result, expected); @@ -975,6 +1004,8 @@ pub mod test { att_obj, attachment: AuthenticatorAttachment::Unknown, extensions: Default::default(), + ep_attestation: None, + large_blob_key: None, }; assert_eq!(make_cred_result, expected); diff --git a/src/ctap2/commands/mod.rs b/src/ctap2/commands/mod.rs index 26144d21..b33562f2 100644 --- a/src/ctap2/commands/mod.rs +++ b/src/ctap2/commands/mod.rs @@ -18,6 +18,7 @@ pub mod get_assertion; pub mod get_info; pub mod get_next_assertion; pub mod get_version; +pub mod large_blobs; pub mod make_credentials; pub mod reset; pub mod selection; @@ -209,6 +210,7 @@ pub enum Command { BioEnrollment = 0x09, CredentialManagement = 0x0A, Selection = 0x0B, + LargeBlobs = 0x0C, AuthenticatorConfig = 0x0D, BioEnrollmentPreview = 0x40, CredentialManagementPreview = 0x41, diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index c51bb745..c9a71f49 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -28,7 +28,8 @@ use crate::ctap2::commands::make_credentials::{ }; use crate::ctap2::commands::reset::Reset; use crate::ctap2::commands::{ - repackage_pin_errors, CommandError, PinUvAuthCommand, PinUvAuthResult, RequestCtap2, StatusCode, + large_blobs, repackage_pin_errors, CommandError, PinUvAuthCommand, PinUvAuthResult, + RequestCtap2, StatusCode, }; use crate::ctap2::preflight::{ do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2, @@ -48,6 +49,7 @@ use std::thread; use std::time::Duration; use self::commands::get_info::AuthenticatorVersion; +use self::commands::large_blobs::LargeBlobArrayElement; macro_rules! unwrap_option { ($item: expr, $callback: expr) => { @@ -192,6 +194,25 @@ fn ask_user_for_pin( } } +fn ask_user_for_large_blob( + status: &Sender, + key: &[u8], +) -> Result { + info!( + "Make credentials was successful. Now asking the user for the additional large blob data" + ); + let (tx, rx) = channel(); + send_status(status, crate::StatusUpdate::LargeBlobData(tx, key.to_vec())); + match rx.recv() { + Ok(elem) => Ok(elem), + Err(RecvError) => { + // recv() can only fail, if the other side is dropping the Sender. + info!("Callback dropped the channel. Aborting."); + Err(AuthenticatorError::CancelledByUser) + } + } +} + /// Try to fetch PinUvAuthToken from the device and derive from it PinUvAuthParam. /// Prefer UV, fallback to PIN. /// Prefer newer pinUvAuth-methods, if supported by the device. @@ -466,6 +487,7 @@ pub fn register( ))); return false; } + let has_large_blob = args.extensions.large_blob_key == Some(true); let mut makecred = MakeCredentials::new( ClientDataHash(args.client_data_hash), @@ -482,8 +504,11 @@ pub fn register( while alive() { // Requesting both because pre-flighting (credential list filtering) // can potentially send GetAssertion-commands - let permissions = + let mut permissions = PinUvAuthTokenPermission::MakeCredential | PinUvAuthTokenPermission::GetAssertion; + if has_large_blob { + permissions |= PinUvAuthTokenPermission::LargeBlobWrite; + } let pin_uv_auth_result = unwrap_result!( determine_puap_if_needed( @@ -536,6 +561,22 @@ pub fn register( let resp = dev.send_msg_cancellable(&makecred, alive); match resp { Ok(result) => { + if has_large_blob && result.large_blob_key.is_some() { + // NOTE: We 'ask' the user for the large blob here, simply to avoid more dependencies. We could do this non-interactively, by adding the plaintext to the RegisterArgs and calculate all necessary things right here, but we would need AEAD-algos and DEFLATE. Leaving that (currently) for the user to do. If we decide to put it here, see ctap2_discoverable_creds.rs for the required steps. + if let Ok(blob) = + ask_user_for_large_blob(&status, result.large_blob_key.as_ref().unwrap()) + { + if let Err(e) = large_blobs::add_large_blob( + dev, + alive, + blob, + pin_uv_auth_result.get_pin_uv_auth_token(), + ) { + // Only warn but continue, as the actual registering worked. + warn!("Failed to write large blob: {e:?}"); + } + } + } callback.call(Ok(result)); return true; } @@ -589,6 +630,7 @@ pub fn sign( } } + let has_large_blob = args.extensions.large_blob_key == Some(true); let mut get_assertion = GetAssertion::new( client_data_hash, rp_id, @@ -681,8 +723,21 @@ pub fn sign( handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv); } }; + // NOTE: We send here the whole array, in order to keep dependencies down. We could + // extract, decipher and inflate the individual elements here, using the large_blob_key + // and only return valid elements. But for that, we would need AEAD and DEFLATE-algos. + let large_blob_array = + if has_large_blob && results.iter().any(|f| f.large_blob_key.is_some()) { + large_blobs::read_large_blob_array(dev, alive) + .ok() + .map(|x| x.large_blob_array) + } else { + None + }; if results.len() == 1 { - callback.call(Ok(results.swap_remove(0))); + let mut result = results.swap_remove(0); + result.large_blob_array = large_blob_array; + callback.call(Ok(result)); return true; } let (tx, rx) = channel(); @@ -696,7 +751,9 @@ pub fn sign( ); match rx.recv() { Ok(Some(index)) if index < results.len() => { - callback.call(Ok(results.swap_remove(index))); + let mut result = results.swap_remove(index); + result.large_blob_array = large_blob_array; + callback.call(Ok(result)); return true; } _ => { diff --git a/src/ctap2/preflight.rs b/src/ctap2/preflight.rs index 65740421..baf11275 100644 --- a/src/ctap2/preflight.rs +++ b/src/ctap2/preflight.rs @@ -300,6 +300,9 @@ pub mod tests { }, attachment: AuthenticatorAttachment::Platform, extensions: AuthenticationExtensionsClientOutputs::default(), + user_selected: None, + large_blob_key: None, + large_blob_array: None, } } diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index b9ce8cbc..ff75f592 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -396,6 +396,7 @@ pub struct AuthenticationExtensionsClientInputs { /// MakeCredential-requests use AsBytes /// GetAssertion-requests use AsBool pub cred_blob: Option, + pub large_blob_key: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] diff --git a/src/errors.rs b/src/errors.rs index 0e3b969b..0c516125 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -19,6 +19,7 @@ pub enum UnsupportedOption { PubCredParams, ResidentKey, UserVerification, + LargeBlobs, } #[derive(Debug)] diff --git a/src/statemachine.rs b/src/statemachine.rs index 71bda827..c8ac631d 100644 --- a/src/statemachine.rs +++ b/src/statemachine.rs @@ -3,11 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::authenticatorservice::{RegisterArgs, SignArgs}; - use crate::ctap2; - use crate::ctap2::commands::client_pin::Pin; - use crate::errors::AuthenticatorError; use crate::statecallback::StateCallback; use crate::status_update::{send_status, InteractiveUpdate}; @@ -18,7 +15,6 @@ use crate::transport::platform::transaction::Transaction; use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol}; use crate::{InteractiveRequest, ManageResult}; use std::sync::mpsc::{channel, RecvTimeoutError, Sender}; - use std::time::Duration; #[derive(Default)] diff --git a/src/status_update.rs b/src/status_update.rs index c4a7fee7..14312c8e 100644 --- a/src/status_update.rs +++ b/src/status_update.rs @@ -5,6 +5,7 @@ use crate::{ authenticator_config::{AuthConfigCommand, AuthConfigResult}, bio_enrollment::BioTemplateId, get_info::AuthenticatorInfo, + large_blobs::LargeBlobArrayElement, PinUvAuthResult, }, server::{PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity}, @@ -107,6 +108,9 @@ pub enum StatusUpdate { InteractiveManagement(InteractiveUpdate), /// Sent when a token returns multiple results for a getAssertion request SelectResultNotice(Sender>, Vec), + /// After MakeCredential, supply the user with the large blob key and let + /// them calculate the payload, to send back to us. + LargeBlobData(Sender, Vec), } pub(crate) fn send_status(status: &Sender, msg: StatusUpdate) { From 5937894a2840ce0dd1c6de1b8372bc02d53eed54 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Fri, 9 Feb 2024 14:35:13 +0100 Subject: [PATCH 3/6] Adding tests (and some minor fixes the tests revealed) --- examples/ctap2_discoverable_creds.rs | 2 +- examples/prf.rs | 3 + src/ctap2/commands/get_assertion.rs | 7 + src/ctap2/commands/large_blobs.rs | 434 ++++++++++++++++++++++++- src/ctap2/commands/make_credentials.rs | 1 + src/ctap2/mod.rs | 10 +- src/transport/mock/device.rs | 2 +- 7 files changed, 449 insertions(+), 10 deletions(-) diff --git a/examples/ctap2_discoverable_creds.rs b/examples/ctap2_discoverable_creds.rs index 30c1b393..f671ad9a 100644 --- a/examples/ctap2_discoverable_creds.rs +++ b/examples/ctap2_discoverable_creds.rs @@ -269,7 +269,7 @@ fn extract_associated_large_blobs(key: Vec, array: Vec { panic!("Unexpected select device notice") } + Ok(StatusUpdate::LargeBlobData(..)) => { + panic!("Unexpected large blob data request") + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index daa491ee..bfb98486 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -1104,6 +1104,7 @@ pub mod test { ), )), cred_blob: None, + large_blob_key: None, }, options: GetAssertionOptions { user_presence: Some(true), @@ -1162,6 +1163,7 @@ pub mod test { ), )), cred_blob: None, + large_blob_key: None, }, options: GetAssertionOptions { user_presence: None, @@ -1208,6 +1210,7 @@ pub mod test { }, )), cred_blob: None, + large_blob_key: None, }, options: GetAssertionOptions { user_presence: None, @@ -1230,6 +1233,7 @@ pub mod test { app_id: None, hmac_secret: Some(HmacGetSecretOrPrf::PrfUnmatched), cred_blob: None, + large_blob_key: None, }, options: GetAssertionOptions { user_presence: None, @@ -2901,6 +2905,9 @@ pub mod test { }, attachment: AuthenticatorAttachment::Unknown, extensions: AuthenticationExtensionsClientOutputs::default(), + user_selected: None, + large_blob_key: None, + large_blob_array: None, }; let mut dev = Device::new_skipping_serialization("commands/get_assertion") diff --git a/src/ctap2/commands/large_blobs.rs b/src/ctap2/commands/large_blobs.rs index 24e9c654..075963a3 100644 --- a/src/ctap2/commands/large_blobs.rs +++ b/src/ctap2/commands/large_blobs.rs @@ -248,7 +248,7 @@ impl<'de> Deserialize<'de> for LargeBlobArrayElement { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, PartialEq, Eq)] pub struct LargeBlobsResponse { pub(crate) large_blob_array: Vec, /// Truncated SHA-256 hash of the preceding bytes @@ -293,20 +293,23 @@ impl<'de> Deserialize<'de> for LargeBlobsResponse { )); } // split off trailing hash-bytes - let (mut large_blob, hash_slice) = payload.split_at(payload.len() - 16); + let (mut large_blob, mut hash_slice) = + payload.split_at(payload.len() - 16); let mut hasher = Sha256::new(); hasher.update(large_blob); let expected_hash = hasher.finalize(); // The initial serialized large-blob array is the value of the serialized large-blob array on a fresh authenticator, as well as immediately after a reset. It is the byte string h'8076be8b528d0075f7aae98d6fa57a6d3c', which is an empty CBOR array (80) followed by LEFT(SHA-256(h'80'), 16). - let default_large_blob = [ - 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, - 0x8d, 0x6f, 0xa5, 0x7a, 0x6d, 0x3c, + let default_large_blob = [0x80]; + let default_hash = [ + 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, + 0x6f, 0xa5, 0x7a, 0x6d, 0x3c, ]; // Once complete, the platform MUST confirm that the embedded SHA-256 hash is correct, based on the definition above. If not, the configuration is corrupt and the platform MUST discard it and act as if the initial serialized large-blob array was received. if &expected_hash.as_slice()[0..16] != hash_slice { warn!("Large blob array hash doesn't match with the expected value! Assuming an empty array."); large_blob = &default_large_blob; + hash_slice = &default_hash; } let byte_len = large_blob.len() as u64; @@ -449,3 +452,424 @@ where bytes.extend_from_slice(&hash[..16]); write_large_blob_segment(dev, keep_alive, &bytes, 0, pin_uv_auth_token) } + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::consts::HIDCmd; + use crate::transport::device_selector::Device; + use crate::transport::hid::HIDDevice; + use crate::transport::platform::device::{IN_HID_RPT_SIZE, OUT_HID_RPT_SIZE}; + use crate::transport::{FidoDevice, FidoProtocol}; + use rand::{thread_rng, RngCore}; + + fn add_bytes_to_read(cid: &[u8], bytes: &[u8], device: &mut Device) { + let mut data = Vec::new(); + let payload_len = (bytes.len() + 1) as u16; + // We skip the very first byte (HIDCmd::Cbor), as we will insert it below + data.extend(payload_len.to_be_bytes()); + data.push(0x00); // status == success + data.extend(bytes); + let chunks = data.chunks(IN_HID_RPT_SIZE - 5); + for (id, chunk) in chunks.enumerate() { + let mut msg = cid.to_vec(); + let state_or_seq = if id == 0 { + HIDCmd::Cbor.into() + } else { + (id - 1) as u8 // SEQ + }; + msg.push(state_or_seq); + msg.extend(chunk); + device.add_read(&msg, 0); + } + } + + fn add_bytes_to_write(cid: &[u8], bytes: &[u8], device: &mut Device) { + let mut data = Vec::new(); + let payload_len = (bytes.len()) as u16; + // We skip the very first byte (HIDCmd::Cbor), as we will insert it below + data.extend(payload_len.to_be_bytes()); + data.extend(bytes); + let chunks = data.chunks(OUT_HID_RPT_SIZE - 5); + for (id, chunk) in chunks.enumerate() { + let mut msg = cid.to_vec(); + let state_or_seq = if id == 0 { + HIDCmd::Cbor.into() + } else { + (id - 1) as u8 // SEQ + }; + msg.push(state_or_seq); + msg.extend(chunk); + device.add_write(&msg, 0); + } + } + + #[test] + fn test_read_large_blob_array() { + let keep_alive = || true; + let mut device = Device::new("commands/large_blobs").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + + // 'initialize' the device + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + let cmd = [ + 0xa2, // map(2) + 0x01, // unsigned(1) - get + 0x19, 0x03, 0xc0, // unsigned(960) + 0x03, // unsigned(3) - offset + 0x00, // unsigned(0) + ]; + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt + msg.extend(vec![0x0C]); // LargeBlobs + msg.extend(cmd); // Actual command + device.add_write(&msg, 0); + + add_bytes_to_read(&cid, &LARGE_BLOB_ARRAY, &mut device); + let array = read_large_blob_array(&mut device, &keep_alive) + .expect("Failed to read large blob array"); + let expected = get_expected_large_blobs_response(); + assert_eq!(expected, array); + } + + #[test] + fn test_read_large_blob_array_with_wrong_hash() { + let keep_alive = || true; + let mut device = Device::new("commands/large_blobs").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + + // 'initialize' the device + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + let cmd = [ + 0xa2, // map(2) + 0x01, // unsigned(1) - get + 0x19, 0x03, 0xc0, // unsigned(960) + 0x03, // unsigned(3) - offset + 0x00, // unsigned(0) + ]; + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt + msg.extend(vec![0x0C]); // LargeBlobs + msg.extend(cmd); // Actual command + device.add_write(&msg, 0); + + let mut payload = LARGE_BLOB_ARRAY; + payload[483] += 1; // Changing one byte in the hash + + add_bytes_to_read(&cid, &payload, &mut device); + // Should succeed, but give us the default empty Large blob array, as defined by the spec + let array = read_large_blob_array(&mut device, &keep_alive) + .expect("Failed to read large blob array"); + let expected = LargeBlobsResponse { + large_blob_array: vec![], + hash: [ + 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a, + 0x6d, 0x3c, + ], + byte_len: 1, + }; + assert_eq!(expected, array); + } + + #[test] + fn test_read_large_blob_array_multi_read() { + let keep_alive = || true; + let mut device = Device::new("commands/large_blobs").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + device.set_authenticator_info(crate::AuthenticatorInfo { + max_msg_size: Some(164), // Note: This value minus 64 will be the fragment size + ..Default::default() + }); + + // 'initialize' the device + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + for ii in 0..5 { + let mut cmd = vec![ + 0xa2, // map(2) + 0x01, // unsigned(1) - get + 0x18, 0x64, // unsigned(100) + 0x03, // unsigned(3) - offset + ]; + cmd.extend(&to_vec(&serde_cbor::Value::Integer(ii * 100)).unwrap()); + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt + msg.extend(vec![0x0C]); // LargeBlobs + msg.extend(cmd); // Actual command + device.add_write(&msg, 0); + } + + for chunk in LARGE_BLOB_ARRAY.chunks(100) { + add_bytes_to_read(&cid, chunk, &mut device); + } + let array = read_large_blob_array(&mut device, &keep_alive) + .expect("Failed to read large blob array"); + let expected = get_expected_large_blobs_response(); + assert_eq!(expected, array); + } + + #[test] + fn test_add_large_blob_element() { + let keep_alive = || true; + let mut device = Device::new("commands/large_blobs").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + + // First we read the whole existing array + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + let cmd = [ + 0xa2, // map(2) + 0x01, // unsigned(1) - get + 0x19, 0x03, 0xc0, // unsigned(960) + 0x03, // unsigned(3) - offset + 0x00, // unsigned(0) + ]; + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt + msg.extend(vec![0x0C]); // LargeBlobs + msg.extend(cmd); // Actual command + device.add_write(&msg, 0); + + add_bytes_to_read(&cid, &LARGE_BLOB_ARRAY, &mut device); + + // Now add write-command + let mut cmd = vec![ + 0x0C, // LargeBlobs + 0xa3, // map(3) + 0x02, // unsigned(1) - set + 0x59, 0x02, 0x78, // unsigned(632) 479+153 + ]; + cmd.extend(LARGE_BLOB_ARRAY_LONGER); + cmd.extend([ + 0x03, // unsigned(3) - offset + 0x00, // unsigned(0) + 0x04, // unsigned(4) - length + 0x19, 0x02, 0x78, // unsigned(631) + ]); + add_bytes_to_write(&cid, &cmd, &mut device); + + // empty success-command + add_bytes_to_read(&cid, &[], &mut device); + + let add_parsed = additional_blob_element(); + add_large_blob(&mut device, &keep_alive, add_parsed, None) + .expect("Failed to write add large blob element"); + } + + #[test] + fn test_add_large_blob_element_multi_write() { + let keep_alive = || true; + let mut device = Device::new("commands/large_blobs").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + device.set_authenticator_info(crate::AuthenticatorInfo { + max_msg_size: Some(164), // Note: This value minus 64 will be the fragment size + ..Default::default() + }); + + // First we read the whole existing array + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + for ii in 0..5 { + let mut cmd = vec![ + 0xa2, // map(2) + 0x01, // unsigned(1) - get + 0x18, 0x64, // unsigned(100) + 0x03, // unsigned(3) - offset + ]; + cmd.extend(&to_vec(&serde_cbor::Value::Integer(ii * 100)).unwrap()); + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt + msg.extend(vec![0x0C]); // LargeBlobs + msg.extend(cmd); // Actual command + device.add_write(&msg, 0); + } + + for chunk in LARGE_BLOB_ARRAY.chunks(100) { + add_bytes_to_read(&cid, chunk, &mut device); + } + + // Now add write-command + for (ii, chunk) in LARGE_BLOB_ARRAY_LONGER.chunks(100).enumerate() { + let mut cmd = vec![ + 0x0C, // LargeBlobs + ]; + + if ii == 0 { + cmd.push(0xa3); // map(3) // with 'length' + } else { + cmd.push(0xa2); // map(2) // without 'length' + } + cmd.push(0x02); // unsigned(1) - set + if ii == 6 { + cmd.extend([0x58, 0x20]); // unsigned(32) Remaining bytes + } else { + cmd.extend([0x58, 0x64]); // unsigned(100) Remaining bytes + } + + cmd.extend(chunk); + cmd.push(0x03); // unsigned(3) - offset + cmd.extend(&to_vec(&serde_cbor::Value::Integer((ii * 100) as i128)).unwrap()); + if ii == 0 { + cmd.extend([ + 0x04, // unsigned(4) - length + 0x19, 0x02, 0x78, // unsigned(631) + ]); + } + add_bytes_to_write(&cid, &cmd, &mut device); + + // empty success-command + add_bytes_to_read(&cid, &[], &mut device); + } + + let add_parsed = additional_blob_element(); + add_large_blob(&mut device, &keep_alive, add_parsed, None) + .expect("Failed to write add large blob element"); + } + + fn additional_blob_element() -> LargeBlobArrayElement { + LargeBlobArrayElement { + ciphertext: vec![ + 116, 199, 82, 206, 68, 131, 237, 242, 213, 144, 244, 185, 155, 148, 217, 62, 245, + 5, 128, 162, 176, 99, 5, 160, 186, 68, 88, 140, 38, 255, 168, 254, 88, 161, 188, + 30, 113, 221, 67, 21, 88, 43, 211, 17, 190, 252, 14, 186, 225, 200, 135, 186, 168, + 255, 232, 51, 151, 183, 194, 134, 160, 250, 191, 141, + ], + nonce: [117, 86, 137, 126, 205, 2, 34, 50, 18, 20, 165, 104], + orig_size: 34, + } + } + fn get_expected_large_blobs_response() -> LargeBlobsResponse { + LargeBlobsResponse { + large_blob_array: vec![ + LargeBlobArrayElement { + ciphertext: vec![ + 116, 199, 82, 206, 68, 131, 237, 242, 213, 144, 244, 185, 155, 148, 217, + 62, 245, 5, 128, 162, 176, 99, 5, 160, 186, 68, 88, 140, 38, 255, 168, 254, + 88, 161, 188, 30, 113, 221, 67, 21, 88, 43, 211, 17, 190, 252, 14, 186, + 225, 200, 135, 186, 168, 255, 232, 51, 151, 183, 194, 134, 160, 250, 191, + 141, + ], + nonce: [117, 86, 137, 126, 205, 2, 34, 50, 18, 20, 165, 104], + orig_size: 34, + }, + LargeBlobArrayElement { + ciphertext: vec![ + 71, 124, 111, 114, 77, 240, 163, 5, 124, 7, 191, 2, 177, 167, 200, 95, 248, + 163, 235, 77, 195, 106, 253, 23, 183, 119, 55, 17, 50, 238, 217, 248, 56, + 135, 48, 49, 101, 132, 66, 78, 58, 23, 101, 77, 52, 213, 89, 73, 34, 61, + 237, 8, 219, 1, 208, 245, 129, 101, 234, 114, 170, 54, 7, 147, 59, 226, 32, + ], + nonce: [99, 132, 251, 236, 134, 156, 86, 195, 121, 49, 205, 162], + orig_size: 36, + }, + LargeBlobArrayElement { + ciphertext: vec![ + 212, 135, 116, 12, 170, 245, 186, 103, 147, 112, 196, 29, 43, 120, 236, + 175, 205, 84, 184, 231, 118, 152, 76, 60, 216, 128, 204, 166, 96, 8, 67, 3, + 163, 242, 243, 124, 156, 65, 138, 98, 66, 46, 201, 40, 219, 236, 53, 43, + 107, 14, 135, 23, 99, 150, 240, 14, 234, 153, 115, 94, 180, 117, 162, 213, + ], + nonce: [231, 165, 15, 21, 64, 8, 234, 133, 6, 223, 226, 134], + orig_size: 34, + }, + ], + hash: [ + 0x15, 0xee, 0x84, 0xa0, 0xce, 0x5d, 0xa7, 0xd6, 0x6d, 0x3e, 0xb6, 0xf2, 0xc1, 0x40, + 0x28, 0x65, + ], + byte_len: 463, + } + } + + #[rustfmt::skip] + pub const LARGE_BLOB_ARRAY: [u8; 484] = [ + 0xa1, // map(1) + 0x01, // unsigned(1) + 0x59, 0x01, 0xdf, // bytes(479) + 0x83, // array(3) + 0xa3, // map(3) + 0x01, // unsigned(1) - ciphertext + 0x98, 0x40, // array(64) + 0x18, 0x74, 0x18, 0xc7, 0x18, 0x52, 0x18, 0xce, 0x18, 0x44, 0x18, 0x83, 0x18, 0xed, 0x18, 0xf2, 0x18, 0xd5, 0x18, 0x90, 0x18, 0xf4, 0x18, 0xb9, 0x18, 0x9b, 0x18, 0x94, 0x18, 0xd9, 0x18, 0x3e, 0x18, 0xf5, 0x05, 0x18, 0x80, 0x18, 0xa2, 0x18, 0xb0, 0x18, 0x63, 0x05, 0x18, 0xa0, 0x18, 0xba, 0x18, 0x44, 0x18, 0x58, 0x18, 0x8c, 0x18, 0x26, 0x18, 0xff, 0x18, 0xa8, 0x18, 0xfe, 0x18, 0x58, 0x18, 0xa1, 0x18, 0xbc, 0x18, 0x1e, 0x18, 0x71, 0x18, 0xdd, 0x18, 0x43, 0x15, 0x18, 0x58, 0x18, 0x2b, 0x18, 0xd3, 0x11, 0x18, 0xbe, 0x18, 0xfc, 0x0e, 0x18, 0xba, 0x18, 0xe1, 0x18, 0xc8, 0x18, 0x87, 0x18, 0xba, 0x18, 0xa8, 0x18, 0xff, 0x18, 0xe8, 0x18, 0x33, 0x18, 0x97, 0x18, 0xb7, 0x18, 0xc2, 0x18, 0x86, 0x18, 0xa0, 0x18, 0xfa, 0x18, 0xbf, 0x18, 0x8d, + 0x02, // unsigned(2) - nonce + 0x8c, // array(12) + 0x18, 0x75, 0x18, 0x56, 0x18, 0x89, 0x18, 0x7e, 0x18, 0xcd, 0x02, 0x18, 0x22, 0x18, 0x32, 0x12, 0x14, 0x18, 0xa5, 0x18, 0x68, + 0x03, // unsigned(3) - origSize + 0x18, 0x22, // unsigned(34) + 0xa3, // map(3) + 0x01, // unsigned(1) - ciphertext + 0x98, 0x43, // array(67) + 0x18, 0x47, 0x18, 0x7c, 0x18, 0x6f, 0x18, 0x72, 0x18, 0x4d, 0x18, 0xf0, 0x18, 0xa3, 0x05, 0x18, 0x7c, 0x07, 0x18, 0xbf, 0x02, 0x18, 0xb1, 0x18, 0xa7, 0x18, 0xc8, 0x18, 0x5f, 0x18, 0xf8, 0x18, 0xa3, 0x18, 0xeb, 0x18, 0x4d, 0x18, 0xc3, 0x18, 0x6a, 0x18, 0xfd, 0x17, 0x18, 0xb7, 0x18, 0x77, 0x18, 0x37, 0x11, 0x18, 0x32, 0x18, 0xee, 0x18, 0xd9, 0x18, 0xf8, 0x18, 0x38, 0x18, 0x87, 0x18, 0x30, 0x18, 0x31, 0x18, 0x65, 0x18, 0x84, 0x18, 0x42, 0x18, 0x4e, 0x18, 0x3a, 0x17, 0x18, 0x65, 0x18, 0x4d, 0x18, 0x34, 0x18, 0xd5, 0x18, 0x59, 0x18, 0x49, 0x18, 0x22, 0x18, 0x3d, 0x18, 0xed, 0x08, 0x18, 0xdb, 0x01, 0x18, 0xd0, 0x18, 0xf5, 0x18, 0x81, 0x18, 0x65, 0x18, 0xea, 0x18, 0x72, 0x18, 0xaa, 0x18, 0x36, 0x07, 0x18, 0x93, 0x18, 0x3b, 0x18, 0xe2, 0x18, 0x20, + 0x02, // unsigned(2) + 0x8c, // array(12) - nonce + 0x18, 0x63, 0x18, 0x84, 0x18, 0xfb, 0x18, 0xec, 0x18, 0x86, 0x18, 0x9c, 0x18, 0x56, 0x18, 0xc3, 0x18, 0x79, 0x18, 0x31, 0x18, 0xcd, 0x18, 0xa2, + 0x03, // unsigned(3) - origSize + 0x18, 0x24, // unsigned(36) + 0xa3, // map(3) + 0x01, // unsigned(1) - ciphertext + 0x98, 0x40, // array(64) + 0x18, 0xd4, 0x18, 0x87, 0x18, 0x74, 0x0c, 0x18, 0xaa, 0x18, 0xf5, 0x18, 0xba, 0x18, 0x67, 0x18, 0x93, 0x18, 0x70, 0x18, 0xc4, 0x18, 0x1d, 0x18, 0x2b, 0x18, 0x78, 0x18, 0xec, 0x18, 0xaf, 0x18, 0xcd, 0x18, 0x54, 0x18, 0xb8, 0x18, 0xe7, 0x18, 0x76, 0x18, 0x98, 0x18, 0x4c, 0x18, 0x3c, 0x18, 0xd8, 0x18, 0x80, 0x18, 0xcc, 0x18, 0xa6, 0x18, 0x60, 0x08, 0x18, 0x43, 0x03, 0x18, 0xa3, 0x18, 0xf2, 0x18, 0xf3, 0x18, 0x7c, 0x18, 0x9c, 0x18, 0x41, 0x18, 0x8a, 0x18, 0x62, 0x18, 0x42, 0x18, 0x2e, 0x18, 0xc9, 0x18, 0x28, 0x18, 0xdb, 0x18, 0xec, 0x18, 0x35, 0x18, 0x2b, 0x18, 0x6b, 0x0e, 0x18, 0x87, 0x17, 0x18, 0x63, 0x18, 0x96, 0x18, 0xf0, 0x0e, 0x18, 0xea, 0x18, 0x99, 0x18, 0x73, 0x18, 0x5e, 0x18, 0xb4, 0x18, 0x75, 0x18, 0xa2, 0x18, 0xd5, + 0x02, // unsigned(2) - nonce + 0x8c, // array(12) + 0x18, 0xe7, 0x18, 0xa5, 0x0f, 0x15, 0x18, 0x40, 0x08, 0x18, 0xea, 0x18, 0x85, 0x06, 0x18, 0xdf, 0x18, 0xe2, 0x18, 0x86, + 0x03, // unsigned(3) - origSize + 0x18, 0x22, // unsigned(34) + 0x15, 0xee, 0x84, 0xa0, 0xce, 0x5d, 0xa7, 0xd6, 0x6d, 0x3e, 0xb6, 0xf2, 0xc1, 0x40, 0x28, 0x65 // trailing hash-bytes + ]; + + #[rustfmt::skip] + pub const LARGE_BLOB_ARRAY_LONGER: [u8; 632] = [ + // Skipping initial map(1) -> unsigned(1) -> bytes(632), as we have to change that with + // fragmented writes + 0x84, // array(4) + 0xa3, // map(3) + 0x01, // unsigned(1) - ciphertext + 0x98, 0x40, // array(64) + 0x18, 0x74, 0x18, 0xc7, 0x18, 0x52, 0x18, 0xce, 0x18, 0x44, 0x18, 0x83, 0x18, 0xed, 0x18, 0xf2, 0x18, 0xd5, 0x18, 0x90, 0x18, 0xf4, 0x18, 0xb9, 0x18, 0x9b, 0x18, 0x94, 0x18, 0xd9, 0x18, 0x3e, 0x18, 0xf5, 0x05, 0x18, 0x80, 0x18, 0xa2, 0x18, 0xb0, 0x18, 0x63, 0x05, 0x18, 0xa0, 0x18, 0xba, 0x18, 0x44, 0x18, 0x58, 0x18, 0x8c, 0x18, 0x26, 0x18, 0xff, 0x18, 0xa8, 0x18, 0xfe, 0x18, 0x58, 0x18, 0xa1, 0x18, 0xbc, 0x18, 0x1e, 0x18, 0x71, 0x18, 0xdd, 0x18, 0x43, 0x15, 0x18, 0x58, 0x18, 0x2b, 0x18, 0xd3, 0x11, 0x18, 0xbe, 0x18, 0xfc, 0x0e, 0x18, 0xba, 0x18, 0xe1, 0x18, 0xc8, 0x18, 0x87, 0x18, 0xba, 0x18, 0xa8, 0x18, 0xff, 0x18, 0xe8, 0x18, 0x33, 0x18, 0x97, 0x18, 0xb7, 0x18, 0xc2, 0x18, 0x86, 0x18, 0xa0, 0x18, 0xfa, 0x18, 0xbf, 0x18, 0x8d, + 0x02, // unsigned(2) - nonce + 0x8c, // array(12) + 0x18, 0x75, 0x18, 0x56, 0x18, 0x89, 0x18, 0x7e, 0x18, 0xcd, 0x02, 0x18, 0x22, 0x18, 0x32, 0x12, 0x14, 0x18, 0xa5, 0x18, 0x68, + 0x03, // unsigned(3) - origSize + 0x18, 0x22, // unsigned(34) + 0xa3, // map(3) + 0x01, // unsigned(1) - ciphertext + 0x98, 0x43, // array(67) + 0x18, 0x47, 0x18, 0x7c, 0x18, 0x6f, 0x18, 0x72, 0x18, 0x4d, 0x18, 0xf0, 0x18, 0xa3, 0x05, 0x18, 0x7c, 0x07, 0x18, 0xbf, 0x02, 0x18, 0xb1, 0x18, 0xa7, 0x18, 0xc8, 0x18, 0x5f, 0x18, 0xf8, 0x18, 0xa3, 0x18, 0xeb, 0x18, 0x4d, 0x18, 0xc3, 0x18, 0x6a, 0x18, 0xfd, 0x17, 0x18, 0xb7, 0x18, 0x77, 0x18, 0x37, 0x11, 0x18, 0x32, 0x18, 0xee, 0x18, 0xd9, 0x18, 0xf8, 0x18, 0x38, 0x18, 0x87, 0x18, 0x30, 0x18, 0x31, 0x18, 0x65, 0x18, 0x84, 0x18, 0x42, 0x18, 0x4e, 0x18, 0x3a, 0x17, 0x18, 0x65, 0x18, 0x4d, 0x18, 0x34, 0x18, 0xd5, 0x18, 0x59, 0x18, 0x49, 0x18, 0x22, 0x18, 0x3d, 0x18, 0xed, 0x08, 0x18, 0xdb, 0x01, 0x18, 0xd0, 0x18, 0xf5, 0x18, 0x81, 0x18, 0x65, 0x18, 0xea, 0x18, 0x72, 0x18, 0xaa, 0x18, 0x36, 0x07, 0x18, 0x93, 0x18, 0x3b, 0x18, 0xe2, 0x18, 0x20, + 0x02, // unsigned(2) + 0x8c, // array(12) - nonce + 0x18, 0x63, 0x18, 0x84, 0x18, 0xfb, 0x18, 0xec, 0x18, 0x86, 0x18, 0x9c, 0x18, 0x56, 0x18, 0xc3, 0x18, 0x79, 0x18, 0x31, 0x18, 0xcd, 0x18, 0xa2, + 0x03, // unsigned(3) - origSize + 0x18, 0x24, // unsigned(36) + 0xa3, // map(3) + 0x01, // unsigned(1) - ciphertext + 0x98, 0x40, // array(64) + 0x18, 0xd4, 0x18, 0x87, 0x18, 0x74, 0x0c, 0x18, 0xaa, 0x18, 0xf5, 0x18, 0xba, 0x18, 0x67, 0x18, 0x93, 0x18, 0x70, 0x18, 0xc4, 0x18, 0x1d, 0x18, 0x2b, 0x18, 0x78, 0x18, 0xec, 0x18, 0xaf, 0x18, 0xcd, 0x18, 0x54, 0x18, 0xb8, 0x18, 0xe7, 0x18, 0x76, 0x18, 0x98, 0x18, 0x4c, 0x18, 0x3c, 0x18, 0xd8, 0x18, 0x80, 0x18, 0xcc, 0x18, 0xa6, 0x18, 0x60, 0x08, 0x18, 0x43, 0x03, 0x18, 0xa3, 0x18, 0xf2, 0x18, 0xf3, 0x18, 0x7c, 0x18, 0x9c, 0x18, 0x41, 0x18, 0x8a, 0x18, 0x62, 0x18, 0x42, 0x18, 0x2e, 0x18, 0xc9, 0x18, 0x28, 0x18, 0xdb, 0x18, 0xec, 0x18, 0x35, 0x18, 0x2b, 0x18, 0x6b, 0x0e, 0x18, 0x87, 0x17, 0x18, 0x63, 0x18, 0x96, 0x18, 0xf0, 0x0e, 0x18, 0xea, 0x18, 0x99, 0x18, 0x73, 0x18, 0x5e, 0x18, 0xb4, 0x18, 0x75, 0x18, 0xa2, 0x18, 0xd5, + 0x02, // unsigned(2) - nonce + 0x8c, // array(12) + 0x18, 0xe7, 0x18, 0xa5, 0x0f, 0x15, 0x18, 0x40, 0x08, 0x18, 0xea, 0x18, 0x85, 0x06, 0x18, 0xdf, 0x18, 0xe2, 0x18, 0x86, + 0x03, // unsigned(3) - origSize + 0x18, 0x22, // unsigned(34) + 0xa3, // map(3) + 0x01, // unsigned(1) - ciphertext + 0x98, 0x40, // array(64) + 0x18, 0x74, 0x18, 0xc7, 0x18, 0x52, 0x18, 0xce, 0x18, 0x44, 0x18, 0x83, 0x18, 0xed, 0x18, 0xf2, 0x18, 0xd5, 0x18, 0x90, 0x18, 0xf4, 0x18, 0xb9, 0x18, 0x9b, 0x18, 0x94, 0x18, 0xd9, 0x18, 0x3e, 0x18, 0xf5, 0x05, 0x18, 0x80, 0x18, 0xa2, 0x18, 0xb0, 0x18, 0x63, 0x05, 0x18, 0xa0, 0x18, 0xba, 0x18, 0x44, 0x18, 0x58, 0x18, 0x8c, 0x18, 0x26, 0x18, 0xff, 0x18, 0xa8, 0x18, 0xfe, 0x18, 0x58, 0x18, 0xa1, 0x18, 0xbc, 0x18, 0x1e, 0x18, 0x71, 0x18, 0xdd, 0x18, 0x43, 0x15, 0x18, 0x58, 0x18, 0x2b, 0x18, 0xd3, 0x11, 0x18, 0xbe, 0x18, 0xfc, 0x0e, 0x18, 0xba, 0x18, 0xe1, 0x18, 0xc8, 0x18, 0x87, 0x18, 0xba, 0x18, 0xa8, 0x18, 0xff, 0x18, 0xe8, 0x18, 0x33, 0x18, 0x97, 0x18, 0xb7, 0x18, 0xc2, 0x18, 0x86, 0x18, 0xa0, 0x18, 0xfa, 0x18, 0xbf, 0x18, 0x8d, + 0x02, // unsigned(2) - nonce + 0x8c, // array(12) + 0x18, 0x75, 0x18, 0x56, 0x18, 0x89, 0x18, 0x7e, 0x18, 0xcd, 0x02, 0x18, 0x22, 0x18, 0x32, 0x12, 0x14, 0x18, 0xa5, 0x18, 0x68, + 0x03, // unsigned(3) - origSize + 0x18, 0x22, // unsigned(34) + 0xb9, 0xd5, 0x4e, 0x96, 0xcf, 0x6e, 0xd8, 0xf6, 0xb4, 0x4c, 0x2e, 0xdc, 0xec, 0x76, 0x67, 0x0, // trailing hash-bytes + ]; +} diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 97248500..8ca52b25 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -804,6 +804,7 @@ pub mod test { hmac_secret: Some(HmacCreateSecretOrPrf::HmacCreateSecret(true)), min_pin_length: Some(true), cred_blob: None, + large_blob_key: None, }, options: MakeCredentialsOptions { resident_key: Some(true), diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index c9a71f49..e8e05cd5 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -728,9 +728,13 @@ pub fn sign( // and only return valid elements. But for that, we would need AEAD and DEFLATE-algos. let large_blob_array = if has_large_blob && results.iter().any(|f| f.large_blob_key.is_some()) { - large_blobs::read_large_blob_array(dev, alive) - .ok() - .map(|x| x.large_blob_array) + match large_blobs::read_large_blob_array(dev, alive) { + Ok(x) => Some(x.large_blob_array), + Err(e) => { + warn!("Failed to read large blob array: {e:?}"); + None + } + } } else { None }; diff --git a/src/transport/mock/device.rs b/src/transport/mock/device.rs index 211da465..40be689a 100644 --- a/src/transport/mock/device.rs +++ b/src/transport/mock/device.rs @@ -16,7 +16,7 @@ use std::io::{self, Read, Write}; use std::sync::mpsc::{channel, Receiver, Sender}; pub(crate) const IN_HID_RPT_SIZE: usize = 64; -const OUT_HID_RPT_SIZE: usize = 64; +pub(crate) const OUT_HID_RPT_SIZE: usize = 64; #[derive(Debug)] pub struct Device { From ba0539b1963bbf6d683dd998606be33393c2ba79 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Thu, 23 Oct 2025 13:51:17 +0200 Subject: [PATCH 4/6] Address feedback --- src/ctap2/commands/large_blobs.rs | 39 +++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/ctap2/commands/large_blobs.rs b/src/ctap2/commands/large_blobs.rs index 075963a3..416ff08b 100644 --- a/src/ctap2/commands/large_blobs.rs +++ b/src/ctap2/commands/large_blobs.rs @@ -1,7 +1,7 @@ use crate::{ crypto::{PinUvAuthParam, PinUvAuthToken}, ctap2::server::UserVerificationRequirement, - errors::AuthenticatorError, + errors::{AuthenticatorError, UnsupportedOption}, transport::errors::HIDError, FidoDevice, }; @@ -69,6 +69,10 @@ impl PinUvAuthCommand for LargeBlobs { fn get_rp_id(&self) -> Option<&String> { None } + + fn hmac_requested(&self) -> bool { + false + } } impl Serialize for LargeBlobs { @@ -132,9 +136,9 @@ impl RequestCtap2 for LargeBlobs { } let status: StatusCode = input[0].into(); - let payload = &input[1..]; if status.is_ok() { - if payload.len() > 1 { + if input.len() > 1 { + let payload = &input[1..]; Ok(payload.to_vec()) } else { // Some subcommands return only an OK-status without any data @@ -142,6 +146,7 @@ impl RequestCtap2 for LargeBlobs { } } else { let data: Option = if input.len() > 1 { + let payload = &input[1..]; Some(from_slice(payload).map_err(CommandError::Deserializing)?) } else { None @@ -314,7 +319,7 @@ impl<'de> Deserialize<'de> for LargeBlobsResponse { let byte_len = large_blob.len() as u64; let large_blob_array: Vec = - from_slice(large_blob).unwrap(); + from_slice(large_blob).map_err(M::Error::custom)?; let mut hash = [0u8; 16]; hash.copy_from_slice(hash_slice); response = Some(LargeBlobsResponse { @@ -354,11 +359,18 @@ where // Spec: // A per-authenticator constant, maxFragmentLength, is here defined as the value of maxMsgSize (from the authenticatorGetInfo response) minus 64. // If no maxMsgSize is given in the authenticatorGetInfo response) then it defaults to 1024, leaving maxFragmentLength to default to 960. - let max_fragment_length = dev + // + // In the highly unlikely case of a max_msg_size smaller than 65 (leaving zero-byte fragment-length), we error out, saying that largeBlobs is unsupported. + let max_msg_size = dev .get_authenticator_info() .and_then(|i| i.max_msg_size) - .unwrap_or(1024) - - 64; + .unwrap_or(1024); + if max_msg_size <= 64 { + return Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::LargeBlobs, + )); + } + let max_fragment_length = max_msg_size - 64; let mut bytes = vec![]; let mut offset = 0; loop { @@ -404,11 +416,18 @@ where // Spec: // A per-authenticator constant, maxFragmentLength, is here defined as the value of maxMsgSize (from the authenticatorGetInfo response) minus 64. // If no maxMsgSize is given in the authenticatorGetInfo response) then it defaults to 1024, leaving maxFragmentLength to default to 960. - let max_fragment_length = dev + // + // In the highly unlikely case of a max_msg_size smaller than 65 (leaving zero-byte fragment-length), we error out, saying that largeBlobs is unsupported. + let max_msg_size = dev .get_authenticator_info() .and_then(|i| i.max_msg_size) - .unwrap_or(1024) - - 64; + .unwrap_or(1024); + if max_msg_size <= 64 { + return Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::LargeBlobs, + )); + } + let max_fragment_length = max_msg_size - 64; let total_length = bytes.len(); let mut offset = initial_offset; for chunk in bytes.chunks(max_fragment_length) { From 327951241d566816e5dab476e2510e73190d0b8a Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Thu, 23 Oct 2025 15:24:41 +0200 Subject: [PATCH 5/6] Remove apparently deprecated as_slice()-calls for Hash-results --- src/ctap2/commands/large_blobs.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/ctap2/commands/large_blobs.rs b/src/ctap2/commands/large_blobs.rs index 416ff08b..e93e6218 100644 --- a/src/ctap2/commands/large_blobs.rs +++ b/src/ctap2/commands/large_blobs.rs @@ -39,9 +39,7 @@ impl PinUvAuthCommand for LargeBlobs { data.extend([0x0c, 0x00]); data.extend((self.offset as u32).to_le_bytes()); if let Some(ref set) = self.set { - let mut hasher = Sha256::new(); - hasher.update(set.as_slice()); - data.extend(hasher.finalize().as_slice()); + data.extend(Sha256::digest(set.as_slice())); } param = Some(token.derive(&data).map_err(CommandError::Crypto)?); } @@ -301,9 +299,7 @@ impl<'de> Deserialize<'de> for LargeBlobsResponse { let (mut large_blob, mut hash_slice) = payload.split_at(payload.len() - 16); - let mut hasher = Sha256::new(); - hasher.update(large_blob); - let expected_hash = hasher.finalize(); + let expected_hash = Sha256::digest(large_blob); // The initial serialized large-blob array is the value of the serialized large-blob array on a fresh authenticator, as well as immediately after a reset. It is the byte string h'8076be8b528d0075f7aae98d6fa57a6d3c', which is an empty CBOR array (80) followed by LEFT(SHA-256(h'80'), 16). let default_large_blob = [0x80]; let default_hash = [ @@ -311,7 +307,7 @@ impl<'de> Deserialize<'de> for LargeBlobsResponse { 0x6f, 0xa5, 0x7a, 0x6d, 0x3c, ]; // Once complete, the platform MUST confirm that the embedded SHA-256 hash is correct, based on the definition above. If not, the configuration is corrupt and the platform MUST discard it and act as if the initial serialized large-blob array was received. - if &expected_hash.as_slice()[0..16] != hash_slice { + if &expected_hash[0..16] != hash_slice { warn!("Large blob array hash doesn't match with the expected value! Assuming an empty array."); large_blob = &default_large_blob; hash_slice = &default_hash; From df3a1b8ee6802ec9ac6c2a09bdbd528ec1b7ace3 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Thu, 23 Oct 2025 15:40:26 +0200 Subject: [PATCH 6/6] Use workaround for broken generic array 0.14.9 by using the new version in compat-mode --- Cargo.toml | 2 ++ examples/ctap2_discoverable_creds.rs | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7bbd4e74..4f49879c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,8 @@ assert_matches = "1.2" rpassword = "5.0" flate3 = "1" aes-gcm = "0.10" +# Workaround for 'broken' generic-array 0.14.9, see ctap2_discoverable_creds.rs for details +generic-array = { version = "1.3", features = ["compat-0_14"] } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } diff --git a/examples/ctap2_discoverable_creds.rs b/examples/ctap2_discoverable_creds.rs index f671ad9a..49660960 100644 --- a/examples/ctap2_discoverable_creds.rs +++ b/examples/ctap2_discoverable_creds.rs @@ -4,7 +4,7 @@ use aes_gcm::{ aead::{Aead, AeadCore, KeyInit, OsRng, Payload}, - Aes256Gcm, Key, + Aes256Gcm, }; use authenticator::{ authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, @@ -21,6 +21,7 @@ use authenticator::{ statecallback::StateCallback, Pin, StatusPinUv, StatusUpdate, }; +use generic_array::GenericArray; use getopts::{Matches, Options}; use sha2::{Digest, Sha256}; use std::sync::mpsc::{channel, RecvError}; @@ -157,8 +158,14 @@ fn register_user( // Let nonce be a fresh, random, 12-byte value. let nonce = Aes256Gcm::generate_nonce(&mut OsRng); // Let ciphertext be the AEAD_AES_256_GCM authenticated encryption of plaintext using key, nonce, and the associated data as specified above. - let gcm_key = Key::::from_slice(&key); - let cipher = Aes256Gcm::new(gcm_key); + // + // Note: Because of bug https://github.com/RustCrypto/traits/issues/2036 and/or https://github.com/fizyk20/generic-array/issues/158 we can't use the + // simple version below, but have to request the new generic-array 1.x in + // our Cargo.toml and use it directly here, as aes_gcm uses the old version + // that got 'broken' by a dot-release + // let gcm_key = Key::::from_slice(&key); + // let cipher = Aes256Gcm::new(gcm_key); + let cipher = Aes256Gcm::new(GenericArray::from_slice(&key).as_ref()); let mut payload = Payload::from(plaintext.as_ref()); // Associated data: The value 0x626c6f62 ("blob") || uint64LittleEndian(origSize). let mut aad = b"blob".to_vec(); @@ -259,8 +266,13 @@ fn extract_associated_large_blobs(key: Vec, array: Vec::from_slice(&key); - let cipher = Aes256Gcm::new(gcm_key); + // Note: Because of bug https://github.com/RustCrypto/traits/issues/2036 and/or https://github.com/fizyk20/generic-array/issues/158 we can't use the + // simple version below, but have to request the new generic-array 1.x in + // our Cargo.toml and use it directly here, as aes_gcm uses the old version + // that got 'broken' by a dot-release + // let gcm_key = Key::::from_slice(&key); + // let cipher = Aes256Gcm::new(gcm_key); + let cipher = Aes256Gcm::new(GenericArray::from_slice(&key).as_ref()); let mut payload = Payload::from(e.ciphertext.as_slice()); // Associated data: The value 0x626c6f62 ("blob") || uint64LittleEndian(origSize). let mut aad = b"blob".to_vec();