diff --git a/libwebauthn/examples/prf_test.rs b/libwebauthn/examples/prf_test.rs index 76dc4fe5..711976a5 100644 --- a/libwebauthn/examples/prf_test.rs +++ b/libwebauthn/examples/prf_test.rs @@ -13,8 +13,8 @@ use tokio::sync::mpsc::Receiver; use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::webauthn::{ - GetAssertionHmacOrPrfInput, GetAssertionHmacOrPrfOutput, GetAssertionRequest, - GetAssertionRequestExtensions, PRFValue, UserVerificationRequirement, + GetAssertionHmacOrPrfInput, GetAssertionRequest, GetAssertionRequestExtensions, PRFValue, + UserVerificationRequirement, }; use libwebauthn::pin::PinRequestReason; use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}; @@ -178,15 +178,15 @@ async fn run_success_test( println!( "{num}. result of {printoutput}: {:?}", assertion - .authenticator_data - .extensions + .unsigned_extensions_output .as_ref() - .map(|e| match &e.hmac_or_prf { - GetAssertionHmacOrPrfOutput::None => String::from("ERROR: No PRF output"), - GetAssertionHmacOrPrfOutput::HmacGetSecret(..) => - String::from("ERROR: Got HMAC instead of PRF output"), - GetAssertionHmacOrPrfOutput::Prf { enabled: _, result } => - hex::encode(result.first), + .map(|e| if let Some(prf) = &e.prf { + let results = prf.results.as_ref().map(|r| hex::encode(r.first)).unwrap(); + format!("Found PRF results: {}", results) + } else if e.hmac_get_secret.is_some() { + String::from("ERROR: Got HMAC instead of PRF output") + } else { + String::from("ERROR: No PRF output") }) .unwrap_or(String::from("ERROR: No extensions returned")) ); diff --git a/libwebauthn/src/fido.rs b/libwebauthn/src/fido.rs index b870a108..de91b540 100644 --- a/libwebauthn/src/fido.rs +++ b/libwebauthn/src/fido.rs @@ -2,8 +2,7 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use cosey::PublicKey; use serde::{ de::{DeserializeOwned, Error as DesError, Visitor}, - ser::Error as SerError, - Deserialize, Deserializer, Serialize, Serializer, + Deserialize, Deserializer, Serialize, }; use serde_bytes::ByteBuf; use std::{ @@ -11,11 +10,14 @@ use std::{ io::{Cursor, Read}, marker::PhantomData, }; -use tracing::warn; - -use crate::proto::{ - ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}, - CtapError, +use tracing::{error, warn}; + +use crate::{ + proto::{ + ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}, + CtapError, + }, + webauthn::{Error, PlatformError}, }; #[derive(Debug, PartialEq, Eq)] @@ -62,28 +64,6 @@ pub struct AttestedCredentialData { pub credential_public_key: PublicKey, } -impl Serialize for AttestedCredentialData { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // Name | Length - // -------------------------------- - // aaguid | 16 - // credentialIdLenght | 2 - // credentialId | L - // credentialPublicKey | variable - let mut res = self.aaguid.to_vec(); - res.write_u16::(self.credential_id.len() as u16) - .map_err(SerError::custom)?; - res.extend(&self.credential_id); - let cose_encoded_public_key = - serde_cbor::to_vec(&self.credential_public_key).map_err(SerError::custom)?; - res.extend(cose_encoded_public_key); - serializer.serialize_bytes(&res) - } -} - impl From<&AttestedCredentialData> for Ctap2PublicKeyCredentialDescriptor { fn from(data: &AttestedCredentialData) -> Self { Self { @@ -103,14 +83,11 @@ pub struct AuthenticatorData { pub extensions: Option, } -impl Serialize for AuthenticatorData +impl AuthenticatorData where T: Clone + Serialize, { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { + pub fn to_response_bytes(&self) -> Result, Error> { // Name | Length // ----------------------------------- // rpIdHash | 32 @@ -121,14 +98,46 @@ where let mut res = self.rp_id_hash.to_vec(); res.push(self.flags.bits()); res.write_u32::(self.signature_count) - .map_err(SerError::custom)?; + .map_err(|e| { + error!("Failed to create AuthenticatorData output vec at signature_count: {e:?}"); + Error::Platform(PlatformError::InvalidDeviceResponse) + })?; + if let Some(att_data) = &self.attested_credential { - res.extend(serde_cbor::to_vec(att_data).map_err(SerError::custom)?); + // Name | Length + // -------------------------------- + // aaguid | 16 + // credentialIdLenght | 2 + // credentialId | L + // credentialPublicKey | variable + res.extend(att_data.aaguid); + res.write_u16::(att_data.credential_id.len() as u16) + .map_err(|e| { + error!( + "Failed to create AuthenticatorData output vec at attested_credential.credential_id: {e:?}" + ); + Error::Platform(PlatformError::InvalidDeviceResponse) + })?; + res.extend(&att_data.credential_id); + let cose_encoded_public_key = + serde_cbor::to_vec(&att_data.credential_public_key) + .map_err(|e| { + error!( + "Failed to create AuthenticatorData output vec at attested_credential.credential_public_key: {e:?}" + ); + Error::Platform(PlatformError::InvalidDeviceResponse) + })?; + res.extend(cose_encoded_public_key); } - if let Some(extensions) = &self.extensions { - res.extend(serde_cbor::to_vec(extensions).map_err(SerError::custom)?); + + if self.extensions.is_some() || self.flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) + { + res.extend(serde_cbor::to_vec(&self.extensions).map_err(|e| { + error!("Failed to create AuthenticatorData output vec at extensions: {e:?}"); + Error::Platform(PlatformError::InvalidDeviceResponse) + })?); } - serializer.serialize_bytes(&res) + Ok(res) } } diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index afb0d2f3..5d969ae6 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -148,7 +148,6 @@ impl UpgradableResponse for Regis attestation_statement, enterprise_attestation: None, large_blob_key: None, - unsigned_extension_output: None, }; Ok(resp.into_make_credential_output(request, None)) } @@ -195,7 +194,6 @@ impl UpgradableResponse for SignResponse { credentials_count: None, user_selected: None, large_blob_key: None, - unsigned_extension_outputs: None, enterprise_attestation: None, attestation_statement: None, }; diff --git a/libwebauthn/src/ops/webauthn.rs b/libwebauthn/src/ops/webauthn.rs index 201e6ff0..49727898 100644 --- a/libwebauthn/src/ops/webauthn.rs +++ b/libwebauthn/src/ops/webauthn.rs @@ -1,11 +1,7 @@ -use std::{ - collections::{BTreeMap, HashMap}, - time::Duration, -}; +use std::{collections::HashMap, time::Duration}; use ctap_types::ctap2::credential_management::CredentialProtectionPolicy as Ctap2CredentialProtectionPolicy; use serde::{Deserialize, Serialize}; -use serde_cbor::Value; use sha2::{Digest, Sha256}; use tracing::{debug, error, instrument, trace}; @@ -16,6 +12,7 @@ use crate::{ ctap1::{Ctap1RegisteredKey, Ctap1Version}, ctap2::{ Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType, + Ctap2GetAssertionResponseExtensions, Ctap2MakeCredentialsResponseExtensions, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity, }, @@ -57,7 +54,23 @@ pub struct MakeCredentialResponse { pub attestation_statement: Ctap2AttestationStatement, pub enterprise_attestation: Option, pub large_blob_key: Option>, - pub unsigned_extension_output: Option>, + pub unsigned_extensions_output: Option, +} + +#[derive(Debug, Default, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MakeCredentialsResponseUnsignedExtensions { + // pub app_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_props: Option, + // #[serde(skip_serializing_if = "Option::is_none")] + // pub cred_blob: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hmac_create_secret: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prf: Option, } #[derive(Debug, Clone)] @@ -79,9 +92,11 @@ pub struct MakeCredentialRequest { pub timeout: Duration, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Serialize)] pub struct PRFValue { + #[serde(with = "serde_bytes")] pub first: [u8; 32], + #[serde(skip_serializing_if = "Option::is_none", with = "serde_bytes")] pub second: Option<[u8; 32]>, } @@ -103,14 +118,10 @@ pub enum MakeCredentialHmacOrPrfInput { // }, } -#[derive(Debug, Default, Clone)] -pub enum MakeCredentialHmacOrPrfOutput { - #[default] - None, - HmacGetSecret(bool), - Prf { - enabled: bool, - }, +#[derive(Debug, Default, Clone, Serialize)] +pub struct MakeCredentialPrfOutput { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, } #[derive(Debug, Clone)] @@ -159,6 +170,13 @@ impl From for CredentialProtectionPolicy { } } +#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct CredentialPropsExtension { + #[serde(skip_serializing_if = "Option::is_none")] + pub rk: Option, +} + #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum MakeCredentialLargeBlobExtension { @@ -168,6 +186,12 @@ pub enum MakeCredentialLargeBlobExtension { Required, } +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] +pub struct MakeCredentialLargeBlobExtensionOutput { + #[serde(skip_serializing_if = "Option::is_none")] + pub supported: Option, +} + #[derive(Debug, Default, Clone)] pub struct MakeCredentialsRequestExtensions { pub cred_props: Option, @@ -178,21 +202,9 @@ pub struct MakeCredentialsRequestExtensions { pub hmac_or_prf: MakeCredentialHmacOrPrfInput, } -#[derive(Debug, Default, Clone)] -pub struct MakeCredentialsResponseExtensions { - pub cred_protect: Option, - /// If storing credBlob was successful - pub cred_blob: Option, - /// Current min PIN lenght - pub min_pin_length: Option, - pub hmac_or_prf: MakeCredentialHmacOrPrfOutput, - // Currently, credProps only returns one value: rk = bool - // If these get more in the future, we can use a struct here. - pub cred_props_rk: Option, -} +pub type MakeCredentialsResponseExtensions = Ctap2MakeCredentialsResponseExtensions; impl MakeCredentialRequest { - #[cfg(test)] pub fn dummy() -> Self { Self { hash: vec![0; 32], @@ -203,7 +215,7 @@ impl MakeCredentialRequest { extensions: None, origin: "example.org".to_owned(), require_resident_key: false, - user_verification: UserVerificationRequirement::Preferred, + user_verification: UserVerificationRequirement::Discouraged, timeout: Duration::from_secs(10), } } @@ -230,17 +242,10 @@ pub enum GetAssertionHmacOrPrfInput { }, } -#[derive(Debug, Default, Clone)] -pub enum GetAssertionHmacOrPrfOutput { - #[default] - None, - HmacGetSecret(HMACGetSecretOutput), - Prf { - enabled: bool, - // The spec tells us this should be a Vec, but doesn't - // explain how it could hold more than 1 value - result: PRFValue, - }, +#[derive(Debug, Default, Clone, Serialize)] +pub struct GetAssertionPrfOutput { + #[serde(skip_serializing_if = "Option::is_none")] + pub results: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -258,6 +263,15 @@ pub enum GetAssertionLargeBlobExtension { // Write(Vec), } +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] +pub struct GetAssertionLargeBlobExtensionOutput { + #[serde(skip_serializing_if = "Option::is_none")] + pub blob: Option>, + // Not yet supported + // #[serde(skip_serializing_if = "Option::is_none")] + // pub written: Option, +} + #[derive(Debug, Default, Clone)] pub struct GetAssertionRequestExtensions { pub cred_blob: Option, @@ -265,13 +279,15 @@ pub struct GetAssertionRequestExtensions { pub large_blob: GetAssertionLargeBlobExtension, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] pub struct HMACGetSecretOutput { pub output1: [u8; 32], + #[serde(skip_serializing_if = "Option::is_none")] pub output2: Option<[u8; 32]>, } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(transparent)] pub struct Ctap2HMACGetSecretOutput { // We get this from the device, but have to decrypt it, and @@ -282,7 +298,7 @@ pub struct Ctap2HMACGetSecretOutput { impl Ctap2HMACGetSecretOutput { pub(crate) fn decrypt_output( - self, + &self, shared_secret: &[u8], uv_proto: &Box, ) -> Option { @@ -298,7 +314,7 @@ impl Ctap2HMACGetSecretOutput { res.output1.copy_from_slice(&output); } else if output.len() == 64 { let (o1, o2) = output.split_at(32); - res.output1.copy_from_slice(&o1); + res.output1.copy_from_slice(o1); res.output2 = Some(o2.try_into().unwrap()); } else { error!("Failed to split HMAC Secret outputs. Unexpected output length: {}. Skipping HMAC extension", output.len()); @@ -309,11 +325,17 @@ impl Ctap2HMACGetSecretOutput { } } -#[derive(Debug, Default, Clone)] -pub struct GetAssertionResponseExtensions { - // Stored credBlob - pub cred_blob: Option>, - pub hmac_or_prf: GetAssertionHmacOrPrfOutput, +pub type GetAssertionResponseExtensions = Ctap2GetAssertionResponseExtensions; + +#[derive(Debug, Default, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAssertionResponseUnsignedExtensions { + #[serde(skip_serializing_if = "Option::is_none")] + pub hmac_get_secret: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prf: Option, } #[derive(Debug, Clone)] @@ -330,7 +352,7 @@ pub struct Assertion { pub credentials_count: Option, pub user_selected: Option, pub large_blob_key: Option>, - pub unsigned_extension_outputs: Option>, + pub unsigned_extensions_output: Option, pub enterprise_attestation: Option, pub attestation_statement: Option, } diff --git a/libwebauthn/src/proto/ctap2/mod.rs b/libwebauthn/src/proto/ctap2/mod.rs index c227e568..6edac09e 100644 --- a/libwebauthn/src/proto/ctap2/mod.rs +++ b/libwebauthn/src/proto/ctap2/mod.rs @@ -25,7 +25,11 @@ pub use model::{ Ctap2CredentialData, Ctap2CredentialManagementMetadata, Ctap2CredentialManagementRequest, Ctap2CredentialManagementResponse, Ctap2RPData, }; -pub use model::{Ctap2GetAssertionRequest, Ctap2GetAssertionResponse}; -pub use model::{Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse}; +pub use model::{ + Ctap2GetAssertionRequest, Ctap2GetAssertionResponse, Ctap2GetAssertionResponseExtensions, +}; +pub use model::{ + Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse, Ctap2MakeCredentialsResponseExtensions, +}; pub mod preflight; pub use protocol::Ctap2; diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index 8e4202bd..aad10b4c 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -25,11 +25,12 @@ pub use client_pin::{ mod make_credential; pub use make_credential::{ Ctap2MakeCredentialOptions, Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse, + Ctap2MakeCredentialsResponseExtensions, }; mod get_assertion; pub use get_assertion::{ Ctap2AttestationStatement, Ctap2GetAssertionOptions, Ctap2GetAssertionRequest, - Ctap2GetAssertionResponse, FidoU2fAttestationStmt, + Ctap2GetAssertionResponse, Ctap2GetAssertionResponseExtensions, FidoU2fAttestationStmt, }; mod credential_management; pub use credential_management::{ diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index 7727321e..4000f686 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -2,9 +2,9 @@ use crate::{ fido::AuthenticatorData, ops::webauthn::{ Assertion, Ctap2HMACGetSecretOutput, GetAssertionHmacOrPrfInput, - GetAssertionHmacOrPrfOutput, GetAssertionLargeBlobExtension, GetAssertionRequest, - GetAssertionRequestExtensions, GetAssertionResponseExtensions, HMACGetSecretInput, - PRFValue, + GetAssertionLargeBlobExtension, GetAssertionLargeBlobExtensionOutput, + GetAssertionPrfOutput, GetAssertionRequest, GetAssertionRequestExtensions, + GetAssertionResponseUnsignedExtensions, HMACGetSecretInput, PRFValue, }, pin::PinUvAuthProtocol, transport::AuthTokenData, @@ -383,9 +383,6 @@ pub struct Ctap2GetAssertionResponse { #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub unsigned_extension_outputs: Option>, - #[serde(skip_serializing_if = "Option::is_none")] pub enterprise_attestation: Option, @@ -438,32 +435,27 @@ impl Ctap2GetAssertionResponse { request: &GetAssertionRequest, auth_data: Option<&AuthTokenData>, ) -> Assertion { - let authenticator_data = AuthenticatorData:: { - rp_id_hash: self.authenticator_data.rp_id_hash, - flags: self.authenticator_data.flags, - signature_count: self.authenticator_data.signature_count, - attested_credential: self.authenticator_data.attested_credential, - extensions: self - .authenticator_data - .extensions - .map(|x| x.into_output(request, auth_data)), - }; + let unsigned_extensions_output = self + .authenticator_data + .extensions + .as_ref() + .map(|x| x.to_unsigned_extensions(request, &self, auth_data)); Assertion { credential_id: self.credential_id, - authenticator_data, + authenticator_data: self.authenticator_data, signature: self.signature.into_vec(), user: self.user, credentials_count: self.credentials_count, user_selected: self.user_selected, large_blob_key: self.large_blob_key.map(ByteBuf::into_vec), - unsigned_extension_outputs: self.unsigned_extension_outputs, + unsigned_extensions_output, enterprise_attestation: self.enterprise_attestation, attestation_statement: self.attestation_statement, } } } -#[derive(Debug, Default, Clone, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Ctap2GetAssertionResponseExtensions { // Stored credBlob @@ -480,14 +472,15 @@ pub struct Ctap2GetAssertionResponseExtensions { } impl Ctap2GetAssertionResponseExtensions { - pub(crate) fn into_output( - self, + pub(crate) fn to_unsigned_extensions( + &self, request: &GetAssertionRequest, + response: &Ctap2GetAssertionResponse, auth_data: Option<&AuthTokenData>, - ) -> GetAssertionResponseExtensions { - let hmac_or_prf = if let Some(orig_ext) = &request.extensions { + ) -> GetAssertionResponseUnsignedExtensions { + let (hmac_get_secret, prf) = if let Some(orig_ext) = &request.extensions { // Decrypt the raw HMAC extension - let decrypted_hmac = self.hmac_secret.and_then(|x| { + let decrypted_hmac = self.hmac_secret.as_ref().and_then(|x| { if let Some(auth_data) = auth_data { let uv_proto = auth_data.protocol_version.create_protocol_object(); x.decrypt_output(&auth_data.shared_secret, &uv_proto) @@ -498,28 +491,46 @@ impl Ctap2GetAssertionResponseExtensions { if let Some(decrypted) = decrypted_hmac { // Repackaging it into output match &orig_ext.hmac_or_prf { - GetAssertionHmacOrPrfInput::None => GetAssertionHmacOrPrfOutput::None, - GetAssertionHmacOrPrfInput::HmacGetSecret(..) => { - GetAssertionHmacOrPrfOutput::HmacGetSecret(decrypted) - } - GetAssertionHmacOrPrfInput::Prf { .. } => GetAssertionHmacOrPrfOutput::Prf { - enabled: true, - result: PRFValue { - first: decrypted.output1, - second: decrypted.output2, - }, - }, + GetAssertionHmacOrPrfInput::None => (None, None), + GetAssertionHmacOrPrfInput::HmacGetSecret(..) => (Some(decrypted), None), + GetAssertionHmacOrPrfInput::Prf { .. } => ( + None, + Some(GetAssertionPrfOutput { + results: Some(PRFValue { + first: decrypted.output1, + second: decrypted.output2, + }), + }), + ), } } else { - GetAssertionHmacOrPrfOutput::None + (None, None) } } else { - GetAssertionHmacOrPrfOutput::None + (None, None) }; - GetAssertionResponseExtensions { - cred_blob: self.cred_blob, - hmac_or_prf, + // LargeBlobs was requested + let large_blob = request + .extensions + .as_ref() + .filter(|x| x.large_blob != GetAssertionLargeBlobExtension::None) + .map(|_| { + Some(GetAssertionLargeBlobExtensionOutput { + blob: response + .large_blob_key + .as_ref() + .map(|x| x.clone().into_vec()), + // Not yet supported + // written: None, + }) + }) + .unwrap_or_default(); + + GetAssertionResponseUnsignedExtensions { + hmac_get_secret, + large_blob, + prf, } } } diff --git a/libwebauthn/src/proto/ctap2/model/make_credential.rs b/libwebauthn/src/proto/ctap2/model/make_credential.rs index 1b28de97..a46257af 100644 --- a/libwebauthn/src/proto/ctap2/model/make_credential.rs +++ b/libwebauthn/src/proto/ctap2/model/make_credential.rs @@ -7,9 +7,10 @@ use super::{ use crate::{ fido::AuthenticatorData, ops::webauthn::{ - CredentialProtectionPolicy, MakeCredentialHmacOrPrfInput, MakeCredentialHmacOrPrfOutput, - MakeCredentialLargeBlobExtension, MakeCredentialRequest, MakeCredentialResponse, - MakeCredentialsRequestExtensions, MakeCredentialsResponseExtensions, + CredentialPropsExtension, CredentialProtectionPolicy, MakeCredentialHmacOrPrfInput, + MakeCredentialLargeBlobExtension, MakeCredentialLargeBlobExtensionOutput, + MakeCredentialPrfOutput, MakeCredentialRequest, MakeCredentialResponse, + MakeCredentialsRequestExtensions, MakeCredentialsResponseUnsignedExtensions, }, pin::PinUvAuthProtocol, proto::CtapError, @@ -18,9 +19,7 @@ use crate::{ use ctap_types::ctap2::credential_management::CredentialProtectionPolicy as Ctap2CredentialProtectionPolicy; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; -use serde_cbor::Value; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; -use std::collections::BTreeMap; #[derive(Debug, Default, Clone, Copy, Serialize)] pub struct Ctap2MakeCredentialOptions { @@ -248,9 +247,6 @@ pub struct Ctap2MakeCredentialResponse { #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub unsigned_extension_output: Option>, } impl Ctap2MakeCredentialResponse { @@ -259,23 +255,18 @@ impl Ctap2MakeCredentialResponse { request: &MakeCredentialRequest, info: Option<&Ctap2GetInfoResponse>, ) -> MakeCredentialResponse { - let authenticator_data = AuthenticatorData:: { - rp_id_hash: self.authenticator_data.rp_id_hash, - flags: self.authenticator_data.flags, - signature_count: self.authenticator_data.signature_count, - attested_credential: self.authenticator_data.attested_credential, - extensions: self - .authenticator_data - .extensions - .map(|x| x.into_output(request, info)), - }; + let unsigned_extensions_output = self + .authenticator_data + .extensions + .as_ref() + .map(|x| x.to_unsigned_extensions(request, info)); MakeCredentialResponse { format: self.format, - authenticator_data, + authenticator_data: self.authenticator_data, attestation_statement: self.attestation_statement, enterprise_attestation: self.enterprise_attestation, large_blob_key: self.large_blob_key.map(|x| x.into_vec()), - unsigned_extension_output: self.unsigned_extension_output, + unsigned_extensions_output, } } } @@ -323,18 +314,11 @@ impl Ctap2UserVerifiableRequest for Ctap2MakeCredentialRequest { #[derive(Debug, Default, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Ctap2MakeCredentialsResponseExtensions { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cred_protect: Option, // If storing credBlob was successful #[serde(default, skip_serializing_if = "Option::is_none")] pub cred_blob: Option, - // No output provided for largeBlobKey in MakeCredential requests - // pub large_blob_key: Option, - - // Current min PIN lenght #[serde(default, skip_serializing_if = "Option::is_none")] - pub min_pin_length: Option, - + pub cred_protect: Option, // Thanks, FIDO-spec for this consistent naming scheme... #[serde( rename = "hmac-secret", @@ -342,33 +326,35 @@ pub struct Ctap2MakeCredentialsResponseExtensions { skip_serializing_if = "Option::is_none" )] pub hmac_secret: Option, + // Current min PIN lenght + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, } impl Ctap2MakeCredentialsResponseExtensions { - pub fn into_output( - self, + pub fn to_unsigned_extensions( + &self, request: &MakeCredentialRequest, info: Option<&Ctap2GetInfoResponse>, - ) -> MakeCredentialsResponseExtensions { - let hmac_or_prf = if let Some(incoming_ext) = &request.extensions { + ) -> MakeCredentialsResponseUnsignedExtensions { + let (hmac_create_secret, prf) = if let Some(incoming_ext) = &request.extensions { match &incoming_ext.hmac_or_prf { - MakeCredentialHmacOrPrfInput::None => MakeCredentialHmacOrPrfOutput::None, - MakeCredentialHmacOrPrfInput::HmacGetSecret => { - MakeCredentialHmacOrPrfOutput::HmacGetSecret( - self.hmac_secret.unwrap_or_default(), - ) - } - MakeCredentialHmacOrPrfInput::Prf => MakeCredentialHmacOrPrfOutput::Prf { - enabled: self.hmac_secret.unwrap_or_default(), - }, + MakeCredentialHmacOrPrfInput::None => (None, None), + MakeCredentialHmacOrPrfInput::HmacGetSecret => (self.hmac_secret, None), + MakeCredentialHmacOrPrfInput::Prf => ( + None, + Some(MakeCredentialPrfOutput { + enabled: self.hmac_secret, + }), + ), } } else { - MakeCredentialHmacOrPrfOutput::None + (None, None) }; // credProps extension // https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension - let cred_props_rk = match &request + let cred_props = match &request .extensions .as_ref() .and_then(|x| x.cred_props.as_ref()) @@ -384,20 +370,43 @@ impl Ctap2MakeCredentialsResponseExtensions { // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#op-makecred-step-rk // if the "rk" option is false: the authenticator MUST create a non-discoverable credential. // Note: This step is a change from CTAP2.0 where if the "rk" option is false the authenticator could optionally create a discoverable credential. - Some(request.require_resident_key) + Some(CredentialPropsExtension { + rk: Some(request.require_resident_key), + }) + } else { + Some(CredentialPropsExtension { + // For CTAP 2.0, we can't say if "rk" is true or not. + rk: None, + }) + } + } + }; + + // largeBlob extension + // https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension + let large_blob = match &request + .extensions + .as_ref() + .and_then(|x| Some(&x.large_blob)) + { + None | Some(MakeCredentialLargeBlobExtension::None) => None, // Not requested, so we don't give an answer + Some(MakeCredentialLargeBlobExtension::Preferred) + | Some(MakeCredentialLargeBlobExtension::Required) => { + if info.map(|x| x.option_enabled("largeBlobs")) == Some(true) { + Some(MakeCredentialLargeBlobExtensionOutput { + supported: Some(true), + }) } else { - // For CTAP 2.0, we can't say if "rk" is true or not. None } } }; - MakeCredentialsResponseExtensions { - cred_protect: self.cred_protect.map(|x| x.into()), - cred_blob: self.cred_blob, - min_pin_length: self.min_pin_length, - hmac_or_prf, - cred_props_rk, + MakeCredentialsResponseUnsignedExtensions { + cred_props, + hmac_create_secret, + large_blob, + prf, } } }