From ebe87939d4184c5f87a7d94a76611eaa8c503bb4 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 24 May 2024 14:46:53 +0200 Subject: [PATCH 01/41] Add support for WebAuthn PRF extension Original context: https://bugzilla.mozilla.org/show_bug.cgi?id=1863819 --- src/ctap2/commands/get_assertion.rs | 32 ++++++++- src/ctap2/commands/make_credentials.rs | 90 ++++++++++++++++++++++--- src/ctap2/mod.rs | 47 +++++++++---- src/ctap2/server.rs | 93 +++++++++++++++++++++++--- 4 files changed, 229 insertions(+), 33 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 54d7d491..293c8dcd 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -8,14 +8,15 @@ use crate::consts::{ U2F_REQUEST_USER_PRESENCE, }; use crate::crypto::{COSEKey, CryptoError, PinUvAuthParam, PinUvAuthToken, SharedSecret}; -use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; +use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags, HmacSecretResponse}; use crate::ctap2::client_data::ClientDataHash; use crate::ctap2::commands::get_next_assertion::GetNextAssertion; use crate::ctap2::commands::make_credentials::UserVerification; use crate::ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, - AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, - RelyingParty, RpIdHash, UserVerificationRequirement, + AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFOutputs, + AuthenticationExtensionsPRFValues, AuthenticatorAttachment, PublicKeyCredentialDescriptor, + PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_be_u32, read_byte}; use crate::errors::AuthenticatorError; @@ -140,12 +141,15 @@ pub struct GetAssertionExtensions { pub app_id: Option, #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, + #[serde(skip)] // prf only exists in the web API, implemented by hmac-secret in CTAP + pub prf: Option, } impl From for GetAssertionExtensions { fn from(input: AuthenticationExtensionsClientInputs) -> Self { Self { app_id: input.app_id, + prf: input.prf, ..Default::default() } } @@ -205,6 +209,28 @@ impl GetAssertion { result.extensions.app_id = Some(result.assertion.auth_data.rp_id_hash == RelyingParty::from(app_id).hash()); } + + // 2. prf + // If the prf extension was requested and hmac-secret returned secrets, + // we need to decrypt and output them as prf client outputs. + if let (Some(_), Some(HmacSecretResponse::Secret(hmac_outputs)), Some(shared_secret)) = ( + &self.extensions.prf, + &result.assertion.auth_data.extensions.hmac_secret, + dev.get_shared_secret(), + ) { + if let Ok(secrets) = shared_secret.decrypt(&hmac_outputs) { + let (first, second) = secrets.split_at(32); + result.extensions.prf = Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: Some(AuthenticationExtensionsPRFValues { + first: first.to_vec(), + second: Some(second) + .filter(|second| !second.is_empty()) + .map(|second| second.to_vec()), + }), + }); + } + } } } diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 1de10484..cf018d5e 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, - AuthenticatorAttachment, CredentialProtectionPolicy, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, - UserVerificationRequirement, + AuthenticationExtensionsPRFOutputs, AuthenticationExtensionsPRFValues, AuthenticatorAttachment, + CredentialProtectionPolicy, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_byte, serde_parse_err}; use crate::errors::AuthenticatorError; @@ -240,11 +240,35 @@ pub struct MakeCredentialsExtensions { #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")] pub cred_protect: Option, #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] - pub hmac_secret: Option, + pub hmac_secret: Option, #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, } +#[derive(Debug, Clone)] +pub enum HmacSecretFromHmacSecretOrPrf { + HmacSecret(bool), + Prf, +} + +impl Default for HmacSecretFromHmacSecretOrPrf { + fn default() -> Self { + Self::HmacSecret(false) + } +} + +impl Serialize for HmacSecretFromHmacSecretOrPrf { + fn serialize(&self, s: S) -> Result + where + S: Serializer, + { + match self { + Self::HmacSecret(hmac_secret) => s.serialize_bool(*hmac_secret), + Self::Prf => s.serialize_bool(true), + } + } +} + impl MakeCredentialsExtensions { fn has_content(&self) -> bool { self.cred_protect.is_some() || self.hmac_secret.is_some() || self.min_pin_length.is_some() @@ -256,7 +280,13 @@ impl From for MakeCredentialsExtensions { Self { cred_props: input.cred_props, cred_protect: input.credential_protection_policy, - hmac_secret: input.hmac_create_secret, + hmac_secret: match (input.hmac_create_secret, input.prf) { + (None, None) => None, + (_, Some(_)) => Some(HmacSecretFromHmacSecretOrPrf::Prf), + (Some(hmac_secret), _) => { + Some(HmacSecretFromHmacSecretOrPrf::HmacSecret(hmac_secret)) + } + }, min_pin_length: input.min_pin_length, } } @@ -343,12 +373,52 @@ impl MakeCredentials { // 2. hmac-secret // The extension returns a flag in the authenticator data which we need to mirror as a // client output. - if self.extensions.hmac_secret == Some(true) { - if let Some(HmacSecretResponse::Confirmed(flag)) = - result.att_obj.auth_data.extensions.hmac_secret - { - result.extensions.hmac_create_secret = Some(flag); + // 3. prf + // hmac-secret returns a flag "enabled" in the authenticator data + // which we need to mirror as a client output. + // If a future version of hmac-secret permits calculating secrets in makeCredential, + // we also need to decrypt and output them as client outputs. + match self.extensions.hmac_secret { + Some(HmacSecretFromHmacSecretOrPrf::HmacSecret(true)) => { + if let Some(HmacSecretResponse::Confirmed(flag)) = + result.att_obj.auth_data.extensions.hmac_secret + { + result.extensions.hmac_create_secret = Some(flag); + } + } + Some(HmacSecretFromHmacSecretOrPrf::Prf) => { + result.extensions.prf = match &result.att_obj.auth_data.extensions.hmac_secret { + None => None, + Some(HmacSecretResponse::Confirmed(flag)) => { + Some(AuthenticationExtensionsPRFOutputs { + enabled: Some(*flag), + results: None, + }) + } + Some(HmacSecretResponse::Secret(outputs)) => { + if let Some(shared_secret) = dev.get_shared_secret() { + if let Ok(secrets) = shared_secret.decrypt(&outputs) { + Some(AuthenticationExtensionsPRFOutputs { + enabled: Some(true), + results: Some(AuthenticationExtensionsPRFValues { + first: secrets[0..32].to_vec(), + second: if secrets.len() > 32 { + Some(secrets[32..64].to_vec()) + } else { + None + }, + }), + }) + } else { + None + } + } else { + None + } + } + } } + None | Some(HmacSecretFromHmacSecretOrPrf::HmacSecret(false)) => {} } } } diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index bc45ceb9..21809b12 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -616,18 +616,6 @@ pub fn sign( ), callback ); - // Third, use the shared secret in the extensions, if requested - if let Some(extension) = get_assertion.extensions.hmac_secret.as_mut() { - if let Some(secret) = dev.get_shared_secret() { - match extension.calculate(secret) { - Ok(x) => x, - Err(e) => { - callback.call(Err(e)); - return false; - } - } - } - } // Do "pre-flight": Filter the allow-list let original_allow_list_was_empty = get_assertion.allow_list.is_empty(); @@ -672,6 +660,41 @@ pub fn sign( return false; } + // Third, use the shared secret in the extensions, if requested + if let Some(extension) = get_assertion.extensions.hmac_secret.as_mut() { + if let Some(secret) = dev.get_shared_secret() { + match extension.calculate(secret) { + Ok(_) => {} + Err(e) => { + callback.call(Err(e)); + return false; + } + } + } + } + + // Calculate prf extension inputs unless hmac-secret has already taken the spot + if let (Some(prf), None) = ( + &get_assertion.extensions.prf, + &get_assertion.extensions.hmac_secret, + ) { + if let Some(secret) = dev.get_shared_secret() { + match prf.calculate(secret, &get_assertion.allow_list) { + Ok(Some((hmac_secret, selected_credential))) => { + get_assertion.extensions.hmac_secret = Some(hmac_secret); + if let Some(selected_cred_id) = selected_credential { + get_assertion.allow_list = vec![selected_cred_id.clone()]; + } + } + Ok(None) => {} + Err(e) => { + callback.call(Err(e)); + return false; + } + } + } + } + debug!("------------------------------------------------------------------"); debug!("{get_assertion:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index fdf26374..479a96d1 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -1,4 +1,4 @@ -use crate::crypto::COSEAlgorithm; +use crate::crypto::{COSEAlgorithm, SharedSecret}; use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle}; use base64::Engine; use serde::de::MapAccess; @@ -9,9 +9,12 @@ use serde::{ }; use serde_bytes::{ByteBuf, Bytes}; use sha2::{Digest, Sha256}; +use std::collections::HashMap; use std::convert::{Into, TryFrom}; use std::fmt; +use super::commands::get_assertion::HmacSecretExtension; + #[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct RpIdHash(pub [u8; 32]); @@ -48,6 +51,12 @@ pub struct RelyingParty { pub name: Option, } +fn sha256(data: impl AsRef<[u8]>) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(data); + hasher.finalize().into() +} + impl RelyingParty { pub fn from(id: S) -> Self where @@ -60,13 +69,7 @@ impl RelyingParty { } pub fn hash(&self) -> RpIdHash { - let mut hasher = Sha256::new(); - hasher.update(&self.id); - - let mut output = [0u8; 32]; - output.copy_from_slice(hasher.finalize().as_slice()); - - RpIdHash(output) + RpIdHash(sha256(&self.id)) } } @@ -365,6 +368,7 @@ pub struct AuthenticationExtensionsClientInputs { pub enforce_credential_protection_policy: Option, pub hmac_create_secret: Option, pub min_pin_length: Option, + pub prf: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -372,11 +376,84 @@ pub struct CredentialProperties { pub rk: bool, } +#[derive(Clone, Debug, Default)] +pub struct AuthenticationExtensionsPRFInputs { + pub eval: Option, + pub eval_by_credential: Option, AuthenticationExtensionsPRFValues>>, +} + +impl AuthenticationExtensionsPRFInputs { + pub fn calculate<'allow_cred>( + &self, + secret: &SharedSecret, + allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], + ) -> Result< + Option<( + HmacSecretExtension, + Option<&'allow_cred PublicKeyCredentialDescriptor>, + )>, + AuthenticatorError, + > { + if let Some((selected_credential, ev)) = self + .eval_by_credential + .as_ref() + .and_then(|eval_by_credential| { + allow_credentials.iter().find_map(|pkcd| { + eval_by_credential + .get(&pkcd.id) + .map(|eval| (Some(pkcd), eval)) + }) + }) + .or(self.eval.as_ref().map(|eval| (None, eval))) + { + let mut hmac_secret = HmacSecretExtension::new( + sha256( + b"WebAuthn PRF" + .iter() + .chain([0x00].iter()) + .chain(ev.first.iter()) + .copied() + .collect::>(), + ) + .to_vec(), + ev.second.as_ref().map(|second| { + sha256( + b"WebAuthn PRF" + .iter() + .chain([0x00].iter()) + .chain(second.iter()) + .copied() + .collect::>(), + ) + .to_vec() + }), + ); + hmac_secret.calculate(secret)?; + Ok(Some((hmac_secret, selected_credential))) + } else { + Ok(None) + } + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AuthenticationExtensionsPRFValues { + pub first: Vec, + pub second: Option>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AuthenticationExtensionsPRFOutputs { + pub enabled: Option, + pub results: Option, +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct AuthenticationExtensionsClientOutputs { pub app_id: Option, pub cred_props: Option, pub hmac_create_secret: Option, + pub prf: Option, } #[derive(Clone, Debug, PartialEq, Eq)] From a70170802c91d7f820e4bc7a74a6012a927df46c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 24 May 2024 14:47:01 +0200 Subject: [PATCH 02/41] Send correct PIN protocol ID in hmac-secret Before this change, OpenSK (tag 2.1, commit 893faa5113f47457337ddb826b1a58870f00bc78) returns CTAP2_ERR_INVALID_PARAMETER in response to attempts to use the WebAuthn PRF extension. Original context: https://bugzilla.mozilla.org/show_bug.cgi?id=1863819 --- src/ctap2/commands/get_assertion.rs | 19 +++++++++++++++++-- src/ctap2/mod.rs | 8 ++++++-- src/ctap2/server.rs | 5 +++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 293c8dcd..1446483e 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -79,6 +79,7 @@ pub struct HmacSecretExtension { pub salt1: Vec, pub salt2: Option>, calculated_hmac: Option, + pin_protocol: Option, } impl HmacSecretExtension { @@ -87,10 +88,15 @@ impl HmacSecretExtension { salt1, salt2, calculated_hmac: None, + pin_protocol: None, } } - pub fn calculate(&mut self, secret: &SharedSecret) -> Result<(), AuthenticatorError> { + pub fn calculate( + &mut self, + secret: &SharedSecret, + puat: Option, + ) -> Result<(), AuthenticatorError> { if self.salt1.len() < 32 { return Err(CryptoError::WrongSaltLength.into()); } @@ -112,6 +118,11 @@ impl HmacSecretExtension { salt_auth, }); + // CTAP2.1 platforms MUST include this parameter if the value of pinUvAuthProtocol is not 1. + self.pin_protocol = puat + .map(|puat| puat.pin_protocol.id()) + .filter(|id| *id != 1); + Ok(()) } } @@ -122,10 +133,14 @@ impl Serialize for HmacSecretExtension { S: Serializer, { if let Some(calc) = &self.calculated_hmac { - let mut map = serializer.serialize_map(Some(3))?; + let mut map = + serializer.serialize_map(Some(3 + self.pin_protocol.map(|_| 1).unwrap_or(0)))?; map.serialize_entry(&1, &calc.public_key)?; map.serialize_entry(&2, serde_bytes::Bytes::new(&calc.salt_enc))?; map.serialize_entry(&3, serde_bytes::Bytes::new(&calc.salt_auth))?; + if let Some(pin_protocol) = &self.pin_protocol { + map.serialize_entry(&4, pin_protocol)?; + } map.end() } else { Err(SerError::custom( diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index 21809b12..d7179d54 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -663,7 +663,7 @@ pub fn sign( // Third, use the shared secret in the extensions, if requested if let Some(extension) = get_assertion.extensions.hmac_secret.as_mut() { if let Some(secret) = dev.get_shared_secret() { - match extension.calculate(secret) { + match extension.calculate(secret, pin_uv_auth_result.get_pin_uv_auth_token()) { Ok(_) => {} Err(e) => { callback.call(Err(e)); @@ -679,7 +679,11 @@ pub fn sign( &get_assertion.extensions.hmac_secret, ) { if let Some(secret) = dev.get_shared_secret() { - match prf.calculate(secret, &get_assertion.allow_list) { + match prf.calculate( + secret, + &get_assertion.allow_list, + pin_uv_auth_result.get_pin_uv_auth_token(), + ) { Ok(Some((hmac_secret, selected_credential))) => { get_assertion.extensions.hmac_secret = Some(hmac_secret); if let Some(selected_cred_id) = selected_credential { diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 479a96d1..b8481f07 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -1,4 +1,4 @@ -use crate::crypto::{COSEAlgorithm, SharedSecret}; +use crate::crypto::{COSEAlgorithm, PinUvAuthToken, SharedSecret}; use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle}; use base64::Engine; use serde::de::MapAccess; @@ -387,6 +387,7 @@ impl AuthenticationExtensionsPRFInputs { &self, secret: &SharedSecret, allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], + puat: Option, ) -> Result< Option<( HmacSecretExtension, @@ -428,7 +429,7 @@ impl AuthenticationExtensionsPRFInputs { .to_vec() }), ); - hmac_secret.calculate(secret)?; + hmac_secret.calculate(secret, puat)?; Ok(Some((hmac_secret, selected_credential))) } else { Ok(None) From 5cc6c1496f233290875d262570af839de7f9fa60 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 30 May 2024 18:11:45 +0200 Subject: [PATCH 03/41] Extract function HmacSecretResponse::decrypt_secrets --- src/ctap2/attestation.rs | 35 +++++++++++++++++++++++++- src/ctap2/commands/get_assertion.rs | 21 +++++++--------- src/ctap2/commands/make_credentials.rs | 20 ++++++--------- src/ctap2/server.rs | 17 +++++++++++++ 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index af33b159..b46b1918 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1,5 +1,6 @@ +use super::server::HMACGetSecretOutput; use super::utils::{from_slice_stream, read_be_u16, read_be_u32, read_byte}; -use crate::crypto::COSEAlgorithm; +use crate::crypto::{COSEAlgorithm, CryptoError, SharedSecret}; use crate::ctap2::server::{CredentialProtectionPolicy, RpIdHash}; use crate::ctap2::utils::serde_parse_err; use crate::{crypto::COSEKey, errors::AuthenticatorError}; @@ -10,6 +11,7 @@ use serde::{ Deserialize, Deserializer, Serialize, }; use serde_cbor; +use std::convert::TryInto; use std::fmt; use std::io::{Cursor, Read}; @@ -23,6 +25,37 @@ pub enum HmacSecretResponse { Secret(Vec), } +impl HmacSecretResponse { + /// Return the decrypted HMAC outputs, if this is an instance of [HmacSecretResponse::Secret]. + pub fn decrypt_secrets( + &self, + shared_secret: &SharedSecret, + ) -> Option> { + if let HmacSecretResponse::Secret(hmac_outputs) = self { + Some(Self::decrypt_secrets_internal(shared_secret, hmac_outputs)) + } else { + None + } + } + + fn decrypt_secrets_internal( + shared_secret: &SharedSecret, + hmac_outputs: &[u8], + ) -> Result { + let output_secrets = shared_secret.decrypt(hmac_outputs)?; + let (output1, output2) = output_secrets.split_at(32); + Ok(HMACGetSecretOutput { + output1: output1 + .try_into() + .map_err(|_| CryptoError::WrongSaltLength)?, + output2: Some(output2) + .filter(|o2| !o2.is_empty()) + .map(|o2| o2.try_into().map_err(|_| CryptoError::WrongSaltLength)) + .transpose()?, + }) + } +} + impl Serialize for HmacSecretResponse { fn serialize(&self, serializer: S) -> Result where diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 1446483e..5d792936 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -14,9 +14,9 @@ use crate::ctap2::commands::get_next_assertion::GetNextAssertion; use crate::ctap2::commands::make_credentials::UserVerification; use crate::ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, - AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFOutputs, - AuthenticationExtensionsPRFValues, AuthenticatorAttachment, PublicKeyCredentialDescriptor, - PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, UserVerificationRequirement, + AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, + PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, + UserVerificationRequirement, }; use crate::ctap2::utils::{read_be_u32, read_byte}; use crate::errors::AuthenticatorError; @@ -92,6 +92,9 @@ impl HmacSecretExtension { } } + /// Calculate inputs for the `hmac-secret` extension. + /// See "authenticatorGetAssertion additional behaviors" + /// in https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-hmac-secret-extension pub fn calculate( &mut self, secret: &SharedSecret, @@ -228,21 +231,15 @@ impl GetAssertion { // 2. prf // If the prf extension was requested and hmac-secret returned secrets, // we need to decrypt and output them as prf client outputs. - if let (Some(_), Some(HmacSecretResponse::Secret(hmac_outputs)), Some(shared_secret)) = ( + if let (Some(_), Some(hmac_response @ HmacSecretResponse::Secret(_)), Some(shared_secret)) = ( &self.extensions.prf, &result.assertion.auth_data.extensions.hmac_secret, dev.get_shared_secret(), ) { - if let Ok(secrets) = shared_secret.decrypt(&hmac_outputs) { - let (first, second) = secrets.split_at(32); + if let Some(Ok(secrets)) = hmac_response.decrypt_secrets(shared_secret) { result.extensions.prf = Some(AuthenticationExtensionsPRFOutputs { enabled: None, - results: Some(AuthenticationExtensionsPRFValues { - first: first.to_vec(), - second: Some(second) - .filter(|second| !second.is_empty()) - .map(|second| second.to_vec()), - }), + results: Some(secrets.into()), }); } } diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index cf018d5e..e0b70f4b 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, AuthenticationExtensionsPRFValues, AuthenticatorAttachment, - CredentialProtectionPolicy, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, - PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, UserVerificationRequirement, + AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, CredentialProtectionPolicy, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, + RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_byte, serde_parse_err}; use crate::errors::AuthenticatorError; @@ -395,19 +395,13 @@ impl MakeCredentials { results: None, }) } - Some(HmacSecretResponse::Secret(outputs)) => { + Some(hmac_response @ HmacSecretResponse::Secret(_)) => { if let Some(shared_secret) = dev.get_shared_secret() { - if let Ok(secrets) = shared_secret.decrypt(&outputs) { + if let Some(Ok(secrets)) = hmac_response.decrypt_secrets(shared_secret) + { Some(AuthenticationExtensionsPRFOutputs { enabled: Some(true), - results: Some(AuthenticationExtensionsPRFValues { - first: secrets[0..32].to_vec(), - second: if secrets.len() > 32 { - Some(secrets[32..64].to_vec()) - } else { - None - }, - }), + results: Some(secrets.into()), }) } else { None diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index b8481f07..95f30a56 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -376,6 +376,14 @@ pub struct CredentialProperties { pub rk: bool, } +/// Decrypted HMAC outputs from the `hmac-secret` extension. +/// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#dictdef-hmacgetsecretoutput +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct HMACGetSecretOutput { + pub output1: [u8; 32], + pub output2: Option<[u8; 32]>, +} + #[derive(Clone, Debug, Default)] pub struct AuthenticationExtensionsPRFInputs { pub eval: Option, @@ -443,6 +451,15 @@ pub struct AuthenticationExtensionsPRFValues { pub second: Option>, } +impl From for AuthenticationExtensionsPRFValues { + fn from(hmac_output: HMACGetSecretOutput) -> Self { + Self { + first: hmac_output.output1.to_vec(), + second: hmac_output.output2.map(|o2| o2.to_vec()), + } + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct AuthenticationExtensionsPRFOutputs { pub enabled: Option, From c4aa4972e2e5d1fd9d8ac42d5ebdcdc70b02f411 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 4 Jun 2024 17:58:11 +0200 Subject: [PATCH 04/41] Clarify and correct hmac-secret and PRF client outputs in makeCredential --- src/ctap2/commands/make_credentials.rs | 58 +++++++++++++------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index e0b70f4b..f36244ca 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -374,43 +374,45 @@ impl MakeCredentials { // The extension returns a flag in the authenticator data which we need to mirror as a // client output. // 3. prf - // hmac-secret returns a flag "enabled" in the authenticator data - // which we need to mirror as a client output. + // hmac-secret returns a flag in the authenticator data + // which we need to mirror as a PRF "enabled" client output. // If a future version of hmac-secret permits calculating secrets in makeCredential, // we also need to decrypt and output them as client outputs. match self.extensions.hmac_secret { Some(HmacSecretFromHmacSecretOrPrf::HmacSecret(true)) => { - if let Some(HmacSecretResponse::Confirmed(flag)) = - result.att_obj.auth_data.extensions.hmac_secret - { - result.extensions.hmac_create_secret = Some(flag); - } + result.extensions.hmac_create_secret = + Some(match result.att_obj.auth_data.extensions.hmac_secret { + Some(HmacSecretResponse::Confirmed(flag)) => flag, + Some(HmacSecretResponse::Secret(_)) => true, + None => false, + }); } Some(HmacSecretFromHmacSecretOrPrf::Prf) => { - result.extensions.prf = match &result.att_obj.auth_data.extensions.hmac_secret { - None => None, - Some(HmacSecretResponse::Confirmed(flag)) => { - Some(AuthenticationExtensionsPRFOutputs { - enabled: Some(*flag), + result.extensions.prf = + Some(match &result.att_obj.auth_data.extensions.hmac_secret { + None => AuthenticationExtensionsPRFOutputs { + enabled: Some(false), results: None, - }) - } - Some(hmac_response @ HmacSecretResponse::Secret(_)) => { - if let Some(shared_secret) = dev.get_shared_secret() { - if let Some(Ok(secrets)) = hmac_response.decrypt_secrets(shared_secret) - { - Some(AuthenticationExtensionsPRFOutputs { - enabled: Some(true), - results: Some(secrets.into()), - }) - } else { - None + }, + Some(HmacSecretResponse::Confirmed(flag)) => { + AuthenticationExtensionsPRFOutputs { + enabled: Some(*flag), + results: None, } - } else { - None } - } - } + Some(hmac_response @ HmacSecretResponse::Secret(_)) => { + AuthenticationExtensionsPRFOutputs { + enabled: Some(true), + results: dev + .get_shared_secret() + .and_then(|shared_secret| { + hmac_response.decrypt_secrets(shared_secret) + }) + .and_then(Result::ok) + .map(|outputs| outputs.into()), + } + } + }) } None | Some(HmacSecretFromHmacSecretOrPrf::HmacSecret(false)) => {} } From 1cac147acd12f325a5b686c0d5181c5bc715cc6e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 4 Jun 2024 18:03:49 +0200 Subject: [PATCH 05/41] Delete unnecessary impl Default --- src/ctap2/commands/make_credentials.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index f36244ca..cf1fda34 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -251,12 +251,6 @@ pub enum HmacSecretFromHmacSecretOrPrf { Prf, } -impl Default for HmacSecretFromHmacSecretOrPrf { - fn default() -> Self { - Self::HmacSecret(false) - } -} - impl Serialize for HmacSecretFromHmacSecretOrPrf { fn serialize(&self, s: S) -> Result where From dee98a062a1cca7170e003702efda21c5ff49ca3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 4 Jun 2024 18:03:55 +0200 Subject: [PATCH 06/41] Rename HmacSecretFromHmacSecretOrPrf to HmacCreateSecretOrPrf --- src/ctap2/commands/make_credentials.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index cf1fda34..4c526e1b 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -240,24 +240,24 @@ pub struct MakeCredentialsExtensions { #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")] pub cred_protect: Option, #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] - pub hmac_secret: Option, + pub hmac_secret: Option, #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, } #[derive(Debug, Clone)] -pub enum HmacSecretFromHmacSecretOrPrf { - HmacSecret(bool), +pub enum HmacCreateSecretOrPrf { + HmacCreateSecret(bool), Prf, } -impl Serialize for HmacSecretFromHmacSecretOrPrf { +impl Serialize for HmacCreateSecretOrPrf { fn serialize(&self, s: S) -> Result where S: Serializer, { match self { - Self::HmacSecret(hmac_secret) => s.serialize_bool(*hmac_secret), + Self::HmacCreateSecret(hmac_secret) => s.serialize_bool(*hmac_secret), Self::Prf => s.serialize_bool(true), } } @@ -276,9 +276,9 @@ impl From for MakeCredentialsExtensions { cred_protect: input.credential_protection_policy, hmac_secret: match (input.hmac_create_secret, input.prf) { (None, None) => None, - (_, Some(_)) => Some(HmacSecretFromHmacSecretOrPrf::Prf), + (_, Some(_)) => Some(HmacCreateSecretOrPrf::Prf), (Some(hmac_secret), _) => { - Some(HmacSecretFromHmacSecretOrPrf::HmacSecret(hmac_secret)) + Some(HmacCreateSecretOrPrf::HmacCreateSecret(hmac_secret)) } }, min_pin_length: input.min_pin_length, @@ -373,7 +373,7 @@ impl MakeCredentials { // If a future version of hmac-secret permits calculating secrets in makeCredential, // we also need to decrypt and output them as client outputs. match self.extensions.hmac_secret { - Some(HmacSecretFromHmacSecretOrPrf::HmacSecret(true)) => { + Some(HmacCreateSecretOrPrf::HmacCreateSecret(true)) => { result.extensions.hmac_create_secret = Some(match result.att_obj.auth_data.extensions.hmac_secret { Some(HmacSecretResponse::Confirmed(flag)) => flag, @@ -381,7 +381,7 @@ impl MakeCredentials { None => false, }); } - Some(HmacSecretFromHmacSecretOrPrf::Prf) => { + Some(HmacCreateSecretOrPrf::Prf) => { result.extensions.prf = Some(match &result.att_obj.auth_data.extensions.hmac_secret { None => AuthenticationExtensionsPRFOutputs { @@ -408,7 +408,7 @@ impl MakeCredentials { } }) } - None | Some(HmacSecretFromHmacSecretOrPrf::HmacSecret(false)) => {} + None | Some(HmacCreateSecretOrPrf::HmacCreateSecret(false)) => {} } } } From 501ad7f3886eb00fc0455679ef0e19b00b120fb0 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 4 Jun 2024 18:42:44 +0200 Subject: [PATCH 07/41] Use HmacGetSecretOrPrf data model in getAssertion too --- src/ctap2/commands/get_assertion.rs | 79 +++++++++++++++++++++++++---- src/ctap2/mod.rs | 70 ++++++++++++++----------- src/ctap2/server.rs | 10 ++++ 3 files changed, 117 insertions(+), 42 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 5d792936..c20eda50 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -74,6 +74,32 @@ pub struct CalculatedHmacSecretExtension { pub salt_auth: Vec, } +/// Wrapper type recording whether the hmac-secret input originally came from the hmacGetSecret or the prf client extension input. +#[derive(Debug, Clone)] +pub enum HmacGetSecretOrPrf { + /// hmac-secret inputs set by the hmacGetSecret client extension input. + HmacGetSecret(HmacSecretExtension), + /// hmac-secret input is to be calculated from PRF inputs, but we haven't yet identified which eval or evalByCredential entry to use. + PrfUninitialized(AuthenticationExtensionsPRFInputs), + /// hmac-secret inputs set by the prf client extension input. + Prf(HmacSecretExtension), +} + +impl Serialize for HmacGetSecretOrPrf { + fn serialize(&self, s: S) -> Result + where + S: Serializer, + { + match self { + Self::HmacGetSecret(ext) => ext.serialize(s), + Self::PrfUninitialized(_) => Err(serde::ser::Error::custom( + "PrfUninitialized must be replaced with Prf before serializing", + )), + Self::Prf(ext) => ext.serialize(s), + } + } +} + #[derive(Debug, Clone, Default)] pub struct HmacSecretExtension { pub salt1: Vec, @@ -158,16 +184,25 @@ pub struct GetAssertionExtensions { #[serde(skip_serializing)] pub app_id: Option, #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] - pub hmac_secret: Option, - #[serde(skip)] // prf only exists in the web API, implemented by hmac-secret in CTAP - pub prf: Option, + pub hmac_secret: Option, } impl From for GetAssertionExtensions { fn from(input: AuthenticationExtensionsClientInputs) -> Self { + let prf = input.prf; Self { app_id: input.app_id, - prf: input.prf, + hmac_secret: input + .hmac_get_secret + .map(|hmac_secret| { + HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + hmac_secret.salt1.into(), + hmac_secret.salt2.map(|salt2| salt2.into()), + )) + }) + .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 + ), ..Default::default() } } @@ -231,17 +266,39 @@ impl GetAssertion { // 2. prf // If the prf extension was requested and hmac-secret returned secrets, // we need to decrypt and output them as prf client outputs. - if let (Some(_), Some(hmac_response @ HmacSecretResponse::Secret(_)), Some(shared_secret)) = ( - &self.extensions.prf, - &result.assertion.auth_data.extensions.hmac_secret, - dev.get_shared_secret(), - ) { - if let Some(Ok(secrets)) = hmac_response.decrypt_secrets(shared_secret) { + match self.extensions.hmac_secret { + Some(HmacGetSecretOrPrf::HmacGetSecret(_)) => { + result.extensions.hmac_get_secret = + if let Some(hmac_response @ HmacSecretResponse::Secret(_)) = + &result.assertion.auth_data.extensions.hmac_secret + { + dev.get_shared_secret() + .and_then(|shared_secret| hmac_response.decrypt_secrets(shared_secret)) + .and_then(Result::ok) + .map(|outputs| outputs.into()) + } else { + None + }; + } + Some(HmacGetSecretOrPrf::PrfUninitialized(_)) => { + unreachable!("Reached GetAssertion.finalize_result without replacing PrfUninitialized instance with Prf") + } + Some(HmacGetSecretOrPrf::Prf(_)) => { result.extensions.prf = Some(AuthenticationExtensionsPRFOutputs { enabled: None, - results: Some(secrets.into()), + results: if let Some(hmac_response @ HmacSecretResponse::Secret(_)) = + &result.assertion.auth_data.extensions.hmac_secret + { + dev.get_shared_secret() + .and_then(|shared_secret| hmac_response.decrypt_secrets(shared_secret)) + .and_then(Result::ok) + .map(|outputs| outputs.into()) + } else { + None + }, }); } + None => {} } } } diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index d7179d54..c647f93a 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -22,7 +22,9 @@ use crate::ctap2::commands::credential_management::{ CredManagementCommand, CredentialList, CredentialListEntry, CredentialManagement, CredentialManagementResult, CredentialRpListEntry, }; -use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionOptions}; +use crate::ctap2::commands::get_assertion::{ + GetAssertion, GetAssertionOptions, HmacGetSecretOrPrf, +}; use crate::ctap2::commands::make_credentials::{ dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsOptions, }; @@ -661,42 +663,48 @@ pub fn sign( } // Third, use the shared secret in the extensions, if requested - if let Some(extension) = get_assertion.extensions.hmac_secret.as_mut() { - if let Some(secret) = dev.get_shared_secret() { - match extension.calculate(secret, pin_uv_auth_result.get_pin_uv_auth_token()) { - Ok(_) => {} - Err(e) => { - callback.call(Err(e)); - return false; + if let Some(hmac_get_secret_or_prf) = get_assertion.extensions.hmac_secret.as_mut() { + match hmac_get_secret_or_prf { + HmacGetSecretOrPrf::HmacGetSecret(extension) => { + if let Some(secret) = dev.get_shared_secret() { + match extension + .calculate(secret, pin_uv_auth_result.get_pin_uv_auth_token()) + { + Ok(_) => {} + Err(e) => { + callback.call(Err(e)); + return false; + } + } } } - } - } - // Calculate prf extension inputs unless hmac-secret has already taken the spot - if let (Some(prf), None) = ( - &get_assertion.extensions.prf, - &get_assertion.extensions.hmac_secret, - ) { - if let Some(secret) = dev.get_shared_secret() { - match prf.calculate( - secret, - &get_assertion.allow_list, - pin_uv_auth_result.get_pin_uv_auth_token(), - ) { - Ok(Some((hmac_secret, selected_credential))) => { - get_assertion.extensions.hmac_secret = Some(hmac_secret); - if let Some(selected_cred_id) = selected_credential { - get_assertion.allow_list = vec![selected_cred_id.clone()]; + HmacGetSecretOrPrf::PrfUninitialized(prf) => { + if let Some(secret) = dev.get_shared_secret() { + match prf.calculate( + secret, + &get_assertion.allow_list, + pin_uv_auth_result.get_pin_uv_auth_token(), + ) { + Ok(Some((hmac_secret, selected_credential))) => { + *hmac_get_secret_or_prf = HmacGetSecretOrPrf::Prf(hmac_secret); + if let Some(selected_cred_id) = selected_credential { + get_assertion.allow_list = vec![selected_cred_id.clone()]; + } + } + Ok(None) => {} + Err(e) => { + callback.call(Err(e)); + return false; + } } } - Ok(None) => {} - Err(e) => { - callback.call(Err(e)); - return false; - } } - } + + HmacGetSecretOrPrf::Prf(_) => { + unreachable!("hmac-secret inputs from PRF already initialized") + } + }; } debug!("------------------------------------------------------------------"); diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 95f30a56..968c065a 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -367,6 +367,7 @@ pub struct AuthenticationExtensionsClientInputs { pub credential_protection_policy: Option, pub enforce_credential_protection_policy: Option, pub hmac_create_secret: Option, + pub hmac_get_secret: Option, pub min_pin_length: Option, pub prf: Option, } @@ -376,6 +377,14 @@ pub struct CredentialProperties { pub rk: bool, } +/// Salt inputs for the `hmac-secret` extension. +/// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#dictdef-hmacgetsecretinput +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct HMACGetSecretInput { + pub salt1: [u8; 32], + pub salt2: Option<[u8; 32]>, +} + /// Decrypted HMAC outputs from the `hmac-secret` extension. /// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#dictdef-hmacgetsecretoutput #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -471,6 +480,7 @@ pub struct AuthenticationExtensionsClientOutputs { pub app_id: Option, pub cred_props: Option, pub hmac_create_secret: Option, + pub hmac_get_secret: Option, pub prf: Option, } From ba1e548d5aea31ad6fe53d28f905e480fad07c11 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 4 Jun 2024 19:19:23 +0200 Subject: [PATCH 08/41] Add examples/prf.rs --- examples/prf.rs | 315 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 examples/prf.rs diff --git a/examples/prf.rs b/examples/prf.rs new file mode 100644 index 00000000..0a2bc128 --- /dev/null +++ b/examples/prf.rs @@ -0,0 +1,315 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 authenticator::{ + authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, + crypto::COSEAlgorithm, + ctap2::server::{ + AuthenticationExtensionsClientInputs, AuthenticationExtensionsPRFInputs, + AuthenticationExtensionsPRFValues, HMACGetSecretInput, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, + ResidentKeyRequirement, Transport, UserVerificationRequirement, + }, + statecallback::StateCallback, + Pin, StatusPinUv, StatusUpdate, +}; +use getopts::Options; +use rand::{thread_rng, RngCore}; +use std::sync::mpsc::{channel, RecvError}; +use std::{env, thread}; + +fn print_usage(program: &str, opts: Options) { + let brief = format!("Usage: {program} [options]"); + print!("{}", opts.usage(&brief)); +} + +fn main() { + env_logger::init(); + + let args: Vec = env::args().collect(); + let program = args[0].clone(); + + let rp_id = "example.com".to_string(); + + let mut opts = Options::new(); + opts.optflag("h", "help", "print this help menu").optopt( + "t", + "timeout", + "timeout in seconds", + "SEC", + ); + opts.optflag("h", "help", "print this help menu"); + opts.optflag( + "", + "hmac-secret", + "Return hmac-secret outputs instead of prf outputs (i.e., do not prefix and hash the inputs)", + ); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => panic!("{}", f.to_string()), + }; + if matches.opt_present("help") { + print_usage(&program, opts); + return; + } + + let mut manager = + AuthenticatorService::new().expect("The auth service should initialize safely"); + manager.add_u2f_usb_hid_platform_transports(); + + let timeout_ms = match matches.opt_get_default::("timeout", 25) { + Ok(timeout_s) => { + println!("Using {}s as the timeout", &timeout_s); + timeout_s * 1_000 + } + Err(e) => { + println!("{e}"); + print_usage(&program, opts); + return; + } + }; + + let (register_hmac_secret, sign_hmac_secret, register_prf, sign_prf) = + if matches.opt_present("hmac-secret") { + let register_hmac_secret = Some(true); + let sign_hmac_secret = Some(HMACGetSecretInput { + salt1: [0x07; 32], + salt2: Some([0x07; 32]), + }); + (register_hmac_secret, sign_hmac_secret, None, None) + } else { + let register_prf = Some(AuthenticationExtensionsPRFInputs::default()); + let sign_prf = Some(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![1, 2, 3, 4], + second: Some(vec![1, 2, 3, 4]), + }), + eval_by_credential: None, + }); + (None, None, register_prf, sign_prf) + }; + + println!("Asking a security key to register now..."); + let mut chall_bytes = [0u8; 32]; + thread_rng().fill_bytes(&mut chall_bytes); + + let (status_tx, status_rx) = channel::(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(StatusUpdate::InteractiveManagement(..)) => { + panic!("STATUS: This can't happen when doing non-interactive usage"); + } + Ok(StatusUpdate::SelectDeviceNotice) => { + println!("STATUS: Please select a device by touching one of them."); + } + Ok(StatusUpdate::PresenceRequired) => { + println!("STATUS: waiting for user presence"); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { + println!( + "Wrong PIN! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + panic!("Too many failed attempts. Your device has been blocked. Reset it.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => { + println!( + "Wrong UV! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { + println!("Too many failed UV-attempts."); + continue; + } + Ok(StatusUpdate::PinUvError(e)) => { + panic!("Unexpected error: {:?}", e) + } + Ok(StatusUpdate::SelectResultNotice(_, _)) => { + panic!("Unexpected select device notice") + } + Err(RecvError) => { + println!("STATUS: end"); + return; + } + } + }); + + let user = PublicKeyCredentialUserEntity { + id: "user_id".as_bytes().to_vec(), + name: Some("A. User".to_string()), + display_name: None, + }; + let relying_party = RelyingParty { + id: rp_id.clone(), + name: None, + }; + let ctap_args = RegisterArgs { + client_data_hash: chall_bytes, + relying_party, + origin: format!("https://{rp_id}"), + user, + pub_cred_params: vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ], + exclude_list: vec![], + user_verification_req: UserVerificationRequirement::Required, + resident_key_req: ResidentKeyRequirement::Discouraged, + extensions: AuthenticationExtensionsClientInputs { + hmac_create_secret: register_hmac_secret, + prf: register_prf, + ..Default::default() + }, + pin: None, + use_ctap1_fallback: false, + }; + + let attestation_object; + loop { + let (register_tx, register_rx) = channel(); + let callback = StateCallback::new(Box::new(move |rv| { + register_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx.clone(), callback) { + panic!("Couldn't register: {:?}", e); + }; + + let register_result = register_rx + .recv() + .expect("Problem receiving, unable to continue"); + match register_result { + Ok(a) => { + println!("Ok!"); + attestation_object = a; + break; + } + Err(e) => panic!("Registration failed: {:?}", e), + }; + } + + println!("Register result: {:?}", &attestation_object); + + println!(); + println!("*********************************************************************"); + println!("Asking a security key to sign now, with the data from the register..."); + println!("*********************************************************************"); + + let allow_list; + if let Some(cred_data) = attestation_object.att_obj.auth_data.credential_data { + allow_list = vec![PublicKeyCredentialDescriptor { + id: cred_data.credential_id, + transports: vec![Transport::USB], + }]; + } else { + allow_list = Vec::new(); + } + + let ctap_args = SignArgs { + client_data_hash: chall_bytes, + origin: format!("https://{rp_id}"), + relying_party_id: rp_id, + allow_list, + user_verification_req: UserVerificationRequirement::Required, + user_presence_req: true, + extensions: AuthenticationExtensionsClientInputs { + hmac_get_secret: sign_hmac_secret.clone(), + prf: sign_prf.clone(), + ..Default::default() + }, + pin: None, + use_ctap1_fallback: false, + }; + + loop { + let (sign_tx, sign_rx) = channel(); + + let callback = StateCallback::new(Box::new(move |rv| { + sign_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.sign(timeout_ms, ctap_args, status_tx, callback) { + panic!("Couldn't sign: {:?}", e); + } + + let sign_result = sign_rx + .recv() + .expect("Problem receiving, unable to continue"); + + match sign_result { + Ok(assertion_object) => { + println!("Assertion Object: {assertion_object:?}"); + println!("Done."); + + if sign_hmac_secret.is_some() { + let hmac_secret_outputs = assertion_object + .extensions + .hmac_get_secret + .as_ref() + .expect("Expected hmac-secret output"); + + assert_eq!( + Some(hmac_secret_outputs.output1), + hmac_secret_outputs.output2, + "Expected hmac-secret outputs to be equal for equal input" + ); + + assert_eq!( + assertion_object.extensions.prf, None, + "Expected no PRF outputs when hmacGetSecret input was present" + ); + } + + if sign_prf.is_some() { + let prf_results = assertion_object + .extensions + .prf + .expect("Expected PRF output") + .results + .expect("Expected PRF output to contain results"); + + assert_eq!( + Some(prf_results.first), + prf_results.second, + "Expected PRF results to be equal for equal input" + ); + + assert_eq!( + assertion_object.extensions.hmac_get_secret, None, + "Expected no hmacGetSecret output when PRF input was present" + ); + } + + break; + } + + Err(e) => panic!("Signing failed: {:?}", e), + } + } +} From 8f63de63047a91094bde85396fe0b16923b8ab81 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 4 Jun 2024 19:21:13 +0200 Subject: [PATCH 09/41] Construct channels outside loop --- examples/prf.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/prf.rs b/examples/prf.rs index 0a2bc128..bc087604 100644 --- a/examples/prf.rs +++ b/examples/prf.rs @@ -191,8 +191,8 @@ fn main() { }; let attestation_object; + let (register_tx, register_rx) = channel(); loop { - let (register_tx, register_rx) = channel(); let callback = StateCallback::new(Box::new(move |rv| { register_tx.send(rv).unwrap(); })); @@ -247,9 +247,8 @@ fn main() { use_ctap1_fallback: false, }; + let (sign_tx, sign_rx) = channel(); loop { - let (sign_tx, sign_rx) = channel(); - let callback = StateCallback::new(Box::new(move |rv| { sign_tx.send(rv).unwrap(); })); From e1fce6eb8306a219cdb03291edf02f0e085447fc Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 4 Jun 2024 19:23:43 +0200 Subject: [PATCH 10/41] Remove unused loop --- examples/prf.rs | 133 +++++++++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 70 deletions(-) diff --git a/examples/prf.rs b/examples/prf.rs index bc087604..58c89efd 100644 --- a/examples/prf.rs +++ b/examples/prf.rs @@ -192,27 +192,24 @@ fn main() { let attestation_object; let (register_tx, register_rx) = channel(); - loop { - let callback = StateCallback::new(Box::new(move |rv| { - register_tx.send(rv).unwrap(); - })); + let callback = StateCallback::new(Box::new(move |rv| { + register_tx.send(rv).unwrap(); + })); - if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx.clone(), callback) { - panic!("Couldn't register: {:?}", e); - }; + if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx.clone(), callback) { + panic!("Couldn't register: {:?}", e); + }; - let register_result = register_rx - .recv() - .expect("Problem receiving, unable to continue"); - match register_result { - Ok(a) => { - println!("Ok!"); - attestation_object = a; - break; - } - Err(e) => panic!("Registration failed: {:?}", e), - }; - } + let register_result = register_rx + .recv() + .expect("Problem receiving, unable to continue"); + match register_result { + Ok(a) => { + println!("Ok!"); + attestation_object = a; + } + Err(e) => panic!("Registration failed: {:?}", e), + }; println!("Register result: {:?}", &attestation_object); @@ -248,67 +245,63 @@ fn main() { }; let (sign_tx, sign_rx) = channel(); - loop { - let callback = StateCallback::new(Box::new(move |rv| { - sign_tx.send(rv).unwrap(); - })); - - if let Err(e) = manager.sign(timeout_ms, ctap_args, status_tx, callback) { - panic!("Couldn't sign: {:?}", e); - } + let callback = StateCallback::new(Box::new(move |rv| { + sign_tx.send(rv).unwrap(); + })); - let sign_result = sign_rx - .recv() - .expect("Problem receiving, unable to continue"); + if let Err(e) = manager.sign(timeout_ms, ctap_args, status_tx, callback) { + panic!("Couldn't sign: {:?}", e); + } - match sign_result { - Ok(assertion_object) => { - println!("Assertion Object: {assertion_object:?}"); - println!("Done."); + let sign_result = sign_rx + .recv() + .expect("Problem receiving, unable to continue"); - if sign_hmac_secret.is_some() { - let hmac_secret_outputs = assertion_object - .extensions - .hmac_get_secret - .as_ref() - .expect("Expected hmac-secret output"); + match sign_result { + Ok(assertion_object) => { + println!("Assertion Object: {assertion_object:?}"); + println!("Done."); - assert_eq!( - Some(hmac_secret_outputs.output1), - hmac_secret_outputs.output2, - "Expected hmac-secret outputs to be equal for equal input" - ); + if sign_hmac_secret.is_some() { + let hmac_secret_outputs = assertion_object + .extensions + .hmac_get_secret + .as_ref() + .expect("Expected hmac-secret output"); - assert_eq!( - assertion_object.extensions.prf, None, - "Expected no PRF outputs when hmacGetSecret input was present" - ); - } + assert_eq!( + Some(hmac_secret_outputs.output1), + hmac_secret_outputs.output2, + "Expected hmac-secret outputs to be equal for equal input" + ); - if sign_prf.is_some() { - let prf_results = assertion_object - .extensions - .prf - .expect("Expected PRF output") - .results - .expect("Expected PRF output to contain results"); + assert_eq!( + assertion_object.extensions.prf, None, + "Expected no PRF outputs when hmacGetSecret input was present" + ); + } - assert_eq!( - Some(prf_results.first), - prf_results.second, - "Expected PRF results to be equal for equal input" - ); + if sign_prf.is_some() { + let prf_results = assertion_object + .extensions + .prf + .expect("Expected PRF output") + .results + .expect("Expected PRF output to contain results"); - assert_eq!( - assertion_object.extensions.hmac_get_secret, None, - "Expected no hmacGetSecret output when PRF input was present" - ); - } + assert_eq!( + Some(prf_results.first), + prf_results.second, + "Expected PRF results to be equal for equal input" + ); - break; + assert_eq!( + assertion_object.extensions.hmac_get_secret, None, + "Expected no hmacGetSecret output when PRF input was present" + ); } - - Err(e) => panic!("Signing failed: {:?}", e), } + + Err(e) => panic!("Signing failed: {:?}", e), } } From a74120a66214d220fa340df38bcc3a8e9da42bed Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 5 Jun 2024 14:36:49 +0200 Subject: [PATCH 11/41] Add tests for HmacSecretResponse::decrypt_secrets --- src/crypto/mod.rs | 18 +++++ src/ctap2/attestation.rs | 160 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 168 insertions(+), 10 deletions(-) diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index fd74030e..4600684c 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -342,6 +342,23 @@ impl SharedSecret { pub fn peer_input(&self) -> &COSEKey { &self.inputs.peer } + + #[cfg(test)] + pub fn new_test( + pin_protocol: PinUvAuthProtocol, + key: Vec, + client_input: COSEKey, + peer_input: COSEKey, + ) -> Self { + Self { + pin_protocol, + key, + inputs: PublicInputs { + client: client_input, + peer: peer_input, + }, + } + } } #[derive(Clone, Debug)] @@ -1056,6 +1073,7 @@ impl Serialize for COSEKey { /// Errors that can be returned from COSE functions. #[derive(Debug, Clone, Serialize)] +#[cfg_attr(test, derive(PartialEq))] pub enum CryptoError { // DecodingFailure, LibraryFailure, diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index b46b1918..a72a99fd 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -43,16 +43,20 @@ impl HmacSecretResponse { hmac_outputs: &[u8], ) -> Result { let output_secrets = shared_secret.decrypt(hmac_outputs)?; - let (output1, output2) = output_secrets.split_at(32); - Ok(HMACGetSecretOutput { - output1: output1 - .try_into() - .map_err(|_| CryptoError::WrongSaltLength)?, - output2: Some(output2) - .filter(|o2| !o2.is_empty()) - .map(|o2| o2.try_into().map_err(|_| CryptoError::WrongSaltLength)) - .transpose()?, - }) + if output_secrets.len() < 32 { + Err(CryptoError::WrongSaltLength) + } else { + let (output1, output2) = output_secrets.split_at(32); + Ok(HMACGetSecretOutput { + output1: output1 + .try_into() + .map_err(|_| CryptoError::WrongSaltLength)?, + output2: Some(output2) + .filter(|o2| !o2.is_empty()) + .map(|o2| o2.try_into().map_err(|_| CryptoError::WrongSaltLength)) + .transpose()?, + }) + } } } @@ -1159,4 +1163,140 @@ pub mod test { ); } } + + mod hmac_secret { + use std::convert::TryFrom; + + use crate::{ + crypto::{ + COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, CryptoError, Curve, + PinUvAuthProtocol, SharedSecret, + }, + ctap2::{attestation::HmacSecretResponse, commands::CommandError}, + AuthenticatorInfo, + }; + + fn make_test_secret(pin_protocol: u64) -> Result { + let fake_unused_key = COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![], + y: vec![], + }), + }; + + let pin_protocol = PinUvAuthProtocol::try_from(&AuthenticatorInfo { + pin_protocols: Some(vec![pin_protocol]), + ..Default::default() + })?; + + let key = { + let aes_key = 0..32; + let hmac_key = 32..64; + match pin_protocol.id() { + 1 => aes_key.collect(), + 2 => hmac_key.chain(aes_key).collect(), + _ => unimplemented!(), + } + }; + + Ok(SharedSecret::new_test( + pin_protocol, + key, + fake_unused_key.clone(), + fake_unused_key, + )) + } + + const PIN_PROTOCOL_2_IV: [u8; 16] = [0; 16]; // PIN protocol 1 uses a hard-coded all-zero IV + const EXPECTED_OUTPUT1: [u8; 32] = [ + 145, 61, 188, 229, 73, 58, 253, 192, 87, 114, 133, 138, 173, 74, 68, 50, 105, 3, 44, 7, + 205, 92, 54, 139, 137, 207, 7, 105, 89, 85, 211, 130, + ]; + const EXPECTED_OUTPUT2: Option<[u8; 32]> = Some([ + 155, 19, 88, 255, 192, 226, 50, 42, 243, 22, 42, 12, 146, 77, 108, 29, 71, 72, 149, + 153, 183, 65, 182, 149, 71, 202, 57, 123, 239, 79, 94, 230, + ]); + + #[test] + fn decrypt_confirmed_returns_none() -> Result<(), CommandError> { + let shared_secret = make_test_secret(2)?; + for flag in [true, false] { + let resp = HmacSecretResponse::Confirmed(flag); + let hmac_output = resp.decrypt_secrets(&shared_secret); + assert_eq!(hmac_output, None, "Failed for confirmed flag: {:?}", flag); + } + Ok(()) + } + + #[test] + fn decrypt_one_secret_pin_protocol_1() -> Result<(), CommandError> { + const CT_LEN: u8 = 32 * 1; + let shared_secret = make_test_secret(1)?; + let resp = HmacSecretResponse::Secret((0..CT_LEN).collect()); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, None, "Incorrect output2"); + Ok(()) + } + + #[test] + fn decrypt_two_secrets_pin_protocol_1() -> Result<(), CommandError> { + const CT_LEN: u8 = 32 * 2; + let shared_secret = make_test_secret(1)?; + let resp = HmacSecretResponse::Secret((0..CT_LEN).collect()); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, EXPECTED_OUTPUT2, "Incorrect output2"); + Ok(()) + } + + #[test] + fn decrypt_one_secret_pin_protocol_2() -> Result<(), CommandError> { + const CT_LEN: u8 = 32 * 1; + let shared_secret = make_test_secret(2)?; + let resp = HmacSecretResponse::Secret( + PIN_PROTOCOL_2_IV.iter().copied().chain(0..CT_LEN).collect(), + ); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, None, "Incorrect output2"); + Ok(()) + } + + #[test] + fn decrypt_two_secrets_pin_protocol_2() -> Result<(), CommandError> { + const CT_LEN: u8 = 32 * 2; + let shared_secret = make_test_secret(2)?; + let resp = HmacSecretResponse::Secret( + PIN_PROTOCOL_2_IV.iter().copied().chain(0..CT_LEN).collect(), + ); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, EXPECTED_OUTPUT2, "Incorrect output2"); + Ok(()) + } + + #[test] + fn decrypt_wrong_length_pin_protocol_2() -> Result<(), CommandError> { + // hmac-secret output can only be multiples of 32 bytes since it operates on whole AES cipher blocks + let shared_secret = make_test_secret(2)?; + { + // Empty cleartext + let resp = HmacSecretResponse::Secret(PIN_PROTOCOL_2_IV.to_vec()); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap(); + assert_eq!(hmac_output, Err(CryptoError::WrongSaltLength)); + } + { + // Too long cleartext + let resp = HmacSecretResponse::Secret( + PIN_PROTOCOL_2_IV.iter().copied().chain(0..96).collect(), + ); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap(); + assert_eq!(hmac_output, Err(CryptoError::WrongSaltLength)); + } + Ok(()) + } + } } From 15b1f5bae07dc4fd5c15dedd99ca17c49b812e83 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 5 Jun 2024 15:07:35 +0200 Subject: [PATCH 12/41] Extract function AuthenticationExtensionsPRFInputs::eval_to_salt --- src/ctap2/server.rs | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 968c065a..cc0c1fd3 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -425,26 +425,10 @@ impl AuthenticationExtensionsPRFInputs { .or(self.eval.as_ref().map(|eval| (None, eval))) { let mut hmac_secret = HmacSecretExtension::new( - sha256( - b"WebAuthn PRF" - .iter() - .chain([0x00].iter()) - .chain(ev.first.iter()) - .copied() - .collect::>(), - ) - .to_vec(), - ev.second.as_ref().map(|second| { - sha256( - b"WebAuthn PRF" - .iter() - .chain([0x00].iter()) - .chain(second.iter()) - .copied() - .collect::>(), - ) - .to_vec() - }), + Self::eval_to_salt(&ev.first).to_vec(), + ev.second + .as_ref() + .map(|second| Self::eval_to_salt(second).to_vec()), ); hmac_secret.calculate(secret, puat)?; Ok(Some((hmac_secret, selected_credential))) @@ -452,6 +436,18 @@ impl AuthenticationExtensionsPRFInputs { Ok(None) } } + + /// Convert a PRF eval input to an hmac-secret salt input. + fn eval_to_salt(eval: &[u8]) -> [u8; 32] { + sha256( + b"WebAuthn PRF" + .iter() + .chain([0x00].iter()) + .chain(eval.iter()) + .copied() + .collect::>(), + ) + } } #[derive(Clone, Debug, Default, Eq, PartialEq)] From 0df9cf22a1c53b2d2d05434b160c09c096697d9e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 5 Jun 2024 15:08:07 +0200 Subject: [PATCH 13/41] Extract AuthenticationExtensionsPRFInputs::select_eval and ::select_credential --- src/ctap2/server.rs | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index cc0c1fd3..a12f587e 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -412,18 +412,7 @@ impl AuthenticationExtensionsPRFInputs { )>, AuthenticatorError, > { - if let Some((selected_credential, ev)) = self - .eval_by_credential - .as_ref() - .and_then(|eval_by_credential| { - allow_credentials.iter().find_map(|pkcd| { - eval_by_credential - .get(&pkcd.id) - .map(|eval| (Some(pkcd), eval)) - }) - }) - .or(self.eval.as_ref().map(|eval| (None, eval))) - { + if let Some((selected_credential, ev)) = self.select_eval(allow_credentials) { let mut hmac_secret = HmacSecretExtension::new( Self::eval_to_salt(&ev.first).to_vec(), ev.second @@ -437,6 +426,37 @@ impl AuthenticationExtensionsPRFInputs { } } + /// Select an `evalByCredential` entry matching any element of `allow_credentials`, + /// or otherwise fall back to `eval`, if present, if no match is found. + fn select_eval<'allow_cred>( + &self, + allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], + ) -> Option<( + Option<&'allow_cred PublicKeyCredentialDescriptor>, + &AuthenticationExtensionsPRFValues, + )> { + self.select_credential(allow_credentials) + .map(|(cred, ev)| (Some(cred), ev)) + .or(self.eval.as_ref().map(|eval| (None, eval))) + } + + /// Select an `evalByCredential` entry matching any element of `allow_credentials`. + fn select_credential<'allow_cred>( + &self, + allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], + ) -> Option<( + &'allow_cred PublicKeyCredentialDescriptor, + &AuthenticationExtensionsPRFValues, + )> { + self.eval_by_credential + .as_ref() + .and_then(|eval_by_credential| { + allow_credentials + .iter() + .find_map(|pkcd| eval_by_credential.get(&pkcd.id).map(|eval| (pkcd, eval))) + }) + } + /// Convert a PRF eval input to an hmac-secret salt input. fn eval_to_salt(eval: &[u8]) -> [u8; 32] { sha256( From 7222884c8a17e3f3bf1b0e357fa7df27523345f8 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 5 Jun 2024 15:08:25 +0200 Subject: [PATCH 14/41] Add doc comment to AuthenticationExtensionsPRFInputs::calculate --- src/ctap2/server.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index a12f587e..ccc015b7 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -400,6 +400,12 @@ pub struct AuthenticationExtensionsPRFInputs { } impl AuthenticationExtensionsPRFInputs { + /// Select an `eval` or `evalByCredential` entry and calculate hmac-secret salt inputs from those inputs. + /// + /// Returns [None] if the `eval` input was not given and no credential in `allow_credentials` matched any `evalByCredential` entry. + /// Otherwise returns the initialized [HmacSecretExtension] and, if an `evalByCredential` entry was used to compute the salt inputs, + /// the [PublicKeyCredentialDescriptor] matching that `evalByCredential` entry. + /// If present, `allowCredentials` SHOULD be set to contain only that [PublicKeyCredentialDescriptor] value. pub fn calculate<'allow_cred>( &self, secret: &SharedSecret, From 25a535ede5e4f2355ecd1c7c11a1514ee23bb6c7 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 10 Jun 2024 14:36:25 +0200 Subject: [PATCH 15/41] Fix clippy lint --- src/ctap2/attestation.rs | 4 ++-- src/ctap2/commands/get_assertion.rs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index a72a99fd..431594f3 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1232,7 +1232,7 @@ pub mod test { #[test] fn decrypt_one_secret_pin_protocol_1() -> Result<(), CommandError> { - const CT_LEN: u8 = 32 * 1; + const CT_LEN: u8 = 32; let shared_secret = make_test_secret(1)?; let resp = HmacSecretResponse::Secret((0..CT_LEN).collect()); let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; @@ -1254,7 +1254,7 @@ pub mod test { #[test] fn decrypt_one_secret_pin_protocol_2() -> Result<(), CommandError> { - const CT_LEN: u8 = 32 * 1; + const CT_LEN: u8 = 32; let shared_secret = make_test_secret(2)?; let resp = HmacSecretResponse::Secret( PIN_PROTOCOL_2_IV.iter().copied().chain(0..CT_LEN).collect(), diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index c20eda50..5fa3ccc5 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -203,7 +203,6 @@ 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 ), - ..Default::default() } } } @@ -275,7 +274,6 @@ impl GetAssertion { dev.get_shared_secret() .and_then(|shared_secret| hmac_response.decrypt_secrets(shared_secret)) .and_then(Result::ok) - .map(|outputs| outputs.into()) } else { None }; From c6d07560ecf7a7259657598e9b3c01afd38670bb Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 10 Jun 2024 16:41:21 +0200 Subject: [PATCH 16/41] Return empty prf output if no eval or evalByCredential entry matched --- src/ctap2/commands/get_assertion.rs | 22 ++++++++++++++++++++-- src/ctap2/mod.rs | 6 ++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 5fa3ccc5..fea31bd1 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -81,10 +81,18 @@ pub enum HmacGetSecretOrPrf { HmacGetSecret(HmacSecretExtension), /// hmac-secret input is to be calculated from PRF inputs, but we haven't yet identified which eval or evalByCredential entry to use. PrfUninitialized(AuthenticationExtensionsPRFInputs), + /// prf client input with no eval or matchin evalByCredential entry. + PrfUnmatched, /// hmac-secret inputs set by the prf client extension input. Prf(HmacSecretExtension), } +impl HmacGetSecretOrPrf { + fn skip_serializing(value: &Option) -> bool { + matches!(value, None | Some(Self::PrfUnmatched)) + } +} + impl Serialize for HmacGetSecretOrPrf { fn serialize(&self, s: S) -> Result where @@ -93,8 +101,9 @@ impl Serialize for HmacGetSecretOrPrf { match self { Self::HmacGetSecret(ext) => ext.serialize(s), Self::PrfUninitialized(_) => Err(serde::ser::Error::custom( - "PrfUninitialized must be replaced with Prf before serializing", + "PrfUninitialized must be replaced with Prf or PrfEmpty before serializing", )), + Self::PrfUnmatched => unreachable!("PrfEmpty serialization should be skipped"), Self::Prf(ext) => ext.serialize(s), } } @@ -183,7 +192,10 @@ impl Serialize for HmacSecretExtension { pub struct GetAssertionExtensions { #[serde(skip_serializing)] pub app_id: Option, - #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "hmac-secret", + skip_serializing_if = "HmacGetSecretOrPrf::skip_serializing" + )] pub hmac_secret: Option, } @@ -281,6 +293,12 @@ impl GetAssertion { Some(HmacGetSecretOrPrf::PrfUninitialized(_)) => { unreachable!("Reached GetAssertion.finalize_result without replacing PrfUninitialized instance with Prf") } + Some(HmacGetSecretOrPrf::PrfUnmatched) => { + result.extensions.prf = Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: None, + }); + } Some(HmacGetSecretOrPrf::Prf(_)) => { result.extensions.prf = Some(AuthenticationExtensionsPRFOutputs { enabled: None, diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index c647f93a..396ee3d4 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -692,7 +692,9 @@ pub fn sign( get_assertion.allow_list = vec![selected_cred_id.clone()]; } } - Ok(None) => {} + Ok(None) => { + *hmac_get_secret_or_prf = HmacGetSecretOrPrf::PrfUnmatched; + } Err(e) => { callback.call(Err(e)); return false; @@ -701,7 +703,7 @@ pub fn sign( } } - HmacGetSecretOrPrf::Prf(_) => { + HmacGetSecretOrPrf::Prf(_) | HmacGetSecretOrPrf::PrfUnmatched => { unreachable!("hmac-secret inputs from PRF already initialized") } }; From 04191f9cbef4a0ab041578a4f3b3d8b6f5a80129 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 13 Jun 2024 17:05:23 +0200 Subject: [PATCH 17/41] Extract function HmacGetSecretOrPrf::calculate --- src/ctap2/commands/get_assertion.rs | 44 ++++++++++++++++++ src/ctap2/mod.rs | 72 +++++++++++------------------ 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index fea31bd1..86b6f518 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -91,6 +91,50 @@ impl HmacGetSecretOrPrf { fn skip_serializing(value: &Option) -> bool { matches!(value, None | Some(Self::PrfUnmatched)) } + + /// Calculate the appropriate hmac-secret or PRF salt inputs from the given inputs. + /// + /// - If this is a `HmacGetSecret` instance, + /// this returns a new `HmacGetSecret` instance with `calculated_hmac` set, paired with [None]. + /// - If this is a `PrfUninitialized` instance, + /// this attempts to select a PRF input to calculate salts from. + /// If an input is found, this returns a `Prf` instance with `calculated_hmac` set. + /// If the selected input came from `eval_by_credential`, + /// then this is paired with a [Some] referencing the matching element of `allow_credentials`. + /// If the selected input was `eval`, then this is paired with [None]. + /// If no input is found, this returns `PrfUnmatched` and [None]. + /// - If this is a `Prf` or `PrfUnmatched` instance, this panics. + /// + /// If the [Option] return value is [Some], the caller SHOULD set `allowCredentials` + /// to contain only that [PublicKeyCredentialDescriptor] value. + /// + /// # Panics + /// If this is a `Prf` or `PrfUnmatched` instance. + pub fn calculate<'allow_cred>( + self, + secret: &SharedSecret, + allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], + puat: Option, + ) -> Result<(Self, Option<&'allow_cred PublicKeyCredentialDescriptor>), AuthenticatorError> + { + Ok(match self { + Self::HmacGetSecret(mut extension) => { + extension.calculate(secret, puat)?; + (Self::HmacGetSecret(extension), None) + } + + Self::PrfUninitialized(prf) => match prf.calculate(secret, allow_credentials, puat)? { + Some((hmac_secret, selected_credential)) => { + (Self::Prf(hmac_secret), selected_credential) + } + None => (Self::PrfUnmatched, None), + }, + + Self::Prf(_) | Self::PrfUnmatched => { + unreachable!("hmac-secret inputs from PRF already initialized") + } + }) + } } impl Serialize for HmacGetSecretOrPrf { diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index 396ee3d4..518f4842 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -22,9 +22,7 @@ use crate::ctap2::commands::credential_management::{ CredManagementCommand, CredentialList, CredentialListEntry, CredentialManagement, CredentialManagementResult, CredentialRpListEntry, }; -use crate::ctap2::commands::get_assertion::{ - GetAssertion, GetAssertionOptions, HmacGetSecretOrPrf, -}; +use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionOptions}; use crate::ctap2::commands::make_credentials::{ dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsOptions, }; @@ -663,51 +661,33 @@ pub fn sign( } // Third, use the shared secret in the extensions, if requested - if let Some(hmac_get_secret_or_prf) = get_assertion.extensions.hmac_secret.as_mut() { - match hmac_get_secret_or_prf { - HmacGetSecretOrPrf::HmacGetSecret(extension) => { - if let Some(secret) = dev.get_shared_secret() { - match extension - .calculate(secret, pin_uv_auth_result.get_pin_uv_auth_token()) - { - Ok(_) => {} - Err(e) => { - callback.call(Err(e)); - return false; - } - } - } - } - - HmacGetSecretOrPrf::PrfUninitialized(prf) => { - if let Some(secret) = dev.get_shared_secret() { - match prf.calculate( - secret, - &get_assertion.allow_list, - pin_uv_auth_result.get_pin_uv_auth_token(), - ) { - Ok(Some((hmac_secret, selected_credential))) => { - *hmac_get_secret_or_prf = HmacGetSecretOrPrf::Prf(hmac_secret); - if let Some(selected_cred_id) = selected_credential { - get_assertion.allow_list = vec![selected_cred_id.clone()]; - } - } - Ok(None) => { - *hmac_get_secret_or_prf = HmacGetSecretOrPrf::PrfUnmatched; - } - Err(e) => { - callback.call(Err(e)); - return false; - } - } + get_assertion.extensions.hmac_secret = match get_assertion + .extensions + .hmac_secret + .take() + .map(|hmac_get_secret_or_prf| { + if let Some(secret) = dev.get_shared_secret() { + let (extension, selected_credential) = hmac_get_secret_or_prf.calculate( + secret, + &get_assertion.allow_list, + pin_uv_auth_result.get_pin_uv_auth_token(), + )?; + if let Some(selected_credential) = selected_credential { + get_assertion.allow_list = vec![selected_credential.clone()]; } + Ok(extension) + } else { + Ok(hmac_get_secret_or_prf) } - - HmacGetSecretOrPrf::Prf(_) | HmacGetSecretOrPrf::PrfUnmatched => { - unreachable!("hmac-secret inputs from PRF already initialized") - } - }; - } + }) + .transpose() + { + Ok(extension) => extension, + Err(e) => { + callback.call(Err(e)); + return false; + } + }; debug!("------------------------------------------------------------------"); debug!("{get_assertion:?} using {pin_uv_auth_result:?}"); From c6b4479062b49df395382f950f9a98725f7b681a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 13 Jun 2024 19:24:06 +0200 Subject: [PATCH 18/41] Add tests of calculating hmac-secret/PRF inputs --- src/ctap2/commands/get_assertion.rs | 538 ++++++++++++++++++++++++++++ src/ctap2/server.rs | 1 + 2 files changed, 539 insertions(+) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 86b6f518..3e8e3f96 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -68,6 +68,7 @@ impl UserVerification for GetAssertionOptions { } #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] pub struct CalculatedHmacSecretExtension { pub public_key: COSEKey, pub salt_enc: Vec, @@ -76,6 +77,7 @@ pub struct CalculatedHmacSecretExtension { /// Wrapper type recording whether the hmac-secret input originally came from the hmacGetSecret or the prf client extension input. #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] pub enum HmacGetSecretOrPrf { /// hmac-secret inputs set by the hmacGetSecret client extension input. HmacGetSecret(HmacSecretExtension), @@ -154,6 +156,7 @@ impl Serialize for HmacGetSecretOrPrf { } #[derive(Debug, Clone, Default)] +#[cfg_attr(test, derive(PartialEq))] pub struct HmacSecretExtension { pub salt1: Vec, pub salt2: Option>, @@ -1646,4 +1649,539 @@ pub mod test { 0x05, // unsigned(5) 0x01, // unsigned(1) ]; + + mod hmac_secret { + use std::convert::TryFrom; + + use crate::{ + crypto::{ + COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, PinUvAuthProtocol, + SharedSecret, + }, + ctap2::{ + commands::{ + client_pin::PinUvAuthTokenPermission, + get_assertion::{ + CalculatedHmacSecretExtension, HmacGetSecretOrPrf, HmacSecretExtension, + }, + CommandError, + }, + server::{ + AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFValues, + PublicKeyCredentialDescriptor, + }, + }, + errors::AuthenticatorError, + AuthenticatorInfo, + }; + + fn make_test_secret(pin_protocol: u64) -> Result<(SharedSecret, COSEKey), CommandError> { + let fake_client_key = COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![1], + y: vec![2], + }), + }; + let fake_peer_key = COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![3], + y: vec![4], + }), + }; + + let pin_protocol = PinUvAuthProtocol::try_from(&AuthenticatorInfo { + pin_protocols: Some(vec![pin_protocol]), + ..Default::default() + })?; + + let key = { + let aes_key = 0..32; + let hmac_key = 32..64; + match pin_protocol.id() { + 1 => aes_key.collect(), + 2 => hmac_key.chain(aes_key).collect(), + _ => unimplemented!(), + } + }; + + Ok(( + SharedSecret::new_test(pin_protocol, key, fake_client_key.clone(), fake_peer_key), + fake_client_key, + )) + } + + #[test] + fn calculate_hmac_get_secret_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; + let extension = HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; 32], + Some(vec![0x02; 32]), + )); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &[], Some(puat))?; + + assert_eq!(selected_cred, None); + assert_eq!( + extension, + HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension { + salt1: vec![0x01; 32], + salt2: Some(vec![0x02; 32]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 117, 226, 8, 41, 23, 33, 18, 187, 242, 160, 77, 61, 43, 18, 67, 61, + 170, 97, 245, 245, 17, 42, 232, 186, 255, 190, 82, 1, 81, 152, 175, 39, + 113, 130, 62, 169, 215, 202, 143, 80, 116, 195, 117, 22, 39, 64, 79, + 110, 216, 117, 7, 144, 87, 73, 144, 75, 255, 173, 169, 201, 122, 160, + 48, 157 + ], + salt_auth: vec![ + 36, 74, 81, 146, 64, 28, 73, 44, 75, 111, 14, 79, 173, 146, 212, 227 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_eval_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![0x01; 8], + second: Some(vec![0x02; 8]), + }), + eval_by_credential: None, + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &[], Some(puat))?; + + assert_eq!(selected_cred, None); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1: vec![ + 0x05, 0xf0, 0xb3, 0xb2, 0x3e, 0x7e, 0xcd, 0xac, 0xb0, 0x69, 0xd3, 0x0d, + 0x56, 0xd2, 0x30, 0xd2, 0xe1, 0xdb, 0xea, 0xf8, 0x10, 0xb6, 0x34, 0xdb, + 0x5c, 0x87, 0x61, 0x77, 0x6b, 0xf5, 0x1e, 0xe2 + ], + salt2: Some(vec![ + 0x60, 0x6b, 0x41, 0xea, 0x4d, 0xb0, 0xfb, 0x18, 0xc1, 0xbc, 0x62, 0x17, + 0x3b, 0xf0, 0xd4, 0x06, 0x68, 0xb0, 0x28, 0xf2, 0x68, 0xbe, 0x20, 0x7c, + 0xe2, 0xf4, 0x13, 0xa0, 0x08, 0x69, 0xfd, 0x6a + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, + 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, 180, + 39, 31, 155, 232, 119, 179, 0, 65, 9, 37, 184, 194, 135, 173, 187, 197, + 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, 60, 109, 33, + 112, 175 + ], + salt_auth: vec![ + 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, 174 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_eval_pin_protocol_2() -> Result<(), AuthenticatorError> { + let (shared_secret, _) = make_test_secret(2)?; + for (second, expected_enc_len) in [(None, 48), (Some(vec![0x02; 8]), 80)] { + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![0x01; 8], + second: second.clone(), + }), + eval_by_credential: None, + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &[], Some(puat))?; + + assert_eq!(selected_cred, None); + assert_matches!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + // PIN protocol 2 uses a random IV, so salt_enc and salt_auth are not static + calculated_hmac: Some(CalculatedHmacSecretExtension { + salt_enc, + salt_auth, + .. + }), + pin_protocol: Some(2), + .. + }) if salt_enc.len() == expected_enc_len && salt_auth.len() == 32, + "Failed with second: {second:?}, expected_enc_len: {expected_enc_len:?}" + ); + } + Ok(()) + } + + #[test] + fn calculate_prf_eval_by_cred_fallback_to_eval_pin_protocol_1( + ) -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![0x01; 8], + second: Some(vec![0x02; 8]), + }), + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + )] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; + + assert_eq!(selected_cred, None); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1: vec![ + 0x05, 0xf0, 0xb3, 0xb2, 0x3e, 0x7e, 0xcd, 0xac, 0xb0, 0x69, 0xd3, 0x0d, + 0x56, 0xd2, 0x30, 0xd2, 0xe1, 0xdb, 0xea, 0xf8, 0x10, 0xb6, 0x34, 0xdb, + 0x5c, 0x87, 0x61, 0x77, 0x6b, 0xf5, 0x1e, 0xe2 + ], + salt2: Some(vec![ + 0x60, 0x6b, 0x41, 0xea, 0x4d, 0xb0, 0xfb, 0x18, 0xc1, 0xbc, 0x62, 0x17, + 0x3b, 0xf0, 0xd4, 0x06, 0x68, 0xb0, 0x28, 0xf2, 0x68, 0xbe, 0x20, 0x7c, + 0xe2, 0xf4, 0x13, 0xa0, 0x08, 0x69, 0xfd, 0x6a + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, + 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, 180, + 39, 31, 155, 232, 119, 179, 0, 65, 9, 37, 184, 194, 135, 173, 187, 197, + 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, 60, 109, 33, + 112, 175 + ], + salt_auth: vec![ + 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, 174 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; + let cred_id = PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![0x01; 8], + second: Some(vec![0x02; 8]), + }), + eval_by_credential: Some( + [ + ( + vec![9, 10, 11, 12], + AuthenticationExtensionsPRFValues { + first: vec![0x06; 8], + second: Some(vec![0x07; 8]), + }, + ), + ( + cred_id.id.clone(), + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + ), + ] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [ + PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }, + cred_id, + PublicKeyCredentialDescriptor { + id: vec![9, 10, 11, 12], + transports: vec![], + }, + ]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; + + assert_eq!(selected_cred, Some(&allow_list[1])); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1: vec![ + 0x8d, 0x31, 0xd7, 0xf0, 0x6e, 0xc1, 0x54, 0x1b, 0x71, 0x99, 0x81, 0x6c, + 0x47, 0x3b, 0x62, 0x05, 0xd1, 0x2d, 0xbe, 0x8e, 0x2f, 0x04, 0x48, 0x4e, + 0xd9, 0x55, 0x63, 0xf3, 0xc0, 0xd9, 0xe8, 0x58 + ], + salt2: Some(vec![ + 0x9c, 0x58, 0x7f, 0x97, 0xcc, 0x5a, 0x91, 0xc8, 0xcf, 0xc9, 0x6a, 0x7c, + 0x13, 0x3c, 0x1d, 0x73, 0x91, 0xc5, 0x1b, 0x94, 0x75, 0x48, 0x12, 0x04, + 0x4e, 0xbb, 0xa1, 0x7a, 0x90, 0xf5, 0x43, 0x01 + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, 47, + 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, 209, + 25, 189, 32, 51, 75, 255, 176, 160, 82, 113, 250, 141, 83, 130, 69, + 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, 167, 210, + 241, 238, 237 + ], + salt_auth: vec![ + 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, 232 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_only_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; + let cred_id = PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [ + ( + vec![9, 10, 11, 12], + AuthenticationExtensionsPRFValues { + first: vec![0x06; 8], + second: Some(vec![0x07; 8]), + }, + ), + ( + cred_id.id.clone(), + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + ), + ] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [ + PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }, + cred_id, + PublicKeyCredentialDescriptor { + id: vec![9, 10, 11, 12], + transports: vec![], + }, + ]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; + + assert_eq!(selected_cred, Some(&allow_list[1])); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1: vec![ + 0x8d, 0x31, 0xd7, 0xf0, 0x6e, 0xc1, 0x54, 0x1b, 0x71, 0x99, 0x81, 0x6c, + 0x47, 0x3b, 0x62, 0x05, 0xd1, 0x2d, 0xbe, 0x8e, 0x2f, 0x04, 0x48, 0x4e, + 0xd9, 0x55, 0x63, 0xf3, 0xc0, 0xd9, 0xe8, 0x58 + ], + salt2: Some(vec![ + 0x9c, 0x58, 0x7f, 0x97, 0xcc, 0x5a, 0x91, 0xc8, 0xcf, 0xc9, 0x6a, 0x7c, + 0x13, 0x3c, 0x1d, 0x73, 0x91, 0xc5, 0x1b, 0x94, 0x75, 0x48, 0x12, 0x04, + 0x4e, 0xbb, 0xa1, 0x7a, 0x90, 0xf5, 0x43, 0x01 + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, 47, + 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, 209, + 25, 189, 32, 51, 75, 255, 176, 160, 82, 113, 250, 141, 83, 130, 69, + 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, 167, 210, + 241, 238, 237 + ], + salt_auth: vec![ + 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, 232 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_unmatched_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, _) = make_test_secret(1)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + )] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; + + assert_eq!(selected_cred, None); + assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched,); + + Ok(()) + } + + #[test] + fn calculate_prf_unmatched_pin_protocol_2() -> Result<(), AuthenticatorError> { + let (shared_secret, _) = make_test_secret(2)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + )] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; + + assert_eq!(selected_cred, None); + assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched); + + Ok(()) + } + + #[test] + #[should_panic( + expected = "unreachable code: hmac-secret inputs from PRF already initialized" + )] + fn calculate_prf_conflict_1() { + let (shared_secret, _) = make_test_secret(2).unwrap(); + let extension = HmacGetSecretOrPrf::PrfUnmatched; + extension.calculate(&shared_secret, &[], None).unwrap(); + } + + #[test] + #[should_panic( + expected = "unreachable code: hmac-secret inputs from PRF already initialized" + )] + fn calculate_prf_conflict_2() { + let (shared_secret, client_key) = make_test_secret(2).unwrap(); + let extension = HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1: vec![], + salt2: Some(vec![]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![], + salt_auth: vec![], + }), + pin_protocol: None, + }); + extension.calculate(&shared_secret, &[], None).unwrap(); + } + } } diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index ccc015b7..4724bc81 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -394,6 +394,7 @@ pub struct HMACGetSecretOutput { } #[derive(Clone, Debug, Default)] +#[cfg_attr(test, derive(PartialEq))] pub struct AuthenticationExtensionsPRFInputs { pub eval: Option, pub eval_by_credential: Option, AuthenticationExtensionsPRFValues>>, From 269eb6a021642ee108a49c2f98937d0268d46c02 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 13 Jun 2024 19:41:21 +0200 Subject: [PATCH 19/41] Fix outdated error messages --- src/ctap2/commands/get_assertion.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 3e8e3f96..4c72b639 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -147,9 +147,9 @@ impl Serialize for HmacGetSecretOrPrf { match self { Self::HmacGetSecret(ext) => ext.serialize(s), Self::PrfUninitialized(_) => Err(serde::ser::Error::custom( - "PrfUninitialized must be replaced with Prf or PrfEmpty before serializing", + "PrfUninitialized must be replaced with Prf or PrfUnmatched before serializing", )), - Self::PrfUnmatched => unreachable!("PrfEmpty serialization should be skipped"), + Self::PrfUnmatched => unreachable!("PrfUnmatched serialization should be skipped"), Self::Prf(ext) => ext.serialize(s), } } From 5fb07b15dcfe34600ca9ec28eca3c05b5f7c5fba Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 09:35:59 +0200 Subject: [PATCH 20/41] Separate hmac_secret tests that require a crypto backend --- src/ctap2/attestation.rs | 152 +++--- src/ctap2/commands/get_assertion.rs | 781 ++++++++++++++-------------- 2 files changed, 461 insertions(+), 472 deletions(-) diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index 431594f3..504c3367 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1169,8 +1169,8 @@ pub mod test { use crate::{ crypto::{ - COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, CryptoError, Curve, - PinUvAuthProtocol, SharedSecret, + COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, PinUvAuthProtocol, + SharedSecret, }, ctap2::{attestation::HmacSecretResponse, commands::CommandError}, AuthenticatorInfo, @@ -1209,16 +1209,6 @@ pub mod test { )) } - const PIN_PROTOCOL_2_IV: [u8; 16] = [0; 16]; // PIN protocol 1 uses a hard-coded all-zero IV - const EXPECTED_OUTPUT1: [u8; 32] = [ - 145, 61, 188, 229, 73, 58, 253, 192, 87, 114, 133, 138, 173, 74, 68, 50, 105, 3, 44, 7, - 205, 92, 54, 139, 137, 207, 7, 105, 89, 85, 211, 130, - ]; - const EXPECTED_OUTPUT2: Option<[u8; 32]> = Some([ - 155, 19, 88, 255, 192, 226, 50, 42, 243, 22, 42, 12, 146, 77, 108, 29, 71, 72, 149, - 153, 183, 65, 182, 149, 71, 202, 57, 123, 239, 79, 94, 230, - ]); - #[test] fn decrypt_confirmed_returns_none() -> Result<(), CommandError> { let shared_secret = make_test_secret(2)?; @@ -1230,73 +1220,93 @@ pub mod test { Ok(()) } - #[test] - fn decrypt_one_secret_pin_protocol_1() -> Result<(), CommandError> { - const CT_LEN: u8 = 32; - let shared_secret = make_test_secret(1)?; - let resp = HmacSecretResponse::Secret((0..CT_LEN).collect()); - let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; - assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); - assert_eq!(hmac_output.output2, None, "Incorrect output2"); - Ok(()) - } + #[cfg(not(feature = "crypto_dummy"))] + mod requires_crypto { + use super::*; - #[test] - fn decrypt_two_secrets_pin_protocol_1() -> Result<(), CommandError> { - const CT_LEN: u8 = 32 * 2; - let shared_secret = make_test_secret(1)?; - let resp = HmacSecretResponse::Secret((0..CT_LEN).collect()); - let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; - assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); - assert_eq!(hmac_output.output2, EXPECTED_OUTPUT2, "Incorrect output2"); - Ok(()) - } + use crate::{ + crypto::CryptoError, + ctap2::{attestation::HmacSecretResponse, commands::CommandError}, + }; - #[test] - fn decrypt_one_secret_pin_protocol_2() -> Result<(), CommandError> { - const CT_LEN: u8 = 32; - let shared_secret = make_test_secret(2)?; - let resp = HmacSecretResponse::Secret( - PIN_PROTOCOL_2_IV.iter().copied().chain(0..CT_LEN).collect(), - ); - let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; - assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); - assert_eq!(hmac_output.output2, None, "Incorrect output2"); - Ok(()) - } + const PIN_PROTOCOL_2_IV: [u8; 16] = [0; 16]; // PIN protocol 1 uses a hard-coded all-zero IV + const EXPECTED_OUTPUT1: [u8; 32] = [ + 145, 61, 188, 229, 73, 58, 253, 192, 87, 114, 133, 138, 173, 74, 68, 50, 105, 3, + 44, 7, 205, 92, 54, 139, 137, 207, 7, 105, 89, 85, 211, 130, + ]; + const EXPECTED_OUTPUT2: Option<[u8; 32]> = Some([ + 155, 19, 88, 255, 192, 226, 50, 42, 243, 22, 42, 12, 146, 77, 108, 29, 71, 72, 149, + 153, 183, 65, 182, 149, 71, 202, 57, 123, 239, 79, 94, 230, + ]); + + #[test] + fn decrypt_one_secret_pin_protocol_1() -> Result<(), CommandError> { + const CT_LEN: u8 = 32; + let shared_secret = make_test_secret(1)?; + let resp = HmacSecretResponse::Secret((0..CT_LEN).collect()); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, None, "Incorrect output2"); + Ok(()) + } - #[test] - fn decrypt_two_secrets_pin_protocol_2() -> Result<(), CommandError> { - const CT_LEN: u8 = 32 * 2; - let shared_secret = make_test_secret(2)?; - let resp = HmacSecretResponse::Secret( - PIN_PROTOCOL_2_IV.iter().copied().chain(0..CT_LEN).collect(), - ); - let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; - assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); - assert_eq!(hmac_output.output2, EXPECTED_OUTPUT2, "Incorrect output2"); - Ok(()) - } + #[test] + fn decrypt_two_secrets_pin_protocol_1() -> Result<(), CommandError> { + const CT_LEN: u8 = 32 * 2; + let shared_secret = make_test_secret(1)?; + let resp = HmacSecretResponse::Secret((0..CT_LEN).collect()); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, EXPECTED_OUTPUT2, "Incorrect output2"); + Ok(()) + } - #[test] - fn decrypt_wrong_length_pin_protocol_2() -> Result<(), CommandError> { - // hmac-secret output can only be multiples of 32 bytes since it operates on whole AES cipher blocks - let shared_secret = make_test_secret(2)?; - { - // Empty cleartext - let resp = HmacSecretResponse::Secret(PIN_PROTOCOL_2_IV.to_vec()); - let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap(); - assert_eq!(hmac_output, Err(CryptoError::WrongSaltLength)); + #[test] + fn decrypt_one_secret_pin_protocol_2() -> Result<(), CommandError> { + const CT_LEN: u8 = 32; + let shared_secret = make_test_secret(2)?; + let resp = HmacSecretResponse::Secret( + PIN_PROTOCOL_2_IV.iter().copied().chain(0..CT_LEN).collect(), + ); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, None, "Incorrect output2"); + Ok(()) } - { - // Too long cleartext + + #[test] + fn decrypt_two_secrets_pin_protocol_2() -> Result<(), CommandError> { + const CT_LEN: u8 = 32 * 2; + let shared_secret = make_test_secret(2)?; let resp = HmacSecretResponse::Secret( - PIN_PROTOCOL_2_IV.iter().copied().chain(0..96).collect(), + PIN_PROTOCOL_2_IV.iter().copied().chain(0..CT_LEN).collect(), ); - let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap(); - assert_eq!(hmac_output, Err(CryptoError::WrongSaltLength)); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, EXPECTED_OUTPUT2, "Incorrect output2"); + Ok(()) + } + + #[test] + fn decrypt_wrong_length_pin_protocol_2() -> Result<(), CommandError> { + // hmac-secret output can only be multiples of 32 bytes since it operates on whole AES cipher blocks + let shared_secret = make_test_secret(2)?; + { + // Empty cleartext + let resp = HmacSecretResponse::Secret(PIN_PROTOCOL_2_IV.to_vec()); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap(); + assert_eq!(hmac_output, Err(CryptoError::WrongSaltLength)); + } + { + // Too long cleartext + let resp = HmacSecretResponse::Secret( + PIN_PROTOCOL_2_IV.iter().copied().chain(0..96).collect(), + ); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap(); + assert_eq!(hmac_output, Err(CryptoError::WrongSaltLength)); + } + Ok(()) } - Ok(()) } } } diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 4c72b639..f60e3aef 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -1658,20 +1658,12 @@ pub mod test { COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, PinUvAuthProtocol, SharedSecret, }, - ctap2::{ - commands::{ - client_pin::PinUvAuthTokenPermission, - get_assertion::{ - CalculatedHmacSecretExtension, HmacGetSecretOrPrf, HmacSecretExtension, - }, - CommandError, - }, - server::{ - AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFValues, - PublicKeyCredentialDescriptor, + ctap2::commands::{ + get_assertion::{ + CalculatedHmacSecretExtension, HmacGetSecretOrPrf, HmacSecretExtension, }, + CommandError, }, - errors::AuthenticatorError, AuthenticatorInfo, }; @@ -1714,107 +1706,74 @@ pub mod test { )) } - #[test] - fn calculate_hmac_get_secret_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; - let extension = HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( - vec![0x01; 32], - Some(vec![0x02; 32]), - )); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; - let (extension, selected_cred) = - extension.calculate(&shared_secret, &[], Some(puat))?; - - assert_eq!(selected_cred, None); - assert_eq!( - extension, - HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension { - salt1: vec![0x01; 32], - salt2: Some(vec![0x02; 32]), - calculated_hmac: Some(CalculatedHmacSecretExtension { - public_key: client_key, - salt_enc: vec![ - 117, 226, 8, 41, 23, 33, 18, 187, 242, 160, 77, 61, 43, 18, 67, 61, - 170, 97, 245, 245, 17, 42, 232, 186, 255, 190, 82, 1, 81, 152, 175, 39, - 113, 130, 62, 169, 215, 202, 143, 80, 116, 195, 117, 22, 39, 64, 79, - 110, 216, 117, 7, 144, 87, 73, 144, 75, 255, 173, 169, 201, 122, 160, - 48, 157 - ], - salt_auth: vec![ - 36, 74, 81, 146, 64, 28, 73, 44, 75, 111, 14, 79, 173, 146, 212, 227 - ], - }), - pin_protocol: None, - }), - ); + #[cfg(not(feature = "crypto_dummy"))] + mod requires_crypto { + use super::*; + use crate::{ + ctap2::{ + commands::{ + client_pin::PinUvAuthTokenPermission, + get_assertion::{ + CalculatedHmacSecretExtension, HmacGetSecretOrPrf, HmacSecretExtension, + }, + }, + server::{ + AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFValues, + PublicKeyCredentialDescriptor, + }, + }, + errors::AuthenticatorError, + }; - Ok(()) - } + #[test] + fn calculate_hmac_get_secret_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; + let extension = HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; 32], + Some(vec![0x02; 32]), + )); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &[], Some(puat))?; - #[test] - fn calculate_prf_eval_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; - let extension = - HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { - eval: Some(AuthenticationExtensionsPRFValues { - first: vec![0x01; 8], - second: Some(vec![0x02; 8]), - }), - eval_by_credential: None, - }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; - let (extension, selected_cred) = - extension.calculate(&shared_secret, &[], Some(puat))?; - - assert_eq!(selected_cred, None); - assert_eq!( - extension, - HmacGetSecretOrPrf::Prf(HmacSecretExtension { - salt1: vec![ - 0x05, 0xf0, 0xb3, 0xb2, 0x3e, 0x7e, 0xcd, 0xac, 0xb0, 0x69, 0xd3, 0x0d, - 0x56, 0xd2, 0x30, 0xd2, 0xe1, 0xdb, 0xea, 0xf8, 0x10, 0xb6, 0x34, 0xdb, - 0x5c, 0x87, 0x61, 0x77, 0x6b, 0xf5, 0x1e, 0xe2 - ], - salt2: Some(vec![ - 0x60, 0x6b, 0x41, 0xea, 0x4d, 0xb0, 0xfb, 0x18, 0xc1, 0xbc, 0x62, 0x17, - 0x3b, 0xf0, 0xd4, 0x06, 0x68, 0xb0, 0x28, 0xf2, 0x68, 0xbe, 0x20, 0x7c, - 0xe2, 0xf4, 0x13, 0xa0, 0x08, 0x69, 0xfd, 0x6a - ]), - calculated_hmac: Some(CalculatedHmacSecretExtension { - public_key: client_key, - salt_enc: vec![ - 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, - 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, 180, - 39, 31, 155, 232, 119, 179, 0, 65, 9, 37, 184, 194, 135, 173, 187, 197, - 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, 60, 109, 33, - 112, 175 - ], - salt_auth: vec![ - 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, 174 - ], + assert_eq!(selected_cred, None); + assert_eq!( + extension, + HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension { + salt1: vec![0x01; 32], + salt2: Some(vec![0x02; 32]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 117, 226, 8, 41, 23, 33, 18, 187, 242, 160, 77, 61, 43, 18, 67, 61, + 170, 97, 245, 245, 17, 42, 232, 186, 255, 190, 82, 1, 81, 152, 175, + 39, 113, 130, 62, 169, 215, 202, 143, 80, 116, 195, 117, 22, 39, + 64, 79, 110, 216, 117, 7, 144, 87, 73, 144, 75, 255, 173, 169, 201, + 122, 160, 48, 157 + ], + salt_auth: vec![ + 36, 74, 81, 146, 64, 28, 73, 44, 75, 111, 14, 79, 173, 146, 212, + 227 + ], + }), + pin_protocol: None, }), - pin_protocol: None, - }), - ); + ); - Ok(()) - } + Ok(()) + } - #[test] - fn calculate_prf_eval_pin_protocol_2() -> Result<(), AuthenticatorError> { - let (shared_secret, _) = make_test_secret(2)?; - for (second, expected_enc_len) in [(None, 48), (Some(vec![0x02; 8]), 80)] { + #[test] + fn calculate_prf_eval_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; let extension = HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { eval: Some(AuthenticationExtensionsPRFValues { first: vec![0x01; 8], - second: second.clone(), + second: Some(vec![0x02; 8]), }), eval_by_credential: None, }); @@ -1826,333 +1785,353 @@ pub mod test { extension.calculate(&shared_secret, &[], Some(puat))?; assert_eq!(selected_cred, None); - assert_matches!( + assert_eq!( extension, HmacGetSecretOrPrf::Prf(HmacSecretExtension { - // PIN protocol 2 uses a random IV, so salt_enc and salt_auth are not static + salt1: vec![ + 0x05, 0xf0, 0xb3, 0xb2, 0x3e, 0x7e, 0xcd, 0xac, 0xb0, 0x69, 0xd3, 0x0d, + 0x56, 0xd2, 0x30, 0xd2, 0xe1, 0xdb, 0xea, 0xf8, 0x10, 0xb6, 0x34, 0xdb, + 0x5c, 0x87, 0x61, 0x77, 0x6b, 0xf5, 0x1e, 0xe2 + ], + salt2: Some(vec![ + 0x60, 0x6b, 0x41, 0xea, 0x4d, 0xb0, 0xfb, 0x18, 0xc1, 0xbc, 0x62, 0x17, + 0x3b, 0xf0, 0xd4, 0x06, 0x68, 0xb0, 0x28, 0xf2, 0x68, 0xbe, 0x20, 0x7c, + 0xe2, 0xf4, 0x13, 0xa0, 0x08, 0x69, 0xfd, 0x6a + ]), calculated_hmac: Some(CalculatedHmacSecretExtension { - salt_enc, - salt_auth, - .. + public_key: client_key, + salt_enc: vec![ + 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, + 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, + 180, 39, 31, 155, 232, 119, 179, 0, 65, 9, 37, 184, 194, 135, 173, + 187, 197, 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, + 60, 109, 33, 112, 175 + ], + salt_auth: vec![ + 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, + 174 + ], }), - pin_protocol: Some(2), - .. - }) if salt_enc.len() == expected_enc_len && salt_auth.len() == 32, - "Failed with second: {second:?}, expected_enc_len: {expected_enc_len:?}" - ); - } - Ok(()) - } - - #[test] - fn calculate_prf_eval_by_cred_fallback_to_eval_pin_protocol_1( - ) -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; - let extension = - HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { - eval: Some(AuthenticationExtensionsPRFValues { - first: vec![0x01; 8], - second: Some(vec![0x02; 8]), - }), - eval_by_credential: Some( - [( - vec![1, 2, 3, 4], - AuthenticationExtensionsPRFValues { - first: vec![0x04; 8], - second: Some(vec![0x05; 8]), - }, - )] - .iter() - .cloned() - .collect(), - ), - }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; - let allow_list = [PublicKeyCredentialDescriptor { - id: vec![5, 6, 7, 8], - transports: vec![], - }]; - let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; - - assert_eq!(selected_cred, None); - assert_eq!( - extension, - HmacGetSecretOrPrf::Prf(HmacSecretExtension { - salt1: vec![ - 0x05, 0xf0, 0xb3, 0xb2, 0x3e, 0x7e, 0xcd, 0xac, 0xb0, 0x69, 0xd3, 0x0d, - 0x56, 0xd2, 0x30, 0xd2, 0xe1, 0xdb, 0xea, 0xf8, 0x10, 0xb6, 0x34, 0xdb, - 0x5c, 0x87, 0x61, 0x77, 0x6b, 0xf5, 0x1e, 0xe2 - ], - salt2: Some(vec![ - 0x60, 0x6b, 0x41, 0xea, 0x4d, 0xb0, 0xfb, 0x18, 0xc1, 0xbc, 0x62, 0x17, - 0x3b, 0xf0, 0xd4, 0x06, 0x68, 0xb0, 0x28, 0xf2, 0x68, 0xbe, 0x20, 0x7c, - 0xe2, 0xf4, 0x13, 0xa0, 0x08, 0x69, 0xfd, 0x6a - ]), - calculated_hmac: Some(CalculatedHmacSecretExtension { - public_key: client_key, - salt_enc: vec![ - 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, - 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, 180, - 39, 31, 155, 232, 119, 179, 0, 65, 9, 37, 184, 194, 135, 173, 187, 197, - 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, 60, 109, 33, - 112, 175 - ], - salt_auth: vec![ - 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, 174 - ], + pin_protocol: None, }), - pin_protocol: None, - }), - ); + ); - Ok(()) - } + Ok(()) + } - #[test] - fn calculate_prf_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; - let cred_id = PublicKeyCredentialDescriptor { - id: vec![1, 2, 3, 4], - transports: vec![], - }; - let extension = - HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { - eval: Some(AuthenticationExtensionsPRFValues { - first: vec![0x01; 8], - second: Some(vec![0x02; 8]), - }), - eval_by_credential: Some( - [ - ( - vec![9, 10, 11, 12], - AuthenticationExtensionsPRFValues { - first: vec![0x06; 8], - second: Some(vec![0x07; 8]), - }, - ), - ( - cred_id.id.clone(), + #[test] + fn calculate_prf_eval_by_cred_fallback_to_eval_pin_protocol_1( + ) -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![0x01; 8], + second: Some(vec![0x02; 8]), + }), + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], AuthenticationExtensionsPRFValues { first: vec![0x04; 8], second: Some(vec![0x05; 8]), }, - ), - ] - .iter() - .cloned() - .collect(), - ), - }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; - let allow_list = [ - PublicKeyCredentialDescriptor { + )] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [PublicKeyCredentialDescriptor { id: vec![5, 6, 7, 8], transports: vec![], - }, - cred_id, - PublicKeyCredentialDescriptor { - id: vec![9, 10, 11, 12], + }]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; + + assert_eq!(selected_cred, None); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1: vec![ + 0x05, 0xf0, 0xb3, 0xb2, 0x3e, 0x7e, 0xcd, 0xac, 0xb0, 0x69, 0xd3, 0x0d, + 0x56, 0xd2, 0x30, 0xd2, 0xe1, 0xdb, 0xea, 0xf8, 0x10, 0xb6, 0x34, 0xdb, + 0x5c, 0x87, 0x61, 0x77, 0x6b, 0xf5, 0x1e, 0xe2 + ], + salt2: Some(vec![ + 0x60, 0x6b, 0x41, 0xea, 0x4d, 0xb0, 0xfb, 0x18, 0xc1, 0xbc, 0x62, 0x17, + 0x3b, 0xf0, 0xd4, 0x06, 0x68, 0xb0, 0x28, 0xf2, 0x68, 0xbe, 0x20, 0x7c, + 0xe2, 0xf4, 0x13, 0xa0, 0x08, 0x69, 0xfd, 0x6a + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, + 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, + 180, 39, 31, 155, 232, 119, 179, 0, 65, 9, 37, 184, 194, 135, 173, + 187, 197, 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, + 60, 109, 33, 112, 175 + ], + salt_auth: vec![ + 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, + 174 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; + let cred_id = PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], transports: vec![], - }, - ]; - let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; - - assert_eq!(selected_cred, Some(&allow_list[1])); - assert_eq!( - extension, - HmacGetSecretOrPrf::Prf(HmacSecretExtension { - salt1: vec![ - 0x8d, 0x31, 0xd7, 0xf0, 0x6e, 0xc1, 0x54, 0x1b, 0x71, 0x99, 0x81, 0x6c, - 0x47, 0x3b, 0x62, 0x05, 0xd1, 0x2d, 0xbe, 0x8e, 0x2f, 0x04, 0x48, 0x4e, - 0xd9, 0x55, 0x63, 0xf3, 0xc0, 0xd9, 0xe8, 0x58 - ], - salt2: Some(vec![ - 0x9c, 0x58, 0x7f, 0x97, 0xcc, 0x5a, 0x91, 0xc8, 0xcf, 0xc9, 0x6a, 0x7c, - 0x13, 0x3c, 0x1d, 0x73, 0x91, 0xc5, 0x1b, 0x94, 0x75, 0x48, 0x12, 0x04, - 0x4e, 0xbb, 0xa1, 0x7a, 0x90, 0xf5, 0x43, 0x01 - ]), - calculated_hmac: Some(CalculatedHmacSecretExtension { - public_key: client_key, - salt_enc: vec![ - 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, 47, - 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, 209, - 25, 189, 32, 51, 75, 255, 176, 160, 82, 113, 250, 141, 83, 130, 69, - 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, 167, 210, - 241, 238, 237 + }; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![0x01; 8], + second: Some(vec![0x02; 8]), + }), + eval_by_credential: Some( + [ + ( + vec![9, 10, 11, 12], + AuthenticationExtensionsPRFValues { + first: vec![0x06; 8], + second: Some(vec![0x07; 8]), + }, + ), + ( + cred_id.id.clone(), + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + ), + ] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [ + PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }, + cred_id, + PublicKeyCredentialDescriptor { + id: vec![9, 10, 11, 12], + transports: vec![], + }, + ]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; + + assert_eq!(selected_cred, Some(&allow_list[1])); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1: vec![ + 0x8d, 0x31, 0xd7, 0xf0, 0x6e, 0xc1, 0x54, 0x1b, 0x71, 0x99, 0x81, 0x6c, + 0x47, 0x3b, 0x62, 0x05, 0xd1, 0x2d, 0xbe, 0x8e, 0x2f, 0x04, 0x48, 0x4e, + 0xd9, 0x55, 0x63, 0xf3, 0xc0, 0xd9, 0xe8, 0x58 ], - salt_auth: vec![ - 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, 232 + salt2: Some(vec![ + 0x9c, 0x58, 0x7f, 0x97, 0xcc, 0x5a, 0x91, 0xc8, 0xcf, 0xc9, 0x6a, 0x7c, + 0x13, 0x3c, 0x1d, 0x73, 0x91, 0xc5, 0x1b, 0x94, 0x75, 0x48, 0x12, 0x04, + 0x4e, 0xbb, 0xa1, 0x7a, 0x90, 0xf5, 0x43, 0x01 + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, + 47, 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, + 209, 25, 189, 32, 51, 75, 255, 176, 160, 82, 113, 250, 141, 83, + 130, 69, 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, + 167, 210, 241, 238, 237 + ], + salt_auth: vec![ + 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, + 232 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_only_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key) = make_test_secret(1)?; + let cred_id = PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [ + ( + vec![9, 10, 11, 12], + AuthenticationExtensionsPRFValues { + first: vec![0x06; 8], + second: Some(vec![0x07; 8]), + }, + ), + ( + cred_id.id.clone(), + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + ), + ] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [ + PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }, + cred_id, + PublicKeyCredentialDescriptor { + id: vec![9, 10, 11, 12], + transports: vec![], + }, + ]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; + + assert_eq!(selected_cred, Some(&allow_list[1])); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1: vec![ + 0x8d, 0x31, 0xd7, 0xf0, 0x6e, 0xc1, 0x54, 0x1b, 0x71, 0x99, 0x81, 0x6c, + 0x47, 0x3b, 0x62, 0x05, 0xd1, 0x2d, 0xbe, 0x8e, 0x2f, 0x04, 0x48, 0x4e, + 0xd9, 0x55, 0x63, 0xf3, 0xc0, 0xd9, 0xe8, 0x58 ], + salt2: Some(vec![ + 0x9c, 0x58, 0x7f, 0x97, 0xcc, 0x5a, 0x91, 0xc8, 0xcf, 0xc9, 0x6a, 0x7c, + 0x13, 0x3c, 0x1d, 0x73, 0x91, 0xc5, 0x1b, 0x94, 0x75, 0x48, 0x12, 0x04, + 0x4e, 0xbb, 0xa1, 0x7a, 0x90, 0xf5, 0x43, 0x01 + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, + 47, 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, + 209, 25, 189, 32, 51, 75, 255, 176, 160, 82, 113, 250, 141, 83, + 130, 69, 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, + 167, 210, 241, 238, 237 + ], + salt_auth: vec![ + 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, + 232 + ], + }), + pin_protocol: None, }), - pin_protocol: None, - }), - ); + ); - Ok(()) - } + Ok(()) + } - #[test] - fn calculate_prf_only_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; - let cred_id = PublicKeyCredentialDescriptor { - id: vec![1, 2, 3, 4], - transports: vec![], - }; - let extension = - HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { - eval: None, - eval_by_credential: Some( - [ - ( - vec![9, 10, 11, 12], - AuthenticationExtensionsPRFValues { - first: vec![0x06; 8], - second: Some(vec![0x07; 8]), - }, - ), - ( - cred_id.id.clone(), + #[test] + fn calculate_prf_unmatched_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, _) = make_test_secret(1)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], AuthenticationExtensionsPRFValues { first: vec![0x04; 8], second: Some(vec![0x05; 8]), }, - ), - ] - .iter() - .cloned() - .collect(), - ), - }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; - let allow_list = [ - PublicKeyCredentialDescriptor { + )] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [PublicKeyCredentialDescriptor { id: vec![5, 6, 7, 8], transports: vec![], - }, - cred_id, - PublicKeyCredentialDescriptor { - id: vec![9, 10, 11, 12], - transports: vec![], - }, - ]; - let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; - - assert_eq!(selected_cred, Some(&allow_list[1])); - assert_eq!( - extension, - HmacGetSecretOrPrf::Prf(HmacSecretExtension { - salt1: vec![ - 0x8d, 0x31, 0xd7, 0xf0, 0x6e, 0xc1, 0x54, 0x1b, 0x71, 0x99, 0x81, 0x6c, - 0x47, 0x3b, 0x62, 0x05, 0xd1, 0x2d, 0xbe, 0x8e, 0x2f, 0x04, 0x48, 0x4e, - 0xd9, 0x55, 0x63, 0xf3, 0xc0, 0xd9, 0xe8, 0x58 - ], - salt2: Some(vec![ - 0x9c, 0x58, 0x7f, 0x97, 0xcc, 0x5a, 0x91, 0xc8, 0xcf, 0xc9, 0x6a, 0x7c, - 0x13, 0x3c, 0x1d, 0x73, 0x91, 0xc5, 0x1b, 0x94, 0x75, 0x48, 0x12, 0x04, - 0x4e, 0xbb, 0xa1, 0x7a, 0x90, 0xf5, 0x43, 0x01 - ]), - calculated_hmac: Some(CalculatedHmacSecretExtension { - public_key: client_key, - salt_enc: vec![ - 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, 47, - 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, 209, - 25, 189, 32, 51, 75, 255, 176, 160, 82, 113, 250, 141, 83, 130, 69, - 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, 167, 210, - 241, 238, 237 - ], - salt_auth: vec![ - 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, 232 - ], - }), - pin_protocol: None, - }), - ); - - Ok(()) - } - - #[test] - fn calculate_prf_unmatched_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, _) = make_test_secret(1)?; - let extension = - HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { - eval: None, - eval_by_credential: Some( - [( - vec![1, 2, 3, 4], - AuthenticationExtensionsPRFValues { - first: vec![0x04; 8], - second: Some(vec![0x05; 8]), - }, - )] - .iter() - .cloned() - .collect(), - ), - }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; - let allow_list = [PublicKeyCredentialDescriptor { - id: vec![5, 6, 7, 8], - transports: vec![], - }]; - let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; + }]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; - assert_eq!(selected_cred, None); - assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched,); + assert_eq!(selected_cred, None); + assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched,); - Ok(()) - } + Ok(()) + } - #[test] - fn calculate_prf_unmatched_pin_protocol_2() -> Result<(), AuthenticatorError> { - let (shared_secret, _) = make_test_secret(2)?; - let extension = - HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { - eval: None, - eval_by_credential: Some( - [( - vec![1, 2, 3, 4], - AuthenticationExtensionsPRFValues { - first: vec![0x04; 8], - second: Some(vec![0x05; 8]), - }, - )] - .iter() - .cloned() - .collect(), - ), - }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; - let allow_list = [PublicKeyCredentialDescriptor { - id: vec![5, 6, 7, 8], - transports: vec![], - }]; - let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; + #[test] + fn calculate_prf_unmatched_pin_protocol_2() -> Result<(), AuthenticatorError> { + let (shared_secret, _) = make_test_secret(2)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + )] + .iter() + .cloned() + .collect(), + ), + }); + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + let allow_list = [PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(puat))?; - assert_eq!(selected_cred, None); - assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched); + assert_eq!(selected_cred, None); + assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched); - Ok(()) + Ok(()) + } } #[test] From 3c04ddabb23310fde06e704175822500a69d5eae Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 12:04:51 +0200 Subject: [PATCH 21/41] Add debug output to error paths of HmacSecretResponse::decrypt_secrets --- src/ctap2/attestation.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index 504c3367..9f371c43 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -43,7 +43,7 @@ impl HmacSecretResponse { hmac_outputs: &[u8], ) -> Result { let output_secrets = shared_secret.decrypt(hmac_outputs)?; - if output_secrets.len() < 32 { + match if output_secrets.len() < 32 { Err(CryptoError::WrongSaltLength) } else { let (output1, output2) = output_secrets.split_at(32); @@ -51,11 +51,20 @@ impl HmacSecretResponse { output1: output1 .try_into() .map_err(|_| CryptoError::WrongSaltLength)?, - output2: Some(output2) - .filter(|o2| !o2.is_empty()) - .map(|o2| o2.try_into().map_err(|_| CryptoError::WrongSaltLength)) + output2: (!output2.is_empty()) + .then(|| output2.try_into().map_err(|_| CryptoError::WrongSaltLength)) .transpose()?, }) + } { + err @ Err(CryptoError::WrongSaltLength) => { + // TODO: Use Result::inspect_err when stable + debug!( + "Bad hmac-secret output length: {} bytes (expected exactly 32 or 64)", + output_secrets.len() + ); + err + } + other => other, } } } From 0f440aac616f12526801274a2de76e7518d9acb1 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 12:05:36 +0200 Subject: [PATCH 22/41] Fix a typo and a cryptic comment --- src/ctap2/commands/get_assertion.rs | 2 +- src/ctap2/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index f60e3aef..3b6a5ad7 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -83,7 +83,7 @@ pub enum HmacGetSecretOrPrf { HmacGetSecret(HmacSecretExtension), /// hmac-secret input is to be calculated from PRF inputs, but we haven't yet identified which eval or evalByCredential entry to use. PrfUninitialized(AuthenticationExtensionsPRFInputs), - /// prf client input with no eval or matchin evalByCredential entry. + /// prf client input with no eval or matching evalByCredential entry. PrfUnmatched, /// hmac-secret inputs set by the prf client extension input. Prf(HmacSecretExtension), diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index 518f4842..9894ccfd 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -660,7 +660,7 @@ pub fn sign( return false; } - // Third, use the shared secret in the extensions, if requested + // Use the shared secret in the extensions, if requested get_assertion.extensions.hmac_secret = match get_assertion .extensions .hmac_secret From 2a5baf563a61c45720efa1b7783adc58fca501cc Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 10:00:52 +0200 Subject: [PATCH 23/41] Eliminate unnecessary sha256 function --- src/ctap2/server.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 4724bc81..860f9e27 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -51,12 +51,6 @@ pub struct RelyingParty { pub name: Option, } -fn sha256(data: impl AsRef<[u8]>) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(data); - hasher.finalize().into() -} - impl RelyingParty { pub fn from(id: S) -> Self where @@ -69,7 +63,7 @@ impl RelyingParty { } pub fn hash(&self) -> RpIdHash { - RpIdHash(sha256(&self.id)) + RpIdHash(Sha256::digest(&self.id).into()) } } @@ -466,14 +460,11 @@ impl AuthenticationExtensionsPRFInputs { /// Convert a PRF eval input to an hmac-secret salt input. fn eval_to_salt(eval: &[u8]) -> [u8; 32] { - sha256( - b"WebAuthn PRF" - .iter() - .chain([0x00].iter()) - .chain(eval.iter()) - .copied() - .collect::>(), - ) + Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update(eval.iter()) + .finalize() + .into() } } From 486a7e99f340ccf2cba7c351bed0d34230356958 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 10:05:31 +0200 Subject: [PATCH 24/41] Simplify to Sha256::digest where possible --- examples/ctap2_discoverable_creds.rs | 8 ++------ examples/test_exclude_list.rs | 4 +--- src/ctap2/commands/client_pin.rs | 9 +-------- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/examples/ctap2_discoverable_creds.rs b/examples/ctap2_discoverable_creds.rs index d19ccc6f..4667dcf5 100644 --- a/examples/ctap2_discoverable_creds.rs +++ b/examples/ctap2_discoverable_creds.rs @@ -74,9 +74,7 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: username, r#""}"# ); - let mut challenge = Sha256::new(); - challenge.update(challenge_str.as_bytes()); - let chall_bytes = challenge.finalize().into(); + let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { @@ -331,9 +329,7 @@ fn main() { } }); - let mut challenge = Sha256::new(); - challenge.update(challenge_str.as_bytes()); - let chall_bytes = challenge.finalize().into(); + let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); let ctap_args = SignArgs { client_data_hash: chall_bytes, origin, diff --git a/examples/test_exclude_list.rs b/examples/test_exclude_list.rs index e24c49d0..958d66b1 100644 --- a/examples/test_exclude_list.rs +++ b/examples/test_exclude_list.rs @@ -72,9 +72,7 @@ fn main() { r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#, r#" "version": "U2F_V2", "appId": "http://example.com"}"# ); - let mut challenge = Sha256::new(); - challenge.update(challenge_str.as_bytes()); - let chall_bytes = challenge.finalize().into(); + let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { diff --git a/src/ctap2/commands/client_pin.rs b/src/ctap2/commands/client_pin.rs index eb3b7136..d9a206c8 100644 --- a/src/ctap2/commands/client_pin.rs +++ b/src/ctap2/commands/client_pin.rs @@ -634,14 +634,7 @@ impl Pin { } pub fn for_pin_token(&self) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(self.0.as_bytes()); - - let mut output = [0u8; 16]; - let len = output.len(); - output.copy_from_slice(&hasher.finalize().as_slice()[..len]); - - output.to_vec() + Sha256::digest(self.as_bytes())[..16].into() } pub fn padded(&self) -> Vec { From 093957b831de339a0b971f91f6aa560353c0ab62 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 12:03:36 +0200 Subject: [PATCH 25/41] Derive PartialEq always, not just in cfg(test) --- src/crypto/mod.rs | 3 +-- src/ctap2/commands/get_assertion.rs | 9 +++------ src/ctap2/server.rs | 3 +-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 4600684c..aae0076d 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1072,8 +1072,7 @@ impl Serialize for COSEKey { } /// Errors that can be returned from COSE functions. -#[derive(Debug, Clone, Serialize)] -#[cfg_attr(test, derive(PartialEq))] +#[derive(Debug, Clone, PartialEq, Serialize)] pub enum CryptoError { // DecodingFailure, LibraryFailure, diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 3b6a5ad7..2212f79e 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -67,8 +67,7 @@ impl UserVerification for GetAssertionOptions { } } -#[derive(Debug, Clone)] -#[cfg_attr(test, derive(PartialEq))] +#[derive(Debug, Clone, PartialEq)] pub struct CalculatedHmacSecretExtension { pub public_key: COSEKey, pub salt_enc: Vec, @@ -76,8 +75,7 @@ pub struct CalculatedHmacSecretExtension { } /// Wrapper type recording whether the hmac-secret input originally came from the hmacGetSecret or the prf client extension input. -#[derive(Debug, Clone)] -#[cfg_attr(test, derive(PartialEq))] +#[derive(Debug, Clone, PartialEq)] pub enum HmacGetSecretOrPrf { /// hmac-secret inputs set by the hmacGetSecret client extension input. HmacGetSecret(HmacSecretExtension), @@ -155,8 +153,7 @@ impl Serialize for HmacGetSecretOrPrf { } } -#[derive(Debug, Clone, Default)] -#[cfg_attr(test, derive(PartialEq))] +#[derive(Debug, Clone, Default, PartialEq)] pub struct HmacSecretExtension { pub salt1: Vec, pub salt2: Option>, diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 860f9e27..8014500f 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -387,8 +387,7 @@ pub struct HMACGetSecretOutput { pub output2: Option<[u8; 32]>, } -#[derive(Clone, Debug, Default)] -#[cfg_attr(test, derive(PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct AuthenticationExtensionsPRFInputs { pub eval: Option, pub eval_by_credential: Option, AuthenticationExtensionsPRFValues>>, From 01cb0d351a310ae8edcf7e2c36481b0af979ad4f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 12:55:57 +0200 Subject: [PATCH 26/41] Document generation of hmac_secret test data --- src/ctap2/attestation.rs | 21 ++++++++ src/ctap2/commands/get_assertion.rs | 74 +++++++++++++++++++---------- 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index 9f371c43..8b796349 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1239,10 +1239,31 @@ pub mod test { }; const PIN_PROTOCOL_2_IV: [u8; 16] = [0; 16]; // PIN protocol 1 uses a hard-coded all-zero IV + + /// Generated using AES key 0..32 and ciphertext 0..64: + /// ``` + /// #!/usr/bin/env python3 + /// from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + /// + /// key = bytes(range(32)) + /// iv = bytes([0] * 16) + /// ciphertext = bytes(range(64)) + /// + /// cipher = Cipher(algorithms.AES256(key), modes.CBC(iv)) + /// decryptor = cipher.decryptor() + /// outputs = list(decryptor.update(ciphertext) + decryptor.finalize()) + /// EXPECTED_OUTPUT1 = outputs[0:32] + /// EXPECTED_OUTPUT2 = outputs[32:64] + /// print(EXPECTED_OUTPUT1) + /// print(EXPECTED_OUTPUT2) + /// ``` + /// Note: Using WebCrypto to generate these is impractical since they MUST NOT be padded, but WebCrypto inserts PKCS#7 padding. const EXPECTED_OUTPUT1: [u8; 32] = [ 145, 61, 188, 229, 73, 58, 253, 192, 87, 114, 133, 138, 173, 74, 68, 50, 105, 3, 44, 7, 205, 92, 54, 139, 137, 207, 7, 105, 89, 85, 211, 130, ]; + + /// See [EXPECTED_OUTPUT1] for generation instructions const EXPECTED_OUTPUT2: Option<[u8; 32]> = Some([ 155, 19, 88, 255, 192, 226, 50, 42, 243, 22, 42, 12, 146, 77, 108, 29, 71, 72, 149, 153, 183, 65, 182, 149, 71, 202, 57, 123, 239, 79, 94, 230, diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 2212f79e..cf19ae09 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -1785,18 +1785,23 @@ pub mod test { assert_eq!( extension, HmacGetSecretOrPrf::Prf(HmacSecretExtension { + // JS: salt1 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(1)]))) salt1: vec![ - 0x05, 0xf0, 0xb3, 0xb2, 0x3e, 0x7e, 0xcd, 0xac, 0xb0, 0x69, 0xd3, 0x0d, - 0x56, 0xd2, 0x30, 0xd2, 0xe1, 0xdb, 0xea, 0xf8, 0x10, 0xb6, 0x34, 0xdb, - 0x5c, 0x87, 0x61, 0x77, 0x6b, 0xf5, 0x1e, 0xe2 + 5, 240, 179, 178, 62, 126, 205, 172, 176, 105, 211, 13, 86, 210, 48, + 210, 225, 219, 234, 248, 16, 182, 52, 219, 92, 135, 97, 119, 107, 245, + 30, 226 ], + // JS: salt2 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(2)]))) salt2: Some(vec![ - 0x60, 0x6b, 0x41, 0xea, 0x4d, 0xb0, 0xfb, 0x18, 0xc1, 0xbc, 0x62, 0x17, - 0x3b, 0xf0, 0xd4, 0x06, 0x68, 0xb0, 0x28, 0xf2, 0x68, 0xbe, 0x20, 0x7c, - 0xe2, 0xf4, 0x13, 0xa0, 0x08, 0x69, 0xfd, 0x6a + 96, 107, 65, 234, 77, 176, 251, 24, 193, 188, 98, 23, 59, 240, 212, 6, + 104, 176, 40, 242, 104, 190, 32, 124, 226, 244, 19, 160, 8, 105, 253, + 106 ]), calculated_hmac: Some(CalculatedHmacSecretExtension { public_key: client_key, + // JS: aesKey = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "AES-CBC" }, false, ["encrypt"]) + // JS: salt_enc = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, aesKey, new Uint8Array([...salt1, ...salt2]))).slice(0, 64) + // (Need to strip trailing padding block inserted by WebCrypto) salt_enc: vec![ 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, @@ -1804,6 +1809,8 @@ pub mod test { 187, 197, 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, 60, 109, 33, 112, 175 ], + // JS: hmacKeyP1 = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + // JS: salt_auth = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKeyP1, salt_enc)).slice(0, 16) salt_auth: vec![ 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, 174 @@ -1854,18 +1861,23 @@ pub mod test { assert_eq!( extension, HmacGetSecretOrPrf::Prf(HmacSecretExtension { + // JS: salt1 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(1)]))) salt1: vec![ - 0x05, 0xf0, 0xb3, 0xb2, 0x3e, 0x7e, 0xcd, 0xac, 0xb0, 0x69, 0xd3, 0x0d, - 0x56, 0xd2, 0x30, 0xd2, 0xe1, 0xdb, 0xea, 0xf8, 0x10, 0xb6, 0x34, 0xdb, - 0x5c, 0x87, 0x61, 0x77, 0x6b, 0xf5, 0x1e, 0xe2 + 5, 240, 179, 178, 62, 126, 205, 172, 176, 105, 211, 13, 86, 210, 48, + 210, 225, 219, 234, 248, 16, 182, 52, 219, 92, 135, 97, 119, 107, 245, + 30, 226 ], + // JS: salt2 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(2)]))) salt2: Some(vec![ - 0x60, 0x6b, 0x41, 0xea, 0x4d, 0xb0, 0xfb, 0x18, 0xc1, 0xbc, 0x62, 0x17, - 0x3b, 0xf0, 0xd4, 0x06, 0x68, 0xb0, 0x28, 0xf2, 0x68, 0xbe, 0x20, 0x7c, - 0xe2, 0xf4, 0x13, 0xa0, 0x08, 0x69, 0xfd, 0x6a + 96, 107, 65, 234, 77, 176, 251, 24, 193, 188, 98, 23, 59, 240, 212, 6, + 104, 176, 40, 242, 104, 190, 32, 124, 226, 244, 19, 160, 8, 105, 253, + 106 ]), calculated_hmac: Some(CalculatedHmacSecretExtension { public_key: client_key, + // JS: aesKey = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "AES-CBC" }, false, ["encrypt"]) + // JS: salt_enc = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, aesKey, new Uint8Array([...salt1, ...salt2]))).slice(0, 64) + // (Need to strip trailing padding block inserted by WebCrypto) salt_enc: vec![ 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, @@ -1873,6 +1885,8 @@ pub mod test { 187, 197, 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, 60, 109, 33, 112, 175 ], + // JS: hmacKeyP1 = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + // JS: salt_auth = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKeyP1, salt_enc)).slice(0, 16) salt_auth: vec![ 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, 174 @@ -1942,18 +1956,22 @@ pub mod test { assert_eq!( extension, HmacGetSecretOrPrf::Prf(HmacSecretExtension { + // JS: salt1 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(4)]))) salt1: vec![ - 0x8d, 0x31, 0xd7, 0xf0, 0x6e, 0xc1, 0x54, 0x1b, 0x71, 0x99, 0x81, 0x6c, - 0x47, 0x3b, 0x62, 0x05, 0xd1, 0x2d, 0xbe, 0x8e, 0x2f, 0x04, 0x48, 0x4e, - 0xd9, 0x55, 0x63, 0xf3, 0xc0, 0xd9, 0xe8, 0x58 + 141, 49, 215, 240, 110, 193, 84, 27, 113, 153, 129, 108, 71, 59, 98, 5, + 209, 45, 190, 142, 47, 4, 72, 78, 217, 85, 99, 243, 192, 217, 232, 88 ], + // JS: salt2 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(5)]))) salt2: Some(vec![ - 0x9c, 0x58, 0x7f, 0x97, 0xcc, 0x5a, 0x91, 0xc8, 0xcf, 0xc9, 0x6a, 0x7c, - 0x13, 0x3c, 0x1d, 0x73, 0x91, 0xc5, 0x1b, 0x94, 0x75, 0x48, 0x12, 0x04, - 0x4e, 0xbb, 0xa1, 0x7a, 0x90, 0xf5, 0x43, 0x01 + 156, 88, 127, 151, 204, 90, 145, 200, 207, 201, 106, 124, 19, 60, 29, + 115, 145, 197, 27, 148, 117, 72, 18, 4, 78, 187, 161, 122, 144, 245, + 67, 1 ]), calculated_hmac: Some(CalculatedHmacSecretExtension { public_key: client_key, + // JS: aesKey = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "AES-CBC" }, false, ["encrypt"]) + // JS: salt_enc = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, aesKey, new Uint8Array([...salt1, ...salt2]))).slice(0, 64) + // (Need to strip trailing padding block inserted by WebCrypto) salt_enc: vec![ 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, 47, 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, @@ -1961,6 +1979,8 @@ pub mod test { 130, 69, 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, 167, 210, 241, 238, 237 ], + // JS: hmacKeyP1 = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + // JS: salt_auth = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKeyP1, salt_enc)).slice(0, 16) salt_auth: vec![ 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, 232 @@ -2027,18 +2047,22 @@ pub mod test { assert_eq!( extension, HmacGetSecretOrPrf::Prf(HmacSecretExtension { + // JS: salt1 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(4)]))) salt1: vec![ - 0x8d, 0x31, 0xd7, 0xf0, 0x6e, 0xc1, 0x54, 0x1b, 0x71, 0x99, 0x81, 0x6c, - 0x47, 0x3b, 0x62, 0x05, 0xd1, 0x2d, 0xbe, 0x8e, 0x2f, 0x04, 0x48, 0x4e, - 0xd9, 0x55, 0x63, 0xf3, 0xc0, 0xd9, 0xe8, 0x58 + 141, 49, 215, 240, 110, 193, 84, 27, 113, 153, 129, 108, 71, 59, 98, 5, + 209, 45, 190, 142, 47, 4, 72, 78, 217, 85, 99, 243, 192, 217, 232, 88 ], + // JS: salt2 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(5)]))) salt2: Some(vec![ - 0x9c, 0x58, 0x7f, 0x97, 0xcc, 0x5a, 0x91, 0xc8, 0xcf, 0xc9, 0x6a, 0x7c, - 0x13, 0x3c, 0x1d, 0x73, 0x91, 0xc5, 0x1b, 0x94, 0x75, 0x48, 0x12, 0x04, - 0x4e, 0xbb, 0xa1, 0x7a, 0x90, 0xf5, 0x43, 0x01 + 156, 88, 127, 151, 204, 90, 145, 200, 207, 201, 106, 124, 19, 60, 29, + 115, 145, 197, 27, 148, 117, 72, 18, 4, 78, 187, 161, 122, 144, 245, + 67, 1 ]), calculated_hmac: Some(CalculatedHmacSecretExtension { public_key: client_key, + // JS: aesKey = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "AES-CBC" }, false, ["encrypt"]) + // JS: salt_enc = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, aesKey, new Uint8Array([...salt1, ...salt2]))).slice(0, 64) + // (Need to strip trailing padding block inserted by WebCrypto) salt_enc: vec![ 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, 47, 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, @@ -2046,6 +2070,8 @@ pub mod test { 130, 69, 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, 167, 210, 241, 238, 237 ], + // JS: hmacKeyP1 = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + // JS: salt_auth = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKeyP1, salt_enc)).slice(0, 16) salt_auth: vec![ 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, 232 From beda0549d2d3151ebef8148ddab0567e31fcd65f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 14:48:01 +0200 Subject: [PATCH 27/41] Remove unnecessary comma --- src/ctap2/commands/get_assertion.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index cf19ae09..45366470 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -2115,7 +2115,7 @@ pub mod test { extension.calculate(&shared_secret, &allow_list, Some(puat))?; assert_eq!(selected_cred, None); - assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched,); + assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched); Ok(()) } From 5035146fe7a0c6ae4282fbae5c92bca2c0742879 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 14:51:19 +0200 Subject: [PATCH 28/41] Tweak imports per review --- src/ctap2/attestation.rs | 3 +-- src/ctap2/server.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index 8b796349..51b20353 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1,7 +1,6 @@ -use super::server::HMACGetSecretOutput; 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, RpIdHash}; +use crate::ctap2::server::{CredentialProtectionPolicy, HMACGetSecretOutput, RpIdHash}; use crate::ctap2::utils::serde_parse_err; use crate::{crypto::COSEKey, errors::AuthenticatorError}; use base64::Engine; diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 8014500f..98c3e529 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -1,3 +1,4 @@ +use super::commands::get_assertion::HmacSecretExtension; use crate::crypto::{COSEAlgorithm, PinUvAuthToken, SharedSecret}; use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle}; use base64::Engine; @@ -13,8 +14,6 @@ use std::collections::HashMap; use std::convert::{Into, TryFrom}; use std::fmt; -use super::commands::get_assertion::HmacSecretExtension; - #[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct RpIdHash(pub [u8; 32]); From 88b8db7618379124839af68c3ce9d344414e36bb Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 17:00:32 +0200 Subject: [PATCH 29/41] Take PinUvAuthToken as reference in HmacSecretExtension::calculate --- src/ctap2/commands/get_assertion.rs | 18 +++++++++--------- src/ctap2/mod.rs | 2 +- src/ctap2/server.rs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 45366470..391deab0 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -114,7 +114,7 @@ impl HmacGetSecretOrPrf { self, secret: &SharedSecret, allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], - puat: Option, + puat: Option<&PinUvAuthToken>, ) -> Result<(Self, Option<&'allow_cred PublicKeyCredentialDescriptor>), AuthenticatorError> { Ok(match self { @@ -177,7 +177,7 @@ impl HmacSecretExtension { pub fn calculate( &mut self, secret: &SharedSecret, - puat: Option, + puat: Option<&PinUvAuthToken>, ) -> Result<(), AuthenticatorError> { if self.salt1.len() < 32 { return Err(CryptoError::WrongSaltLength.into()); @@ -1734,7 +1734,7 @@ pub mod test { &shared_secret.encrypt(&[0x03; 32])?, )?; let (extension, selected_cred) = - extension.calculate(&shared_secret, &[], Some(puat))?; + extension.calculate(&shared_secret, &[], Some(&puat))?; assert_eq!(selected_cred, None); assert_eq!( @@ -1779,7 +1779,7 @@ pub mod test { &shared_secret.encrypt(&[0x03; 32])?, )?; let (extension, selected_cred) = - extension.calculate(&shared_secret, &[], Some(puat))?; + extension.calculate(&shared_secret, &[], Some(&puat))?; assert_eq!(selected_cred, None); assert_eq!( @@ -1855,7 +1855,7 @@ pub mod test { transports: vec![], }]; let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; assert_eq!(selected_cred, None); assert_eq!( @@ -1950,7 +1950,7 @@ pub mod test { }, ]; let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; assert_eq!(selected_cred, Some(&allow_list[1])); assert_eq!( @@ -2041,7 +2041,7 @@ pub mod test { }, ]; let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; assert_eq!(selected_cred, Some(&allow_list[1])); assert_eq!( @@ -2112,7 +2112,7 @@ pub mod test { transports: vec![], }]; let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; assert_eq!(selected_cred, None); assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched); @@ -2148,7 +2148,7 @@ pub mod test { transports: vec![], }]; let (extension, selected_cred) = - extension.calculate(&shared_secret, &allow_list, Some(puat))?; + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; assert_eq!(selected_cred, None); assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched); diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index 9894ccfd..321d1d4f 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -670,7 +670,7 @@ pub fn sign( let (extension, selected_credential) = hmac_get_secret_or_prf.calculate( secret, &get_assertion.allow_list, - pin_uv_auth_result.get_pin_uv_auth_token(), + pin_uv_auth_result.get_pin_uv_auth_token().as_ref(), )?; if let Some(selected_credential) = selected_credential { get_assertion.allow_list = vec![selected_credential.clone()]; diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 98c3e529..e942bc85 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -403,7 +403,7 @@ impl AuthenticationExtensionsPRFInputs { &self, secret: &SharedSecret, allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], - puat: Option, + puat: Option<&PinUvAuthToken>, ) -> Result< Option<( HmacSecretExtension, From 80d0f35ad049669bb947c476008e77c6fc5cf6f9 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 16:49:12 +0200 Subject: [PATCH 30/41] Deduplicate decrypt_pin_token code in tests --- src/ctap2/commands/get_assertion.rs | 71 ++++++++++++----------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 391deab0..7f9b2a8b 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -1664,7 +1664,9 @@ pub mod test { AuthenticatorInfo, }; - fn make_test_secret(pin_protocol: u64) -> Result<(SharedSecret, COSEKey), CommandError> { + fn make_test_secret_without_puat( + pin_protocol: u64, + ) -> Result<(SharedSecret, COSEKey), CommandError> { let fake_client_key = COSEKey { alg: COSEAlgorithm::ECDH_ES_HKDF256, key: COSEKeyType::EC2(COSEEC2Key { @@ -1697,16 +1699,17 @@ pub mod test { } }; - Ok(( - SharedSecret::new_test(pin_protocol, key, fake_client_key.clone(), fake_peer_key), - fake_client_key, - )) + let shared_secret = + SharedSecret::new_test(pin_protocol, key, fake_client_key.clone(), fake_peer_key); + + Ok((shared_secret, fake_client_key)) } #[cfg(not(feature = "crypto_dummy"))] mod requires_crypto { use super::*; use crate::{ + crypto::PinUvAuthToken, ctap2::{ commands::{ client_pin::PinUvAuthTokenPermission, @@ -1722,17 +1725,25 @@ pub mod test { errors::AuthenticatorError, }; + fn make_test_secret( + pin_protocol: u64, + ) -> Result<(SharedSecret, COSEKey, PinUvAuthToken), CommandError> { + let (shared_secret, fake_client_key) = make_test_secret_without_puat(pin_protocol)?; + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + + Ok((shared_secret, fake_client_key, puat)) + } + #[test] fn calculate_hmac_get_secret_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; + let (shared_secret, client_key, puat) = make_test_secret(1)?; let extension = HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( vec![0x01; 32], Some(vec![0x02; 32]), )); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; let (extension, selected_cred) = extension.calculate(&shared_secret, &[], Some(&puat))?; @@ -1765,7 +1776,7 @@ pub mod test { #[test] fn calculate_prf_eval_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; + let (shared_secret, client_key, puat) = make_test_secret(1)?; let extension = HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { eval: Some(AuthenticationExtensionsPRFValues { @@ -1774,10 +1785,6 @@ pub mod test { }), eval_by_credential: None, }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; let (extension, selected_cred) = extension.calculate(&shared_secret, &[], Some(&puat))?; @@ -1826,7 +1833,7 @@ pub mod test { #[test] fn calculate_prf_eval_by_cred_fallback_to_eval_pin_protocol_1( ) -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; + let (shared_secret, client_key, puat) = make_test_secret(1)?; let extension = HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { eval: Some(AuthenticationExtensionsPRFValues { @@ -1846,10 +1853,6 @@ pub mod test { .collect(), ), }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; let allow_list = [PublicKeyCredentialDescriptor { id: vec![5, 6, 7, 8], transports: vec![], @@ -1901,7 +1904,7 @@ pub mod test { #[test] fn calculate_prf_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; + let (shared_secret, client_key, puat) = make_test_secret(1)?; let cred_id = PublicKeyCredentialDescriptor { id: vec![1, 2, 3, 4], transports: vec![], @@ -1934,10 +1937,6 @@ pub mod test { .collect(), ), }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; let allow_list = [ PublicKeyCredentialDescriptor { id: vec![5, 6, 7, 8], @@ -1995,7 +1994,7 @@ pub mod test { #[test] fn calculate_prf_only_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, client_key) = make_test_secret(1)?; + let (shared_secret, client_key, puat) = make_test_secret(1)?; let cred_id = PublicKeyCredentialDescriptor { id: vec![1, 2, 3, 4], transports: vec![], @@ -2025,10 +2024,6 @@ pub mod test { .collect(), ), }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; let allow_list = [ PublicKeyCredentialDescriptor { id: vec![5, 6, 7, 8], @@ -2086,7 +2081,7 @@ pub mod test { #[test] fn calculate_prf_unmatched_pin_protocol_1() -> Result<(), AuthenticatorError> { - let (shared_secret, _) = make_test_secret(1)?; + let (shared_secret, _, puat) = make_test_secret(1)?; let extension = HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { eval: None, @@ -2103,10 +2098,6 @@ pub mod test { .collect(), ), }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; let allow_list = [PublicKeyCredentialDescriptor { id: vec![5, 6, 7, 8], transports: vec![], @@ -2122,7 +2113,7 @@ pub mod test { #[test] fn calculate_prf_unmatched_pin_protocol_2() -> Result<(), AuthenticatorError> { - let (shared_secret, _) = make_test_secret(2)?; + let (shared_secret, _, puat) = make_test_secret(2)?; let extension = HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { eval: None, @@ -2139,10 +2130,6 @@ pub mod test { .collect(), ), }); - let puat = shared_secret.decrypt_pin_token( - PinUvAuthTokenPermission::empty(), - &shared_secret.encrypt(&[0x03; 32])?, - )?; let allow_list = [PublicKeyCredentialDescriptor { id: vec![5, 6, 7, 8], transports: vec![], @@ -2162,7 +2149,7 @@ pub mod test { expected = "unreachable code: hmac-secret inputs from PRF already initialized" )] fn calculate_prf_conflict_1() { - let (shared_secret, _) = make_test_secret(2).unwrap(); + let (shared_secret, _) = make_test_secret_without_puat(2).unwrap(); let extension = HmacGetSecretOrPrf::PrfUnmatched; extension.calculate(&shared_secret, &[], None).unwrap(); } @@ -2172,7 +2159,7 @@ pub mod test { expected = "unreachable code: hmac-secret inputs from PRF already initialized" )] fn calculate_prf_conflict_2() { - let (shared_secret, client_key) = make_test_secret(2).unwrap(); + let (shared_secret, client_key) = make_test_secret_without_puat(2).unwrap(); let extension = HmacGetSecretOrPrf::Prf(HmacSecretExtension { salt1: vec![], salt2: Some(vec![]), From eede8c7c2617021cd1636c28a85a701f1b31e206 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 Jul 2024 15:24:48 +0200 Subject: [PATCH 31/41] Extract function GetAssertion::process_hmac_secret_and_prf_extension --- src/ctap2/commands/get_assertion.rs | 31 +++++++++++++++++++++++++++-- src/ctap2/mod.rs | 26 ++++-------------------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 7f9b2a8b..d8de903d 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -1,7 +1,7 @@ use super::get_info::AuthenticatorInfo; use super::{ - Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable, - StatusCode, + Command, CommandError, CtapResponse, PinUvAuthCommand, PinUvAuthResult, RequestCtap1, + RequestCtap2, Retryable, StatusCode, }; use crate::consts::{ PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN, @@ -304,6 +304,33 @@ impl GetAssertion { } } + pub fn process_hmac_secret_and_prf_extension( + mut self, + shared_secret: Option<(&SharedSecret, &PinUvAuthResult)>, + ) -> Result { + self.extensions.hmac_secret = self + .extensions + .hmac_secret + .take() + .map(|hmac_get_secret_or_prf| { + if let Some((secret, pin_uv_auth_result)) = shared_secret { + let (extension, selected_credential) = hmac_get_secret_or_prf.calculate( + secret, + &self.allow_list, + pin_uv_auth_result.get_pin_uv_auth_token().as_ref(), + )?; + if let Some(selected_credential) = selected_credential { + self.allow_list = vec![selected_credential.clone()]; + } + Ok::<_, AuthenticatorError>(extension) + } else { + Ok(hmac_get_secret_or_prf) + } + }) + .transpose()?; + Ok(self) + } + pub fn finalize_result(&self, dev: &Dev, result: &mut GetAssertionResult) { result.attachment = match dev.get_authenticator_info() { Some(info) if info.options.platform_device => AuthenticatorAttachment::Platform, diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index 321d1d4f..6ec768b4 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -661,28 +661,10 @@ pub fn sign( } // Use the shared secret in the extensions, if requested - get_assertion.extensions.hmac_secret = match get_assertion - .extensions - .hmac_secret - .take() - .map(|hmac_get_secret_or_prf| { - if let Some(secret) = dev.get_shared_secret() { - let (extension, selected_credential) = hmac_get_secret_or_prf.calculate( - secret, - &get_assertion.allow_list, - pin_uv_auth_result.get_pin_uv_auth_token().as_ref(), - )?; - if let Some(selected_credential) = selected_credential { - get_assertion.allow_list = vec![selected_credential.clone()]; - } - Ok(extension) - } else { - Ok(hmac_get_secret_or_prf) - } - }) - .transpose() - { - Ok(extension) => extension, + get_assertion = match get_assertion.process_hmac_secret_and_prf_extension( + dev.get_shared_secret().map(|s| (s, &pin_uv_auth_result)), + ) { + Ok(value) => value, Err(e) => { callback.call(Err(e)); return false; From fd8e062a73acdd44098831350b732121c49a1c59 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 Jul 2024 17:53:38 +0200 Subject: [PATCH 32/41] Move allow_list assignment to top level scope --- src/ctap2/commands/get_assertion.rs | 30 ++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index d8de903d..62829f9e 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -308,26 +308,30 @@ impl GetAssertion { mut self, shared_secret: Option<(&SharedSecret, &PinUvAuthResult)>, ) -> Result { - self.extensions.hmac_secret = self + let (new_hmac_secret, new_allow_list) = self .extensions .hmac_secret .take() - .map(|hmac_get_secret_or_prf| { - if let Some((secret, pin_uv_auth_result)) = shared_secret { - let (extension, selected_credential) = hmac_get_secret_or_prf.calculate( + .and_then(|hmac_get_secret_or_prf| { + shared_secret.map(|(secret, pin_uv_auth_result)| { + hmac_get_secret_or_prf.calculate( secret, &self.allow_list, pin_uv_auth_result.get_pin_uv_auth_token().as_ref(), - )?; - if let Some(selected_credential) = selected_credential { - self.allow_list = vec![selected_credential.clone()]; - } - Ok::<_, AuthenticatorError>(extension) - } else { - Ok(hmac_get_secret_or_prf) - } + ) + }) }) - .transpose()?; + .transpose()? + .map(|(nhs, nal)| (Some(nhs), nal)) + .unwrap_or((None, None)); + + (self.extensions.hmac_secret, self.allow_list) = ( + new_hmac_secret, + new_allow_list + .map(|selected_credential| vec![selected_credential.clone()]) + .unwrap_or(self.allow_list), + ); + Ok(self) } From 06e74c4b752e3297fbc52159f0cb577998eca8f1 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 Jul 2024 17:07:13 +0200 Subject: [PATCH 33/41] Add tests of hmac-secret and prf processing in GetAssertion::finalize_result --- src/ctap2/commands/get_assertion.rs | 221 +++++++++++++++++++++++++++- src/transport/mock/device.rs | 9 +- 2 files changed, 222 insertions(+), 8 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 62829f9e..3317e5f1 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -1686,13 +1686,25 @@ pub mod test { COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, PinUvAuthProtocol, SharedSecret, }, - ctap2::commands::{ - get_assertion::{ - CalculatedHmacSecretExtension, HmacGetSecretOrPrf, HmacSecretExtension, + ctap2::{ + attestation::{ + AuthenticatorData, AuthenticatorDataFlags, Extension, HmacSecretResponse, + }, + client_data::ClientDataHash, + commands::{ + get_assertion::{ + CalculatedHmacSecretExtension, GetAssertion, GetAssertionExtensions, + HmacGetSecretOrPrf, HmacSecretExtension, + }, + CommandError, + }, + server::{ + AuthenticationExtensionsClientOutputs, AuthenticationExtensionsPRFOutputs, + AuthenticatorAttachment, RelyingParty, RpIdHash, }, - CommandError, }, - AuthenticatorInfo, + transport::platform::device::Device, + Assertion, AuthenticatorInfo, FidoDevice, GetAssertionResult, }; fn make_test_secret_without_puat( @@ -2173,6 +2185,49 @@ pub mod test { Ok(()) } + + #[test] + fn finalize_result_hmac_get_secret_input_with_secret_output_becomes_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![], + None, + ))), + Some(HmacSecretResponse::Secret(vec![0x01; ONE_OUTPUT_LEN_PP2])), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: Some(_), + prf: None, + .. + } + ); + } + + #[test] + fn finalize_result_prf_input_with_secret_output_becomes_results_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension::new( + vec![], + None, + ))), + Some(HmacSecretResponse::Secret(vec![0x01; ONE_OUTPUT_LEN_PP2])), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: Some(_), + }), + .. + } + ); + } } #[test] @@ -2203,5 +2258,161 @@ pub mod test { }); extension.calculate(&shared_secret, &[], None).unwrap(); } + + fn finalize_result_with_hmac_secret_input_and_output( + hmac_secret_input: Option, + hmac_secret_response: Option, + ) -> Result { + let get_assertion = GetAssertion::new( + ClientDataHash([0x01; 32]), + RelyingParty::from("example.com"), + vec![], + Default::default(), + GetAssertionExtensions { + hmac_secret: hmac_secret_input, + ..Default::default() + }, + ); + let mut result = GetAssertionResult { + assertion: Assertion { + credentials: None, + auth_data: AuthenticatorData { + rp_id_hash: RpIdHash([0x01; 32]), + flags: AuthenticatorDataFlags::empty(), + counter: 0, + credential_data: None, + extensions: Extension { + cred_protect: None, + hmac_secret: hmac_secret_response, + min_pin_length: None, + }, + }, + signature: vec![], + user: None, + }, + attachment: AuthenticatorAttachment::Unknown, + extensions: AuthenticationExtensionsClientOutputs::default(), + }; + + let mut dev = Device::new_skipping_serialization("commands/get_assertion") + .expect("Failed to create mock Device"); + let (shared_secret, _) = make_test_secret_without_puat(2)?; + dev.set_shared_secret(shared_secret); + get_assertion.finalize_result(&dev, &mut result); + Ok(result) + } + + /// Encrypted salt output for pin protocol 2: iv || ct + const ONE_OUTPUT_LEN_PP2: usize = 16 + 32; + + #[test] + fn finalize_result_no_input_with_no_output_becomes_no_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output(None, None) + .expect("Failed to run test"); + assert_matches!(result.extensions.hmac_get_secret, None); + } + + #[test] + fn finalize_result_no_input_with_secret_output_becomes_no_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + None, + Some(HmacSecretResponse::Secret(vec![0x01; ONE_OUTPUT_LEN_PP2])), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: None, + .. + } + ); + } + + #[test] + fn finalize_result_hmac_get_secret_input_with_no_output_becomes_no_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![], + None, + ))), + None, + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: None, + .. + } + ); + } + + #[test] + fn finalize_result_hmac_get_secret_input_with_confirmed_output_becomes_no_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![], + None, + ))), + Some(HmacSecretResponse::Confirmed(true)), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: None, + .. + } + ); + } + + #[test] + fn finalize_result_prf_input_with_no_output_becomes_empty_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension::new( + vec![], + None, + ))), + None, + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: None, + }), + .. + } + ); + } + + #[test] + fn finalize_result_prf_input_with_confirmed_output_becomes_empty_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension::new( + vec![], + None, + ))), + Some(HmacSecretResponse::Confirmed(true)), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: None, + }), + .. + } + ); + } } } diff --git a/src/transport/mock/device.rs b/src/transport/mock/device.rs index ac4e156d..211da465 100644 --- a/src/transport/mock/device.rs +++ b/src/transport/mock/device.rs @@ -32,6 +32,7 @@ pub struct Device { skip_serialization: bool, pub upcoming_requests: VecDeque>, pub upcoming_responses: VecDeque, HIDError>>, + pub shared_secret: Option, } impl Device { @@ -91,6 +92,7 @@ impl Device { skip_serialization: true, upcoming_requests: VecDeque::new(), upcoming_responses: VecDeque::new(), + shared_secret: None, }) } } @@ -171,6 +173,7 @@ impl HIDDevice for Device { skip_serialization: false, upcoming_requests: VecDeque::new(), upcoming_responses: VecDeque::new(), + shared_secret: None, }) } @@ -303,11 +306,11 @@ impl FidoDevice for Device { } fn get_shared_secret(&self) -> std::option::Option<&SharedSecret> { - None + self.shared_secret.as_ref() } - fn set_shared_secret(&mut self, _: SharedSecret) { - // Nothing + fn set_shared_secret(&mut self, shared_secret: SharedSecret) { + self.shared_secret = Some(shared_secret); } fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { From 5cebc5ba24b23886ca3c7aee3bdf77dddf889abb Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 8 Jul 2024 16:38:18 +0200 Subject: [PATCH 34/41] Fail hmac-secret salt calculation if input salts are too long This is prescribed by the [CTAP spec][ctap]: >**Client extension processing** >1. [...] >2. If present in a get(): > 1. Verify that salt1 is a 32-byte ArrayBuffer. > 2. If salt2 is present, verify that it is a 32-byte ArrayBuffer. > [...] [ctap]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-hmac-secret-extension --- src/ctap2/commands/get_assertion.rs | 67 ++++++++++++++++++++++------- src/ctap2/server.rs | 4 +- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 3317e5f1..9c5ed526 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -30,6 +30,7 @@ use serde::{ }; use serde_bytes::ByteBuf; use serde_cbor::{de::from_slice, ser, Value}; +use std::convert::TryFrom; use std::fmt; use std::io::Cursor; @@ -115,8 +116,7 @@ impl HmacGetSecretOrPrf { secret: &SharedSecret, allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], puat: Option<&PinUvAuthToken>, - ) -> Result<(Self, Option<&'allow_cred PublicKeyCredentialDescriptor>), AuthenticatorError> - { + ) -> Result<(Self, Option<&'allow_cred PublicKeyCredentialDescriptor>), CryptoError> { Ok(match self { Self::HmacGetSecret(mut extension) => { extension.calculate(secret, puat)?; @@ -178,19 +178,18 @@ impl HmacSecretExtension { &mut self, secret: &SharedSecret, puat: Option<&PinUvAuthToken>, - ) -> Result<(), AuthenticatorError> { - if self.salt1.len() < 32 { - return Err(CryptoError::WrongSaltLength.into()); - } - let salt_enc = match &self.salt2 { - Some(salt2) => { - if salt2.len() < 32 { - return Err(CryptoError::WrongSaltLength.into()); - } - let salts = [&self.salt1[..32], &salt2[..32]].concat(); // salt1 || salt2 - secret.encrypt(&salts) + ) -> Result<(), CryptoError> { + let salt_enc = match ( + <[u8; 32]>::try_from(self.salt1.as_slice()), + self.salt2.as_deref().map(<[u8; 32]>::try_from), + ) { + (Ok(salt1), None) => secret.encrypt(&salt1), + (Ok(salt1), Some(Ok(salt2))) => secret.encrypt(&[salt1, salt2].concat()), + (Err(_), _) | (_, Some(Err(_))) => { + debug!("Invalid hmac-secret salt length(s): salt1: {}, salt2: {:?} (expected 32 and 32|None)", + self.salt1.len(), self.salt2.as_ref().map(Vec::len)); + Err(CryptoError::WrongSaltLength) } - None => secret.encrypt(&self.salt1[..32]), }?; let salt_auth = secret.authenticate(&salt_enc)?; let public_key = secret.client_input().clone(); @@ -1752,7 +1751,7 @@ pub mod test { mod requires_crypto { use super::*; use crate::{ - crypto::PinUvAuthToken, + crypto::{CryptoError, PinUvAuthToken}, ctap2::{ commands::{ client_pin::PinUvAuthTokenPermission, @@ -1817,6 +1816,44 @@ pub mod test { Ok(()) } + #[test] + fn calculate_hmac_get_secret_wrong_length_salt1() -> Result<(), AuthenticatorError> { + let (shared_secret, _, puat) = make_test_secret(1)?; + for len in [0, 1, 31, 33, 64] { + let extension = HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; len], + None, + )); + let result = extension.calculate(&shared_secret, &[], Some(&puat)); + assert_eq!( + result, + Err(CryptoError::WrongSaltLength), + "At salt1 length: {}", + len, + ); + } + Ok(()) + } + + #[test] + fn calculate_hmac_get_secret_wrong_length_salt2() -> Result<(), AuthenticatorError> { + let (shared_secret, _, puat) = make_test_secret(1)?; + for len in [0, 1, 31, 33, 64] { + let extension = HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; 32], + Some(vec![0x02; len]), + )); + let result = extension.calculate(&shared_secret, &[], Some(&puat)); + assert_eq!( + result, + Err(CryptoError::WrongSaltLength), + "At salt2 length: {}", + len, + ); + } + Ok(()) + } + #[test] fn calculate_prf_eval_pin_protocol_1() -> Result<(), AuthenticatorError> { let (shared_secret, client_key, puat) = make_test_secret(1)?; diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index e942bc85..676d5d0c 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -1,5 +1,5 @@ use super::commands::get_assertion::HmacSecretExtension; -use crate::crypto::{COSEAlgorithm, PinUvAuthToken, SharedSecret}; +use crate::crypto::{COSEAlgorithm, CryptoError, PinUvAuthToken, SharedSecret}; use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle}; use base64::Engine; use serde::de::MapAccess; @@ -409,7 +409,7 @@ impl AuthenticationExtensionsPRFInputs { HmacSecretExtension, Option<&'allow_cred PublicKeyCredentialDescriptor>, )>, - AuthenticatorError, + CryptoError, > { if let Some((selected_credential, ev)) = self.select_eval(allow_credentials) { let mut hmac_secret = HmacSecretExtension::new( From b959eeb19b18e7930658bf2c06ac2e8eb2e84de1 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 Jul 2024 15:52:05 +0200 Subject: [PATCH 35/41] Add tests of GetAssertion::process_hmac_secret_and_prf_extension --- src/ctap2/commands/get_assertion.rs | 257 +++++++++++++++++++++++++++- 1 file changed, 255 insertions(+), 2 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 9c5ed526..b15f1c78 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -1749,19 +1749,24 @@ pub mod test { #[cfg(not(feature = "crypto_dummy"))] mod requires_crypto { + use sha2::{Digest, Sha256}; + use super::*; use crate::{ crypto::{CryptoError, PinUvAuthToken}, ctap2::{ + client_data::ClientDataHash, commands::{ client_pin::PinUvAuthTokenPermission, get_assertion::{ - CalculatedHmacSecretExtension, HmacGetSecretOrPrf, HmacSecretExtension, + CalculatedHmacSecretExtension, GetAssertion, GetAssertionExtensions, + HmacGetSecretOrPrf, HmacSecretExtension, }, + PinUvAuthResult, }, server::{ AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFValues, - PublicKeyCredentialDescriptor, + PublicKeyCredentialDescriptor, RelyingParty, }, }, errors::AuthenticatorError, @@ -1779,6 +1784,254 @@ pub mod test { Ok((shared_secret, fake_client_key, puat)) } + fn get_assertion_process_hmac_secret( + secret_available: bool, + allow_list: Vec, + hmac_secret: Option, + ) -> Result { + let (shared_secret, _, puat) = make_test_secret(1)?; + GetAssertion::new( + ClientDataHash([0x01; 32]), + RelyingParty::from("example.com"), + allow_list, + Default::default(), + GetAssertionExtensions { + hmac_secret, + ..Default::default() + }, + ) + .process_hmac_secret_and_prf_extension( + secret_available + .then_some((&shared_secret, &PinUvAuthResult::SuccessGetPinToken(puat))), + ) + } + + #[test] + fn get_assertion_hmac_secret_and_prf_absent_uses_no_input() { + let get_assertion = get_assertion_process_hmac_secret(true, vec![], None).unwrap(); + assert_matches!(get_assertion.extensions.hmac_secret, None); + } + + #[test] + fn get_assertion_prf_no_input_uses_unmatched_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: None, + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::PrfUnmatched) + ); + } + + #[test] + fn get_assertion_hmac_get_secret_uses_hmac_get_secret_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![], + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; 32], + None, + ))), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension { + calculated_hmac: Some(_), + .. + })) + ); + } + + #[test] + fn get_assertion_prf_eval_uses_eval_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![1, 2, 3, 4], + second: None, + }), + eval_by_credential: None, + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1, + .. + })) if salt1 == Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update([1, 2, 3, 4].iter()) + .finalize() + .to_vec() + ); + } + + #[test] + fn get_assertion_prf_eval_by_credential_unmatched_uses_unmatched_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![5, 6, 7, 8], + AuthenticationExtensionsPRFValues { + first: vec![9, 10, 11, 12], + second: None, + }, + )] + .into(), + ), + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::PrfUnmatched) + ); + } + + #[test] + fn get_assertion_prf_eval_by_credential_matched_uses_eval_by_credential_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![9, 10, 11, 12], + second: None, + }, + )] + .into(), + ), + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1, + .. + })) if salt1 == Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update([9, 10, 11, 12].iter()) + .finalize() + .to_vec() + ); + } + + #[test] + fn get_assertion_prf_eval_and_eval_by_credential_unmatched_uses_eval_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![13, 14, 15, 16], + second: None, + }), + eval_by_credential: Some( + [( + vec![5, 6, 7, 8], + AuthenticationExtensionsPRFValues { + first: vec![9, 10, 11, 12], + second: None, + }, + )] + .into(), + ), + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1, + .. + })) if salt1 == Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update([13, 14, 15, 16].iter()) + .finalize() + .to_vec() + ); + } + + #[test] + fn get_assertion_prf_eval_and_eval_by_credential_matched_uses_eval_by_credential_input() + { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![13, 14, 15, 16], + second: None, + }), + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![9, 10, 11, 12], + second: None, + }, + )] + .into(), + ), + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1, + .. + })) if salt1 == Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update([9, 10, 11, 12].iter()) + .finalize() + .to_vec() + ); + } + #[test] fn calculate_hmac_get_secret_pin_protocol_1() -> Result<(), AuthenticatorError> { let (shared_secret, client_key, puat) = make_test_secret(1)?; From 9036264bd1b9dd52347197b67fecce2a5b65858a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 Jul 2024 18:11:01 +0200 Subject: [PATCH 36/41] Propagate WrongSaltLength as InvalidRelyingPartyInput in GetAssertion::process_hmac_secret_and_prf_extension --- src/ctap2/commands/get_assertion.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index b15f1c78..be977909 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -320,7 +320,11 @@ impl GetAssertion { ) }) }) - .transpose()? + .transpose() + .map_err(|err| match err { + CryptoError::WrongSaltLength => AuthenticatorError::InvalidRelyingPartyInput, + e => e.into(), + })? .map(|(nhs, nal)| (Some(nhs), nal)) .unwrap_or((None, None)); @@ -1851,6 +1855,22 @@ pub mod test { ); } + #[test] + fn get_assertion_hmac_get_secret_bad_length_returns_invalid_input_error() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![], + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; 31], + None, + ))), + ); + assert_matches!( + get_assertion, + Err(AuthenticatorError::InvalidRelyingPartyInput) + ); + } + #[test] fn get_assertion_prf_eval_uses_eval_input() { let get_assertion = get_assertion_process_hmac_secret( From 978725b534161a9cba107dc0c1bbcadbfc971370 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 Jul 2024 18:15:18 +0200 Subject: [PATCH 37/41] Return PrfUnmatched instead of None when shared secret is not available This is needed because the PRF extension should return an empty extension output `prf: {}` when the extension is processed but no eligible authenticator is found. Thus we need to differentiate these cases so that `GetAssertion::finalize_result` can match on `PrfUnmatched` and generate the empty output. --- src/ctap2/commands/get_assertion.rs | 39 ++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index be977909..4886b2c3 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -312,13 +312,22 @@ impl GetAssertion { .hmac_secret .take() .and_then(|hmac_get_secret_or_prf| { - shared_secret.map(|(secret, pin_uv_auth_result)| { - hmac_get_secret_or_prf.calculate( + if let Some((secret, pin_uv_auth_result)) = shared_secret { + Some(hmac_get_secret_or_prf.calculate( secret, &self.allow_list, pin_uv_auth_result.get_pin_uv_auth_token().as_ref(), - ) - }) + )) + } else { + match hmac_get_secret_or_prf { + HmacGetSecretOrPrf::HmacGetSecret(_) => None, + HmacGetSecretOrPrf::PrfUninitialized(_) + | HmacGetSecretOrPrf::PrfUnmatched + | HmacGetSecretOrPrf::Prf(_) => { + Some(Ok((HmacGetSecretOrPrf::PrfUnmatched, None))) + } + } + } }) .transpose() .map_err(|err| match err { @@ -1835,6 +1844,28 @@ pub mod test { ); } + #[test] + fn get_assertion_prf_no_secret_uses_unmatched_input() { + let get_assertion = get_assertion_process_hmac_secret( + false, + vec![], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![1, 2, 3, 4], + second: None, + }), + eval_by_credential: None, + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::PrfUnmatched) + ); + } + #[test] fn get_assertion_hmac_get_secret_uses_hmac_get_secret_input() { let get_assertion = get_assertion_process_hmac_secret( From b833a60f55c98574244f811715afa2acb85cd1ab Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 Jul 2024 18:04:29 +0200 Subject: [PATCH 38/41] Add debug logging when no shared secret is available --- src/ctap2/commands/get_assertion.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 4886b2c3..b8435935 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -319,6 +319,10 @@ impl GetAssertion { pin_uv_auth_result.get_pin_uv_auth_token().as_ref(), )) } else { + debug!( + "Shared secret not available - will not send hmac-secret extension input: {:?}", + hmac_get_secret_or_prf + ); match hmac_get_secret_or_prf { HmacGetSecretOrPrf::HmacGetSecret(_) => None, HmacGetSecretOrPrf::PrfUninitialized(_) From a02ed2b54980b5428e2d6f3073b9620c9e4348f6 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 10 Jul 2024 17:07:35 +0200 Subject: [PATCH 39/41] Add debug logging when hmac-secret output decryption fails --- src/ctap2/commands/get_assertion.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index b8435935..5ac64c67 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -376,7 +376,13 @@ impl GetAssertion { { dev.get_shared_secret() .and_then(|shared_secret| hmac_response.decrypt_secrets(shared_secret)) - .and_then(Result::ok) + .and_then(|result| match result { + Ok(ok) => Some(ok), + Err(err) => { + debug!("Failed to decrypt hmac-secret response: {:?}", err); + None + } + }) } else { None }; @@ -398,7 +404,13 @@ impl GetAssertion { { dev.get_shared_secret() .and_then(|shared_secret| hmac_response.decrypt_secrets(shared_secret)) - .and_then(Result::ok) + .and_then(|result| match result { + Ok(ok) => Some(ok), + Err(err) => { + debug!("Failed to decrypt hmac-secret response: {:?}", err); + None + } + }) .map(|outputs| outputs.into()) } else { None From 4ae64105d4178e7b00ee3d15d38db1e2e496ac6d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sat, 20 Jul 2024 00:22:39 +0200 Subject: [PATCH 40/41] Add test of serializing uninitialized and unmatched PRF inputs --- src/ctap2/commands/get_assertion.rs | 67 +++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 7676fdf5..44cdccd3 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -819,7 +819,9 @@ pub mod test { }; use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, PinUvAuthParam}; use crate::ctap2::attestation::{AAGuid, AuthenticatorData, AuthenticatorDataFlags}; - use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; + use crate::ctap2::client_data::{ + Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType, + }; use crate::ctap2::commands::get_assertion::{ CalculatedHmacSecretExtension, GetAssertionExtensions, HmacGetSecretOrPrf, HmacSecretExtension, @@ -833,8 +835,8 @@ pub mod test { do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2, }; use crate::ctap2::server::{ - AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, - RelyingParty, RpIdHash, Transport, + AuthenticationExtensionsPRFInputs, AuthenticatorAttachment, PublicKeyCredentialDescriptor, + PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, Transport, }; use crate::transport::device_selector::Device; use crate::transport::hid::HIDDevice; @@ -1075,6 +1077,65 @@ pub mod test { ); } + #[test] + #[should_panic( + expected = "PrfUninitialized must be replaced with Prf or PrfUnmatched before serializing" + )] + fn test_serialize_prf_uninitialized() { + let assertion = GetAssertion { + client_data_hash: ClientDataHash([0; 32]), + rp: RelyingParty::from("example.com"), + allow_list: vec![], + extensions: GetAssertionExtensions { + app_id: None, + hmac_secret: Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: None, + }, + )), + }, + options: GetAssertionOptions { + user_presence: None, + user_verification: None, + }, + pin_uv_auth_param: None, + }; + assertion + .wire_format() + .expect("Failed to serialize GetAssertion request"); + } + + #[test] + fn test_serialize_prf_unmatched() { + let assertion = GetAssertion { + client_data_hash: ClientDataHash([0; 32]), + rp: RelyingParty::from("example.com"), + allow_list: vec![], + extensions: GetAssertionExtensions { + app_id: None, + hmac_secret: Some(HmacGetSecretOrPrf::PrfUnmatched), + }, + options: GetAssertionOptions { + user_presence: None, + user_verification: None, + }, + pin_uv_auth_param: None, + }; + let req_serialized = assertion + .wire_format() + .expect("Failed to serialize GetAssertion request"); + assert_eq!( + req_serialized, + [ + // Value copied from test failure output as regression test snapshot + 163, 1, 107, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 2, 88, 32, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 4, 160 + ] + ); + } + fn fill_device_ctap1(device: &mut Device, cid: [u8; 4], flags: u8, answer_status: [u8; 2]) { // ctap2 request let mut msg = cid.to_vec(); From cf92871f3600720aab85a1f2cf6299567676fd0a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sat, 20 Jul 2024 00:59:23 +0200 Subject: [PATCH 41/41] Add missing test of serializing hmac-secret with PIN protocol 2 --- src/ctap2/commands/get_assertion.rs | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 44cdccd3..890febae 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -822,6 +822,7 @@ pub mod test { use crate::ctap2::client_data::{ Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType, }; + use crate::ctap2::commands::client_pin::PinUvAuthTokenPermission; use crate::ctap2::commands::get_assertion::{ CalculatedHmacSecretExtension, GetAssertionExtensions, HmacGetSecretOrPrf, HmacSecretExtension, @@ -1077,6 +1078,61 @@ pub mod test { ); } + #[test] + fn test_serialize_get_assertion_ctap2_pin_protocol_2() { + let assertion = GetAssertion { + client_data_hash: ClientDataHash([0; 32]), + rp: RelyingParty::from("example.com"), + allow_list: vec![], + extensions: GetAssertionExtensions { + app_id: None, + hmac_secret: Some(HmacGetSecretOrPrf::HmacGetSecret( + HmacSecretExtension::new_test( + vec![32; 32], + None, + CalculatedHmacSecretExtension { + public_key: COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![], + y: vec![], + }), + }, + salt_enc: vec![7; 32], + salt_auth: vec![8; 16], + }, + Some(2), + ), + )), + }, + options: GetAssertionOptions { + user_presence: None, + user_verification: None, + }, + pin_uv_auth_param: Some(PinUvAuthParam::create_test( + 2, + vec![9; 4], + PinUvAuthTokenPermission::GetAssertion, + )), + }; + let req_serialized = assertion + .wire_format() + .expect("Failed to serialize GetAssertion request"); + assert_eq!( + req_serialized, + [ + // Value copied from test failure output as regression test snapshot + 165, 1, 107, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 2, 88, 32, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 4, 161, 107, 104, 109, 97, 99, 45, 115, 101, 99, 114, 101, 116, 164, 1, 165, 1, + 2, 3, 56, 24, 32, 1, 33, 64, 34, 64, 2, 88, 32, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 3, 80, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 4, 2, 6, 68, 9, 9, 9, 9, 7, 2 + ] + ); + } + #[test] #[should_panic( expected = "PrfUninitialized must be replaced with Prf or PrfUnmatched before serializing"