webauthn_rp

WebAuthn Level 3 RP library.
git clone https://git.philomathiclife.com/repos/webauthn_rp
Log | Files | Refs | README

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:
MCargo.toml | 3+++
Msrc/bin.rs | 19+++++++++++++++++++
Msrc/lib.rs | 2+-
Msrc/request.rs | 7081++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/request/auth.rs | 72+++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/request/auth/ser.rs | 6+++---
Msrc/request/auth/ser_server_state.rs | 12++++++------
Msrc/request/register.rs | 38+++++++++++++++++++++++++++-----------
Msrc/request/register/ser.rs | 177++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/request/ser.rs | 2+-
Msrc/response.rs | 24++++++++++++------------
Msrc/response/auth.rs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/response/auth/error.rs | 6+++---
Msrc/response/auth/ser.rs | 2+-
Msrc/response/bin.rs | 10+++++-----
Msrc/response/cbor.rs | 2++
Msrc/response/custom.rs | 6+++---
Msrc/response/register.rs | 898+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/response/register/bin.rs | 140++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/response/register/error.rs | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/response/register/ser.rs | 7412++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/response/register/ser_relaxed.rs | 7025++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/response/ser.rs | 16++++++++--------
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")]