From fc616c2d92d13cdaac2ead23081d33c912edf37a Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Thu, 22 May 2025 13:01:42 +0200 Subject: [PATCH] Parse unsigned extensions, even if no signed extensions are present --- libwebauthn/src/ops/webauthn.rs | 107 ++++++++++++++++-- .../src/proto/ctap2/model/make_credential.rs | 98 ++-------------- 2 files changed, 108 insertions(+), 97 deletions(-) diff --git a/libwebauthn/src/ops/webauthn.rs b/libwebauthn/src/ops/webauthn.rs index 49727898..9e2ea0e2 100644 --- a/libwebauthn/src/ops/webauthn.rs +++ b/libwebauthn/src/ops/webauthn.rs @@ -12,9 +12,9 @@ use crate::{ ctap1::{Ctap1RegisteredKey, Ctap1Version}, ctap2::{ Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType, - Ctap2GetAssertionResponseExtensions, Ctap2MakeCredentialsResponseExtensions, - Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, - Ctap2PublicKeyCredentialUserEntity, + Ctap2GetAssertionResponseExtensions, Ctap2GetInfoResponse, + Ctap2MakeCredentialsResponseExtensions, Ctap2PublicKeyCredentialDescriptor, + Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity, }, }, webauthn::CtapError, @@ -54,7 +54,7 @@ pub struct MakeCredentialResponse { pub attestation_statement: Ctap2AttestationStatement, pub enterprise_attestation: Option, pub large_blob_key: Option>, - pub unsigned_extensions_output: Option, + pub unsigned_extensions_output: MakeCredentialsResponseUnsignedExtensions, } #[derive(Debug, Default, Clone, Serialize)] @@ -73,6 +73,95 @@ pub struct MakeCredentialsResponseUnsignedExtensions { pub prf: Option, } +impl MakeCredentialsResponseUnsignedExtensions { + pub fn has_some(&self) -> bool { + self.cred_props.is_some() + || self.hmac_create_secret.is_some() + || self.large_blob.is_some() + || self.prf.is_some() + } + + pub fn from_signed_extensions( + signed_extensions: &Option, + request: &MakeCredentialRequest, + info: Option<&Ctap2GetInfoResponse>, + ) -> MakeCredentialsResponseUnsignedExtensions { + let mut hmac_create_secret = None; + let mut prf = None; + if let Some(signed_extensions) = signed_extensions { + (hmac_create_secret, prf) = if let Some(incoming_ext) = &request.extensions { + match &incoming_ext.hmac_or_prf { + MakeCredentialHmacOrPrfInput::None => (None, None), + MakeCredentialHmacOrPrfInput::HmacGetSecret => { + (signed_extensions.hmac_secret, None) + } + MakeCredentialHmacOrPrfInput::Prf => ( + None, + Some(MakeCredentialPrfOutput { + enabled: signed_extensions.hmac_secret, + }), + ), + } + } else { + (None, None) + }; + } + + // credProps extension + // https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension + let cred_props = match &request + .extensions + .as_ref() + .and_then(|x| x.cred_props.as_ref()) + { + None | Some(false) => None, // Not requested, so we don't give an answer + Some(true) => { + // https://w3c.github.io/webauthn/#dom-credentialpropertiesoutput-rk + // Some authenticators create discoverable credentials even when not + // requested by the client platform. Because of this, client platforms may be + // forced to omit the rk property because they lack the assurance to be able + // to set it to false. + if info.map(|x| x.supports_fido_2_1()) == Some(true) { + // 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(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().map(|x| &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 { + None + } + } + }; + + MakeCredentialsResponseUnsignedExtensions { + cred_props, + hmac_create_secret, + large_blob, + prf, + } + } +} + #[derive(Debug, Clone)] pub struct MakeCredentialRequest { pub hash: Vec, @@ -131,10 +220,12 @@ pub struct CredentialProtectionExtension { } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] pub enum CredentialProtectionPolicy { + #[serde(rename = "userVerificationOptional")] UserVerificationOptional = 1, - UserVerificationOptionalWithCredentialIdList = 2, + #[serde(rename = "userVerificationOptionalWithCredentialIDList")] + UserVerificationOptionalWithCredentialIDList = 2, + #[serde(rename = "userVerificationRequired")] UserVerificationRequired = 3, } @@ -144,7 +235,7 @@ impl From for Ctap2CredentialProtectionPolicy { CredentialProtectionPolicy::UserVerificationOptional => { Ctap2CredentialProtectionPolicy::Optional } - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => { + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList => { Ctap2CredentialProtectionPolicy::OptionalWithCredentialIdList } CredentialProtectionPolicy::UserVerificationRequired => { @@ -161,7 +252,7 @@ impl From for CredentialProtectionPolicy { CredentialProtectionPolicy::UserVerificationOptional } Ctap2CredentialProtectionPolicy::OptionalWithCredentialIdList => { - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList } Ctap2CredentialProtectionPolicy::Required => { CredentialProtectionPolicy::UserVerificationRequired diff --git a/libwebauthn/src/proto/ctap2/model/make_credential.rs b/libwebauthn/src/proto/ctap2/model/make_credential.rs index a46257af..19fab8b1 100644 --- a/libwebauthn/src/proto/ctap2/model/make_credential.rs +++ b/libwebauthn/src/proto/ctap2/model/make_credential.rs @@ -7,10 +7,9 @@ use super::{ use crate::{ fido::AuthenticatorData, ops::webauthn::{ - CredentialPropsExtension, CredentialProtectionPolicy, MakeCredentialHmacOrPrfInput, - MakeCredentialLargeBlobExtension, MakeCredentialLargeBlobExtensionOutput, - MakeCredentialPrfOutput, MakeCredentialRequest, MakeCredentialResponse, - MakeCredentialsRequestExtensions, MakeCredentialsResponseUnsignedExtensions, + CredentialProtectionPolicy, MakeCredentialHmacOrPrfInput, MakeCredentialLargeBlobExtension, + MakeCredentialRequest, MakeCredentialResponse, MakeCredentialsRequestExtensions, + MakeCredentialsResponseUnsignedExtensions, }, pin::PinUvAuthProtocol, proto::CtapError, @@ -255,11 +254,12 @@ impl Ctap2MakeCredentialResponse { request: &MakeCredentialRequest, info: Option<&Ctap2GetInfoResponse>, ) -> MakeCredentialResponse { - let unsigned_extensions_output = self - .authenticator_data - .extensions - .as_ref() - .map(|x| x.to_unsigned_extensions(request, info)); + let unsigned_extensions_output = + MakeCredentialsResponseUnsignedExtensions::from_signed_extensions( + &self.authenticator_data.extensions, + request, + info, + ); MakeCredentialResponse { format: self.format, authenticator_data: self.authenticator_data, @@ -330,83 +330,3 @@ pub struct Ctap2MakeCredentialsResponseExtensions { #[serde(default, skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, } - -impl Ctap2MakeCredentialsResponseExtensions { - pub fn to_unsigned_extensions( - &self, - request: &MakeCredentialRequest, - info: Option<&Ctap2GetInfoResponse>, - ) -> MakeCredentialsResponseUnsignedExtensions { - let (hmac_create_secret, prf) = if let Some(incoming_ext) = &request.extensions { - match &incoming_ext.hmac_or_prf { - MakeCredentialHmacOrPrfInput::None => (None, None), - MakeCredentialHmacOrPrfInput::HmacGetSecret => (self.hmac_secret, None), - MakeCredentialHmacOrPrfInput::Prf => ( - None, - Some(MakeCredentialPrfOutput { - enabled: self.hmac_secret, - }), - ), - } - } else { - (None, None) - }; - - // credProps extension - // https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension - let cred_props = match &request - .extensions - .as_ref() - .and_then(|x| x.cred_props.as_ref()) - { - None | Some(false) => None, // Not requested, so we don't give an answer - Some(true) => { - // https://w3c.github.io/webauthn/#dom-credentialpropertiesoutput-rk - // Some authenticators create discoverable credentials even when not - // requested by the client platform. Because of this, client platforms may be - // forced to omit the rk property because they lack the assurance to be able - // to set it to false. - if info.map(|x| x.supports_fido_2_1()) == Some(true) { - // 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(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 { - None - } - } - }; - - MakeCredentialsResponseUnsignedExtensions { - cred_props, - hmac_create_secret, - large_blob, - prf, - } - } -}