commit cba1fad2f62181c5847e5e2ebef7f4e8e7983617
parent 242d7971fba90bff1b6d03e3e5f18f220b11d3ff
Author: Zack Newman <zack@philomathiclife.com>
Date: Wed, 25 Mar 2026 14:48:56 -0600
add ml-dsa keys. use box instead of vec
Diffstat:
23 files changed, 22626 insertions(+), 500 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -118,6 +118,7 @@ targets = [
base64url_nopad = { version = "0.1.4", default-features = false }
ed25519-dalek = { version = "2.2.0", default-features = false }
hashbrown = { version = "0.16.1", default-features = false }
+ml-dsa = { version = "0.1.0-rc.7", default-features = false }
p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] }
p384 = { version = "0.13.1", default-features = false, features = ["ecdsa"] }
precis-profiles = { version = "0.1.13", default-features = false }
@@ -130,8 +131,10 @@ url = { version = "2.5.8", default-features = false }
[dev-dependencies]
base64url_nopad = { version = "0.1.4", default-features = false, features = ["alloc"] }
ed25519-dalek = { version = "2.2.0", default-features = false, features = ["alloc", "pkcs8"] }
+ml-dsa = { version = "0.1.0-rc.7", default-features = false, features = ["alloc", "pkcs8"] }
p256 = { version = "0.13.2", default-features = false, features = ["pem"] }
p384 = { version = "0.13.1", default-features = false, features = ["pkcs8"] }
+pkcs8 = { version = "0.11.0-rc.11", default-features = false }
serde_json = { version = "1.0.149", default-features = false, features = ["preserve_order"] }
diff --git a/src/bin.rs b/src/bin.rs
@@ -71,6 +71,17 @@ impl EncodeBufferFallible for [u8] {
})
}
}
+// We don't implement `EncodeBuffer` for `Box<T>` since we only ever need `Box<[u8]>`; and one can specialize
+// the implementation such that it's _a lot_ faster than a generic `T`.
+impl EncodeBufferFallible for Box<[u8]> {
+ type Err = EncDecErr;
+ /// # Errors
+ ///
+ /// See [`[u8]::encode_into_buffer`].
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ (&self).encode_into_buffer(buffer)
+ }
+}
// We don't implement `EncodeBuffer` for `Vec<T>` since we only ever need `Vec<u8>`; and one can specialize
// the implementation such that it's _a lot_ faster than a generic `T`.
impl EncodeBufferFallible for Vec<u8> {
@@ -301,6 +312,14 @@ impl<'a> DecodeBuffer<'a> for &'a [u8] {
})
}
}
+// We don't implement `DecodeBuffer` for `Box<T>` since we only ever need `Box<[u8]>`; and one can specialize
+// the implementation such that it's _a lot_ faster than a generic `T`.
+impl<'a> DecodeBuffer<'a> for Box<[u8]> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ <&[u8]>::decode_from_buffer(data).map(Self::from)
+ }
+}
// We don't implement `DecodeBuffer` for `Vec<T>` since we only ever need `Vec<u8>`; and one can specialize
// the implementation such that it's _a lot_ faster than a generic `T`.
impl<'a> DecodeBuffer<'a> for Vec<u8> {
diff --git a/src/lib.rs b/src/lib.rs
@@ -282,7 +282,7 @@
//! ) -> Result<
//! Option<(
//! PublicKeyCredentialUserEntity64<'name, 'display_name, '_>,
-//! Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+//! Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
//! )>,
//! AppErr,
//! > {
diff --git a/src/request.rs b/src/request.rs
@@ -83,7 +83,7 @@ use url::Url as Uri;
/// # let mut creds = AllowedCredentials::new();
/// # creds.push(
/// # PublicKeyCredentialDescriptor {
-/// # id: CredentialId::try_from(vec![0; CRED_ID_MIN_LEN])?,
+/// # id: CredentialId::try_from(vec![0; CRED_ID_MIN_LEN].into_boxed_slice())?,
/// # transports: AuthTransports::NONE,
/// # }
/// # .into(),
@@ -165,7 +165,7 @@ pub mod error;
/// /// an empty `Vec` should be passed.
/// fn get_registered_credentials(
/// user: &UserHandle64,
-/// ) -> Result<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, AggErr> {
+/// ) -> Result<Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>, AggErr> {
/// // ⋮
/// # Ok(Vec::new())
/// }
@@ -1307,7 +1307,7 @@ impl From<Backup> for BackupReq {
/// # #[cfg(feature = "custom")]
/// fn get_excluded_credentials<const LEN: usize>(
/// user_handle: &UserHandle<LEN>,
-/// ) -> Vec<PublicKeyCredentialDescriptor<Vec<u8>>> {
+/// ) -> Vec<PublicKeyCredentialDescriptor<Box<[u8]>>> {
/// get_credentials(user_handle)
/// }
/// /// Used to fetch the excluded `PublicKeyCredentialDescriptor`s associated with `user_handle` during
@@ -1317,7 +1317,7 @@ impl From<Backup> for BackupReq {
/// fn get_credentials<const LEN: usize, T>(user_handle: &UserHandle<LEN>) -> T
/// where
/// T: Credentials,
-/// PublicKeyCredentialDescriptor<Vec<u8>>: Into<T::Credential>,
+/// PublicKeyCredentialDescriptor<Box<[u8]>>: Into<T::Credential>,
/// {
/// let iter = get_cred_parts(user_handle);
/// let len = iter.size_hint().0;
@@ -1337,10 +1337,10 @@ impl From<Backup> for BackupReq {
/// # #[cfg(feature = "custom")]
/// fn get_cred_parts<const LEN: usize>(
/// user_handle: &UserHandle<LEN>,
-/// ) -> impl Iterator<Item = (CredentialId<Vec<u8>>, AuthTransports)> {
+/// ) -> impl Iterator<Item = (CredentialId<Box<[u8]>>, AuthTransports)> {
/// // ⋮
/// # [(
-/// # CredentialId::try_from(vec![0; 16]).unwrap(),
+/// # CredentialId::try_from(vec![0; 16].into_boxed_slice()).unwrap(),
/// # AuthTransports::NONE,
/// # )]
/// # .into_iter()
@@ -1717,8 +1717,9 @@ mod tests {
AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation,
AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputs,
ClientExtensionsOutputsStaticState, CompressedP256PubKey, CompressedP384PubKey,
- CompressedPubKey, CredentialProtectionPolicy, DynamicState, Ed25519PubKey,
- Registration, RsaPubKey, StaticState, UncompressedPubKey,
+ CompressedPubKeyOwned, CredentialProtectionPolicy, DynamicState, Ed25519PubKey,
+ MlDsa44PubKey, MlDsa65PubKey, MlDsa87PubKey, Registration, RsaPubKey,
+ StaticState, UncompressedPubKey,
},
},
},
@@ -1738,6 +1739,11 @@ mod tests {
#[cfg(feature = "custom")]
use ed25519_dalek::{Signer as _, SigningKey};
#[cfg(feature = "custom")]
+ use ml_dsa::{
+ MlDsa44, MlDsa65, MlDsa87, Signature as MlDsaSignature, SigningKey as MlDsaSigKey,
+ signature::Signer as _,
+ };
+ #[cfg(feature = "custom")]
use p256::{
ecdsa::{DerSignature as P256DerSig, SigningKey as P256Key},
elliptic_curve::sec1::Tag,
@@ -2129,15 +2135,6806 @@ mod tests {
]
.as_slice(),
);
- attestation_object.extend_from_slice(&Sha256::digest(client_data_json.as_slice()));
- let sig_key = SigningKey::from_bytes(&[0; 32]);
- let ver_key = sig_key.verifying_key();
- let pub_key = ver_key.as_bytes();
- attestation_object[107..139].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes()));
- attestation_object[188..220].copy_from_slice(pub_key);
- let sig = sig_key.sign(&attestation_object[107..]);
- attestation_object[32..96].copy_from_slice(sig.to_bytes().as_slice());
- attestation_object.truncate(261);
+ attestation_object.extend_from_slice(&Sha256::digest(client_data_json.as_slice()));
+ let sig_key = SigningKey::from_bytes(&[0; 32]);
+ let ver_key = sig_key.verifying_key();
+ let pub_key = ver_key.as_bytes();
+ attestation_object[107..139].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes()));
+ attestation_object[188..220].copy_from_slice(pub_key);
+ let sig = sig_key.sign(&attestation_object[107..]);
+ attestation_object[32..96].copy_from_slice(sig.to_bytes().as_slice());
+ attestation_object.truncate(261);
+ assert!(matches!(opts.start_ceremony()?.0.verify(
+ RP_ID,
+ &Registration {
+ response: AuthenticatorAttestation::new(
+ client_data_json,
+ attestation_object,
+ AuthTransports::NONE,
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ client_extension_results: ClientExtensionsOutputs {
+ cred_props: None,
+ prf: Some(AuthenticationExtensionsPrfOutputs { enabled: true, }),
+ },
+ },
+ &RegistrationVerificationOptions::<&str, &str>::default(),
+ )?.static_state.credential_public_key, UncompressedPubKey::Ed25519(k) if k.into_inner() == pub_key));
+ Ok(())
+ }
+ #[expect(clippy::panic_in_result_fn, reason = "OK in tests")]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ #[cfg(feature = "custom")]
+ fn eddsa_auth() -> Result<(), AggErr> {
+ let mut creds = AllowedCredentials::with_capacity(1);
+ _ = creds.push(AllowedCredential {
+ credential: PublicKeyCredentialDescriptor {
+ id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
+ transports: AuthTransports::NONE,
+ },
+ extension: CredentialSpecificExtension {
+ prf: Some(PrfInputOwned {
+ first: Vec::new(),
+ second: Some(Vec::new()),
+ ext_req: ExtensionReq::Require,
+ }),
+ },
+ });
+ let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(RP_ID, creds);
+ opts.options.user_verification = UserVerificationRequirement::Required;
+ opts.options.challenge = Challenge(0);
+ opts.options.extensions = AuthExt { prf: None };
+ let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
+ let mut authenticator_data = Vec::with_capacity(164);
+ authenticator_data.extend_from_slice(
+ [
+ // rpIdHash.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // flags.
+ // UP, UV, and ED (right-to-left).
+ 0b1000_0101,
+ // signCount.
+ // 0 as 32-bit big endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ CBOR_MAP | 1,
+ CBOR_TEXT | 11,
+ b'h',
+ b'm',
+ b'a',
+ b'c',
+ b'-',
+ b's',
+ b'e',
+ b'c',
+ b'r',
+ b'e',
+ b't',
+ CBOR_BYTES | 24,
+ // Length is 80.
+ 80,
+ // Two HMAC outputs concatenated and encrypted.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ authenticator_data[..32].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes()));
+ authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice()));
+ let ed_priv = SigningKey::from([0; 32]);
+ let sig = ed_priv.sign(authenticator_data.as_slice()).to_vec();
+ authenticator_data.truncate(132);
+ assert!(!opts.start_ceremony()?.0.verify(
+ RP_ID,
+ &NonDiscoverableAuthentication {
+ raw_id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
+ response: NonDiscoverableAuthenticatorAssertion::with_user(
+ client_data_json,
+ authenticator_data,
+ sig,
+ UserHandle::from([0]),
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ &mut AuthenticatedCredential::new(
+ CredentialId::try_from([0; 16].as_slice())?,
+ &UserHandle::from([0]),
+ StaticState {
+ credential_public_key: CompressedPubKeyOwned::Ed25519(Ed25519PubKey::from(
+ ed_priv.verifying_key().to_bytes()
+ ),),
+ extensions: AuthenticatorExtensionOutputStaticState {
+ cred_protect: CredentialProtectionPolicy::None,
+ hmac_secret: Some(true),
+ },
+ client_extension_results: ClientExtensionsOutputsStaticState {
+ prf: Some(AuthenticationExtensionsPrfOutputs { enabled: true }),
+ }
+ },
+ DynamicState {
+ user_verified: true,
+ backup: Backup::NotEligible,
+ sign_count: 0,
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ )?,
+ &AuthenticationVerificationOptions::<&str, &str>::default(),
+ )?);
+ Ok(())
+ }
+ #[expect(clippy::panic_in_result_fn, reason = "OK in tests")]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ #[cfg(feature = "custom")]
+ fn mldsa87_reg() -> Result<(), AggErr> {
+ let id = UserHandle::from([0]);
+ let mut opts = CredentialCreationOptions::passkey(
+ RP_ID,
+ PublicKeyCredentialUserEntity {
+ name: "foo".try_into()?,
+ id: &id,
+ display_name: DisplayName::Blank,
+ },
+ Vec::new(),
+ );
+ opts.public_key.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
+ let mut attestation_object = Vec::with_capacity(2736);
+ attestation_object.extend_from_slice(
+ [
+ CBOR_MAP | 3,
+ CBOR_TEXT | 3,
+ b'f',
+ b'm',
+ b't',
+ CBOR_TEXT | 4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ CBOR_TEXT | 7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ CBOR_MAP,
+ CBOR_TEXT | 8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ CBOR_BYTES | 25,
+ 10,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // ML-DSA-87 COSE key.
+ CBOR_MAP | 3,
+ // COSE kty.
+ CBOR_UINT | 1,
+ // COSE AKP
+ CBOR_UINT | 7,
+ // COSE alg.
+ CBOR_UINT | 3,
+ CBOR_NEG | 24,
+ // COSE ML-DSA-87.
+ 49,
+ // `pub`.
+ CBOR_NEG,
+ CBOR_BYTES | 25,
+ // Length is 2592 as 16-bit big-endian.
+ 10,
+ 32,
+ // Encoded key.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ]
+ .as_slice(),
+ );
+ attestation_object[31..63].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes()));
+ assert!(matches!(opts.start_ceremony()?.0.verify(
+ RP_ID,
+ &Registration {
+ response: AuthenticatorAttestation::new(
+ client_data_json,
+ attestation_object,
+ AuthTransports::NONE,
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ client_extension_results: ClientExtensionsOutputs {
+ cred_props: None,
+ prf: None,
+ },
+ },
+ &RegistrationVerificationOptions::<&str, &str>::default(),
+ )?.static_state.credential_public_key, UncompressedPubKey::MlDsa87(k) if **k.inner() == [1; 2592]));
+ Ok(())
+ }
+ #[expect(
+ clippy::panic_in_result_fn,
+ clippy::unwrap_in_result,
+ clippy::unwrap_used,
+ reason = "OK in tests"
+ )]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[test]
+ #[cfg(feature = "custom")]
+ fn mldsa87_auth() -> Result<(), AggErr> {
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(RP_ID);
+ opts.public_key.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
+ let mut authenticator_data = Vec::with_capacity(69);
+ authenticator_data.extend_from_slice(
+ [
+ // rpIdHash.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // flags.
+ // UP and UV (right-to-left).
+ 0b0000_0101,
+ // signCount.
+ // 0 as 32-bit big endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ authenticator_data[..32].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes()));
+ authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice()));
+ let mldsa87_key = MlDsaSigKey::<MlDsa87>::from_seed((&[0; 32]).into());
+ let sig: MlDsaSignature<MlDsa87> = mldsa87_key.sign(authenticator_data.as_slice());
+ let pub_key = mldsa87_key.verifying_key().encode();
+ authenticator_data.truncate(37);
+ assert!(!opts.start_ceremony()?.0.verify(
+ RP_ID,
+ &DiscoverableAuthentication {
+ raw_id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
+ response: DiscoverableAuthenticatorAssertion::new(
+ client_data_json,
+ authenticator_data,
+ sig.encode().0.to_vec(),
+ UserHandle::from([0]),
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ &mut AuthenticatedCredential::new(
+ CredentialId::try_from([0; 16].as_slice())?,
+ &UserHandle::from([0]),
+ StaticState {
+ credential_public_key: CompressedPubKeyOwned::MlDsa87(
+ MlDsa87PubKey::try_from(Box::from(pub_key.as_slice())).unwrap()
+ ),
+ extensions: AuthenticatorExtensionOutputStaticState {
+ cred_protect: CredentialProtectionPolicy::None,
+ hmac_secret: None,
+ },
+ client_extension_results: ClientExtensionsOutputsStaticState { prf: None }
+ },
+ DynamicState {
+ user_verified: true,
+ backup: Backup::NotEligible,
+ sign_count: 0,
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ )?,
+ &AuthenticationVerificationOptions::<&str, &str>::default(),
+ )?);
+ Ok(())
+ }
+ #[expect(clippy::panic_in_result_fn, reason = "OK in tests")]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ #[cfg(feature = "custom")]
+ fn mldsa65_reg() -> Result<(), AggErr> {
+ let id = UserHandle::from([0]);
+ let mut opts = CredentialCreationOptions::passkey(
+ RP_ID,
+ PublicKeyCredentialUserEntity {
+ name: "foo".try_into()?,
+ id: &id,
+ display_name: DisplayName::Blank,
+ },
+ Vec::new(),
+ );
+ opts.public_key.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
+ let mut attestation_object = Vec::with_capacity(2096);
+ attestation_object.extend_from_slice(
+ [
+ CBOR_MAP | 3,
+ CBOR_TEXT | 3,
+ b'f',
+ b'm',
+ b't',
+ CBOR_TEXT | 4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ CBOR_TEXT | 7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ CBOR_MAP,
+ CBOR_TEXT | 8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ CBOR_BYTES | 25,
+ 7,
+ 241,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // ML-DSA-65 COSE key.
+ CBOR_MAP | 3,
+ // COSE kty.
+ CBOR_UINT | 1,
+ // COSE AKP
+ CBOR_UINT | 7,
+ // COSE alg.
+ CBOR_UINT | 3,
+ CBOR_NEG | 24,
+ // COSE ML-DSA-65.
+ 48,
+ // `pub`.
+ CBOR_NEG,
+ CBOR_BYTES | 25,
+ // Length is 1952 as 16-bit big-endian.
+ 7,
+ 160,
+ // Encoded key.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ]
+ .as_slice(),
+ );
+ attestation_object[31..63].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes()));
+ assert!(matches!(opts.start_ceremony()?.0.verify(
+ RP_ID,
+ &Registration {
+ response: AuthenticatorAttestation::new(
+ client_data_json,
+ attestation_object,
+ AuthTransports::NONE,
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ client_extension_results: ClientExtensionsOutputs {
+ cred_props: None,
+ prf: None,
+ },
+ },
+ &RegistrationVerificationOptions::<&str, &str>::default(),
+ )?.static_state.credential_public_key, UncompressedPubKey::MlDsa65(k) if **k.inner() == [1; 1952]));
+ Ok(())
+ }
+ #[expect(
+ clippy::panic_in_result_fn,
+ clippy::unwrap_in_result,
+ clippy::unwrap_used,
+ reason = "OK in tests"
+ )]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[test]
+ #[cfg(feature = "custom")]
+ fn mldsa65_auth() -> Result<(), AggErr> {
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(RP_ID);
+ opts.public_key.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
+ let mut authenticator_data = Vec::with_capacity(69);
+ authenticator_data.extend_from_slice(
+ [
+ // rpIdHash.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // flags.
+ // UP and UV (right-to-left).
+ 0b0000_0101,
+ // signCount.
+ // 0 as 32-bit big endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ authenticator_data[..32].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes()));
+ authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice()));
+ let mldsa65_key = MlDsaSigKey::<MlDsa65>::from_seed((&[0; 32]).into());
+ let sig: MlDsaSignature<MlDsa65> = mldsa65_key.sign(authenticator_data.as_slice());
+ let pub_key = mldsa65_key.verifying_key().encode();
+ authenticator_data.truncate(37);
+ assert!(!opts.start_ceremony()?.0.verify(
+ RP_ID,
+ &DiscoverableAuthentication {
+ raw_id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
+ response: DiscoverableAuthenticatorAssertion::new(
+ client_data_json,
+ authenticator_data,
+ sig.encode().0.to_vec(),
+ UserHandle::from([0]),
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ &mut AuthenticatedCredential::new(
+ CredentialId::try_from([0; 16].as_slice())?,
+ &UserHandle::from([0]),
+ StaticState {
+ credential_public_key: CompressedPubKeyOwned::MlDsa65(
+ MlDsa65PubKey::try_from(Box::from(pub_key.as_slice())).unwrap()
+ ),
+ extensions: AuthenticatorExtensionOutputStaticState {
+ cred_protect: CredentialProtectionPolicy::None,
+ hmac_secret: None,
+ },
+ client_extension_results: ClientExtensionsOutputsStaticState { prf: None }
+ },
+ DynamicState {
+ user_verified: true,
+ backup: Backup::NotEligible,
+ sign_count: 0,
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ )?,
+ &AuthenticationVerificationOptions::<&str, &str>::default(),
+ )?);
+ Ok(())
+ }
+ #[expect(clippy::panic_in_result_fn, reason = "OK in tests")]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ #[cfg(feature = "custom")]
+ fn mldsa44_reg() -> Result<(), AggErr> {
+ let id = UserHandle::from([0]);
+ let mut opts = CredentialCreationOptions::passkey(
+ RP_ID,
+ PublicKeyCredentialUserEntity {
+ name: "foo".try_into()?,
+ id: &id,
+ display_name: DisplayName::Blank,
+ },
+ Vec::new(),
+ );
+ opts.public_key.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
+ let mut attestation_object = Vec::with_capacity(1456);
+ attestation_object.extend_from_slice(
+ [
+ CBOR_MAP | 3,
+ CBOR_TEXT | 3,
+ b'f',
+ b'm',
+ b't',
+ CBOR_TEXT | 4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ CBOR_TEXT | 7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ CBOR_MAP,
+ CBOR_TEXT | 8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ CBOR_BYTES | 25,
+ 5,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // ML-DSA-44 COSE key.
+ CBOR_MAP | 3,
+ // COSE kty.
+ CBOR_UINT | 1,
+ // COSE AKP
+ CBOR_UINT | 7,
+ // COSE alg.
+ CBOR_UINT | 3,
+ CBOR_NEG | 24,
+ // COSE ML-DSA-44.
+ 47,
+ // `pub`.
+ CBOR_NEG,
+ CBOR_BYTES | 25,
+ // Length is 1312 as 16-bit big-endian.
+ 5,
+ 32,
+ // Encoded key.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ]
+ .as_slice(),
+ );
+ attestation_object[31..63].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes()));
assert!(matches!(opts.start_ceremony()?.0.verify(
RP_ID,
&Registration {
@@ -2149,40 +8946,28 @@ mod tests {
authenticator_attachment: AuthenticatorAttachment::None,
client_extension_results: ClientExtensionsOutputs {
cred_props: None,
- prf: Some(AuthenticationExtensionsPrfOutputs { enabled: true, }),
+ prf: None,
},
},
&RegistrationVerificationOptions::<&str, &str>::default(),
- )?.static_state.credential_public_key, UncompressedPubKey::Ed25519(k) if k.into_inner() == pub_key));
+ )?.static_state.credential_public_key, UncompressedPubKey::MlDsa44(k) if **k.inner() == [1; 1312]));
Ok(())
}
- #[expect(clippy::panic_in_result_fn, reason = "OK in tests")]
+ #[expect(
+ clippy::panic_in_result_fn,
+ clippy::unwrap_in_result,
+ clippy::unwrap_used,
+ reason = "OK in tests"
+ )]
#[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
- #[expect(clippy::too_many_lines, reason = "a lot to test")]
#[test]
#[cfg(feature = "custom")]
- fn eddsa_auth() -> Result<(), AggErr> {
- let mut creds = AllowedCredentials::with_capacity(1);
- _ = creds.push(AllowedCredential {
- credential: PublicKeyCredentialDescriptor {
- id: CredentialId::try_from(vec![0; 16])?,
- transports: AuthTransports::NONE,
- },
- extension: CredentialSpecificExtension {
- prf: Some(PrfInputOwned {
- first: Vec::new(),
- second: Some(Vec::new()),
- ext_req: ExtensionReq::Require,
- }),
- },
- });
- let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(RP_ID, creds);
- opts.options.user_verification = UserVerificationRequirement::Required;
- opts.options.challenge = Challenge(0);
- opts.options.extensions = AuthExt { prf: None };
+ fn mldsa44_auth() -> Result<(), AggErr> {
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(RP_ID);
+ opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
- let mut authenticator_data = Vec::with_capacity(164);
+ let mut authenticator_data = Vec::with_capacity(69);
authenticator_data.extend_from_slice(
[
// rpIdHash.
@@ -2220,127 +9005,31 @@ mod tests {
0,
0,
// flags.
- // UP, UV, and ED (right-to-left).
- 0b1000_0101,
+ // UP and UV (right-to-left).
+ 0b0000_0101,
// signCount.
// 0 as 32-bit big endian.
0,
0,
0,
0,
- CBOR_MAP | 1,
- CBOR_TEXT | 11,
- b'h',
- b'm',
- b'a',
- b'c',
- b'-',
- b's',
- b'e',
- b'c',
- b'r',
- b'e',
- b't',
- CBOR_BYTES | 24,
- // Length is 80.
- 80,
- // Two HMAC outputs concatenated and encrypted.
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
]
.as_slice(),
);
authenticator_data[..32].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes()));
authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice()));
- let ed_priv = SigningKey::from([0; 32]);
- let sig = ed_priv.sign(authenticator_data.as_slice()).to_vec();
- authenticator_data.truncate(132);
+ let mldsa44_key = MlDsaSigKey::<MlDsa44>::from_seed((&[0; 32]).into());
+ let sig: MlDsaSignature<MlDsa44> = mldsa44_key.sign(authenticator_data.as_slice());
+ let pub_key = mldsa44_key.verifying_key().encode();
+ authenticator_data.truncate(37);
assert!(!opts.start_ceremony()?.0.verify(
RP_ID,
- &NonDiscoverableAuthentication {
- raw_id: CredentialId::try_from(vec![0; 16])?,
- response: NonDiscoverableAuthenticatorAssertion::with_user(
+ &DiscoverableAuthentication {
+ raw_id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
+ response: DiscoverableAuthenticatorAssertion::new(
client_data_json,
authenticator_data,
- sig,
+ sig.encode().0.to_vec(),
UserHandle::from([0]),
),
authenticator_attachment: AuthenticatorAttachment::None,
@@ -2349,16 +9038,14 @@ mod tests {
CredentialId::try_from([0; 16].as_slice())?,
&UserHandle::from([0]),
StaticState {
- credential_public_key: CompressedPubKey::<_, &[u8], &[u8], &[u8]>::Ed25519(
- Ed25519PubKey::from(ed_priv.verifying_key().to_bytes()),
+ credential_public_key: CompressedPubKeyOwned::MlDsa44(
+ MlDsa44PubKey::try_from(Box::from(pub_key.as_slice())).unwrap()
),
extensions: AuthenticatorExtensionOutputStaticState {
cred_protect: CredentialProtectionPolicy::None,
- hmac_secret: Some(true),
+ hmac_secret: None,
},
- client_extension_results: ClientExtensionsOutputsStaticState {
- prf: Some(AuthenticationExtensionsPrfOutputs { enabled: true }),
- }
+ client_extension_results: ClientExtensionsOutputsStaticState { prf: None }
},
DynamicState {
user_verified: true,
@@ -2714,7 +9401,7 @@ mod tests {
assert!(!opts.start_ceremony()?.0.verify(
RP_ID,
&DiscoverableAuthentication {
- raw_id: CredentialId::try_from(vec![0; 16])?,
+ raw_id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
response: DiscoverableAuthenticatorAssertion::new(
client_data_json,
authenticator_data,
@@ -2727,12 +9414,12 @@ mod tests {
CredentialId::try_from([0; 16].as_slice())?,
&UserHandle::from([0]),
StaticState {
- credential_public_key: CompressedPubKey::<&[u8], _, &[u8], &[u8]>::P256(
- CompressedP256PubKey::from((
+ credential_public_key: CompressedPubKeyOwned::P256(CompressedP256PubKey::from(
+ (
(*pub_key.x().unwrap()).into(),
pub_key.tag() == Tag::CompressedOddY
- )),
- ),
+ )
+ ),),
extensions: AuthenticatorExtensionOutputStaticState {
cred_protect: CredentialProtectionPolicy::None,
hmac_secret: None,
@@ -3129,7 +9816,7 @@ mod tests {
assert!(!opts.start_ceremony()?.0.verify(
RP_ID,
&DiscoverableAuthentication {
- raw_id: CredentialId::try_from(vec![0; 16])?,
+ raw_id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
response: DiscoverableAuthenticatorAssertion::new(
client_data_json,
authenticator_data,
@@ -3142,12 +9829,12 @@ mod tests {
CredentialId::try_from([0; 16].as_slice())?,
&UserHandle::from([0]),
StaticState {
- credential_public_key: CompressedPubKey::<&[u8], &[u8], _, &[u8]>::P384(
- CompressedP384PubKey::from((
+ credential_public_key: CompressedPubKeyOwned::P384(CompressedP384PubKey::from(
+ (
(*pub_key.x().unwrap()).into(),
pub_key.tag() == Tag::CompressedOddY
- )),
- ),
+ )
+ ),),
extensions: AuthenticatorExtensionOutputStaticState {
cred_protect: CredentialProtectionPolicy::None,
hmac_secret: None,
@@ -3817,40 +10504,46 @@ mod tests {
let rsa_pub = rsa_key.verifying_key();
let sig = rsa_key.sign(authenticator_data.as_slice()).to_vec();
authenticator_data.truncate(37);
- assert!(!opts.start_ceremony()?.0.verify(
- RP_ID,
- &DiscoverableAuthentication {
- raw_id: CredentialId::try_from(vec![0; 16])?,
- response: DiscoverableAuthenticatorAssertion::new(
- client_data_json,
- authenticator_data,
- sig,
- UserHandle::from([0]),
- ),
- authenticator_attachment: AuthenticatorAttachment::None,
- },
- &mut AuthenticatedCredential::new(
- CredentialId::try_from([0; 16].as_slice())?,
- &UserHandle::from([0]),
- StaticState {
- credential_public_key: CompressedPubKey::<&[u8], &[u8], &[u8], _>::Rsa(
- RsaPubKey::try_from((rsa_pub.as_ref().n().to_bytes_be(), e)).unwrap(),
+ assert!(
+ !opts.start_ceremony()?.0.verify(
+ RP_ID,
+ &DiscoverableAuthentication {
+ raw_id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
+ response: DiscoverableAuthenticatorAssertion::new(
+ client_data_json,
+ authenticator_data,
+ sig,
+ UserHandle::from([0]),
),
- extensions: AuthenticatorExtensionOutputStaticState {
- cred_protect: CredentialProtectionPolicy::None,
- hmac_secret: None,
- },
- client_extension_results: ClientExtensionsOutputsStaticState { prf: None }
- },
- DynamicState {
- user_verified: true,
- backup: Backup::NotEligible,
- sign_count: 0,
authenticator_attachment: AuthenticatorAttachment::None,
},
- )?,
- &AuthenticationVerificationOptions::<&str, &str>::default(),
- )?);
+ &mut AuthenticatedCredential::new(
+ CredentialId::try_from([0; 16].as_slice())?,
+ &UserHandle::from([0]),
+ StaticState {
+ credential_public_key: CompressedPubKeyOwned::Rsa(
+ RsaPubKey::try_from((
+ rsa_pub.as_ref().n().to_bytes_be().into_boxed_slice(),
+ e
+ ))
+ .unwrap(),
+ ),
+ extensions: AuthenticatorExtensionOutputStaticState {
+ cred_protect: CredentialProtectionPolicy::None,
+ hmac_secret: None,
+ },
+ client_extension_results: ClientExtensionsOutputsStaticState { prf: None }
+ },
+ DynamicState {
+ user_verified: true,
+ backup: Backup::NotEligible,
+ sign_count: 0,
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ )?,
+ &AuthenticationVerificationOptions::<&str, &str>::default(),
+ )?
+ );
Ok(())
}
}
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -154,20 +154,20 @@ pub struct CredentialSpecificExtension {
#[derive(Clone, Debug)]
pub struct AllowedCredential {
/// The registered credential.
- pub credential: PublicKeyCredentialDescriptor<Vec<u8>>,
+ pub credential: PublicKeyCredentialDescriptor<Box<[u8]>>,
/// Credential-specific extensions.
pub extension: CredentialSpecificExtension,
}
-impl From<PublicKeyCredentialDescriptor<Vec<u8>>> for AllowedCredential {
+impl From<PublicKeyCredentialDescriptor<Box<[u8]>>> for AllowedCredential {
#[inline]
- fn from(credential: PublicKeyCredentialDescriptor<Vec<u8>>) -> Self {
+ fn from(credential: PublicKeyCredentialDescriptor<Box<[u8]>>) -> Self {
Self {
credential,
extension: CredentialSpecificExtension::default(),
}
}
}
-impl From<AllowedCredential> for PublicKeyCredentialDescriptor<Vec<u8>> {
+impl From<AllowedCredential> for PublicKeyCredentialDescriptor<Box<[u8]>> {
#[inline]
fn from(credential: AllowedCredential) -> Self {
credential.credential
@@ -220,13 +220,13 @@ impl Credentials for AllowedCredentials {
/// // likely never needed since the `CredentialId` was originally sent from the client and is likely
/// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let transports = get_transports((&id).into())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into()));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let id_copy = CredentialId::try_from(vec![0; 16])?;
+ /// let id_copy = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let transports_2 = AuthTransports::NONE;
/// // Duplicate `CredentialId`s don't get added.
@@ -274,23 +274,24 @@ impl AsRef<[AllowedCredential]> for AllowedCredentials {
self.creds.as_slice()
}
}
-impl From<&AllowedCredentials> for Vec<CredInfo> {
+impl From<&AllowedCredentials> for Box<[CredInfo]> {
#[inline]
fn from(value: &AllowedCredentials) -> Self {
let len = value.creds.len();
value
.creds
.iter()
- .fold(Self::with_capacity(len), |mut creds, cred| {
+ .fold(Vec::with_capacity(len), |mut creds, cred| {
creds.push(CredInfo {
id: cred.credential.id.clone(),
ext: (&cred.extension).into(),
});
creds
})
+ .into_boxed_slice()
}
}
-impl From<AllowedCredentials> for Vec<PublicKeyCredentialDescriptor<Vec<u8>>> {
+impl From<AllowedCredentials> for Vec<PublicKeyCredentialDescriptor<Box<[u8]>>> {
#[inline]
fn from(value: AllowedCredentials) -> Self {
let mut creds = Self::with_capacity(value.creds.len());
@@ -300,9 +301,9 @@ impl From<AllowedCredentials> for Vec<PublicKeyCredentialDescriptor<Vec<u8>>> {
creds
}
}
-impl From<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>> for AllowedCredentials {
+impl From<Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>> for AllowedCredentials {
#[inline]
- fn from(value: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>) -> Self {
+ fn from(value: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>) -> Self {
let mut creds = Self::with_capacity(value.len());
value.into_iter().fold((), |(), credential| {
_ = creds.push(AllowedCredential {
@@ -461,7 +462,7 @@ impl<'rp_id, 'prf_first, 'prf_second>
/// // likely never needed since the `CredentialId` was originally sent from the client and is likely
/// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let transports = get_transports((&id).into())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
@@ -529,7 +530,7 @@ impl<'rp_id, 'prf_first, 'prf_second>
extensions: self.options.extensions.into(),
expiration,
},
- allow_credentials: Vec::from(&self.allow_credentials),
+ allow_credentials: Box::from(&self.allow_credentials),
},
NonDiscoverableAuthenticationClientState(self),
)
@@ -895,7 +896,7 @@ fn validate_extensions(
#[derive(Debug)]
struct CredInfo {
/// The Credential ID.
- id: CredentialId<Vec<u8>>,
+ id: CredentialId<Box<[u8]>>,
/// Any credential-specific extensions.
ext: ServerCredSpecificExtensionInfo,
}
@@ -1097,6 +1098,9 @@ impl DiscoverableAuthenticationServerState {
const USER_LEN: usize,
O: PartialEq<Origin<'a>>,
T: PartialEq<Origin<'a>>,
+ MlDsa87Key: AsRef<[u8]>,
+ MlDsa65Key: AsRef<[u8]>,
+ MlDsa44Key: AsRef<[u8]>,
EdKey: AsRef<[u8]>,
P256Key: AsRef<[u8]>,
P384Key: AsRef<[u8]>,
@@ -1109,14 +1113,14 @@ impl DiscoverableAuthenticationServerState {
'_,
'_,
USER_LEN,
- CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>,
+ CompressedPubKey<MlDsa87Key, MlDsa65Key, MlDsa44Key, EdKey, P256Key, P384Key, RsaKey>,
>,
options: &AuthenticationVerificationOptions<'_, '_, O, T>,
) -> Result<bool, AuthCeremonyErr> {
// Step 6 item 2.
if cred.user_id == response.response.user_handle() {
// Step 6 item 2.
- if cred.id == response.raw_id {
+ if cred.id.as_ref() == response.raw_id.as_ref() {
self.0.verify(rp_id, response, cred, options, None)
} else {
Err(AuthCeremonyErr::CredentialIdMismatch)
@@ -1131,7 +1135,7 @@ impl DiscoverableAuthenticationServerState {
}
}
// This is essentially the `NonDiscoverableCredentialRequestOptions` used to create it; however to reduce
-// memory usage, we remove all unnecessary data making an instance of this as small as 80 bytes in size on
+// memory usage, we remove all unnecessary data making an instance of this as small as 64 bytes in size on
// `x86_64-unknown-linux-gnu` platforms. This does not include the size of each `CredInfo` which should exist
// elsewhere on the heap but obviously contributes memory overall.
/// State needed to be saved when beginning the authentication ceremony.
@@ -1148,7 +1152,7 @@ pub struct NonDiscoverableAuthenticationServerState {
/// Most server state.
state: AuthenticationServerState,
/// The set of credentials that are allowed.
- allow_credentials: Vec<CredInfo>,
+ allow_credentials: Box<[CredInfo]>,
}
impl NonDiscoverableAuthenticationServerState {
/// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true`
@@ -1171,6 +1175,9 @@ impl NonDiscoverableAuthenticationServerState {
const USER_LEN: usize,
O: PartialEq<Origin<'a>>,
T: PartialEq<Origin<'a>>,
+ MlDsa87Key: AsRef<[u8]>,
+ MlDsa65Key: AsRef<[u8]>,
+ MlDsa44Key: AsRef<[u8]>,
EdKey: AsRef<[u8]>,
P256Key: AsRef<[u8]>,
P384Key: AsRef<[u8]>,
@@ -1183,7 +1190,7 @@ impl NonDiscoverableAuthenticationServerState {
'_,
'_,
USER_LEN,
- CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>,
+ CompressedPubKey<MlDsa87Key, MlDsa65Key, MlDsa44Key, EdKey, P256Key, P384Key, RsaKey>,
>,
options: &AuthenticationVerificationOptions<'_, '_, O, T>,
) -> Result<bool, AuthCeremonyErr> {
@@ -1207,7 +1214,7 @@ impl NonDiscoverableAuthenticationServerState {
.ok_or(AuthCeremonyErr::NoMatchingAllowedCredential)
.and_then(|c| {
// Step 6 item 1.
- if c.id == cred.id {
+ if c.id.as_ref() == cred.id.as_ref() {
self.state
.verify(rp_id, response, cred, options, Some(c.ext))
} else {
@@ -1268,6 +1275,9 @@ impl AuthenticationServerState {
const DISCOVERABLE: bool,
O: PartialEq<Origin<'a>>,
T: PartialEq<Origin<'a>>,
+ MlDsa87Key: AsRef<[u8]>,
+ MlDsa65Key: AsRef<[u8]>,
+ MlDsa44Key: AsRef<[u8]>,
EdKey: AsRef<[u8]>,
P256Key: AsRef<[u8]>,
P384Key: AsRef<[u8]>,
@@ -1280,7 +1290,7 @@ impl AuthenticationServerState {
'_,
'_,
USER_LEN,
- CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>,
+ CompressedPubKey<MlDsa87Key, MlDsa65Key, MlDsa44Key, EdKey, P256Key, P384Key, RsaKey>,
>,
options: &AuthenticationVerificationOptions<'_, '_, O, T>,
cred_ext: Option<ServerCredSpecificExtensionInfo>,
@@ -1612,13 +1622,14 @@ mod tests {
auth::{DiscoverableAuthenticatorAssertion, HmacSecret},
register::{
AuthenticationExtensionsPrfOutputs, AuthenticatorExtensionOutputStaticState,
- ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, Ed25519PubKey,
+ ClientExtensionsOutputsStaticState, CompressedPubKeyOwned,
+ CredentialProtectionPolicy, Ed25519PubKey,
},
},
},
AuthCeremonyErr, AuthenticationVerificationOptions, AuthenticatorAttachment,
- AuthenticatorAttachmentEnforcement, CompressedPubKey, DiscoverableAuthentication,
- ExtensionErr, OneOrTwo, PrfInput, SignatureCounterEnforcement,
+ AuthenticatorAttachmentEnforcement, DiscoverableAuthentication, ExtensionErr, OneOrTwo,
+ PrfInput, SignatureCounterEnforcement,
};
#[cfg(all(
feature = "custom",
@@ -1659,7 +1670,7 @@ mod tests {
let mut creds = AllowedCredentials::with_capacity(1);
_ = creds.push(AllowedCredential {
credential: PublicKeyCredentialDescriptor {
- id: CredentialId::try_from(vec![0; 16])?,
+ id: CredentialId::try_from(vec![0; 16].into_boxed_slice())?,
transports: AuthTransports::NONE,
},
extension: CredentialSpecificExtension {
@@ -1748,16 +1759,11 @@ mod tests {
json
}
#[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
- #[expect(clippy::type_complexity, reason = "fine")]
#[expect(clippy::too_many_lines, reason = "a lot to test")]
#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
fn generate_authenticator_data_public_key_sig(
opts: TestResponseOptions,
- ) -> (
- Vec<u8>,
- CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>,
- Vec<u8>,
- ) {
+ ) -> (Vec<u8>, CompressedPubKeyOwned, Vec<u8>) {
let mut authenticator_data = Vec::with_capacity(256);
authenticator_data.extend_from_slice(
[
@@ -1877,7 +1883,7 @@ mod tests {
authenticator_data.truncate(len);
(
authenticator_data,
- CompressedPubKey::Ed25519(Ed25519PubKey::from(sig_key.verifying_key().to_bytes())),
+ CompressedPubKeyOwned::Ed25519(Ed25519PubKey::from(sig_key.verifying_key().to_bytes())),
sig,
)
}
@@ -1887,7 +1893,7 @@ mod tests {
let user = UserHandle::from([0; 1]);
let (authenticator_data, credential_public_key, signature) =
generate_authenticator_data_public_key_sig(options.response);
- let credential_id = CredentialId::try_from(vec![0; 16])?;
+ let credential_id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
let authentication = DiscoverableAuthentication::new(
credential_id.clone(),
DiscoverableAuthenticatorAssertion::new(
diff --git a/src/request/auth/ser.rs b/src/request/auth/ser.rs
@@ -53,7 +53,7 @@ impl Serialize for AllowedCredential {
/// // likely never needed since the `CredentialId` was originally sent from the client and is likely
/// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let transports = get_transports((&id).into())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
@@ -98,7 +98,7 @@ impl Serialize for AllowedCredentials {
/// // likely never needed since the `CredentialId` was originally sent from the client and is likely
/// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let transports = get_transports((&id).into())?;
/// let mut creds = AllowedCredentials::with_capacity(1);
@@ -428,7 +428,7 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_, '_> {
/// // likely never needed since the `CredentialId` was originally sent from the client and is likely
/// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let transports = get_transports((&id).into())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
diff --git a/src/request/auth/ser_server_state.rs b/src/request/auth/ser_server_state.rs
@@ -62,7 +62,7 @@ impl EncodeBuffer for CredInfo {
impl<'a> DecodeBuffer<'a> for CredInfo {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- CredentialId::<Vec<u8>>::decode_from_buffer(data).and_then(|id| {
+ CredentialId::<Box<[u8]>>::decode_from_buffer(data).and_then(|id| {
ServerCredSpecificExtensionInfo::decode_from_buffer(data).map(|ext| Self { id, ext })
})
}
@@ -115,16 +115,16 @@ impl EncodeBufferFallible for &[CredInfo] {
})
}
}
-impl<'a> DecodeBuffer<'a> for Vec<CredInfo> {
+impl<'a> DecodeBuffer<'a> for Box<[CredInfo]> {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
u16::decode_from_buffer(data).and_then(|len| {
let l = usize::from(len);
- let mut creds = Self::with_capacity(l);
+ let mut creds = Vec::with_capacity(l);
while creds.len() < l {
creds.push(CredInfo::decode_from_buffer(data)?);
}
- Ok(creds)
+ Ok(creds.into_boxed_slice())
})
}
}
@@ -214,7 +214,7 @@ impl Encode for NonDiscoverableAuthenticationServerState {
.map_err(EncodeNonDiscoverableAuthenticationServerStateErr::SystemTime)
.and_then(|()| {
self.allow_credentials
- .as_slice()
+ .as_ref()
.encode_into_buffer(&mut buffer)
.map_err(|_e| {
EncodeNonDiscoverableAuthenticationServerStateErr::AllowedCredentialsCount
@@ -304,7 +304,7 @@ impl Decode for NonDiscoverableAuthenticationServerState {
AuthenticationServerState::decode_from_buffer(&mut input)
.map_err(|_e| DecodeNonDiscoverableAuthenticationServerStateErr::Other)
.and_then(|state| {
- Vec::decode_from_buffer(&mut input)
+ Box::<[_]>::decode_from_buffer(&mut input)
.map_err(|_e| DecodeNonDiscoverableAuthenticationServerStateErr::Other)
.and_then(|allow_credentials| {
if allow_credentials.is_empty() {
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -688,12 +688,19 @@ impl Ord for Username<'_> {
///
/// Note the order of variants is the following:
///
-/// [`Self::Eddsa`] `<` [`Self::Es256`] `<` [`Self::Es384`] `<` [`Self::Rs256`].
+/// [`Self::Mldsa87`] `<` [`Self::Mldsa65`] `<` [`Self::Mldsa44`] `<` [`Self::Eddsa`] `<` [`Self::Es256`]
+/// `<` [`Self::Es384`] `<` [`Self::Rs256`].
///
/// This is relevant for [`CoseAlgorithmIdentifiers`]. For example a `CoseAlgorithmIdentifiers`
-/// that contains `Self::Eddsa` will prioritize it over all others.
+/// that contains `Self::Mldsa87` will prioritize it over all others.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum CoseAlgorithmIdentifier {
+ /// [ML-DSA-87](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+ Mldsa87,
+ /// [ML-DSA-65](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+ Mldsa65,
+ /// [ML-DSA-44](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+ Mldsa44,
/// [EdDSA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
///
/// Note that Ed25519 must be used for the `crv` parameter
@@ -716,10 +723,13 @@ impl CoseAlgorithmIdentifier {
/// Transforms `self` into a `u8`.
const fn to_u8(self) -> u8 {
match self {
- Self::Eddsa => 1,
- Self::Es256 => 2,
- Self::Es384 => 4,
- Self::Rs256 => 8,
+ Self::Mldsa87 => 0x1,
+ Self::Mldsa65 => 0x2,
+ Self::Mldsa44 => 0x4,
+ Self::Eddsa => 0x8,
+ Self::Es256 => 0x10,
+ Self::Es384 => 0x20,
+ Self::Rs256 => 0x40,
}
}
}
@@ -741,6 +751,9 @@ pub struct CoseAlgorithmIdentifiers(u8);
impl CoseAlgorithmIdentifiers {
/// Contains all [`CoseAlgorithmIdentifier`]s.
pub const ALL: Self = Self(0)
+ .add(CoseAlgorithmIdentifier::Mldsa87)
+ .add(CoseAlgorithmIdentifier::Mldsa65)
+ .add(CoseAlgorithmIdentifier::Mldsa44)
.add(CoseAlgorithmIdentifier::Eddsa)
.add(CoseAlgorithmIdentifier::Es256)
.add(CoseAlgorithmIdentifier::Es384)
@@ -771,6 +784,9 @@ impl CoseAlgorithmIdentifiers {
/// Validates `other` is allowed based on `self`.
const fn validate(self, other: UncompressedPubKey<'_>) -> Result<(), RegCeremonyErr> {
if match other {
+ UncompressedPubKey::MlDsa87(_) => self.contains(CoseAlgorithmIdentifier::Mldsa87),
+ UncompressedPubKey::MlDsa65(_) => self.contains(CoseAlgorithmIdentifier::Mldsa65),
+ UncompressedPubKey::MlDsa44(_) => self.contains(CoseAlgorithmIdentifier::Mldsa44),
UncompressedPubKey::Ed25519(_) => self.contains(CoseAlgorithmIdentifier::Eddsa),
UncompressedPubKey::P256(_) => self.contains(CoseAlgorithmIdentifier::Es256),
UncompressedPubKey::P384(_) => self.contains(CoseAlgorithmIdentifier::Es384),
@@ -1594,7 +1610,7 @@ impl<
pub fn passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>(
rp_id: &'a RpId,
user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
) -> Self {
Self {
mediation: CredentialMediationRequirement::default(),
@@ -1613,7 +1629,7 @@ impl<
pub fn second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>(
rp_id: &'a RpId,
user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
) -> Self {
let mut opts = Self::passkey(rp_id, user, exclude_credentials);
opts.public_key.authenticator_selection = AuthenticatorSelectionCriteria::second_factor();
@@ -1774,7 +1790,7 @@ pub struct PublicKeyCredentialCreationOptions<
/// when attesting credentials as no timeout would make out-of-memory (OOM) conditions more likely.
pub timeout: NonZeroU32,
/// [`excludeCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-excludecredentials).
- pub exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ pub exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
/// [`authenticatorSelection`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection).
pub authenticator_selection: AuthenticatorSelectionCriteria,
/// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions).
@@ -1831,7 +1847,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
pub fn passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>(
rp_id: &'a RpId,
user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
) -> Self {
Self {
rp_id,
@@ -1903,7 +1919,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
pub fn second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>(
rp_id: &'a RpId,
user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
) -> Self {
let mut opts = Self::passkey(rp_id, user, exclude_credentials);
opts.authenticator_selection = AuthenticatorSelectionCriteria::second_factor();
diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs
@@ -105,6 +105,12 @@ const EDDSA: i16 = -8i16;
const ES256: i16 = -7i16;
/// [ES384](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
const ES384: i16 = -35i16;
+/// [ML-DSA-44](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
+const MLDSA44: i16 = -48i16;
+/// [ML-DSA-65](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
+const MLDSA65: i16 = -49i16;
+/// [ML-DSA-87](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
+const MLDSA87: i16 = -50i16;
/// [RS256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
const RS256: i16 = -257i16;
impl Serialize for CoseAlgorithmIdentifier {
@@ -122,6 +128,9 @@ impl Serialize for CoseAlgorithmIdentifier {
ser.serialize_field(
ALG,
&match *self {
+ Self::Mldsa87 => MLDSA87,
+ Self::Mldsa65 => MLDSA65,
+ Self::Mldsa44 => MLDSA44,
Self::Eddsa => EDDSA,
Self::Es256 => ES256,
Self::Es384 => ES384,
@@ -143,11 +152,11 @@ impl Serialize for CoseAlgorithmIdentifiers {
/// # use webauthn_rp::request::register::{CoseAlgorithmIdentifier,CoseAlgorithmIdentifiers};
/// assert_eq!(
/// serde_json::to_string(&CoseAlgorithmIdentifiers::ALL)?,
- /// r#"[{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},{"type":"public-key","alg":-257}]"#
+ /// r#"[{"type":"public-key","alg":-50},{"type":"public-key","alg":-49},{"type":"public-key","alg":-48},{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},{"type":"public-key","alg":-257}]"#
/// );
/// assert_eq!(
/// serde_json::to_string(&CoseAlgorithmIdentifiers::default().remove(CoseAlgorithmIdentifier::Es384))?,
- /// r#"[{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-257}]"#
+ /// r#"[{"type":"public-key","alg":-50},{"type":"public-key","alg":-49},{"type":"public-key","alg":-48},{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-257}]"#
/// );
/// # Ok::<_, serde_json::Error>(())
/// ```
@@ -160,39 +169,63 @@ impl Serialize for CoseAlgorithmIdentifiers {
where
S: Serializer,
{
- // At most we add `1` four times which clearly cannot overflow or `usize`.
+ // At most we add `1` seven times which clearly cannot overflow or `usize`.
serializer
.serialize_seq(Some(
- usize::from(self.contains(CoseAlgorithmIdentifier::Eddsa))
+ usize::from(self.contains(CoseAlgorithmIdentifier::Mldsa87))
+ + usize::from(self.contains(CoseAlgorithmIdentifier::Mldsa65))
+ + usize::from(self.contains(CoseAlgorithmIdentifier::Mldsa44))
+ + usize::from(self.contains(CoseAlgorithmIdentifier::Eddsa))
+ usize::from(self.contains(CoseAlgorithmIdentifier::Es256))
+ usize::from(self.contains(CoseAlgorithmIdentifier::Es384))
+ usize::from(self.contains(CoseAlgorithmIdentifier::Es384)),
))
.and_then(|mut ser| {
- if self.contains(CoseAlgorithmIdentifier::Eddsa) {
- ser.serialize_element(&CoseAlgorithmIdentifier::Eddsa)
+ if self.contains(CoseAlgorithmIdentifier::Mldsa87) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Mldsa87)
} else {
Ok(())
}
.and_then(|()| {
- if self.contains(CoseAlgorithmIdentifier::Es256) {
- ser.serialize_element(&CoseAlgorithmIdentifier::Es256)
+ if self.contains(CoseAlgorithmIdentifier::Mldsa65) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Mldsa65)
} else {
Ok(())
}
.and_then(|()| {
- if self.contains(CoseAlgorithmIdentifier::Es384) {
- ser.serialize_element(&CoseAlgorithmIdentifier::Es384)
+ if self.contains(CoseAlgorithmIdentifier::Mldsa44) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Mldsa44)
} else {
Ok(())
}
.and_then(|()| {
- if self.contains(CoseAlgorithmIdentifier::Rs256) {
- ser.serialize_element(&CoseAlgorithmIdentifier::Rs256)
+ if self.contains(CoseAlgorithmIdentifier::Eddsa) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Eddsa)
} else {
Ok(())
}
- .and_then(|()| ser.end())
+ .and_then(|()| {
+ if self.contains(CoseAlgorithmIdentifier::Es256) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Es256)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(CoseAlgorithmIdentifier::Es384) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Es384)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(CoseAlgorithmIdentifier::Rs256) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Rs256)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| ser.end())
+ })
+ })
+ })
})
})
})
@@ -852,7 +885,7 @@ where
/// // likely never needed since the `CredentialId` was originally sent from the client and is likely
/// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let transports = get_transports((&id).into())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
@@ -880,6 +913,18 @@ where
/// "pubKeyCredParams":[
/// {
/// "type":"public-key",
+ /// "alg":-50
+ /// },
+ /// {
+ /// "type":"public-key",
+ /// "alg":-49
+ /// },
+ /// {
+ /// "type":"public-key",
+ /// "alg":-48
+ /// },
+ /// {
+ /// "type":"public-key",
/// "alg":-8
/// },
/// {
@@ -1149,13 +1194,19 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifier {
E: Error,
{
match v {
+ RS256 => Ok(CoseAlgorithmIdentifier::Rs256),
+ MLDSA87 => Ok(CoseAlgorithmIdentifier::Mldsa87),
+ MLDSA65 => Ok(CoseAlgorithmIdentifier::Mldsa65),
+ MLDSA44 => Ok(CoseAlgorithmIdentifier::Mldsa44),
+ ES384 => Ok(CoseAlgorithmIdentifier::Es384),
EDDSA => Ok(CoseAlgorithmIdentifier::Eddsa),
ES256 => Ok(CoseAlgorithmIdentifier::Es256),
- ES384 => Ok(CoseAlgorithmIdentifier::Es384),
- RS256 => Ok(CoseAlgorithmIdentifier::Rs256),
_ => Err(E::invalid_value(
Unexpected::Signed(i64::from(v)),
- &format!("{EDDSA}, {ES256}, {ES384}, or {RS256}").as_str(),
+ &format!(
+ "{MLDSA87}, {MLDSA65}, {MLDSA44}, {EDDSA}, {ES256}, {ES384}, or {RS256}"
+ )
+ .as_str(),
)),
}
}
@@ -1647,7 +1698,7 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifiers {
/// except [`type`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialparameters-type) is not required.
///
/// Note the sequence of [`CoseAlgorithmIdentifier`]s MUST match [`CoseAlgorithmIdentifier::cmp`] or an
- /// error will occur (e.g., if [`CoseAlgorithmIdentifier::Eddsa`] exists, then it must appear first).
+ /// error will occur (e.g., if [`CoseAlgorithmIdentifier::Mldsa87`] exists, then it must appear first).
///
/// An empty sequence will be treated as [`Self::ALL`].
///
@@ -1657,8 +1708,9 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifiers {
///
/// ```
/// # use webauthn_rp::request::register::CoseAlgorithmIdentifiers;
- /// assert!(serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},{"type":"public-key","alg":-257}]"#).is_ok());
+ /// assert!(serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":"public-key","alg":-50},{"type":"public-key","alg":-49},{"type":"public-key","alg":-48},{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},{"type":"public-key","alg":-257}]"#).is_ok());
/// ```
+ #[expect(clippy::too_many_lines, reason = "132 is fine")]
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -1671,17 +1723,57 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifiers {
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("CoseAlgorithmIdentifiers")
}
+ #[expect(clippy::too_many_lines, reason = "118 is fine")]
#[expect(clippy::else_if_without_else, reason = "prefer it this way")]
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'d>,
{
+ let mut mldsa87 = false;
+ let mut mldsa65 = false;
+ let mut mldsa44 = false;
let mut eddsa = false;
let mut es256 = false;
let mut es384 = false;
let mut rs256 = false;
while let Some(elem) = seq.next_element::<PubParam>()? {
match elem.0 {
+ CoseAlgorithmIdentifier::Mldsa87 => {
+ if mldsa87 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained duplicate ML-DSA-87 values",
+ ));
+ } else if mldsa65 || mldsa44 || eddsa || es256 || es384 || rs256 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained ML-DSA-87, but it wasn't the first value",
+ ));
+ }
+ mldsa87 = true;
+ }
+ CoseAlgorithmIdentifier::Mldsa65 => {
+ if mldsa65 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained duplicate ML-DSA-65 values",
+ ));
+ } else if mldsa44 || eddsa || es256 || es384 || rs256 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained ML-DSA-65, but it was preceded by Mldsa44, Eddsa, Es256, Es384, or Rs256",
+ ));
+ }
+ mldsa65 = true;
+ }
+ CoseAlgorithmIdentifier::Mldsa44 => {
+ if mldsa44 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained duplicate ML-DSA-44 values",
+ ));
+ } else if eddsa || es256 || es384 || rs256 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained ML-DSA-44, but it was preceded by Eddsa, Es256, Es384, or Rs256",
+ ));
+ }
+ mldsa44 = true;
+ }
CoseAlgorithmIdentifier::Eddsa => {
if eddsa {
return Err(Error::custom(
@@ -1689,7 +1781,7 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifiers {
));
} else if es256 || es384 || rs256 {
return Err(Error::custom(
- "pubKeyCredParams contained EdDSA, but it wasn't the first value",
+ "pubKeyCredParams contained Eddsa, but it was preceded by Es256, Es384, or Rs256",
));
}
eddsa = true;
@@ -1729,6 +1821,15 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifiers {
}
}
let mut algs = CoseAlgorithmIdentifiers(0);
+ if mldsa87 {
+ algs = algs.add(CoseAlgorithmIdentifier::Mldsa87);
+ }
+ if mldsa65 {
+ algs = algs.add(CoseAlgorithmIdentifier::Mldsa65);
+ }
+ if mldsa44 {
+ algs = algs.add(CoseAlgorithmIdentifier::Mldsa44);
+ }
if eddsa {
algs = algs.add(CoseAlgorithmIdentifier::Eddsa);
}
@@ -2536,7 +2637,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER
#[inline]
pub fn as_options(
&self,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
) -> Result<
PublicKeyCredentialCreationOptions<'_, '_, '_, '_, '_, '_, USER_LEN>,
PublicKeyCredentialCreationOptionsOwnedErr,
@@ -2570,7 +2671,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER
#[inline]
pub fn with_rp_id<'rp_id>(
&self,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
rp_id: &'rp_id RpId,
) -> Result<
PublicKeyCredentialCreationOptions<'rp_id, '_, '_, '_, '_, '_, USER_LEN>,
@@ -2600,7 +2701,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER
#[inline]
pub fn with_user<'user_name, 'user_display_name, 'user_id>(
&self,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>,
) -> Result<
PublicKeyCredentialCreationOptions<
@@ -2638,7 +2739,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER
#[inline]
pub fn with_extensions<'prf_first, 'prf_second>(
&self,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
extensions: Extension<'prf_first, 'prf_second>,
) -> Result<
PublicKeyCredentialCreationOptions<'_, '_, '_, '_, 'prf_first, 'prf_second, USER_LEN>,
@@ -2671,7 +2772,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER
#[must_use]
pub fn with_rp_id_and_user<'rp_id, 'user_name, 'user_display_name, 'user_id>(
&self,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
rp_id: &'rp_id RpId,
user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>,
) -> PublicKeyCredentialCreationOptions<
@@ -2706,7 +2807,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER
#[inline]
pub fn with_rp_id_and_extensions<'rp_id, 'prf_first, 'prf_second>(
&self,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
rp_id: &'rp_id RpId,
extensions: Extension<'prf_first, 'prf_second>,
) -> Result<
@@ -2745,7 +2846,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER
'prf_second,
>(
&self,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>,
extensions: Extension<'prf_first, 'prf_second>,
) -> Result<
@@ -2790,7 +2891,7 @@ impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER
'prf_second,
>(
&self,
- exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
rp_id: &'rp_id RpId,
user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>,
extensions: Extension<'prf_first, 'prf_second>,
@@ -3452,6 +3553,9 @@ mod test {
assert_eq!(
options.public_key.pub_key_cred_params.0,
CoseAlgorithmIdentifiers::ALL
+ .remove(CoseAlgorithmIdentifier::Mldsa87)
+ .remove(CoseAlgorithmIdentifier::Mldsa65)
+ .remove(CoseAlgorithmIdentifier::Mldsa44)
.remove(CoseAlgorithmIdentifier::Es256)
.remove(CoseAlgorithmIdentifier::Es384)
.remove(CoseAlgorithmIdentifier::Rs256)
@@ -3699,6 +3803,9 @@ mod test {
assert_eq!(
key.pub_key_cred_params.0,
CoseAlgorithmIdentifiers::ALL
+ .remove(CoseAlgorithmIdentifier::Mldsa87)
+ .remove(CoseAlgorithmIdentifier::Mldsa65)
+ .remove(CoseAlgorithmIdentifier::Mldsa44)
.remove(CoseAlgorithmIdentifier::Es256)
.remove(CoseAlgorithmIdentifier::Es384)
.remove(CoseAlgorithmIdentifier::Rs256)
@@ -4093,8 +4200,8 @@ mod test {
serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":"public-key","alg":-6}]"#)
.unwrap_err();
assert_eq!(
- err.to_string().get(..58),
- Some("invalid value: integer `-6`, expected -8, -7, -35, or -257")
+ err.to_string().get(..73),
+ Some("invalid value: integer `-6`, expected -50, -49, -48, -8, -7, -35, or -257")
);
err = serde_json::from_str::<CoseAlgorithmIdentifiers>(
r#"[{"type":"public-key","alg":-7},{"type":"public-key","alg":-7}]"#,
@@ -4109,17 +4216,23 @@ mod test {
)
.unwrap_err();
assert_eq!(
- err.to_string().get(..63),
- Some("pubKeyCredParams contained EdDSA, but it wasn't the first value")
+ err.to_string().get(..79),
+ Some("pubKeyCredParams contained Eddsa, but it was preceded by Es256, Es384, or Rs256")
);
let mut alg = serde_json::from_str::<CoseAlgorithmIdentifiers>(
r#"[{"type":"public-key","alg":-8},{"alg":-7}]"#,
)?;
assert!(alg.contains(CoseAlgorithmIdentifier::Eddsa));
assert!(alg.contains(CoseAlgorithmIdentifier::Es256));
+ assert!(!alg.contains(CoseAlgorithmIdentifier::Mldsa87));
+ assert!(!alg.contains(CoseAlgorithmIdentifier::Mldsa65));
+ assert!(!alg.contains(CoseAlgorithmIdentifier::Mldsa44));
assert!(!alg.contains(CoseAlgorithmIdentifier::Es384));
assert!(!alg.contains(CoseAlgorithmIdentifier::Rs256));
alg = serde_json::from_str::<CoseAlgorithmIdentifiers>("[]")?;
+ assert!(alg.contains(CoseAlgorithmIdentifier::Mldsa87));
+ assert!(alg.contains(CoseAlgorithmIdentifier::Mldsa65));
+ assert!(alg.contains(CoseAlgorithmIdentifier::Mldsa44));
assert!(alg.contains(CoseAlgorithmIdentifier::Eddsa));
assert!(alg.contains(CoseAlgorithmIdentifier::Es256));
assert!(alg.contains(CoseAlgorithmIdentifier::Es384));
diff --git a/src/request/ser.rs b/src/request/ser.rs
@@ -182,7 +182,7 @@ where
/// // likely never needed since the `CredentialId` was originally sent from the client and is likely
/// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// let id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let transports = get_transports((&id).into())?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
diff --git a/src/response.rs b/src/response.rs
@@ -237,7 +237,7 @@ pub mod error;
/// /// an empty `Vec` should be passed.
/// fn get_registered_credentials(
/// user: UserHandle<USER_HANDLE_MAX_LEN>,
-/// ) -> Vec<PublicKeyCredentialDescriptor<Vec<u8>>> {
+/// ) -> Vec<PublicKeyCredentialDescriptor<Box<[u8]>>> {
/// // ⋮
/// # Vec::new()
/// }
@@ -513,28 +513,28 @@ impl<T: Borrow<[u8]>> Borrow<[u8]> for CredentialId<T> {
self.0.borrow()
}
}
-impl<'a: 'b, 'b> From<&'a CredentialId<Vec<u8>>> for CredentialId<&'b Vec<u8>> {
+impl<'a: 'b, 'b> From<&'a CredentialId<Box<[u8]>>> for CredentialId<&'b Box<[u8]>> {
#[inline]
- fn from(value: &'a CredentialId<Vec<u8>>) -> Self {
+ fn from(value: &'a CredentialId<Box<[u8]>>) -> Self {
Self(&value.0)
}
}
-impl<'a: 'b, 'b> From<CredentialId<&'a Vec<u8>>> for CredentialId<&'b [u8]> {
+impl<'a: 'b, 'b> From<CredentialId<&'a Box<[u8]>>> for CredentialId<&'b [u8]> {
#[inline]
- fn from(value: CredentialId<&'a Vec<u8>>) -> Self {
- Self(value.0.as_slice())
+ fn from(value: CredentialId<&'a Box<[u8]>>) -> Self {
+ Self(value.0)
}
}
-impl<'a: 'b, 'b> From<&'a CredentialId<Vec<u8>>> for CredentialId<&'b [u8]> {
+impl<'a: 'b, 'b> From<&'a CredentialId<Box<[u8]>>> for CredentialId<&'b [u8]> {
#[inline]
- fn from(value: &'a CredentialId<Vec<u8>>) -> Self {
- Self(value.0.as_slice())
+ fn from(value: &'a CredentialId<Box<[u8]>>) -> Self {
+ Self(&value.0)
}
}
-impl From<CredentialId<&[u8]>> for CredentialId<Vec<u8>> {
+impl From<CredentialId<&[u8]>> for CredentialId<Box<[u8]>> {
#[inline]
fn from(value: CredentialId<&[u8]>) -> Self {
- Self(value.0.to_owned())
+ Self(value.0.into())
}
}
impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CredentialId<T>> for CredentialId<T2> {
@@ -1614,7 +1614,7 @@ pub struct AllAcceptedCredentialsOptions<'rp, 'user, const USER_LEN: usize> {
/// [`userId`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-userid).
pub user_id: &'user UserHandle<USER_LEN>,
/// [`allAcceptedCredentialIds`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-allacceptedcredentialids).
- pub all_accepted_credential_ids: Vec<CredentialId<Vec<u8>>>,
+ pub all_accepted_credential_ids: Vec<CredentialId<Box<[u8]>>>,
}
/// [`CurrentUserDetailsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions).
///
diff --git a/src/response/auth.rs b/src/response/auth.rs
@@ -25,10 +25,11 @@ use super::{
auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr, MissingUserHandleErr},
cbor,
error::CollectedClientDataErr,
- register::CompressedPubKey,
+ register::CompressedPubKeyBorrowed,
};
use core::convert::Infallible;
use ed25519_dalek::{Signature, Verifier as _};
+use ml_dsa::{MlDsa44, MlDsa65, MlDsa87, Signature as MlDsaSig, signature::Verifier as _};
use p256::ecdsa::DerSignature as P256DerSig;
use p384::ecdsa::DerSignature as P384DerSig;
use rsa::{
@@ -401,7 +402,8 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> AuthResponse
= AuthenticatorData<'a>
where
Self: 'a;
- type CredKey<'a> = CompressedPubKey<&'a [u8], &'a [u8], &'a [u8], &'a [u8]>;
+ type CredKey<'a> = CompressedPubKeyBorrowed<'a>;
+ #[expect(clippy::too_many_lines, reason = "134 lines is OK")]
fn parse_data_and_verify_sig(
&self,
key: Self::CredKey<'_>,
@@ -442,7 +444,58 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> AuthResponse
.map_err(AuthRespErr::Auth)
.and_then(|val| {
match key {
- CompressedPubKey::Ed25519(k) => k
+ CompressedPubKeyBorrowed::MlDsa87(k) => self
+ .signature
+ .as_slice()
+ .try_into()
+ .map_err(|_e| AuthRespErr::Signature)
+ .and_then(|s| {
+ MlDsaSig::<MlDsa87>::decode(s)
+ .ok_or(AuthRespErr::Signature)
+ .and_then(|sig| {
+ k.into_ver_key()
+ .verify(
+ self.authenticator_data_and_c_data_hash.as_slice(),
+ &sig,
+ )
+ .map_err(|_e| AuthRespErr::Signature)
+ })
+ }),
+ CompressedPubKeyBorrowed::MlDsa65(k) => self
+ .signature
+ .as_slice()
+ .try_into()
+ .map_err(|_e| AuthRespErr::Signature)
+ .and_then(|s| {
+ MlDsaSig::<MlDsa65>::decode(s)
+ .ok_or(AuthRespErr::Signature)
+ .and_then(|sig| {
+ k.into_ver_key()
+ .verify(
+ self.authenticator_data_and_c_data_hash.as_slice(),
+ &sig,
+ )
+ .map_err(|_e| AuthRespErr::Signature)
+ })
+ }),
+ CompressedPubKeyBorrowed::MlDsa44(k) => self
+ .signature
+ .as_slice()
+ .try_into()
+ .map_err(|_e| AuthRespErr::Signature)
+ .and_then(|s| {
+ MlDsaSig::<MlDsa44>::decode(s)
+ .ok_or(AuthRespErr::Signature)
+ .and_then(|sig| {
+ k.into_ver_key()
+ .verify(
+ self.authenticator_data_and_c_data_hash.as_slice(),
+ &sig,
+ )
+ .map_err(|_e| AuthRespErr::Signature)
+ })
+ }),
+ CompressedPubKeyBorrowed::Ed25519(k) => k
.into_ver_key()
.map_err(AuthRespErr::PubKey)
.and_then(|ver_key| {
@@ -461,7 +514,7 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> AuthResponse
})
.map_err(|_e| AuthRespErr::Signature)
}),
- CompressedPubKey::P256(k) => k
+ CompressedPubKeyBorrowed::P256(k) => k
.into_ver_key()
.map_err(AuthRespErr::PubKey)
.and_then(|ver_key| {
@@ -474,7 +527,7 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> AuthResponse
})
.map_err(|_e| AuthRespErr::Signature)
}),
- CompressedPubKey::P384(k) => k
+ CompressedPubKeyBorrowed::P384(k) => k
.into_ver_key()
.map_err(AuthRespErr::PubKey)
.and_then(|ver_key| {
@@ -487,7 +540,7 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> AuthResponse
})
.map_err(|_e| AuthRespErr::Signature)
}),
- CompressedPubKey::Rsa(k) => {
+ CompressedPubKeyBorrowed::Rsa(k) => {
pkcs1v15::Signature::try_from(self.signature.as_slice())
.and_then(|sig| {
k.as_ver_key().verify(
@@ -550,7 +603,7 @@ impl<const USER_LEN: usize> TryFrom<NonDiscoverableAuthenticatorAssertion<USER_L
#[derive(Debug)]
pub struct Authentication<const USER_LEN: usize, const DISCOVERABLE: bool> {
/// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
- pub(crate) raw_id: CredentialId<Vec<u8>>,
+ pub(crate) raw_id: CredentialId<Box<[u8]>>,
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response)
pub(crate) response: AuthenticatorAssertion<USER_LEN, DISCOVERABLE>,
/// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
@@ -580,7 +633,7 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, D
#[inline]
#[must_use]
pub const fn new(
- raw_id: CredentialId<Vec<u8>>,
+ raw_id: CredentialId<Box<[u8]>>,
response: AuthenticatorAssertion<USER_LEN, DISCOVERABLE>,
authenticator_attachment: AuthenticatorAttachment,
) -> Self {
diff --git a/src/response/auth/error.rs b/src/response/auth/error.rs
@@ -23,7 +23,7 @@ use super::{
register::CredentialProtectionPolicy,
},
Authentication, AuthenticatorAssertion, AuthenticatorAttachment, AuthenticatorData,
- AuthenticatorExtensionOutput, CollectedClientData, CompressedPubKey, Flag, HmacSecret,
+ AuthenticatorExtensionOutput, CollectedClientData, CompressedPubKeyBorrowed, Flag, HmacSecret,
Signature, UserHandle,
};
use core::{
@@ -211,9 +211,9 @@ pub enum AuthCeremonyErr {
/// [`AuthenticatorAssertion::authenticator_data`] could not be parsed into an
/// [`AuthenticatorData`].
AuthenticatorData(AuthenticatorDataErr),
- /// [`CompressedPubKey`] was not valid.
+ /// [`CompressedPubKeyBorrowed`] was not valid.
PubKey(PubKeyErr),
- /// [`CompressedPubKey`] was not able to verify [`AuthenticatorAssertion::signature`].
+ /// [`CompressedPubKeyBorrowed`] was not able to verify [`AuthenticatorAssertion::signature`].
AssertionSignature,
/// [`CollectedClientData::origin`] does not match one of the values in
/// [`AuthenticationVerificationOptions::allowed_origins`].
diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs
@@ -417,7 +417,7 @@ impl Serialize for UnknownCredentialOptions<'_, '_> {
/// # use core::str::FromStr;
/// # use webauthn_rp::{request::{AsciiDomain, RpId}, response::{auth::error::UnknownCredentialOptions, CredentialId}};
/// # #[cfg(feature = "custom")]
- /// let credential_id = CredentialId::try_from(vec![0; 16])?;
+ /// let credential_id = CredentialId::try_from(vec![0; 16].into_boxed_slice())?;
/// # #[cfg(feature = "custom")]
/// assert_eq!(
/// serde_json::to_string(&UnknownCredentialOptions {
diff --git a/src/response/bin.rs b/src/response/bin.rs
@@ -91,12 +91,12 @@ impl<T: AsRef<[u8]>> Encode for CredentialId<T> {
Ok(self.0.as_ref())
}
}
-impl Decode for CredentialId<Vec<u8>> {
- type Input<'a> = Vec<u8>;
+impl Decode for CredentialId<Box<[u8]>> {
+ type Input<'a> = Box<[u8]>;
type Err = CredentialIdErr;
#[inline]
fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
- match CredentialId::<&[u8]>::from_slice(input.as_slice()) {
+ match CredentialId::<&[u8]>::from_slice(&input) {
Ok(_) => Ok(Self(input)),
Err(e) => Err(e),
}
@@ -123,13 +123,13 @@ impl<T: AsRef<[u8]>> EncodeBuffer for CredentialId<T> {
.unwrap_or_else(|_e| unreachable!("there is a bug in [u8]::encode_into_buffer"));
}
}
-impl<'a> DecodeBuffer<'a> for CredentialId<Vec<u8>> {
+impl<'a> DecodeBuffer<'a> for CredentialId<Box<[u8]>> {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
<&[u8]>::decode_from_buffer(data).and_then(|val| {
CredentialId::<&[u8]>::from_slice(val)
.map_err(|_e| EncDecErr)
- .map(|_| Self(val.to_owned()))
+ .map(|_| Self(val.into()))
})
}
}
diff --git a/src/response/cbor.rs b/src/response/cbor.rs
@@ -18,6 +18,8 @@ pub(super) const TWO: u8 = UINT | 2;
pub(super) const THREE: u8 = UINT | 3;
/// [`UINT`] value `6`.
pub(super) const SIX: u8 = UINT | 6;
+/// [`UINT`] value `7`.
+pub(super) const SEVEN: u8 = UINT | 7;
/// [`NEG`] value `-1`.
pub(super) const NEG_ONE: u8 = NEG;
/// [`NEG`] value `-2`.
diff --git a/src/response/custom.rs b/src/response/custom.rs
@@ -7,11 +7,11 @@ impl<'a: 'b, 'b> TryFrom<&'a [u8]> for CredentialId<&'b [u8]> {
Self::from_slice(value)
}
}
-impl TryFrom<Vec<u8>> for CredentialId<Vec<u8>> {
+impl TryFrom<Box<[u8]>> for CredentialId<Box<[u8]>> {
type Error = CredentialIdErr;
#[inline]
- fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
- match CredentialId::<&[u8]>::try_from(value.as_slice()) {
+ fn try_from(value: Box<[u8]>) -> Result<Self, Self::Error> {
+ match CredentialId::<&[u8]>::try_from(&*value) {
Ok(_) => Ok(Self(value)),
Err(e) => Err(e),
}
diff --git a/src/response/register.rs b/src/response/register.rs
@@ -17,8 +17,9 @@ use super::{
register::error::{
AaguidErr, AttestationErr, AttestationObjectErr, AttestedCredentialDataErr,
AuthenticatorDataErr, AuthenticatorExtensionOutputErr, CompressedP256PubKeyErr,
- CompressedP384PubKeyErr, CoseKeyErr, Ed25519PubKeyErr, Ed25519SignatureErr, PubKeyErr,
- RsaPubKeyErr, UncompressedP256PubKeyErr, UncompressedP384PubKeyErr,
+ CompressedP384PubKeyErr, CoseKeyErr, Ed25519PubKeyErr, Ed25519SignatureErr,
+ MlDsa44PubKeyErr, MlDsa65PubKeyErr, MlDsa87PubKeyErr, PubKeyErr, RsaPubKeyErr,
+ UncompressedP256PubKeyErr, UncompressedP384PubKeyErr,
},
};
#[cfg(doc)]
@@ -40,6 +41,10 @@ use core::{
fmt::{self, Display, Formatter},
};
use ed25519_dalek::{Signature, Verifier as _, VerifyingKey};
+use ml_dsa::{
+ MlDsa44, MlDsa65, MlDsa87, Signature as MlDsaSignature, VerifyingKey as MlDsaVerKey,
+ signature::Verifier as _,
+};
use p256::{
AffinePoint as P256Affine, EncodedPoint as P256Pt, NistP256,
ecdsa::{DerSignature as P256Sig, VerifyingKey as P256VerKey},
@@ -703,6 +708,276 @@ impl FromCbor<'_> for AuthenticatorExtensionOutput {
)
}
}
+/// 2592 bytes representing an alleged ML-DSA-87 public key.
+#[derive(Clone, Copy, Debug)]
+pub struct MlDsa87PubKey<T>(T);
+impl<T> MlDsa87PubKey<T> {
+ /// Returns the contained data consuming `self`.
+ #[inline]
+ pub fn into_inner(self) -> T {
+ self.0
+ }
+ /// Returns the contained data.
+ #[inline]
+ pub const fn inner(&self) -> &T {
+ &self.0
+ }
+}
+impl<T: AsRef<[u8]>> MlDsa87PubKey<T> {
+ /// Returns the contained data.
+ #[inline]
+ #[must_use]
+ pub fn encoded_data(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+impl MlDsa87PubKey<&[u8]> {
+ /// Converts `self` into an [`MlDsaVerKey`].
+ pub(super) fn into_ver_key(self) -> MlDsaVerKey<MlDsa87> {
+ self.into_owned().into_ver_key()
+ }
+ /// Transforms `self` into an "owned" version.
+ #[inline]
+ #[must_use]
+ pub fn into_owned(self) -> MlDsa87PubKey<Box<[u8]>> {
+ MlDsa87PubKey(self.0.into())
+ }
+}
+impl MlDsa87PubKey<Box<[u8]>> {
+ /// Converts `self` into [`MlDsaVerKey`].
+ #[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
+ fn into_ver_key(self) -> MlDsaVerKey<MlDsa87> {
+ MlDsaVerKey::decode(
+ self.0
+ .as_array()
+ .unwrap_or_else(|| unreachable!("there is a bug in slice::as_array"))
+ .into(),
+ )
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for MlDsa87PubKey<&'b [u8]> {
+ type Error = MlDsa87PubKeyErr;
+ /// Interprets `value` as an encoded ML-DSA-87 public key.
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ if value.len() == 2592 {
+ Ok(Self(value))
+ } else {
+ Err(MlDsa87PubKeyErr)
+ }
+ }
+}
+impl TryFrom<Box<[u8]>> for MlDsa87PubKey<Box<[u8]>> {
+ type Error = MlDsa87PubKeyErr;
+ /// Interprets `value` as an encoded ML-DSA-87 public key.
+ #[inline]
+ fn try_from(value: Box<[u8]>) -> Result<Self, Self::Error> {
+ if value.len() == 2592 {
+ Ok(Self(value))
+ } else {
+ Err(MlDsa87PubKeyErr)
+ }
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<MlDsa87PubKey<T>> for MlDsa87PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &MlDsa87PubKey<T>) -> bool {
+ self.0 == other.0
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<MlDsa87PubKey<T>> for &MlDsa87PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &MlDsa87PubKey<T>) -> bool {
+ **self == *other
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&MlDsa87PubKey<T>> for MlDsa87PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &&MlDsa87PubKey<T>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq> Eq for MlDsa87PubKey<T> {}
+/// 1952 bytes representing an alleged ML-DSA-65 public key.
+#[derive(Clone, Copy, Debug)]
+pub struct MlDsa65PubKey<T>(T);
+impl<T> MlDsa65PubKey<T> {
+ /// Returns the contained data consuming `self`.
+ #[inline]
+ pub fn into_inner(self) -> T {
+ self.0
+ }
+ /// Returns the contained data.
+ #[inline]
+ pub const fn inner(&self) -> &T {
+ &self.0
+ }
+}
+impl<T: AsRef<[u8]>> MlDsa65PubKey<T> {
+ /// Returns the contained data.
+ #[inline]
+ #[must_use]
+ pub fn encoded_data(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+impl MlDsa65PubKey<&[u8]> {
+ /// Converts `self` into an [`MlDsaVerKey`].
+ pub(super) fn into_ver_key(self) -> MlDsaVerKey<MlDsa65> {
+ self.into_owned().into_ver_key()
+ }
+ /// Transforms `self` into an "owned" version.
+ #[inline]
+ #[must_use]
+ pub fn into_owned(self) -> MlDsa65PubKey<Box<[u8]>> {
+ MlDsa65PubKey(self.0.into())
+ }
+}
+impl MlDsa65PubKey<Box<[u8]>> {
+ /// Converts `self` into [`MlDsaVerKey`].
+ #[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
+ fn into_ver_key(self) -> MlDsaVerKey<MlDsa65> {
+ MlDsaVerKey::decode(
+ self.0
+ .as_array()
+ .unwrap_or_else(|| unreachable!("there is a bug in slice::as_array"))
+ .into(),
+ )
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for MlDsa65PubKey<&'b [u8]> {
+ type Error = MlDsa65PubKeyErr;
+ /// Interprets `value` as an encoded ML-DSA-65 public key.
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ if value.len() == 1952 {
+ Ok(Self(value))
+ } else {
+ Err(MlDsa65PubKeyErr)
+ }
+ }
+}
+impl TryFrom<Box<[u8]>> for MlDsa65PubKey<Box<[u8]>> {
+ type Error = MlDsa65PubKeyErr;
+ /// Interprets `value` as an encoded ML-DSA-65 public key.
+ #[inline]
+ fn try_from(value: Box<[u8]>) -> Result<Self, Self::Error> {
+ if value.len() == 1952 {
+ Ok(Self(value))
+ } else {
+ Err(MlDsa65PubKeyErr)
+ }
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<MlDsa65PubKey<T>> for MlDsa65PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &MlDsa65PubKey<T>) -> bool {
+ self.0 == other.0
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<MlDsa65PubKey<T>> for &MlDsa65PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &MlDsa65PubKey<T>) -> bool {
+ **self == *other
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&MlDsa65PubKey<T>> for MlDsa65PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &&MlDsa65PubKey<T>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq> Eq for MlDsa65PubKey<T> {}
+/// 1312 bytes representing an alleged ML-DSA-44 public key.
+#[derive(Clone, Copy, Debug)]
+pub struct MlDsa44PubKey<T>(T);
+impl<T> MlDsa44PubKey<T> {
+ /// Returns the contained data consuming `self`.
+ #[inline]
+ pub fn into_inner(self) -> T {
+ self.0
+ }
+ /// Returns the contained data.
+ #[inline]
+ pub const fn inner(&self) -> &T {
+ &self.0
+ }
+}
+impl<T: AsRef<[u8]>> MlDsa44PubKey<T> {
+ /// Returns the contained data.
+ #[inline]
+ #[must_use]
+ pub fn encoded_data(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+impl MlDsa44PubKey<&[u8]> {
+ /// Converts `self` into an [`MlDsaVerKey`].
+ pub(super) fn into_ver_key(self) -> MlDsaVerKey<MlDsa44> {
+ self.into_owned().into_ver_key()
+ }
+ /// Transforms `self` into an "owned" version.
+ #[inline]
+ #[must_use]
+ pub fn into_owned(self) -> MlDsa44PubKey<Box<[u8]>> {
+ MlDsa44PubKey(self.0.into())
+ }
+}
+impl MlDsa44PubKey<Box<[u8]>> {
+ /// Converts `self` into [`MlDsaVerKey`].
+ #[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
+ fn into_ver_key(self) -> MlDsaVerKey<MlDsa44> {
+ MlDsaVerKey::decode(
+ self.0
+ .as_array()
+ .unwrap_or_else(|| unreachable!("there is a bug in slice::as_array"))
+ .into(),
+ )
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for MlDsa44PubKey<&'b [u8]> {
+ type Error = MlDsa44PubKeyErr;
+ /// Interprets `value` as an encoded ML-DSA-44 public key.
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ if value.len() == 1312 {
+ Ok(Self(value))
+ } else {
+ Err(MlDsa44PubKeyErr)
+ }
+ }
+}
+impl TryFrom<Box<[u8]>> for MlDsa44PubKey<Box<[u8]>> {
+ type Error = MlDsa44PubKeyErr;
+ /// Interprets `value` as an encoded ML-DSA-44 public key.
+ #[inline]
+ fn try_from(value: Box<[u8]>) -> Result<Self, Self::Error> {
+ if value.len() == 1312 {
+ Ok(Self(value))
+ } else {
+ Err(MlDsa44PubKeyErr)
+ }
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<MlDsa44PubKey<T>> for MlDsa44PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &MlDsa44PubKey<T>) -> bool {
+ self.0 == other.0
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<MlDsa44PubKey<T>> for &MlDsa44PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &MlDsa44PubKey<T>) -> bool {
+ **self == *other
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&MlDsa44PubKey<T>> for MlDsa44PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &&MlDsa44PubKey<T>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq> Eq for MlDsa44PubKey<T> {}
/// 32-bytes representing an alleged Ed25519 public key (i.e., compressed y-coordinate).
#[derive(Clone, Copy, Debug)]
pub struct Ed25519PubKey<T>(T);
@@ -844,8 +1119,8 @@ impl<'a: 'b, 'b> From<Ed25519PubKey<&'a [u8]>>
Self(
value
.0
- .try_into()
- .unwrap_or_else(|_e| unreachable!("there is a bug in &Array::try_from")),
+ .as_array()
+ .unwrap_or_else(|| unreachable!("there is a bug in slice::as_array")),
)
}
}
@@ -1096,8 +1371,8 @@ impl<'a: 'b, 'b> From<CompressedP256PubKey<&'a [u8]>>
Self {
x: value
.x
- .try_into()
- .unwrap_or_else(|_e| unreachable!("&Array::try_from has a bug")),
+ .as_array()
+ .unwrap_or_else(|| unreachable!("there is a bug in slice::as_array")),
y_is_odd: value.y_is_odd,
}
}
@@ -1356,8 +1631,8 @@ impl<'a: 'b, 'b> From<CompressedP384PubKey<&'a [u8]>>
Self {
x: value
.x
- .try_into()
- .unwrap_or_else(|_e| unreachable!("&Array::try_from has a bug")),
+ .as_array()
+ .unwrap_or_else(|| unreachable!("there is a bug in slice::as_array")),
y_is_odd: value.y_is_odd,
}
}
@@ -1442,8 +1717,8 @@ impl RsaPubKey<&[u8]> {
/// Transforms `self` into an "owned" version.
#[inline]
#[must_use]
- pub fn into_owned(self) -> RsaPubKey<Vec<u8>> {
- RsaPubKey(self.0.to_owned(), self.1)
+ pub fn into_owned(self) -> RsaPubKey<Box<[u8]>> {
+ RsaPubKey(self.0.into(), self.1)
}
}
impl<'a: 'b, 'b> TryFrom<(&'a [u8], u32)> for RsaPubKey<&'b [u8]> {
@@ -1487,33 +1762,33 @@ impl<'a: 'b, 'b> TryFrom<(&'a [u8], u32)> for RsaPubKey<&'b [u8]> {
})
}
}
-impl TryFrom<(Vec<u8>, u32)> for RsaPubKey<Vec<u8>> {
+impl TryFrom<(Box<[u8]>, u32)> for RsaPubKey<Box<[u8]>> {
type Error = RsaPubKeyErr;
- /// Similar to [`RsaPubKey::try_from`] except `n` is a `Vec`.
+ /// Similar to [`RsaPubKey::try_from`] except `n` is a `Box`.
#[inline]
- fn try_from((n, e): (Vec<u8>, u32)) -> Result<Self, Self::Error> {
- match RsaPubKey::<&[u8]>::try_from((n.as_slice(), e)) {
+ fn try_from((n, e): (Box<[u8]>, u32)) -> Result<Self, Self::Error> {
+ match RsaPubKey::<&[u8]>::try_from((&*n, e)) {
Ok(_) => Ok(Self(n, e)),
Err(err) => Err(err),
}
}
}
-impl<'a: 'b, 'b> From<&'a RsaPubKey<Vec<u8>>> for RsaPubKey<&'b Vec<u8>> {
+impl<'a: 'b, 'b> From<&'a RsaPubKey<Box<[u8]>>> for RsaPubKey<&'b Box<[u8]>> {
#[inline]
- fn from(value: &'a RsaPubKey<Vec<u8>>) -> Self {
+ fn from(value: &'a RsaPubKey<Box<[u8]>>) -> Self {
Self(&value.0, value.1)
}
}
-impl<'a: 'b, 'b> From<RsaPubKey<&'a Vec<u8>>> for RsaPubKey<&'b [u8]> {
+impl<'a: 'b, 'b> From<RsaPubKey<&'a Box<[u8]>>> for RsaPubKey<&'b [u8]> {
#[inline]
- fn from(value: RsaPubKey<&'a Vec<u8>>) -> Self {
- Self(value.0.as_slice(), value.1)
+ fn from(value: RsaPubKey<&'a Box<[u8]>>) -> Self {
+ Self(value.0, value.1)
}
}
-impl<'a: 'b, 'b> From<&'a RsaPubKey<Vec<u8>>> for RsaPubKey<&'b [u8]> {
+impl<'a: 'b, 'b> From<&'a RsaPubKey<Box<[u8]>>> for RsaPubKey<&'b [u8]> {
#[inline]
- fn from(value: &'a RsaPubKey<Vec<u8>>) -> Self {
- Self(value.0.as_slice(), value.1)
+ fn from(value: &'a RsaPubKey<Box<[u8]>>) -> Self {
+ Self(&value.0, value.1)
}
}
impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<RsaPubKey<T>> for RsaPubKey<T2> {
@@ -1547,6 +1822,9 @@ const EC2: u8 = cbor::TWO;
/// `RSA` COSE key type as defined by
/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type).
const RSA: u8 = cbor::THREE;
+/// `AKP` COSE key type as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type).
+const AKP: u8 = cbor::SEVEN;
/// `alg` COSE key common parameter as defined by
/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters).
const ALG: u8 = cbor::THREE;
@@ -1562,12 +1840,156 @@ const ES256: u8 = cbor::NEG_SEVEN;
/// This is -35 encoded in cbor which is encoded as |-35| - 1 = 35 - 1 = 34. Note
/// this must be preceded with `cbor::NEG_INFO_24`.
const ES384: u8 = 34;
+/// `ML-DSA-44` COSE algorithm as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+///
+/// This is -48 encoded in cbor which is encoded as |-48| - 1 = 48 - 1 = 47. Note
+/// this must be preceded with `cbor::NEG_INFO_24`.
+const MLDSA44: u8 = 47;
+/// `ML-DSA-65` COSE algorithm as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+///
+/// This is -49 encoded in cbor which is encoded as |-49| - 1 = 49 - 1 = 48. Note
+/// this must be preceded with `cbor::NEG_INFO_24`.
+const MLDSA65: u8 = 48;
+/// `ML-DSA-87` COSE algorithm as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+///
+/// This is -50 encoded in cbor which is encoded as |-50| - 1 = 50 - 1 = 49. Note
+/// this must be preceded with `cbor::NEG_INFO_24`.
+const MLDSA87: u8 = 49;
/// `RS256` COSE algorithm as defined by
/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
///
/// This is -257 encoded in cbor which is encoded as |-257| - 1 = 257 - 1 = 256 = [1, 0] in big endian.
/// Note this must be preceded with `cbor::NEG_INFO_25`.
const RS256: [u8; 2] = [1, 0];
+impl<'a> FromCbor<'a> for MlDsa87PubKey<&'a [u8]> {
+ type Err = CoseKeyErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// `pub` COSE key type parameter for [`AKP`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const PUB: u8 = cbor::NEG_ONE;
+ /// COSE header.
+ /// {kty:AKP,alg:ML-DSA-87,pub:<encodedKey>}.
+ /// `kty` and `alg` come before `pub` since map order first
+ /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s.
+ /// `kty` comes before `alg` since order is done byte-wise and
+ /// 1 is before 3.
+ const HEADER: [u8; 10] = [
+ cbor::MAP_3,
+ KTY,
+ AKP,
+ ALG,
+ cbor::NEG_INFO_24,
+ MLDSA87,
+ PUB,
+ cbor::BYTES_INFO_25,
+ // 10 *256 + 32 = 2592
+ 10,
+ 32,
+ ];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(2592)
+ .ok_or(CoseKeyErr::Len)
+ .map(|(key, remaining)| CborSuccess {
+ value: Self(key),
+ remaining,
+ })
+ } else {
+ Err(CoseKeyErr::MlDsa87CoseEncoding)
+ }
+ })
+ }
+}
+impl<'a> FromCbor<'a> for MlDsa65PubKey<&'a [u8]> {
+ type Err = CoseKeyErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// `pub` COSE key type parameter for [`AKP`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const PUB: u8 = cbor::NEG_ONE;
+ /// COSE header.
+ /// {kty:AKP,alg:ML-DSA-65,pub:<encodedKey>}.
+ /// `kty` and `alg` come before `pub` since map order first
+ /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s.
+ /// `kty` comes before `alg` since order is done byte-wise and
+ /// 1 is before 3.
+ const HEADER: [u8; 10] = [
+ cbor::MAP_3,
+ KTY,
+ AKP,
+ ALG,
+ cbor::NEG_INFO_24,
+ MLDSA65,
+ PUB,
+ cbor::BYTES_INFO_25,
+ // 7 *256 + 160 = 1952
+ 7,
+ 160,
+ ];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(1952)
+ .ok_or(CoseKeyErr::Len)
+ .map(|(key, remaining)| CborSuccess {
+ value: Self(key),
+ remaining,
+ })
+ } else {
+ Err(CoseKeyErr::MlDsa65CoseEncoding)
+ }
+ })
+ }
+}
+impl<'a> FromCbor<'a> for MlDsa44PubKey<&'a [u8]> {
+ type Err = CoseKeyErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// `pub` COSE key type parameter for [`AKP`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const PUB: u8 = cbor::NEG_ONE;
+ /// COSE header.
+ /// {kty:AKP,alg:ML-DSA-44,pub:<encodedKey>}.
+ /// `kty` and `alg` come before `pub` since map order first
+ /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s.
+ /// `kty` comes before `alg` since order is done byte-wise and
+ /// 1 is before 3.
+ const HEADER: [u8; 10] = [
+ cbor::MAP_3,
+ KTY,
+ AKP,
+ ALG,
+ cbor::NEG_INFO_24,
+ MLDSA44,
+ PUB,
+ cbor::BYTES_INFO_25,
+ // 5 *256 + 32 = 1312
+ 5,
+ 32,
+ ];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(1312)
+ .ok_or(CoseKeyErr::Len)
+ .map(|(key, remaining)| CborSuccess {
+ value: Self(key),
+ remaining,
+ })
+ } else {
+ Err(CoseKeyErr::MlDsa44CoseEncoding)
+ }
+ })
+ }
+}
impl<'a> FromCbor<'a> for Ed25519PubKey<&'a [u8]> {
type Err = CoseKeyErr;
fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
@@ -1903,9 +2325,15 @@ impl<'a> FromCbor<'a> for RsaPubKey<&'a [u8]> {
}
/// An alleged uncompressed public key that borrows the key data.
///
-/// Note [`Self::Ed25519`] is compressed.
+/// Note [`Self::MlDsa87`], [`Self::MlDsa65`], [`Self::MlDsa44`], and [`Self::Ed25519`] are compressed.
#[derive(Clone, Copy, Debug)]
pub enum UncompressedPubKey<'a> {
+ /// An alleged ML-DSA-87 public key.
+ MlDsa87(MlDsa87PubKey<&'a [u8]>),
+ /// An alleged ML-DSA-65 public key.
+ MlDsa65(MlDsa65PubKey<&'a [u8]>),
+ /// An alleged ML-DSA-44 public key.
+ MlDsa44(MlDsa44PubKey<&'a [u8]>),
/// An alleged Ed25519 public key.
Ed25519(Ed25519PubKey<&'a [u8]>),
/// An alleged uncompressed P-256 public key.
@@ -1924,28 +2352,24 @@ impl UncompressedPubKey<'_> {
#[inline]
pub fn validate(self) -> Result<(), PubKeyErr> {
match self {
+ Self::MlDsa87(_) | Self::MlDsa65(_) | Self::MlDsa44(_) | Self::Rsa(_) => Ok(()),
Self::Ed25519(k) => k.validate(),
Self::P256(k) => k.validate(),
Self::P384(k) => k.validate(),
- Self::Rsa(_) => Ok(()),
}
}
/// Transforms `self` into the compressed version that owns the data.
#[inline]
#[must_use]
- pub fn into_compressed(
- self,
- ) -> CompressedPubKey<
- [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
- [u8; <NistP256 as Curve>::FieldBytesSize::INT],
- [u8; <NistP384 as Curve>::FieldBytesSize::INT],
- Vec<u8>,
- > {
+ pub fn into_compressed(self) -> CompressedPubKeyOwned {
match self {
- Self::Ed25519(key) => CompressedPubKey::Ed25519(key.into_owned()),
- Self::P256(key) => CompressedPubKey::P256(key.into_compressed()),
- Self::P384(key) => CompressedPubKey::P384(key.into_compressed()),
- Self::Rsa(key) => CompressedPubKey::Rsa(key.into_owned()),
+ Self::MlDsa87(key) => CompressedPubKeyOwned::MlDsa87(key.into_owned()),
+ Self::MlDsa65(key) => CompressedPubKeyOwned::MlDsa65(key.into_owned()),
+ Self::MlDsa44(key) => CompressedPubKeyOwned::MlDsa44(key.into_owned()),
+ Self::Ed25519(key) => CompressedPubKeyOwned::Ed25519(key.into_owned()),
+ Self::P256(key) => CompressedPubKeyOwned::P256(key.into_compressed()),
+ Self::P384(key) => CompressedPubKeyOwned::P384(key.into_compressed()),
+ Self::Rsa(key) => CompressedPubKeyOwned::Rsa(key.into_owned()),
}
}
}
@@ -1953,6 +2377,9 @@ impl PartialEq<UncompressedPubKey<'_>> for UncompressedPubKey<'_> {
#[inline]
fn eq(&self, other: &UncompressedPubKey<'_>) -> bool {
match *self {
+ Self::MlDsa87(k) => matches!(*other, UncompressedPubKey::MlDsa87(k2) if k == k2),
+ Self::MlDsa65(k) => matches!(*other, UncompressedPubKey::MlDsa65(k2) if k == k2),
+ Self::MlDsa44(k) => matches!(*other, UncompressedPubKey::MlDsa44(k2) if k == k2),
Self::Ed25519(k) => matches!(*other, UncompressedPubKey::Ed25519(k2) if k == k2),
Self::P256(k) => matches!(*other, UncompressedPubKey::P256(k2) if k == k2),
Self::P384(k) => matches!(*other, UncompressedPubKey::P384(k2) if k == k2),
@@ -1977,26 +2404,36 @@ impl Eq for UncompressedPubKey<'_> {}
///
/// Note [`Self::Rsa`] is uncompressed.
#[derive(Clone, Copy, Debug)]
-pub enum CompressedPubKey<T, T2, T3, T4> {
+pub enum CompressedPubKey<T, T2, T3, T4, T5, T6, T7> {
+ /// An alleged ML-DSA-87 public key.
+ MlDsa87(MlDsa87PubKey<T>),
+ /// An alleged ML-DSA-65 public key.
+ MlDsa65(MlDsa65PubKey<T2>),
+ /// An alleged ML-DSA-44 public key.
+ MlDsa44(MlDsa44PubKey<T3>),
/// An alleged Ed25519 public key.
- Ed25519(Ed25519PubKey<T>),
+ Ed25519(Ed25519PubKey<T4>),
/// An alleged compressed P-256 public key.
- P256(CompressedP256PubKey<T2>),
+ P256(CompressedP256PubKey<T5>),
/// An alleged compressed P-384 public key.
- P384(CompressedP384PubKey<T3>),
+ P384(CompressedP384PubKey<T6>),
/// An alleged RSA public key.
- Rsa(RsaPubKey<T4>),
+ Rsa(RsaPubKey<T7>),
}
/// `CompressedPubKey` that owns the key data.
pub type CompressedPubKeyOwned = CompressedPubKey<
+ Box<[u8]>,
+ Box<[u8]>,
+ Box<[u8]>,
[u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
[u8; <NistP256 as Curve>::FieldBytesSize::INT],
[u8; <NistP384 as Curve>::FieldBytesSize::INT],
- Vec<u8>,
+ Box<[u8]>,
>;
/// `CompressedPubKey` that borrows the key data.
-pub type CompressedPubKeyBorrowed<'a> = CompressedPubKey<&'a [u8], &'a [u8], &'a [u8], &'a [u8]>;
-impl CompressedPubKey<&[u8], &[u8], &[u8], &[u8]> {
+pub type CompressedPubKeyBorrowed<'a> =
+ CompressedPubKey<&'a [u8], &'a [u8], &'a [u8], &'a [u8], &'a [u8], &'a [u8], &'a [u8]>;
+impl CompressedPubKeyBorrowed<'_> {
/// Validates `self` is in fact a valid public key.
///
/// # Errors
@@ -2005,20 +2442,31 @@ impl CompressedPubKey<&[u8], &[u8], &[u8], &[u8]> {
#[inline]
pub fn validate(self) -> Result<(), PubKeyErr> {
match self {
+ Self::MlDsa87(_) | Self::MlDsa65(_) | Self::MlDsa44(_) | Self::Rsa(_) => Ok(()),
Self::Ed25519(k) => k.validate(),
Self::P256(k) => k.validate(),
Self::P384(k) => k.validate(),
- Self::Rsa(_) => Ok(()),
}
}
}
-impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8]>>
- From<&'a CompressedPubKey<T, T2, T3, T4>>
- for CompressedPubKey<&'b [u8], &'b [u8], &'b [u8], &'b [u8]>
+impl<
+ 'a: 'b,
+ 'b,
+ T: AsRef<[u8]>,
+ T2: AsRef<[u8]>,
+ T3: AsRef<[u8]>,
+ T4: AsRef<[u8]>,
+ T5: AsRef<[u8]>,
+ T6: AsRef<[u8]>,
+ T7: AsRef<[u8]>,
+> From<&'a CompressedPubKey<T, T2, T3, T4, T5, T6, T7>> for CompressedPubKeyBorrowed<'b>
{
#[inline]
- fn from(value: &'a CompressedPubKey<T, T2, T3, T4>) -> Self {
+ fn from(value: &'a CompressedPubKey<T, T2, T3, T4, T5, T6, T7>) -> Self {
match *value {
+ CompressedPubKey::MlDsa87(ref val) => Self::MlDsa87(MlDsa87PubKey(val.0.as_ref())),
+ CompressedPubKey::MlDsa65(ref val) => Self::MlDsa65(MlDsa65PubKey(val.0.as_ref())),
+ CompressedPubKey::MlDsa44(ref val) => Self::MlDsa44(MlDsa44PubKey(val.0.as_ref())),
CompressedPubKey::Ed25519(ref val) => Self::Ed25519(Ed25519PubKey(val.0.as_ref())),
CompressedPubKey::P256(ref val) => Self::P256(CompressedP256PubKey {
x: val.x.as_ref(),
@@ -2033,19 +2481,35 @@ impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8
}
}
impl<
- T: PartialEq<T5>,
- T5: PartialEq<T>,
- T2: PartialEq<T6>,
- T6: PartialEq<T2>,
- T3: PartialEq<T7>,
- T7: PartialEq<T3>,
- T4: PartialEq<T8>,
- T8: PartialEq<T4>,
-> PartialEq<CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8>
+ T: PartialEq<T8>,
+ T8: PartialEq<T>,
+ T2: PartialEq<T9>,
+ T9: PartialEq<T2>,
+ T3: PartialEq<T10>,
+ T10: PartialEq<T3>,
+ T4: PartialEq<T11>,
+ T11: PartialEq<T4>,
+ T5: PartialEq<T12>,
+ T12: PartialEq<T5>,
+ T6: PartialEq<T13>,
+ T13: PartialEq<T6>,
+ T7: PartialEq<T14>,
+ T14: PartialEq<T7>,
+> PartialEq<CompressedPubKey<T, T2, T3, T4, T5, T6, T7>>
+ for CompressedPubKey<T8, T9, T10, T11, T12, T13, T14>
{
#[inline]
- fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool {
+ fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4, T5, T6, T7>) -> bool {
match *self {
+ Self::MlDsa87(ref val) => {
+ matches!(*other, CompressedPubKey::MlDsa87(ref val2) if val == val2)
+ }
+ Self::MlDsa65(ref val) => {
+ matches!(*other, CompressedPubKey::MlDsa65(ref val2) if val == val2)
+ }
+ Self::MlDsa44(ref val) => {
+ matches!(*other, CompressedPubKey::MlDsa44(ref val2) if val == val2)
+ }
Self::Ed25519(ref val) => {
matches!(*other, CompressedPubKey::Ed25519(ref val2) if val == val2)
}
@@ -2060,38 +2524,55 @@ impl<
}
}
impl<
- T: PartialEq<T5>,
- T5: PartialEq<T>,
- T2: PartialEq<T6>,
- T6: PartialEq<T2>,
- T3: PartialEq<T7>,
- T7: PartialEq<T3>,
- T4: PartialEq<T8>,
- T8: PartialEq<T4>,
-> PartialEq<CompressedPubKey<T, T2, T3, T4>> for &CompressedPubKey<T5, T6, T7, T8>
+ T: PartialEq<T8>,
+ T8: PartialEq<T>,
+ T2: PartialEq<T9>,
+ T9: PartialEq<T2>,
+ T3: PartialEq<T10>,
+ T10: PartialEq<T3>,
+ T4: PartialEq<T11>,
+ T11: PartialEq<T4>,
+ T5: PartialEq<T12>,
+ T12: PartialEq<T5>,
+ T6: PartialEq<T13>,
+ T13: PartialEq<T6>,
+ T7: PartialEq<T14>,
+ T14: PartialEq<T7>,
+> PartialEq<CompressedPubKey<T, T2, T3, T4, T5, T6, T7>>
+ for &CompressedPubKey<T8, T9, T10, T11, T12, T13, T14>
{
#[inline]
- fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool {
+ fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4, T5, T6, T7>) -> bool {
**self == *other
}
}
impl<
- T: PartialEq<T5>,
- T5: PartialEq<T>,
- T2: PartialEq<T6>,
- T6: PartialEq<T2>,
- T3: PartialEq<T7>,
- T7: PartialEq<T3>,
- T4: PartialEq<T8>,
- T8: PartialEq<T4>,
-> PartialEq<&CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8>
+ T: PartialEq<T8>,
+ T8: PartialEq<T>,
+ T2: PartialEq<T9>,
+ T9: PartialEq<T2>,
+ T3: PartialEq<T10>,
+ T10: PartialEq<T3>,
+ T4: PartialEq<T11>,
+ T11: PartialEq<T4>,
+ T5: PartialEq<T12>,
+ T12: PartialEq<T5>,
+ T6: PartialEq<T13>,
+ T13: PartialEq<T6>,
+ T7: PartialEq<T14>,
+ T14: PartialEq<T7>,
+> PartialEq<&CompressedPubKey<T, T2, T3, T4, T5, T6, T7>>
+ for CompressedPubKey<T8, T9, T10, T11, T12, T13, T14>
{
#[inline]
- fn eq(&self, other: &&CompressedPubKey<T, T2, T3, T4>) -> bool {
+ fn eq(&self, other: &&CompressedPubKey<T, T2, T3, T4, T5, T6, T7>) -> bool {
*self == **other
}
}
-impl<T: Eq, T2: Eq, T3: Eq, T4: Eq> Eq for CompressedPubKey<T, T2, T3, T4> {}
+impl<T: Eq, T2: Eq, T3: Eq, T4: Eq, T5: Eq, T6: Eq, T7: Eq> Eq
+ for CompressedPubKey<T, T2, T3, T4, T5, T6, T7>
+{
+}
impl<'a> FromCbor<'a> for UncompressedPubKey<'a> {
type Err = CoseKeyErr;
fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
@@ -2121,6 +2602,24 @@ impl<'a> FromCbor<'a> for UncompressedPubKey<'a> {
value: Self::Rsa(key.value),
remaining: key.remaining,
}),
+ // {kty:AKP,alg:ML-DSA-87|ML-DSA-65|ML-DSA-44,...}
+ AKP => cbor
+ .get(5)
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|alg| match *alg {
+ MLDSA44 => MlDsa44PubKey::from_cbor(cbor).map(|key| CborSuccess {
+ value: Self::MlDsa44(key.value),
+ remaining: key.remaining,
+ }),
+ MLDSA65 => MlDsa65PubKey::from_cbor(cbor).map(|key| CborSuccess {
+ value: Self::MlDsa65(key.value),
+ remaining: key.remaining,
+ }),
+ _ => MlDsa87PubKey::from_cbor(cbor).map(|key| CborSuccess {
+ value: Self::MlDsa87(key.value),
+ remaining: key.remaining,
+ }),
+ }),
_ => Err(CoseKeyErr::CoseKeyType),
})
}
@@ -2317,6 +2816,81 @@ impl FromCbor<'_> for NoneAttestation {
})
}
}
+/// A 4627-byte slice that allegedly represents an ML-DSA-87 signature.
+struct MlDsa87Signature<'a>(&'a [u8]);
+impl<'a> FromCbor<'a> for MlDsa87Signature<'a> {
+ type Err = AttestationErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// CBOR metadata describing the signature.
+ /// 18 * 256 + 19 = 4627.
+ const HEADER: [u8; 3] = [cbor::BYTES_INFO_25, 18, 19];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(4627)
+ .ok_or(AttestationErr::Len)
+ .map(|(sig, remaining)| CborSuccess {
+ value: Self(sig),
+ remaining,
+ })
+ } else {
+ Err(AttestationErr::PackedFormatCborMlDsa87Signature)
+ }
+ })
+ }
+}
+/// A 3309-byte slice that allegedly represents an ML-DSA-65 signature.
+struct MlDsa65Signature<'a>(&'a [u8]);
+impl<'a> FromCbor<'a> for MlDsa65Signature<'a> {
+ type Err = AttestationErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// CBOR metadata describing the signature.
+ /// 12 * 256 + 237 = 3309.
+ const HEADER: [u8; 3] = [cbor::BYTES_INFO_25, 12, 237];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(3309)
+ .ok_or(AttestationErr::Len)
+ .map(|(sig, remaining)| CborSuccess {
+ value: Self(sig),
+ remaining,
+ })
+ } else {
+ Err(AttestationErr::PackedFormatCborMlDsa65Signature)
+ }
+ })
+ }
+}
+/// A 2420-byte slice that allegedly represents an ML-DSA-44 signature.
+struct MlDsa44Signature<'a>(&'a [u8]);
+impl<'a> FromCbor<'a> for MlDsa44Signature<'a> {
+ type Err = AttestationErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// CBOR metadata describing the signature.
+ /// 9 * 256 + 116 = 2420.
+ const HEADER: [u8; 3] = [cbor::BYTES_INFO_25, 9, 116];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(2420)
+ .ok_or(AttestationErr::Len)
+ .map(|(sig, remaining)| CborSuccess {
+ value: Self(sig),
+ remaining,
+ })
+ } else {
+ Err(AttestationErr::PackedFormatCborMlDsa44Signature)
+ }
+ })
+ }
+}
/// A 64-byte slice that allegedly represents an Ed25519 signature.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Ed25519Signature<'a>(&'a [u8]);
@@ -2522,6 +3096,12 @@ impl<'a> FromCbor<'a> for RsaPkcs1v15Sig<'a> {
/// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) signature.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Sig<'a> {
+ /// Alleged ML-DSA-87.
+ MlDsa87(&'a [u8]),
+ /// Alleged ML-DSA-65.
+ MlDsa65(&'a [u8]),
+ /// Alleged ML-DSA-44.
+ MlDsa44(&'a [u8]),
/// Alleged Ed25519 signature.
Ed25519(Ed25519Signature<'a>),
/// Alleged DER-encoded P-256 signature using SHA-256.
@@ -2590,26 +3170,68 @@ impl<'a> FromCbor<'a> for PackedAttestation<'a> {
cbor::NEG_INFO_24 => cose_rem
.split_first()
.ok_or(AttestationErr::Len)
- .and_then(|(len, len_rem)| {
- if *len == ES384 {
- len_rem
- .split_at_checked(SIG.len())
- .ok_or(AttestationErr::Len)
- .and_then(|(sig, sig_rem)| {
- if sig == SIG {
- P384DerSig::from_cbor(sig_rem).map(
- |success| CborSuccess {
- value: Sig::P384(success.value.0),
- remaining: success.remaining,
- },
- )
- } else {
- Err(AttestationErr::PackedFormatMissingSig)
- }
- })
- } else {
- Err(AttestationErr::PackedFormatUnsupportedAlg)
- }
+ .and_then(|(len, len_rem)| match *len {
+ ES384 => len_rem
+ .split_at_checked(SIG.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(sig, sig_rem)| {
+ if sig == SIG {
+ P384DerSig::from_cbor(sig_rem).map(|success| {
+ CborSuccess {
+ value: Sig::P384(success.value.0),
+ remaining: success.remaining,
+ }
+ })
+ } else {
+ Err(AttestationErr::PackedFormatMissingSig)
+ }
+ }),
+ MLDSA44 => len_rem
+ .split_at_checked(SIG.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(sig, sig_rem)| {
+ if sig == SIG {
+ MlDsa44Signature::from_cbor(sig_rem).map(
+ |success| CborSuccess {
+ value: Sig::MlDsa44(success.value.0),
+ remaining: success.remaining,
+ },
+ )
+ } else {
+ Err(AttestationErr::PackedFormatMissingSig)
+ }
+ }),
+ MLDSA65 => len_rem
+ .split_at_checked(SIG.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(sig, sig_rem)| {
+ if sig == SIG {
+ MlDsa65Signature::from_cbor(sig_rem).map(
+ |success| CborSuccess {
+ value: Sig::MlDsa65(success.value.0),
+ remaining: success.remaining,
+ },
+ )
+ } else {
+ Err(AttestationErr::PackedFormatMissingSig)
+ }
+ }),
+ MLDSA87 => len_rem
+ .split_at_checked(SIG.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(sig, sig_rem)| {
+ if sig == SIG {
+ MlDsa87Signature::from_cbor(sig_rem).map(
+ |success| CborSuccess {
+ value: Sig::MlDsa87(success.value.0),
+ remaining: success.remaining,
+ },
+ )
+ } else {
+ Err(AttestationErr::PackedFormatMissingSig)
+ }
+ }),
+ _ => Err(AttestationErr::PackedFormatUnsupportedAlg),
}),
cbor::NEG_INFO_25 => cose_rem
.split_at_checked(2)
@@ -2801,6 +3423,21 @@ impl<'a> AttestationObject<'a> {
match att.value {
AttestationFormat::None => Ok(()),
AttestationFormat::Packed(ref val) => match val.signature {
+ Sig::MlDsa87(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::MlDsa87(_)) {
+ Ok(())
+ } else {
+ Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch)
+ },
+ Sig::MlDsa65(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::MlDsa65(_)) {
+ Ok(())
+ } else {
+ Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch)
+ },
+ Sig::MlDsa44(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::MlDsa44(_)) {
+ Ok(())
+ } else {
+ Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch)
+ },
Sig::Ed25519(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::Ed25519(_)) {
Ok(())
} else {
@@ -2964,6 +3601,33 @@ impl AuthResponse for AuthenticatorAttestation {
.map_err(AuthRespErr::Auth)
.and_then(|val| {
match val.data.auth_data.attested_credential_data.credential_public_key {
+ UncompressedPubKey::MlDsa87(key) => {
+ match val.data.attestation {
+ AttestationFormat::None => Ok(()),
+ AttestationFormat::Packed(packed) => match packed.signature {
+ Sig::MlDsa87(sig) => MlDsaSignature::<MlDsa87>::decode(sig.as_array().unwrap_or_else(|| unreachable!("there is a bug in slice::as_array")).into()).ok_or(AuthRespErr::Signature).and_then(|s| key.into_ver_key().verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)),
+ Sig::MlDsa65(_) | Sig::MlDsa44(_) | Sig::Ed25519(_) | Sig::P256(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ }
+ }
+ }
+ UncompressedPubKey::MlDsa65(key) => {
+ match val.data.attestation {
+ AttestationFormat::None => Ok(()),
+ AttestationFormat::Packed(packed) => match packed.signature {
+ Sig::MlDsa65(sig) => MlDsaSignature::<MlDsa65>::decode(sig.as_array().unwrap_or_else(|| unreachable!("there is a bug in slice::as_array")).into()).ok_or(AuthRespErr::Signature).and_then(|s| key.into_ver_key().verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)),
+ Sig::MlDsa87(_) | Sig::MlDsa44(_) | Sig::Ed25519(_) | Sig::P256(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ }
+ }
+ }
+ UncompressedPubKey::MlDsa44(key) => {
+ match val.data.attestation {
+ AttestationFormat::None => Ok(()),
+ AttestationFormat::Packed(packed) => match packed.signature {
+ Sig::MlDsa44(sig) => MlDsaSignature::<MlDsa44>::decode(sig.as_array().unwrap_or_else(|| unreachable!("there is a bug in slice::as_array")).into()).ok_or(AuthRespErr::Signature).and_then(|s| key.into_ver_key().verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)),
+ Sig::MlDsa87(_) | Sig::MlDsa65(_) | Sig::Ed25519(_) | Sig::P256(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ }
+ }
+ }
UncompressedPubKey::Ed25519(key) => key.into_ver_key().map_err(AuthRespErr::PubKey).and_then(|ver_key| {
match val.data.attestation {
AttestationFormat::None => Ok(()),
@@ -2975,7 +3639,7 @@ impl AuthResponse for AuthenticatorAttestation {
// doesn't provide additional benefits and is still not enough to comply
// with standards like RFC 8032 or NIST SP 800-186.
Sig::Ed25519(sig) => ver_key.verify(val.auth_data_and_32_trailing_bytes, &sig.into_sig()).map_err(|_e| AuthRespErr::Signature),
- Sig::P256(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ Sig::MlDsa87(_) | Sig::MlDsa65(_) | Sig::MlDsa44(_) | Sig::P256(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
}
}
}),
@@ -2984,7 +3648,7 @@ impl AuthResponse for AuthenticatorAttestation {
AttestationFormat::None => Ok(()),
AttestationFormat::Packed(packed) => match packed.signature {
Sig::P256(sig) => P256Sig::from_bytes(sig).map_err(|_e| AuthRespErr::Signature).and_then(|s| ver_key.verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)),
- Sig::Ed25519(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ Sig::MlDsa87(_) | Sig::MlDsa65(_) | Sig::MlDsa44(_) | Sig::Ed25519(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
}
}
}),
@@ -2993,7 +3657,7 @@ impl AuthResponse for AuthenticatorAttestation {
AttestationFormat::None => Ok(()),
AttestationFormat::Packed(packed) => match packed.signature {
Sig::P384(sig) => P384Sig::from_bytes(sig).map_err(|_e| AuthRespErr::Signature).and_then(|s| ver_key.verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)),
- Sig::Ed25519(_) | Sig::P256(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ Sig::MlDsa87(_) | Sig::MlDsa65(_) | Sig::MlDsa44(_) | Sig::Ed25519(_) | Sig::P256(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
}
}
}),
@@ -3001,7 +3665,7 @@ impl AuthResponse for AuthenticatorAttestation {
AttestationFormat::None => Ok(()),
AttestationFormat::Packed(packed) => match packed.signature {
Sig::Rs256(sig) => pkcs1v15::Signature::try_from(sig).map_err(|_e| AuthRespErr::Signature).and_then(|s| key.as_ver_key().verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)),
- Sig::Ed25519(_) | Sig::P256(_) | Sig::P384(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ Sig::MlDsa87(_) | Sig::MlDsa65(_) | Sig::MlDsa44(_) | Sig::Ed25519(_) | Sig::P256(_) | Sig::P384(_) => unreachable!("there is a bug in AttestationObject::from_data"),
}
},
}.map(|()| (client_data_json, val.data))
@@ -3389,12 +4053,21 @@ pub struct StaticState<PublicKey> {
/// output during registration that are used during authentication ceremonies.
pub client_extension_results: ClientExtensionsOutputsStaticState,
}
-impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8]>>
- From<&'a StaticState<CompressedPubKey<T, T2, T3, T4>>>
- for StaticState<CompressedPubKey<&'b [u8], &'b [u8], &'b [u8], &'b [u8]>>
+impl<
+ 'a: 'b,
+ 'b,
+ T: AsRef<[u8]>,
+ T2: AsRef<[u8]>,
+ T3: AsRef<[u8]>,
+ T4: AsRef<[u8]>,
+ T5: AsRef<[u8]>,
+ T6: AsRef<[u8]>,
+ T7: AsRef<[u8]>,
+> From<&'a StaticState<CompressedPubKey<T, T2, T3, T4, T5, T6, T7>>>
+ for StaticState<CompressedPubKeyBorrowed<'b>>
{
#[inline]
- fn from(value: &'a StaticState<CompressedPubKey<T, T2, T3, T4>>) -> Self {
+ fn from(value: &'a StaticState<CompressedPubKey<T, T2, T3, T4, T5, T6, T7>>) -> Self {
Self {
credential_public_key: (&value.credential_public_key).into(),
extensions: value.extensions,
@@ -3476,6 +4149,8 @@ mod tests {
};
use ed25519_dalek::Verifier as _;
use p256::ecdsa::{DerSignature as P256Sig, SigningKey as P256Key};
+ #[cfg(not(feature = "serde"))]
+ use pkcs8 as _;
use rsa::sha2::{Digest as _, Sha256};
#[expect(clippy::panic, reason = "OK in tests")]
#[expect(
@@ -3631,7 +4306,12 @@ mod tests {
AttestationFormat::None => false,
AttestationFormat::Packed(attest) => {
match attest.signature {
- Sig::Ed25519(_) | Sig::P384(_) | Sig::Rs256(_) => false,
+ Sig::MlDsa87(_)
+ | Sig::MlDsa65(_)
+ | Sig::MlDsa44(_)
+ | Sig::Ed25519(_)
+ | Sig::P384(_)
+ | Sig::Rs256(_) => false,
Sig::P256(sig) => {
let s = P256Sig::from_bytes(sig).unwrap();
key.verify(
diff --git a/src/response/register/bin.rs b/src/response/register/bin.rs
@@ -5,9 +5,10 @@ use super::{
Aaguid, Attestation, AuthenticationExtensionsPrfOutputs, AuthenticatorAttachment,
AuthenticatorExtensionOutputMetadata, AuthenticatorExtensionOutputStaticState, Backup,
ClientExtensionsOutputsMetadata, ClientExtensionsOutputsStaticState, CompressedP256PubKey,
- CompressedP384PubKey, CompressedPubKey, CredentialPropertiesOutput, CredentialProtectionPolicy,
- DynamicState, Ed25519PubKey, FourToSixtyThree, Metadata, ResidentKeyRequirement, RsaPubKey,
- StaticState, UncompressedP256PubKey, UncompressedP384PubKey, UncompressedPubKey,
+ CompressedP384PubKey, CompressedPubKeyOwned, CredentialPropertiesOutput,
+ CredentialProtectionPolicy, DynamicState, Ed25519PubKey, FourToSixtyThree, Metadata,
+ MlDsa44PubKey, MlDsa65PubKey, MlDsa87PubKey, ResidentKeyRequirement, RsaPubKey, StaticState,
+ UncompressedP256PubKey, UncompressedP384PubKey, UncompressedPubKey,
};
use core::{
convert::Infallible,
@@ -63,6 +64,75 @@ impl<'a> DecodeBuffer<'a> for ResidentKeyRequirement {
})
}
}
+impl EncodeBuffer for MlDsa87PubKey<&[u8]> {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ // We don't rely on `[u8]::encode_into_buffer` since
+ // we always know the `slice` has length 2592; thus
+ // we want to "pretend" this is an array (i.e., don't encode the length).
+ buffer.extend_from_slice(self.0);
+ }
+}
+impl<'a> DecodeBuffer<'a> for MlDsa87PubKey<Box<[u8]>> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ // Only `array`s that implement `Default` implement `DecodeBuffer`;
+ // thus we must manually implement it.
+ let mut key = vec![0; 2592];
+ data.split_at_checked(key.len())
+ .ok_or(EncDecErr)
+ .map(|(key_slice, rem)| {
+ *data = rem;
+ key.copy_from_slice(key_slice);
+ Self(key.into_boxed_slice())
+ })
+ }
+}
+impl EncodeBuffer for MlDsa65PubKey<&[u8]> {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ // We don't rely on `[u8]::encode_into_buffer` since
+ // we always know the `slice` has length 1952; thus
+ // we want to "pretend" this is an array (i.e., don't encode the length).
+ buffer.extend_from_slice(self.0);
+ }
+}
+impl<'a> DecodeBuffer<'a> for MlDsa65PubKey<Box<[u8]>> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ // Only `array`s that implement `Default` implement `DecodeBuffer`;
+ // thus we must manually implement it.
+ let mut key = vec![0; 1952];
+ data.split_at_checked(key.len())
+ .ok_or(EncDecErr)
+ .map(|(key_slice, rem)| {
+ *data = rem;
+ key.copy_from_slice(key_slice);
+ Self(key.into_boxed_slice())
+ })
+ }
+}
+impl EncodeBuffer for MlDsa44PubKey<&[u8]> {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ // We don't rely on `[u8]::encode_into_buffer` since
+ // we always know the `slice` has length 1312; thus
+ // we want to "pretend" this is an array (i.e., don't encode the length).
+ buffer.extend_from_slice(self.0);
+ }
+}
+impl<'a> DecodeBuffer<'a> for MlDsa44PubKey<Box<[u8]>> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ // Only `array`s that implement `Default` implement `DecodeBuffer`;
+ // thus we must manually implement it.
+ let mut key = vec![0; 1312];
+ data.split_at_checked(key.len())
+ .ok_or(EncDecErr)
+ .map(|(key_slice, rem)| {
+ *data = rem;
+ key.copy_from_slice(key_slice);
+ Self(key.into_boxed_slice())
+ })
+ }
+}
impl EncodeBuffer for Ed25519PubKey<&[u8]> {
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
// We don't rely on `[u8]::encode_into_buffer` since
@@ -159,7 +229,7 @@ impl EncodeBuffer for RsaPubKey<&[u8]> {
self.1.encode_into_buffer(buffer);
}
}
-impl<'a> DecodeBuffer<'a> for RsaPubKey<Vec<u8>> {
+impl<'a> DecodeBuffer<'a> for RsaPubKey<Box<[u8]>> {
type Err = EncDecErr;
// We don't verify `Self` is in fact "valid" (i.e., we don't call
// [`Self::validate`]) since that's expensive and an error will
@@ -168,7 +238,7 @@ impl<'a> DecodeBuffer<'a> for RsaPubKey<Vec<u8>> {
// storage in such a way that it's still valid; thus there is no
// benefit in performing "expensive" validation checks.
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- Vec::decode_from_buffer(data).and_then(|n| {
+ Box::decode_from_buffer(data).and_then(|n| {
u32::decode_from_buffer(data)
.and_then(|e| Self::try_from((n, e)).map_err(|_e| EncDecErr))
})
@@ -177,33 +247,38 @@ impl<'a> DecodeBuffer<'a> for RsaPubKey<Vec<u8>> {
impl EncodeBuffer for UncompressedPubKey<'_> {
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
match *self {
- Self::Ed25519(key) => {
+ Self::MlDsa87(key) => {
0u8.encode_into_buffer(buffer);
key.encode_into_buffer(buffer);
}
- Self::P256(key) => {
+ Self::MlDsa65(key) => {
1u8.encode_into_buffer(buffer);
key.encode_into_buffer(buffer);
}
- Self::P384(key) => {
+ Self::MlDsa44(key) => {
2u8.encode_into_buffer(buffer);
key.encode_into_buffer(buffer);
}
- Self::Rsa(key) => {
+ Self::Ed25519(key) => {
3u8.encode_into_buffer(buffer);
key.encode_into_buffer(buffer);
}
+ Self::P256(key) => {
+ 4u8.encode_into_buffer(buffer);
+ key.encode_into_buffer(buffer);
+ }
+ Self::P384(key) => {
+ 5u8.encode_into_buffer(buffer);
+ key.encode_into_buffer(buffer);
+ }
+ Self::Rsa(key) => {
+ 6u8.encode_into_buffer(buffer);
+ key.encode_into_buffer(buffer);
+ }
}
}
}
-impl<'a> DecodeBuffer<'a>
- for CompressedPubKey<
- [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
- [u8; <NistP256 as Curve>::FieldBytesSize::INT],
- [u8; <NistP384 as Curve>::FieldBytesSize::INT],
- Vec<u8>,
- >
-{
+impl<'a> DecodeBuffer<'a> for CompressedPubKeyOwned {
type Err = EncDecErr;
// We don't verify `Self` is in fact "valid" (i.e., we don't call
// [`Self::validate`]) since that's expensive and an error will
@@ -213,10 +288,13 @@ impl<'a> DecodeBuffer<'a>
// benefit in performing "expensive" validation checks.
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
u8::decode_from_buffer(data).and_then(|val| match val {
- 0 => Ed25519PubKey::decode_from_buffer(data).map(Self::Ed25519),
- 1 => CompressedP256PubKey::decode_from_buffer(data).map(Self::P256),
- 2 => CompressedP384PubKey::decode_from_buffer(data).map(Self::P384),
- 3 => RsaPubKey::decode_from_buffer(data).map(Self::Rsa),
+ 0 => MlDsa87PubKey::decode_from_buffer(data).map(Self::MlDsa87),
+ 1 => MlDsa65PubKey::decode_from_buffer(data).map(Self::MlDsa65),
+ 2 => MlDsa44PubKey::decode_from_buffer(data).map(Self::MlDsa44),
+ 3 => Ed25519PubKey::decode_from_buffer(data).map(Self::Ed25519),
+ 4 => CompressedP256PubKey::decode_from_buffer(data).map(Self::P256),
+ 5 => CompressedP384PubKey::decode_from_buffer(data).map(Self::P384),
+ 6 => RsaPubKey::decode_from_buffer(data).map(Self::Rsa),
_ => Err(EncDecErr),
})
}
@@ -468,7 +546,7 @@ impl Encode for StaticState<UncompressedPubKey<'_>> {
Self: 'a;
type Err = Infallible;
/// Transforms `self` into a `Vec` that can subsequently be [`StaticState::decode`]d into a [`StaticState`] of
- /// [`CompressedPubKey`].
+ /// [`CompressedPubKeyOwned`].
#[expect(
clippy::arithmetic_side_effects,
reason = "comment justifies its correctness"
@@ -476,9 +554,12 @@ impl Encode for StaticState<UncompressedPubKey<'_>> {
#[inline]
fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
let mut buffer = Vec::with_capacity(
- // The maximum value is 1 + 2 + 2048 + 4 + 1 + 1 + 1 + 1 + 1 = 2060 so overflow cannot happen.
+ // The maximum value is 2593 so overflow cannot happen.
// `key.0.len() <= MAX_RSA_N_BYTES` which is 2048.
match self.credential_public_key {
+ UncompressedPubKey::MlDsa87(_) => 2593,
+ UncompressedPubKey::MlDsa65(_) => 1953,
+ UncompressedPubKey::MlDsa44(_) => 1313,
UncompressedPubKey::Ed25519(_) => 33,
UncompressedPubKey::P256(_) => 34,
UncompressedPubKey::P384(_) => 50,
@@ -520,22 +601,13 @@ impl Display for DecodeStaticStateErr {
}
}
impl Error for DecodeStaticStateErr {}
-impl Decode
- for StaticState<
- CompressedPubKey<
- [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
- [u8; <NistP256 as Curve>::FieldBytesSize::INT],
- [u8; <NistP384 as Curve>::FieldBytesSize::INT],
- Vec<u8>,
- >,
- >
-{
+impl Decode for StaticState<CompressedPubKeyOwned> {
type Input<'a> = &'a [u8];
type Err = DecodeStaticStateErr;
/// Interprets `input` as the [`StaticState::Output`] of [`StaticState::encode`].
#[inline]
fn decode(mut input: Self::Input<'_>) -> Result<Self, Self::Err> {
- CompressedPubKey::decode_from_buffer(&mut input)
+ CompressedPubKeyOwned::decode_from_buffer(&mut input)
.map_err(|_e| DecodeStaticStateErr::CredentialPublicKey)
.and_then(|credential_public_key| {
AuthenticatorExtensionOutputStaticState::decode_from_buffer(&mut input)
diff --git a/src/response/register/error.rs b/src/response/register/error.rs
@@ -17,8 +17,8 @@ use super::{
AuthenticatorAttestation, AuthenticatorData, AuthenticatorExtensionOutput, Backup,
ClientExtensionsOutputs, CollectedClientData, CompressedP256PubKey, CompressedP384PubKey,
Ed25519PubKey, Ed25519Signature, Flag, MAX_RSA_N_BITS, MIN_RSA_E, MIN_RSA_N_BITS, Metadata,
- PackedAttestation, RsaPubKey, UncompressedP256PubKey, UncompressedP384PubKey,
- UncompressedPubKey,
+ MlDsa44PubKey, MlDsa65PubKey, MlDsa87PubKey, PackedAttestation, RsaPubKey,
+ UncompressedP256PubKey, UncompressedP384PubKey, UncompressedPubKey,
};
use super::{
super::{
@@ -33,30 +33,60 @@ use core::{
error::Error,
fmt::{self, Display, Formatter},
};
-/// Error returned from [`Ed25519PubKey::try_from`] when the `slice` is not 32-bytes in length.
+/// Error returned from [`MlDsa87PubKey::try_from`] when the `slice` is not 2592 bytes in length.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct MlDsa87PubKeyErr;
+impl Display for MlDsa87PubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("the ML-DSA-87 public key is not 2592 bytes in length")
+ }
+}
+impl Error for MlDsa87PubKeyErr {}
+/// Error returned from [`MlDsa65PubKey::try_from`] when the `slice` is not 1952 bytes in length.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct MlDsa65PubKeyErr;
+impl Display for MlDsa65PubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("the ML-DSA-65 public key is not 1952 bytes in length")
+ }
+}
+impl Error for MlDsa65PubKeyErr {}
+/// Error returned from [`MlDsa44PubKey::try_from`] when the `slice` is not 1312 bytes in length.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct MlDsa44PubKeyErr;
+impl Display for MlDsa44PubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("the ML-DSA-44 public key is not 1312 bytes in length")
+ }
+}
+impl Error for MlDsa44PubKeyErr {}
+/// Error returned from [`Ed25519PubKey::try_from`] when the `slice` is not 32 bytes in length.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Ed25519PubKeyErr;
impl Display for Ed25519PubKeyErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- f.write_str("the Ed25519 public key is not 32-bytes in length")
+ f.write_str("the Ed25519 public key is not 32 bytes in length")
}
}
impl Error for Ed25519PubKeyErr {}
/// Error returned from [`UncompressedP256PubKey::try_from`].
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UncompressedP256PubKeyErr {
- /// Variant returned when the x-coordinate is not 32-bytes in length.
+ /// Variant returned when the x-coordinate is not 32 bytes in length.
X,
- /// Variant returned when the y-coordinate is not 32-bytes in length.
+ /// Variant returned when the y-coordinate is not 32 bytes in length.
Y,
}
impl Display for UncompressedP256PubKeyErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
- Self::X => "the P-256 public key x-coordinate is not 32-bytes in length",
- Self::Y => "the P-256 public key y-coordinate is not 32-bytes in length",
+ Self::X => "the P-256 public key x-coordinate is not 32 bytes in length",
+ Self::Y => "the P-256 public key y-coordinate is not 32 bytes in length",
})
}
}
@@ -68,24 +98,24 @@ pub struct CompressedP256PubKeyErr;
impl Display for CompressedP256PubKeyErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- f.write_str("the compressed P-256 public key x-coordinate is not 32-bytes in length")
+ f.write_str("the compressed P-256 public key x-coordinate is not 32 bytes in length")
}
}
impl Error for CompressedP256PubKeyErr {}
/// Error returned from [`UncompressedP384PubKey::try_from`].
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UncompressedP384PubKeyErr {
- /// Variant returned when the x-coordinate is not 48-bytes in length.
+ /// Variant returned when the x-coordinate is not 48 bytes in length.
X,
- /// Variant returned when the y-coordinate is not 48-bytes in length.
+ /// Variant returned when the y-coordinate is not 48 bytes in length.
Y,
}
impl Display for UncompressedP384PubKeyErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
- Self::X => "the P-384 public key x-coordinate is not 48-bytes in length",
- Self::Y => "the P-384 public key y-coordinate is not 48-bytes in length",
+ Self::X => "the P-384 public key x-coordinate is not 48 bytes in length",
+ Self::Y => "the P-384 public key y-coordinate is not 48 bytes in length",
})
}
}
@@ -97,7 +127,7 @@ pub struct CompressedP384PubKeyErr;
impl Display for CompressedP384PubKeyErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- f.write_str("the compressed P-384 public key x-coordinate is not 48-bytes in length")
+ f.write_str("the compressed P-384 public key x-coordinate is not 48 bytes in length")
}
}
impl Error for CompressedP384PubKeyErr {}
@@ -161,18 +191,18 @@ pub struct Ed25519SignatureErr;
impl Display for Ed25519SignatureErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- f.write_str("the Ed25519 signature is not 64-bytes in length")
+ f.write_str("the Ed25519 signature is not 64 bytes in length")
}
}
impl Error for Ed25519SignatureErr {}
/// Error returned from [`Aaguid::try_from`] when the slice is not exactly
-/// 16-bytes in length.
+/// 16 bytes in length.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AaguidErr;
impl Display for AaguidErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- f.write_str("the AAGUID is not 16-bytes in length")
+ f.write_str("the AAGUID is not 16 bytes in length")
}
}
impl Error for AaguidErr {}
@@ -217,8 +247,17 @@ impl Error for AuthenticatorExtensionOutputErr {}
pub enum CoseKeyErr {
/// The `slice` had an invalid length.
Len,
- /// The COSE Key type was not `OKP`, `EC2`, or `RSA`.
+ /// The COSE Key type was not `AKP`, `OKP`, `EC2`, or `RSA`.
CoseKeyType,
+ /// The `slice` was malformed and did not conform to an ML-DSA-87 public key encoded as a COSE Key per
+ /// [Draft IETF COSE Dilithium 10](https://datatracker.ietf.org/doc/html/draft-ietf-cose-dilithium-10).
+ MlDsa87CoseEncoding,
+ /// The `slice` was malformed and did not conform to an ML-DSA-65 public key encoded as a COSE Key per
+ /// [Draft IETF COSE Dilithium 10](https://datatracker.ietf.org/doc/html/draft-ietf-cose-dilithium-10).
+ MlDsa65CoseEncoding,
+ /// The `slice` was malformed and did not conform to an ML-DSA-44 public key encoded as a COSE Key per
+ /// [Draft IETF COSE Dilithium 10](https://datatracker.ietf.org/doc/html/draft-ietf-cose-dilithium-10).
+ MlDsa44CoseEncoding,
/// The `slice` was malformed and did not conform to an Ed25519 public key encoded as a COSE Key per
/// [RFC 9052](https://www.rfc-editor.org/rfc/rfc9052) and [RFC 9053](https://www.rfc-editor.org/rfc/rfc9053).
Ed25519CoseEncoding,
@@ -243,7 +282,16 @@ impl Display for CoseKeyErr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::Len => f.write_str("COSE key data had an invalid length"),
- Self::CoseKeyType => f.write_str("COSE key type was not 'OKP', 'EC2', or 'RSA'"),
+ Self::CoseKeyType => f.write_str("COSE key type was not 'AKP', 'OKP', 'EC2', or 'RSA'"),
+ Self::MlDsa87CoseEncoding => {
+ f.write_str("ML-DSA-87 COSE key was not encoded correctly")
+ }
+ Self::MlDsa65CoseEncoding => {
+ f.write_str("ML-DSA-65 COSE key was not encoded correctly")
+ }
+ Self::MlDsa44CoseEncoding => {
+ f.write_str("ML-DSA-44 COSE key was not encoded correctly")
+ }
Self::Ed25519CoseEncoding => f.write_str("Ed25519 COSE key was not encoded correctly"),
Self::P256CoseEncoding => {
f.write_str("ECDSA with P-256 and SHA-256 COSE key was not encoded correctly")
@@ -384,6 +432,18 @@ pub enum AttestationErr {
PackedFormatUnsupportedAlg,
/// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) did not have a signature.
PackedFormatMissingSig,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) ML-DSA-87 signature CBOR was invalid.
+ PackedFormatCborMlDsa87Signature,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) ML-DSA-87 signature was invalid.
+ PackedFormatMlDsa87,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) ML-DSA-65 signature CBOR was invalid.
+ PackedFormatCborMlDsa65Signature,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) ML-DSA-65 signature was invalid.
+ PackedFormatMlDsa65,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) ML-DSA-44 signature CBOR was invalid.
+ PackedFormatCborMlDsa44Signature,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) ML-DSA-44 signature was invalid.
+ PackedFormatMlDsa44,
/// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) Ed25519 signature CBOR was invalid.
PackedFormatCborEd25519Signature,
/// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) P-256 signature CBOR was invalid.
@@ -412,6 +472,12 @@ impl Display for AttestationErr {
Self::PackedFormatMissingAlg => "CBOR attestation did not have an algorithm for the packed attestation",
Self::PackedFormatUnsupportedAlg => "CBOR attestation had an unsupported algorithm for the packed attestation",
Self::PackedFormatMissingSig => "CBOR attestation did not have a signature for the packed attestation",
+ Self::PackedFormatCborMlDsa87Signature => "CBOR attestation ML-DSA-87 signature had the wrong CBOR format for the packed attestation",
+ Self::PackedFormatMlDsa87 => "CBOR attestation ML-DSA-87 signature was invalid for the packed attestation",
+ Self::PackedFormatCborMlDsa65Signature => "CBOR attestation ML-DSA-65 signature had the wrong CBOR format for the packed attestation",
+ Self::PackedFormatMlDsa65 => "CBOR attestation ML-DSA-65 signature was invalid for the packed attestation",
+ Self::PackedFormatCborMlDsa44Signature => "CBOR attestation ML-DSA-44 signature had the wrong CBOR format for the packed attestation",
+ Self::PackedFormatMlDsa44 => "CBOR attestation ML-DSA-44 signature was invalid for the packed attestation",
Self::PackedFormatCborEd25519Signature => "CBOR attestation Ed25519 signature had the wrong CBOR format for the packed attestation",
Self::PackedFormatCborP256Signature => "CBOR attestation P-256 signature had the wrong CBOR format for the packed attestation",
Self::PackedFormatP256 => "CBOR attestation P-256 signature was invalid for the packed attestation",
diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs
@@ -24,8 +24,8 @@ use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpec
/// the public key in the attestation object.
mod spki {
use super::super::{
- Ed25519PubKey, RsaPubKey, RsaPubKeyErr, UncompressedP256PubKey, UncompressedP384PubKey,
- UncompressedPubKey,
+ Ed25519PubKey, MlDsa44PubKey, MlDsa65PubKey, MlDsa87PubKey, RsaPubKey, RsaPubKeyErr,
+ UncompressedP256PubKey, UncompressedP384PubKey, UncompressedPubKey,
};
use core::fmt::{self, Display, Formatter};
use p256::{
@@ -54,6 +54,21 @@ mod spki {
/// All sequences are constructed once encoded, so this will likely always be used instead of
/// `SEQUENCE`.
const CONSTRUCTED_SEQUENCE: u8 = SEQUENCE | 0b0010_0000;
+ /// Length of the header before the encoded key in a DER-encoded ASN.1 `SubjectPublicKeyInfo`
+ /// for an ML-DSA-87 public key.
+ const MLDSA87_HEADER_LEN: usize = 22;
+ /// Length of a DER-encoded ASN.1 `SubjectPublicKeyInfo` for ML-DSA-87 public key.
+ const MLDSA87_LEN: usize = MLDSA87_HEADER_LEN + 2592;
+ /// Length of the header before the encoded key in a DER-encoded ASN.1 `SubjectPublicKeyInfo`
+ /// for an ML-DSA-65 public key.
+ const MLDSA65_HEADER_LEN: usize = 22;
+ /// Length of a DER-encoded ASN.1 `SubjectPublicKeyInfo` for ML-DSA-65 public key.
+ const MLDSA65_LEN: usize = MLDSA65_HEADER_LEN + 1952;
+ /// Length of the header before the encoded key in a DER-encoded ASN.1 `SubjectPublicKeyInfo`
+ /// for an ML-DSA-44 public key.
+ const MLDSA44_HEADER_LEN: usize = 22;
+ /// Length of a DER-encoded ASN.1 `SubjectPublicKeyInfo` for ML-DSA-44 public key.
+ const MLDSA44_LEN: usize = MLDSA44_HEADER_LEN + 1312;
/// Length of the header before the compressed y-coordinate in a DER-encoded ASN.1 `SubjectPublicKeyInfo`
/// for an Ed25519 public key.
const ED25519_HEADER_LEN: usize = 12;
@@ -109,8 +124,18 @@ mod spki {
const P384_LEN_U8: u8 = P384_LEN as u8;
/// Error returned from [`SubjectPublicKeyInfo::from_der`].
pub(super) enum SubjectPublicKeyInfoErr {
- /// The DER-encoded `SubjectPublicKeyInfo` had an invalid length.
- Len,
+ /// The length of the DER-encoded ML-DSA-87 key was invalid.
+ MlDsa87Len,
+ /// The header of the DER-encoded ML-DSA-87 key was invalid.
+ MlDsa87Header,
+ /// The length of the DER-encoded ML-DSA-65 key was invalid.
+ MlDsa65Len,
+ /// The header of the DER-encoded ML-DSA-65 key was invalid.
+ MlDsa65Header,
+ /// The length of the DER-encoded ML-DSA-44 key was invalid.
+ MlDsa44Len,
+ /// The header of the DER-encoded ML-DSA-44 key was invalid.
+ MlDsa44Header,
/// The length of the DER-encoded Ed25519 key was invalid.
Ed25519Len,
/// The header of the DER-encoded Ed25519 key was invalid.
@@ -135,8 +160,23 @@ mod spki {
impl Display for SubjectPublicKeyInfoErr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
- Self::Len => {
- f.write_str("the DER-encoded SubjectPublicKeyInfo had an invalid length")
+ Self::MlDsa87Len => {
+ f.write_str("length of the DER-encoded ML-DSA-87 key was invalid")
+ }
+ Self::MlDsa87Header => {
+ f.write_str("header of the DER-encoded ML-DSA-87 key was invalid")
+ }
+ Self::MlDsa65Len => {
+ f.write_str("length of the DER-encoded ML-DSA-65 key was invalid")
+ }
+ Self::MlDsa65Header => {
+ f.write_str("header of the DER-encoded ML-DSA-65 key was invalid")
+ }
+ Self::MlDsa44Len => {
+ f.write_str("length of the DER-encoded ML-DSA-44 key was invalid")
+ }
+ Self::MlDsa44Header => {
+ f.write_str("header of the DER-encoded ML-DSA-44 key was invalid")
}
Self::Ed25519Len => {
f.write_str("length of the DER-encoded Ed25519 key was invalid")
@@ -173,6 +213,192 @@ mod spki {
#[expect(single_use_lifetimes, reason = "false positive")]
fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr>;
}
+ impl<'a> SubjectPublicKeyInfo<'a> for MlDsa87PubKey<&'a [u8]> {
+ #[expect(single_use_lifetimes, reason = "false positive")]
+ fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
+ /// ```asn
+ /// SubjectPublicKeyInfo ::= SEQUENCE {
+ /// algorithm AlgorithmIdentifier,
+ /// subjectPublicKey BIT STRING
+ /// }
+ ///
+ /// AlgorithmIdentifier ::= SEQUENCE {
+ /// algorithm OBJECT IDENTIFIER,
+ /// parameters ANY DEFINED BY algorithm OPTIONAL
+ /// }
+ /// ```
+ ///
+ /// [RFC 9882](https://www.rfc-editor.org/rfc/rfc9882.html) requires parameters to not exist
+ /// in `AlgorithmIdentifier`.
+ ///
+ /// RFC 9882 defines the OID as 2.16.840.1.101.3.4.3.19 which is encoded as 96.134.72.1.101.3.4.3.19
+ /// per [X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en).
+ ///
+ /// [FIPS 204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf) defines the bitstring as a reinterpretation of the byte string.
+ const HEADER: [u8; MLDSA87_HEADER_LEN] = [
+ CONSTRUCTED_SEQUENCE,
+ 130,
+ 10,
+ 50,
+ CONSTRUCTED_SEQUENCE,
+ 9 + 2,
+ OID,
+ 9,
+ 96,
+ 134,
+ 72,
+ 1,
+ 101,
+ 3,
+ 4,
+ 3,
+ 19,
+ BITSTRING,
+ 130,
+ 10,
+ 33,
+ // The number of unused bits.
+ 0,
+ ];
+ der.split_at_checked(HEADER.len())
+ .ok_or(SubjectPublicKeyInfoErr::MlDsa87Len)
+ .and_then(|(header, rem)| {
+ if header == HEADER {
+ if rem.len() == 2592 {
+ Ok(Self(rem))
+ } else {
+ Err(SubjectPublicKeyInfoErr::MlDsa87Len)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::MlDsa87Header)
+ }
+ })
+ }
+ }
+ impl<'a> SubjectPublicKeyInfo<'a> for MlDsa65PubKey<&'a [u8]> {
+ #[expect(single_use_lifetimes, reason = "false positive")]
+ fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
+ /// ```asn
+ /// SubjectPublicKeyInfo ::= SEQUENCE {
+ /// algorithm AlgorithmIdentifier,
+ /// subjectPublicKey BIT STRING
+ /// }
+ ///
+ /// AlgorithmIdentifier ::= SEQUENCE {
+ /// algorithm OBJECT IDENTIFIER,
+ /// parameters ANY DEFINED BY algorithm OPTIONAL
+ /// }
+ /// ```
+ ///
+ /// [RFC 9882](https://www.rfc-editor.org/rfc/rfc9882.html) requires parameters to not exist
+ /// in `AlgorithmIdentifier`.
+ ///
+ /// RFC 9882 defines the OID as 2.16.840.1.101.3.4.3.18 which is encoded as 96.134.72.1.101.3.4.3.18
+ /// per [X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en).
+ ///
+ /// [FIPS 204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf) defines the bitstring as a reinterpretation of the byte string.
+ const HEADER: [u8; MLDSA65_HEADER_LEN] = [
+ CONSTRUCTED_SEQUENCE,
+ 130,
+ 7,
+ 178,
+ CONSTRUCTED_SEQUENCE,
+ 9 + 2,
+ OID,
+ 9,
+ 96,
+ 134,
+ 72,
+ 1,
+ 101,
+ 3,
+ 4,
+ 3,
+ 18,
+ BITSTRING,
+ 130,
+ 7,
+ 161,
+ // The number of unused bits.
+ 0,
+ ];
+ der.split_at_checked(HEADER.len())
+ .ok_or(SubjectPublicKeyInfoErr::MlDsa65Len)
+ .and_then(|(header, rem)| {
+ if header == HEADER {
+ if rem.len() == 1952 {
+ Ok(Self(rem))
+ } else {
+ Err(SubjectPublicKeyInfoErr::MlDsa65Len)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::MlDsa65Header)
+ }
+ })
+ }
+ }
+ impl<'a> SubjectPublicKeyInfo<'a> for MlDsa44PubKey<&'a [u8]> {
+ #[expect(single_use_lifetimes, reason = "false positive")]
+ fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
+ /// ```asn
+ /// SubjectPublicKeyInfo ::= SEQUENCE {
+ /// algorithm AlgorithmIdentifier,
+ /// subjectPublicKey BIT STRING
+ /// }
+ ///
+ /// AlgorithmIdentifier ::= SEQUENCE {
+ /// algorithm OBJECT IDENTIFIER,
+ /// parameters ANY DEFINED BY algorithm OPTIONAL
+ /// }
+ /// ```
+ ///
+ /// [RFC 9882](https://www.rfc-editor.org/rfc/rfc9882.html) requires parameters to not exist
+ /// in `AlgorithmIdentifier`.
+ ///
+ /// RFC 9882 defines the OID as 2.16.840.1.101.3.4.3.17 which is encoded as 96.134.72.1.101.3.4.3.17
+ /// per [X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en).
+ ///
+ /// [FIPS 204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf) defines the bitstring as a reinterpretation of the byte string.
+ const HEADER: [u8; MLDSA44_HEADER_LEN] = [
+ CONSTRUCTED_SEQUENCE,
+ 130,
+ 5,
+ 50,
+ CONSTRUCTED_SEQUENCE,
+ 9 + 2,
+ OID,
+ 9,
+ 96,
+ 134,
+ 72,
+ 1,
+ 101,
+ 3,
+ 4,
+ 3,
+ 17,
+ BITSTRING,
+ 130,
+ 5,
+ 33,
+ // The number of unused bits.
+ 0,
+ ];
+ der.split_at_checked(HEADER.len())
+ .ok_or(SubjectPublicKeyInfoErr::MlDsa44Len)
+ .and_then(|(header, rem)| {
+ if header == HEADER {
+ if rem.len() == 1312 {
+ Ok(Self(rem))
+ } else {
+ Err(SubjectPublicKeyInfoErr::MlDsa44Len)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::MlDsa44Header)
+ }
+ })
+ }
+ }
impl<'a> SubjectPublicKeyInfo<'a> for Ed25519PubKey<&'a [u8]> {
#[expect(single_use_lifetimes, reason = "false positive")]
fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
@@ -647,18 +873,47 @@ mod spki {
}
}
impl<'a> SubjectPublicKeyInfo<'a> for UncompressedPubKey<'a> {
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
#[expect(single_use_lifetimes, reason = "false positive")]
fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
- // The lengths of the three key types do not overlap.
+ /// Index in a DER-encoded payload of the ML-DSA-* public key that corresponds
+ /// to the OID length.
+ const ML_DSA_OID_LEN_IDX: usize = 5;
+ /// Length of the OID for the DER-encoded ML-DSA-* public keys.
+ ///
+ /// Note this is _not_ the same as the OID length for an RSA public key (i.e., 13).
+ const ML_DSA_OID_LEN: u8 = 11;
+ // The only lengths that overlap is RSA with ML-DSA-44 and ML-DSA-65.
match der.len() {
// The minimum modulus we support for RSA is 2048 bits which is 256 bytes;
// thus clearly its encoding will be at least 256 which is greater than
- // all of the other values.
+ // all of the non-ML-DSA-* values and less than the ML-DSA-* values.
+ // The maximum modulus we support for RSA is 16K bits which is 2048 bytes and the maximum
+ // exponent we support for RSA is 4 bytes which is hundreds of bytes less than 2614
+ // (i.e., the length of a DER-encoded ML-DSAS-87 public key).
ED25519_LEN => Ed25519PubKey::from_der(der).map(Self::Ed25519),
P256_LEN => UncompressedP256PubKey::from_der(der).map(Self::P256),
P384_LEN => UncompressedP384PubKey::from_der(der).map(Self::P384),
- 256.. => RsaPubKey::from_der(der).map(Self::Rsa),
- _ => Err(SubjectPublicKeyInfoErr::Len),
+ MLDSA44_LEN => {
+ // `ML_DSA_OID_LEN_IDX < MLDSA44_LEN = der.len()` so indexing
+ // won't `panic`.
+ if der[ML_DSA_OID_LEN_IDX] == ML_DSA_OID_LEN {
+ MlDsa44PubKey::from_der(der).map(Self::MlDsa44)
+ } else {
+ RsaPubKey::from_der(der).map(Self::Rsa)
+ }
+ }
+ MLDSA65_LEN => {
+ // `ML_DSA_OID_LEN_IDX < MLDSA65_LEN = der.len()` so indexing
+ // won't `panic`.
+ if der[ML_DSA_OID_LEN_IDX] == ML_DSA_OID_LEN {
+ MlDsa65PubKey::from_der(der).map(Self::MlDsa65)
+ } else {
+ RsaPubKey::from_der(der).map(Self::Rsa)
+ }
+ }
+ MLDSA87_LEN => MlDsa87PubKey::from_der(der).map(Self::MlDsa87),
+ _ => RsaPubKey::from_der(der).map(Self::Rsa),
}
}
}
@@ -903,6 +1158,9 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
|flag| {
if R {
attested_info.as_ref().map_or(Ok(None), |&(ref attested_data, cred_id_start)| Ok(Some((match attested_data.credential_public_key {
+ UncompressedPubKey::MlDsa87(_) => CoseAlgorithmIdentifier::Mldsa87,
+ UncompressedPubKey::MlDsa65(_) => CoseAlgorithmIdentifier::Mldsa65,
+ UncompressedPubKey::MlDsa44(_) => CoseAlgorithmIdentifier::Mldsa44,
UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
@@ -917,6 +1175,21 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
attested_info.as_ref().map_or_else(
|| AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
match att_obj.auth_data.attested_credential_data.credential_public_key {
+ UncompressedPubKey::MlDsa87(_) => {
+ // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
+ // is the start of the raw authenticator data which itself contains the raw Credential ID.
+ Ok(Some((CoseAlgorithmIdentifier::Mldsa87, auth_idx, auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
+ }
+ UncompressedPubKey::MlDsa65(_) => {
+ // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
+ // is the start of the raw authenticator data which itself contains the raw Credential ID.
+ Ok(Some((CoseAlgorithmIdentifier::Mldsa65, auth_idx, auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
+ }
+ UncompressedPubKey::MlDsa44(_) => {
+ // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
+ // is the start of the raw authenticator data which itself contains the raw Credential ID.
+ Ok(Some((CoseAlgorithmIdentifier::Mldsa44, auth_idx, auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
+ }
UncompressedPubKey::P384(_) => {
// This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
// is the start of the raw authenticator data which itself contains the raw Credential ID.
@@ -927,6 +1200,21 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
}),
|&(ref attested_data, cred_id_start)| {
match attested_data.credential_public_key {
+ UncompressedPubKey::MlDsa87(_) => {
+ // Overflow won't occur since this is correct. This is correct since we successfully parsed
+ // `AttestedCredentialData` and calculated `cred_id_start` from it.
+ Ok(Some((CoseAlgorithmIdentifier::Mldsa87, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
+ }
+ UncompressedPubKey::MlDsa65(_) => {
+ // Overflow won't occur since this is correct. This is correct since we successfully parsed
+ // `AttestedCredentialData` and calculated `cred_id_start` from it.
+ Ok(Some((CoseAlgorithmIdentifier::Mldsa65, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
+ }
+ UncompressedPubKey::MlDsa44(_) => {
+ // Overflow won't occur since this is correct. This is correct since we successfully parsed
+ // `AttestedCredentialData` and calculated `cred_id_start` from it.
+ Ok(Some((CoseAlgorithmIdentifier::Mldsa44, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
+ }
UncompressedPubKey::P384(_) => {
// Overflow won't occur since this is correct. This is correct since we successfully parsed
// `AttestedCredentialData` and calculated `cred_id_start` from it.
@@ -944,6 +1232,9 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
|| AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
if key == att_obj.auth_data.attested_credential_data.credential_public_key {
let alg = match att_obj.auth_data.attested_credential_data.credential_public_key {
+ UncompressedPubKey::MlDsa87(_) => CoseAlgorithmIdentifier::Mldsa87,
+ UncompressedPubKey::MlDsa65(_) => CoseAlgorithmIdentifier::Mldsa65,
+ UncompressedPubKey::MlDsa44(_) => CoseAlgorithmIdentifier::Mldsa44,
UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
@@ -959,6 +1250,9 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
|&(ref attested_data, cred_id_start)| {
if key == attested_data.credential_public_key {
let alg = match attested_data.credential_public_key {
+ UncompressedPubKey::MlDsa87(_) => CoseAlgorithmIdentifier::Mldsa87,
+ UncompressedPubKey::MlDsa65(_) => CoseAlgorithmIdentifier::Mldsa65,
+ UncompressedPubKey::MlDsa44(_) => CoseAlgorithmIdentifier::Mldsa44,
UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
@@ -989,6 +1283,9 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
cred_key_alg_cred_info.map_or_else(
|| AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
let att_obj_alg = match att_obj.auth_data.attested_credential_data.credential_public_key {
+ UncompressedPubKey::MlDsa87(_) => CoseAlgorithmIdentifier::Mldsa87,
+ UncompressedPubKey::MlDsa65(_) => CoseAlgorithmIdentifier::Mldsa65,
+ UncompressedPubKey::MlDsa44(_) => CoseAlgorithmIdentifier::Mldsa44,
UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
@@ -1363,7 +1660,7 @@ impl<'de> Deserialize<'de> for Registration {
let id = cred.id.unwrap_or_else(|| unreachable!("there is a bug in PublicKeyCredential::deserialize"));
cred.response.cred_info.map_or_else(
|| AttestationObject::try_from(cred.response.attest.attestation_object()).map_err(Error::custom).and_then(|att_obj| {
- if id == att_obj.auth_data.attested_credential_data.credential_id {
+ if id.as_ref() == att_obj.auth_data.attested_credential_data.credential_id.as_ref() {
Ok(())
} else {
Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", att_obj.auth_data.attested_credential_data.credential_id.0).as_str()))
@@ -1372,7 +1669,7 @@ impl<'de> Deserialize<'de> for Registration {
// `start` and `last` were calculated based on `cred.response.attest.attestation_object()`
// and represent the starting and ending index of the `CredentialId`; therefore this is correct
// let alone won't `panic`.
- |(start, last)| if id.0 == cred.response.attest.attestation_object()[start..last] {
+ |(start, last)| if *id.0 == cred.response.attest.attestation_object()[start..last] {
Ok(())
} else {
Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", &cred.response.attest.attestation_object()[start..last]).as_str()))
@@ -1385,18 +1682,21 @@ impl<'de> Deserialize<'de> for Registration {
mod tests {
use super::{
super::{
- ALG, AuthenticatorAttachment, EC2, EDDSA, ES256, ES384, Ed25519PubKey, KTY, OKP, RSA,
+ AKP, ALG, AuthenticatorAttachment, EC2, EDDSA, ES256, ES384, Ed25519PubKey, KTY,
+ MLDSA44, MLDSA65, MLDSA87, MlDsa44PubKey, MlDsa65PubKey, MlDsa87PubKey, OKP, RSA,
Registration, RsaPubKey, UncompressedP256PubKey, UncompressedP384PubKey, cbor,
},
CoseAlgorithmIdentifier,
spki::SubjectPublicKeyInfo as _,
};
use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey as _};
+ use ml_dsa::{MlDsa44, MlDsa65, MlDsa87, VerifyingKey as MlDsaVerKey};
use p256::{
EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key,
elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _},
};
use p384::{EncodedPoint as P384Pt, PublicKey as P384PubKey, SecretKey as P384Key};
+ use pkcs8::EncodePublicKey as _;
use rsa::{
BigUint, RsaPrivateKey,
sha2::{Digest as _, Sha256},
@@ -1406,6 +1706,45 @@ mod tests {
use serde_json::Error;
#[expect(clippy::unwrap_used, reason = "OK in tests")]
#[test]
+ fn mldsa87_spki() {
+ assert!(
+ MlDsa87PubKey::from_der(
+ MlDsaVerKey::<MlDsa87>::decode(&[1; 2592].into())
+ .to_public_key_der()
+ .unwrap()
+ .as_bytes()
+ )
+ .is_ok_and(|k| k.0 == [1; 2592])
+ );
+ }
+ #[expect(clippy::unwrap_used, reason = "OK in tests")]
+ #[test]
+ fn mldsa65_spki() {
+ assert!(
+ MlDsa65PubKey::from_der(
+ MlDsaVerKey::<MlDsa65>::decode(&[1; 1952].into())
+ .to_public_key_der()
+ .unwrap()
+ .as_bytes()
+ )
+ .is_ok_and(|k| k.0 == [1; 1952])
+ );
+ }
+ #[expect(clippy::unwrap_used, reason = "OK in tests")]
+ #[test]
+ fn mldsa44_spki() {
+ assert!(
+ MlDsa44PubKey::from_der(
+ MlDsaVerKey::<MlDsa44>::decode(&[1; 1312].into())
+ .to_public_key_der()
+ .unwrap()
+ .as_bytes()
+ )
+ .is_ok_and(|k| k.0 == [1; 1312])
+ );
+ }
+ #[expect(clippy::unwrap_used, reason = "OK in tests")]
+ #[test]
fn ed25519_spki() {
assert!(
Ed25519PubKey::from_der(
@@ -3915,6 +4254,7053 @@ mod tests {
Some(err.as_slice())
);
}
+ #[expect(
+ clippy::assertions_on_result_states,
+ clippy::unwrap_used,
+ reason = "OK in tests"
+ )]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ fn mldsa87_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj: [u8; 2704] = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_25,
+ 10,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // ML-DSA-87 COSE key.
+ cbor::MAP_3,
+ KTY,
+ AKP,
+ ALG,
+ cbor::NEG_INFO_24,
+ MLDSA87,
+ // `pub`.
+ cbor::NEG_ONE,
+ cbor::BYTES_INFO_25,
+ 10,
+ 32,
+ // Encoded key.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = MlDsaVerKey::<MlDsa87>::decode(&[1u8; 2592].into())
+ .to_public_key_der()
+ .unwrap();
+ let att_obj_len = att_obj.len();
+ let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 2673..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -50i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok_and(
+ |reg| reg.response.client_data_json == c_data_json.as_bytes()
+ && reg.response.attestation_object_and_c_data_hash[..att_obj_len] == att_obj
+ && reg.response.attestation_object_and_c_data_hash[att_obj_len..]
+ == *Sha256::digest(c_data_json.as_bytes())
+ && reg.response.transports.is_empty()
+ && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
+ && reg.client_extension_results.cred_props.is_none()
+ && reg.client_extension_results.prf.is_none()
+ )
+ );
+ // `publicKeyAlgorithm` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa87).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKeyAlgorithm`.
+ err = Error::missing_field("publicKeyAlgorithm")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `null` `publicKeyAlgorithm`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `publicKey` mismatch.
+ let bad_pub_key = MlDsaVerKey::<MlDsa87>::decode(&[2; 2592].into());
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: MlDsa87(MlDsa87PubKey({:?}))",
+ &[1u8; 2592]
+ )
+ .as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -50i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes().get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -50i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa87).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `null` `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -50i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` is null.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa87).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ }
+ #[expect(
+ clippy::assertions_on_result_states,
+ clippy::unwrap_used,
+ reason = "OK in tests"
+ )]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ fn mldsa65_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj: [u8; 2064] = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_25,
+ 7,
+ 241,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // ML-DSA-65 COSE key.
+ cbor::MAP_3,
+ KTY,
+ AKP,
+ ALG,
+ cbor::NEG_INFO_24,
+ MLDSA65,
+ // `pub`.
+ cbor::NEG_ONE,
+ cbor::BYTES_INFO_25,
+ 7,
+ 160,
+ // Encoded key.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = MlDsaVerKey::<MlDsa65>::decode(&[1u8; 1952].into())
+ .to_public_key_der()
+ .unwrap();
+ let att_obj_len = att_obj.len();
+ let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 2033..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -49i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok_and(
+ |reg| reg.response.client_data_json == c_data_json.as_bytes()
+ && reg.response.attestation_object_and_c_data_hash[..att_obj_len] == att_obj
+ && reg.response.attestation_object_and_c_data_hash[att_obj_len..]
+ == *Sha256::digest(c_data_json.as_bytes())
+ && reg.response.transports.is_empty()
+ && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
+ && reg.client_extension_results.cred_props.is_none()
+ && reg.client_extension_results.prf.is_none()
+ )
+ );
+ // `publicKeyAlgorithm` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa65).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKeyAlgorithm`.
+ err = Error::missing_field("publicKeyAlgorithm")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `null` `publicKeyAlgorithm`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `publicKey` mismatch.
+ let bad_pub_key = MlDsaVerKey::<MlDsa65>::decode(&[2; 1952].into());
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: MlDsa65(MlDsa65PubKey({:?}))",
+ &[1u8; 1952]
+ )
+ .as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -49i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes().get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -49i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa65).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `null` `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -49i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` is null.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa65).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ }
+ #[expect(
+ clippy::assertions_on_result_states,
+ clippy::unwrap_used,
+ reason = "OK in tests"
+ )]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ fn mldsa44_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj: [u8; 1424] = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_25,
+ 5,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // ML-DSA-44 COSE key.
+ cbor::MAP_3,
+ KTY,
+ AKP,
+ ALG,
+ cbor::NEG_INFO_24,
+ MLDSA44,
+ // `pub`.
+ cbor::NEG_ONE,
+ cbor::BYTES_INFO_25,
+ 5,
+ 32,
+ // Encoded key.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = MlDsaVerKey::<MlDsa44>::decode(&[1u8; 1312].into())
+ .to_public_key_der()
+ .unwrap();
+ let att_obj_len = att_obj.len();
+ let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 1393..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -48i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok_and(
+ |reg| reg.response.client_data_json == c_data_json.as_bytes()
+ && reg.response.attestation_object_and_c_data_hash[..att_obj_len] == att_obj
+ && reg.response.attestation_object_and_c_data_hash[att_obj_len..]
+ == *Sha256::digest(c_data_json.as_bytes())
+ && reg.response.transports.is_empty()
+ && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
+ && reg.client_extension_results.cred_props.is_none()
+ && reg.client_extension_results.prf.is_none()
+ )
+ );
+ // `publicKeyAlgorithm` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa44).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKeyAlgorithm`.
+ err = Error::missing_field("publicKeyAlgorithm")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `null` `publicKeyAlgorithm`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `publicKey` mismatch.
+ let bad_pub_key = MlDsaVerKey::<MlDsa44>::decode(&[2; 1312].into());
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: MlDsa44(MlDsa44PubKey({:?}))",
+ &[1u8; 1312]
+ )
+ .as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -48i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes().get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -48i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa44).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `null` `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -48i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` is null.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa44).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ }
#[expect(clippy::unwrap_used, reason = "OK in tests")]
#[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
#[expect(clippy::too_many_lines, reason = "a lot to test")]
diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs
@@ -185,7 +185,7 @@ impl<'de> Deserialize<'de> for RegistrationRelaxed {
cred.id.map_or_else(|| Ok(()), |id| {
cred.response.0.cred_info.map_or_else(
|| AttestationObject::try_from(cred.response.0.attest.attestation_object()).map_err(Error::custom).and_then(|att_obj| {
- if id == att_obj.auth_data.attested_credential_data.credential_id {
+ if id.as_ref() == att_obj.auth_data.attested_credential_data.credential_id.as_ref() {
Ok(())
} else {
Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", att_obj.auth_data.attested_credential_data.credential_id.0).as_str()))
@@ -194,7 +194,7 @@ impl<'de> Deserialize<'de> for RegistrationRelaxed {
// `start` and `last` were calculated based on `cred.response.attest.attestation_object()`
// and represent the starting and ending index of the `CredentialId`; therefore this is correct
// let alone won't `panic`.
- |(start, last)| if id.0 == cred.response.0.attest.attestation_object()[start..last] {
+ |(start, last)| if *id.0 == cred.response.0.attest.attestation_object()[start..last] {
Ok(())
} else {
Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", &cred.response.0.attest.attestation_object()[start..last]).as_str()))
@@ -437,17 +437,20 @@ impl<'de> Deserialize<'de> for CustomRegistration {
mod tests {
use super::{
super::{
- super::super::request::register::CoseAlgorithmIdentifier, ALG, AuthenticatorAttachment,
- EC2, EDDSA, ES256, ES384, KTY, OKP, RSA, cbor,
+ super::super::request::register::CoseAlgorithmIdentifier, AKP, ALG,
+ AuthenticatorAttachment, EC2, EDDSA, ES256, ES384, KTY, MLDSA44, MLDSA65, MLDSA87, OKP,
+ RSA, cbor,
},
CustomRegistration, RegistrationRelaxed,
};
use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey as _};
+ use ml_dsa::{MlDsa44, MlDsa65, MlDsa87, VerifyingKey as MlDsaVerKey};
use p256::{
EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key,
elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _},
};
use p384::{EncodedPoint as P384Pt, PublicKey as P384PubKey, SecretKey as P384Key};
+ use pkcs8::EncodePublicKey as _;
use rsa::{
BigUint, RsaPrivateKey,
sha2::{Digest as _, Sha256},
@@ -3132,6 +3135,7020 @@ mod tests {
Some(err.as_slice())
);
}
+ #[expect(
+ clippy::assertions_on_result_states,
+ clippy::unwrap_used,
+ reason = "OK in tests"
+ )]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ fn mldsa87_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj: [u8; 2704] = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_25,
+ 10,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // ML-DSA-87 COSE key.
+ cbor::MAP_3,
+ KTY,
+ AKP,
+ ALG,
+ cbor::NEG_INFO_24,
+ MLDSA87,
+ // `pub`.
+ cbor::NEG_ONE,
+ cbor::BYTES_INFO_25,
+ 10,
+ 32,
+ // Encoded key.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = MlDsaVerKey::<MlDsa87>::decode(&[1u8; 2592].into())
+ .to_public_key_der()
+ .unwrap();
+ let att_obj_len = att_obj.len();
+ let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 2673..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -50i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok_and(
+ |reg| reg.0.response.client_data_json == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj_len] == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj_len..]
+ == *Sha256::digest(c_data_json.as_bytes())
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none()
+ )
+ );
+ // `publicKeyAlgorithm` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa87).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKeyAlgorithm`.
+ drop(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str(),
+ )
+ .unwrap(),
+ );
+ // `null` `publicKeyAlgorithm`.
+ drop(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str(),
+ )
+ .unwrap(),
+ );
+ // `publicKey` mismatch.
+ let bad_pub_key = MlDsaVerKey::<MlDsa87>::decode(&[2; 2592].into());
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: MlDsa87(MlDsa87PubKey({:?}))",
+ &[1u8; 2592]
+ )
+ .as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -50i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes().get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -50i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa87).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `null` `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -50i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` is null.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa87).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ }
+ #[expect(
+ clippy::assertions_on_result_states,
+ clippy::unwrap_used,
+ reason = "OK in tests"
+ )]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ fn mldsa65_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj: [u8; 2064] = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_25,
+ 7,
+ 241,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // ML-DSA-65 COSE key.
+ cbor::MAP_3,
+ KTY,
+ AKP,
+ ALG,
+ cbor::NEG_INFO_24,
+ MLDSA65,
+ // `pub`.
+ cbor::NEG_ONE,
+ cbor::BYTES_INFO_25,
+ 7,
+ 160,
+ // Encoded key.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = MlDsaVerKey::<MlDsa65>::decode(&[1u8; 1952].into())
+ .to_public_key_der()
+ .unwrap();
+ let att_obj_len = att_obj.len();
+ let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 2033..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -49i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok_and(
+ |reg| reg.0.response.client_data_json == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj_len] == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj_len..]
+ == *Sha256::digest(c_data_json.as_bytes())
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none()
+ )
+ );
+ // `publicKeyAlgorithm` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa65).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKeyAlgorithm`.
+ drop(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str(),
+ )
+ .unwrap(),
+ );
+ // `null` `publicKeyAlgorithm`.
+ drop(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str(),
+ )
+ .unwrap(),
+ );
+ // `publicKey` mismatch.
+ let bad_pub_key = MlDsaVerKey::<MlDsa65>::decode(&[2; 1952].into());
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: MlDsa65(MlDsa65PubKey({:?}))",
+ &[1u8; 1952]
+ )
+ .as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -49i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes().get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -49i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa65).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `null` `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -49i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` is null.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa65).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ }
+ #[expect(
+ clippy::assertions_on_result_states,
+ clippy::unwrap_used,
+ reason = "OK in tests"
+ )]
+ #[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+ #[expect(clippy::too_many_lines, reason = "a lot to test")]
+ #[test]
+ fn mldsa44_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj: [u8; 1424] = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_25,
+ 5,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // ML-DSA-44 COSE key.
+ cbor::MAP_3,
+ KTY,
+ AKP,
+ ALG,
+ cbor::NEG_INFO_24,
+ MLDSA44,
+ // `pub`.
+ cbor::NEG_ONE,
+ cbor::BYTES_INFO_25,
+ 5,
+ 32,
+ // Encoded key.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = MlDsaVerKey::<MlDsa44>::decode(&[1u8; 1312].into())
+ .to_public_key_der()
+ .unwrap();
+ let att_obj_len = att_obj.len();
+ let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 1393..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -48i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok_and(
+ |reg| reg.0.response.client_data_json == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj_len] == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj_len..]
+ == *Sha256::digest(c_data_json.as_bytes())
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none()
+ )
+ );
+ // `publicKeyAlgorithm` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa44).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKeyAlgorithm`.
+ drop(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str(),
+ )
+ .unwrap(),
+ );
+ // `null` `publicKeyAlgorithm`.
+ drop(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str(),
+ )
+ .unwrap(),
+ );
+ // `publicKey` mismatch.
+ let bad_pub_key = MlDsaVerKey::<MlDsa44>::decode(&[2; 1312].into());
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: MlDsa44(MlDsa44PubKey({:?}))",
+ &[1u8; 1312]
+ )
+ .as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -48i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes().get(..err.len()),
+ Some(err.as_slice())
+ );
+ // Missing `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -48i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa44).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ // `null` `publicKey` is allowed when not using EdDSA, ES256, or RS256.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -48i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch when `publicKey` is null.
+ err = Error::invalid_value(
+ Unexpected::Other(format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Mldsa44).as_str()
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata_json,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -7i8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()
+ .get(..err.len()),
+ Some(err.as_slice())
+ );
+ }
#[expect(clippy::unwrap_used, reason = "OK in tests")]
#[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
#[expect(clippy::too_many_lines, reason = "a lot to test")]
diff --git a/src/response/ser.rs b/src/response/ser.rs
@@ -236,7 +236,7 @@ impl<T: AsRef<[u8]>> Serialize for CredentialId<T> {
/// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
/// # #[cfg(feature = "custom")]
/// assert_eq!(
- /// serde_json::to_string(&CredentialId::try_from(vec![0; 16])?).unwrap(),
+ /// serde_json::to_string(&CredentialId::try_from(vec![0; 16].into_boxed_slice())?).unwrap(),
/// r#""AAAAAAAAAAAAAAAAAAAAAA""#
/// );
/// # Ok::<_, webauthn_rp::AggErr>(())
@@ -249,7 +249,7 @@ impl<T: AsRef<[u8]>> Serialize for CredentialId<T> {
serializer.serialize_str(base64url_nopad::encode(self.0.as_ref()).as_str())
}
}
-impl<'de> Deserialize<'de> for CredentialId<Vec<u8>> {
+impl<'de> Deserialize<'de> for CredentialId<Box<[u8]>> {
/// Deserializes [`prim@str`] based on
/// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptorjson-id).
///
@@ -260,7 +260,7 @@ impl<'de> Deserialize<'de> for CredentialId<Vec<u8>> {
/// # #[cfg(feature = "custom")]
/// assert_eq!(
/// serde_json::from_str::<CredentialId<_>>(r#""AAAAAAAAAAAAAAAAAAAAAA""#).unwrap(),
- /// CredentialId::try_from(vec![0; 16])?
+ /// CredentialId::try_from(vec![0; 16].into_boxed_slice())?
/// );
/// # Ok::<_, webauthn_rp::AggErr>(())
///```
@@ -272,7 +272,7 @@ impl<'de> Deserialize<'de> for CredentialId<Vec<u8>> {
/// `Visitor` for `CredentialId`.
struct CredentialIdVisitor;
impl Visitor<'_> for CredentialIdVisitor {
- type Value = CredentialId<Vec<u8>>;
+ type Value = CredentialId<Box<[u8]>>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("CredentialId")
}
@@ -287,7 +287,7 @@ impl<'de> Deserialize<'de> for CredentialId<Vec<u8>> {
if (MIN_LEN..=MAX_LEN).contains(&v.len()) {
base64url_nopad::decode(v.as_bytes())
.map_err(E::custom)
- .map(CredentialId)
+ .map(|c| CredentialId(c.into_boxed_slice()))
} else {
Err(E::invalid_value(
Unexpected::Str(v),
@@ -505,7 +505,7 @@ pub(super) trait ClientExtensions: Sized {
/// `RELAXED` and `REG` are used purely for deserialization purposes.
pub(super) struct PublicKeyCredential<const RELAXED: bool, const REG: bool, AuthResp, Ext> {
/// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
- pub id: Option<CredentialId<Vec<u8>>>,
+ pub id: Option<CredentialId<Box<[u8]>>>,
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
pub response: AuthResp,
/// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
@@ -803,9 +803,9 @@ where
/// # };
/// /// Retrieves the `CredentialId`s associated with `user_id` from the database.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// fn get_credential_ids(user_id: UserHandle<USER_HANDLE_MIN_LEN>) -> Result<Vec<CredentialId<Vec<u8>>>, CredentialIdErr> {
+ /// fn get_credential_ids(user_id: UserHandle<USER_HANDLE_MIN_LEN>) -> Result<Vec<CredentialId<Box<[u8]>>>, CredentialIdErr> {
/// // ⋮
- /// # CredentialId::decode(vec![0; 16]).map(|cred_id| vec![cred_id])
+ /// # CredentialId::decode(vec![0; 16].into_boxed_slice()).map(|cred_id| vec![cred_id])
/// }
/// /// Retrieves the `UserHandle` from a session cookie.
/// # #[cfg(feature = "custom")]