From f8a921886b26f8a0cd39af55c3bee7c6b1265118 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 14:49:16 -0500 Subject: [PATCH 1/5] Update serde_workaround pattern This breaks the rusty syntax but fixes ambiguity around the default keyword whitch was being miss-interpreted. Ideally this would be a TT muncher macro but thats a lot more work than I have time to put into this --- .../src/ctap2/extensions/hmac_secret.rs | 2 +- passkey-types/src/ctap2/get_assertion.rs | 30 ++++++++++++------- passkey-types/src/ctap2/get_info.rs | 10 +++---- passkey-types/src/ctap2/make_credential.rs | 22 ++++++++------ passkey-types/src/utils/serde_workaround.rs | 6 ++-- 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/passkey-types/src/ctap2/extensions/hmac_secret.rs b/passkey-types/src/ctap2/extensions/hmac_secret.rs index ecc9cea..6fedb4d 100644 --- a/passkey-types/src/ctap2/extensions/hmac_secret.rs +++ b/passkey-types/src/ctap2/extensions/hmac_secret.rs @@ -20,7 +20,7 @@ serde_workaround! { pub salt_auth: Bytes, /// The Pin Authentication protocol used in the derivation of the shared secret. - #[serde(rename=0x04, default, skip_serializing_if= Option::is_none)] + #[serde(rename=0x04; default, skip_serializing_if= Option::is_none)] pub pin_uv_auth_protocol: Option, } } diff --git a/passkey-types/src/ctap2/get_assertion.rs b/passkey-types/src/ctap2/get_assertion.rs index cceca4d..1407cfa 100644 --- a/passkey-types/src/ctap2/get_assertion.rs +++ b/passkey-types/src/ctap2/get_assertion.rs @@ -34,25 +34,33 @@ serde_workaround! { /// A sequence of PublicKeyCredentialDescriptor structures, each denoting a credential. If /// this parameter is present and has 1 or more entries, the authenticator MUST only /// generate an assertion using one of the denoted credentials. - #[serde(rename = 0x03, default, skip_serializing_if = Option::is_none)] + #[serde( + rename = 0x03; + default, + skip_serializing_if = Option::is_none + )] pub allow_list: Option>, /// Parameters to influence authenticator operation. These parameters might be authenticator /// specific. - #[serde(rename = 0x04, default, skip_serializing_if = Option::is_none)] + #[serde( + rename = 0x04; + default, + skip_serializing_if = Option::is_none + )] pub extensions: Option, /// Parameters to influence authenticator operation, see [`Options`] for more details. - #[serde(rename = 0x05, default)] + #[serde(rename = 0x05; default)] pub options: Options, /// First 16 bytes of HMAC-SHA-256 of clientDataHash using pinToken which platform got from /// the authenticator: HMAC-SHA-256(pinToken, clientDataHash). (NOT YET SUPPORTED) - #[serde(rename = 0x06, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x06; default, skip_serializing_if = Option::is_none)] pub pin_auth: Option, /// PIN protocol version chosen by the client - #[serde(rename = 0x07, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x07; default, skip_serializing_if = Option::is_none)] pub pin_protocol: Option, } } @@ -99,7 +107,7 @@ serde_workaround! { /// PublicKeyCredentialDescriptor structure containing the credential identifier whose /// private key was used to generate the assertion. May be omitted if the allowList has /// exactly one Credential. - #[serde(rename = 0x01, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x01; default, skip_serializing_if = Option::is_none)] pub credential: Option, /// The signed-over contextual bindings made by the authenticator @@ -134,7 +142,7 @@ serde_workaround! { /// authenticator returns "id" as well as other fields to the platform. Platform will use /// this information to show the account selection UX to the user and for the user selected /// account, it will ONLY return "id" back to the WebAuthn layer and discard other user details. - #[serde(rename = 0x04, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x04; default, skip_serializing_if = Option::is_none)] pub user: Option, /// Total number of account credentials for the RP. This member is required when more than @@ -143,7 +151,7 @@ serde_workaround! { /// /// It seems unlikely that more than 256 credentials would be needed for any given RP. Please /// file an enhancement request if this limit impacts your application. - #[serde(rename = 0x05, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x05; default, skip_serializing_if = Option::is_none)] pub number_of_credentials: Option, /// Indicates that a credential was selected by the user via interaction directly with the authenticator, @@ -152,20 +160,20 @@ serde_workaround! { /// MUST NOT be present in response to a request where an [`Request::allow_list`] was given, /// where [`Self::number_of_credentials`] is greater than one, /// nor in response to an `authenticatorGetNextAssertion` request. - #[serde(rename = 0x06, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x06; default, skip_serializing_if = Option::is_none)] pub user_selected: Option, /// The contents of the associated `largeBlobKey` if present for the asserted credential, /// and if [largeBlobKey[] was true in the extensions input. /// /// This extension is currently un-supported by this library. - #[serde(rename = 0x07, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x07; default, skip_serializing_if = Option::is_none)] pub large_blob_key: Option, /// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any. /// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs. /// Clients MUST treat an empty map the same as an omitted field. - #[serde(rename = 0x08, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x08; default, skip_serializing_if = Option::is_none)] pub unsigned_extension_outputs: Option, } } diff --git a/passkey-types/src/ctap2/get_info.rs b/passkey-types/src/ctap2/get_info.rs index a092c2b..eae6c6d 100644 --- a/passkey-types/src/ctap2/get_info.rs +++ b/passkey-types/src/ctap2/get_info.rs @@ -19,7 +19,7 @@ serde_workaround! { pub versions: Vec, /// List of supported extensions. (Optional) - #[serde(rename = 0x02, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x02; default, skip_serializing_if = Option::is_none)] pub extensions: Option>, /// The claimed AAGUID. 16 bytes in length @@ -27,7 +27,7 @@ serde_workaround! { pub aaguid: Aaguid, /// List of supported options. - #[serde(rename = 0x04, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x04; default, skip_serializing_if = Option::is_none)] pub options: Option, /// Maximum message size supported by the authenticator. By default, authenticators MUST support @@ -35,20 +35,20 @@ serde_workaround! { /// /// This can be passed as a CBOR unsigned number so a u128 is used as the backing value since it /// is the maximum a CBOR number can represent. - #[serde(rename = 0x05, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x05; default, skip_serializing_if = Option::is_none)] pub max_msg_size: Option, /// List of supported PIN Protocol versions. /// /// If we ever end up with more than 256 pin protocols, an enhancement request should be filed. - #[serde(rename = 0x06, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x06; default, skip_serializing_if = Option::is_none)] pub pin_protocols: Option>, /// List of supported transports. Values are taken from the [`AuthenticatorTransport`] enum. /// The list MUST NOT include duplicate values nor be empty if present. /// Platforms MUST tolerate unknown values by ignoring them. #[serde( - rename = 0x09, + rename = 0x09; default, skip_serializing_if = Option::is_none, deserialize_with = ignore_unknown_opt_vec diff --git a/passkey-types/src/ctap2/make_credential.rs b/passkey-types/src/ctap2/make_credential.rs index 367386a..32f7628 100644 --- a/passkey-types/src/ctap2/make_credential.rs +++ b/passkey-types/src/ctap2/make_credential.rs @@ -55,34 +55,38 @@ serde_workaround! { /// are values that SHOULD be registered in the IANA COSE Algorithms registry /// [`coset::iana::Algorithm`]. This sequence is ordered from most preferred (by the RP) to least /// preferred. - #[serde(rename = 0x04)] + #[serde(rename = 0x04, deserialize_with = ignore_unknown_vec)] pub pub_key_cred_params: Vec, /// A sequence of [`PublicKeyCredentialDescriptor`] structures, as specified in [`webauthn`]. /// The authenticator returns an error if the authenticator already contains one of /// the credentials enumerated in this sequence. This allows RPs to limit the creation of /// multiple credentials for the same account on a single authenticator. - #[serde(rename = 0x05, default, skip_serializing_if = Option::is_none)] + #[serde( + rename = 0x05; + default, + skip_serializing_if = Option::is_none + )] pub exclude_list: Option>, /// Parameters to influence authenticator operation, as specified in [`webauthn`]. /// These parameters might be authenticator specific. - #[serde(rename = 0x06, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x06; default, skip_serializing_if = Option::is_none)] pub extensions: Option, /// Parameters to influence authenticator operation, see [`Options`] for more details. - #[serde(rename = 0x07, default)] + #[serde(rename = 0x07; default)] pub options: Options, /// First 16 bytes of HMAC-SHA-256 of clientDataHash using pinToken which platform got from /// the authenticator: HMAC-SHA-256(pinToken, clientDataHash). (NOT YET SUPPORTED) - #[serde(rename = 0x08, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x08; default, skip_serializing_if = Option::is_none)] pub pin_auth: Option, /// PIN protocol version chosen by the client /// /// if ever we hit more than 256 protocol versions, an enhacement request should be filed. - #[serde(rename = 0x09, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x09; default, skip_serializing_if = Option::is_none)] pub pin_protocol: Option, } } @@ -277,19 +281,19 @@ serde_workaround! { /// If `ep_att` is present and set to true, then an enterprise attestation was returned. /// /// Enterprise attestation is currently unsupported by this library. - #[serde(rename = 0x04, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x04; default, skip_serializing_if = Option::is_none)] pub ep_att: Option, /// Contains the `largeBlobKey` for the credential, if requested with the `largeBlobKey` extension. /// /// The `largeBlobKey` extension is currently unsupported by this library. - #[serde(rename = 0x05, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x05; default, skip_serializing_if = Option::is_none)] pub large_blob_key: Option, /// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any. /// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs. /// Clients MUST treat an empty map the same as an omitted field. - #[serde(rename = 0x06, default, skip_serializing_if = Option::is_none)] + #[serde(rename = 0x06; default, skip_serializing_if = Option::is_none)] pub unsigned_extension_outputs: Option, } } diff --git a/passkey-types/src/utils/serde_workaround.rs b/passkey-types/src/utils/serde_workaround.rs index 17329db..1794e26 100644 --- a/passkey-types/src/utils/serde_workaround.rs +++ b/passkey-types/src/utils/serde_workaround.rs @@ -11,7 +11,7 @@ macro_rules! serde_workaround { $(#[$attr:meta])* pub struct $name:ident {$( $(#[doc=$doc:literal])* - #[serde(rename = $discriminant:literal$(,$default:ident)?$(,skip_serializing_if = $method:path)?$(,deserialize_with = $de:path)?)] + #[serde(rename = $discriminant:literal$(;$default:ident)?$(,skip_serializing_if = $method:path)?$(,deserialize_with = $de:path)?)] $vis:vis $field:ident: $ty:ty, )*} ) => { @@ -171,10 +171,10 @@ macro_rules! serde_workaround { } macro_rules! serde_workaround_struct_len { - ($field:expr_2021; $skip_if:path) => { + ($field:expr; $skip_if:path) => { if $skip_if(&$field) { 0 } else { 1 } }; - ($field:expr_2021 ) => { + ($field:expr ) => { 1 }; } From 4a05ca3c637bf3920c2edb82d16e8ba0c33539b9 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 14:53:34 -0500 Subject: [PATCH 2/5] Update ctap types to ignore unknown values Just like their webauthn equivalents --- CHANGELOG.md | 2 + .../src/ctap2/extensions/hmac_secret.rs | 2 +- passkey-types/src/ctap2/extensions/prf.rs | 4 +- passkey-types/src/ctap2/get_assertion.rs | 16 +++- passkey-types/src/ctap2/make_credential.rs | 20 +++-- .../src/ctap2/make_credential/tests.rs | 79 +++++++++++++++++++ passkey-types/src/webauthn/attestation.rs | 4 +- passkey-types/src/webauthn/common.rs | 2 +- 8 files changed, 112 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b55f1b9..fe2c671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ - Support stringified booleans in webauthn requests (#67) - Be more tolerant to failed deserialization of optional vectors (#67) - ⚠ BREAKING: Add `username` and `user_display_name` to the `Passkey` type and its mock builder. (#87) +- Update CTAP2 types to ignore unknown values during deserialization, + just like their WebAuthn equivalents. (#88) ## Passkey v0.4.0 ### passkey-authenticator v0.4.0 diff --git a/passkey-types/src/ctap2/extensions/hmac_secret.rs b/passkey-types/src/ctap2/extensions/hmac_secret.rs index 6fedb4d..0d6c417 100644 --- a/passkey-types/src/ctap2/extensions/hmac_secret.rs +++ b/passkey-types/src/ctap2/extensions/hmac_secret.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; serde_workaround! { /// Object holding the initial salts for creating the secret. - #[derive(Debug, Clone)] + #[derive(Debug, Clone, PartialEq)] pub struct HmacGetSecretInput { /// Should be of form [`coset::CoseKey`] but that doesn't implement [`Serialize`] or [`Deserialize`]. #[serde(rename=0x01)] diff --git a/passkey-types/src/ctap2/extensions/prf.rs b/passkey-types/src/ctap2/extensions/prf.rs index b221f09..632cc8d 100644 --- a/passkey-types/src/ctap2/extensions/prf.rs +++ b/passkey-types/src/ctap2/extensions/prf.rs @@ -12,7 +12,7 @@ use crate::{Bytes, webauthn}; use crate::ctap2::{get_assertion, make_credential}; /// This struct is a more opiniated mirror of [`webauthn::AuthenticationExtensionsPrfInputs`]. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct AuthenticatorPrfInputs { /// See [`webauthn::AuthenticationExtensionsPrfInputs::eval`]. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -24,7 +24,7 @@ pub struct AuthenticatorPrfInputs { } /// This struct is a more opiniated mirror of [`webauthn::AuthenticationExtensionsPrfValues`]. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct AuthenticatorPrfValues { /// This is the already hashed values of [`webauthn::AuthenticationExtensionsPrfValues::first`]. pub first: [u8; 32], diff --git a/passkey-types/src/ctap2/get_assertion.rs b/passkey-types/src/ctap2/get_assertion.rs index 1407cfa..fa35dab 100644 --- a/passkey-types/src/ctap2/get_assertion.rs +++ b/passkey-types/src/ctap2/get_assertion.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::{ Bytes, ctap2::AuthenticatorData, + utils::serde::{ignore_unknown, ignore_unknown_opt_vec}, webauthn::{PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity}, }; @@ -37,7 +38,8 @@ serde_workaround! { #[serde( rename = 0x03; default, - skip_serializing_if = Option::is_none + skip_serializing_if = Option::is_none, + deserialize_with = ignore_unknown_opt_vec )] pub allow_list: Option>, @@ -46,7 +48,8 @@ serde_workaround! { #[serde( rename = 0x04; default, - skip_serializing_if = Option::is_none + skip_serializing_if = Option::is_none, + deserialize_with = ignore_unknown )] pub extensions: Option, @@ -74,7 +77,8 @@ pub struct ExtensionInputs { #[serde( rename = "hmac-secret", default, - skip_serializing_if = "Option::is_none" + skip_serializing_if = "Option::is_none", + deserialize_with = "ignore_unknown" )] pub hmac_secret: Option, @@ -83,7 +87,11 @@ pub struct ExtensionInputs { /// The output from a request using the `prf` extension will not be signed /// and will be un-encrypted. /// This input should already be hashed by the client. - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "ignore_unknown" + )] pub prf: Option, } diff --git a/passkey-types/src/ctap2/make_credential.rs b/passkey-types/src/ctap2/make_credential.rs index 32f7628..530022f 100644 --- a/passkey-types/src/ctap2/make_credential.rs +++ b/passkey-types/src/ctap2/make_credential.rs @@ -3,7 +3,12 @@ use ciborium::{Value, cbor}; use serde::{Deserialize, Serialize}; -use crate::{Bytes, ctap2::AuthenticatorData, webauthn}; +use crate::{ + Bytes, + ctap2::AuthenticatorData, + utils::serde::{ignore_unknown_opt_vec, ignore_unknown_vec}, + webauthn, +}; #[cfg(doc)] use crate::webauthn::{ @@ -15,7 +20,7 @@ use super::extensions::{AuthenticatorPrfInputs, AuthenticatorPrfMakeOutputs, Hma serde_workaround! { /// While similar in structure to [`PublicKeyCredentialCreationOptions`], /// it is not completely identical, namely the presence of the `options` key. - #[derive(Debug)] + #[derive(Debug, PartialEq)] pub struct Request { /// Hash of the ClientData contextual binding specified by host. #[serde(rename = 0x01)] @@ -65,7 +70,8 @@ serde_workaround! { #[serde( rename = 0x05; default, - skip_serializing_if = Option::is_none + skip_serializing_if = Option::is_none, + deserialize_with = ignore_unknown_opt_vec )] pub exclude_list: Option>, @@ -97,7 +103,7 @@ serde_workaround! { /// /// [WebAuthn]: https://w3c.github.io/webauthn/#dictdef-publickeycredentialrpentity /// [CTAP2]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PublicKeyCredentialRpEntity { /// The domain of the relying party pub id: String, @@ -107,7 +113,7 @@ pub struct PublicKeyCredentialRpEntity { } /// This is a copy of [`webauthn::PublicKeyCredentialUserEntity`] with differing optional fields. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PublicKeyCredentialUserEntity { /// The ID of the user pub id: Bytes, @@ -177,7 +183,7 @@ impl TryFrom for PublicKeyCredentialRpEnt } /// The options that control how an authenticator will behave. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Options { /// Specifies whether this credential is to be discoverable or not. #[serde(default)] @@ -207,7 +213,7 @@ const fn default_true() -> bool { } /// All supported Authenticator extensions inputs during credential creation -#[derive(Debug, Serialize, Deserialize, Default)] +#[derive(Debug, Serialize, Deserialize, Default, PartialEq)] pub struct ExtensionInputs { /// A boolean value to indicate that this extension is requested by the Relying Party /// diff --git a/passkey-types/src/ctap2/make_credential/tests.rs b/passkey-types/src/ctap2/make_credential/tests.rs index c9b70f4..423ef9a 100644 --- a/passkey-types/src/ctap2/make_credential/tests.rs +++ b/passkey-types/src/ctap2/make_credential/tests.rs @@ -1,3 +1,7 @@ +use ciborium::{Value, cbor}; + +use crate::{ctap2::get_assertion::Options, webauthn}; + #[test] fn windows_sanity_test() { // Note that this CBOR is incorrect, it encodes PublicKeyCredentialDescriptor::transport as @@ -89,3 +93,78 @@ fn windows_sanity_test() { .expect("There should be at least one entry"); assert!(first.transports.is_none()) } + +#[test] +fn deserialize_ignores_unknown_values() { + let raw_request = cbor!({ + 0x01 => Value::Bytes(vec![15,182,44,101,156,230,202,214,145,124,211,35,25,79,66,233,132,163,144,124,208,29,114,112,109,213,99,107,75,218,37,59]), + 0x02 => { + "id" => "webauthn.io", + "name" => "webauthn.io" + }, + 0x03 => { + "id" => Value::Bytes(b"webauthnio-test".to_vec()), + "name" => "test", + "displayName" => "test" + }, + 0x04 => [ + { + "alg" => -8, // EdDSA + "type" => "public-key" + }, + { + "alg" => -7, // ES256 + "type" => "public-key" + }, + { + "alg" => -50, // ML-DSA-87 + "type" => "public-key" + }, + { + "alg" => -65537, // Reserved for private use, should be ignored + "type" => "public-key" + } + ] + }).expect("Failed to hand-write cbor"); + + let request: super::Request = raw_request.deserialized().expect("Failed to ignore values"); + + assert_eq!( + request, + super::Request { + client_data_hash: vec![ + 15, 182, 44, 101, 156, 230, 202, 214, 145, 124, 211, 35, 25, 79, 66, 233, 132, 163, + 144, 124, 208, 29, 114, 112, 109, 213, 99, 107, 75, 218, 37, 59 + ] + .into(), + rp: super::PublicKeyCredentialRpEntity { + id: "webauthn.io".into(), + name: Some("webauthn.io".into()) + }, + user: webauthn::PublicKeyCredentialUserEntity { + id: b"webauthnio-test".to_vec().into(), + name: "test".into(), + display_name: "test".into(), + }, + pub_key_cred_params: vec![ + webauthn::PublicKeyCredentialParameters { + ty: webauthn::PublicKeyCredentialType::PublicKey, + alg: coset::iana::Algorithm::EdDSA + }, + webauthn::PublicKeyCredentialParameters { + ty: webauthn::PublicKeyCredentialType::PublicKey, + alg: coset::iana::Algorithm::ES256 + }, + webauthn::PublicKeyCredentialParameters { + ty: webauthn::PublicKeyCredentialType::PublicKey, + alg: coset::iana::Algorithm::ML_DSA_87 + } + ], + exclude_list: None, + extensions: None, + options: Options::default(), + pin_auth: None, + pin_protocol: None + } + ) +} diff --git a/passkey-types/src/webauthn/attestation.rs b/passkey-types/src/webauthn/attestation.rs index 6e1934c..12a49f1 100644 --- a/passkey-types/src/webauthn/attestation.rs +++ b/passkey-types/src/webauthn/attestation.rs @@ -223,7 +223,7 @@ pub struct PublicKeyCredentialRpEntity { /// [RFC8266]: https://www.rfc-editor.org/rfc/rfc8266 /// [RFC8264]: https://www.rfc-editor.org/rfc/rfc8264 /// [Lang]: https://w3c.github.io/webauthn/#sctn-strings-langdir -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "typeshare", typeshare)] pub struct PublicKeyCredentialUserEntity { @@ -275,7 +275,7 @@ pub struct PublicKeyCredentialUserEntity { /// This type is used to supply additional parameters when creating a new credential. /// /// -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "typeshare", typeshare)] pub struct PublicKeyCredentialParameters { /// This member specifies the type of credential to be created. The value SHOULD be a member of diff --git a/passkey-types/src/webauthn/common.rs b/passkey-types/src/webauthn/common.rs index a3b5a9c..d3a710a 100644 --- a/passkey-types/src/webauthn/common.rs +++ b/passkey-types/src/webauthn/common.rs @@ -41,7 +41,7 @@ pub enum PublicKeyCredentialType { /// It is recommended to ignore any credential whose type is [`PublicKeyCredentialType::Unknown`] /// /// -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "typeshare", typeshare)] pub struct PublicKeyCredentialDescriptor { /// This member contains the type of the public key credential the caller is referring to. The From 6b50904a4931f220022312edb8c931def2538529 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 15:05:12 -0500 Subject: [PATCH 3/5] Provide algorithms supported through get_info All the other fields from ctap2.2 have also been added to get_info for future use --- CHANGELOG.md | 3 + .../src/authenticator/get_info.rs | 21 +- passkey-authenticator/src/ctap2.rs | 4 +- passkey-client/src/tests/mod.rs | 2 + passkey-types/src/ctap2/get_info.rs | 403 +++++++++++++++++- passkey-types/src/ctap2/get_info/tests.rs | 11 +- passkey-types/src/webauthn/attestation.rs | 9 + 7 files changed, 442 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe2c671..3a4054a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ to a new `PasskeyAccessor` trait instead of the `TryInto`, making it possible to use a custom passkey representation type that goes throughout the entire flow without losing any additional information through a conversion. (#87) +- ⚠ BREAKING: The `Ctap2Api::get_info` method now returns a boxed response due to the size of + the response. (#88) ### passkey-client v0.5.0 @@ -37,6 +39,7 @@ - ⚠ BREAKING: Add `username` and `user_display_name` to the `Passkey` type and its mock builder. (#87) - Update CTAP2 types to ignore unknown values during deserialization, just like their WebAuthn equivalents. (#88) +- ⚠ BREAKING: Update `ctap2::get_info::Response` to have all the fields from ctap 2.2 (#88) ## Passkey v0.4.0 ### passkey-authenticator v0.4.0 diff --git a/passkey-authenticator/src/authenticator/get_info.rs b/passkey-authenticator/src/authenticator/get_info.rs index 5bbcc51..f893762 100644 --- a/passkey-authenticator/src/authenticator/get_info.rs +++ b/passkey-authenticator/src/authenticator/get_info.rs @@ -1,4 +1,7 @@ -use passkey_types::ctap2::get_info::{Options, Response, Version}; +use passkey_types::{ + ctap2::get_info::{Options, Response, Version}, + webauthn::PublicKeyCredentialParameters, +}; use crate::{ Authenticator, CredentialStore, UserValidationMethod, credential_store::DiscoverabilitySupport, @@ -7,9 +10,9 @@ use crate::{ impl Authenticator { /// Using this method, the host can request that the authenticator report a list of all /// supported protocol versions, supported extensions, AAGUID of the device, and its capabilities. - pub async fn get_info(&self) -> Response { - Response { - versions: vec![Version::FIDO_2_0, Version::U2F_V2], + pub async fn get_info(&self) -> Box { + Box::new(Response { + versions: vec![Version::FIDO_2_0, Version::FIDO_2_1], extensions: self.extensions.list_extensions(), aaguid: *self.aaguid(), options: Some(Options { @@ -22,6 +25,14 @@ impl Authenticator { max_msg_size: None, pin_protocols: None, transports: Some(self.transports.clone()), - } + algorithms: Some( + self.algs + .iter() + .copied() + .map(PublicKeyCredentialParameters::from) + .collect(), + ), + ..Default::default() + }) } } diff --git a/passkey-authenticator/src/ctap2.rs b/passkey-authenticator/src/ctap2.rs index bad121a..0016490 100644 --- a/passkey-authenticator/src/ctap2.rs +++ b/passkey-authenticator/src/ctap2.rs @@ -30,7 +30,7 @@ mod sealed { #[async_trait::async_trait] pub trait Ctap2Api: sealed::Sealed { /// Request to get the information of the authenticator and see what it supports. - async fn get_info(&self) -> get_info::Response; + async fn get_info(&self) -> Box; /// Request to create and save a new credential in the authenticator. async fn make_credential( @@ -51,7 +51,7 @@ where S: CredentialStore + Sync + Send, U: UserValidationMethod::PasskeyItem> + Sync + Send, { - async fn get_info(&self) -> get_info::Response { + async fn get_info(&self) -> Box { Authenticator::get_info(self).await } diff --git a/passkey-client/src/tests/mod.rs b/passkey-client/src/tests/mod.rs index 53984d0..c2c5b10 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -535,10 +535,12 @@ fn map_rk_maps_criteria_to_rk_bool() { up: true, plat: true, client_pin: None, + ..Default::default() }), max_msg_size: None, pin_protocols: None, transports: None, + ..Default::default() }; let client = Client::new(Authenticator::new( ctap2::Aaguid::new_empty(), diff --git a/passkey-types/src/ctap2/get_info.rs b/passkey-types/src/ctap2/get_info.rs index eae6c6d..0e9cbd4 100644 --- a/passkey-types/src/ctap2/get_info.rs +++ b/passkey-types/src/ctap2/get_info.rs @@ -3,13 +3,20 @@ use std::num::NonZeroU128; use serde::{Deserialize, Serialize}; -use crate::{utils::serde::ignore_unknown_opt_vec, webauthn::AuthenticatorTransport}; +use crate::{ + Bytes, + utils::serde::ignore_unknown_opt_vec, + webauthn::{AuthenticatorTransport, PublicKeyCredentialParameters}, +}; use super::Aaguid; +#[cfg(doc)] +use crate::webauthn; + serde_workaround! { /// An Authenticator's metadata and capabilities. - #[derive(Debug, PartialEq, Eq)] + #[derive(Debug, Default, PartialEq, Eq)] pub struct Response { /// List of supported versions. /// Supported versions are: @@ -54,6 +61,222 @@ serde_workaround! { deserialize_with = ignore_unknown_opt_vec )] pub transports: Option>, + + /// List of supported algorithms for credential generation, as specified in [webauthn]. + /// The array is ordered from most preferred to least preferred + /// and MUST NOT include duplicate entries nor be empty if present. + /// [`PublicKeyCredentialParameters`]' algorithm identifiers are values that + /// SHOULD be registered in the IANA COSE Algorithms registry [`coset::iana::Algorithm`]. + #[serde( + rename = 0x0A; + default, + skip_serializing_if = Option::is_none, + deserialize_with = ignore_unknown_opt_vec + )] + pub algorithms: Option>, + + /// The maximum size, in bytes, of the serialized large-blob array that this authenticator can store. + /// If the authenticatorLargeBlobs command is supported, this MUST be specified. + /// Otherwise it MUST NOT be. If specified, the value MUST be ≥ 1024. + /// Thus, 1024 bytes is the least amount of storage an authenticator must make available for + /// per-credential serialized large-blob arrays if it supports the large, per-credential blobs feature. + /// This value is not specified and not pertinent if the authenticator implements the largeBlob extension. + #[serde( + rename = 0x0B; + default, + skip_serializing_if = Option::is_none + )] + pub mac_serialized_large_blob_array: Option, + + /// If this member is: + /// * present and set to true + /// * getPinToken and getPinUvAuthTokenUsingPinWithPermissions will return errors + /// until after a successful PIN Change. + /// * present and set to false, or absent. + /// * no PIN Change is required. + #[serde( + rename = 0x0C; + default, + skip_serializing_if = Option::is_none + )] + pub force_pin_change: Option, + + /// This specifies the current minimum PIN length, in Unicode code points, + /// the authenticator enforces for ClientPIN. + /// This is applicable for ClientPIN only: + /// the minPINLength member MUST be absent if the clientPin option ID is absent; + /// it MUST be present if the authenticator supports authenticatorClientPIN. + #[serde( + rename = 0x0D; + default, + skip_serializing_if = Option::is_none + )] + pub min_pin_length: Option, + + /// Indicates the firmware version of the authenticator model identified by AAGUID. + /// Whenever releasing any code change to the authenticator firmware, authenticator MUST increase the version. + #[serde( + rename = 0x0E; + default, + skip_serializing_if = Option::is_none + )] + pub firmware_version: Option, + + /// Maximum credBlob length in bytes supported by the authenticator. + /// Must be present if, and only if, credBlob is included in the supported extensions list. + /// If present, this value MUST be at least 32 bytes. + #[serde( + rename = 0x0F; + default, + skip_serializing_if = Option::is_none + )] + pub max_cred_blob_length: Option, + + /// This specifies the max number of RP ID that the authenticator will accept via setMinPINLength subcommand. + /// The platform MUST NOT send more than this number of RP ID to the setMinPINLength subcommand. + /// This is in addition to pre-configured list authenticator may have. + /// If the authenticator does not support adding additional RP IDs, its value is 0. + /// This MUST ONLY be present if, and only if, the authenticator supports the setMinPINLength subcommand. + #[serde( + rename = 0x10; + default, + skip_serializing_if = Option::is_none + )] + pub max_rpids_for_set_min_pin_length: Option, + + /// This specifies the preferred number of invocations of the getPinUvAuthTokenUsingUvWithPermissions + /// subCommand the platform may attempt before falling back to the getPinUvAuthTokenUsingPinWithPermissions + /// subCommand or displaying an error. MUST be greater than zero. + /// If the value is 1 then all uvRetries are internal and the platform MUST only invoke the + /// getPinUvAuthTokenUsingUvWithPermissions subCommand a single time. + /// If the value is > 1 the authenticator MUST only decrement uvRetries by 1 for each iteration. + #[serde( + rename = 0x11; + default, + skip_serializing_if = Option::is_none + )] + pub preferred_platform_uv_attempts: Option, + + /// This specifies the user verification modality supported by the authenticator via authenticatorClientPIN’s + /// getPinUvAuthTokenUsingUvWithPermissions subcommand. This is a hint to help the platform construct user dialogs. + /// The values are defined in FIDORegistry Section 3.1 User Verification Methods. + /// Combining multiple bit-flags from the FIDORegistry is allowed. + /// If clientPin is supported it MUST NOT be included in the bit-flags, + /// as clientPIN is not a built-in user verification method. + #[serde( + rename = 0x12; + default, + skip_serializing_if = Option::is_none + )] + pub uv_modality: Option, + + // /// This specifies a list of [authenticator certifications][1]. + // /// The value should be a [`Value::Map`], but there is no way to define this in a consistent way in the type. + // /// + // /// [1]: https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-feature-descriptions-certifications + // #[serde( + // rename = 0x13, + // default, + // skip_serializing_if = Option::is_none + // )] + // pub certifications: Option, + + /// If this member is present it indicates the estimated number of additional discoverable credentials that can be stored. + /// If this value is zero then platforms SHOULD create non-discoverable credentials if possible. + #[serde( + rename = 0x14; + default, + skip_serializing_if = Option::is_none + )] + pub remaining_discoverable_credentials: Option, + + /// If present the authenticator supports the authenticatorConfig vendorPrototype subcommand, + /// and its value is a list of authenticatorConfig vendorCommandId values supported, which MAY be empty. + #[serde( + rename = 0x15; + default, + skip_serializing_if = Option::is_none, + deserialize_with = ignore_unknown_opt_vec + )] + pub vendor_prototype_config_commands: Option>, + + /// List of supported attestation formats. + /// Authenticators that support multiple attestation formats, not counting "none", MUST set this field. + /// Otherwise it is optional. + #[serde( + rename = 0x16; + default, + skip_serializing_if = Option::is_none, + deserialize_with = ignore_unknown_opt_vec + )] + pub attestation_formats: Option>, + + /// If present the number of internal User Verification operations since the last pin entry including all failed attempts. + /// This allows the platform to periodically prompt the user for PIN on a biometric device so they don’t forget the PIN. + /// This is optional platform behavior and the interval is at the discretion of the platform. + #[serde( + rename = 0x17; + default, + skip_serializing_if = Option::is_none + )] + pub uv_count_since_last_pin_entry: Option, + + /// If present the authenticator requires a 10 second touch for reset. + #[serde( + rename = 0x18; + default, + skip_serializing_if = Option::is_none + )] + pub long_touch_for_reset: Option, + + /// The value is a byte value containing iv || ct. + /// Where ct is the AES-128-CBC encryption of (128-bit device identifier) using + /// HKDF-SHA-256(salt = 32 zero bytes, IKM = persistentPinUvAuthToken, L = 16, info = "encIdentifier"). + /// The encryption iv must be regenerated for each output of getInfo. + #[serde( + rename = 0x19; + default, + skip_serializing_if = Option::is_none + )] + pub enc_identifier: Option, + + /// List of transports that support the reset command. + /// The list MUST NOT include duplicate values nor be empty if present. + /// Platforms MUST tolerate unknown values. + #[serde( + rename = 0x1A; + default, + skip_serializing_if = Option::is_none, + deserialize_with = ignore_unknown_opt_vec + )] + pub transports_for_reset: Option>, + + /// If present, whether the authenticator is enforcing an additional current PIN complexity policy beyond minPINLength. + /// PIN complexity policies for authenticators are listed in the FIDO MDS. + /// The authenticator may have a pre-configured PIN complexity policy value that is applied after a reset. + #[serde( + rename = 0x1B; + default, + skip_serializing_if = Option::is_none + )] + pub pin_complexity_policy: Option, + + /// If present, a URL that the platform can use to provide the user more information about the enforced PIN policy. + #[serde( + rename = 0x1C; + default, + skip_serializing_if = Option::is_none + )] + pub pin_complexity_policy_url: Option, + + /// This specifies the maximum PIN length, in Unicode code points, the authenticator enforces for ClientPIN. + /// An authenticator setting this value still MUST restrict the PIN to be represented in 63 or fewer bytes. + #[serde( + rename = 0x1D; + default, + skip_serializing_if = Option::is_none + )] + pub max_pin_length: Option, } } @@ -108,6 +331,166 @@ pub struct Options { /// it will return both "uv" and the Client PIN option. #[serde(default, skip_serializing_if = "Option::is_none")] pub uv: Option, + + /// If pinUvAuthToken is: + /// + /// `Some(true)`, then the authenticator supports authenticatorClientPIN’s getPinUvAuthTokenUsingPinWithPermissions subcommand. + /// If the uv option id is present and set to true, then the authenticator supports authenticatorClientPIN’s + /// getPinUvAuthTokenUsingUvWithPermissions subcommand. + /// + /// `Some(false)` or `None`, the authenticator does not support authenticatorClientPIN’s getPinUvAuthTokenUsingPinWithPermissions + /// and getPinUvAuthTokenUsingUvWithPermissions subcommands. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pin_uv_auth_token: Option, + + /// If this noMcGaPermissionsWithClientPin is: + /// + /// `Some(true)`, A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions + /// (or getPinToken) cannot be used for authenticatorMakeCredential or authenticatorGetAssertion commands, + /// because it will lack the necessary mc and ga permissions. + /// In this situation, platforms SHOULD NOT attempt to use getPinUvAuthTokenUsingPinWithPermissions if using + /// getPinUvAuthTokenUsingUvWithPermissions fails. + /// + /// `Some(false)` or `None`, A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions + /// (or getPinToken) can be used for authenticatorMakeCredential or authenticatorGetAssertion commands. + /// + /// Note: noMcGaPermissionsWithClientPin MUST only be present if the [`Self::client_pin`] field is present. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub no_mc_ga_permissions_with_client_pin: Option, + + /// If largeBlobs is: + /// `Some(true)`, the authenticator supports the authenticatorLargeBlobs command. + /// + /// `Some(false)` or `None`, The authenticatorLargeBlobs command is NOT supported. + /// + /// This option MUST NOT be set to true if the largeBlob extension is supported instead. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub large_blobs: Option, + + /// Enterprise Attestation feature support: + /// + /// If ep is `Some(true)`, The authenticator is enterprise attestation capable, + /// and enterprise attestation is enabled. + /// + /// If ep is `Some(false)`, The authenticator is enterprise attestation capable, + /// and enterprise attestation is disabled. + /// + /// If ep is `None`, The Enterprise Attestation feature is NOT supported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ep: Option, + + /// If bioEnroll is: + /// + /// `Some(true)`, the authenticator supports the authenticatorBioEnrollment commands, + /// and has at least one bio enrollment presently provisioned. + /// + /// `Some(false)`, the authenticator supports the authenticatorBioEnrollment commands, + /// and does not yet have any bio enrollments provisioned. + /// + /// `None`, the authenticatorBioEnrollment commands are NOT supported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bio_enroll: Option, + + /// "FIDO_2_1_PRE" Prototype Bio enrollment support: + /// + /// If is `Some(true)`, the authenticator supports the Prototype authenticatorBioEnrollment (0x40) commands, + /// and has at least one bio enrollment presently provisioned. + /// + /// If is `Some(false)`, the authenticator supports the Prototype authenticatorBioEnrollment (0x40) commands, + /// and does not yet have any bio enrollments provisioned. + /// + /// If is `None`, the Prototype authenticatorBioEnrollment (0x40) commands are not supported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_verification_mgmt_preview: Option, + + /// getPinUvAuthTokenUsingUvWithPermissions support for requesting the be permission. + /// This option ID MUST only be present if [`Self::bio_enroll`] is also present. + /// + /// If is `Some(true)`, requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions is supported. + /// + /// If is `Some(false)` or `None`, requesting the be permission when invoking + /// getPinUvAuthTokenUsingUvWithPermissions is NOT supported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub uv_bio_enroll: Option, + + /// authenticatorConfig command support: + /// + /// If is `Some(true)`, the authenticatorConfig command is supported. + /// + /// If is `Some(false)` or `None`, the authenticatorConfig command is NOT supported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub authnr_cfg: Option, + + /// getPinUvAuthTokenUsingUvWithPermissions support for requesting the acfg permission. + /// This option ID MUST only be present if [`Self::authnr_cfg`] is also present. + /// + /// If is `Some(true)`, requesting the acfg permission when invoking + /// getPinUvAuthTokenUsingUvWithPermissions is supported. + /// + /// If is `Some(false)` or `None`, requesting the acfg permission when invoking + /// getPinUvAuthTokenUsingUvWithPermissions is NOT supported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub uv_acfg: Option, + + /// Credential management support: + /// + /// If is `Some(true)`, the authenticatorCredentialManagement command is supported. + /// + /// If is `Some(false)` or `None`, the authenticatorCredentialManagement command is NOT supported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cred_mgmt: Option, + + /// Credential management Read Only support: + /// + /// If is `Some(true)`, requesting the pcmr permission when invoking + /// getPinUvAuthTokenUsingUvWithPermissions or getPinUvAuthTokenUsingPinWithPermissions is supported. + /// + /// If is `Some(false)` or `None`, requesting the pcmr permission when invoking + /// getPinUvAuthTokenUsingUvWithPermissions or getPinUvAuthTokenUsingPinWithPermissions is NOT supported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub credential_mgmt_preview: Option, + + /// Support for the Set Minimum PIN Length feature. + /// + /// If is `Some(true)`, the setMinPINLength subcommand is supported. + /// + /// If is `Some(false)` or `None`, the setMinPINLength subcommand is NOT supported. + /// + /// Note: setMinPINLength MUST only be present if the [`Self::client_pin`] option ID is present. + #[serde( + rename = "setMinPINLength", + default, + skip_serializing_if = "Option::is_none" + )] + pub set_min_pin_length: Option, + + /// Support for making non-discoverable credentials without requiring User Verification. + /// + /// If is `Some(true)`, the authenticator allows creation of non-discoverable credentials + /// without requiring any form of user verification, if the platform requests this behaviour. + /// + /// If is `Some(false)` or `None`, the authenticator requires some form of user verification + /// for creating non-discoverable credentials, regardless of the parameters the platform supplies + /// for the authenticatorMakeCredential command. + /// + /// Authenticators SHOULD include this option with the value true. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub make_cred_uv_not_rqd: Option, + + /// Support for the Always Require User Verification feature: + /// + /// If is `Some(true)`, the authenticator supports the Always Require User Verification feature + /// and it is enabled. + /// + /// If is `Some(false)`, the authenticator supports the Always Require User Verification feature + /// but it is disabled. + /// + /// If is `None`, the authenticator does not support the Always Require User Verification feature. + /// + /// Note: If the alwaysUv option ID is present and true the authenticator MUST set the value of + /// [`Self::make_cred_uv_not_rqd`] to false. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub always_uv: Option, } #[must_use] @@ -124,6 +507,20 @@ impl Default for Options { client_pin: None, up: true, uv: None, + pin_uv_auth_token: None, + no_mc_ga_permissions_with_client_pin: None, + large_blobs: None, + ep: None, + bio_enroll: None, + user_verification_mgmt_preview: None, + uv_bio_enroll: None, + authnr_cfg: None, + uv_acfg: None, + cred_mgmt: None, + credential_mgmt_preview: None, + set_min_pin_length: None, + make_cred_uv_not_rqd: None, + always_uv: None, } } } @@ -136,6 +533,8 @@ pub enum Version { U2F_V2, /// Client To Authenticator Protocol version 2.0 FIDO_2_0, + /// Client To Authenticator Protocol version 2.1 + FIDO_2_1, /// Unknown version catching the value #[serde(untagged)] Unknown(String), diff --git a/passkey-types/src/ctap2/get_info/tests.rs b/passkey-types/src/ctap2/get_info/tests.rs index 0f758f2..c479570 100644 --- a/passkey-types/src/ctap2/get_info/tests.rs +++ b/passkey-types/src/ctap2/get_info/tests.rs @@ -21,6 +21,7 @@ fn serialization_round_trip() { AuthenticatorTransport::Internal, AuthenticatorTransport::Hybrid, ]), + ..Default::default() }; let mut serialized = Vec::new(); ciborium::ser::into_writer(&expected, &mut serialized).expect("Could not serialize to cbor"); @@ -53,6 +54,7 @@ fn serialization_expected_wire_fmt() { AuthenticatorTransport::Internal, AuthenticatorTransport::Hybrid, ]), + ..Default::default() }; let mut serialized = Vec::new(); ciborium::ser::into_writer(&input, &mut serialized).expect("Could not serialize to cbor"); @@ -83,7 +85,7 @@ fn serialization_expected_wire_fmt() { #[test] fn unknown_transports_gets_ignored() { let input = cbor!({ - 0x01 => vec!["FIDO_2_0", "FIDO_2_1"], + 0x01 => vec!["FIDO_2_0", "FIDO_2_1", "FIDO_X_Y"], 0x02 => vec!["hmac-secret", "credProtect"], 0x03 => ciborium::value::Value::Bytes([0;16].into()), 0x04 => { @@ -106,7 +108,11 @@ fn unknown_transports_gets_ignored() { ciborium::de::from_reader(serialized.as_slice()).expect("Could not deserialize"); let expected = Response { - versions: vec![Version::FIDO_2_0, Version::Unknown("FIDO_2_1".into())], + versions: vec![ + Version::FIDO_2_0, + Version::FIDO_2_1, + Version::Unknown("FIDO_X_Y".into()), + ], extensions: Some(vec![ Extension::HmacSecret, Extension::Unknown("credProtect".into()), @@ -121,6 +127,7 @@ fn unknown_transports_gets_ignored() { max_msg_size: None, pin_protocols: Some(vec![1]), transports: Some(vec![AuthenticatorTransport::Hybrid]), + ..Default::default() }; assert_eq!(expected, deserialized); diff --git a/passkey-types/src/webauthn/attestation.rs b/passkey-types/src/webauthn/attestation.rs index 12a49f1..e853bb4 100644 --- a/passkey-types/src/webauthn/attestation.rs +++ b/passkey-types/src/webauthn/attestation.rs @@ -320,6 +320,15 @@ impl PublicKeyCredentialParameters { } } +impl From for PublicKeyCredentialParameters { + fn from(value: iana::Algorithm) -> Self { + Self { + ty: PublicKeyCredentialType::PublicKey, + alg: value, + } + } +} + /// [Relying Parties] may use this type to specify their requirements regarding authenticator attributes. /// /// From 614a0868fe74f7d1dc2af11edbc767d632b24a2b Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 15:08:49 -0500 Subject: [PATCH 4/5] Fix attestation format response in CTAP2 firefox is not happy with the capitalization of none, which is fair since its not spec compliant --- passkey-authenticator/src/authenticator/make_credential.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passkey-authenticator/src/authenticator/make_credential.rs b/passkey-authenticator/src/authenticator/make_credential.rs index d7f6b46..0edb65b 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -167,7 +167,7 @@ where .set_make_credential_extensions(extensions.signed)?; let response = Response { - fmt: "None".into(), + fmt: "none".into(), auth_data, att_stmt: coset::cbor::value::Value::Map(vec![]), ep_att: None, From a0d6fcb9021bf6704b8b8fdf081b51bf011b7f46 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 15:10:12 -0500 Subject: [PATCH 5/5] update management of missing hmac credentials to fail silently rather than error out. --- .../src/authenticator/extensions/hmac_secret.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs b/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs index 5c91fd2..68f7de3 100644 --- a/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs +++ b/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs @@ -3,7 +3,7 @@ use std::ops::Not; use passkey_types::{ crypto::hmac_sha256, ctap2::{ - Ctap2Error, StatusCode, U2FError, + Ctap2Error, StatusCode, extensions::{ AuthenticatorPrfGetOutputs, AuthenticatorPrfInputs, AuthenticatorPrfMakeOutputs, AuthenticatorPrfValues, HmacSecretSaltOrOutput, @@ -150,7 +150,9 @@ impl Authenticator { return Ok(None); }; - let hmac_creds = passkey_ext.ok_or(U2FError::InvalidParameter)?; + let Some(hmac_creds) = passkey_ext else { + return Ok(None); + }; let Some(request) = select_salts(credential_id, salts) else { return Ok(None);