From d78778a0a9c2e8bc885720774b92383fb182c1a1 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 15:19:40 -0500 Subject: [PATCH 1/5] Improve deserialization code and trim whitespace seen in incoming base64 values --- passkey-types/src/utils/bytes.rs | 2 ++ passkey-types/src/utils/serde.rs | 17 +++++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/passkey-types/src/utils/bytes.rs b/passkey-types/src/utils/bytes.rs index 5c6fd64..b58ada8 100644 --- a/passkey-types/src/utils/bytes.rs +++ b/passkey-types/src/utils/bytes.rs @@ -143,6 +143,8 @@ impl<'de> Deserialize<'de> for Bytes { where E: serde::de::Error, { + // There have been some whitespace seen in incoming base64 encodings. + let v = v.trim(); v.try_into().map_err(|_| { E::invalid_value( serde::de::Unexpected::Str(v), diff --git a/passkey-types/src/utils/serde.rs b/passkey-types/src/utils/serde.rs index 59b3a05..6b40f84 100644 --- a/passkey-types/src/utils/serde.rs +++ b/passkey-types/src/utils/serde.rs @@ -83,7 +83,6 @@ where Ok(de .deserialize_seq(IgnoreUnknown(std::marker::PhantomData)) .unwrap_or_default()) - // de.deserialize_seq(IgnoreUnknown(std::marker::PhantomData)) } pub(crate) fn ignore_unknown_vec<'de, D, T>(de: D) -> Result, D::Error> @@ -141,15 +140,12 @@ where where E: Error, { - match FromStr::from_str(v) { - Ok(v) => Ok(v), - _ => { - if let Ok(v) = f64::from_str(v) { - self.visit_f64(v) - } else { - Err(E::custom("Was not a stringified number")) - } - } + if let Ok(v) = FromStr::from_str(v) { + Ok(v) + } else if let Ok(v) = f64::from_str(v) { + self.visit_f64(v) + } else { + Err(E::custom("Was not a stringified number")) } } @@ -279,5 +275,6 @@ where { de.deserialize_any(StringOrBool) } + #[cfg(test)] mod tests; From 9ac2d51e69d3683de05b120e93e8e80ff483f5ed Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 15:20:52 -0500 Subject: [PATCH 2/5] use fully qualified path for ciborium value --- passkey-types/src/ctap2/make_credential.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/passkey-types/src/ctap2/make_credential.rs b/passkey-types/src/ctap2/make_credential.rs index 530022f..fc1a8ba 100644 --- a/passkey-types/src/ctap2/make_credential.rs +++ b/passkey-types/src/ctap2/make_credential.rs @@ -1,6 +1,6 @@ //! -use ciborium::{Value, cbor}; +use ciborium::cbor; use serde::{Deserialize, Serialize}; use crate::{ @@ -11,8 +11,11 @@ use crate::{ }; #[cfg(doc)] -use crate::webauthn::{ - CollectedClientData, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor, +use { + crate::webauthn::{ + CollectedClientData, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor, + }, + ciborium::value::Value, }; use super::extensions::{AuthenticatorPrfInputs, AuthenticatorPrfMakeOutputs, HmacGetSecretInput}; @@ -280,7 +283,7 @@ serde_workaround! { // TODO: Change to a flattened enum when `content, type` serde enums can use numbers as // the keys #[serde(rename = 0x03)] - pub att_stmt: Value, + pub att_stmt: ciborium::value::Value, /// Indicates whether an enterprise attestation was returned for this credential. /// If `ep_att` is absent or present and set to false, then an enterprise attestation was not returned. @@ -323,7 +326,7 @@ impl Response { "fmt" => "none", "attStmt" => {}, // Explicitly define these fields as bytes since specialization is still fairly far - "authData" => Value::Bytes(self.auth_data.to_vec()), + "authData" => ciborium::value::Value::Bytes(self.auth_data.to_vec()), }) .unwrap(); ciborium::ser::into_writer(&attestation_object_value, &mut attestation_object).unwrap(); From 3e852b15898e88c53f510d662724b024a44efecf Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 15:21:31 -0500 Subject: [PATCH 3/5] Return an error if ever we fail serialization rather than unwrap --- passkey-client/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/passkey-client/src/lib.rs b/passkey-client/src/lib.rs index 2fd0982..c3c4d32 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -80,6 +80,8 @@ pub enum WebauthnError { RedirectError, /// Related Origins endpoint contains a number of labels exceeding the max limit ExceedsMaxLabelLimit, + /// JSON serialization error + SerializationError, } impl WebauthnError { @@ -253,8 +255,8 @@ where unknown_keys: Default::default(), }; - // SAFETY: it is a developer error if serializing this struct fails. - let client_data_json = serde_json::to_string(&collected_client_data).unwrap(); + let client_data_json = serde_json::to_string(&collected_client_data) + .map_err(|_| WebauthnError::SerializationError)?; let client_data_json_hash = client_data .client_data_hash() .unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec()); @@ -374,8 +376,8 @@ where unknown_keys: Default::default(), }; - // SAFETY: it is a developer error if serializing this struct fails. - let client_data_json = serde_json::to_string(&collected_client_data).unwrap(); + let client_data_json = serde_json::to_string(&collected_client_data) + .map_err(|_| WebauthnError::SerializationError)?; let client_data_json_hash = client_data .client_data_hash() .unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec()); From 170aa1934bf370f9dd8f30a9a11612bdeb536976 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 15:21:59 -0500 Subject: [PATCH 4/5] add some space between tests --- passkey-authenticator/src/authenticator/make_credential/tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/passkey-authenticator/src/authenticator/make_credential/tests.rs b/passkey-authenticator/src/authenticator/make_credential/tests.rs index b76b789..eddcff5 100644 --- a/passkey-authenticator/src/authenticator/make_credential/tests.rs +++ b/passkey-authenticator/src/authenticator/make_credential/tests.rs @@ -440,6 +440,7 @@ async fn make_credential_returns_err_when_rk_is_requested_but_not_supported() { // Assert assert_eq!(err, Ctap2Error::UnsupportedOption.into()); } + #[tokio::test] async fn empty_store_with_exclude_credentials_succeeds() { // This test verifies the fix for the issue where an empty credential store From 3675189e2327ab353dcd41ba0204366ad54afa04 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 29 Dec 2025 15:43:36 -0500 Subject: [PATCH 5/5] remove quirks file which wasnt referenced as a module anymore --- passkey-client/src/quirks.rs | 64 ------------------------------------ 1 file changed, 64 deletions(-) delete mode 100644 passkey-client/src/quirks.rs diff --git a/passkey-client/src/quirks.rs b/passkey-client/src/quirks.rs deleted file mode 100644 index 2950263..0000000 --- a/passkey-client/src/quirks.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! The goal of this module is to address quirks with RP's different implementations. -//! We don't want to limit this library's functionality for all RPs because of only -//! a few RPs misbehave. - -use passkey_types::webauthn::CreatedPublicKeyCredential; - -/// List of quirky RPs, the default is [`Self::NotQuirky`] which maps to being a no-op -#[derive(Default)] -pub(crate) enum QuirkyRp { - /// The RP is not known to be quirky, thus the mapping methods will be no-ops. - #[default] - NotQuirky, - - /// Adobe crashes on their server when they encounter the key - /// [credProps.authenticatorDisplayName][adn] during key creation. - /// - /// RP_IDs: - /// * `adobe.com` - /// - /// [adn]: https://w3c.github.io/webauthn/#dom-credentialpropertiesoutput-authenticatordisplayname - Adobe, - - /// Hyatt returns an "invalid request" error when they encounter the key - /// [credProps.authenticatorDisplayName][adn] during key creation. - /// - /// RP_IDs: - /// * `hyatt.com` - /// - /// [adn]: https://w3c.github.io/webauthn/#dom-credentialpropertiesoutput-authenticatordisplayname - Hyatt, -} - -impl QuirkyRp { - pub fn from_rp_id(rp_id: &str) -> Self { - match rp_id { - "adobe.com" => QuirkyRp::Adobe, - "hyatt.com" => QuirkyRp::Hyatt, - _ => QuirkyRp::NotQuirky, - } - } - - /// Use this after creating the response but before returning it to the function caller - #[inline] - pub fn map_create_credential( - &self, - response: CreatedPublicKeyCredential, - ) -> CreatedPublicKeyCredential { - match self { - // no-op - Self::NotQuirky => response, - Self::Adobe | Self::Hyatt => remove_authenticator_display_name(response), - } - } -} - -#[inline] -fn remove_authenticator_display_name( - mut response: CreatedPublicKeyCredential, -) -> CreatedPublicKeyCredential { - if let Some(cp) = response.client_extension_results.cred_props.as_mut() { - cp.authenticator_display_name = None; - } - response -}