webauthn_rp

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

commit 5b5f26830d93e14fb5172c8db50e950a4de46112
parent 9d4b0de0577d131bba754f47c096c6a70e2b3f8c
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon,  6 Apr 2026 15:44:40 -0600

move tests to their own files

Diffstat:
MCargo.toml | 4++--
Msrc/hash/hash_map.rs | 90++-----------------------------------------------------------------------------
Asrc/hash/hash_map/tests.rs | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/hash/hash_set.rs | 87++-----------------------------------------------------------------------------
Asrc/hash/hash_set/tests.rs | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/request.rs | 8981+------------------------------------------------------------------------------
Msrc/request/auth.rs | 532+------------------------------------------------------------------------------
Msrc/request/auth/ser.rs | 238+------------------------------------------------------------------------------
Asrc/request/auth/ser/tests.rs | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/auth/tests.rs | 525+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/request/register.rs | 883+------------------------------------------------------------------------------
Msrc/request/register/ser.rs | 851+------------------------------------------------------------------------------
Asrc/request/register/ser/tests.rs | 830+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/register/tests.rs | 889+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/tests.rs | 8963+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/response.rs | 126++-----------------------------------------------------------------------------
Msrc/response/auth/ser.rs | 1315+------------------------------------------------------------------------------
Asrc/response/auth/ser/tests.rs | 1303+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/response/auth/ser_relaxed.rs | 1751+------------------------------------------------------------------------------
Asrc/response/auth/ser_relaxed/tests.rs | 1746+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/response/custom.rs | 42++----------------------------------------
Asrc/response/custom/tests.rs | 37+++++++++++++++++++++++++++++++++++++
Msrc/response/register.rs | 429+------------------------------------------------------------------------------
Msrc/response/register/ser.rs | 11349+------------------------------------------------------------------------------
Asrc/response/register/ser/tests.rs | 11340+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/response/register/ser_relaxed.rs | 11453+------------------------------------------------------------------------------
Asrc/response/register/ser_relaxed/tests.rs | 11442+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/register/tests.rs | 421+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/response/ser_relaxed.rs | 601+------------------------------------------------------------------------------
Asrc/response/ser_relaxed/tests.rs | 583+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/tests.rs | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
31 files changed, 38681 insertions(+), 38700 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" name = "webauthn_rp" readme = "README.md" repository = "https://git.philomathiclife.com/repos/webauthn_rp/" -rust-version = "1.94.0" +rust-version = "1.94.1" version = "0.4.0+spec-3" [lints.rust] @@ -139,7 +139,7 @@ serde_json = { version = "1.0.149", default-features = false, features = ["prese ### FEATURES ################################################################# [features] -default = ["serializable_server_state", "serde_relaxed", "custom"] +default = ["bin", "serde"] # Provide binary (de)serialization for persistent data. bin = [] diff --git a/src/hash/hash_map.rs b/src/hash/hash_map.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; use super::{super::request::TimedCeremony, BuildIdentityHasher}; #[cfg(doc)] use core::hash::Hasher; @@ -697,91 +699,3 @@ impl<K, V, S> From<MaxLenHashMap<K, V, S>> for HashMap<K, V, S> { value.0 } } -#[cfg(test)] -mod tests { - use super::{Equivalent, Insert, InsertRemoveExpired, MaxLenHashMap, TimedCeremony}; - use core::hash::{Hash, Hasher}; - #[cfg(not(feature = "serializable_server_state"))] - use std::time::Instant; - #[cfg(feature = "serializable_server_state")] - use std::time::SystemTime; - #[derive(Clone, Copy)] - struct Ceremony { - id: usize, - #[cfg(not(feature = "serializable_server_state"))] - exp: Instant, - #[cfg(feature = "serializable_server_state")] - exp: SystemTime, - } - impl Default for Ceremony { - fn default() -> Self { - Self { - id: 0, - #[cfg(not(feature = "serializable_server_state"))] - exp: Instant::now(), - #[cfg(feature = "serializable_server_state")] - exp: SystemTime::now(), - } - } - } - impl PartialEq for Ceremony { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } - } - impl Eq for Ceremony {} - impl Hash for Ceremony { - fn hash<H: Hasher>(&self, state: &mut H) { - self.id.hash(state); - } - } - impl TimedCeremony for Ceremony { - #[cfg(not(feature = "serializable_server_state"))] - fn expiration(&self) -> Instant { - self.exp - } - #[cfg(feature = "serializable_server_state")] - fn expiration(&self) -> SystemTime { - self.exp - } - } - impl Equivalent<Ceremony> for usize { - #[inline] - fn equivalent(&self, key: &Ceremony) -> bool { - *self == key.id - } - } - #[test] - fn hash_map_insert_removed() { - const REQ_MAX_LEN: usize = 8; - let mut map = MaxLenHashMap::new(REQ_MAX_LEN); - let cap = map.as_ref().capacity(); - let max_len = map.max_len(); - assert_eq!(cap >> 1u8, max_len); - assert!(max_len >= REQ_MAX_LEN); - let mut cer = Ceremony::default(); - for i in 0..max_len { - assert!(map.as_ref().capacity() <= cap); - cer.id = i; - assert_eq!(map.insert(cer, i), Insert::Success); - } - assert!(map.as_ref().capacity() <= cap); - assert_eq!(map.as_ref().len(), max_len); - for i in 0..max_len { - assert!(map.as_ref().contains_key(&i)); - } - cer.id = cap; - assert_eq!( - map.insert_remove_expired(cer, 10), - InsertRemoveExpired::Success - ); - assert!(map.as_ref().capacity() <= cap); - assert_eq!(map.as_ref().len(), max_len); - let mut counter = 0; - for i in 0..max_len { - counter += usize::from(map.as_ref().contains_key(&i)); - } - assert_eq!(counter, max_len - 1); - assert!(map.as_ref().contains_key(&(max_len - 1))); - } -} diff --git a/src/hash/hash_map/tests.rs b/src/hash/hash_map/tests.rs @@ -0,0 +1,85 @@ +use super::{Equivalent, Insert, InsertRemoveExpired, MaxLenHashMap, TimedCeremony}; +use core::hash::{Hash, Hasher}; +#[cfg(not(feature = "serializable_server_state"))] +use std::time::Instant; +#[cfg(feature = "serializable_server_state")] +use std::time::SystemTime; +#[derive(Clone, Copy)] +struct Ceremony { + id: usize, + #[cfg(not(feature = "serializable_server_state"))] + exp: Instant, + #[cfg(feature = "serializable_server_state")] + exp: SystemTime, +} +impl Default for Ceremony { + fn default() -> Self { + Self { + id: 0, + #[cfg(not(feature = "serializable_server_state"))] + exp: Instant::now(), + #[cfg(feature = "serializable_server_state")] + exp: SystemTime::now(), + } + } +} +impl PartialEq for Ceremony { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} +impl Eq for Ceremony {} +impl Hash for Ceremony { + fn hash<H: Hasher>(&self, state: &mut H) { + self.id.hash(state); + } +} +impl TimedCeremony for Ceremony { + #[cfg(not(feature = "serializable_server_state"))] + fn expiration(&self) -> Instant { + self.exp + } + #[cfg(feature = "serializable_server_state")] + fn expiration(&self) -> SystemTime { + self.exp + } +} +impl Equivalent<Ceremony> for usize { + #[inline] + fn equivalent(&self, key: &Ceremony) -> bool { + *self == key.id + } +} +#[test] +fn hash_map_insert_removed() { + const REQ_MAX_LEN: usize = 8; + let mut map = MaxLenHashMap::new(REQ_MAX_LEN); + let cap = map.as_ref().capacity(); + let max_len = map.max_len(); + assert_eq!(cap >> 1u8, max_len); + assert!(max_len >= REQ_MAX_LEN); + let mut cer = Ceremony::default(); + for i in 0..max_len { + assert!(map.as_ref().capacity() <= cap); + cer.id = i; + assert_eq!(map.insert(cer, i), Insert::Success); + } + assert!(map.as_ref().capacity() <= cap); + assert_eq!(map.as_ref().len(), max_len); + for i in 0..max_len { + assert!(map.as_ref().contains_key(&i)); + } + cer.id = cap; + assert_eq!( + map.insert_remove_expired(cer, 10), + InsertRemoveExpired::Success + ); + assert!(map.as_ref().capacity() <= cap); + assert_eq!(map.as_ref().len(), max_len); + let mut counter = 0; + for i in 0..max_len { + counter += usize::from(map.as_ref().contains_key(&i)); + } + assert_eq!(counter, max_len - 1); + assert!(map.as_ref().contains_key(&(max_len - 1))); +} diff --git a/src/hash/hash_set.rs b/src/hash/hash_set.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; use super::{super::request::TimedCeremony, BuildIdentityHasher}; #[cfg(doc)] use core::hash::Hasher; @@ -522,88 +524,3 @@ impl<T, S> From<MaxLenHashSet<T, S>> for HashSet<T, S> { value.0 } } -#[cfg(test)] -mod tests { - use super::{Equivalent, Insert, InsertRemoveExpired, MaxLenHashSet, TimedCeremony}; - use core::hash::{Hash, Hasher}; - #[cfg(not(feature = "serializable_server_state"))] - use std::time::Instant; - #[cfg(feature = "serializable_server_state")] - use std::time::SystemTime; - #[derive(Clone, Copy)] - struct Ceremony { - id: usize, - #[cfg(not(feature = "serializable_server_state"))] - exp: Instant, - #[cfg(feature = "serializable_server_state")] - exp: SystemTime, - } - impl Default for Ceremony { - fn default() -> Self { - Self { - id: 0, - #[cfg(not(feature = "serializable_server_state"))] - exp: Instant::now(), - #[cfg(feature = "serializable_server_state")] - exp: SystemTime::now(), - } - } - } - impl PartialEq for Ceremony { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } - } - impl Eq for Ceremony {} - impl Hash for Ceremony { - fn hash<H: Hasher>(&self, state: &mut H) { - self.id.hash(state); - } - } - impl TimedCeremony for Ceremony { - #[cfg(not(feature = "serializable_server_state"))] - fn expiration(&self) -> Instant { - self.exp - } - #[cfg(feature = "serializable_server_state")] - fn expiration(&self) -> SystemTime { - self.exp - } - } - impl Equivalent<Ceremony> for usize { - #[inline] - fn equivalent(&self, key: &Ceremony) -> bool { - *self == key.id - } - } - #[test] - fn hash_set_insert_removed() { - const REQ_MAX_LEN: usize = 8; - let mut set = MaxLenHashSet::new(REQ_MAX_LEN); - let cap = set.as_ref().capacity(); - let max_len = set.max_len(); - assert_eq!(cap >> 1u8, max_len); - assert!(max_len >= REQ_MAX_LEN); - let mut cer = Ceremony::default(); - for i in 0..max_len { - assert!(set.as_ref().capacity() <= cap); - cer.id = i; - assert_eq!(set.insert(cer), Insert::Success); - } - assert!(set.as_ref().capacity() <= cap); - assert_eq!(set.as_ref().len(), max_len); - for i in 0..max_len { - assert!(set.as_ref().contains(&i)); - } - cer.id = cap; - assert_eq!(set.insert_remove_expired(cer), InsertRemoveExpired::Success); - assert!(set.as_ref().capacity() <= cap); - assert_eq!(set.as_ref().len(), max_len); - let mut counter = 0; - for i in 0..max_len { - counter += usize::from(set.as_ref().contains(&i)); - } - assert_eq!(counter, max_len - 1); - assert!(set.as_ref().contains(&(max_len - 1))); - } -} diff --git a/src/hash/hash_set/tests.rs b/src/hash/hash_set/tests.rs @@ -0,0 +1,82 @@ +use super::{Equivalent, Insert, InsertRemoveExpired, MaxLenHashSet, TimedCeremony}; +use core::hash::{Hash, Hasher}; +#[cfg(not(feature = "serializable_server_state"))] +use std::time::Instant; +#[cfg(feature = "serializable_server_state")] +use std::time::SystemTime; +#[derive(Clone, Copy)] +struct Ceremony { + id: usize, + #[cfg(not(feature = "serializable_server_state"))] + exp: Instant, + #[cfg(feature = "serializable_server_state")] + exp: SystemTime, +} +impl Default for Ceremony { + fn default() -> Self { + Self { + id: 0, + #[cfg(not(feature = "serializable_server_state"))] + exp: Instant::now(), + #[cfg(feature = "serializable_server_state")] + exp: SystemTime::now(), + } + } +} +impl PartialEq for Ceremony { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} +impl Eq for Ceremony {} +impl Hash for Ceremony { + fn hash<H: Hasher>(&self, state: &mut H) { + self.id.hash(state); + } +} +impl TimedCeremony for Ceremony { + #[cfg(not(feature = "serializable_server_state"))] + fn expiration(&self) -> Instant { + self.exp + } + #[cfg(feature = "serializable_server_state")] + fn expiration(&self) -> SystemTime { + self.exp + } +} +impl Equivalent<Ceremony> for usize { + #[inline] + fn equivalent(&self, key: &Ceremony) -> bool { + *self == key.id + } +} +#[test] +fn hash_set_insert_removed() { + const REQ_MAX_LEN: usize = 8; + let mut set = MaxLenHashSet::new(REQ_MAX_LEN); + let cap = set.as_ref().capacity(); + let max_len = set.max_len(); + assert_eq!(cap >> 1u8, max_len); + assert!(max_len >= REQ_MAX_LEN); + let mut cer = Ceremony::default(); + for i in 0..max_len { + assert!(set.as_ref().capacity() <= cap); + cer.id = i; + assert_eq!(set.insert(cer), Insert::Success); + } + assert!(set.as_ref().capacity() <= cap); + assert_eq!(set.as_ref().len(), max_len); + for i in 0..max_len { + assert!(set.as_ref().contains(&i)); + } + cer.id = cap; + assert_eq!(set.insert_remove_expired(cer), InsertRemoveExpired::Success); + assert!(set.as_ref().capacity() <= cap); + assert_eq!(set.as_ref().len(), max_len); + let mut counter = 0; + for i in 0..max_len { + counter += usize::from(set.as_ref().contains(&i)); + } + assert_eq!(counter, max_len - 1); + assert!(set.as_ref().contains(&(max_len - 1))); +} diff --git a/src/request.rs b/src/request.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; #[cfg(doc)] use super::{ hash::hash_set::MaxLenHashSet, @@ -1872,8982 +1874,3 @@ impl<'first, 'second> PrfInput<'first, 'second> { /// This is the recommended default timeout duration for ceremonies /// [in the spec](https://www.w3.org/TR/webauthn-3/#sctn-timeout-recommended-range). pub const FIVE_MINUTES: NonZeroU32 = NonZeroU32::new(300_000).unwrap(); -#[cfg(test)] -mod tests { - #[cfg(feature = "custom")] - use super::{ - super::{ - AggErr, AuthenticatedCredential, - response::{ - AuthTransports, AuthenticatorAttachment, Backup, CredentialId, - auth::{ - DiscoverableAuthentication, DiscoverableAuthenticatorAssertion, - NonDiscoverableAuthentication, NonDiscoverableAuthenticatorAssertion, - }, - register::{ - AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation, - AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputs, - ClientExtensionsOutputsStaticState, CompressedP256PubKey, CompressedP384PubKey, - CompressedPubKeyOwned, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, - MlDsa44PubKey, MlDsa65PubKey, MlDsa87PubKey, Registration, RsaPubKey, - StaticState, UncompressedPubKey, - }, - }, - }, - Challenge, Credentials as _, ExtensionInfo, ExtensionReq, PrfInput, - PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement, - auth::{ - AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions, - CredentialSpecificExtension, DiscoverableCredentialRequestOptions, - Extension as AuthExt, NonDiscoverableCredentialRequestOptions, PrfInputOwned, - }, - register::{ - CredProtect, CredentialCreationOptions, Extension as RegExt, FourToSixtyThree, - PublicKeyCredentialUserEntity, RegistrationVerificationOptions, UserHandle, - }, - }; - use super::{AsciiDomainStatic, Hints, PublicKeyCredentialHint}; - #[cfg(feature = "custom")] - use ed25519_dalek::{Signer as _, SigningKey}; - #[cfg(feature = "custom")] - use ml_dsa::{ - ExpandedSigningKey as MlDsaSigKey, MlDsa44, MlDsa65, MlDsa87, Signature as MlDsaSignature, - }; - #[cfg(feature = "custom")] - use p256::{ - ecdsa::{DerSignature as P256DerSig, SigningKey as P256Key}, - elliptic_curve::sec1::Tag, - }; - #[cfg(feature = "custom")] - use p384::ecdsa::{DerSignature as P384DerSig, SigningKey as P384Key}; - #[cfg(feature = "custom")] - use rsa::{ - BoxedUint, RsaPrivateKey, - pkcs1v15::SigningKey as RsaKey, - sha2::{Digest as _, Sha256}, - signature::{Keypair as _, SignatureEncoding as _}, - traits::PublicKeyParts as _, - }; - use serde_json as _; - #[cfg(feature = "custom")] - const CBOR_UINT: u8 = 0b000_00000; - #[cfg(feature = "custom")] - const CBOR_NEG: u8 = 0b001_00000; - #[cfg(feature = "custom")] - const CBOR_BYTES: u8 = 0b010_00000; - #[cfg(feature = "custom")] - const CBOR_TEXT: u8 = 0b011_00000; - #[cfg(feature = "custom")] - const CBOR_MAP: u8 = 0b101_00000; - #[cfg(feature = "custom")] - const CBOR_SIMPLE: u8 = 0b111_00000; - #[cfg(feature = "custom")] - const CBOR_TRUE: u8 = CBOR_SIMPLE | 21; - #[test] - fn ascii_domain_static() { - /// No trailing dot, max label length, max domain length. - const LONG: AsciiDomainStatic = AsciiDomainStatic::new( - "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", - ) - .unwrap(); - /// Trailing dot, min label length, max domain length. - const LONG_TRAILING: AsciiDomainStatic = AsciiDomainStatic::new("w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.").unwrap(); - /// Single character domain. - const SHORT: AsciiDomainStatic = AsciiDomainStatic::new("w").unwrap(); - let long_label = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"; - assert_eq!(long_label.len(), 63); - let mut long = format!("{long_label}.{long_label}.{long_label}.{long_label}"); - _ = long.pop(); - _ = long.pop(); - assert_eq!(LONG.0.len(), 253); - assert_eq!(LONG.0, long.as_str()); - let trailing = "w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w."; - assert_eq!(LONG_TRAILING.0.len(), 254); - assert_eq!(LONG_TRAILING.0, trailing); - assert_eq!(SHORT.0.len(), 1); - assert_eq!(SHORT.0, "w"); - assert!(AsciiDomainStatic::new("www.Example.com").is_none()); - assert!(AsciiDomainStatic::new("").is_none()); - assert!(AsciiDomainStatic::new(".").is_none()); - assert!(AsciiDomainStatic::new("www..c").is_none()); - let too_long_label = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"; - assert_eq!(too_long_label.len(), 64); - assert!(AsciiDomainStatic::new(too_long_label).is_none()); - let dom_254_no_trailing_dot = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"; - assert_eq!(dom_254_no_trailing_dot.len(), 254); - assert!(AsciiDomainStatic::new(dom_254_no_trailing_dot).is_none()); - assert!(AsciiDomainStatic::new("\u{3bb}.com").is_none()); - } - #[cfg(feature = "custom")] - const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); - #[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_reg() -> Result<(), AggErr> { - let id = UserHandle::from([0]); - let mut opts = CredentialCreationOptions::passkey( - RP_ID, - PublicKeyCredentialUserEntity { - name: "foo", - id: &id, - display_name: "", - }, - Vec::new(), - ); - opts.public_key.challenge = Challenge(0); - opts.public_key.extensions = RegExt { - cred_props: None, - cred_protect: CredProtect::UserVerificationRequired( - false, - ExtensionInfo::RequireEnforceValue, - ), - min_pin_length: Some((FourToSixtyThree::Ten, ExtensionInfo::RequireEnforceValue)), - prf: Some(( - PrfInput { - first: [0].as_slice(), - second: None, - }, - ExtensionInfo::RequireEnforceValue, - )), - }; - 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::new(); - attestation_object.extend_from_slice( - [ - CBOR_MAP | 3, - CBOR_TEXT | 3, - b'f', - b'm', - b't', - CBOR_TEXT | 6, - b'p', - b'a', - b'c', - b'k', - b'e', - b'd', - CBOR_TEXT | 7, - b'a', - b't', - b't', - b'S', - b't', - b'm', - b't', - CBOR_MAP | 2, - CBOR_TEXT | 3, - b'a', - b'l', - b'g', - // COSE EdDSA. - CBOR_NEG | 7, - CBOR_TEXT | 3, - b's', - b'i', - b'g', - CBOR_BYTES | 24, - 64, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - CBOR_TEXT | 8, - b'a', - b'u', - b't', - b'h', - b'D', - b'a', - b't', - b'a', - CBOR_BYTES | 24, - // Length is 154. - 154, - // RP ID HASH. - // 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, AT, and ED (right-to-left). - 0b1100_0101, - // COUNTER. - // 0 as 32-bit big endian. - 0, - 0, - 0, - 0, - // AAGUID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // L. - // CREDENTIAL ID length is 16 as 16-bit big endian. - 0, - 16, - // CREDENTIAL ID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - CBOR_MAP | 4, - // COSE kty. - CBOR_UINT | 1, - // COSE OKP. - CBOR_UINT | 1, - // COSE alg. - CBOR_UINT | 3, - // COSE EdDSA. - CBOR_NEG | 7, - // COSE OKP crv. - CBOR_NEG, - // COSE Ed25519. - CBOR_UINT | 6, - // COSE OKP x. - CBOR_NEG | 1, - CBOR_BYTES | 24, - // Length is 32. - 32, - // Compressed-y coordinate. - // 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, - CBOR_MAP | 3, - CBOR_TEXT | 11, - b'c', - b'r', - b'e', - b'd', - b'P', - b'r', - b'o', - b't', - b'e', - b'c', - b't', - // userVerificationRequired. - CBOR_UINT | 3, - // CBOR text of length 11. - 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_TRUE, - CBOR_TEXT | 12, - b'm', - b'i', - b'n', - b'P', - b'i', - b'n', - b'L', - b'e', - b'n', - b'g', - b't', - b'h', - CBOR_UINT | 16, - ] - .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); - 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", - id: &id, - display_name: "", - }, - 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", - id: &id, - display_name: "", - }, - 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", - id: &id, - display_name: "", - }, - 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 { - 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::MlDsa44(k) if **k.inner() == [1; 1312])); - 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 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(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 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, - &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::MlDsa44( - MlDsa44PubKey::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, - 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 es256_reg() -> Result<(), AggErr> { - let id = UserHandle::from([0]); - let mut opts = CredentialCreationOptions::passkey( - RP_ID, - PublicKeyCredentialUserEntity { - name: "foo", - id: &id, - display_name: "", - }, - 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(210); - 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 | 24, - // Length is 148. - 148, - // RP ID HASH. - // 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 AT (right-to-left). - 0b0100_0101, - // COUNTER. - // 0 as 32-bit big endian. - 0, - 0, - 0, - 0, - // AAGUID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // L. - // CREDENTIAL ID length is 16 as 16-bit big endian. - 0, - 16, - // CREDENTIAL ID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - CBOR_MAP | 5, - // COSE kty. - CBOR_UINT | 1, - // COSE EC2. - CBOR_UINT | 2, - // COSE alg. - CBOR_UINT | 3, - // COSE ES256. - CBOR_NEG | 6, - // COSE EC2 crv. - CBOR_NEG, - // COSE P-256. - CBOR_UINT | 1, - // COSE EC2 x. - CBOR_NEG | 1, - CBOR_BYTES | 24, - // Length is 32. - 32, - // X-coordinate. 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, - // COSE EC2 y. - CBOR_NEG | 2, - CBOR_BYTES | 24, - // Length is 32. - 32, - // Y-coordinate. 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, - ] - .as_slice(), - ); - attestation_object[30..62].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); - let p256_key = P256Key::from_bytes( - &[ - 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, - 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, - ] - .into(), - ) - .unwrap() - .verifying_key() - .to_sec1_point(false); - let x = p256_key.x().unwrap(); - let y = p256_key.y().unwrap(); - attestation_object[111..143].copy_from_slice(x); - attestation_object[146..].copy_from_slice(y); - 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::P256(k) if *k.x() == **x && *k.y() == **y)); - 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 es256_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 p256_key = P256Key::from_bytes( - &[ - 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, - 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, - ] - .into(), - ) - .unwrap(); - let der_sig: P256DerSig = p256_key.sign(authenticator_data.as_slice()); - let pub_key = p256_key.verifying_key().to_sec1_point(true); - 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, - der_sig.as_bytes().into(), - 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::P256(CompressedP256PubKey::from( - ( - (*pub_key.x().unwrap()).into(), - pub_key.tag() == Tag::CompressedOddY - ) - ),), - 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, - 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 es384_reg() -> Result<(), AggErr> { - let id = UserHandle::from([0]); - let mut opts = CredentialCreationOptions::passkey( - RP_ID, - PublicKeyCredentialUserEntity { - name: "foo", - id: &id, - display_name: "", - }, - 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(243); - 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 of length 7. - 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 | 24, - // Length is 181. - 181, - // RP ID HASH. - // 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 AT (right-to-left). - 0b0100_0101, - // COUNTER. - // 0 as 32-bit big-endian. - 0, - 0, - 0, - 0, - // AAGUID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // L. - // CREDENTIAL ID length is 16 as 16-bit big endian. - 0, - 16, - // CREDENTIAL ID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - CBOR_MAP | 5, - // COSE kty. - CBOR_UINT | 1, - // COSE EC2. - CBOR_UINT | 2, - // COSE alg. - CBOR_UINT | 3, - CBOR_NEG | 24, - // COSE ES384. - 34, - // COSE EC2 crv. - CBOR_NEG, - // COSE P-384. - CBOR_UINT | 2, - // COSE EC2 x. - CBOR_NEG | 1, - CBOR_BYTES | 24, - // Length is 48. - 48, - // X-coordinate. 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // COSE EC2 y. - CBOR_NEG | 2, - CBOR_BYTES | 24, - // Length is 48. - 48, - // Y-coordinate. 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ] - .as_slice(), - ); - attestation_object[30..62].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); - let p384_key = P384Key::from_bytes( - &[ - 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, - 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, - 32, 86, 220, 68, 182, 11, 105, 223, 75, 70, - ] - .into(), - ) - .unwrap() - .verifying_key() - .to_sec1_point(false); - let x = p384_key.x().unwrap(); - let y = p384_key.y().unwrap(); - attestation_object[112..160].copy_from_slice(x); - attestation_object[163..].copy_from_slice(y); - 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::P384(k) if *k.x() == **x && *k.y() == **y)); - 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 es384_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 p384_key = P384Key::from_bytes( - &[ - 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, - 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, - 32, 86, 220, 68, 182, 11, 105, 223, 75, 70, - ] - .into(), - ) - .unwrap(); - let der_sig: P384DerSig = p384_key.sign(authenticator_data.as_slice()); - let pub_key = p384_key.verifying_key().to_sec1_point(true); - 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, - der_sig.as_bytes().into(), - 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::P384(CompressedP384PubKey::from( - ( - (*pub_key.x().unwrap()).into(), - pub_key.tag() == Tag::CompressedOddY - ) - ),), - 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, - 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")] - #[expect(clippy::many_single_char_names, reason = "fine")] - #[test] - #[cfg(feature = "custom")] - fn rs256_reg() -> Result<(), AggErr> { - let id = UserHandle::from([0]); - let mut opts = CredentialCreationOptions::passkey( - RP_ID, - PublicKeyCredentialUserEntity { - name: "foo", - id: &id, - display_name: "", - }, - 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(406); - 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, - // Length is 343 as 16-bit big-endian. - 1, - 87, - // RP ID HASH. - // 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 AT (right-to-left). - 0b0100_0101, - // COUNTER. - // 0 as 32-bit big-endian. - 0, - 0, - 0, - 0, - // AAGUID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // L. - // CREDENTIAL ID length is 16 as 16-bit big endian. - 0, - 16, - // CREDENTIAL ID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - CBOR_MAP | 4, - // COSE kty. - CBOR_UINT | 1, - // COSE RSA. - CBOR_UINT | 3, - // COSE alg. - CBOR_UINT | 3, - CBOR_NEG | 25, - // COSE RS256. - 1, - 0, - // COSE n. - CBOR_NEG, - CBOR_BYTES | 25, - // Length is 256 as 16-bit big-endian. - 1, - 0, - // N. 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // COSE e. - CBOR_NEG | 1, - CBOR_BYTES | 3, - // 65537 as 24-bit big-endian. - 1, - 0, - 1, - ] - .as_slice(), - ); - attestation_object[31..63].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); - let n = [ - 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, - 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, - 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, - 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, - 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, - 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, - 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, - 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, - 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, - 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, - 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, - 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, - 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, - 153, 79, 0, 133, 78, 7, 218, 165, 241, - ]; - let e = 0x0001_0001; - let d = [ - 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, - 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, - 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, - 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, - 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, - 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, - 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, - 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, - 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, - 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, - 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, - 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, - 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, - 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, - ]; - let p = BoxedUint::from_le_slice_vartime( - [ - 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, - 69, 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, - 250, 195, 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, - 99, 76, 240, 14, 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, - 82, 98, 233, 236, 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, - 147, 179, 30, 62, 247, 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, - 168, 253, 228, 214, 54, 16, 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, - 24, 247, - ] - .as_slice(), - ); - let p_2 = BoxedUint::from_le_slice_vartime( - [ - 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, - 85, 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, - 141, 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, - 39, 85, 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, - 250, 187, 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, - 112, 211, 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, - 173, 9, 236, 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, - 250, - ] - .as_slice(), - ); - let rsa_key = RsaKey::<Sha256>::new( - RsaPrivateKey::from_components( - BoxedUint::from_le_slice_vartime(n.as_slice()), - e.into(), - BoxedUint::from_le_slice_vartime(d.as_slice()), - vec![p, p_2], - ) - .unwrap(), - ) - .verifying_key(); - let n_other = rsa_key.as_ref().n().to_be_bytes(); - attestation_object[113..369].copy_from_slice(&n_other); - 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::Rsa(k) if **k.n() == *n_other && k.e() == e)); - 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")] - #[expect(clippy::too_many_lines, reason = "a lot to test")] - #[test] - #[cfg(feature = "custom")] - fn rs256_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 n = [ - 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, - 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, - 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, - 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, - 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, - 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, - 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, - 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, - 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, - 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, - 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, - 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, - 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, - 153, 79, 0, 133, 78, 7, 218, 165, 241, - ]; - let e = 0x0001_0001; - let d = [ - 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, - 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, - 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, - 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, - 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, - 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, - 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, - 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, - 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, - 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, - 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, - 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, - 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, - 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, - ]; - let p = BoxedUint::from_le_slice_vartime( - [ - 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, - 69, 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, - 250, 195, 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, - 99, 76, 240, 14, 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, - 82, 98, 233, 236, 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, - 147, 179, 30, 62, 247, 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, - 168, 253, 228, 214, 54, 16, 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, - 24, 247, - ] - .as_slice(), - ); - let p_2 = BoxedUint::from_le_slice_vartime( - [ - 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, - 85, 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, - 141, 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, - 39, 85, 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, - 250, 187, 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, - 112, 211, 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, - 173, 9, 236, 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, - 250, - ] - .as_slice(), - ); - let rsa_key = RsaKey::<Sha256>::new( - RsaPrivateKey::from_components( - BoxedUint::from_le_slice_vartime(n.as_slice()), - e.into(), - BoxedUint::from_le_slice_vartime(d.as_slice()), - vec![p, p_2], - ) - .unwrap(), - ); - 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].into_boxed_slice())?, - 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: CompressedPubKeyOwned::Rsa( - RsaPubKey::try_from((rsa_pub.as_ref().n().to_be_bytes(), 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(()) - } - #[expect( - clippy::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[test] - fn hints() { - assert_eq!(Hints::EMPTY.0, [None; 3]); - assert!( - Hints::EMPTY.first().is_none() - && Hints::EMPTY.second().is_none() - && Hints::EMPTY.third().is_none() - ); - assert_eq!(Hints::EMPTY.count(), 0); - assert!(Hints::EMPTY.is_empty()); - assert!( - !(Hints::EMPTY.contains_cross_platform_hints() - || Hints::EMPTY.contains_platform_hints()) - ); - assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::SecurityKey)); - assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::ClientDevice)); - assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::Hybrid)); - let mut hints = Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey); - assert_eq!( - hints.0, - [Some(PublicKeyCredentialHint::SecurityKey), None, None] - ); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::SecurityKey).0); - assert!( - hints.first() == Some(PublicKeyCredentialHint::SecurityKey) - && hints.second().is_none() - && hints.third().is_none() - ); - assert_eq!(hints.count(), 1); - assert!(!hints.is_empty()); - assert!(hints.contains_cross_platform_hints() && !hints.contains_platform_hints()); - assert!(hints.contains(PublicKeyCredentialHint::SecurityKey)); - assert!(!hints.contains(PublicKeyCredentialHint::ClientDevice)); - assert!(!hints.contains(PublicKeyCredentialHint::Hybrid)); - hints = Hints::EMPTY.add(PublicKeyCredentialHint::Hybrid); - assert_eq!(hints.0, [Some(PublicKeyCredentialHint::Hybrid), None, None]); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); - assert!( - hints.first() == Some(PublicKeyCredentialHint::Hybrid) - && hints.second().is_none() - && hints.third().is_none() - ); - assert_eq!(hints.count(), 1); - assert!(!hints.is_empty()); - assert!(hints.contains_cross_platform_hints() && !hints.contains_platform_hints()); - assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); - assert!(!hints.contains(PublicKeyCredentialHint::SecurityKey)); - assert!(!hints.contains(PublicKeyCredentialHint::ClientDevice)); - hints = Hints::EMPTY.add(PublicKeyCredentialHint::ClientDevice); - assert_eq!( - hints.0, - [Some(PublicKeyCredentialHint::ClientDevice), None, None] - ); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::ClientDevice).0); - assert!( - hints.first() == Some(PublicKeyCredentialHint::ClientDevice) - && hints.second().is_none() - && hints.third().is_none() - ); - assert_eq!(hints.count(), 1); - assert!(!hints.is_empty()); - assert!(!hints.contains_cross_platform_hints() && hints.contains_platform_hints()); - assert!(hints.contains(PublicKeyCredentialHint::ClientDevice)); - assert!(!hints.contains(PublicKeyCredentialHint::SecurityKey)); - assert!(!hints.contains(PublicKeyCredentialHint::Hybrid)); - hints = hints.add(PublicKeyCredentialHint::Hybrid); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::ClientDevice).0); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); - assert_eq!( - hints.0, - [ - Some(PublicKeyCredentialHint::ClientDevice), - Some(PublicKeyCredentialHint::Hybrid), - None - ] - ); - assert!( - hints.first() == Some(PublicKeyCredentialHint::ClientDevice) - && hints.second() == Some(PublicKeyCredentialHint::Hybrid) - && hints.third().is_none() - ); - assert_eq!(hints.count(), 2); - assert!(!hints.is_empty()); - assert!(hints.contains_cross_platform_hints() && hints.contains_platform_hints()); - assert!(hints.contains(PublicKeyCredentialHint::ClientDevice)); - assert!(!hints.contains(PublicKeyCredentialHint::SecurityKey)); - assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); - hints = hints.add(PublicKeyCredentialHint::SecurityKey); - assert_eq!( - hints.0, - [ - Some(PublicKeyCredentialHint::ClientDevice), - Some(PublicKeyCredentialHint::Hybrid), - Some(PublicKeyCredentialHint::SecurityKey), - ] - ); - assert!( - hints.first() == Some(PublicKeyCredentialHint::ClientDevice) - && hints.second() == Some(PublicKeyCredentialHint::Hybrid) - && hints.third() == Some(PublicKeyCredentialHint::SecurityKey) - ); - assert_eq!(hints.count(), 3); - assert!(!hints.is_empty()); - assert!(hints.contains_cross_platform_hints() && hints.contains_platform_hints()); - assert!(hints.contains(PublicKeyCredentialHint::ClientDevice)); - assert!(hints.contains(PublicKeyCredentialHint::SecurityKey)); - assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::SecurityKey).0); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::ClientDevice).0); - hints = Hints::EMPTY - .add(PublicKeyCredentialHint::Hybrid) - .add(PublicKeyCredentialHint::SecurityKey); - assert_eq!( - hints.0, - [ - Some(PublicKeyCredentialHint::Hybrid), - Some(PublicKeyCredentialHint::SecurityKey), - None, - ] - ); - assert!( - hints.first() == Some(PublicKeyCredentialHint::Hybrid) - && hints.second() == Some(PublicKeyCredentialHint::SecurityKey) - && hints.third().is_none() - ); - assert_eq!(hints.count(), 2); - assert!(!hints.is_empty()); - assert!(hints.contains_cross_platform_hints() && !hints.contains_platform_hints()); - assert!(!hints.contains(PublicKeyCredentialHint::ClientDevice)); - assert!(hints.contains(PublicKeyCredentialHint::SecurityKey)); - assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::SecurityKey).0); - assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); - } -} diff --git a/src/request/auth.rs b/src/request/auth.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; #[cfg(doc)] use super::{ super::response::{ @@ -1611,533 +1613,3 @@ impl Ord for NonDiscoverableAuthenticationServerState { self.state.cmp(&other.state) } } -#[cfg(test)] -mod tests { - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - use super::{ - super::super::{ - AuthenticatedCredential, DynamicState, StaticState, UserHandle, - response::{ - Backup, - auth::{DiscoverableAuthenticatorAssertion, HmacSecret}, - register::{ - AuthenticationExtensionsPrfOutputs, AuthenticatorExtensionOutputStaticState, - ClientExtensionsOutputsStaticState, CompressedPubKeyOwned, - CredentialProtectionPolicy, Ed25519PubKey, - }, - }, - }, - AuthCeremonyErr, AuthenticationVerificationOptions, AuthenticatorAttachment, - AuthenticatorAttachmentEnforcement, DiscoverableAuthentication, ExtensionErr, OneOrTwo, - PrfInput, SignatureCounterEnforcement, - }; - #[cfg(all( - feature = "custom", - any( - feature = "serializable_server_state", - not(any(feature = "bin", feature = "serde")) - ) - ))] - use super::{ - super::{super::AggErr, Challenge, CredentialId, RpId, UserVerificationRequirement}, - DiscoverableCredentialRequestOptions, ExtensionReq, - }; - #[cfg(all(feature = "custom", feature = "serializable_server_state"))] - use super::{ - super::{ - super::bin::{Decode as _, Encode as _}, - AsciiDomain, AuthTransports, - }, - AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Credentials as _, - DiscoverableAuthenticationServerState, Extension, NonDiscoverableAuthenticationServerState, - NonDiscoverableCredentialRequestOptions, PrfInputOwned, PublicKeyCredentialDescriptor, - }; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - use ed25519_dalek::{Signer as _, SigningKey}; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - use rsa::sha2::{Digest as _, Sha256}; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_BYTES: u8 = 0b010_00000; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_TEXT: u8 = 0b011_00000; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_MAP: u8 = 0b101_00000; - #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] - #[test] - #[cfg(all(feature = "custom", feature = "serializable_server_state"))] - fn eddsa_auth_ser() -> Result<(), AggErr> { - let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); - 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 = Extension { prf: None }; - let server = opts.start_ceremony()?.0; - let enc_data = server.encode()?; - assert_eq!(enc_data.capacity(), 16 + 2 + 1 + 1 + 12 + 2 + 128 + 1); - assert_eq!(enc_data.len(), 16 + 2 + 1 + 1 + 12 + 2 + 16 + 2); - assert!( - server.is_eq(&NonDiscoverableAuthenticationServerState::decode( - enc_data.as_slice() - )?) - ); - let mut opts_2 = DiscoverableCredentialRequestOptions::passkey(&rp_id); - opts_2.public_key.challenge = Challenge(0); - opts_2.public_key.extensions = Extension { prf: None }; - let server_2 = opts_2.start_ceremony()?.0; - let enc_data_2 = server_2 - .encode() - .map_err(AggErr::EncodeDiscoverableAuthenticationServerState)?; - assert_eq!(enc_data_2.capacity(), enc_data_2.len()); - assert_eq!(enc_data_2.len(), 16 + 1 + 1 + 12); - assert!( - server_2.is_eq(&DiscoverableAuthenticationServerState::decode( - enc_data_2.as_slice() - )?) - ); - Ok(()) - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - struct TestResponseOptions { - user_verified: bool, - hmac: HmacSecret, - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - enum PrfCredOptions { - None, - FalseNoHmac, - FalseHmacFalse, - TrueNoHmac, - TrueHmacTrue, - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - struct TestCredOptions { - cred_protect: CredentialProtectionPolicy, - prf: PrfCredOptions, - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - enum PrfUvOptions { - /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise - /// `UserVerificationRequirement::Preferred` is used. - None(bool), - Prf((PrfInput<'static, 'static>, ExtensionReq)), - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - struct TestRequestOptions { - error_unsolicited: bool, - prf_uv: PrfUvOptions, - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - struct TestOptions { - request: TestRequestOptions, - response: TestResponseOptions, - cred: TestCredOptions, - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn generate_client_data_json() -> Vec<u8> { - let mut json = Vec::with_capacity(256); - json.extend_from_slice(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()); - json - } - #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] - #[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>, CompressedPubKeyOwned, Vec<u8>) { - let mut authenticator_data = Vec::with_capacity(256); - authenticator_data.extend_from_slice( - [ - // RP ID HASH. - // 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, AT, and ED (right-to-left). - 0b0000_0001 - | if opts.user_verified { - 0b0000_0100 - } else { - 0b0000_0000 - } - | if matches!(opts.hmac, HmacSecret::None) { - 0 - } else { - 0b1000_0000 - }, - // COUNTER. - // 0 as 32-bit big endian. - 0, - 0, - 0, - 0, - ] - .as_slice(), - ); - authenticator_data[..32].copy_from_slice(&Sha256::digest(b"example.com")); - match opts.hmac { - HmacSecret::None => {} - HmacSecret::One => { - authenticator_data.extend_from_slice( - [ - CBOR_MAP | 1, - // CBOR text of length 11. - 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, - 48, - ] - .as_slice(), - ); - authenticator_data.extend_from_slice([0; 48].as_slice()); - } - HmacSecret::Two => { - authenticator_data.extend_from_slice( - [ - CBOR_MAP | 1, - // CBOR text of length 11. - 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, - 80, - ] - .as_slice(), - ); - authenticator_data.extend_from_slice([0; 80].as_slice()); - } - } - let len = authenticator_data.len(); - authenticator_data - .extend_from_slice(&Sha256::digest(generate_client_data_json().as_slice())); - let sig_key = SigningKey::from_bytes(&[0; 32]); - let sig = sig_key.sign(authenticator_data.as_slice()).to_vec(); - authenticator_data.truncate(len); - ( - authenticator_data, - CompressedPubKeyOwned::Ed25519(Ed25519PubKey::from(sig_key.verifying_key().to_bytes())), - sig, - ) - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn validate(options: TestOptions) -> Result<(), AggErr> { - let rp_id = RpId::Domain("example.com".to_owned().try_into()?); - 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].into_boxed_slice())?; - let authentication = DiscoverableAuthentication::new( - credential_id.clone(), - DiscoverableAuthenticatorAssertion::new( - generate_client_data_json(), - authenticator_data, - signature, - user, - ), - AuthenticatorAttachment::None, - ); - let auth_opts = AuthenticationVerificationOptions::<'static, 'static, &str, &str> { - allowed_origins: [].as_slice(), - allowed_top_origins: None, - auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Update(false), - backup_requirement: None, - error_on_unsolicited_extensions: options.request.error_unsolicited, - sig_counter_enforcement: SignatureCounterEnforcement::Fail, - update_uv: false, - #[cfg(feature = "serde_relaxed")] - client_data_json_relaxed: false, - }; - let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id); - opts.public_key.challenge = Challenge(0); - opts.public_key.user_verification = UserVerificationRequirement::Preferred; - match options.request.prf_uv { - PrfUvOptions::None(required) => { - if required { - opts.public_key.user_verification = UserVerificationRequirement::Required; - } - } - PrfUvOptions::Prf(input) => { - opts.public_key.user_verification = UserVerificationRequirement::Required; - opts.public_key.extensions.prf = Some(input); - } - } - let mut cred = AuthenticatedCredential::new( - (&credential_id).into(), - &user, - StaticState { - credential_public_key, - extensions: AuthenticatorExtensionOutputStaticState { - cred_protect: options.cred.cred_protect, - hmac_secret: match options.cred.prf { - PrfCredOptions::None - | PrfCredOptions::FalseNoHmac - | PrfCredOptions::TrueNoHmac => None, - PrfCredOptions::FalseHmacFalse => Some(false), - PrfCredOptions::TrueHmacTrue => Some(true), - }, - }, - client_extension_results: ClientExtensionsOutputsStaticState { - prf: match options.cred.prf { - PrfCredOptions::None => None, - PrfCredOptions::FalseNoHmac | PrfCredOptions::FalseHmacFalse => { - Some(AuthenticationExtensionsPrfOutputs { enabled: false }) - } - PrfCredOptions::TrueNoHmac | PrfCredOptions::TrueHmacTrue => { - Some(AuthenticationExtensionsPrfOutputs { enabled: true }) - } - }, - }, - }, - DynamicState { - user_verified: true, - backup: Backup::NotEligible, - sign_count: 0, - authenticator_attachment: AuthenticatorAttachment::None, - }, - )?; - opts.start_ceremony()? - .0 - .verify(&rp_id, &authentication, &mut cred, &auth_opts) - .map_err(AggErr::AuthCeremony) - .map(|_| ()) - } - /// Test all, and only, possible `UserNotVerified` errors. - /// 4 * 5 * 3 * 2 * 5 = 600 tests. - /// We ignore this due to how long it takes (around 4 seconds or so). - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[ignore = "slow"] - #[test] - fn uv_required_err() { - const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ - CredentialProtectionPolicy::None, - CredentialProtectionPolicy::UserVerificationOptional, - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, - CredentialProtectionPolicy::UserVerificationRequired, - ]; - const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [ - PrfCredOptions::None, - PrfCredOptions::FalseNoHmac, - PrfCredOptions::FalseHmacFalse, - PrfCredOptions::TrueNoHmac, - PrfCredOptions::TrueHmacTrue, - ]; - const ALL_HMAC_OPTIONS: [HmacSecret; 3] = - [HmacSecret::None, HmacSecret::One, HmacSecret::Two]; - const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true]; - const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [ - PrfUvOptions::None(true), - PrfUvOptions::Prf(( - PrfInput { - first: [].as_slice(), - second: None, - }, - ExtensionReq::Require, - )), - PrfUvOptions::Prf(( - PrfInput { - first: [].as_slice(), - second: None, - }, - ExtensionReq::Allow, - )), - PrfUvOptions::Prf(( - PrfInput { - first: [].as_slice(), - second: Some([].as_slice()), - }, - ExtensionReq::Require, - )), - PrfUvOptions::Prf(( - PrfInput { - first: [].as_slice(), - second: Some([].as_slice()), - }, - ExtensionReq::Allow, - )), - ]; - for cred_protect in ALL_CRED_PROTECT_OPTIONS { - for prf in ALL_PRF_CRED_OPTIONS { - for hmac in ALL_HMAC_OPTIONS { - for error_unsolicited in ALL_UNSOLICIT_OPTIONS { - for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS { - assert!(validate(TestOptions { - request: TestRequestOptions { - error_unsolicited, - prf_uv, - }, - response: TestResponseOptions { - user_verified: false, - hmac, - }, - cred: TestCredOptions { cred_protect, prf, }, - }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::UserNotVerified)))); - } - } - } - } - } - } - /// Test all, and only, possible `UserNotVerified` errors. - /// 4 * 5 * 2 * 2 = 80 tests. - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[test] - fn forbidden_hmac() { - const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ - CredentialProtectionPolicy::None, - CredentialProtectionPolicy::UserVerificationOptional, - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, - CredentialProtectionPolicy::UserVerificationRequired, - ]; - const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [ - PrfCredOptions::None, - PrfCredOptions::FalseNoHmac, - PrfCredOptions::FalseHmacFalse, - PrfCredOptions::TrueNoHmac, - PrfCredOptions::TrueHmacTrue, - ]; - const ALL_HMAC_OPTIONS: [HmacSecret; 2] = [HmacSecret::One, HmacSecret::Two]; - const ALL_UV_OPTIONS: [bool; 2] = [false, true]; - for cred_protect in ALL_CRED_PROTECT_OPTIONS { - for prf in ALL_PRF_CRED_OPTIONS { - for hmac in ALL_HMAC_OPTIONS { - for user_verified in ALL_UV_OPTIONS { - assert!(validate(TestOptions { - request: TestRequestOptions { - error_unsolicited: true, - prf_uv: PrfUvOptions::None(false), - }, - response: TestResponseOptions { - user_verified, - hmac, - }, - cred: TestCredOptions { cred_protect, prf, }, - }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenHmacSecret))))); - } - } - } - } - } - #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[test] - fn prf() -> Result<(), AggErr> { - let mut opts = TestOptions { - request: TestRequestOptions { - error_unsolicited: false, - prf_uv: PrfUvOptions::Prf(( - PrfInput { - first: [].as_slice(), - second: None, - }, - ExtensionReq::Allow, - )), - }, - response: TestResponseOptions { - user_verified: true, - hmac: HmacSecret::None, - }, - cred: TestCredOptions { - cred_protect: CredentialProtectionPolicy::None, - prf: PrfCredOptions::None, - }, - }; - validate(opts)?; - opts.request.prf_uv = PrfUvOptions::Prf(( - PrfInput { - first: [].as_slice(), - second: None, - }, - ExtensionReq::Require, - )); - opts.cred.prf = PrfCredOptions::TrueHmacTrue; - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::MissingHmacSecret))))); - opts.response.hmac = HmacSecret::One; - opts.request.prf_uv = PrfUvOptions::Prf(( - PrfInput { - first: [].as_slice(), - second: None, - }, - ExtensionReq::Allow, - )); - opts.cred.prf = PrfCredOptions::TrueNoHmac; - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential))))); - opts.response.hmac = HmacSecret::Two; - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue(OneOrTwo::One, OneOrTwo::Two)))))); - opts.response.hmac = HmacSecret::One; - opts.cred.prf = PrfCredOptions::FalseNoHmac; - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential))))); - opts.response.user_verified = false; - opts.request.prf_uv = PrfUvOptions::None(false); - opts.cred.prf = PrfCredOptions::TrueHmacTrue; - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::UserNotVerifiedHmacSecret))))); - Ok(()) - } -} diff --git a/src/request/auth/ser.rs b/src/request/auth/ser.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; use super::{ super::{super::response::ser::Null, ser::PrfHelper}, AllowedCredential, AllowedCredentials, Challenge, CredentialMediationRequirement, @@ -1073,239 +1075,3 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions { ) } } -#[cfg(test)] -mod test { - use super::{ - super::{super::PublicKeyCredentialHint, ExtensionReq}, - ClientCredentialRequestOptions, CredentialMediationRequirement, ExtensionOwned, - FIVE_MINUTES, Hints, NonZeroU32, PublicKeyCredentialRequestOptionsOwned, - UserVerificationRequirement, - }; - use serde_json::Error; - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[expect(clippy::cognitive_complexity, reason = "a lot to test")] - #[test] - fn client_options() -> Result<(), Error> { - let mut err = - serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"bob":true}"#).unwrap_err(); - assert_eq!( - err.to_string().get(..56), - Some("unknown field `bob`, expected `mediation` or `publicKey`") - ); - err = serde_json::from_str::<ClientCredentialRequestOptions>( - r#"{"mediation":"required","mediation":"required"}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..27), - Some("duplicate field `mediation`") - ); - let mut options = serde_json::from_str::<ClientCredentialRequestOptions>("{}")?; - assert!(matches!( - options.mediation, - CredentialMediationRequirement::Required - )); - assert!(options.public_key.rp_id.is_none()); - assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert!(matches!( - options.public_key.user_verification, - UserVerificationRequirement::Preferred - )); - assert_eq!(options.public_key.hints, Hints::EMPTY); - assert!(options.public_key.extensions.prf.is_none()); - options = serde_json::from_str::<ClientCredentialRequestOptions>( - r#"{"mediation":null,"publicKey":null}"#, - )?; - assert!(matches!( - options.mediation, - CredentialMediationRequirement::Required - )); - assert!(options.public_key.rp_id.is_none()); - assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert!(matches!( - options.public_key.user_verification, - UserVerificationRequirement::Preferred - )); - assert_eq!(options.public_key.hints, Hints::EMPTY); - assert!(options.public_key.extensions.prf.is_none()); - options = serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"publicKey":{}}"#)?; - assert!(options.public_key.rp_id.is_none()); - assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert!(matches!( - options.public_key.user_verification, - UserVerificationRequirement::Preferred - )); - assert_eq!(options.public_key.hints, Hints::EMPTY); - assert!(options.public_key.extensions.prf.is_none()); - options = serde_json::from_str::<ClientCredentialRequestOptions>( - r#"{"mediation":"conditional","publicKey":{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}}"#, - )?; - assert!(matches!( - options.mediation, - CredentialMediationRequirement::Conditional - )); - assert!( - options - .public_key - .rp_id - .is_some_and(|val| val.as_ref() == "example.com") - ); - assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert!(matches!( - options.public_key.user_verification, - UserVerificationRequirement::Required - )); - assert!( - options - .public_key - .extensions - .prf - .is_some_and(|prf| prf.first.is_empty() - && prf.second.is_some_and(|p| p.is_empty()) - && matches!(prf.ext_req, ExtensionReq::Allow)) - ); - Ok(()) - } - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[expect(clippy::cognitive_complexity, reason = "a lot to test")] - #[test] - fn key_options() -> Result<(), Error> { - let mut err = - serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{"bob":true}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..130), - Some( - "unknown field `bob`, expected one of `rpId`, `userVerification`, `challenge`, `timeout`, `allowCredentials`, `hints`, `extensions`" - ) - ); - err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( - r#"{"rpId":"example.com","rpId":"example.com"}"#, - ) - .unwrap_err(); - assert_eq!(err.to_string().get(..22), Some("duplicate field `rpId`")); - err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( - r#"{"challenge":"AAAAAAAAAAAAAAAAAAAAAA"}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..41), - Some("invalid type: Option value, expected null") - ); - err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( - r#"{"allowCredentials":[{"type":"public-key","transports":["usb"],"id":"AAAAAAAAAAAAAAAAAAAAAA"}]}"#, - ) - .unwrap_err(); - assert_eq!(err.to_string().get(..19), Some("trailing characters")); - err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{"timeout":0}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..50), - Some("invalid value: integer `0`, expected a nonzero u32") - ); - err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( - r#"{"timeout":4294967296}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..59), - Some("invalid value: integer `4294967296`, expected a nonzero u32") - ); - let mut key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>("{}")?; - assert!(key.rp_id.is_none()); - assert_eq!(key.timeout, FIVE_MINUTES); - assert!(matches!( - key.user_verification, - UserVerificationRequirement::Preferred - )); - assert!(key.extensions.prf.is_none()); - assert_eq!(key.hints, Hints::EMPTY); - key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( - r#"{"rpId":null,"timeout":null,"allowCredentials":null,"userVerification":null,"extensions":null,"hints":null,"challenge":null}"#, - )?; - assert!(key.rp_id.is_none()); - assert_eq!(key.timeout, FIVE_MINUTES); - assert!(matches!( - key.user_verification, - UserVerificationRequirement::Preferred - )); - assert!(key.extensions.prf.is_none()); - assert_eq!(key.hints, Hints::EMPTY); - key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( - r#"{"allowCredentials":[],"extensions":{},"hints":[]}"#, - )?; - assert!(matches!( - key.user_verification, - UserVerificationRequirement::Preferred - )); - assert_eq!(key.hints, Hints::EMPTY); - assert!(key.extensions.prf.is_none()); - key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( - r#"{"extensions":{"prf":null}}"#, - )?; - assert!(key.extensions.prf.is_none()); - key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( - r#"{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}"#, - )?; - assert!(key.rp_id.is_some_and(|val| val.as_ref() == "example.com")); - assert_eq!(key.timeout, FIVE_MINUTES); - assert!(matches!( - key.user_verification, - UserVerificationRequirement::Required - )); - assert_eq!( - key.hints, - Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey) - ); - assert!(key.extensions.prf.is_some_and(|prf| prf.first.is_empty() - && prf.second.is_some_and(|p| p.is_empty()) - && matches!(prf.ext_req, ExtensionReq::Allow))); - key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( - r#"{"timeout":4294967295}"#, - )?; - assert_eq!(key.timeout, NonZeroU32::MAX); - Ok(()) - } - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[test] - fn extension() -> Result<(), Error> { - let mut err = serde_json::from_str::<ExtensionOwned>(r#"{"bob":true}"#).unwrap_err(); - assert_eq!( - err.to_string().get(..35), - Some("unknown field `bob`, expected `prf`") - ); - err = serde_json::from_str::<ExtensionOwned>( - r#"{"prf":{"eval":{"first":"","second":""}},"prf":{"eval":{"first":"","second":""}}}"#, - ) - .unwrap_err(); - assert_eq!(err.to_string().get(..21), Some("duplicate field `prf`")); - err = serde_json::from_str::<ExtensionOwned>(r#"{"prf":{"eval":{"first":null}}}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..51), - Some("invalid type: null, expected base64url-encoded data") - ); - let mut ext = - serde_json::from_str::<ExtensionOwned>(r#"{"prf":{"eval":{"first":"","second":""}}}"#)?; - assert!(ext.prf.is_some_and(|prf| prf.first.is_empty() - && prf.second.is_some_and(|v| v.is_empty()) - && matches!(prf.ext_req, ExtensionReq::Allow))); - ext = serde_json::from_str::<ExtensionOwned>(r#"{"prf":null}"#)?; - assert!(ext.prf.is_none()); - ext = serde_json::from_str::<ExtensionOwned>("{}")?; - assert!(ext.prf.is_none()); - Ok(()) - } -} diff --git a/src/request/auth/ser/tests.rs b/src/request/auth/ser/tests.rs @@ -0,0 +1,229 @@ +use super::{ + super::{super::PublicKeyCredentialHint, ExtensionReq}, + ClientCredentialRequestOptions, CredentialMediationRequirement, ExtensionOwned, FIVE_MINUTES, + Hints, NonZeroU32, PublicKeyCredentialRequestOptionsOwned, UserVerificationRequirement, +}; +use serde_json::Error; +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[expect(clippy::cognitive_complexity, reason = "a lot to test")] +#[test] +fn client_options() -> Result<(), Error> { + let mut err = + serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"bob":true}"#).unwrap_err(); + assert_eq!( + err.to_string().get(..56), + Some("unknown field `bob`, expected `mediation` or `publicKey`") + ); + err = serde_json::from_str::<ClientCredentialRequestOptions>( + r#"{"mediation":"required","mediation":"required"}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..27), + Some("duplicate field `mediation`") + ); + let mut options = serde_json::from_str::<ClientCredentialRequestOptions>("{}")?; + assert!(matches!( + options.mediation, + CredentialMediationRequirement::Required + )); + assert!(options.public_key.rp_id.is_none()); + assert_eq!(options.public_key.timeout, FIVE_MINUTES); + assert!(matches!( + options.public_key.user_verification, + UserVerificationRequirement::Preferred + )); + assert_eq!(options.public_key.hints, Hints::EMPTY); + assert!(options.public_key.extensions.prf.is_none()); + options = serde_json::from_str::<ClientCredentialRequestOptions>( + r#"{"mediation":null,"publicKey":null}"#, + )?; + assert!(matches!( + options.mediation, + CredentialMediationRequirement::Required + )); + assert!(options.public_key.rp_id.is_none()); + assert_eq!(options.public_key.timeout, FIVE_MINUTES); + assert!(matches!( + options.public_key.user_verification, + UserVerificationRequirement::Preferred + )); + assert_eq!(options.public_key.hints, Hints::EMPTY); + assert!(options.public_key.extensions.prf.is_none()); + options = serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"publicKey":{}}"#)?; + assert!(options.public_key.rp_id.is_none()); + assert_eq!(options.public_key.timeout, FIVE_MINUTES); + assert!(matches!( + options.public_key.user_verification, + UserVerificationRequirement::Preferred + )); + assert_eq!(options.public_key.hints, Hints::EMPTY); + assert!(options.public_key.extensions.prf.is_none()); + options = serde_json::from_str::<ClientCredentialRequestOptions>( + r#"{"mediation":"conditional","publicKey":{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}}"#, + )?; + assert!(matches!( + options.mediation, + CredentialMediationRequirement::Conditional + )); + assert!( + options + .public_key + .rp_id + .is_some_and(|val| val.as_ref() == "example.com") + ); + assert_eq!(options.public_key.timeout, FIVE_MINUTES); + assert!(matches!( + options.public_key.user_verification, + UserVerificationRequirement::Required + )); + assert!( + options + .public_key + .extensions + .prf + .is_some_and(|prf| prf.first.is_empty() + && prf.second.is_some_and(|p| p.is_empty()) + && matches!(prf.ext_req, ExtensionReq::Allow)) + ); + Ok(()) +} +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[expect(clippy::cognitive_complexity, reason = "a lot to test")] +#[test] +fn key_options() -> Result<(), Error> { + let mut err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{"bob":true}"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..130), + Some( + "unknown field `bob`, expected one of `rpId`, `userVerification`, `challenge`, `timeout`, `allowCredentials`, `hints`, `extensions`" + ) + ); + err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( + r#"{"rpId":"example.com","rpId":"example.com"}"#, + ) + .unwrap_err(); + assert_eq!(err.to_string().get(..22), Some("duplicate field `rpId`")); + err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( + r#"{"challenge":"AAAAAAAAAAAAAAAAAAAAAA"}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..41), + Some("invalid type: Option value, expected null") + ); + err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( + r#"{"allowCredentials":[{"type":"public-key","transports":["usb"],"id":"AAAAAAAAAAAAAAAAAAAAAA"}]}"#, + ) + .unwrap_err(); + assert_eq!(err.to_string().get(..19), Some("trailing characters")); + err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{"timeout":0}"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..50), + Some("invalid value: integer `0`, expected a nonzero u32") + ); + err = + serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{"timeout":4294967296}"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..59), + Some("invalid value: integer `4294967296`, expected a nonzero u32") + ); + let mut key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>("{}")?; + assert!(key.rp_id.is_none()); + assert_eq!(key.timeout, FIVE_MINUTES); + assert!(matches!( + key.user_verification, + UserVerificationRequirement::Preferred + )); + assert!(key.extensions.prf.is_none()); + assert_eq!(key.hints, Hints::EMPTY); + key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( + r#"{"rpId":null,"timeout":null,"allowCredentials":null,"userVerification":null,"extensions":null,"hints":null,"challenge":null}"#, + )?; + assert!(key.rp_id.is_none()); + assert_eq!(key.timeout, FIVE_MINUTES); + assert!(matches!( + key.user_verification, + UserVerificationRequirement::Preferred + )); + assert!(key.extensions.prf.is_none()); + assert_eq!(key.hints, Hints::EMPTY); + key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( + r#"{"allowCredentials":[],"extensions":{},"hints":[]}"#, + )?; + assert!(matches!( + key.user_verification, + UserVerificationRequirement::Preferred + )); + assert_eq!(key.hints, Hints::EMPTY); + assert!(key.extensions.prf.is_none()); + key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( + r#"{"extensions":{"prf":null}}"#, + )?; + assert!(key.extensions.prf.is_none()); + key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( + r#"{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}"#, + )?; + assert!(key.rp_id.is_some_and(|val| val.as_ref() == "example.com")); + assert_eq!(key.timeout, FIVE_MINUTES); + assert!(matches!( + key.user_verification, + UserVerificationRequirement::Required + )); + assert_eq!( + key.hints, + Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey) + ); + assert!(key.extensions.prf.is_some_and(|prf| prf.first.is_empty() + && prf.second.is_some_and(|p| p.is_empty()) + && matches!(prf.ext_req, ExtensionReq::Allow))); + key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( + r#"{"timeout":4294967295}"#, + )?; + assert_eq!(key.timeout, NonZeroU32::MAX); + Ok(()) +} +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[test] +fn extension() -> Result<(), Error> { + let mut err = serde_json::from_str::<ExtensionOwned>(r#"{"bob":true}"#).unwrap_err(); + assert_eq!( + err.to_string().get(..35), + Some("unknown field `bob`, expected `prf`") + ); + err = serde_json::from_str::<ExtensionOwned>( + r#"{"prf":{"eval":{"first":"","second":""}},"prf":{"eval":{"first":"","second":""}}}"#, + ) + .unwrap_err(); + assert_eq!(err.to_string().get(..21), Some("duplicate field `prf`")); + err = serde_json::from_str::<ExtensionOwned>(r#"{"prf":{"eval":{"first":null}}}"#).unwrap_err(); + assert_eq!( + err.to_string().get(..51), + Some("invalid type: null, expected base64url-encoded data") + ); + let mut ext = + serde_json::from_str::<ExtensionOwned>(r#"{"prf":{"eval":{"first":"","second":""}}}"#)?; + assert!(ext.prf.is_some_and(|prf| prf.first.is_empty() + && prf.second.is_some_and(|v| v.is_empty()) + && matches!(prf.ext_req, ExtensionReq::Allow))); + ext = serde_json::from_str::<ExtensionOwned>(r#"{"prf":null}"#)?; + assert!(ext.prf.is_none()); + ext = serde_json::from_str::<ExtensionOwned>("{}")?; + assert!(ext.prf.is_none()); + Ok(()) +} diff --git a/src/request/auth/tests.rs b/src/request/auth/tests.rs @@ -0,0 +1,525 @@ +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +use super::{ + super::super::{ + AuthenticatedCredential, DynamicState, StaticState, UserHandle, + response::{ + Backup, + auth::{DiscoverableAuthenticatorAssertion, HmacSecret}, + register::{ + AuthenticationExtensionsPrfOutputs, AuthenticatorExtensionOutputStaticState, + ClientExtensionsOutputsStaticState, CompressedPubKeyOwned, + CredentialProtectionPolicy, Ed25519PubKey, + }, + }, + }, + AuthCeremonyErr, AuthenticationVerificationOptions, AuthenticatorAttachment, + AuthenticatorAttachmentEnforcement, DiscoverableAuthentication, ExtensionErr, OneOrTwo, + PrfInput, SignatureCounterEnforcement, +}; +#[cfg(all( + feature = "custom", + any( + feature = "serializable_server_state", + not(any(feature = "bin", feature = "serde")) + ) +))] +use super::{ + super::{super::AggErr, Challenge, CredentialId, RpId, UserVerificationRequirement}, + DiscoverableCredentialRequestOptions, ExtensionReq, +}; +#[cfg(all(feature = "custom", feature = "serializable_server_state"))] +use super::{ + super::{ + super::bin::{Decode as _, Encode as _}, + AsciiDomain, AuthTransports, + }, + AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Credentials as _, + DiscoverableAuthenticationServerState, Extension, NonDiscoverableAuthenticationServerState, + NonDiscoverableCredentialRequestOptions, PrfInputOwned, PublicKeyCredentialDescriptor, +}; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +use ed25519_dalek::{Signer as _, SigningKey}; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +use rsa::sha2::{Digest as _, Sha256}; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_BYTES: u8 = 0b010_00000; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_TEXT: u8 = 0b011_00000; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_MAP: u8 = 0b101_00000; +#[expect(clippy::panic_in_result_fn, reason = "OK in tests")] +#[test] +#[cfg(all(feature = "custom", feature = "serializable_server_state"))] +fn eddsa_auth_ser() -> Result<(), AggErr> { + let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); + 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 = Extension { prf: None }; + let server = opts.start_ceremony()?.0; + let enc_data = server.encode()?; + assert_eq!(enc_data.capacity(), 16 + 2 + 1 + 1 + 12 + 2 + 128 + 1); + assert_eq!(enc_data.len(), 16 + 2 + 1 + 1 + 12 + 2 + 16 + 2); + assert!( + server.is_eq(&NonDiscoverableAuthenticationServerState::decode( + enc_data.as_slice() + )?) + ); + let mut opts_2 = DiscoverableCredentialRequestOptions::passkey(&rp_id); + opts_2.public_key.challenge = Challenge(0); + opts_2.public_key.extensions = Extension { prf: None }; + let server_2 = opts_2.start_ceremony()?.0; + let enc_data_2 = server_2 + .encode() + .map_err(AggErr::EncodeDiscoverableAuthenticationServerState)?; + assert_eq!(enc_data_2.capacity(), enc_data_2.len()); + assert_eq!(enc_data_2.len(), 16 + 1 + 1 + 12); + assert!( + server_2.is_eq(&DiscoverableAuthenticationServerState::decode( + enc_data_2.as_slice() + )?) + ); + Ok(()) +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +struct TestResponseOptions { + user_verified: bool, + hmac: HmacSecret, +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +enum PrfCredOptions { + None, + FalseNoHmac, + FalseHmacFalse, + TrueNoHmac, + TrueHmacTrue, +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +struct TestCredOptions { + cred_protect: CredentialProtectionPolicy, + prf: PrfCredOptions, +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +enum PrfUvOptions { + /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise + /// `UserVerificationRequirement::Preferred` is used. + None(bool), + Prf((PrfInput<'static, 'static>, ExtensionReq)), +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +struct TestRequestOptions { + error_unsolicited: bool, + prf_uv: PrfUvOptions, +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +struct TestOptions { + request: TestRequestOptions, + response: TestResponseOptions, + cred: TestCredOptions, +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +fn generate_client_data_json() -> Vec<u8> { + let mut json = Vec::with_capacity(256); + json.extend_from_slice(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()); + json +} +#[expect(clippy::indexing_slicing, reason = "comments justify correctness")] +#[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>, CompressedPubKeyOwned, Vec<u8>) { + let mut authenticator_data = Vec::with_capacity(256); + authenticator_data.extend_from_slice( + [ + // RP ID HASH. + // 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, AT, and ED (right-to-left). + 0b0000_0001 + | if opts.user_verified { + 0b0000_0100 + } else { + 0b0000_0000 + } + | if matches!(opts.hmac, HmacSecret::None) { + 0 + } else { + 0b1000_0000 + }, + // COUNTER. + // 0 as 32-bit big endian. + 0, + 0, + 0, + 0, + ] + .as_slice(), + ); + authenticator_data[..32].copy_from_slice(&Sha256::digest(b"example.com")); + match opts.hmac { + HmacSecret::None => {} + HmacSecret::One => { + authenticator_data.extend_from_slice( + [ + CBOR_MAP | 1, + // CBOR text of length 11. + 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, + 48, + ] + .as_slice(), + ); + authenticator_data.extend_from_slice([0; 48].as_slice()); + } + HmacSecret::Two => { + authenticator_data.extend_from_slice( + [ + CBOR_MAP | 1, + // CBOR text of length 11. + 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, + 80, + ] + .as_slice(), + ); + authenticator_data.extend_from_slice([0; 80].as_slice()); + } + } + let len = authenticator_data.len(); + authenticator_data.extend_from_slice(&Sha256::digest(generate_client_data_json().as_slice())); + let sig_key = SigningKey::from_bytes(&[0; 32]); + let sig = sig_key.sign(authenticator_data.as_slice()).to_vec(); + authenticator_data.truncate(len); + ( + authenticator_data, + CompressedPubKeyOwned::Ed25519(Ed25519PubKey::from(sig_key.verifying_key().to_bytes())), + sig, + ) +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +fn validate(options: TestOptions) -> Result<(), AggErr> { + let rp_id = RpId::Domain("example.com".to_owned().try_into()?); + 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].into_boxed_slice())?; + let authentication = DiscoverableAuthentication::new( + credential_id.clone(), + DiscoverableAuthenticatorAssertion::new( + generate_client_data_json(), + authenticator_data, + signature, + user, + ), + AuthenticatorAttachment::None, + ); + let auth_opts = AuthenticationVerificationOptions::<'static, 'static, &str, &str> { + allowed_origins: [].as_slice(), + allowed_top_origins: None, + auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Update(false), + backup_requirement: None, + error_on_unsolicited_extensions: options.request.error_unsolicited, + sig_counter_enforcement: SignatureCounterEnforcement::Fail, + update_uv: false, + #[cfg(feature = "serde_relaxed")] + client_data_json_relaxed: false, + }; + let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id); + opts.public_key.challenge = Challenge(0); + opts.public_key.user_verification = UserVerificationRequirement::Preferred; + match options.request.prf_uv { + PrfUvOptions::None(required) => { + if required { + opts.public_key.user_verification = UserVerificationRequirement::Required; + } + } + PrfUvOptions::Prf(input) => { + opts.public_key.user_verification = UserVerificationRequirement::Required; + opts.public_key.extensions.prf = Some(input); + } + } + let mut cred = AuthenticatedCredential::new( + (&credential_id).into(), + &user, + StaticState { + credential_public_key, + extensions: AuthenticatorExtensionOutputStaticState { + cred_protect: options.cred.cred_protect, + hmac_secret: match options.cred.prf { + PrfCredOptions::None + | PrfCredOptions::FalseNoHmac + | PrfCredOptions::TrueNoHmac => None, + PrfCredOptions::FalseHmacFalse => Some(false), + PrfCredOptions::TrueHmacTrue => Some(true), + }, + }, + client_extension_results: ClientExtensionsOutputsStaticState { + prf: match options.cred.prf { + PrfCredOptions::None => None, + PrfCredOptions::FalseNoHmac | PrfCredOptions::FalseHmacFalse => { + Some(AuthenticationExtensionsPrfOutputs { enabled: false }) + } + PrfCredOptions::TrueNoHmac | PrfCredOptions::TrueHmacTrue => { + Some(AuthenticationExtensionsPrfOutputs { enabled: true }) + } + }, + }, + }, + DynamicState { + user_verified: true, + backup: Backup::NotEligible, + sign_count: 0, + authenticator_attachment: AuthenticatorAttachment::None, + }, + )?; + opts.start_ceremony()? + .0 + .verify(&rp_id, &authentication, &mut cred, &auth_opts) + .map_err(AggErr::AuthCeremony) + .map(|_| ()) +} +/// Test all, and only, possible `UserNotVerified` errors. +/// 4 * 5 * 3 * 2 * 5 = 600 tests. +/// We ignore this due to how long it takes (around 4 seconds or so). +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[ignore = "slow"] +#[test] +fn uv_required_err() { + const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ + CredentialProtectionPolicy::None, + CredentialProtectionPolicy::UserVerificationOptional, + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, + CredentialProtectionPolicy::UserVerificationRequired, + ]; + const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [ + PrfCredOptions::None, + PrfCredOptions::FalseNoHmac, + PrfCredOptions::FalseHmacFalse, + PrfCredOptions::TrueNoHmac, + PrfCredOptions::TrueHmacTrue, + ]; + const ALL_HMAC_OPTIONS: [HmacSecret; 3] = [HmacSecret::None, HmacSecret::One, HmacSecret::Two]; + const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true]; + const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [ + PrfUvOptions::None(true), + PrfUvOptions::Prf(( + PrfInput { + first: [].as_slice(), + second: None, + }, + ExtensionReq::Require, + )), + PrfUvOptions::Prf(( + PrfInput { + first: [].as_slice(), + second: None, + }, + ExtensionReq::Allow, + )), + PrfUvOptions::Prf(( + PrfInput { + first: [].as_slice(), + second: Some([].as_slice()), + }, + ExtensionReq::Require, + )), + PrfUvOptions::Prf(( + PrfInput { + first: [].as_slice(), + second: Some([].as_slice()), + }, + ExtensionReq::Allow, + )), + ]; + for cred_protect in ALL_CRED_PROTECT_OPTIONS { + for prf in ALL_PRF_CRED_OPTIONS { + for hmac in ALL_HMAC_OPTIONS { + for error_unsolicited in ALL_UNSOLICIT_OPTIONS { + for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS { + assert!(validate(TestOptions { + request: TestRequestOptions { + error_unsolicited, + prf_uv, + }, + response: TestResponseOptions { + user_verified: false, + hmac, + }, + cred: TestCredOptions { cred_protect, prf, }, + }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::UserNotVerified)))); + } + } + } + } + } +} +/// Test all, and only, possible `UserNotVerified` errors. +/// 4 * 5 * 2 * 2 = 80 tests. +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[test] +fn forbidden_hmac() { + const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ + CredentialProtectionPolicy::None, + CredentialProtectionPolicy::UserVerificationOptional, + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, + CredentialProtectionPolicy::UserVerificationRequired, + ]; + const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [ + PrfCredOptions::None, + PrfCredOptions::FalseNoHmac, + PrfCredOptions::FalseHmacFalse, + PrfCredOptions::TrueNoHmac, + PrfCredOptions::TrueHmacTrue, + ]; + const ALL_HMAC_OPTIONS: [HmacSecret; 2] = [HmacSecret::One, HmacSecret::Two]; + const ALL_UV_OPTIONS: [bool; 2] = [false, true]; + for cred_protect in ALL_CRED_PROTECT_OPTIONS { + for prf in ALL_PRF_CRED_OPTIONS { + for hmac in ALL_HMAC_OPTIONS { + for user_verified in ALL_UV_OPTIONS { + assert!(validate(TestOptions { + request: TestRequestOptions { + error_unsolicited: true, + prf_uv: PrfUvOptions::None(false), + }, + response: TestResponseOptions { + user_verified, + hmac, + }, + cred: TestCredOptions { cred_protect, prf, }, + }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenHmacSecret))))); + } + } + } + } +} +#[expect(clippy::panic_in_result_fn, reason = "OK in tests")] +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[test] +fn prf() -> Result<(), AggErr> { + let mut opts = TestOptions { + request: TestRequestOptions { + error_unsolicited: false, + prf_uv: PrfUvOptions::Prf(( + PrfInput { + first: [].as_slice(), + second: None, + }, + ExtensionReq::Allow, + )), + }, + response: TestResponseOptions { + user_verified: true, + hmac: HmacSecret::None, + }, + cred: TestCredOptions { + cred_protect: CredentialProtectionPolicy::None, + prf: PrfCredOptions::None, + }, + }; + validate(opts)?; + opts.request.prf_uv = PrfUvOptions::Prf(( + PrfInput { + first: [].as_slice(), + second: None, + }, + ExtensionReq::Require, + )); + opts.cred.prf = PrfCredOptions::TrueHmacTrue; + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::MissingHmacSecret))))); + opts.response.hmac = HmacSecret::One; + opts.request.prf_uv = PrfUvOptions::Prf(( + PrfInput { + first: [].as_slice(), + second: None, + }, + ExtensionReq::Allow, + )); + opts.cred.prf = PrfCredOptions::TrueNoHmac; + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential))))); + opts.response.hmac = HmacSecret::Two; + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue(OneOrTwo::One, OneOrTwo::Two)))))); + opts.response.hmac = HmacSecret::One; + opts.cred.prf = PrfCredOptions::FalseNoHmac; + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential))))); + opts.response.user_verified = false; + opts.request.prf_uv = PrfUvOptions::None(false); + opts.cred.prf = PrfCredOptions::TrueHmacTrue; + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::UserNotVerifiedHmacSecret))))); + Ok(()) +} diff --git a/src/request/register.rs b/src/request/register.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; use super::{ super::{ DynamicState, Metadata, RegisteredCredential, StaticState, @@ -2120,884 +2122,3 @@ impl<const USER_LEN: usize> Ord for RegistrationServerState<USER_LEN> { pub type RegistrationServerState64 = RegistrationServerState<USER_HANDLE_MAX_LEN>; /// `RegistrationServerState` based on a [`UserHandle16`]. pub type RegistrationServerState16 = RegistrationServerState<16>; -#[cfg(test)] -mod tests { - #[cfg(all( - feature = "custom", - any( - feature = "serializable_server_state", - not(any(feature = "bin", feature = "serde")) - ) - ))] - use super::{ - super::{super::AggErr, ExtensionInfo}, - Challenge, CredProtect, CredentialCreationOptions, FourToSixtyThree, PrfInput, - PublicKeyCredentialUserEntity, RpId, UserHandle, - }; - #[cfg(all(feature = "custom", feature = "serializable_server_state"))] - use super::{ - super::{ - super::bin::{Decode as _, Encode as _}, - AsciiDomain, - }, - Extension, RegistrationServerState, - }; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - use super::{ - super::{ - super::{ - CredentialErr, - response::register::{ - AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation, - ClientExtensionsOutputs, CredentialPropertiesOutput, - CredentialProtectionPolicy, HmacSecret, - }, - }, - AuthTransports, - }, - AuthenticatorAttachment, BackupReq, ExtensionErr, ExtensionReq, RegCeremonyErr, - Registration, RegistrationVerificationOptions, UserVerificationRequirement, - }; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - use rsa::sha2::{Digest as _, Sha256}; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_UINT: u8 = 0b000_00000; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_NEG: u8 = 0b001_00000; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_BYTES: u8 = 0b010_00000; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_TEXT: u8 = 0b011_00000; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_MAP: u8 = 0b101_00000; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_SIMPLE: u8 = 0b111_00000; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_FALSE: u8 = CBOR_SIMPLE | 20; - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - const CBOR_TRUE: u8 = CBOR_SIMPLE | 21; - #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] - #[test] - #[cfg(all(feature = "custom", feature = "serializable_server_state"))] - fn eddsa_reg_ser() -> Result<(), AggErr> { - let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); - let id = UserHandle::from([0; 1]); - let mut opts = CredentialCreationOptions::passkey( - &rp_id, - PublicKeyCredentialUserEntity { - name: "foo", - id: &id, - display_name: "", - }, - Vec::new(), - ); - opts.public_key.challenge = Challenge(0); - opts.public_key.extensions = Extension { - cred_props: None, - cred_protect: CredProtect::UserVerificationRequired( - false, - ExtensionInfo::RequireEnforceValue, - ), - min_pin_length: Some((FourToSixtyThree::Ten, ExtensionInfo::RequireEnforceValue)), - prf: Some(( - PrfInput { - first: [0].as_slice(), - second: None, - }, - ExtensionInfo::RequireEnforceValue, - )), - }; - let server = opts.start_ceremony()?.0; - let enc_data = server - .encode() - .map_err(AggErr::EncodeRegistrationServerState)?; - assert_eq!(enc_data.capacity(), enc_data.len()); - assert_eq!(enc_data.len(), 1 + 16 + 1 + 3 + (1 + 3 + 3 + 2) + 12 + 1); - assert!(server.is_eq(&RegistrationServerState::decode(enc_data.as_slice())?)); - Ok(()) - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - struct TestResponseOptions { - user_verified: bool, - cred_protect: CredentialProtectionPolicy, - prf: Option<bool>, - hmac: HmacSecret, - min_pin: Option<FourToSixtyThree>, - #[expect(clippy::option_option, reason = "fine")] - cred_props: Option<Option<bool>>, - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - enum PrfUvOptions { - /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise - /// `UserVerificationRequirement::Preferred` is used. - None(bool), - Prf(ExtensionInfo), - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - struct TestRequestOptions { - error_unsolicited: bool, - protect: CredProtect, - prf_uv: PrfUvOptions, - props: Option<ExtensionReq>, - pin: Option<(FourToSixtyThree, ExtensionInfo)>, - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - #[derive(Clone, Copy)] - struct TestOptions { - request: TestRequestOptions, - response: TestResponseOptions, - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn generate_client_data_json() -> Vec<u8> { - let mut json = Vec::with_capacity(256); - json.extend_from_slice(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()); - json - } - #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] - #[expect( - clippy::arithmetic_side_effects, - clippy::indexing_slicing, - reason = "comments justify correctness" - )] - #[expect( - clippy::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn generate_attestation_object(options: TestResponseOptions) -> Vec<u8> { - let mut attestation_object = Vec::with_capacity(256); - 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 | 24, - // Length. - // Addition won't overflow. - 113 + if matches!(options.cred_protect, CredentialProtectionPolicy::None) { - if matches!(options.hmac, HmacSecret::None) { - options.min_pin.map_or(0, |_| 15) - } else { - 14 + options.min_pin.map_or(0, |_| 14) - + match options.hmac { - HmacSecret::None => unreachable!("bug"), - HmacSecret::NotEnabled | HmacSecret::Enabled => 0, - HmacSecret::One => 65, - HmacSecret::Two => 97, - } - } - } else { - 14 + if matches!(options.hmac, HmacSecret::None) { - 0 - } else { - 13 - } + options.min_pin.map_or(0, |_| 14) - + match options.hmac { - HmacSecret::None | HmacSecret::NotEnabled | HmacSecret::Enabled => 0, - HmacSecret::One => 65, - HmacSecret::Two => 97, - } - }, - // RP ID HASH. - // 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, AT, and ED (right-to-left). - 0b0100_0001 - | if options.user_verified { - 0b0000_0100 - } else { - 0b0000_0000 - } - | if matches!(options.cred_protect, CredentialProtectionPolicy::None) - && matches!(options.hmac, HmacSecret::None) - && options.min_pin.is_none() - { - 0 - } else { - 0b1000_0000 - }, - // COUNTER. - // 0 as 32-bit big endian. - 0, - 0, - 0, - 0, - // AAGUID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // L. - // CREDENTIAL ID length is 16 as 16-bit big endian. - 0, - 16, - // CREDENTIAL ID. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - CBOR_MAP | 4, - // COSE kty. - CBOR_UINT | 1, - // COSE OKP. - CBOR_UINT | 1, - // COSE alg. - CBOR_UINT | 3, - // COSE Eddsa. - CBOR_NEG | 7, - // COSE OKP crv. - CBOR_NEG, - // COSE Ed25519. - CBOR_UINT | 6, - // COSE OKP x. - CBOR_NEG | 1, - CBOR_BYTES | 24, - // Length is 32. - 32, - // Compressed-y coordinate. - 59, - 106, - 39, - 188, - 206, - 182, - 164, - 45, - 98, - 163, - 168, - 208, - 42, - 111, - 13, - 115, - 101, - 50, - 21, - 119, - 29, - 226, - 67, - 166, - 58, - 192, - 72, - 161, - 139, - 89, - 218, - 41, - ] - .as_slice(), - ); - attestation_object[30..62].copy_from_slice(&Sha256::digest(b"example.com")); - if matches!(options.cred_protect, CredentialProtectionPolicy::None) { - if matches!(options.hmac, HmacSecret::None) { - if options.min_pin.is_some() { - attestation_object.push(CBOR_MAP | 1); - } - } else if options.min_pin.is_some() { - attestation_object.push( - // Addition won't overflow. - CBOR_MAP - | (2 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two))), - ); - } else { - attestation_object.push( - // Addition won't overflow. - CBOR_MAP - | (1 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two))), - ); - } - } else { - attestation_object.extend_from_slice( - [ - // Addition won't overflow. - CBOR_MAP | (1 + match options.hmac { HmacSecret::None => 0, HmacSecret::NotEnabled | HmacSecret::Enabled => 1, HmacSecret::One | HmacSecret::Two => 2, } + u8::from(options.min_pin.is_some())), - // CBOR text of length 11. - CBOR_TEXT | 11, - b'c', - b'r', - b'e', - b'd', - b'P', - b'r', - b'o', - b't', - b'e', - b'c', - b't', - // Addition won't overflow. - match options.cred_protect { CredentialProtectionPolicy::None => unreachable!("bug"), CredentialProtectionPolicy::UserVerificationOptional => 1, CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => 2, CredentialProtectionPolicy::UserVerificationRequired => 3, }, - ].as_slice() - ); - } - if !matches!(options.hmac, HmacSecret::None) { - attestation_object.extend_from_slice( - [ - // CBOR text of length 11. - CBOR_TEXT | 11, - b'h', - b'm', - b'a', - b'c', - b'-', - b's', - b'e', - b'c', - b'r', - b'e', - b't', - if matches!(options.hmac, HmacSecret::NotEnabled) { - CBOR_FALSE - } else { - CBOR_TRUE - }, - ] - .as_slice(), - ); - } - _ = options.min_pin.map(|p| { - assert!(p <= FourToSixtyThree::TwentyThree, "bug"); - attestation_object.extend_from_slice( - [ - // CBOR text of length 12. - CBOR_TEXT | 12, - b'm', - b'i', - b'n', - b'P', - b'i', - b'n', - b'L', - b'e', - b'n', - b'g', - b't', - b'h', - CBOR_UINT | p.into_u8(), - ] - .as_slice(), - ); - }); - if matches!(options.hmac, HmacSecret::One | HmacSecret::Two) { - attestation_object.extend_from_slice( - [ - // CBOR text of length 14. - CBOR_TEXT | 14, - b'h', - b'm', - b'a', - b'c', - b'-', - b's', - b'e', - b'c', - b'r', - b'e', - b't', - b'-', - b'm', - b'c', - CBOR_BYTES | 24, - ] - .as_slice(), - ); - if matches!(options.hmac, HmacSecret::One) { - attestation_object.push(48); - attestation_object.extend_from_slice([1; 48].as_slice()); - } else { - attestation_object.push(80); - attestation_object.extend_from_slice([1; 80].as_slice()); - } - } - attestation_object - } - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn validate(options: TestOptions) -> Result<(), AggErr> { - let rp_id = RpId::Domain("example.com".to_owned().try_into()?); - let registration = Registration::new( - AuthenticatorAttestation::new( - generate_client_data_json(), - generate_attestation_object(options.response), - AuthTransports::NONE, - ), - AuthenticatorAttachment::None, - ClientExtensionsOutputs { - cred_props: options - .response - .cred_props - .map(|rk| CredentialPropertiesOutput { rk }), - prf: options - .response - .prf - .map(|enabled| AuthenticationExtensionsPrfOutputs { enabled }), - }, - ); - let reg_opts = RegistrationVerificationOptions::<'static, 'static, &str, &str> { - allowed_origins: [].as_slice(), - allowed_top_origins: None, - backup_requirement: BackupReq::None, - error_on_unsolicited_extensions: options.request.error_unsolicited, - require_authenticator_attachment: false, - #[cfg(feature = "serde_relaxed")] - client_data_json_relaxed: false, - }; - let user = UserHandle::from([0; 1]); - let mut opts = CredentialCreationOptions::passkey( - &rp_id, - PublicKeyCredentialUserEntity { - id: &user, - name: "", - display_name: "", - }, - Vec::new(), - ); - opts.public_key.challenge = Challenge(0); - opts.public_key.authenticator_selection.user_verification = - UserVerificationRequirement::Preferred; - match options.request.prf_uv { - PrfUvOptions::None(required) => { - if required - || matches!( - options.request.protect, - CredProtect::UserVerificationRequired(_, _) - ) - { - opts.public_key.authenticator_selection.user_verification = - UserVerificationRequirement::Required; - } - } - PrfUvOptions::Prf(info) => { - opts.public_key.authenticator_selection.user_verification = - UserVerificationRequirement::Required; - opts.public_key.extensions.prf = Some(( - PrfInput { - first: [0].as_slice(), - second: None, - }, - info, - )); - } - } - opts.public_key.extensions.cred_protect = options.request.protect; - opts.public_key.extensions.cred_props = options.request.props; - opts.public_key.extensions.min_pin_length = options.request.pin; - opts.start_ceremony()? - .0 - .verify(&rp_id, &registration, &reg_opts) - .map_err(AggErr::RegCeremony) - .map(|_| ()) - } - /// Test all, and only, possible `UserNotVerified` errors. - /// 4 * 3 * 5 * 2 * 13 * 5 * 3 * 5 * 4 * 4 = 1,872,000 tests. - /// We ignore this due to how long it takes (around 30 seconds or so). - #[expect(clippy::too_many_lines, reason = "a lot to test")] - #[test] - #[ignore = "slow"] - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn uv_required_err() { - const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ - CredentialProtectionPolicy::None, - CredentialProtectionPolicy::UserVerificationOptional, - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, - CredentialProtectionPolicy::UserVerificationRequired, - ]; - const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)]; - const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [ - HmacSecret::None, - HmacSecret::NotEnabled, - HmacSecret::Enabled, - HmacSecret::One, - HmacSecret::Two, - ]; - const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true]; - const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [ - CredProtect::None, - CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue), - CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue), - CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue), - CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue), - CredProtect::UserVerificationOptionalWithCredentialIdList( - false, - ExtensionInfo::RequireEnforceValue, - ), - CredProtect::UserVerificationOptionalWithCredentialIdList( - true, - ExtensionInfo::RequireDontEnforceValue, - ), - CredProtect::UserVerificationOptionalWithCredentialIdList( - false, - ExtensionInfo::AllowEnforceValue, - ), - CredProtect::UserVerificationOptionalWithCredentialIdList( - true, - ExtensionInfo::AllowDontEnforceValue, - ), - CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), - CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue), - CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue), - CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue), - ]; - const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [ - PrfUvOptions::None(true), - PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), - PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue), - PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue), - PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue), - ]; - const ALL_PROPS_OPTIONS: [Option<ExtensionReq>; 3] = - [None, Some(ExtensionReq::Require), Some(ExtensionReq::Allow)]; - const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [ - None, - Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)), - Some(( - FourToSixtyThree::Five, - ExtensionInfo::RequireDontEnforceValue, - )), - Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)), - Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)), - ]; - #[expect(clippy::option_option, reason = "fine")] - const ALL_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 4] = - [None, Some(None), Some(Some(false)), Some(Some(true))]; - const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [ - None, - Some(FourToSixtyThree::Four), - Some(FourToSixtyThree::Five), - Some(FourToSixtyThree::Six), - ]; - for cred_protect in ALL_CRED_PROTECTION_OPTIONS { - for prf in ALL_PRF_OPTIONS { - for hmac in ALL_HMAC_OPTIONS { - for cred_props in ALL_CRED_PROPS_OPTIONS { - for min_pin in ALL_MIN_PIN_OPTIONS { - for error_unsolicited in ALL_UNSOLICIT_OPTIONS { - for protect in ALL_CRED_PROTECT_OPTIONS { - for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS { - for props in ALL_PROPS_OPTIONS { - for pin in ALL_PIN_OPTIONS { - assert!(validate(TestOptions { - request: TestRequestOptions { - error_unsolicited, - protect, - prf_uv, - props, - pin, - }, - response: TestResponseOptions { - user_verified: false, - hmac, - cred_protect, - prf, - min_pin, - cred_props, - }, - }).is_err_and(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::UserNotVerified)))); - } - } - } - } - } - } - } - } - } - } - } - /// Test all, and only, possible `ForbiddenCredProps` errors. - /// 4 * 3 * 5 * 2 * 13 * 6 * 5 * 3 * 4 = 561,600 - /// - - /// 4 * 3 * 5 * 4 * 6 * 5 * 3 * 4 = 86,400 - /// - - /// 4 * 3 * 5 * 13 * 5 * 5 * 3 * 4 = 234,000 - /// + - /// 4 * 3 * 5 * 4 * 5 * 5 * 3 * 4 = 72,000 - /// = - /// 313,200 total tests. - /// We ignore this due to how long it takes (around 6 seconds or so). - #[expect(clippy::too_many_lines, reason = "a lot to test")] - #[test] - #[ignore = "slow"] - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn forbidden_cred_props() { - const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ - CredentialProtectionPolicy::None, - CredentialProtectionPolicy::UserVerificationOptional, - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, - CredentialProtectionPolicy::UserVerificationRequired, - ]; - const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)]; - const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [ - HmacSecret::None, - HmacSecret::NotEnabled, - HmacSecret::Enabled, - HmacSecret::One, - HmacSecret::Two, - ]; - const ALL_UV_OPTIONS: [bool; 2] = [false, true]; - const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [ - CredProtect::None, - CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue), - CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue), - CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue), - CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue), - CredProtect::UserVerificationOptionalWithCredentialIdList( - false, - ExtensionInfo::RequireEnforceValue, - ), - CredProtect::UserVerificationOptionalWithCredentialIdList( - true, - ExtensionInfo::RequireDontEnforceValue, - ), - CredProtect::UserVerificationOptionalWithCredentialIdList( - false, - ExtensionInfo::AllowEnforceValue, - ), - CredProtect::UserVerificationOptionalWithCredentialIdList( - true, - ExtensionInfo::AllowDontEnforceValue, - ), - CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), - CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue), - CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue), - CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue), - ]; - const ALL_PRF_UV_OPTIONS: [PrfUvOptions; 6] = [ - PrfUvOptions::None(false), - PrfUvOptions::None(true), - PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), - PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue), - PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue), - PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue), - ]; - const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [ - None, - Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)), - Some(( - FourToSixtyThree::Five, - ExtensionInfo::RequireDontEnforceValue, - )), - Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)), - Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)), - ]; - #[expect(clippy::option_option, reason = "fine")] - const ALL_NON_EMPTY_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 3] = - [Some(None), Some(Some(false)), Some(Some(true))]; - const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [ - None, - Some(FourToSixtyThree::Four), - Some(FourToSixtyThree::Five), - Some(FourToSixtyThree::Six), - ]; - for cred_protect in ALL_CRED_PROTECTION_OPTIONS { - for prf in ALL_PRF_OPTIONS { - for hmac in ALL_HMAC_OPTIONS { - for cred_props in ALL_NON_EMPTY_CRED_PROPS_OPTIONS { - for min_pin in ALL_MIN_PIN_OPTIONS { - for user_verified in ALL_UV_OPTIONS { - for protect in ALL_CRED_PROTECT_OPTIONS { - for prf_uv in ALL_PRF_UV_OPTIONS { - for pin in ALL_PIN_OPTIONS { - if user_verified - || (!matches!( - protect, - CredProtect::UserVerificationRequired(_, _) - ) && matches!(prf_uv, PrfUvOptions::None(uv) if !uv)) - { - assert!(validate(TestOptions { - request: TestRequestOptions { - error_unsolicited: true, - protect, - prf_uv, - props: None, - pin, - }, - response: TestResponseOptions { - user_verified, - cred_protect, - prf, - hmac, - min_pin, - cred_props, - }, - }).is_err_and(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenCredProps))))); - } - } - } - } - } - } - } - } - } - } - } - #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] - #[test] - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn prf() -> Result<(), AggErr> { - let mut opts = TestOptions { - request: TestRequestOptions { - error_unsolicited: false, - protect: CredProtect::None, - prf_uv: PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), - props: None, - pin: None, - }, - response: TestResponseOptions { - user_verified: true, - hmac: HmacSecret::None, - cred_protect: CredentialProtectionPolicy::None, - prf: Some(true), - min_pin: None, - cred_props: None, - }, - }; - validate(opts)?; - opts.response.prf = Some(false); - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidPrfValue))))); - opts.response.hmac = HmacSecret::NotEnabled; - opts.response.prf = Some(true); - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue))))); - opts.request.prf_uv = PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue); - opts.response.hmac = HmacSecret::Enabled; - opts.response.prf = None; - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))))); - opts.response.hmac = HmacSecret::NotEnabled; - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))))); - opts.response.prf = Some(true); - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::PrfWithoutHmacSecret))))); - opts.response.prf = Some(false); - validate(opts)?; - opts.request.prf_uv = PrfUvOptions::None(false); - opts.response.user_verified = false; - opts.response.hmac = HmacSecret::Enabled; - opts.response.prf = Some(true); - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutUserVerified))))); - opts.response.prf = None; - opts.response.hmac = HmacSecret::None; - validate(opts)?; - Ok(()) - } - #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] - #[test] - #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn cred_protect() -> Result<(), AggErr> { - let mut opts = TestOptions { - request: TestRequestOptions { - error_unsolicited: false, - protect: CredProtect::UserVerificationRequired( - false, - ExtensionInfo::RequireEnforceValue, - ), - prf_uv: PrfUvOptions::None(false), - props: None, - pin: None, - }, - response: TestResponseOptions { - user_verified: true, - hmac: HmacSecret::None, - cred_protect: CredentialProtectionPolicy::UserVerificationRequired, - prf: None, - min_pin: None, - cred_props: None, - }, - }; - validate(opts)?; - opts.response.cred_protect = - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidCredProtectValue(CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList)))))); - opts.request.protect = - CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireEnforceValue); - opts.response.user_verified = false; - opts.response.cred_protect = CredentialProtectionPolicy::UserVerificationRequired; - assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::CredProtectUserVerificationRequiredWithoutUserVerified))))); - Ok(()) - } -} diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; use super::{ super::{ super::response::ser::{Null, Type}, @@ -2975,852 +2977,3 @@ where ) } } -#[cfg(test)] -mod test { - use super::{ - AuthenticatorAttachment, AuthenticatorSelectionCriteria, ClientCredentialCreationOptions, - CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, - CredentialMediationRequirement, ExtensionInfo, ExtensionOwned, ExtensionReq, FIVE_MINUTES, - FourToSixtyThree, NonZeroU32, PublicKeyCredentialCreationOptionsOwned, - PublicKeyCredentialUserEntityOwned, ResidentKeyRequirement, UserVerificationRequirement, - }; - use serde_json::Error; - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[expect( - clippy::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[test] - fn client_options() -> Result<(), Error> { - let mut err = - serde_json::from_str::<ClientCredentialCreationOptions<16>>(r#"{"bob":true}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..56), - Some("unknown field `bob`, expected `mediation` or `publicKey`") - ); - err = serde_json::from_str::<ClientCredentialCreationOptions<1>>( - r#"{"mediation":"required","mediation":"required"}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..27), - Some("duplicate field `mediation`") - ); - let mut options = serde_json::from_str::<ClientCredentialCreationOptions<1>>("{}")?; - assert!(matches!( - options.mediation, - CredentialMediationRequirement::Required - )); - assert!(options.public_key.rp_id.is_none()); - assert!(options.public_key.user.name.is_none()); - assert!(options.public_key.user.id.is_none()); - assert!(options.public_key.user.display_name.is_none()); - assert_eq!( - options.public_key.pub_key_cred_params.0, - CoseAlgorithmIdentifiers::ALL.0 - ); - assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert_eq!( - options - .public_key - .authenticator_selection - .authenticator_attachment, - AuthenticatorAttachment::None, - ); - assert!(matches!( - options.public_key.authenticator_selection.resident_key, - ResidentKeyRequirement::Discouraged - )); - assert!(matches!( - options.public_key.authenticator_selection.user_verification, - UserVerificationRequirement::Preferred - )); - assert!(options.public_key.extensions.cred_props.is_none()); - assert!(matches!( - options.public_key.extensions.cred_protect, - CredProtect::None - )); - assert!(options.public_key.extensions.min_pin_length.is_none()); - assert!(options.public_key.extensions.prf.is_none()); - options = serde_json::from_str::<ClientCredentialCreationOptions<1>>( - r#"{"mediation":null,"publicKey":null}"#, - )?; - assert!(matches!( - options.mediation, - CredentialMediationRequirement::Required - )); - assert!(options.public_key.rp_id.is_none()); - assert!(options.public_key.user.name.is_none()); - assert!(options.public_key.user.id.is_none()); - assert!(options.public_key.user.display_name.is_none()); - assert_eq!( - options.public_key.pub_key_cred_params.0, - CoseAlgorithmIdentifiers::ALL.0 - ); - assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert_eq!( - options - .public_key - .authenticator_selection - .authenticator_attachment, - AuthenticatorAttachment::None, - ); - assert!(matches!( - options.public_key.authenticator_selection.resident_key, - ResidentKeyRequirement::Discouraged - )); - assert!(matches!( - options.public_key.authenticator_selection.user_verification, - UserVerificationRequirement::Preferred - )); - assert!(options.public_key.extensions.cred_props.is_none()); - assert!(matches!( - options.public_key.extensions.cred_protect, - CredProtect::None - )); - assert!(options.public_key.extensions.min_pin_length.is_none()); - assert!(options.public_key.extensions.prf.is_none()); - options = - serde_json::from_str::<ClientCredentialCreationOptions<1>>(r#"{"publicKey":{}}"#)?; - assert!(options.public_key.rp_id.is_none()); - assert!(options.public_key.user.name.is_none()); - assert!(options.public_key.user.id.is_none()); - assert!(options.public_key.user.display_name.is_none()); - assert_eq!( - options.public_key.pub_key_cred_params.0, - CoseAlgorithmIdentifiers::ALL.0 - ); - assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert_eq!( - options - .public_key - .authenticator_selection - .authenticator_attachment, - AuthenticatorAttachment::None, - ); - assert!(matches!( - options.public_key.authenticator_selection.resident_key, - ResidentKeyRequirement::Discouraged - )); - assert!(matches!( - options.public_key.authenticator_selection.user_verification, - UserVerificationRequirement::Preferred - )); - assert!(options.public_key.extensions.cred_props.is_none()); - assert!(matches!( - options.public_key.extensions.cred_protect, - CredProtect::None - )); - assert!(options.public_key.extensions.min_pin_length.is_none()); - assert!(options.public_key.extensions.prf.is_none()); - options = serde_json::from_str::<ClientCredentialCreationOptions<1>>( - r#"{"mediation":"conditional","publicKey":{"rp":{"name":"Example.com","id":"example.com"},"user":{"name":"bob","displayName":"Bob","id":"AQ"},"timeout":300000,"excludeCredentials":[],"attestation":"none","attestationFormats":["none"],"authenticatorSelection":{"authenticatorAttachment":"cross-platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"},"extensions":{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":""}}},"pubKeyCredParams":[{"type":"public-key","alg":-8}],"hints":["security-key"],"challenge":null}}"#, - )?; - assert!(matches!( - options.mediation, - CredentialMediationRequirement::Conditional - )); - assert!( - options - .public_key - .rp_id - .is_some_and(|val| val.as_ref() == "example.com") - ); - assert!(options.public_key.user.name.is_some_and(|val| val == "bob")); - assert!( - options - .public_key - .user - .display_name - .is_some_and(|val| val == "Bob") - ); - assert!( - options - .public_key - .user - .id - .is_some_and(|val| val.as_ref() == [1; 1]) - ); - 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) - .0 - ); - assert_eq!(options.public_key.timeout, FIVE_MINUTES); - assert_eq!( - options - .public_key - .authenticator_selection - .authenticator_attachment, - AuthenticatorAttachment::CrossPlatform, - ); - assert!(matches!( - options.public_key.authenticator_selection.resident_key, - ResidentKeyRequirement::Required - )); - assert!(matches!( - options.public_key.authenticator_selection.user_verification, - UserVerificationRequirement::Required - )); - assert!( - options - .public_key - .extensions - .cred_props - .is_some_and(|req| matches!(req, ExtensionReq::Allow)) - ); - assert!( - matches!(options.public_key.extensions.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) - ); - assert!( - options - .public_key - .extensions - .min_pin_length - .is_some_and(|min| min.0 == FourToSixtyThree::Four - && matches!(min.1, ExtensionInfo::AllowEnforceValue)) - ); - assert!( - options - .public_key - .extensions - .prf - .is_some_and(|prf| prf.first.is_empty() - && prf.second.is_some_and(|p| p.is_empty()) - && matches!(prf.ext_req, ExtensionReq::Allow)) - ); - Ok(()) - } - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[expect( - clippy::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[test] - fn key_options() -> Result<(), Error> { - let mut err = - serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<16>>(r#"{"bob":true}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..201), - Some( - "unknown field `bob`, expected one of `rp`, `user`, `challenge`, `pubKeyCredParams`, `timeout`, `excludeCredentials`, `authenticatorSelection`, `hints`, `extensions`, `attestation`, `attestationFormats`" - ) - ); - err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"attestation":"none","attestation":"none"}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..29), - Some("duplicate field `attestation`") - ); - err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"challenge":"AAAAAAAAAAAAAAAAAAAAAA"}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..41), - Some("invalid type: Option value, expected null") - ); - err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"excludeCredentials":[{"type":"public-key","transports":["usb"],"id":"AAAAAAAAAAAAAAAAAAAAAA"}]}"#, - ) - .unwrap_err(); - assert_eq!(err.to_string().get(..19), Some("trailing characters")); - err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"attestation":"foo"}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..27), - Some("invalid value: string \"foo\"") - ); - err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"attestationFormats":["none","none"]}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..96), - Some( - "attestationFormats must be an empty sequence or contain exactly one string whose value is 'none'" - ) - ); - err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"attestationFormats":["foo"]}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..42), - Some("invalid value: string \"foo\", expected none") - ); - err = - serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>(r#"{"timeout":0}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..50), - Some("invalid value: integer `0`, expected a nonzero u32") - ); - err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"timeout":4294967296}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..59), - Some("invalid value: integer `4294967296`, expected a nonzero u32") - ); - let mut key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>("{}")?; - assert!(key.rp_id.is_none()); - assert!(key.user.name.is_none()); - assert!(key.user.id.is_none()); - assert!(key.user.display_name.is_none()); - assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); - assert_eq!(key.timeout, FIVE_MINUTES); - assert_eq!( - key.authenticator_selection.authenticator_attachment, - AuthenticatorAttachment::None, - ); - assert!(matches!( - key.authenticator_selection.resident_key, - ResidentKeyRequirement::Discouraged - )); - assert!(matches!( - key.authenticator_selection.user_verification, - UserVerificationRequirement::Preferred - )); - assert!(key.extensions.cred_props.is_none()); - assert!(matches!(key.extensions.cred_protect, CredProtect::None)); - assert!(key.extensions.min_pin_length.is_none()); - assert!(key.extensions.prf.is_none()); - key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"rp":null,"user":null,"timeout":null,"excludeCredentials":null,"attestation":null,"attestationFormats":null,"authenticatorSelection":null,"extensions":null,"pubKeyCredParams":null,"hints":null,"challenge":null}"#, - )?; - assert!(key.rp_id.is_none()); - assert!(key.user.name.is_none()); - assert!(key.user.id.is_none()); - assert!(key.user.display_name.is_none()); - assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); - assert_eq!(key.timeout, FIVE_MINUTES); - assert_eq!( - key.authenticator_selection.authenticator_attachment, - AuthenticatorAttachment::None, - ); - assert!(matches!( - key.authenticator_selection.resident_key, - ResidentKeyRequirement::Discouraged - )); - assert!(matches!( - key.authenticator_selection.user_verification, - UserVerificationRequirement::Preferred - )); - assert!(key.extensions.cred_props.is_none()); - assert!(matches!(key.extensions.cred_protect, CredProtect::None)); - assert!(key.extensions.min_pin_length.is_none()); - assert!(key.extensions.prf.is_none()); - key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"rp":{},"user":{},"excludeCredentials":[],"attestationFormats":[],"authenticatorSelection":{},"extensions":{},"pubKeyCredParams":[],"hints":[]}"#, - )?; - assert!(key.rp_id.is_none()); - assert!(key.user.name.is_none()); - assert!(key.user.id.is_none()); - assert!(key.user.display_name.is_none()); - assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); - assert_eq!( - key.authenticator_selection.authenticator_attachment, - AuthenticatorAttachment::None, - ); - assert!(matches!( - key.authenticator_selection.resident_key, - ResidentKeyRequirement::Discouraged - )); - assert!(matches!( - key.authenticator_selection.user_verification, - UserVerificationRequirement::Preferred - )); - assert!(key.extensions.cred_props.is_none()); - assert!(matches!(key.extensions.cred_protect, CredProtect::None)); - assert!(key.extensions.min_pin_length.is_none()); - assert!(key.extensions.prf.is_none()); - key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"rp":{"name":null,"id":null},"user":{"name":null,"id":null,"displayName":null},"authenticatorSelection":{"residentKey":null,"requireResidentKey":null,"userVerification":null,"authenticatorAttachment":null},"extensions":{"credProps":null,"credentialProtectionPolicy":null,"enforceCredentialProtectionPolicy":null,"minPinLength":null,"prf":null}}"#, - )?; - assert!(key.rp_id.is_none()); - assert!(key.user.name.is_none()); - assert!(key.user.id.is_none()); - assert!(key.user.display_name.is_none()); - assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); - assert_eq!( - key.authenticator_selection.authenticator_attachment, - AuthenticatorAttachment::None, - ); - assert!(matches!( - key.authenticator_selection.resident_key, - ResidentKeyRequirement::Discouraged - )); - assert!(matches!( - key.authenticator_selection.user_verification, - UserVerificationRequirement::Preferred - )); - assert!(key.extensions.cred_props.is_none()); - assert!(matches!(key.extensions.cred_protect, CredProtect::None)); - assert!(key.extensions.min_pin_length.is_none()); - assert!(key.extensions.prf.is_none()); - key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"rp":{"name":"Example.com","id":"example.com"},"user":{"name":"bob","displayName":"Bob","id":"AQ"},"timeout":300000,"excludeCredentials":[],"attestation":"none","attestationFormats":["none"],"authenticatorSelection":{"authenticatorAttachment":"cross-platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"},"extensions":{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":""}}},"pubKeyCredParams":[{"type":"public-key","alg":-8}],"hints":["security-key"],"challenge":null}"#, - )?; - assert!(key.rp_id.is_some_and(|val| val.as_ref() == "example.com")); - assert!(key.user.name.is_some_and(|val| val == "bob")); - assert!(key.user.display_name.is_some_and(|val| val == "Bob")); - assert!(key.user.id.is_some_and(|val| val.as_ref() == [1; 1])); - 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) - .0 - ); - assert_eq!(key.timeout, FIVE_MINUTES); - assert_eq!( - key.authenticator_selection.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform, - ); - assert!(matches!( - key.authenticator_selection.resident_key, - ResidentKeyRequirement::Required - )); - assert!(matches!( - key.authenticator_selection.user_verification, - UserVerificationRequirement::Required - )); - assert!( - key.extensions - .cred_props - .is_some_and(|req| matches!(req, ExtensionReq::Allow)) - ); - assert!( - matches!(key.extensions.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) - ); - assert!( - key.extensions - .min_pin_length - .is_some_and(|min| min.0 == FourToSixtyThree::Four - && matches!(min.1, ExtensionInfo::AllowEnforceValue)) - ); - assert!(key.extensions.prf.is_some_and(|prf| prf.first.is_empty() - && prf.second.is_some_and(|p| p.is_empty()) - && matches!(prf.ext_req, ExtensionReq::Allow))); - key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( - r#"{"timeout":4294967295}"#, - )?; - assert_eq!(key.timeout, NonZeroU32::MAX); - Ok(()) - } - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[expect(clippy::cognitive_complexity, reason = "a lot to test")] - #[test] - fn extension() -> Result<(), Error> { - let mut err = serde_json::from_str::<ExtensionOwned>(r#"{"bob":true}"#).unwrap_err(); - assert_eq!( - err.to_string().get(..138), - Some( - "unknown field `bob`, expected one of `credProps`, `credentialProtectionPolicy`, `enforceCredentialProtectionPolicy`, `minPinLength`, `prf`" - ) - ); - err = serde_json::from_str::<ExtensionOwned>(r#"{"credProps":true,"credProps":true}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..27), - Some("duplicate field `credProps`") - ); - err = - serde_json::from_str::<ExtensionOwned>(r#"{"enforceCredentialProtectionPolicy":null}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..84), - Some( - "'enforceCredentialProtectionPolicy' must not exist when 'credentialProtectionPolicy'" - ) - ); - err = serde_json::from_str::<ExtensionOwned>( - r#"{"enforceCredentialProtectionPolicy":false,"credentialProtectionPolicy":null}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..103), - Some( - "'enforceCredentialProtectionPolicy' must be null or not exist when 'credentialProtectionPolicy' is null" - ) - ); - let mut ext = serde_json::from_str::<ExtensionOwned>( - r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":""}}}"#, - )?; - assert!( - ext.cred_props - .is_some_and(|props| matches!(props, ExtensionReq::Allow)) - ); - assert!( - matches!(ext.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) - ); - assert!( - ext.min_pin_length - .is_some_and(|min| min.0 == FourToSixtyThree::Four - && matches!(min.1, ExtensionInfo::AllowEnforceValue)) - ); - assert!(ext.prf.is_some_and(|prf| prf.first.is_empty() - && prf.second.is_some_and(|v| v.is_empty()) - && matches!(prf.ext_req, ExtensionReq::Allow))); - ext = serde_json::from_str::<ExtensionOwned>( - r#"{"credProps":null,"credentialProtectionPolicy":null,"enforceCredentialProtectionPolicy":null,"minPinLength":null,"prf":null}"#, - )?; - assert!(ext.cred_props.is_none()); - assert!(matches!(ext.cred_protect, CredProtect::None)); - assert!(ext.min_pin_length.is_none()); - assert!(ext.prf.is_none()); - ext = serde_json::from_str::<ExtensionOwned>("{}")?; - assert!(ext.cred_props.is_none()); - assert!(matches!(ext.cred_protect, CredProtect::None)); - assert!(ext.min_pin_length.is_none()); - assert!(ext.prf.is_none()); - ext = serde_json::from_str::<ExtensionOwned>(r#"{"credentialProtectionPolicy":null}"#)?; - assert!(matches!(ext.cred_protect, CredProtect::None)); - ext = serde_json::from_str::<ExtensionOwned>( - r#"{"credentialProtectionPolicy":"userVerificationOptional"}"#, - )?; - assert!( - matches!(ext.cred_protect, CredProtect::UserVerificationOptional(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) - ); - ext = serde_json::from_str::<ExtensionOwned>( - r#"{"credentialProtectionPolicy":"userVerificationOptionalWithCredentialIDList","enforceCredentialProtectionPolicy":null}"#, - )?; - assert!( - matches!(ext.cred_protect, CredProtect::UserVerificationOptionalWithCredentialIdList(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) - ); - Ok(()) - } - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[test] - fn user_entity() -> Result<(), Error> { - let mut err = - serde_json::from_str::<PublicKeyCredentialUserEntityOwned<16>>(r#"{"bob":true}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..64), - Some("unknown field `bob`, expected one of `id`, `name`, `displayName`") - ); - err = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<1>>( - r#"{"name":"bob","name":"bob"}"#, - ) - .unwrap_err(); - assert_eq!(err.to_string().get(..22), Some("duplicate field `name`")); - let mut user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<1>>( - r#"{"id":"AQ","name":"bob","displayName":"Bob"}"#, - )?; - assert!( - user.id - .is_some_and(|val| val.as_slice() == [1; 1].as_slice()) - ); - assert!(user.name.is_some_and(|val| val == "bob")); - assert!(user.display_name.is_some_and(|val| val == "Bob")); - user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<1>>( - r#"{"id":null,"name":null,"displayName":null}"#, - )?; - assert!(user.name.is_none()); - assert!(user.display_name.is_none()); - assert!(user.id.is_none()); - user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<1>>("{}")?; - assert!(user.name.is_none()); - assert!(user.display_name.is_none()); - assert!(user.id.is_none()); - Ok(()) - } - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[expect( - clippy::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[test] - fn auth_crit() -> Result<(), Error> { - let mut err = serde_json::from_str::<AuthenticatorSelectionCriteria>("null").unwrap_err(); - assert_eq!( - err.to_string().get(..59), - Some("invalid type: null, expected AuthenticatorSelectionCriteria") - ); - err = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":"required","requireResidentKey":false}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..62), - Some("'residentKey' is 'required', but 'requireResidentKey' is false") - ); - err = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":"preferred","requireResidentKey":true}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..65), - Some("'residentKey' is not 'required', but 'requireResidentKey' is true") - ); - err = - serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"residentKey":"prefered"}"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..84), - Some( - "invalid value: string \"prefered\", expected 'required', 'discouraged', or 'preferred'" - ) - ); - err = - serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"bob":true}"#).unwrap_err(); - assert_eq!( - err.to_string().get(..119), - Some( - "unknown field `bob`, expected one of `authenticatorAttachment`, `residentKey`, `requireResidentKey`, `userVerification`" - ) - ); - err = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"requireResidentKey":true,"requireResidentKey":true}"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..36), - Some("duplicate field `requireResidentKey`") - ); - let mut crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"authenticatorAttachment":"platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"}"#, - )?; - assert_eq!( - crit.authenticator_attachment, - AuthenticatorAttachment::Platform, - ); - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Required - )); - assert!(matches!( - crit.user_verification, - UserVerificationRequirement::Required - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"authenticatorAttachment":null,"residentKey":null,"requireResidentKey":null,"userVerification":null}"#, - )?; - assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None,); - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Discouraged - )); - assert!(matches!( - crit.user_verification, - UserVerificationRequirement::Preferred - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>("{}")?; - assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None,); - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Discouraged - )); - assert!(matches!( - crit.user_verification, - UserVerificationRequirement::Preferred - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":"preferred","requireResidentKey":false}"#, - )?; - assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None,); - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Preferred - )); - assert!(matches!( - crit.user_verification, - UserVerificationRequirement::Preferred - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":"preferred"}"#, - )?; - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Preferred - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"requireResidentKey":true}"#, - )?; - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Required - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"requireResidentKey":false}"#, - )?; - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Discouraged - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":"required"}"#, - )?; - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Required - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":"discouraged"}"#, - )?; - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Discouraged - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":"discouraged","requireResidentKey":null}"#, - )?; - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Discouraged - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":"required","requireResidentKey":null}"#, - )?; - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Required - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":null,"requireResidentKey":true}"#, - )?; - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Required - )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( - r#"{"residentKey":null,"requireResidentKey":false}"#, - )?; - assert!(matches!( - crit.resident_key, - ResidentKeyRequirement::Discouraged - )); - Ok(()) - } - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[test] - fn cose_algs() -> Result<(), Error> { - let mut err = serde_json::from_str::<CoseAlgorithmIdentifiers>("null").unwrap_err(); - assert_eq!( - err.to_string().get(..53), - Some("invalid type: null, expected CoseAlgorithmIdentifiers") - ); - err = serde_json::from_str::<CoseAlgorithmIdentifiers>("[null]").unwrap_err(); - assert_eq!( - err.to_string().get(..37), - Some("invalid type: null, expected PubParam") - ); - err = serde_json::from_str::<CoseAlgorithmIdentifiers>("[{}]").unwrap_err(); - assert_eq!(err.to_string().get(..19), Some("missing field `alg`")); - err = serde_json::from_str::<CoseAlgorithmIdentifiers>( - r#"[{"type":"public-key","alg":-7,"foo":true}]"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..45), - Some("unknown field `foo`, expected `type` or `alg`") - ); - err = serde_json::from_str::<CoseAlgorithmIdentifiers>( - r#"[{"type":"public-key","alg":-7,"alg":-7}]"#, - ) - .unwrap_err(); - assert_eq!(err.to_string().get(..21), Some("duplicate field `alg`")); - err = serde_json::from_str::<CoseAlgorithmIdentifiers>( - r#"[{"type":"public-key","alg":null}]"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..52), - Some("invalid type: null, expected CoseAlgorithmIdentifier") - ); - err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":null,"alg":-8}]"#) - .unwrap_err(); - assert_eq!( - err.to_string().get(..39), - Some("invalid type: null, expected public-key") - ); - err = - serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":"public-key","alg":-6}]"#) - .unwrap_err(); - assert_eq!( - 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}]"#, - ) - .unwrap_err(); - assert_eq!( - err.to_string().get(..49), - Some("pubKeyCredParams contained duplicate Es256 values") - ); - err = serde_json::from_str::<CoseAlgorithmIdentifiers>( - r#"[{"type":"public-key","alg":-7},{"type":"public-key","alg":-8}]"#, - ) - .unwrap_err(); - assert_eq!( - 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)); - assert!(alg.contains(CoseAlgorithmIdentifier::Rs256)); - Ok(()) - } -} diff --git a/src/request/register/ser/tests.rs b/src/request/register/ser/tests.rs @@ -0,0 +1,830 @@ +use super::{ + AuthenticatorAttachment, AuthenticatorSelectionCriteria, ClientCredentialCreationOptions, + CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, CredentialMediationRequirement, + ExtensionInfo, ExtensionOwned, ExtensionReq, FIVE_MINUTES, FourToSixtyThree, NonZeroU32, + PublicKeyCredentialCreationOptionsOwned, PublicKeyCredentialUserEntityOwned, + ResidentKeyRequirement, UserVerificationRequirement, +}; +use serde_json::Error; +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[test] +fn client_options() -> Result<(), Error> { + let mut err = + serde_json::from_str::<ClientCredentialCreationOptions<16>>(r#"{"bob":true}"#).unwrap_err(); + assert_eq!( + err.to_string().get(..56), + Some("unknown field `bob`, expected `mediation` or `publicKey`") + ); + err = serde_json::from_str::<ClientCredentialCreationOptions<1>>( + r#"{"mediation":"required","mediation":"required"}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..27), + Some("duplicate field `mediation`") + ); + let mut options = serde_json::from_str::<ClientCredentialCreationOptions<1>>("{}")?; + assert!(matches!( + options.mediation, + CredentialMediationRequirement::Required + )); + assert!(options.public_key.rp_id.is_none()); + assert!(options.public_key.user.name.is_none()); + assert!(options.public_key.user.id.is_none()); + assert!(options.public_key.user.display_name.is_none()); + assert_eq!( + options.public_key.pub_key_cred_params.0, + CoseAlgorithmIdentifiers::ALL.0 + ); + assert_eq!(options.public_key.timeout, FIVE_MINUTES); + assert_eq!( + options + .public_key + .authenticator_selection + .authenticator_attachment, + AuthenticatorAttachment::None, + ); + assert!(matches!( + options.public_key.authenticator_selection.resident_key, + ResidentKeyRequirement::Discouraged + )); + assert!(matches!( + options.public_key.authenticator_selection.user_verification, + UserVerificationRequirement::Preferred + )); + assert!(options.public_key.extensions.cred_props.is_none()); + assert!(matches!( + options.public_key.extensions.cred_protect, + CredProtect::None + )); + assert!(options.public_key.extensions.min_pin_length.is_none()); + assert!(options.public_key.extensions.prf.is_none()); + options = serde_json::from_str::<ClientCredentialCreationOptions<1>>( + r#"{"mediation":null,"publicKey":null}"#, + )?; + assert!(matches!( + options.mediation, + CredentialMediationRequirement::Required + )); + assert!(options.public_key.rp_id.is_none()); + assert!(options.public_key.user.name.is_none()); + assert!(options.public_key.user.id.is_none()); + assert!(options.public_key.user.display_name.is_none()); + assert_eq!( + options.public_key.pub_key_cred_params.0, + CoseAlgorithmIdentifiers::ALL.0 + ); + assert_eq!(options.public_key.timeout, FIVE_MINUTES); + assert_eq!( + options + .public_key + .authenticator_selection + .authenticator_attachment, + AuthenticatorAttachment::None, + ); + assert!(matches!( + options.public_key.authenticator_selection.resident_key, + ResidentKeyRequirement::Discouraged + )); + assert!(matches!( + options.public_key.authenticator_selection.user_verification, + UserVerificationRequirement::Preferred + )); + assert!(options.public_key.extensions.cred_props.is_none()); + assert!(matches!( + options.public_key.extensions.cred_protect, + CredProtect::None + )); + assert!(options.public_key.extensions.min_pin_length.is_none()); + assert!(options.public_key.extensions.prf.is_none()); + options = serde_json::from_str::<ClientCredentialCreationOptions<1>>(r#"{"publicKey":{}}"#)?; + assert!(options.public_key.rp_id.is_none()); + assert!(options.public_key.user.name.is_none()); + assert!(options.public_key.user.id.is_none()); + assert!(options.public_key.user.display_name.is_none()); + assert_eq!( + options.public_key.pub_key_cred_params.0, + CoseAlgorithmIdentifiers::ALL.0 + ); + assert_eq!(options.public_key.timeout, FIVE_MINUTES); + assert_eq!( + options + .public_key + .authenticator_selection + .authenticator_attachment, + AuthenticatorAttachment::None, + ); + assert!(matches!( + options.public_key.authenticator_selection.resident_key, + ResidentKeyRequirement::Discouraged + )); + assert!(matches!( + options.public_key.authenticator_selection.user_verification, + UserVerificationRequirement::Preferred + )); + assert!(options.public_key.extensions.cred_props.is_none()); + assert!(matches!( + options.public_key.extensions.cred_protect, + CredProtect::None + )); + assert!(options.public_key.extensions.min_pin_length.is_none()); + assert!(options.public_key.extensions.prf.is_none()); + options = serde_json::from_str::<ClientCredentialCreationOptions<1>>( + r#"{"mediation":"conditional","publicKey":{"rp":{"name":"Example.com","id":"example.com"},"user":{"name":"bob","displayName":"Bob","id":"AQ"},"timeout":300000,"excludeCredentials":[],"attestation":"none","attestationFormats":["none"],"authenticatorSelection":{"authenticatorAttachment":"cross-platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"},"extensions":{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":""}}},"pubKeyCredParams":[{"type":"public-key","alg":-8}],"hints":["security-key"],"challenge":null}}"#, + )?; + assert!(matches!( + options.mediation, + CredentialMediationRequirement::Conditional + )); + assert!( + options + .public_key + .rp_id + .is_some_and(|val| val.as_ref() == "example.com") + ); + assert!(options.public_key.user.name.is_some_and(|val| val == "bob")); + assert!( + options + .public_key + .user + .display_name + .is_some_and(|val| val == "Bob") + ); + assert!( + options + .public_key + .user + .id + .is_some_and(|val| val.as_ref() == [1; 1]) + ); + 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) + .0 + ); + assert_eq!(options.public_key.timeout, FIVE_MINUTES); + assert_eq!( + options + .public_key + .authenticator_selection + .authenticator_attachment, + AuthenticatorAttachment::CrossPlatform, + ); + assert!(matches!( + options.public_key.authenticator_selection.resident_key, + ResidentKeyRequirement::Required + )); + assert!(matches!( + options.public_key.authenticator_selection.user_verification, + UserVerificationRequirement::Required + )); + assert!( + options + .public_key + .extensions + .cred_props + .is_some_and(|req| matches!(req, ExtensionReq::Allow)) + ); + assert!( + matches!(options.public_key.extensions.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) + ); + assert!( + options + .public_key + .extensions + .min_pin_length + .is_some_and(|min| min.0 == FourToSixtyThree::Four + && matches!(min.1, ExtensionInfo::AllowEnforceValue)) + ); + assert!( + options + .public_key + .extensions + .prf + .is_some_and(|prf| prf.first.is_empty() + && prf.second.is_some_and(|p| p.is_empty()) + && matches!(prf.ext_req, ExtensionReq::Allow)) + ); + Ok(()) +} +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[test] +fn key_options() -> Result<(), Error> { + let mut err = + serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<16>>(r#"{"bob":true}"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..201), + Some( + "unknown field `bob`, expected one of `rp`, `user`, `challenge`, `pubKeyCredParams`, `timeout`, `excludeCredentials`, `authenticatorSelection`, `hints`, `extensions`, `attestation`, `attestationFormats`" + ) + ); + err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"attestation":"none","attestation":"none"}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..29), + Some("duplicate field `attestation`") + ); + err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"challenge":"AAAAAAAAAAAAAAAAAAAAAA"}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..41), + Some("invalid type: Option value, expected null") + ); + err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"excludeCredentials":[{"type":"public-key","transports":["usb"],"id":"AAAAAAAAAAAAAAAAAAAAAA"}]}"#, + ) + .unwrap_err(); + assert_eq!(err.to_string().get(..19), Some("trailing characters")); + err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"attestation":"foo"}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..27), + Some("invalid value: string \"foo\"") + ); + err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"attestationFormats":["none","none"]}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..96), + Some( + "attestationFormats must be an empty sequence or contain exactly one string whose value is 'none'" + ) + ); + err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"attestationFormats":["foo"]}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..42), + Some("invalid value: string \"foo\", expected none") + ); + err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>(r#"{"timeout":0}"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..50), + Some("invalid value: integer `0`, expected a nonzero u32") + ); + err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"timeout":4294967296}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..59), + Some("invalid value: integer `4294967296`, expected a nonzero u32") + ); + let mut key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>("{}")?; + assert!(key.rp_id.is_none()); + assert!(key.user.name.is_none()); + assert!(key.user.id.is_none()); + assert!(key.user.display_name.is_none()); + assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); + assert_eq!(key.timeout, FIVE_MINUTES); + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::None, + ); + assert!(matches!( + key.authenticator_selection.resident_key, + ResidentKeyRequirement::Discouraged + )); + assert!(matches!( + key.authenticator_selection.user_verification, + UserVerificationRequirement::Preferred + )); + assert!(key.extensions.cred_props.is_none()); + assert!(matches!(key.extensions.cred_protect, CredProtect::None)); + assert!(key.extensions.min_pin_length.is_none()); + assert!(key.extensions.prf.is_none()); + key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"rp":null,"user":null,"timeout":null,"excludeCredentials":null,"attestation":null,"attestationFormats":null,"authenticatorSelection":null,"extensions":null,"pubKeyCredParams":null,"hints":null,"challenge":null}"#, + )?; + assert!(key.rp_id.is_none()); + assert!(key.user.name.is_none()); + assert!(key.user.id.is_none()); + assert!(key.user.display_name.is_none()); + assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); + assert_eq!(key.timeout, FIVE_MINUTES); + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::None, + ); + assert!(matches!( + key.authenticator_selection.resident_key, + ResidentKeyRequirement::Discouraged + )); + assert!(matches!( + key.authenticator_selection.user_verification, + UserVerificationRequirement::Preferred + )); + assert!(key.extensions.cred_props.is_none()); + assert!(matches!(key.extensions.cred_protect, CredProtect::None)); + assert!(key.extensions.min_pin_length.is_none()); + assert!(key.extensions.prf.is_none()); + key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"rp":{},"user":{},"excludeCredentials":[],"attestationFormats":[],"authenticatorSelection":{},"extensions":{},"pubKeyCredParams":[],"hints":[]}"#, + )?; + assert!(key.rp_id.is_none()); + assert!(key.user.name.is_none()); + assert!(key.user.id.is_none()); + assert!(key.user.display_name.is_none()); + assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::None, + ); + assert!(matches!( + key.authenticator_selection.resident_key, + ResidentKeyRequirement::Discouraged + )); + assert!(matches!( + key.authenticator_selection.user_verification, + UserVerificationRequirement::Preferred + )); + assert!(key.extensions.cred_props.is_none()); + assert!(matches!(key.extensions.cred_protect, CredProtect::None)); + assert!(key.extensions.min_pin_length.is_none()); + assert!(key.extensions.prf.is_none()); + key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"rp":{"name":null,"id":null},"user":{"name":null,"id":null,"displayName":null},"authenticatorSelection":{"residentKey":null,"requireResidentKey":null,"userVerification":null,"authenticatorAttachment":null},"extensions":{"credProps":null,"credentialProtectionPolicy":null,"enforceCredentialProtectionPolicy":null,"minPinLength":null,"prf":null}}"#, + )?; + assert!(key.rp_id.is_none()); + assert!(key.user.name.is_none()); + assert!(key.user.id.is_none()); + assert!(key.user.display_name.is_none()); + assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0); + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::None, + ); + assert!(matches!( + key.authenticator_selection.resident_key, + ResidentKeyRequirement::Discouraged + )); + assert!(matches!( + key.authenticator_selection.user_verification, + UserVerificationRequirement::Preferred + )); + assert!(key.extensions.cred_props.is_none()); + assert!(matches!(key.extensions.cred_protect, CredProtect::None)); + assert!(key.extensions.min_pin_length.is_none()); + assert!(key.extensions.prf.is_none()); + key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"rp":{"name":"Example.com","id":"example.com"},"user":{"name":"bob","displayName":"Bob","id":"AQ"},"timeout":300000,"excludeCredentials":[],"attestation":"none","attestationFormats":["none"],"authenticatorSelection":{"authenticatorAttachment":"cross-platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"},"extensions":{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":""}}},"pubKeyCredParams":[{"type":"public-key","alg":-8}],"hints":["security-key"],"challenge":null}"#, + )?; + assert!(key.rp_id.is_some_and(|val| val.as_ref() == "example.com")); + assert!(key.user.name.is_some_and(|val| val == "bob")); + assert!(key.user.display_name.is_some_and(|val| val == "Bob")); + assert!(key.user.id.is_some_and(|val| val.as_ref() == [1; 1])); + 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) + .0 + ); + assert_eq!(key.timeout, FIVE_MINUTES); + assert_eq!( + key.authenticator_selection.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform, + ); + assert!(matches!( + key.authenticator_selection.resident_key, + ResidentKeyRequirement::Required + )); + assert!(matches!( + key.authenticator_selection.user_verification, + UserVerificationRequirement::Required + )); + assert!( + key.extensions + .cred_props + .is_some_and(|req| matches!(req, ExtensionReq::Allow)) + ); + assert!( + matches!(key.extensions.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) + ); + assert!( + key.extensions + .min_pin_length + .is_some_and(|min| min.0 == FourToSixtyThree::Four + && matches!(min.1, ExtensionInfo::AllowEnforceValue)) + ); + assert!(key.extensions.prf.is_some_and(|prf| prf.first.is_empty() + && prf.second.is_some_and(|p| p.is_empty()) + && matches!(prf.ext_req, ExtensionReq::Allow))); + key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<1>>( + r#"{"timeout":4294967295}"#, + )?; + assert_eq!(key.timeout, NonZeroU32::MAX); + Ok(()) +} +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[expect(clippy::cognitive_complexity, reason = "a lot to test")] +#[test] +fn extension() -> Result<(), Error> { + let mut err = serde_json::from_str::<ExtensionOwned>(r#"{"bob":true}"#).unwrap_err(); + assert_eq!( + err.to_string().get(..138), + Some( + "unknown field `bob`, expected one of `credProps`, `credentialProtectionPolicy`, `enforceCredentialProtectionPolicy`, `minPinLength`, `prf`" + ) + ); + err = serde_json::from_str::<ExtensionOwned>(r#"{"credProps":true,"credProps":true}"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..27), + Some("duplicate field `credProps`") + ); + err = serde_json::from_str::<ExtensionOwned>(r#"{"enforceCredentialProtectionPolicy":null}"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..84), + Some( + "'enforceCredentialProtectionPolicy' must not exist when 'credentialProtectionPolicy'" + ) + ); + err = serde_json::from_str::<ExtensionOwned>( + r#"{"enforceCredentialProtectionPolicy":false,"credentialProtectionPolicy":null}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..103), + Some( + "'enforceCredentialProtectionPolicy' must be null or not exist when 'credentialProtectionPolicy' is null" + ) + ); + let mut ext = serde_json::from_str::<ExtensionOwned>( + r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":""}}}"#, + )?; + assert!( + ext.cred_props + .is_some_and(|props| matches!(props, ExtensionReq::Allow)) + ); + assert!( + matches!(ext.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) + ); + assert!( + ext.min_pin_length + .is_some_and(|min| min.0 == FourToSixtyThree::Four + && matches!(min.1, ExtensionInfo::AllowEnforceValue)) + ); + assert!(ext.prf.is_some_and(|prf| prf.first.is_empty() + && prf.second.is_some_and(|v| v.is_empty()) + && matches!(prf.ext_req, ExtensionReq::Allow))); + ext = serde_json::from_str::<ExtensionOwned>( + r#"{"credProps":null,"credentialProtectionPolicy":null,"enforceCredentialProtectionPolicy":null,"minPinLength":null,"prf":null}"#, + )?; + assert!(ext.cred_props.is_none()); + assert!(matches!(ext.cred_protect, CredProtect::None)); + assert!(ext.min_pin_length.is_none()); + assert!(ext.prf.is_none()); + ext = serde_json::from_str::<ExtensionOwned>("{}")?; + assert!(ext.cred_props.is_none()); + assert!(matches!(ext.cred_protect, CredProtect::None)); + assert!(ext.min_pin_length.is_none()); + assert!(ext.prf.is_none()); + ext = serde_json::from_str::<ExtensionOwned>(r#"{"credentialProtectionPolicy":null}"#)?; + assert!(matches!(ext.cred_protect, CredProtect::None)); + ext = serde_json::from_str::<ExtensionOwned>( + r#"{"credentialProtectionPolicy":"userVerificationOptional"}"#, + )?; + assert!( + matches!(ext.cred_protect, CredProtect::UserVerificationOptional(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) + ); + ext = serde_json::from_str::<ExtensionOwned>( + r#"{"credentialProtectionPolicy":"userVerificationOptionalWithCredentialIDList","enforceCredentialProtectionPolicy":null}"#, + )?; + assert!( + matches!(ext.cred_protect, CredProtect::UserVerificationOptionalWithCredentialIdList(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) + ); + Ok(()) +} +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[test] +fn user_entity() -> Result<(), Error> { + let mut err = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<16>>(r#"{"bob":true}"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..64), + Some("unknown field `bob`, expected one of `id`, `name`, `displayName`") + ); + err = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<1>>( + r#"{"name":"bob","name":"bob"}"#, + ) + .unwrap_err(); + assert_eq!(err.to_string().get(..22), Some("duplicate field `name`")); + let mut user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<1>>( + r#"{"id":"AQ","name":"bob","displayName":"Bob"}"#, + )?; + assert!( + user.id + .is_some_and(|val| val.as_slice() == [1; 1].as_slice()) + ); + assert!(user.name.is_some_and(|val| val == "bob")); + assert!(user.display_name.is_some_and(|val| val == "Bob")); + user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<1>>( + r#"{"id":null,"name":null,"displayName":null}"#, + )?; + assert!(user.name.is_none()); + assert!(user.display_name.is_none()); + assert!(user.id.is_none()); + user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<1>>("{}")?; + assert!(user.name.is_none()); + assert!(user.display_name.is_none()); + assert!(user.id.is_none()); + Ok(()) +} +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[test] +fn auth_crit() -> Result<(), Error> { + let mut err = serde_json::from_str::<AuthenticatorSelectionCriteria>("null").unwrap_err(); + assert_eq!( + err.to_string().get(..59), + Some("invalid type: null, expected AuthenticatorSelectionCriteria") + ); + err = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"residentKey":"required","requireResidentKey":false}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..62), + Some("'residentKey' is 'required', but 'requireResidentKey' is false") + ); + err = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"residentKey":"preferred","requireResidentKey":true}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..65), + Some("'residentKey' is not 'required', but 'requireResidentKey' is true") + ); + err = serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"residentKey":"prefered"}"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..84), + Some( + "invalid value: string \"prefered\", expected 'required', 'discouraged', or 'preferred'" + ) + ); + err = serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"bob":true}"#).unwrap_err(); + assert_eq!( + err.to_string().get(..119), + Some( + "unknown field `bob`, expected one of `authenticatorAttachment`, `residentKey`, `requireResidentKey`, `userVerification`" + ) + ); + err = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"requireResidentKey":true,"requireResidentKey":true}"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..36), + Some("duplicate field `requireResidentKey`") + ); + let mut crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"authenticatorAttachment":"platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"}"#, + )?; + assert_eq!( + crit.authenticator_attachment, + AuthenticatorAttachment::Platform, + ); + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Required + )); + assert!(matches!( + crit.user_verification, + UserVerificationRequirement::Required + )); + crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"authenticatorAttachment":null,"residentKey":null,"requireResidentKey":null,"userVerification":null}"#, + )?; + assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None,); + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Discouraged + )); + assert!(matches!( + crit.user_verification, + UserVerificationRequirement::Preferred + )); + crit = serde_json::from_str::<AuthenticatorSelectionCriteria>("{}")?; + assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None,); + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Discouraged + )); + assert!(matches!( + crit.user_verification, + UserVerificationRequirement::Preferred + )); + crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"residentKey":"preferred","requireResidentKey":false}"#, + )?; + assert_eq!(crit.authenticator_attachment, AuthenticatorAttachment::None,); + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Preferred + )); + assert!(matches!( + crit.user_verification, + UserVerificationRequirement::Preferred + )); + crit = + serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"residentKey":"preferred"}"#)?; + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Preferred + )); + crit = + serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"requireResidentKey":true}"#)?; + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Required + )); + crit = + serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"requireResidentKey":false}"#)?; + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Discouraged + )); + crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"residentKey":"required"}"#)?; + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Required + )); + crit = + serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"residentKey":"discouraged"}"#)?; + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Discouraged + )); + crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"residentKey":"discouraged","requireResidentKey":null}"#, + )?; + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Discouraged + )); + crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"residentKey":"required","requireResidentKey":null}"#, + )?; + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Required + )); + crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"residentKey":null,"requireResidentKey":true}"#, + )?; + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Required + )); + crit = serde_json::from_str::<AuthenticatorSelectionCriteria>( + r#"{"residentKey":null,"requireResidentKey":false}"#, + )?; + assert!(matches!( + crit.resident_key, + ResidentKeyRequirement::Discouraged + )); + Ok(()) +} +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[test] +fn cose_algs() -> Result<(), Error> { + let mut err = serde_json::from_str::<CoseAlgorithmIdentifiers>("null").unwrap_err(); + assert_eq!( + err.to_string().get(..53), + Some("invalid type: null, expected CoseAlgorithmIdentifiers") + ); + err = serde_json::from_str::<CoseAlgorithmIdentifiers>("[null]").unwrap_err(); + assert_eq!( + err.to_string().get(..37), + Some("invalid type: null, expected PubParam") + ); + err = serde_json::from_str::<CoseAlgorithmIdentifiers>("[{}]").unwrap_err(); + assert_eq!(err.to_string().get(..19), Some("missing field `alg`")); + err = serde_json::from_str::<CoseAlgorithmIdentifiers>( + r#"[{"type":"public-key","alg":-7,"foo":true}]"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..45), + Some("unknown field `foo`, expected `type` or `alg`") + ); + err = serde_json::from_str::<CoseAlgorithmIdentifiers>( + r#"[{"type":"public-key","alg":-7,"alg":-7}]"#, + ) + .unwrap_err(); + assert_eq!(err.to_string().get(..21), Some("duplicate field `alg`")); + err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":"public-key","alg":null}]"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..52), + Some("invalid type: null, expected CoseAlgorithmIdentifier") + ); + err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":null,"alg":-8}]"#) + .unwrap_err(); + assert_eq!( + err.to_string().get(..39), + Some("invalid type: null, expected public-key") + ); + err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":"public-key","alg":-6}]"#) + .unwrap_err(); + assert_eq!( + 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}]"#, + ) + .unwrap_err(); + assert_eq!( + err.to_string().get(..49), + Some("pubKeyCredParams contained duplicate Es256 values") + ); + err = serde_json::from_str::<CoseAlgorithmIdentifiers>( + r#"[{"type":"public-key","alg":-7},{"type":"public-key","alg":-8}]"#, + ) + .unwrap_err(); + assert_eq!( + 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)); + assert!(alg.contains(CoseAlgorithmIdentifier::Rs256)); + Ok(()) +} diff --git a/src/request/register/tests.rs b/src/request/register/tests.rs @@ -0,0 +1,889 @@ +#[cfg(all( + feature = "custom", + any( + feature = "serializable_server_state", + not(any(feature = "bin", feature = "serde")) + ) +))] +use super::{ + super::{super::AggErr, ExtensionInfo}, + Challenge, CredProtect, CredentialCreationOptions, FourToSixtyThree, PrfInput, + PublicKeyCredentialUserEntity, RpId, UserHandle, +}; +#[cfg(all(feature = "custom", feature = "serializable_server_state"))] +use super::{ + super::{ + super::bin::{Decode as _, Encode as _}, + AsciiDomain, + }, + Extension, RegistrationServerState, +}; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +use super::{ + super::{ + super::{ + CredentialErr, + response::register::{ + AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation, + ClientExtensionsOutputs, CredentialPropertiesOutput, CredentialProtectionPolicy, + HmacSecret, + }, + }, + AuthTransports, + }, + AuthenticatorAttachment, BackupReq, ExtensionErr, ExtensionReq, RegCeremonyErr, Registration, + RegistrationVerificationOptions, UserVerificationRequirement, +}; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +use rsa::sha2::{Digest as _, Sha256}; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_UINT: u8 = 0b000_00000; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_NEG: u8 = 0b001_00000; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_BYTES: u8 = 0b010_00000; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_TEXT: u8 = 0b011_00000; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_MAP: u8 = 0b101_00000; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_SIMPLE: u8 = 0b111_00000; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_FALSE: u8 = CBOR_SIMPLE | 20; +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +const CBOR_TRUE: u8 = CBOR_SIMPLE | 21; +#[expect(clippy::panic_in_result_fn, reason = "OK in tests")] +#[test] +#[cfg(all(feature = "custom", feature = "serializable_server_state"))] +fn eddsa_reg_ser() -> Result<(), AggErr> { + let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); + let id = UserHandle::from([0; 1]); + let mut opts = CredentialCreationOptions::passkey( + &rp_id, + PublicKeyCredentialUserEntity { + name: "foo", + id: &id, + display_name: "", + }, + Vec::new(), + ); + opts.public_key.challenge = Challenge(0); + opts.public_key.extensions = Extension { + cred_props: None, + cred_protect: CredProtect::UserVerificationRequired( + false, + ExtensionInfo::RequireEnforceValue, + ), + min_pin_length: Some((FourToSixtyThree::Ten, ExtensionInfo::RequireEnforceValue)), + prf: Some(( + PrfInput { + first: [0].as_slice(), + second: None, + }, + ExtensionInfo::RequireEnforceValue, + )), + }; + let server = opts.start_ceremony()?.0; + let enc_data = server + .encode() + .map_err(AggErr::EncodeRegistrationServerState)?; + assert_eq!(enc_data.capacity(), enc_data.len()); + assert_eq!(enc_data.len(), 1 + 16 + 1 + 3 + (1 + 3 + 3 + 2) + 12 + 1); + assert!(server.is_eq(&RegistrationServerState::decode(enc_data.as_slice())?)); + Ok(()) +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +struct TestResponseOptions { + user_verified: bool, + cred_protect: CredentialProtectionPolicy, + prf: Option<bool>, + hmac: HmacSecret, + min_pin: Option<FourToSixtyThree>, + #[expect(clippy::option_option, reason = "fine")] + cred_props: Option<Option<bool>>, +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +enum PrfUvOptions { + /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise + /// `UserVerificationRequirement::Preferred` is used. + None(bool), + Prf(ExtensionInfo), +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +struct TestRequestOptions { + error_unsolicited: bool, + protect: CredProtect, + prf_uv: PrfUvOptions, + props: Option<ExtensionReq>, + pin: Option<(FourToSixtyThree, ExtensionInfo)>, +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +#[derive(Clone, Copy)] +struct TestOptions { + request: TestRequestOptions, + response: TestResponseOptions, +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +fn generate_client_data_json() -> Vec<u8> { + let mut json = Vec::with_capacity(256); + json.extend_from_slice(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()); + json +} +#[expect(clippy::unreachable, reason = "want to crash when there is a bug")] +#[expect( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + reason = "comments justify correctness" +)] +#[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +fn generate_attestation_object(options: TestResponseOptions) -> Vec<u8> { + let mut attestation_object = Vec::with_capacity(256); + 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 | 24, + // Length. + // Addition won't overflow. + 113 + if matches!(options.cred_protect, CredentialProtectionPolicy::None) { + if matches!(options.hmac, HmacSecret::None) { + options.min_pin.map_or(0, |_| 15) + } else { + 14 + options.min_pin.map_or(0, |_| 14) + + match options.hmac { + HmacSecret::None => unreachable!("bug"), + HmacSecret::NotEnabled | HmacSecret::Enabled => 0, + HmacSecret::One => 65, + HmacSecret::Two => 97, + } + } + } else { + 14 + if matches!(options.hmac, HmacSecret::None) { + 0 + } else { + 13 + } + options.min_pin.map_or(0, |_| 14) + + match options.hmac { + HmacSecret::None | HmacSecret::NotEnabled | HmacSecret::Enabled => 0, + HmacSecret::One => 65, + HmacSecret::Two => 97, + } + }, + // RP ID HASH. + // 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, AT, and ED (right-to-left). + 0b0100_0001 + | if options.user_verified { + 0b0000_0100 + } else { + 0b0000_0000 + } + | if matches!(options.cred_protect, CredentialProtectionPolicy::None) + && matches!(options.hmac, HmacSecret::None) + && options.min_pin.is_none() + { + 0 + } else { + 0b1000_0000 + }, + // COUNTER. + // 0 as 32-bit big endian. + 0, + 0, + 0, + 0, + // AAGUID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // L. + // CREDENTIAL ID length is 16 as 16-bit big endian. + 0, + 16, + // CREDENTIAL ID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CBOR_MAP | 4, + // COSE kty. + CBOR_UINT | 1, + // COSE OKP. + CBOR_UINT | 1, + // COSE alg. + CBOR_UINT | 3, + // COSE Eddsa. + CBOR_NEG | 7, + // COSE OKP crv. + CBOR_NEG, + // COSE Ed25519. + CBOR_UINT | 6, + // COSE OKP x. + CBOR_NEG | 1, + CBOR_BYTES | 24, + // Length is 32. + 32, + // Compressed-y coordinate. + 59, + 106, + 39, + 188, + 206, + 182, + 164, + 45, + 98, + 163, + 168, + 208, + 42, + 111, + 13, + 115, + 101, + 50, + 21, + 119, + 29, + 226, + 67, + 166, + 58, + 192, + 72, + 161, + 139, + 89, + 218, + 41, + ] + .as_slice(), + ); + attestation_object[30..62].copy_from_slice(&Sha256::digest(b"example.com")); + if matches!(options.cred_protect, CredentialProtectionPolicy::None) { + if matches!(options.hmac, HmacSecret::None) { + if options.min_pin.is_some() { + attestation_object.push(CBOR_MAP | 1); + } + } else if options.min_pin.is_some() { + attestation_object.push( + // Addition won't overflow. + CBOR_MAP + | (2 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two))), + ); + } else { + attestation_object.push( + // Addition won't overflow. + CBOR_MAP + | (1 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two))), + ); + } + } else { + attestation_object.extend_from_slice( + [ + // Addition won't overflow. + CBOR_MAP + | (1 + match options.hmac { + HmacSecret::None => 0, + HmacSecret::NotEnabled | HmacSecret::Enabled => 1, + HmacSecret::One | HmacSecret::Two => 2, + } + u8::from(options.min_pin.is_some())), + // CBOR text of length 11. + CBOR_TEXT | 11, + b'c', + b'r', + b'e', + b'd', + b'P', + b'r', + b'o', + b't', + b'e', + b'c', + b't', + // Addition won't overflow. + match options.cred_protect { + CredentialProtectionPolicy::None => unreachable!("bug"), + CredentialProtectionPolicy::UserVerificationOptional => 1, + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => 2, + CredentialProtectionPolicy::UserVerificationRequired => 3, + }, + ] + .as_slice(), + ); + } + if !matches!(options.hmac, HmacSecret::None) { + attestation_object.extend_from_slice( + [ + // CBOR text of length 11. + CBOR_TEXT | 11, + b'h', + b'm', + b'a', + b'c', + b'-', + b's', + b'e', + b'c', + b'r', + b'e', + b't', + if matches!(options.hmac, HmacSecret::NotEnabled) { + CBOR_FALSE + } else { + CBOR_TRUE + }, + ] + .as_slice(), + ); + } + _ = options.min_pin.map(|p| { + assert!(p <= FourToSixtyThree::TwentyThree, "bug"); + attestation_object.extend_from_slice( + [ + // CBOR text of length 12. + CBOR_TEXT | 12, + b'm', + b'i', + b'n', + b'P', + b'i', + b'n', + b'L', + b'e', + b'n', + b'g', + b't', + b'h', + CBOR_UINT | p.into_u8(), + ] + .as_slice(), + ); + }); + if matches!(options.hmac, HmacSecret::One | HmacSecret::Two) { + attestation_object.extend_from_slice( + [ + // CBOR text of length 14. + CBOR_TEXT | 14, + b'h', + b'm', + b'a', + b'c', + b'-', + b's', + b'e', + b'c', + b'r', + b'e', + b't', + b'-', + b'm', + b'c', + CBOR_BYTES | 24, + ] + .as_slice(), + ); + if matches!(options.hmac, HmacSecret::One) { + attestation_object.push(48); + attestation_object.extend_from_slice([1; 48].as_slice()); + } else { + attestation_object.push(80); + attestation_object.extend_from_slice([1; 80].as_slice()); + } + } + attestation_object +} +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +fn validate(options: TestOptions) -> Result<(), AggErr> { + let rp_id = RpId::Domain("example.com".to_owned().try_into()?); + let registration = Registration::new( + AuthenticatorAttestation::new( + generate_client_data_json(), + generate_attestation_object(options.response), + AuthTransports::NONE, + ), + AuthenticatorAttachment::None, + ClientExtensionsOutputs { + cred_props: options + .response + .cred_props + .map(|rk| CredentialPropertiesOutput { rk }), + prf: options + .response + .prf + .map(|enabled| AuthenticationExtensionsPrfOutputs { enabled }), + }, + ); + let reg_opts = RegistrationVerificationOptions::<'static, 'static, &str, &str> { + allowed_origins: [].as_slice(), + allowed_top_origins: None, + backup_requirement: BackupReq::None, + error_on_unsolicited_extensions: options.request.error_unsolicited, + require_authenticator_attachment: false, + #[cfg(feature = "serde_relaxed")] + client_data_json_relaxed: false, + }; + let user = UserHandle::from([0; 1]); + let mut opts = CredentialCreationOptions::passkey( + &rp_id, + PublicKeyCredentialUserEntity { + id: &user, + name: "", + display_name: "", + }, + Vec::new(), + ); + opts.public_key.challenge = Challenge(0); + opts.public_key.authenticator_selection.user_verification = + UserVerificationRequirement::Preferred; + match options.request.prf_uv { + PrfUvOptions::None(required) => { + if required + || matches!( + options.request.protect, + CredProtect::UserVerificationRequired(_, _) + ) + { + opts.public_key.authenticator_selection.user_verification = + UserVerificationRequirement::Required; + } + } + PrfUvOptions::Prf(info) => { + opts.public_key.authenticator_selection.user_verification = + UserVerificationRequirement::Required; + opts.public_key.extensions.prf = Some(( + PrfInput { + first: [0].as_slice(), + second: None, + }, + info, + )); + } + } + opts.public_key.extensions.cred_protect = options.request.protect; + opts.public_key.extensions.cred_props = options.request.props; + opts.public_key.extensions.min_pin_length = options.request.pin; + opts.start_ceremony()? + .0 + .verify(&rp_id, &registration, &reg_opts) + .map_err(AggErr::RegCeremony) + .map(|_| ()) +} +/// Test all, and only, possible `UserNotVerified` errors. +/// 4 * 3 * 5 * 2 * 13 * 5 * 3 * 5 * 4 * 4 = 1,872,000 tests. +/// We ignore this due to how long it takes (around 30 seconds or so). +#[expect(clippy::too_many_lines, reason = "a lot to test")] +#[test] +#[ignore = "slow"] +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +fn uv_required_err() { + const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ + CredentialProtectionPolicy::None, + CredentialProtectionPolicy::UserVerificationOptional, + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, + CredentialProtectionPolicy::UserVerificationRequired, + ]; + const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)]; + const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [ + HmacSecret::None, + HmacSecret::NotEnabled, + HmacSecret::Enabled, + HmacSecret::One, + HmacSecret::Two, + ]; + const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true]; + const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [ + CredProtect::None, + CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue), + CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue), + CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue), + CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue), + CredProtect::UserVerificationOptionalWithCredentialIdList( + false, + ExtensionInfo::RequireEnforceValue, + ), + CredProtect::UserVerificationOptionalWithCredentialIdList( + true, + ExtensionInfo::RequireDontEnforceValue, + ), + CredProtect::UserVerificationOptionalWithCredentialIdList( + false, + ExtensionInfo::AllowEnforceValue, + ), + CredProtect::UserVerificationOptionalWithCredentialIdList( + true, + ExtensionInfo::AllowDontEnforceValue, + ), + CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), + CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue), + CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue), + CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue), + ]; + const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [ + PrfUvOptions::None(true), + PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), + PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue), + PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue), + PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue), + ]; + const ALL_PROPS_OPTIONS: [Option<ExtensionReq>; 3] = + [None, Some(ExtensionReq::Require), Some(ExtensionReq::Allow)]; + const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [ + None, + Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)), + Some(( + FourToSixtyThree::Five, + ExtensionInfo::RequireDontEnforceValue, + )), + Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)), + Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)), + ]; + #[expect(clippy::option_option, reason = "fine")] + const ALL_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 4] = + [None, Some(None), Some(Some(false)), Some(Some(true))]; + const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [ + None, + Some(FourToSixtyThree::Four), + Some(FourToSixtyThree::Five), + Some(FourToSixtyThree::Six), + ]; + for cred_protect in ALL_CRED_PROTECTION_OPTIONS { + for prf in ALL_PRF_OPTIONS { + for hmac in ALL_HMAC_OPTIONS { + for cred_props in ALL_CRED_PROPS_OPTIONS { + for min_pin in ALL_MIN_PIN_OPTIONS { + for error_unsolicited in ALL_UNSOLICIT_OPTIONS { + for protect in ALL_CRED_PROTECT_OPTIONS { + for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS { + for props in ALL_PROPS_OPTIONS { + for pin in ALL_PIN_OPTIONS { + assert!(validate(TestOptions { + request: TestRequestOptions { + error_unsolicited, + protect, + prf_uv, + props, + pin, + }, + response: TestResponseOptions { + user_verified: false, + hmac, + cred_protect, + prf, + min_pin, + cred_props, + }, + }).is_err_and(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::UserNotVerified)))); + } + } + } + } + } + } + } + } + } + } +} +/// Test all, and only, possible `ForbiddenCredProps` errors. +/// 4 * 3 * 5 * 2 * 13 * 6 * 5 * 3 * 4 = 561,600 +/// - +/// 4 * 3 * 5 * 4 * 6 * 5 * 3 * 4 = 86,400 +/// - +/// 4 * 3 * 5 * 13 * 5 * 5 * 3 * 4 = 234,000 +/// + +/// 4 * 3 * 5 * 4 * 5 * 5 * 3 * 4 = 72,000 +/// = +/// 313,200 total tests. +/// We ignore this due to how long it takes (around 6 seconds or so). +#[expect(clippy::too_many_lines, reason = "a lot to test")] +#[test] +#[ignore = "slow"] +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +fn forbidden_cred_props() { + const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ + CredentialProtectionPolicy::None, + CredentialProtectionPolicy::UserVerificationOptional, + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, + CredentialProtectionPolicy::UserVerificationRequired, + ]; + const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)]; + const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [ + HmacSecret::None, + HmacSecret::NotEnabled, + HmacSecret::Enabled, + HmacSecret::One, + HmacSecret::Two, + ]; + const ALL_UV_OPTIONS: [bool; 2] = [false, true]; + const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [ + CredProtect::None, + CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue), + CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue), + CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue), + CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue), + CredProtect::UserVerificationOptionalWithCredentialIdList( + false, + ExtensionInfo::RequireEnforceValue, + ), + CredProtect::UserVerificationOptionalWithCredentialIdList( + true, + ExtensionInfo::RequireDontEnforceValue, + ), + CredProtect::UserVerificationOptionalWithCredentialIdList( + false, + ExtensionInfo::AllowEnforceValue, + ), + CredProtect::UserVerificationOptionalWithCredentialIdList( + true, + ExtensionInfo::AllowDontEnforceValue, + ), + CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), + CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue), + CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue), + CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue), + ]; + const ALL_PRF_UV_OPTIONS: [PrfUvOptions; 6] = [ + PrfUvOptions::None(false), + PrfUvOptions::None(true), + PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), + PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue), + PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue), + PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue), + ]; + const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [ + None, + Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)), + Some(( + FourToSixtyThree::Five, + ExtensionInfo::RequireDontEnforceValue, + )), + Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)), + Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)), + ]; + #[expect(clippy::option_option, reason = "fine")] + const ALL_NON_EMPTY_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 3] = + [Some(None), Some(Some(false)), Some(Some(true))]; + const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [ + None, + Some(FourToSixtyThree::Four), + Some(FourToSixtyThree::Five), + Some(FourToSixtyThree::Six), + ]; + for cred_protect in ALL_CRED_PROTECTION_OPTIONS { + for prf in ALL_PRF_OPTIONS { + for hmac in ALL_HMAC_OPTIONS { + for cred_props in ALL_NON_EMPTY_CRED_PROPS_OPTIONS { + for min_pin in ALL_MIN_PIN_OPTIONS { + for user_verified in ALL_UV_OPTIONS { + for protect in ALL_CRED_PROTECT_OPTIONS { + for prf_uv in ALL_PRF_UV_OPTIONS { + for pin in ALL_PIN_OPTIONS { + if user_verified + || (!matches!( + protect, + CredProtect::UserVerificationRequired(_, _) + ) && matches!(prf_uv, PrfUvOptions::None(uv) if !uv)) + { + assert!(validate(TestOptions { + request: TestRequestOptions { + error_unsolicited: true, + protect, + prf_uv, + props: None, + pin, + }, + response: TestResponseOptions { + user_verified, + cred_protect, + prf, + hmac, + min_pin, + cred_props, + }, + }).is_err_and(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenCredProps))))); + } + } + } + } + } + } + } + } + } + } +} +#[expect(clippy::panic_in_result_fn, reason = "OK in tests")] +#[test] +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +fn prf() -> Result<(), AggErr> { + let mut opts = TestOptions { + request: TestRequestOptions { + error_unsolicited: false, + protect: CredProtect::None, + prf_uv: PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue), + props: None, + pin: None, + }, + response: TestResponseOptions { + user_verified: true, + hmac: HmacSecret::None, + cred_protect: CredentialProtectionPolicy::None, + prf: Some(true), + min_pin: None, + cred_props: None, + }, + }; + validate(opts)?; + opts.response.prf = Some(false); + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidPrfValue))))); + opts.response.hmac = HmacSecret::NotEnabled; + opts.response.prf = Some(true); + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue))))); + opts.request.prf_uv = PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue); + opts.response.hmac = HmacSecret::Enabled; + opts.response.prf = None; + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))))); + opts.response.hmac = HmacSecret::NotEnabled; + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))))); + opts.response.prf = Some(true); + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::PrfWithoutHmacSecret))))); + opts.response.prf = Some(false); + validate(opts)?; + opts.request.prf_uv = PrfUvOptions::None(false); + opts.response.user_verified = false; + opts.response.hmac = HmacSecret::Enabled; + opts.response.prf = Some(true); + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutUserVerified))))); + opts.response.prf = None; + opts.response.hmac = HmacSecret::None; + validate(opts)?; + Ok(()) +} +#[expect(clippy::panic_in_result_fn, reason = "OK in tests")] +#[test] +#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] +fn cred_protect() -> Result<(), AggErr> { + let mut opts = TestOptions { + request: TestRequestOptions { + error_unsolicited: false, + protect: CredProtect::UserVerificationRequired( + false, + ExtensionInfo::RequireEnforceValue, + ), + prf_uv: PrfUvOptions::None(false), + props: None, + pin: None, + }, + response: TestResponseOptions { + user_verified: true, + hmac: HmacSecret::None, + cred_protect: CredentialProtectionPolicy::UserVerificationRequired, + prf: None, + min_pin: None, + cred_props: None, + }, + }; + validate(opts)?; + opts.response.cred_protect = + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidCredProtectValue(CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue), CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList)))))); + opts.request.protect = + CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireEnforceValue); + opts.response.user_verified = false; + opts.response.cred_protect = CredentialProtectionPolicy::UserVerificationRequired; + assert!(validate(opts).is_err_and(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::CredProtectUserVerificationRequiredWithoutUserVerified))))); + Ok(()) +} diff --git a/src/request/tests.rs b/src/request/tests.rs @@ -0,0 +1,8963 @@ +#[cfg(feature = "custom")] +use super::{ + super::{ + AggErr, AuthenticatedCredential, + response::{ + AuthTransports, AuthenticatorAttachment, Backup, CredentialId, + auth::{ + DiscoverableAuthentication, DiscoverableAuthenticatorAssertion, + NonDiscoverableAuthentication, NonDiscoverableAuthenticatorAssertion, + }, + register::{ + AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation, + AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputs, + ClientExtensionsOutputsStaticState, CompressedP256PubKey, CompressedP384PubKey, + CompressedPubKeyOwned, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, + MlDsa44PubKey, MlDsa65PubKey, MlDsa87PubKey, Registration, RsaPubKey, StaticState, + UncompressedPubKey, + }, + }, + }, + Challenge, Credentials as _, ExtensionInfo, ExtensionReq, PrfInput, + PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement, + auth::{ + AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions, + CredentialSpecificExtension, DiscoverableCredentialRequestOptions, Extension as AuthExt, + NonDiscoverableCredentialRequestOptions, PrfInputOwned, + }, + register::{ + CredProtect, CredentialCreationOptions, Extension as RegExt, FourToSixtyThree, + PublicKeyCredentialUserEntity, RegistrationVerificationOptions, UserHandle, + }, +}; +use super::{AsciiDomainStatic, Hints, PublicKeyCredentialHint}; +#[cfg(feature = "custom")] +use ed25519_dalek::{Signer as _, SigningKey}; +#[cfg(feature = "custom")] +use ml_dsa::{ + ExpandedSigningKey as MlDsaSigKey, MlDsa44, MlDsa65, MlDsa87, Signature as MlDsaSignature, +}; +#[cfg(feature = "custom")] +use p256::{ + ecdsa::{DerSignature as P256DerSig, SigningKey as P256Key}, + elliptic_curve::sec1::Tag, +}; +#[cfg(feature = "custom")] +use p384::ecdsa::{DerSignature as P384DerSig, SigningKey as P384Key}; +#[cfg(feature = "custom")] +use rsa::{ + BoxedUint, RsaPrivateKey, + pkcs1v15::SigningKey as RsaKey, + sha2::{Digest as _, Sha256}, + signature::{Keypair as _, SignatureEncoding as _}, + traits::PublicKeyParts as _, +}; +use serde_json as _; +#[cfg(feature = "custom")] +const CBOR_UINT: u8 = 0b000_00000; +#[cfg(feature = "custom")] +const CBOR_NEG: u8 = 0b001_00000; +#[cfg(feature = "custom")] +const CBOR_BYTES: u8 = 0b010_00000; +#[cfg(feature = "custom")] +const CBOR_TEXT: u8 = 0b011_00000; +#[cfg(feature = "custom")] +const CBOR_MAP: u8 = 0b101_00000; +#[cfg(feature = "custom")] +const CBOR_SIMPLE: u8 = 0b111_00000; +#[cfg(feature = "custom")] +const CBOR_TRUE: u8 = CBOR_SIMPLE | 21; +#[test] +fn ascii_domain_static() { + /// No trailing dot, max label length, max domain length. + const LONG: AsciiDomainStatic = AsciiDomainStatic::new( + "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", + ) + .unwrap(); + /// Trailing dot, min label length, max domain length. + const LONG_TRAILING: AsciiDomainStatic = AsciiDomainStatic::new("w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.").unwrap(); + /// Single character domain. + const SHORT: AsciiDomainStatic = AsciiDomainStatic::new("w").unwrap(); + let long_label = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"; + assert_eq!(long_label.len(), 63); + let mut long = format!("{long_label}.{long_label}.{long_label}.{long_label}"); + _ = long.pop(); + _ = long.pop(); + assert_eq!(LONG.0.len(), 253); + assert_eq!(LONG.0, long.as_str()); + let trailing = "w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w."; + assert_eq!(LONG_TRAILING.0.len(), 254); + assert_eq!(LONG_TRAILING.0, trailing); + assert_eq!(SHORT.0.len(), 1); + assert_eq!(SHORT.0, "w"); + assert!(AsciiDomainStatic::new("www.Example.com").is_none()); + assert!(AsciiDomainStatic::new("").is_none()); + assert!(AsciiDomainStatic::new(".").is_none()); + assert!(AsciiDomainStatic::new("www..c").is_none()); + let too_long_label = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"; + assert_eq!(too_long_label.len(), 64); + assert!(AsciiDomainStatic::new(too_long_label).is_none()); + let dom_254_no_trailing_dot = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"; + assert_eq!(dom_254_no_trailing_dot.len(), 254); + assert!(AsciiDomainStatic::new(dom_254_no_trailing_dot).is_none()); + assert!(AsciiDomainStatic::new("\u{3bb}.com").is_none()); +} +#[cfg(feature = "custom")] +const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); +#[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_reg() -> Result<(), AggErr> { + let id = UserHandle::from([0]); + let mut opts = CredentialCreationOptions::passkey( + RP_ID, + PublicKeyCredentialUserEntity { + name: "foo", + id: &id, + display_name: "", + }, + Vec::new(), + ); + opts.public_key.challenge = Challenge(0); + opts.public_key.extensions = RegExt { + cred_props: None, + cred_protect: CredProtect::UserVerificationRequired( + false, + ExtensionInfo::RequireEnforceValue, + ), + min_pin_length: Some((FourToSixtyThree::Ten, ExtensionInfo::RequireEnforceValue)), + prf: Some(( + PrfInput { + first: [0].as_slice(), + second: None, + }, + ExtensionInfo::RequireEnforceValue, + )), + }; + 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::new(); + attestation_object.extend_from_slice( + [ + CBOR_MAP | 3, + CBOR_TEXT | 3, + b'f', + b'm', + b't', + CBOR_TEXT | 6, + b'p', + b'a', + b'c', + b'k', + b'e', + b'd', + CBOR_TEXT | 7, + b'a', + b't', + b't', + b'S', + b't', + b'm', + b't', + CBOR_MAP | 2, + CBOR_TEXT | 3, + b'a', + b'l', + b'g', + // COSE EdDSA. + CBOR_NEG | 7, + CBOR_TEXT | 3, + b's', + b'i', + b'g', + CBOR_BYTES | 24, + 64, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CBOR_TEXT | 8, + b'a', + b'u', + b't', + b'h', + b'D', + b'a', + b't', + b'a', + CBOR_BYTES | 24, + // Length is 154. + 154, + // RP ID HASH. + // 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, AT, and ED (right-to-left). + 0b1100_0101, + // COUNTER. + // 0 as 32-bit big endian. + 0, + 0, + 0, + 0, + // AAGUID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // L. + // CREDENTIAL ID length is 16 as 16-bit big endian. + 0, + 16, + // CREDENTIAL ID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CBOR_MAP | 4, + // COSE kty. + CBOR_UINT | 1, + // COSE OKP. + CBOR_UINT | 1, + // COSE alg. + CBOR_UINT | 3, + // COSE EdDSA. + CBOR_NEG | 7, + // COSE OKP crv. + CBOR_NEG, + // COSE Ed25519. + CBOR_UINT | 6, + // COSE OKP x. + CBOR_NEG | 1, + CBOR_BYTES | 24, + // Length is 32. + 32, + // Compressed-y coordinate. + // 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, + CBOR_MAP | 3, + CBOR_TEXT | 11, + b'c', + b'r', + b'e', + b'd', + b'P', + b'r', + b'o', + b't', + b'e', + b'c', + b't', + // userVerificationRequired. + CBOR_UINT | 3, + // CBOR text of length 11. + 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_TRUE, + CBOR_TEXT | 12, + b'm', + b'i', + b'n', + b'P', + b'i', + b'n', + b'L', + b'e', + b'n', + b'g', + b't', + b'h', + CBOR_UINT | 16, + ] + .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); + 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", + id: &id, + display_name: "", + }, + 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", + id: &id, + display_name: "", + }, + 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", + id: &id, + display_name: "", + }, + 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 { + 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::MlDsa44(k) if **k.inner() == [1; 1312])); + 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 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(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 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, + &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::MlDsa44( + MlDsa44PubKey::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, + 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 es256_reg() -> Result<(), AggErr> { + let id = UserHandle::from([0]); + let mut opts = CredentialCreationOptions::passkey( + RP_ID, + PublicKeyCredentialUserEntity { + name: "foo", + id: &id, + display_name: "", + }, + 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(210); + 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 | 24, + // Length is 148. + 148, + // RP ID HASH. + // 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 AT (right-to-left). + 0b0100_0101, + // COUNTER. + // 0 as 32-bit big endian. + 0, + 0, + 0, + 0, + // AAGUID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // L. + // CREDENTIAL ID length is 16 as 16-bit big endian. + 0, + 16, + // CREDENTIAL ID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CBOR_MAP | 5, + // COSE kty. + CBOR_UINT | 1, + // COSE EC2. + CBOR_UINT | 2, + // COSE alg. + CBOR_UINT | 3, + // COSE ES256. + CBOR_NEG | 6, + // COSE EC2 crv. + CBOR_NEG, + // COSE P-256. + CBOR_UINT | 1, + // COSE EC2 x. + CBOR_NEG | 1, + CBOR_BYTES | 24, + // Length is 32. + 32, + // X-coordinate. 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, + // COSE EC2 y. + CBOR_NEG | 2, + CBOR_BYTES | 24, + // Length is 32. + 32, + // Y-coordinate. 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, + ] + .as_slice(), + ); + attestation_object[30..62].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); + let p256_key = P256Key::from_bytes( + &[ + 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, + 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, + ] + .into(), + ) + .unwrap() + .verifying_key() + .to_sec1_point(false); + let x = p256_key.x().unwrap(); + let y = p256_key.y().unwrap(); + attestation_object[111..143].copy_from_slice(x); + attestation_object[146..].copy_from_slice(y); + 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::P256(k) if *k.x() == **x && *k.y() == **y)); + 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 es256_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 p256_key = P256Key::from_bytes( + &[ + 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, + 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, + ] + .into(), + ) + .unwrap(); + let der_sig: P256DerSig = p256_key.sign(authenticator_data.as_slice()); + let pub_key = p256_key.verifying_key().to_sec1_point(true); + 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, + der_sig.as_bytes().into(), + 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::P256(CompressedP256PubKey::from(( + (*pub_key.x().unwrap()).into(), + pub_key.tag() == Tag::CompressedOddY + )),), + 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, + 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 es384_reg() -> Result<(), AggErr> { + let id = UserHandle::from([0]); + let mut opts = CredentialCreationOptions::passkey( + RP_ID, + PublicKeyCredentialUserEntity { + name: "foo", + id: &id, + display_name: "", + }, + 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(243); + 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 of length 7. + 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 | 24, + // Length is 181. + 181, + // RP ID HASH. + // 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 AT (right-to-left). + 0b0100_0101, + // COUNTER. + // 0 as 32-bit big-endian. + 0, + 0, + 0, + 0, + // AAGUID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // L. + // CREDENTIAL ID length is 16 as 16-bit big endian. + 0, + 16, + // CREDENTIAL ID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CBOR_MAP | 5, + // COSE kty. + CBOR_UINT | 1, + // COSE EC2. + CBOR_UINT | 2, + // COSE alg. + CBOR_UINT | 3, + CBOR_NEG | 24, + // COSE ES384. + 34, + // COSE EC2 crv. + CBOR_NEG, + // COSE P-384. + CBOR_UINT | 2, + // COSE EC2 x. + CBOR_NEG | 1, + CBOR_BYTES | 24, + // Length is 48. + 48, + // X-coordinate. 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, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // COSE EC2 y. + CBOR_NEG | 2, + CBOR_BYTES | 24, + // Length is 48. + 48, + // Y-coordinate. 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, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + .as_slice(), + ); + attestation_object[30..62].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); + let p384_key = P384Key::from_bytes( + &[ + 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, 42, + 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, 32, 86, + 220, 68, 182, 11, 105, 223, 75, 70, + ] + .into(), + ) + .unwrap() + .verifying_key() + .to_sec1_point(false); + let x = p384_key.x().unwrap(); + let y = p384_key.y().unwrap(); + attestation_object[112..160].copy_from_slice(x); + attestation_object[163..].copy_from_slice(y); + 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::P384(k) if *k.x() == **x && *k.y() == **y)); + 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 es384_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 p384_key = P384Key::from_bytes( + &[ + 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, 42, + 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, 32, 86, + 220, 68, 182, 11, 105, 223, 75, 70, + ] + .into(), + ) + .unwrap(); + let der_sig: P384DerSig = p384_key.sign(authenticator_data.as_slice()); + let pub_key = p384_key.verifying_key().to_sec1_point(true); + 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, + der_sig.as_bytes().into(), + 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::P384(CompressedP384PubKey::from(( + (*pub_key.x().unwrap()).into(), + pub_key.tag() == Tag::CompressedOddY + )),), + 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, + 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")] +#[expect(clippy::many_single_char_names, reason = "fine")] +#[test] +#[cfg(feature = "custom")] +fn rs256_reg() -> Result<(), AggErr> { + let id = UserHandle::from([0]); + let mut opts = CredentialCreationOptions::passkey( + RP_ID, + PublicKeyCredentialUserEntity { + name: "foo", + id: &id, + display_name: "", + }, + 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(406); + 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, + // Length is 343 as 16-bit big-endian. + 1, + 87, + // RP ID HASH. + // 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 AT (right-to-left). + 0b0100_0101, + // COUNTER. + // 0 as 32-bit big-endian. + 0, + 0, + 0, + 0, + // AAGUID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // L. + // CREDENTIAL ID length is 16 as 16-bit big endian. + 0, + 16, + // CREDENTIAL ID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CBOR_MAP | 4, + // COSE kty. + CBOR_UINT | 1, + // COSE RSA. + CBOR_UINT | 3, + // COSE alg. + CBOR_UINT | 3, + CBOR_NEG | 25, + // COSE RS256. + 1, + 0, + // COSE n. + CBOR_NEG, + CBOR_BYTES | 25, + // Length is 256 as 16-bit big-endian. + 1, + 0, + // N. 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, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // COSE e. + CBOR_NEG | 1, + CBOR_BYTES | 3, + // 65537 as 24-bit big-endian. + 1, + 0, + 1, + ] + .as_slice(), + ); + attestation_object[31..63].copy_from_slice(&Sha256::digest(RP_ID.as_ref().as_bytes())); + let n = [ + 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, 195, + 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, 185, 19, + 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, 100, 128, + 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, 145, 50, 174, + 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, 87, 145, 45, 32, + 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, 81, 217, 151, 162, + 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, 117, 248, 233, 151, + 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, 254, 81, 202, 64, 232, + 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, 108, 139, 253, 230, 80, + 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, 42, 198, 212, 23, 182, 222, + 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, 104, 68, 94, 198, 196, 35, 200, + 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, 72, 93, 53, 65, 111, 59, 242, 122, + 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, 153, 79, 0, 133, 78, 7, 218, 165, 241, + ]; + let e = 0x0001_0001; + let d = [ + 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, + 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, 35, + 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, 154, + 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, 6, 219, + 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, 82, 104, + 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, 216, 152, 94, + 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, 215, 107, 212, + 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, 140, 252, 248, 162, + 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, 249, 237, 203, 202, 48, 26, + 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, 31, 253, 55, 43, 158, 90, 248, + 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, 162, 60, 91, 99, 220, 51, 44, 85, + 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, 24, 245, 127, 122, 247, 152, 212, 75, + 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, + ]; + let p = BoxedUint::from_le_slice_vartime( + [ + 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, 69, + 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, 250, 195, + 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, 99, 76, 240, 14, + 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, 82, 98, 233, 236, + 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, 147, 179, 30, 62, 247, + 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, 168, 253, 228, 214, 54, 16, + 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, 24, 247, + ] + .as_slice(), + ); + let p_2 = BoxedUint::from_le_slice_vartime( + [ + 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, 85, + 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, 141, + 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, 39, 85, + 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, 250, 187, + 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, 112, 211, + 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, 173, 9, 236, + 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, 250, + ] + .as_slice(), + ); + let rsa_key = RsaKey::<Sha256>::new( + RsaPrivateKey::from_components( + BoxedUint::from_le_slice_vartime(n.as_slice()), + e.into(), + BoxedUint::from_le_slice_vartime(d.as_slice()), + vec![p, p_2], + ) + .unwrap(), + ) + .verifying_key(); + let n_other = rsa_key.as_ref().n().to_be_bytes(); + attestation_object[113..369].copy_from_slice(&n_other); + 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::Rsa(k) if **k.n() == *n_other && k.e() == e)); + 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")] +#[expect(clippy::too_many_lines, reason = "a lot to test")] +#[test] +#[cfg(feature = "custom")] +fn rs256_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 n = [ + 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, 195, + 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, 185, 19, + 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, 100, 128, + 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, 145, 50, 174, + 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, 87, 145, 45, 32, + 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, 81, 217, 151, 162, + 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, 117, 248, 233, 151, + 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, 254, 81, 202, 64, 232, + 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, 108, 139, 253, 230, 80, + 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, 42, 198, 212, 23, 182, 222, + 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, 104, 68, 94, 198, 196, 35, 200, + 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, 72, 93, 53, 65, 111, 59, 242, 122, + 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, 153, 79, 0, 133, 78, 7, 218, 165, 241, + ]; + let e = 0x0001_0001; + let d = [ + 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, + 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, 35, + 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, 154, + 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, 6, 219, + 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, 82, 104, + 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, 216, 152, 94, + 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, 215, 107, 212, + 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, 140, 252, 248, 162, + 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, 249, 237, 203, 202, 48, 26, + 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, 31, 253, 55, 43, 158, 90, 248, + 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, 162, 60, 91, 99, 220, 51, 44, 85, + 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, 24, 245, 127, 122, 247, 152, 212, 75, + 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, + ]; + let p = BoxedUint::from_le_slice_vartime( + [ + 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, 69, + 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, 250, 195, + 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, 99, 76, 240, 14, + 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, 82, 98, 233, 236, + 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, 147, 179, 30, 62, 247, + 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, 168, 253, 228, 214, 54, 16, + 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, 24, 247, + ] + .as_slice(), + ); + let p_2 = BoxedUint::from_le_slice_vartime( + [ + 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, 85, + 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, 141, + 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, 39, 85, + 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, 250, 187, + 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, 112, 211, + 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, 173, 9, 236, + 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, 250, + ] + .as_slice(), + ); + let rsa_key = RsaKey::<Sha256>::new( + RsaPrivateKey::from_components( + BoxedUint::from_le_slice_vartime(n.as_slice()), + e.into(), + BoxedUint::from_le_slice_vartime(d.as_slice()), + vec![p, p_2], + ) + .unwrap(), + ); + 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].into_boxed_slice())?, + 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: CompressedPubKeyOwned::Rsa( + RsaPubKey::try_from((rsa_pub.as_ref().n().to_be_bytes(), 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(()) +} +#[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[test] +fn hints() { + assert_eq!(Hints::EMPTY.0, [None; 3]); + assert!( + Hints::EMPTY.first().is_none() + && Hints::EMPTY.second().is_none() + && Hints::EMPTY.third().is_none() + ); + assert_eq!(Hints::EMPTY.count(), 0); + assert!(Hints::EMPTY.is_empty()); + assert!( + !(Hints::EMPTY.contains_cross_platform_hints() || Hints::EMPTY.contains_platform_hints()) + ); + assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(!Hints::EMPTY.contains(PublicKeyCredentialHint::Hybrid)); + let mut hints = Hints::EMPTY.add(PublicKeyCredentialHint::SecurityKey); + assert_eq!( + hints.0, + [Some(PublicKeyCredentialHint::SecurityKey), None, None] + ); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::SecurityKey).0); + assert!( + hints.first() == Some(PublicKeyCredentialHint::SecurityKey) + && hints.second().is_none() + && hints.third().is_none() + ); + assert_eq!(hints.count(), 1); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && !hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(!hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(!hints.contains(PublicKeyCredentialHint::Hybrid)); + hints = Hints::EMPTY.add(PublicKeyCredentialHint::Hybrid); + assert_eq!(hints.0, [Some(PublicKeyCredentialHint::Hybrid), None, None]); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); + assert!( + hints.first() == Some(PublicKeyCredentialHint::Hybrid) + && hints.second().is_none() + && hints.third().is_none() + ); + assert_eq!(hints.count(), 1); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && !hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); + assert!(!hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(!hints.contains(PublicKeyCredentialHint::ClientDevice)); + hints = Hints::EMPTY.add(PublicKeyCredentialHint::ClientDevice); + assert_eq!( + hints.0, + [Some(PublicKeyCredentialHint::ClientDevice), None, None] + ); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::ClientDevice).0); + assert!( + hints.first() == Some(PublicKeyCredentialHint::ClientDevice) + && hints.second().is_none() + && hints.third().is_none() + ); + assert_eq!(hints.count(), 1); + assert!(!hints.is_empty()); + assert!(!hints.contains_cross_platform_hints() && hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(!hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(!hints.contains(PublicKeyCredentialHint::Hybrid)); + hints = hints.add(PublicKeyCredentialHint::Hybrid); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::ClientDevice).0); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); + assert_eq!( + hints.0, + [ + Some(PublicKeyCredentialHint::ClientDevice), + Some(PublicKeyCredentialHint::Hybrid), + None + ] + ); + assert!( + hints.first() == Some(PublicKeyCredentialHint::ClientDevice) + && hints.second() == Some(PublicKeyCredentialHint::Hybrid) + && hints.third().is_none() + ); + assert_eq!(hints.count(), 2); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(!hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); + hints = hints.add(PublicKeyCredentialHint::SecurityKey); + assert_eq!( + hints.0, + [ + Some(PublicKeyCredentialHint::ClientDevice), + Some(PublicKeyCredentialHint::Hybrid), + Some(PublicKeyCredentialHint::SecurityKey), + ] + ); + assert!( + hints.first() == Some(PublicKeyCredentialHint::ClientDevice) + && hints.second() == Some(PublicKeyCredentialHint::Hybrid) + && hints.third() == Some(PublicKeyCredentialHint::SecurityKey) + ); + assert_eq!(hints.count(), 3); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && hints.contains_platform_hints()); + assert!(hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::SecurityKey).0); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::ClientDevice).0); + hints = Hints::EMPTY + .add(PublicKeyCredentialHint::Hybrid) + .add(PublicKeyCredentialHint::SecurityKey); + assert_eq!( + hints.0, + [ + Some(PublicKeyCredentialHint::Hybrid), + Some(PublicKeyCredentialHint::SecurityKey), + None, + ] + ); + assert!( + hints.first() == Some(PublicKeyCredentialHint::Hybrid) + && hints.second() == Some(PublicKeyCredentialHint::SecurityKey) + && hints.third().is_none() + ); + assert_eq!(hints.count(), 2); + assert!(!hints.is_empty()); + assert!(hints.contains_cross_platform_hints() && !hints.contains_platform_hints()); + assert!(!hints.contains(PublicKeyCredentialHint::ClientDevice)); + assert!(hints.contains(PublicKeyCredentialHint::SecurityKey)); + assert!(hints.contains(PublicKeyCredentialHint::Hybrid)); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::SecurityKey).0); + assert_eq!(hints.0, hints.add(PublicKeyCredentialHint::Hybrid).0); +} diff --git a/src/response.rs b/src/response.rs @@ -1,4 +1,6 @@ extern crate alloc; +#[cfg(test)] +mod tests; use crate::{ request::{register::{PublicKeyCredentialUserEntity, UserHandle}, Challenge, RpId, Url}, response::{ @@ -1770,127 +1772,3 @@ impl<const REG: bool> FromCbor<'_> for HmacSecretGet<REG> { }) } } -#[cfg(test)] -mod tests { - use super::{CollectedClientDataErr, ClientDataJsonParser as _, LimitedVerificationParser}; - #[test] - fn parse_string() { - assert!(LimitedVerificationParser::<true>::parse_string(br#"abc""#) - .is_ok_and(|tup| { tup.0 == "abc" && tup.1 == b"" })); - assert!(LimitedVerificationParser::<false>::parse_string(br#"abc"23"#) - .is_ok_and(|tup| { tup.0 == "abc" && tup.1 == b"23" })); - assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\"c"23"#) - .is_ok_and(|tup| { tup.0 == r#"ab"c"# && tup.1 == b"23" })); - assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\\c"23"#) - .is_ok_and(|tup| { tup.0 == r"ab\c" && tup.1 == b"23" })); - assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\u001fc"23"#) - .is_ok_and(|tup| { tup.0 == "ab\u{001f}c" && tup.1 == b"23" })); - assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\u000dc"23"#) - .is_ok_and(|tup| { tup.0 == "ab\u{000d}c" && tup.1 == b"23" })); - assert!( - LimitedVerificationParser::<true>::parse_string(b"\\\\\\\\\\\\a\\\\\\\\a\\\\\"").is_ok_and(|tup| { - tup.0 == "\\\\\\a\\\\a\\" && tup.1.is_empty() - }) - ); - assert!( - LimitedVerificationParser::<false>::parse_string(b"\\\\\\\\\\a\\\\\\\\a\\\\\"").is_err_and( - |e| matches!(e, CollectedClientDataErr::InvalidEscapedString), - ) - ); - assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\u0020c"23"#).is_err_and( - |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), - )); - assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\ac"23"#).is_err_and( - |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), - )); - assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\""#).is_err_and( - |err| matches!(err, CollectedClientDataErr::InvalidObject), - )); - assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\u001Fc"23"#).is_err_and( - |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), - )); - assert!(LimitedVerificationParser::<true>::parse_string([0, b'"'].as_slice()).is_err_and( - |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), - )); - assert!(LimitedVerificationParser::<false>::parse_string([b'a', 255, b'"'].as_slice()) - .is_err_and(|err| matches!(err, CollectedClientDataErr::Utf8(_)))); - assert!(LimitedVerificationParser::<true>::parse_string([b'a', b'"', 255].as_slice()).is_ok_and(|tup| tup.0 == "a" && tup.1 == [255])); - assert!( - LimitedVerificationParser::<false>::parse_string(br#"""#).is_ok_and(|tup| tup.0.is_empty() && tup.1.is_empty()) - ); - } - #[expect(clippy::cognitive_complexity, reason = "a lot of things to test")] - #[test] - fn c_data_json() { - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,{}}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_some_and(|v| v == "bob"))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob",a}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_some_and(|v| v == "bob"))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"a}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Challenge))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0.is_empty() && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Type))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::ChallengeKey))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::OriginKey))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\\e.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\"e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\"e.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0013e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\u{0013}e.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\3e.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); - assert!(LimitedVerificationParser::<true>::parse([].as_slice()) - .is_err_and(|e| matches!(e, CollectedClientDataErr::Len))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidStart))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::OriginKey))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::CrossOriginKey))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::CrossOrigin))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true","topOrigin":"https://abc.com"a}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<false>::parse(b"{\"type\":\"webauthn.get\",\"challenge\":\"AAAAAAAAAAAAAAAAAAAAAA\",\"origin\":\"https://example.com\",\"crossOrigin\":false,\xff}".as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_some_and(|v| v == "bob"))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Challenge))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0.is_empty() && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Type))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::ChallengeKey))); - assert!(LimitedVerificationParser::<false>::parse( - br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false}"# - .as_slice() - ) - .is_err_and(|e| matches!(e, CollectedClientDataErr::OriginKey))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\\e.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\"e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\"e.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0013e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\u{0013}e.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\3e.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); - assert!(LimitedVerificationParser::<false>::parse([].as_slice()) - .is_err_and(|e| matches!(e, CollectedClientDataErr::Len))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidStart))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::OriginKey))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::CrossOriginKey))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::CrossOrigin))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"https://example.com"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::TopOriginSameAsOrigin))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"foo":true}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challengE":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"foo":true}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::ChallengeKey))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create"challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossorigin":false,"foo":true}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::ChallengeKey))); - } - #[test] - fn c_data_challenge() { - assert!(LimitedVerificationParser::<false>::get_sent_challenge([].as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Len))); - assert!(LimitedVerificationParser::<true>::get_sent_challenge([].as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Len))); - assert!(LimitedVerificationParser::<true>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBB").is_err_and(|e| matches!(e, CollectedClientDataErr::Challenge))); - assert!(LimitedVerificationParser::<false>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBB").is_err_and(|e| matches!(e, CollectedClientDataErr::Challenge))); - assert!(LimitedVerificationParser::<true>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".as_slice()).is_ok_and(|c| c.0 == 0)); - assert!(LimitedVerificationParser::<false>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".as_slice()).is_ok_and(|c| c.0 == 0)); - } -} diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; use super::{ super::{ super::response::ser::{Base64DecodedVal, PublicKeyCredential}, @@ -444,1316 +446,3 @@ impl Serialize for UnknownCredentialOptions<'_, '_> { }) } } -#[cfg(test)] -mod tests { - use super::super::{ - super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment, - DiscoverableAuthentication, NonDiscoverableAuthentication, - }; - use rsa::sha2::{Digest as _, Sha256}; - use serde::de::{Error as _, Unexpected}; - use serde_json::Error; - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] - #[expect( - clippy::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[test] - fn eddsa_authentication_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let auth_data = [ - // `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`. - 0b0000_0101, - // `signCount`. - 0, - 0, - 0, - 0, - ]; - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(auth_data.as_slice()); - let b64_sig = base64url_nopad::encode([].as_slice()); - let b64_user = base64url_nopad::encode(b"\x00".as_slice()); - let auth_data_len = 37; - // Base case is valid. - assert!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and( - |auth| auth.response.client_data_json == c_data_json.as_bytes() - && auth.response.authenticator_data_and_c_data_hash[..auth_data_len] - == auth_data - && auth.response.authenticator_data_and_c_data_hash[auth_data_len..] - == *Sha256::digest(c_data_json.as_bytes()) - && matches!( - auth.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - ) - ) - ); - // `id` and `rawId` mismatch. - let mut err = Error::invalid_value( - Unexpected::Bytes( - base64url_nopad::decode(b"ABABABABABABABABABABAA") - .unwrap() - .as_slice(), - ), - &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "ABABABABABABABABABABAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // missing `id`. - err = Error::missing_field("id").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `id`. - err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": null, - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // missing `rawId`. - err = Error::missing_field("rawId").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `rawId`. - err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": null, - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `authenticatorData`. - err = Error::missing_field("authenticatorData") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `authenticatorData`. - err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": null, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `signature`. - err = Error::missing_field("signature").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `signature`. - err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": null, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `userHandle`. - drop( - serde_json::from_str::<NonDiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `userHandle`. - drop( - serde_json::from_str::<NonDiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": null, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `authenticatorAttachment`. - assert!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|auth| matches!( - auth.authenticator_attachment, - AuthenticatorAttachment::None - )) - ); - // Unknown `authenticatorAttachment`. - err = Error::invalid_value( - Unexpected::Str("Platform"), - &"'platform' or 'cross-platform'", - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "Platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientDataJSON`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `clientDataJSON`. - err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": null, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `response`. - err = Error::missing_field("response").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `response`. - err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAssertion") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty `response`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": {}, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientExtensionResults`. - err = Error::missing_field("clientExtensionResults") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `clientExtensionResults`. - err = Error::invalid_type( - Unexpected::Other("null"), - &"clientExtensionResults to be a map of allowed client extensions", - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": null, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `type`. - err = Error::missing_field("type").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `type`. - err = Error::invalid_type(Unexpected::Other("null"), &"public-key") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": null - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Not exactly `public-type` `type`. - err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "Public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null`. - err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!(null).to_string().as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty. - err = Error::missing_field("response").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({}).to_string().as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field in `response`. - err = Error::unknown_field( - "foo", - [ - "clientDataJSON", - "authenticatorData", - "signature", - "userHandle", - ] - .as_slice(), - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "foo": true, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field in `response`. - err = Error::duplicate_field("userHandle") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field in `PublicKeyCredential`. - err = Error::unknown_field( - "foo", - [ - "id", - "type", - "rawId", - "response", - "authenticatorAttachment", - "clientExtensionResults", - ] - .as_slice(), - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key", - "foo": true, - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field in `PublicKeyCredential`. - err = Error::duplicate_field("id").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) - .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")] - #[test] - fn client_extensions() { - let c_data_json = serde_json::json!({}).to_string(); - let auth_data: [u8; 37] = [ - // `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`. - 0b0000_0101, - // `signCount`. - 0, - 0, - 0, - 0, - ]; - let auth_data_len = 37; - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(auth_data.as_slice()); - let b64_sig = base64url_nopad::encode([].as_slice()); - let b64_user = base64url_nopad::encode(b"\x00".as_slice()); - // Base case is valid. - assert!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and( - |auth| auth.response.client_data_json == c_data_json.as_bytes() - && auth.response.authenticator_data_and_c_data_hash[..auth_data_len] - == auth_data - && auth.response.authenticator_data_and_c_data_hash[auth_data_len..] - == *Sha256::digest(c_data_json.as_bytes()) - && matches!( - auth.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - ) - ) - ); - // `null` `prf`. - drop( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Unknown `clientExtensionResults`. - let mut err = Error::unknown_field("Prf", ["prf"].as_slice()) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "Prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field. - err = Error::duplicate_field("prf").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": null, - \"prf\": null - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `results`. - drop( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": null, - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field in `prf`. - err = Error::duplicate_field("results").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": {{ - \"results\": null, - \"results\": null - }} - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `first`. - err = Error::missing_field("first").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": {}, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `first`. - drop( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `second`. - drop( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null, - "second": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Non-`null` `first`. - err = Error::invalid_type(Unexpected::Option, &"null") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": "" - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Non-`null` `second`. - err = Error::invalid_type(Unexpected::Option, &"null") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null, - "second": "" - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown `prf` field. - err = Error::unknown_field("enabled", ["results"].as_slice()) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown `results` field. - err = Error::unknown_field("Second", ["first", "second"].as_slice()) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null, - "Second": null - } - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field in `results`. - err = Error::duplicate_field("first").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": {{ - \"results\": {{ - \"first\": null, - \"first\": null - }} - }} - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - } -} diff --git a/src/response/auth/ser/tests.rs b/src/response/auth/ser/tests.rs @@ -0,0 +1,1303 @@ +use super::super::{ + super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment, + DiscoverableAuthentication, NonDiscoverableAuthentication, +}; +use rsa::sha2::{Digest as _, Sha256}; +use serde::de::{Error as _, Unexpected}; +use serde_json::Error; +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[expect(clippy::indexing_slicing, reason = "comments justify correctness")] +#[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[test] +fn eddsa_authentication_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let auth_data = [ + // `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`. + 0b0000_0101, + // `signCount`. + 0, + 0, + 0, + 0, + ]; + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(auth_data.as_slice()); + let b64_sig = base64url_nopad::encode([].as_slice()); + let b64_user = base64url_nopad::encode(b"\x00".as_slice()); + let auth_data_len = 37; + // Base case is valid. + assert!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and( + |auth| auth.response.client_data_json == c_data_json.as_bytes() + && auth.response.authenticator_data_and_c_data_hash[..auth_data_len] == auth_data + && auth.response.authenticator_data_and_c_data_hash[auth_data_len..] + == *Sha256::digest(c_data_json.as_bytes()) + && matches!( + auth.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + ) + ); + // `id` and `rawId` mismatch. + let mut err = Error::invalid_value( + Unexpected::Bytes( + base64url_nopad::decode(b"ABABABABABABABABABABAA") + .unwrap() + .as_slice(), + ), + &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "ABABABABABABABABABABAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // missing `id`. + err = Error::missing_field("id").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `id`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": null, + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // missing `rawId`. + err = Error::missing_field("rawId").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `rawId`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": null, + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `authenticatorData`. + err = Error::missing_field("authenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `authenticatorData`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": null, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `signature`. + err = Error::missing_field("signature").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `signature`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": null, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `userHandle`. + drop( + serde_json::from_str::<NonDiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `userHandle`. + drop( + serde_json::from_str::<NonDiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": null, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `authenticatorAttachment`. + assert!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|auth| matches!(auth.authenticator_attachment, AuthenticatorAttachment::None)) + ); + // Unknown `authenticatorAttachment`. + err = Error::invalid_value( + Unexpected::Str("Platform"), + &"'platform' or 'cross-platform'", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "Platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientDataJSON`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `clientDataJSON`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": null, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `response`. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `response`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAssertion") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty `response`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": {}, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientExtensionResults`. + err = Error::missing_field("clientExtensionResults") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `clientExtensionResults`. + err = Error::invalid_type( + Unexpected::Other("null"), + &"clientExtensionResults to be a map of allowed client extensions", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": null, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `type`. + err = Error::missing_field("type").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `type`. + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": null + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Not exactly `public-type` `type`. + err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "Public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null`. + err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!(null).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({}).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field in `response`. + err = Error::unknown_field( + "foo", + [ + "clientDataJSON", + "authenticatorData", + "signature", + "userHandle", + ] + .as_slice(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "foo": true, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field in `response`. + err = Error::duplicate_field("userHandle") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field in `PublicKeyCredential`. + err = Error::unknown_field( + "foo", + [ + "id", + "type", + "rawId", + "response", + "authenticatorAttachment", + "clientExtensionResults", + ] + .as_slice(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key", + "foo": true, + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field in `PublicKeyCredential`. + err = Error::duplicate_field("id").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + }}" + ) + .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")] +#[test] +fn client_extensions() { + let c_data_json = serde_json::json!({}).to_string(); + let auth_data: [u8; 37] = [ + // `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`. + 0b0000_0101, + // `signCount`. + 0, + 0, + 0, + 0, + ]; + let auth_data_len = 37; + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(auth_data.as_slice()); + let b64_sig = base64url_nopad::encode([].as_slice()); + let b64_user = base64url_nopad::encode(b"\x00".as_slice()); + // Base case is valid. + assert!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and( + |auth| auth.response.client_data_json == c_data_json.as_bytes() + && auth.response.authenticator_data_and_c_data_hash[..auth_data_len] == auth_data + && auth.response.authenticator_data_and_c_data_hash[auth_data_len..] + == *Sha256::digest(c_data_json.as_bytes()) + && matches!( + auth.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + ) + ); + // `null` `prf`. + drop( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Unknown `clientExtensionResults`. + let mut err = Error::unknown_field("Prf", ["prf"].as_slice()) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "Prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field. + err = Error::duplicate_field("prf").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": null, + \"prf\": null + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `results`. + drop( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": null, + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field in `prf`. + err = Error::duplicate_field("results").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": {{ + \"results\": null, + \"results\": null + }} + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `first`. + err = Error::missing_field("first").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": {}, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `first`. + drop( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `second`. + drop( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null, + "second": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Non-`null` `first`. + err = Error::invalid_type(Unexpected::Option, &"null") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": "" + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Non-`null` `second`. + err = Error::invalid_type(Unexpected::Option, &"null") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null, + "second": "" + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown `prf` field. + err = Error::unknown_field("enabled", ["results"].as_slice()) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown `results` field. + err = Error::unknown_field("Second", ["first", "second"].as_slice()) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null, + "Second": null + } + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field in `results`. + err = Error::duplicate_field("first").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": {{ + \"results\": {{ + \"first\": null, + \"first\": null + }} + }} + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); +} diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; #[cfg(doc)] use super::super::{Challenge, CredentialId}; use super::{ @@ -458,1752 +460,3 @@ pub type NonDiscoverableCustomAuthentication<const USER_LEN: usize> = pub type NonDiscoverableCustomAuthentication64 = CustomAuthentication<USER_HANDLE_MAX_LEN, false>; /// `CustomAuthentication` with an optional `UserHandle16`. pub type NonDiscoverableCustomAuthentication16 = CustomAuthentication<16, false>; -#[cfg(test)] -mod tests { - use super::{ - super::{super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment}, - DiscoverableAuthenticationRelaxed, DiscoverableCustomAuthentication, - NonDiscoverableAuthenticationRelaxed, NonDiscoverableCustomAuthentication, - }; - use rsa::sha2::{Digest as _, Sha256}; - use serde::de::{Error as _, Unexpected}; - use serde_json::Error; - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] - #[expect( - clippy::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[test] - fn eddsa_authentication_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let auth_data: [u8; 37] = [ - // `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`. - 0b0000_0101, - // `signCount`. - 0, - 0, - 0, - 0, - ]; - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(auth_data.as_slice()); - let b64_sig = base64url_nopad::encode([].as_slice()); - let b64_user = base64url_nopad::encode(b"\x00".as_slice()); - // Base case is valid. - assert!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|auth| auth.0.response.client_data_json - == c_data_json.as_bytes() - && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data - && auth.0.response.authenticator_data_and_c_data_hash[37..] - == *Sha256::digest(c_data_json.as_bytes()) - && matches!( - auth.0.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - )) - ); - // `id` and `rawId` mismatch. - let mut err = Error::invalid_value( - Unexpected::Bytes( - base64url_nopad::decode(b"ABABABABABABABABABABAA") - .unwrap() - .as_slice(), - ), - &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "ABABABABABABABABABABAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // missing `id`. - err = Error::missing_field("id").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `id`. - err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": null, - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // missing `rawId`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `rawId`. - err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": null, - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `authenticatorData`. - err = Error::missing_field("authenticatorData") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `authenticatorData`. - err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": null, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `signature`. - err = Error::missing_field("signature").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `signature`. - err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": null, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `userHandle`. - drop( - serde_json::from_str::<NonDiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `userHandle`. - drop( - serde_json::from_str::<NonDiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": null, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `authenticatorAttachment`. - assert!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|auth| matches!( - auth.0.authenticator_attachment, - AuthenticatorAttachment::None - )) - ); - // Unknown `authenticatorAttachment`. - err = Error::invalid_value( - Unexpected::Str("Platform"), - &"'platform' or 'cross-platform'", - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "Platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientDataJSON`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `clientDataJSON`. - err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": null, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `response`. - err = Error::missing_field("response").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `response`. - err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAssertion") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty `response`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": {}, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientExtensionResults`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `clientExtensionResults`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": null, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Missing `type`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `type`. - err = Error::invalid_type(Unexpected::Other("null"), &"public-key") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": null - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Not exactly `public-type` `type`. - err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "Public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null`. - err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!(null).to_string().as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty. - err = Error::missing_field("response").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({}).to_string().as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field in `response`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "foo": true, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field in `response`. - err = Error::duplicate_field("userHandle") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field in `PublicKeyCredential`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": {}, - "type": "public-key", - "foo": true, - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field in `PublicKeyCredential`. - err = Error::duplicate_field("id").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Base case is valid. - assert!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|auth| auth.0.response.client_data_json - == c_data_json.as_bytes() - && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data - && auth.0.response.authenticator_data_and_c_data_hash[37..] - == *Sha256::digest(c_data_json.as_bytes()) - && matches!( - auth.0.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - )) - ); - // missing `id`. - err = Error::missing_field("id").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `id`. - err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": null, - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `authenticatorData`. - err = Error::missing_field("authenticatorData") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "signature": b64_sig, - "userHandle": b64_user, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `authenticatorData`. - err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": null, - "signature": b64_sig, - "userHandle": b64_user, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `signature`. - err = Error::missing_field("signature").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "userHandle": b64_user, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `signature`. - err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": null, - "userHandle": b64_user, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `userHandle`. - drop( - serde_json::from_str::<NonDiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `userHandle`. - drop( - serde_json::from_str::<NonDiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `authenticatorAttachment`. - assert!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "authenticatorAttachment": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|auth| matches!( - auth.0.authenticator_attachment, - AuthenticatorAttachment::None - )) - ); - // Unknown `authenticatorAttachment`. - err = Error::invalid_value( - Unexpected::Str("Platform"), - &"'platform' or 'cross-platform'", - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "authenticatorAttachment": "Platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientDataJSON`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `clientDataJSON`. - err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": null, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty. - err = Error::missing_field("authenticatorData") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({}).to_string().as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientExtensionResults`. - err = Error::missing_field("clientExtensionResults") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `clientExtensionResults`. - err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "clientExtensionResults": null, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - drop( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "clientExtensionResults": {}, - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `type`. - err = Error::invalid_type(Unexpected::Other("null"), &"public-key") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "clientExtensionResults": {}, - "type": null - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Not exactly `public-type` `type`. - err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "clientExtensionResults": {}, - "type": "Public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null`. - err = Error::invalid_type(Unexpected::Other("null"), &"CustomAuthentication") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!(null).to_string().as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field. - err = Error::unknown_field( - "foo", - [ - "authenticatorAttachment", - "authenticatorData", - "clientDataJSON", - "clientExtensionResults", - "id", - "signature", - "type", - "userHandle", - ] - .as_slice(), - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - "foo": true, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field. - err = Error::duplicate_field("userHandle") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\", - \"userHandle\": \"{b64_user}\" - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) - .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")] - #[test] - fn client_extensions() { - let c_data_json = serde_json::json!({}).to_string(); - let auth_data: [u8; 37] = [ - // `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`. - 0b0000_0101, - // `signCount`. - 0, - 0, - 0, - 0, - ]; - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(auth_data.as_slice()); - let b64_sig = base64url_nopad::encode([].as_slice()); - let b64_user = base64url_nopad::encode(b"\x00".as_slice()); - // Base case is valid. - assert!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "authenticatorAttachment": "cross-platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|auth| auth.0.response.client_data_json - == c_data_json.as_bytes() - && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data - && auth.0.response.authenticator_data_and_c_data_hash[37..] - == *Sha256::digest(c_data_json.as_bytes()) - && matches!( - auth.0.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - )) - ); - // `null` `prf`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Unknown `clientExtensionResults`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "Prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field. - let mut err = Error::duplicate_field("prf").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": null, - \"prf\": null - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `results`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": null, - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field in `prf`. - err = Error::duplicate_field("results").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": {{ - \"results\": null, - \"results\": null - }} - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `first`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": {}, - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `first`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `second`. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null, - "second": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Non-`null` `first`. - err = Error::invalid_type(Unexpected::Option, &"null") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": "" - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Non-`null` `second`. - err = Error::invalid_type(Unexpected::Option, &"null") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null, - "second": "" - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `enabled` is still not allowed. - err = Error::unknown_field("enabled", ["results"].as_slice()) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown `prf` field. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "foo": true, - "results": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Unknown `results` field. - drop( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "signature": b64_sig, - "userHandle": b64_user, - }, - "clientExtensionResults": { - "prf": { - "results": { - "first": null, - "Second": null - } - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field in `results`. - err = Error::duplicate_field("first").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"signature\": \"{b64_sig}\", - \"userHandle\": \"{b64_user}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": {{ - \"results\": {{ - \"first\": null, - \"first\": null - }} - }} - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - } -} diff --git a/src/response/auth/ser_relaxed/tests.rs b/src/response/auth/ser_relaxed/tests.rs @@ -0,0 +1,1746 @@ +use super::{ + super::{super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment}, + DiscoverableAuthenticationRelaxed, DiscoverableCustomAuthentication, + NonDiscoverableAuthenticationRelaxed, NonDiscoverableCustomAuthentication, +}; +use rsa::sha2::{Digest as _, Sha256}; +use serde::de::{Error as _, Unexpected}; +use serde_json::Error; +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[expect(clippy::indexing_slicing, reason = "comments justify correctness")] +#[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[test] +fn eddsa_authentication_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let auth_data: [u8; 37] = [ + // `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`. + 0b0000_0101, + // `signCount`. + 0, + 0, + 0, + 0, + ]; + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(auth_data.as_slice()); + let b64_sig = base64url_nopad::encode([].as_slice()); + let b64_user = base64url_nopad::encode(b"\x00".as_slice()); + // Base case is valid. + assert!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and( + |auth| auth.0.response.client_data_json == c_data_json.as_bytes() + && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data + && auth.0.response.authenticator_data_and_c_data_hash[37..] + == *Sha256::digest(c_data_json.as_bytes()) + && matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + ) + ); + // `id` and `rawId` mismatch. + let mut err = Error::invalid_value( + Unexpected::Bytes( + base64url_nopad::decode(b"ABABABABABABABABABABAA") + .unwrap() + .as_slice(), + ), + &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "ABABABABABABABABABABAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // missing `id`. + err = Error::missing_field("id").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `id`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": null, + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // missing `rawId`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `rawId`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": null, + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `authenticatorData`. + err = Error::missing_field("authenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `authenticatorData`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": null, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `signature`. + err = Error::missing_field("signature").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `signature`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": null, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `userHandle`. + drop( + serde_json::from_str::<NonDiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `userHandle`. + drop( + serde_json::from_str::<NonDiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": null, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `authenticatorAttachment`. + assert!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|auth| matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); + // Unknown `authenticatorAttachment`. + err = Error::invalid_value( + Unexpected::Str("Platform"), + &"'platform' or 'cross-platform'", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "Platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientDataJSON`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `clientDataJSON`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": null, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `response`. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `response`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAssertion") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty `response`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": {}, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientExtensionResults`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `clientExtensionResults`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": null, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Missing `type`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `type`. + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": null + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Not exactly `public-type` `type`. + err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "Public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null`. + err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!(null).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({}).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field in `response`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "foo": true, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field in `response`. + err = Error::duplicate_field("userHandle") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field in `PublicKeyCredential`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": {}, + "type": "public-key", + "foo": true, + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field in `PublicKeyCredential`. + err = Error::duplicate_field("id").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Base case is valid. + assert!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and( + |auth| auth.0.response.client_data_json == c_data_json.as_bytes() + && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data + && auth.0.response.authenticator_data_and_c_data_hash[37..] + == *Sha256::digest(c_data_json.as_bytes()) + && matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + ) + ); + // missing `id`. + err = Error::missing_field("id").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `id`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": null, + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `authenticatorData`. + err = Error::missing_field("authenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `authenticatorData`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": null, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `signature`. + err = Error::missing_field("signature").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `signature`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": null, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `userHandle`. + drop( + serde_json::from_str::<NonDiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `userHandle`. + drop( + serde_json::from_str::<NonDiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `authenticatorAttachment`. + assert!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|auth| matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); + // Unknown `authenticatorAttachment`. + err = Error::invalid_value( + Unexpected::Str("Platform"), + &"'platform' or 'cross-platform'", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "authenticatorAttachment": "Platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientDataJSON`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `clientDataJSON`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": null, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty. + err = Error::missing_field("authenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({}).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientExtensionResults`. + err = Error::missing_field("clientExtensionResults") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `clientExtensionResults`. + err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": null, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + drop( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `type`. + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": null + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Not exactly `public-type` `type`. + err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "clientExtensionResults": {}, + "type": "Public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null`. + err = Error::invalid_type(Unexpected::Other("null"), &"CustomAuthentication") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!(null).to_string().as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field. + err = Error::unknown_field( + "foo", + [ + "authenticatorAttachment", + "authenticatorData", + "clientDataJSON", + "clientExtensionResults", + "id", + "signature", + "type", + "userHandle", + ] + .as_slice(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + "foo": true, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field. + err = Error::duplicate_field("userHandle") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\", + \"userHandle\": \"{b64_user}\" + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + }}" + ) + .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")] +#[test] +fn client_extensions() { + let c_data_json = serde_json::json!({}).to_string(); + let auth_data: [u8; 37] = [ + // `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`. + 0b0000_0101, + // `signCount`. + 0, + 0, + 0, + 0, + ]; + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(auth_data.as_slice()); + let b64_sig = base64url_nopad::encode([].as_slice()); + let b64_user = base64url_nopad::encode(b"\x00".as_slice()); + // Base case is valid. + assert!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "authenticatorAttachment": "cross-platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and( + |auth| auth.0.response.client_data_json == c_data_json.as_bytes() + && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data + && auth.0.response.authenticator_data_and_c_data_hash[37..] + == *Sha256::digest(c_data_json.as_bytes()) + && matches!( + auth.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + ) + ); + // `null` `prf`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Unknown `clientExtensionResults`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "Prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field. + let mut err = Error::duplicate_field("prf").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": null, + \"prf\": null + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `results`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": null, + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field in `prf`. + err = Error::duplicate_field("results").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": {{ + \"results\": null, + \"results\": null + }} + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `first`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": {}, + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `first`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `second`. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null, + "second": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Non-`null` `first`. + err = Error::invalid_type(Unexpected::Option, &"null") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": "" + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Non-`null` `second`. + err = Error::invalid_type(Unexpected::Option, &"null") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null, + "second": "" + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `enabled` is still not allowed. + err = Error::unknown_field("enabled", ["results"].as_slice()) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown `prf` field. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "foo": true, + "results": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Unknown `results` field. + drop( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "signature": b64_sig, + "userHandle": b64_user, + }, + "clientExtensionResults": { + "prf": { + "results": { + "first": null, + "Second": null + } + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field in `results`. + err = Error::duplicate_field("first").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"signature\": \"{b64_sig}\", + \"userHandle\": \"{b64_user}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": {{ + \"results\": {{ + \"first\": null, + \"first\": null + }} + }} + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); +} diff --git a/src/response/custom.rs b/src/response/custom.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; use super::{AuthTransports, AuthenticatorTransport, CredentialId, CredentialIdErr}; use core::iter::FusedIterator; impl<'a: 'b, 'b> TryFrom<&'a [u8]> for CredentialId<&'b [u8]> { @@ -132,43 +134,3 @@ impl IntoIterator for AuthTransports { AuthTransportIter(self) } } -#[cfg(test)] -mod tests { - use super::{AuthTransports, AuthenticatorTransport}; - #[test] - fn iter_all() { - let mut iter = AuthTransports::ALL.into_iter(); - assert_eq!(iter.len(), 6); - assert!( - iter.next() - .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Ble)) - ); - assert_eq!(iter.len(), 5); - assert!( - iter.next() - .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Hybrid)) - ); - assert_eq!(iter.len(), 4); - assert!( - iter.next_back() - .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Usb)) - ); - assert_eq!(iter.len(), 3); - assert!( - iter.next() - .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Internal)) - ); - assert_eq!(iter.len(), 2); - assert!( - iter.next_back() - .is_some_and(|tran| matches!(tran, AuthenticatorTransport::SmartCard)) - ); - assert_eq!(iter.len(), 1); - assert!( - iter.next() - .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Nfc)) - ); - assert_eq!(iter.len(), 0); - assert!(iter.next().is_none()); - } -} diff --git a/src/response/custom/tests.rs b/src/response/custom/tests.rs @@ -0,0 +1,37 @@ +use super::{AuthTransports, AuthenticatorTransport}; +#[test] +fn iter_all() { + let mut iter = AuthTransports::ALL.into_iter(); + assert_eq!(iter.len(), 6); + assert!( + iter.next() + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Ble)) + ); + assert_eq!(iter.len(), 5); + assert!( + iter.next() + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Hybrid)) + ); + assert_eq!(iter.len(), 4); + assert!( + iter.next_back() + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Usb)) + ); + assert_eq!(iter.len(), 3); + assert!( + iter.next() + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Internal)) + ); + assert_eq!(iter.len(), 2); + assert!( + iter.next_back() + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::SmartCard)) + ); + assert_eq!(iter.len(), 1); + assert!( + iter.next() + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Nfc)) + ); + assert_eq!(iter.len(), 0); + assert!(iter.next().is_none()); +} diff --git a/src/response/register.rs b/src/response/register.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; #[cfg(feature = "serde_relaxed")] use self::{ super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}, @@ -4153,430 +4155,3 @@ impl PartialEq<DynamicState> for &DynamicState { **self == *other } } -#[cfg(test)] -mod tests { - use super::{ - super::{ - super::{ - AggErr, - request::{AsciiDomain, RpId}, - }, - auth::{AuthenticatorData, NonDiscoverableAuthenticatorAssertion}, - }, - AttestationFormat, AttestationObject, AuthDataContainer as _, AuthExtOutput as _, - AuthTransports, AuthenticatorAttestation, AuthenticatorExtensionOutput, - AuthenticatorExtensionOutputErr, Backup, CborSuccess, CredentialProtectionPolicy, - FourToSixtyThree, FromCbor as _, HmacSecret, Sig, UncompressedPubKey, - cbor::{ - BYTES, BYTES_INFO_24, MAP_1, MAP_2, MAP_3, MAP_4, SIMPLE_FALSE, SIMPLE_TRUE, TEXT_11, - TEXT_12, TEXT_14, - }, - }; - use ed25519_dalek::Verifier as _; - use p256::ecdsa::{DerSignature as P256Sig, SigningKey as P256Key}; - use rsa::sha2::{Digest as _, Sha256}; - #[expect(clippy::panic, reason = "OK in tests")] - #[expect( - clippy::arithmetic_side_effects, - clippy::indexing_slicing, - clippy::missing_asserts_for_indexing, - reason = "comments justifies correctness" - )] - fn hex_decode<const N: usize>(input: &[u8; N]) -> Vec<u8> { - /// Value to subtract from a lowercase hex digit. - const LOWER_OFFSET: u8 = b'a' - 10; - assert_eq!( - N & 1, - 0, - "hex_decode must be passed a reference to an array of even length" - ); - let mut data = Vec::with_capacity(N >> 1); - input.chunks_exact(2).fold((), |(), byte| { - // `byte.len() == 2`. - let mut hex = byte[0]; - let val = match hex { - // `Won't underflow`. - b'0'..=b'9' => hex - b'0', - // `Won't underflow`. - b'a'..=b'f' => hex - LOWER_OFFSET, - _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"), - } << 4u8; - // `byte.len() == 2`. - hex = byte[1]; - data.push( - val | match hex { - // `Won't underflow`. - b'0'..=b'9' => hex - b'0', - // `Won't underflow`. - b'a'..=b'f' => hex - LOWER_OFFSET, - _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"), - }, - ); - }); - data - } - /// <https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-none-es256> - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_in_result, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[test] - fn es256_test_vector() -> Result<(), AggErr> { - let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?); - let credential_private_key = - hex_decode(b"6e68e7a58484a3264f66b77f5d6dc5bc36a47085b615c9727ab334e8c369c2ee"); - let aaguid = hex_decode(b"8446ccb9ab1db374750b2367ff6f3a1f"); - let credential_id = - hex_decode(b"f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4"); - let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c4535513d3d227d"); - let attestation_object = hex_decode(b"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220"); - let key = *P256Key::from_slice(credential_private_key.as_slice()) - .unwrap() - .verifying_key(); - let enc_key = key.to_sec1_point(false); - let auth_attest = - AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0)); - let att_obj = AttestationObject::from_data( - auth_attest.attestation_object_and_c_data_hash.as_slice(), - )?; - assert_eq!( - aaguid, - att_obj.data.auth_data.attested_credential_data.aaguid.0 - ); - assert_eq!( - credential_id, - att_obj - .data - .auth_data - .attested_credential_data - .credential_id - .0 - ); - assert!( - matches!(att_obj.data.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if **enc_key.x().unwrap() == *pub_key.0 && **enc_key.y().unwrap() == *pub_key.1) - ); - assert_eq!( - *att_obj.data.auth_data.rp_id_hash, - *Sha256::digest(rp_id.as_ref()) - ); - assert!(att_obj.data.auth_data.flags.user_present); - assert!(matches!(att_obj.data.attestation, AttestationFormat::None)); - let authenticator_data = hex_decode( - b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000", - ); - let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d"); - let signature = hex_decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87"); - let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user( - client_data_json_2, - authenticator_data, - signature, - ); - let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; - assert_eq!(*auth_data.rp_id_hash(), *Sha256::digest(rp_id.as_ref())); - assert!(auth_data.flags().user_present); - assert!(match att_obj.data.auth_data.flags.backup { - Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible), - Backup::Eligible => !matches!(auth_data.flags().backup, Backup::NotEligible), - Backup::Exists => matches!(auth_data.flags().backup, Backup::Exists), - }); - let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap(); - let mut msg = auth_assertion.authenticator_data().to_owned(); - msg.extend_from_slice(&Sha256::digest(auth_assertion.client_data_json())); - key.verify(msg.as_slice(), &sig).unwrap(); - Ok(()) - } - /// <https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-packed-self-es256> - #[expect( - clippy::panic_in_result_fn, - clippy::unwrap_in_result, - clippy::unwrap_used, - reason = "OK in tests" - )] - #[expect(clippy::indexing_slicing, reason = "comment justifies correctness")] - #[test] - fn es256_self_attest_test_vector() -> Result<(), AggErr> { - let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?); - let credential_private_key = - hex_decode(b"b4bbfa5d68e1693b6ef5a19a0e60ef7ee2cbcac81f7fec7006ac3a21e0c5116a"); - let aaguid = hex_decode(b"df850e09db6afbdfab51697791506cfc"); - let credential_id = - hex_decode(b"455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c"); - let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f3078594856673d3d227d"); - let attestation_object = hex_decode(b"a363666d74667061636b65646761747453746d74a263616c67266373696758483046022100ae045923ded832b844cae4d5fc864277c0dc114ad713e271af0f0d371bd3ac540221009077a088ed51a673951ad3ba2673d5029bab65b64f4ea67b234321f86fcfac5d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2"); - let key = *P256Key::from_slice(credential_private_key.as_slice()) - .unwrap() - .verifying_key(); - let enc_key = key.to_sec1_point(false); - let auth_attest = - AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0)); - let (att_obj, auth_idx) = AttestationObject::parse_data(auth_attest.attestation_object())?; - assert_eq!(aaguid, att_obj.auth_data.attested_credential_data.aaguid.0); - assert_eq!( - credential_id, - att_obj.auth_data.attested_credential_data.credential_id.0 - ); - assert!( - matches!(att_obj.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if **enc_key.x().unwrap() == *pub_key.0 && **enc_key.y().unwrap() == *pub_key.1) - ); - assert_eq!( - *att_obj.auth_data.rp_id_hash, - *Sha256::digest(rp_id.as_ref()) - ); - assert!(att_obj.auth_data.flags.user_present); - assert!(match att_obj.attestation { - AttestationFormat::None => false, - AttestationFormat::Packed(attest) => { - match attest.signature { - 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( - // Won't `panic` since `auth_idx` is returned from `AttestationObject::parse_data`. - &auth_attest.attestation_object_and_c_data_hash[auth_idx..], - &s, - ) - .is_ok() - } - } - } - }); - let authenticator_data = hex_decode( - b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000", - ); - let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d"); - let signature = hex_decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744"); - let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user( - client_data_json_2, - authenticator_data, - signature, - ); - let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; - assert_eq!(*auth_data.rp_id_hash(), *Sha256::digest(rp_id.as_ref())); - assert!(auth_data.flags().user_present); - assert!(match att_obj.auth_data.flags.backup { - Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible), - Backup::Eligible | Backup::Exists => - !matches!(auth_data.flags().backup, Backup::NotEligible), - }); - let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap(); - let mut msg = auth_assertion.authenticator_data().to_owned(); - msg.extend_from_slice(&Sha256::digest(auth_assertion.client_data_json())); - key.verify(msg.as_slice(), &sig).unwrap(); - Ok(()) - } - struct AuthExtOptions<'a> { - cred_protect: Option<u8>, - hmac_secret: Option<bool>, - min_pin_length: Option<u8>, - hmac_secret_mc: Option<&'a [u8]>, - } - #[expect( - clippy::panic, - clippy::unreachable, - reason = "want to crash when there is a bug" - )] - #[expect( - clippy::arithmetic_side_effects, - clippy::as_conversions, - clippy::cast_possible_truncation, - reason = "comments justify correctness" - )] - fn generate_auth_extensions(opts: &AuthExtOptions<'_>) -> Vec<u8> { - // Maxes at 4, so addition is clearly free from overflow. - let map_len = u8::from(opts.cred_protect.is_some()) - + u8::from(opts.hmac_secret.is_some()) - + u8::from(opts.min_pin_length.is_some()) - + u8::from(opts.hmac_secret_mc.is_some()); - let header = match map_len { - 0 => return Vec::new(), - 1 => MAP_1, - 2 => MAP_2, - 3 => MAP_3, - 4 => MAP_4, - _ => unreachable!("bug"), - }; - let mut cbor = Vec::with_capacity(128); - cbor.push(header); - if let Some(protect) = opts.cred_protect { - cbor.push(TEXT_11); - cbor.extend_from_slice(b"credProtect".as_slice()); - if protect >= 24 { - cbor.push(24); - } - cbor.push(protect); - } - if let Some(hmac) = opts.hmac_secret { - cbor.push(TEXT_11); - cbor.extend_from_slice(b"hmac-secret".as_slice()); - cbor.push(if hmac { SIMPLE_TRUE } else { SIMPLE_FALSE }); - } - if let Some(pin) = opts.min_pin_length { - cbor.push(TEXT_12); - cbor.extend_from_slice(b"minPinLength".as_slice()); - if pin >= 24 { - cbor.push(24); - } - cbor.push(pin); - } - if let Some(mc) = opts.hmac_secret_mc { - cbor.push(TEXT_14); - cbor.extend_from_slice(b"hmac-secret-mc".as_slice()); - match mc.len() { - len @ ..=23 => { - // `as` is clearly OK. - cbor.push(BYTES | len as u8); - } - len @ 24..=255 => { - cbor.push(BYTES_INFO_24); - // `as` is clearly OK. - cbor.push(len as u8); - } - _ => panic!( - "AuthExtOptions does not allow hmac_secret_mc to have length greater than 255" - ), - } - cbor.extend_from_slice(mc); - } - cbor - } - #[expect(clippy::panic_in_result_fn, reason = "not a problem for a test")] - #[expect(clippy::shadow_unrelated, reason = "struct destructuring is prefered")] - #[expect(clippy::too_many_lines, reason = "a lot to test")] - #[test] - fn auth_ext() -> Result<(), AuthenticatorExtensionOutputErr> { - let mut opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: None, - hmac_secret: None, - min_pin_length: None, - hmac_secret_mc: None, - }); - let CborSuccess { value, remaining } = - AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; - assert!(remaining.is_empty()); - assert!(value.missing()); - opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: None, - hmac_secret: None, - min_pin_length: None, - hmac_secret_mc: Some([0; 48].as_slice()), - }); - assert!( - AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( - |e| matches!(e, AuthenticatorExtensionOutputErr::Missing), - |_| false, - ) - ); - opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: None, - hmac_secret: Some(true), - min_pin_length: None, - hmac_secret_mc: Some([0; 48].as_slice()), - }); - let CborSuccess { value, remaining } = - AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; - assert!(remaining.is_empty()); - assert!( - matches!(value.cred_protect, CredentialProtectionPolicy::None) - && matches!(value.hmac_secret, HmacSecret::One) - && value.min_pin_length.is_none() - ); - opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: None, - hmac_secret: Some(false), - min_pin_length: None, - hmac_secret_mc: Some([0; 48].as_slice()), - }); - assert!( - AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( - |e| matches!(e, AuthenticatorExtensionOutputErr::Missing), - |_| false, - ) - ); - opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: None, - hmac_secret: Some(true), - min_pin_length: None, - hmac_secret_mc: Some([0; 49].as_slice()), - }); - assert!( - AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( - |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcValue), - |_| false, - ) - ); - opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: None, - hmac_secret: Some(true), - min_pin_length: None, - hmac_secret_mc: Some([0; 23].as_slice()), - }); - assert!( - AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( - |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcType), - |_| false, - ) - ); - opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: Some(1), - hmac_secret: Some(true), - min_pin_length: Some(5), - hmac_secret_mc: Some([0; 48].as_slice()), - }); - let CborSuccess { value, remaining } = - AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; - assert!(remaining.is_empty()); - assert!( - matches!( - value.cred_protect, - CredentialProtectionPolicy::UserVerificationOptional - ) && matches!(value.hmac_secret, HmacSecret::One) - && value - .min_pin_length - .is_some_and(|pin| pin == FourToSixtyThree::Five) - ); - opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: Some(0), - hmac_secret: None, - min_pin_length: None, - hmac_secret_mc: None, - }); - assert!( - AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( - |e| matches!(e, AuthenticatorExtensionOutputErr::CredProtectValue), - |_| false, - ) - ); - opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: None, - hmac_secret: None, - min_pin_length: Some(3), - hmac_secret_mc: None, - }); - assert!( - AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( - |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue), - |_| false, - ) - ); - opts = generate_auth_extensions(&AuthExtOptions { - cred_protect: None, - hmac_secret: None, - min_pin_length: Some(64), - hmac_secret_mc: None, - }); - assert!( - AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( - |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue), - |_| false, - ) - ); - Ok(()) - } -} diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; use super::{ super::{ super::request::register::CoseAlgorithmIdentifier, @@ -1678,11350 +1680,3 @@ impl<'de> Deserialize<'de> for Registration { }) } } -#[cfg(test)] -mod tests { - use super::{ - super::{ - 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::{ - PublicKey as P256PubKey, Sec1Point as P256Pt, SecretKey as P256Key, - elliptic_curve::sec1::{FromSec1Point as _, ToSec1Point as _}, - }; - use p384::{PublicKey as P384PubKey, Sec1Point as P384Pt, SecretKey as P384Key}; - use rsa::{ - BoxedUint, RsaPrivateKey, - sha2::{Digest as _, Sha256}, - traits::PublicKeyParts as _, - }; - use serde::de::{Error as _, Unexpected}; - 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( - VerifyingKey::from_bytes(&[1; 32]) - .unwrap() - .to_public_key_der() - .unwrap() - .as_bytes() - ) - .is_ok_and(|k| k.0 == [1; 32]) - ); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[test] - fn p256_spki() { - let key = P256Key::from_bytes( - &[ - 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, - 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, - ] - .into(), - ) - .unwrap() - .public_key(); - let enc_key = key.to_sec1_point(false); - assert!( - UncompressedP256PubKey::from_der(key.to_public_key_der().unwrap().as_bytes()) - .is_ok_and(|k| *k.0 == **enc_key.x().unwrap() && *k.1 == **enc_key.y().unwrap()) - ); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[test] - fn p384_spki() { - let key = P384Key::from_bytes( - &[ - 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, - 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, - 32, 86, 220, 68, 182, 11, 105, 223, 75, 70, - ] - .into(), - ) - .unwrap() - .public_key(); - let enc_key = key.to_sec1_point(false); - assert!( - UncompressedP384PubKey::from_der(key.to_public_key_der().unwrap().as_bytes()) - .is_ok_and(|k| *k.0 == **enc_key.x().unwrap() && *k.1 == **enc_key.y().unwrap()) - ); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[test] - fn rsa_spki() { - let n = [ - 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, - 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, - 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, - 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, - 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, - 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, - 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, - 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, - 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, - 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, - 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, - 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, - 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, - 153, 79, 0, 133, 78, 7, 218, 165, 241, - ]; - let e = 0x0001_0001u32; - let d = [ - 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, - 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, - 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, - 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, - 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, - 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, - 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, - 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, - 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, - 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, - 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, - 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, - 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, - 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, - ]; - let p = BoxedUint::from_le_slice_vartime( - [ - 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, - 69, 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, - 250, 195, 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, - 99, 76, 240, 14, 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, - 82, 98, 233, 236, 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, - 147, 179, 30, 62, 247, 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, - 168, 253, 228, 214, 54, 16, 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, - 24, 247, - ] - .as_slice(), - ); - let p_2 = BoxedUint::from_le_slice_vartime( - [ - 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, - 85, 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, - 141, 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, - 39, 85, 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, - 250, 187, 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, - 112, 211, 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, - 173, 9, 236, 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, - 250, - ] - .as_slice(), - ); - let key = RsaPrivateKey::from_components( - BoxedUint::from_le_slice_vartime(n.as_slice()), - e.into(), - BoxedUint::from_le_slice_vartime(d.as_slice()), - vec![p, p_2], - ) - .unwrap() - .to_public_key(); - assert!( - RsaPubKey::from_der(key.to_public_key_der().unwrap().as_bytes()) - .is_ok_and(|k| *k.0 == *key.n().to_be_bytes() && BoxedUint::from(k.1) == *key.e()) - ); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] - #[expect( - clippy::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[test] - fn eddsa_registration_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let att_obj: [u8; 143] = [ - 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_24, - 115, - // `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, - // Ed25519 COSE key. - cbor::MAP_4, - KTY, - OKP, - ALG, - EDDSA, - // `crv`. - cbor::NEG_ONE, - // `Ed25519`. - cbor::SIX, - // `x`. - cbor::NEG_TWO, - cbor::BYTES_INFO_24, - 32, - // Compressed y-coordinate. - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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 = VerifyingKey::from_bytes(&[1; 32]) - .unwrap() - .to_public_key_der() - .unwrap(); - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let att_obj_len = att_obj.len(); - let auth_data_start = att_obj_len - 113; - let b64_adata = base64url_nopad::encode(&att_obj[auth_data_start..]); - 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": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], - "publicKey": b64_key, - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "authenticatorAttachment": "cross-platform", - "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.count() == 6 - && matches!( - reg.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - ) - && reg.client_extension_results.cred_props.is_none() - && reg.client_extension_results.prf.is_none() - ) - ); - // `id` and `rawId` mismatch. - let mut err = Error::invalid_value( - Unexpected::Bytes( - base64url_nopad::decode(b"ABABABABABABABABABABAA") - .unwrap() - .as_slice(), - ), - &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "ABABABABABABABABABABAA", - "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 `id`. - err = Error::missing_field("id").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "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()) - ); - // `null` `id`. - err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": null, - "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 `rawId`. - err = Error::missing_field("rawId").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "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()) - ); - // `null` `rawId`. - err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": null, - "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()) - ); - // `id` and the credential id in authenticator data mismatch. - err = Error::invalid_value( - Unexpected::Bytes( - base64url_nopad - ::decode(b"ABABABABABABABABABABAA") - .unwrap() - .as_slice(), - ), - &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", [0u8; 16]).as_str(), - ) - .to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "ABABABABABABABABABABAA", - "rawId": "ABABABABABABABABABABAA", - "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()) - ); - // `authenticatorData` mismatches `authData` in attestation object. - let mut bad_auth = [0; 113]; - bad_auth.copy_from_slice(&att_obj[auth_data_start..]); - bad_auth[113 - 32..].copy_from_slice([0; 32].as_slice()); - err = Error::invalid_value( - Unexpected::Bytes(bad_auth.as_slice()), - &format!("authenticator data to match the authenticator data portion of attestation object: {:?}", &att_obj[att_obj_len - bad_auth.len()..]).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": base64url_nopad::encode(bad_auth.as_slice()), - "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 `authenticatorData`. - err = Error::missing_field("authenticatorData") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "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()) - ); - // `null` `authenticatorData`. - err = Error::invalid_type(Unexpected::Other("null"), &"authenticatorData") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "transports": [], - "authenticatorData": null, - "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()) - ); - // `publicKeyAlgorithm` mismatch. - 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::Eddsa).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": -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()) - ); - // 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. - err = Error::invalid_value( - Unexpected::Bytes([0; 32].as_slice()), - &format!( - "DER-encoded public key to match the public key within the attestation object: Ed25519(Ed25519PubKey({:?}))", - &att_obj[att_obj.len() - 32..], - ) - .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(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()), - "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 `publicKey` when using EdDSA, ES256, or RS256. - err = Error::missing_field("publicKey").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": -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()) - ); - // `null` `publicKey` when using EdDSA, ES256, or RS256. - err = Error::invalid_type(Unexpected::Other("null"), &"publicKey") - .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": -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 `transports`. - err = Error::missing_field("transports").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, - "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()) - ); - // Duplicate `transports` are allowed. - assert!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "transports": ["usb", "usb"], - "publicKey": b64_key, - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.response.transports.count() == 1) - ); - // `null` `transports`. - err = Error::invalid_type(Unexpected::Other("null"), &"transports") - .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": null, - "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()) - ); - // Unknown `transports`. - err = Error::invalid_value( - Unexpected::Str("Usb"), - &"'ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'", - ) - .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": ["Usb"], - "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()) - ); - // `null` `authenticatorAttachment`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "authenticatorAttachment": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)) - ); - // Unknown `authenticatorAttachment`. - err = Error::invalid_value( - Unexpected::Str("Platform"), - &"'platform' or 'cross-platform'", - ) - .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, - }, - "authenticatorAttachment": "Platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientDataJSON`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "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()) - ); - // `null` `clientDataJSON`. - err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": null, - "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 `attestationObject`. - err = Error::missing_field("attestationObject") - .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, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `attestationObject`. - err = Error::invalid_type( - Unexpected::Other("null"), - &"base64url-encoded attestation object", - ) - .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": null, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `response`. - err = Error::missing_field("response").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `response`. - err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty `response`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": {}, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientExtensionResults`. - err = Error::missing_field("clientExtensionResults") - .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, - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `clientExtensionResults`. - err = Error::invalid_type( - Unexpected::Other("null"), - &"clientExtensionResults to be a map of allowed client extensions", - ) - .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": null, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `type`. - err = Error::missing_field("type").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": {}, - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `type`. - err = Error::invalid_type(Unexpected::Other("null"), &"public-key") - .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": null - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Not exactly `public-type` `type`. - err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") - .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()) - ); - // `null`. - err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>(serde_json::json!(null).to_string().as_str()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty. - err = Error::missing_field("response").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>(serde_json::json!({}).to_string().as_str()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field in `response`. - err = Error::unknown_field( - "foo", - [ - "clientDataJSON", - "attestationObject", - "authenticatorData", - "transports", - "publicKey", - "publicKeyAlgorithm", - ] - .as_slice(), - ) - .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, - "foo": true, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field in `response`. - err = Error::duplicate_field("transports") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\", - \"transports\": [] - }}, - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field in `PublicKeyCredential`. - err = Error::unknown_field( - "foo", - [ - "id", - "type", - "rawId", - "response", - "authenticatorAttachment", - "clientExtensionResults", - ] - .as_slice(), - ) - .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", - "foo": true, - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field in `PublicKeyCredential`. - err = Error::duplicate_field("id").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) - .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::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[test] - fn client_extensions() { - let c_data_json = serde_json::json!({}).to_string(); - let att_obj: [u8; 143] = [ - 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_24, - 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, - // Ed25519 COSE key. - cbor::MAP_4, - KTY, - OKP, - ALG, - EDDSA, - // `crv`. - cbor::NEG_ONE, - // `Ed25519`. - cbor::SIX, - // `x`. - cbor::NEG_TWO, - cbor::BYTES_INFO_24, - 32, - // Compressed y-coordinate. - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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 = VerifyingKey::from_bytes(&[1; 32]) - .unwrap() - .to_public_key_der() - .unwrap(); - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 113..]); - 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": -8i8, - "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() - ) - ); - // `null` `credProps`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() - && reg.client_extension_results.prf.is_none()) - ); - // `null` `prf`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() - && reg.client_extension_results.prf.is_none()) - ); - // Unknown `clientExtensionResults`. - let mut err = Error::unknown_field("CredProps", ["credProps", "prf"].as_slice()) - .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": { - "CredProps": { - "rk": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field. - err = Error::duplicate_field("credProps").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{ - \"credProps\": null, - \"credProps\": null - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `rk`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg - .client_extension_results - .cred_props - .is_some_and(|props| props.rk.is_none()) - && reg.client_extension_results.prf.is_none()) - ); - // Missing `rk`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": {} - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg - .client_extension_results - .cred_props - .is_some_and(|props| props.rk.is_none()) - && reg.client_extension_results.prf.is_none()) - ); - // `true` rk`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg - .client_extension_results - .cred_props - .is_some_and(|props| props.rk.unwrap_or_default()) - && reg.client_extension_results.prf.is_none()) - ); - // `false` rk`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": false - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg - .client_extension_results - .cred_props - .is_some_and(|props| props.rk.is_some_and(|rk| !rk)) - && reg.client_extension_results.prf.is_none()) - ); - // Invalid `rk`. - err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") - .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": { - "credProps": { - "rk": 3u8 - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown `credProps` field. - err = Error::unknown_field("Rk", ["rk"].as_slice()) - .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": { - "credProps": { - "Rk": true, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field in `credProps`. - err = Error::duplicate_field("rk").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{ - \"credProps\": {{ - \"rk\": true, - \"rk\": true - }} - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `enabled`. - err = Error::invalid_type(Unexpected::Other("null"), &"a boolean") - .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": { - "prf": { - "enabled": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `enabled`. - err = Error::missing_field("enabled").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": { - "prf": {} - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `true` `enabled`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() - && reg - .client_extension_results - .prf - .is_some_and(|prf| prf.enabled)) - ); - // `false` `enabled`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": false, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() - && reg - .client_extension_results - .prf - .is_some_and(|prf| !prf.enabled)) - ); - // Invalid `enabled`. - err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") - .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": { - "prf": { - "enabled": 3u8 - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `results` with `enabled` `true`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": null, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() - && reg - .client_extension_results - .prf - .is_some_and(|prf| prf.enabled)) - ); - // `null` `results` with `enabled` `false`. - err = Error::custom( - "prf must not have 'results', including a null 'results', if 'enabled' is false", - ) - .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": { - "prf": { - "enabled": false, - "results": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field in `prf`. - err = Error::duplicate_field("enabled").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": {{ - \"enabled\": true, - \"enabled\": true - }} - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `first`. - err = Error::missing_field("first").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": { - "prf": { - "enabled": true, - "results": {}, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `first`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() - && reg - .client_extension_results - .prf - .is_some_and(|prf| prf.enabled)) - ); - // `null` `second`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null, - "second": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() - && reg - .client_extension_results - .prf - .is_some_and(|prf| prf.enabled)) - ); - // Non-`null` `first`. - err = Error::invalid_type(Unexpected::Option, &"null") - .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": { - "prf": { - "enabled": true, - "results": { - "first": "" - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Non-`null` `second`. - err = Error::invalid_type(Unexpected::Option, &"null") - .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": { - "prf": { - "enabled": true, - "results": { - "first": null, - "second": "" - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown `prf` field. - err = Error::unknown_field("Results", ["enabled", "results"].as_slice()) - .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": { - "prf": { - "enabled": true, - "Results": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown `results` field. - err = Error::unknown_field("Second", ["first", "second"].as_slice()) - .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": { - "prf": { - "enabled": true, - "results": { - "first": null, - "Second": null - } - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field in `results`. - err = Error::duplicate_field("first").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<Registration>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": {{ - \"enabled\": true, - \"results\": {{ - \"first\": null, - \"first\": null - }} - }} - }}, - \"type\": \"public-key\" - }}" - ) - .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 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")] - #[test] - fn es256_registration_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let mut att_obj: [u8; 178] = [ - 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_24, - 148, - // `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, - // P-256 COSE key. - cbor::MAP_5, - KTY, - EC2, - ALG, - ES256, - // `crv`. - cbor::NEG_ONE, - // `P-256`. - cbor::ONE, - // `x`. - cbor::NEG_TWO, - cbor::BYTES_INFO_24, - 32, - // x-coordinate. 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, - // `y`. - cbor::NEG_THREE, - cbor::BYTES_INFO_24, - 32, - // y-coordinate. 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, - ]; - let key = P256Key::from_bytes( - &[ - 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, - 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, - ] - .into(), - ) - .unwrap() - .public_key(); - let enc_key = key.to_sec1_point(false); - let pub_key = key.to_public_key_der().unwrap(); - let att_obj_len = att_obj.len(); - let x_start = att_obj_len - 67; - let y_meta_start = x_start + 32; - let y_start = y_meta_start + 3; - att_obj[x_start..y_meta_start].copy_from_slice(enc_key.x().unwrap()); - att_obj[y_start..].copy_from_slice(enc_key.y().unwrap()); - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 148..]); - 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": -7i8, - "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::Es256).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 = P256PubKey::from_sec1_point(&P256Pt::from_affine_coordinates( - &[ - 66, 71, 188, 41, 125, 2, 226, 44, 148, 62, 63, 190, 172, 64, 33, 214, 6, 37, 148, - 23, 240, 235, 203, 84, 112, 219, 232, 197, 54, 182, 17, 235, - ] - .into(), - &[ - 22, 172, 123, 13, 170, 242, 217, 248, 193, 209, 206, 163, 92, 4, 162, 168, 113, 63, - 2, 117, 16, 223, 239, 196, 109, 179, 10, 130, 43, 213, 205, 92, - ] - .into(), - false, - )) - .unwrap(); - err = Error::invalid_value( - Unexpected::Bytes([0; 32].as_slice()), - &format!( - "DER-encoded public key to match the public key within the attestation object: P256(UncompressedP256PubKey({:?}, {:?}))", - &att_obj[x_start..y_meta_start], - &att_obj[y_start..], - ) - .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": -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()) - ); - // Missing `publicKey` when using EdDSA, ES256, or RS256. - err = Error::missing_field("publicKey").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` when using EdDSA, ES256, or RS256. - err = Error::invalid_type(Unexpected::Other("null"), &"publicKey") - .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 es384_registration_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let mut att_obj: [u8; 211] = [ - 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_24, - 181, - // `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, - // P-384 COSE key. - cbor::MAP_5, - KTY, - EC2, - ALG, - cbor::NEG_INFO_24, - ES384, - // `crv`. - cbor::NEG_ONE, - // `P-384`. - cbor::TWO, - // `x`. - cbor::NEG_TWO, - cbor::BYTES_INFO_24, - 48, - // x-coordinate. 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // `y`. - cbor::NEG_THREE, - cbor::BYTES_INFO_24, - 48, - // y-coordinate. 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ]; - let key = P384Key::from_bytes( - &[ - 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, - 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, - 32, 86, 220, 68, 182, 11, 105, 223, 75, 70, - ] - .into(), - ) - .unwrap() - .public_key(); - let enc_key = key.to_sec1_point(false); - let pub_key = key.to_public_key_der().unwrap(); - let att_obj_len = att_obj.len(); - let x_start = att_obj_len - 99; - let y_meta_start = x_start + 48; - let y_start = y_meta_start + 3; - att_obj[x_start..y_meta_start].copy_from_slice(enc_key.x().unwrap()); - att_obj[y_start..].copy_from_slice(enc_key.y().unwrap()); - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 181..]); - 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": -35i8, - "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::Es256).as_str()), - &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).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": -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()) - ); - // 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 = P384PubKey::from_sec1_point(&P384Pt::from_affine_coordinates( - &[ - 192, 10, 27, 46, 66, 67, 80, 98, 33, 230, 156, 95, 1, 135, 150, 110, 64, 243, 22, - 118, 5, 255, 107, 44, 234, 111, 217, 105, 125, 114, 39, 7, 126, 2, 191, 111, 48, - 93, 234, 175, 18, 172, 59, 28, 97, 106, 178, 152, - ] - .into(), - &[ - 57, 36, 196, 12, 109, 129, 253, 115, 88, 154, 6, 43, 195, 85, 169, 5, 230, 51, 28, - 205, 142, 28, 150, 35, 24, 222, 170, 253, 14, 248, 84, 151, 109, 191, 152, 111, - 222, 70, 134, 247, 109, 171, 211, 33, 214, 217, 200, 111, - ] - .into(), - false, - )) - .unwrap(); - err = Error::invalid_value( - Unexpected::Bytes([0; 32].as_slice()), - &format!( - "DER-encoded public key to match the public key within the attestation object: P384(UncompressedP384PubKey({:?}, {:?}))", - &att_obj[x_start..y_meta_start], - &att_obj[y_start..], - ) - .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": -35i8, - "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": -35i8, - "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::Es384).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": -35i8, - "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::Es384).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")] - #[test] - fn rs256_registration_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let mut att_obj: [u8; 374] = [ - 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, - 1, - 87, - // `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, - // RSA COSE key. - cbor::MAP_4, - KTY, - RSA, - ALG, - cbor::NEG_INFO_25, - // RS256. - 1, - 0, - // `n`. - cbor::NEG_ONE, - cbor::BYTES_INFO_25, - 1, - 0, - // n. 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // `e`. - cbor::NEG_TWO, - cbor::BYTES | 3, - // e. - 1, - 0, - 1, - ]; - let n = [ - 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, - 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, - 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, - 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, - 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, - 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, - 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, - 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, - 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, - 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, - 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, - 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, - 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, - 153, 79, 0, 133, 78, 7, 218, 165, 241, - ]; - let e = 0x0001_0001u32; - let d = [ - 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, - 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, - 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, - 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, - 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, - 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, - 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, - 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, - 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, - 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, - 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, - 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, - 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, - 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, - ]; - let p = BoxedUint::from_le_slice_vartime( - [ - 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, - 69, 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, - 250, 195, 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, - 99, 76, 240, 14, 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, - 82, 98, 233, 236, 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, - 147, 179, 30, 62, 247, 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, - 168, 253, 228, 214, 54, 16, 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, - 24, 247, - ] - .as_slice(), - ); - let p_2 = BoxedUint::from_le_slice_vartime( - [ - 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, - 85, 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, - 141, 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, - 39, 85, 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, - 250, 187, 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, - 112, 211, 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, - 173, 9, 236, 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, - 250, - ] - .as_slice(), - ); - let key = RsaPrivateKey::from_components( - BoxedUint::from_le_slice_vartime(n.as_slice()), - e.into(), - BoxedUint::from_le_slice_vartime(d.as_slice()), - vec![p, p_2], - ) - .unwrap() - .to_public_key(); - let pub_key = key.to_public_key_der().unwrap(); - let att_obj_len = att_obj.len(); - let n_start_idx = att_obj_len - 261; - let e_meta_start_idx = n_start_idx + 256; - // Correct and won't `panic`. - att_obj[n_start_idx..e_meta_start_idx] - .copy_from_slice(key.n().to_be_bytes_trimmed_vartime().as_ref()); - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - // Won't `panic`. - let b64_adata = base64url_nopad::encode(&att_obj[31..]); - 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": -257i16, - "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::Rs256).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 = RsaPrivateKey::from_components( - BoxedUint::from_le_slice_vartime( - [ - 175, 161, 161, 75, 52, 244, 72, 168, 29, 119, 33, 120, 3, 222, 231, 152, 222, - 119, 112, 83, 221, 237, 74, 174, 79, 216, 147, 251, 245, 94, 234, 114, 254, 21, - 17, 254, 8, 115, 75, 127, 150, 87, 59, 109, 230, 116, 85, 90, 11, 160, 63, 217, - 9, 38, 187, 250, 226, 183, 38, 164, 182, 218, 22, 19, 58, 189, 83, 219, 11, - 144, 15, 99, 151, 166, 46, 57, 17, 111, 189, 131, 142, 113, 85, 122, 188, 238, - 52, 21, 116, 125, 102, 195, 182, 165, 29, 156, 213, 182, 125, 156, 88, 56, 221, - 2, 98, 43, 210, 115, 32, 4, 105, 88, 181, 158, 207, 236, 162, 250, 253, 240, - 72, 8, 253, 50, 220, 247, 76, 170, 143, 68, 225, 231, 113, 64, 244, 17, 138, - 162, 233, 33, 2, 67, 11, 223, 188, 232, 152, 193, 20, 32, 243, 52, 64, 43, 2, - 243, 8, 77, 150, 232, 109, 148, 95, 127, 55, 71, 162, 34, 54, 83, 135, 52, 172, - 191, 32, 42, 106, 43, 211, 206, 100, 104, 110, 232, 5, 43, 120, 180, 166, 40, - 144, 233, 239, 103, 134, 103, 255, 224, 138, 184, 208, 137, 127, 36, 189, 143, - 248, 201, 2, 218, 51, 232, 96, 30, 83, 124, 109, 241, 23, 179, 247, 151, 238, - 212, 204, 44, 43, 223, 148, 241, 172, 10, 235, 155, 94, 68, 116, 24, 116, 191, - 86, 53, 127, 35, 133, 198, 204, 59, 76, 110, 16, 1, 15, 148, 135, 157, - ] - .as_slice(), - ), - 0x0001_0001u32.into(), - BoxedUint::from_le_slice_vartime( - [ - 129, 93, 123, 251, 104, 29, 84, 203, 116, 100, 75, 237, 111, 160, 12, 100, 172, - 76, 57, 178, 144, 235, 81, 61, 115, 243, 28, 40, 183, 22, 56, 150, 68, 38, 220, - 62, 233, 110, 48, 174, 35, 197, 244, 109, 148, 109, 36, 69, 69, 82, 225, 113, - 175, 6, 239, 27, 193, 101, 50, 239, 122, 102, 7, 46, 98, 79, 195, 116, 155, - 158, 138, 147, 51, 93, 24, 237, 246, 82, 14, 109, 144, 250, 239, 93, 63, 214, - 96, 130, 226, 134, 198, 145, 161, 11, 231, 97, 214, 180, 255, 95, 158, 88, 108, - 254, 243, 177, 133, 184, 92, 95, 148, 88, 55, 124, 245, 244, 84, 86, 4, 121, - 44, 231, 97, 176, 190, 29, 155, 40, 57, 69, 165, 80, 168, 9, 56, 43, 233, 6, - 14, 157, 112, 223, 64, 88, 141, 7, 65, 23, 64, 208, 6, 83, 61, 8, 182, 248, - 126, 84, 179, 163, 80, 238, 90, 133, 4, 14, 71, 177, 175, 27, 29, 151, 211, - 108, 162, 195, 7, 157, 167, 86, 169, 3, 87, 235, 89, 158, 237, 216, 31, 243, - 197, 62, 5, 84, 131, 230, 186, 248, 49, 12, 93, 244, 61, 135, 180, 17, 162, - 241, 13, 115, 241, 138, 219, 98, 155, 166, 191, 63, 12, 37, 1, 165, 178, 84, - 200, 72, 80, 41, 77, 136, 217, 141, 246, 209, 31, 243, 159, 71, 43, 246, 159, - 182, 171, 116, 12, 3, 142, 235, 218, 164, 70, 90, 147, 238, 42, 75, - ] - .as_slice(), - ), - vec![ - BoxedUint::from_le_slice_vartime( - [ - 215, 199, 110, 28, 64, 16, 16, 109, 106, 152, 150, 124, 52, 166, 121, 92, - 242, 13, 0, 69, 7, 152, 72, 172, 118, 63, 156, 180, 140, 39, 53, 29, 197, - 224, 177, 48, 41, 221, 102, 65, 17, 185, 55, 62, 219, 152, 227, 7, 78, 219, - 14, 139, 71, 204, 144, 152, 14, 39, 247, 244, 165, 224, 234, 60, 213, 74, - 237, 30, 102, 177, 242, 138, 168, 31, 122, 47, 206, 155, 225, 113, 103, - 175, 152, 244, 27, 233, 112, 223, 248, 38, 215, 178, 20, 244, 8, 121, 26, - 11, 70, 122, 16, 85, 167, 87, 64, 216, 228, 227, 173, 57, 250, 8, 221, 38, - 12, 203, 212, 1, 112, 43, 72, 91, 225, 97, 228, 57, 154, 193, - ] - .as_slice(), - ), - BoxedUint::from_le_slice_vartime( - [ - 233, 89, 204, 152, 31, 242, 8, 110, 38, 190, 111, 159, 105, 105, 45, 85, - 15, 244, 30, 250, 174, 226, 219, 111, 107, 191, 196, 135, 17, 123, 186, - 167, 85, 13, 120, 197, 159, 129, 78, 237, 152, 31, 230, 26, 229, 253, 197, - 211, 105, 204, 126, 142, 250, 55, 26, 172, 65, 160, 45, 6, 99, 86, 66, 238, - 107, 6, 98, 171, 93, 224, 201, 160, 31, 204, 82, 120, 228, 158, 238, 6, - 190, 12, 150, 153, 239, 95, 57, 71, 100, 239, 235, 155, 73, 200, 5, 225, - 127, 185, 46, 48, 243, 84, 33, 142, 17, 19, 20, 23, 215, 16, 114, 58, 211, - 14, 73, 148, 168, 252, 159, 252, 125, 57, 101, 211, 188, 12, 77, 208, - ] - .as_slice(), - ), - ], - ) - .unwrap() - .to_public_key(); - err = Error::invalid_value( - Unexpected::Bytes([0; 32].as_slice()), - &format!( - "DER-encoded public key to match the public key within the attestation object: Rsa(RsaPubKey({:?}, 65537))", - // Correct and won't `panic`. - &att_obj[n_start_idx..e_meta_start_idx], - ) - .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": -257i16, - "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` when using EdDSA, ES256, or RS256. - err = Error::missing_field("publicKey").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": -257i16, - "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` when using EdDSA, ES256, or RS256. - err = Error::invalid_type(Unexpected::Other("null"), &"publicKey") - .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": -257i16, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - } -} diff --git a/src/response/register/ser/tests.rs b/src/response/register/ser/tests.rs @@ -0,0 +1,11340 @@ +use super::{ + super::{ + 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::{ + PublicKey as P256PubKey, Sec1Point as P256Pt, SecretKey as P256Key, + elliptic_curve::sec1::{FromSec1Point as _, ToSec1Point as _}, +}; +use p384::{PublicKey as P384PubKey, Sec1Point as P384Pt, SecretKey as P384Key}; +use rsa::{ + BoxedUint, RsaPrivateKey, + sha2::{Digest as _, Sha256}, + traits::PublicKeyParts as _, +}; +use serde::de::{Error as _, Unexpected}; +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( + VerifyingKey::from_bytes(&[1; 32]) + .unwrap() + .to_public_key_der() + .unwrap() + .as_bytes() + ) + .is_ok_and(|k| k.0 == [1; 32]) + ); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[test] +fn p256_spki() { + let key = P256Key::from_bytes( + &[ + 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, + 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, + ] + .into(), + ) + .unwrap() + .public_key(); + let enc_key = key.to_sec1_point(false); + assert!( + UncompressedP256PubKey::from_der(key.to_public_key_der().unwrap().as_bytes()) + .is_ok_and(|k| *k.0 == **enc_key.x().unwrap() && *k.1 == **enc_key.y().unwrap()) + ); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[test] +fn p384_spki() { + let key = P384Key::from_bytes( + &[ + 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, 42, + 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, 32, 86, + 220, 68, 182, 11, 105, 223, 75, 70, + ] + .into(), + ) + .unwrap() + .public_key(); + let enc_key = key.to_sec1_point(false); + assert!( + UncompressedP384PubKey::from_der(key.to_public_key_der().unwrap().as_bytes()) + .is_ok_and(|k| *k.0 == **enc_key.x().unwrap() && *k.1 == **enc_key.y().unwrap()) + ); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[test] +fn rsa_spki() { + let n = [ + 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, 195, + 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, 185, 19, + 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, 100, 128, + 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, 145, 50, 174, + 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, 87, 145, 45, 32, + 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, 81, 217, 151, 162, + 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, 117, 248, 233, 151, + 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, 254, 81, 202, 64, 232, + 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, 108, 139, 253, 230, 80, + 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, 42, 198, 212, 23, 182, 222, + 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, 104, 68, 94, 198, 196, 35, 200, + 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, 72, 93, 53, 65, 111, 59, 242, 122, + 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, 153, 79, 0, 133, 78, 7, 218, 165, 241, + ]; + let e = 0x0001_0001u32; + let d = [ + 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, + 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, 35, + 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, 154, + 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, 6, 219, + 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, 82, 104, + 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, 216, 152, 94, + 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, 215, 107, 212, + 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, 140, 252, 248, 162, + 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, 249, 237, 203, 202, 48, 26, + 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, 31, 253, 55, 43, 158, 90, 248, + 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, 162, 60, 91, 99, 220, 51, 44, 85, + 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, 24, 245, 127, 122, 247, 152, 212, 75, + 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, + ]; + let p = BoxedUint::from_le_slice_vartime( + [ + 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, 69, + 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, 250, 195, + 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, 99, 76, 240, 14, + 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, 82, 98, 233, 236, + 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, 147, 179, 30, 62, 247, + 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, 168, 253, 228, 214, 54, 16, + 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, 24, 247, + ] + .as_slice(), + ); + let p_2 = BoxedUint::from_le_slice_vartime( + [ + 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, 85, + 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, 141, + 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, 39, 85, + 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, 250, 187, + 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, 112, 211, + 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, 173, 9, 236, + 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, 250, + ] + .as_slice(), + ); + let key = RsaPrivateKey::from_components( + BoxedUint::from_le_slice_vartime(n.as_slice()), + e.into(), + BoxedUint::from_le_slice_vartime(d.as_slice()), + vec![p, p_2], + ) + .unwrap() + .to_public_key(); + assert!( + RsaPubKey::from_der(key.to_public_key_der().unwrap().as_bytes()) + .is_ok_and(|k| *k.0 == *key.n().to_be_bytes() && BoxedUint::from(k.1) == *key.e()) + ); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[expect(clippy::indexing_slicing, reason = "comments justify correctness")] +#[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[test] +fn eddsa_registration_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let att_obj: [u8; 143] = [ + 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_24, + 115, + // `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, + // Ed25519 COSE key. + cbor::MAP_4, + KTY, + OKP, + ALG, + EDDSA, + // `crv`. + cbor::NEG_ONE, + // `Ed25519`. + cbor::SIX, + // `x`. + cbor::NEG_TWO, + cbor::BYTES_INFO_24, + 32, + // Compressed y-coordinate. + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 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 = VerifyingKey::from_bytes(&[1; 32]) + .unwrap() + .to_public_key_der() + .unwrap(); + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let att_obj_len = att_obj.len(); + let auth_data_start = att_obj_len - 113; + let b64_adata = base64url_nopad::encode(&att_obj[auth_data_start..]); + 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": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], + "publicKey": b64_key, + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "authenticatorAttachment": "cross-platform", + "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.count() == 6 + && matches!( + reg.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + && reg.client_extension_results.cred_props.is_none() + && reg.client_extension_results.prf.is_none() + ) + ); + // `id` and `rawId` mismatch. + let mut err = Error::invalid_value( + Unexpected::Bytes( + base64url_nopad::decode(b"ABABABABABABABABABABAA") + .unwrap() + .as_slice(), + ), + &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "ABABABABABABABABABABAA", + "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 `id`. + err = Error::missing_field("id").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "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()) + ); + // `null` `id`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": null, + "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 `rawId`. + err = Error::missing_field("rawId").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "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()) + ); + // `null` `rawId`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": null, + "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()) + ); + // `id` and the credential id in authenticator data mismatch. + err = Error::invalid_value( + Unexpected::Bytes( + base64url_nopad::decode(b"ABABABABABABABABABABAA") + .unwrap() + .as_slice(), + ), + &format!( + "id, rawId, and the credential id in the attested credential data to all match: {:?}", + [0u8; 16] + ) + .as_str(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "ABABABABABABABABABABAA", + "rawId": "ABABABABABABABABABABAA", + "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()) + ); + // `authenticatorData` mismatches `authData` in attestation object. + let mut bad_auth = [0; 113]; + bad_auth.copy_from_slice(&att_obj[auth_data_start..]); + bad_auth[113 - 32..].copy_from_slice([0; 32].as_slice()); + err = Error::invalid_value( + Unexpected::Bytes(bad_auth.as_slice()), + &format!("authenticator data to match the authenticator data portion of attestation object: {:?}", &att_obj[att_obj_len - bad_auth.len()..]).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": base64url_nopad::encode(bad_auth.as_slice()), + "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 `authenticatorData`. + err = Error::missing_field("authenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "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()) + ); + // `null` `authenticatorData`. + err = Error::invalid_type(Unexpected::Other("null"), &"authenticatorData") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "transports": [], + "authenticatorData": null, + "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()) + ); + // `publicKeyAlgorithm` mismatch. + 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::Eddsa).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": -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()) + ); + // 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. + err = Error::invalid_value( + Unexpected::Bytes([0; 32].as_slice()), + &format!( + "DER-encoded public key to match the public key within the attestation object: Ed25519(Ed25519PubKey({:?}))", + &att_obj[att_obj.len() - 32..], + ) + .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(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()), + "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 `publicKey` when using EdDSA, ES256, or RS256. + err = Error::missing_field("publicKey").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": -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()) + ); + // `null` `publicKey` when using EdDSA, ES256, or RS256. + err = Error::invalid_type(Unexpected::Other("null"), &"publicKey") + .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": -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 `transports`. + err = Error::missing_field("transports").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, + "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()) + ); + // Duplicate `transports` are allowed. + assert!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "transports": ["usb", "usb"], + "publicKey": b64_key, + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.response.transports.count() == 1) + ); + // `null` `transports`. + err = Error::invalid_type(Unexpected::Other("null"), &"transports") + .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": null, + "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()) + ); + // Unknown `transports`. + err = Error::invalid_value( + Unexpected::Str("Usb"), + &"'ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'", + ) + .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": ["Usb"], + "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()) + ); + // `null` `authenticatorAttachment`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)) + ); + // Unknown `authenticatorAttachment`. + err = Error::invalid_value( + Unexpected::Str("Platform"), + &"'platform' or 'cross-platform'", + ) + .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, + }, + "authenticatorAttachment": "Platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientDataJSON`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "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()) + ); + // `null` `clientDataJSON`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": null, + "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 `attestationObject`. + err = Error::missing_field("attestationObject") + .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, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `attestationObject`. + err = Error::invalid_type( + Unexpected::Other("null"), + &"base64url-encoded attestation object", + ) + .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": null, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `response`. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `response`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty `response`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": {}, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientExtensionResults`. + err = Error::missing_field("clientExtensionResults") + .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, + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `clientExtensionResults`. + err = Error::invalid_type( + Unexpected::Other("null"), + &"clientExtensionResults to be a map of allowed client extensions", + ) + .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": null, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `type`. + err = Error::missing_field("type").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": {}, + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `type`. + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .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": null + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Not exactly `public-type` `type`. + err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") + .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()) + ); + // `null`. + err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>(serde_json::json!(null).to_string().as_str()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>(serde_json::json!({}).to_string().as_str()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field in `response`. + err = Error::unknown_field( + "foo", + [ + "clientDataJSON", + "attestationObject", + "authenticatorData", + "transports", + "publicKey", + "publicKeyAlgorithm", + ] + .as_slice(), + ) + .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, + "foo": true, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field in `response`. + err = Error::duplicate_field("transports") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\", + \"transports\": [] + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field in `PublicKeyCredential`. + err = Error::unknown_field( + "foo", + [ + "id", + "type", + "rawId", + "response", + "authenticatorAttachment", + "clientExtensionResults", + ] + .as_slice(), + ) + .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", + "foo": true, + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field in `PublicKeyCredential`. + err = Error::duplicate_field("id").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + + }}" + ) + .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::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[test] +fn client_extensions() { + let c_data_json = serde_json::json!({}).to_string(); + let att_obj: [u8; 143] = [ + 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_24, + 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, + // Ed25519 COSE key. + cbor::MAP_4, + KTY, + OKP, + ALG, + EDDSA, + // `crv`. + cbor::NEG_ONE, + // `Ed25519`. + cbor::SIX, + // `x`. + cbor::NEG_TWO, + cbor::BYTES_INFO_24, + 32, + // Compressed y-coordinate. + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 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 = VerifyingKey::from_bytes(&[1; 32]) + .unwrap() + .to_public_key_der() + .unwrap(); + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 113..]); + 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": -8i8, + "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() + ) + ); + // `null` `credProps`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() + && reg.client_extension_results.prf.is_none()) + ); + // `null` `prf`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() + && reg.client_extension_results.prf.is_none()) + ); + // Unknown `clientExtensionResults`. + let mut err = Error::unknown_field("CredProps", ["credProps", "prf"].as_slice()) + .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": { + "CredProps": { + "rk": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field. + err = Error::duplicate_field("credProps").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{ + \"credProps\": null, + \"credProps\": null + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `rk`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg + .client_extension_results + .cred_props + .is_some_and(|props| props.rk.is_none()) + && reg.client_extension_results.prf.is_none()) + ); + // Missing `rk`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": {} + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg + .client_extension_results + .cred_props + .is_some_and(|props| props.rk.is_none()) + && reg.client_extension_results.prf.is_none()) + ); + // `true` rk`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg + .client_extension_results + .cred_props + .is_some_and(|props| props.rk.unwrap_or_default()) + && reg.client_extension_results.prf.is_none()) + ); + // `false` rk`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": false + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg + .client_extension_results + .cred_props + .is_some_and(|props| props.rk.is_some_and(|rk| !rk)) + && reg.client_extension_results.prf.is_none()) + ); + // Invalid `rk`. + err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") + .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": { + "credProps": { + "rk": 3u8 + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown `credProps` field. + err = Error::unknown_field("Rk", ["rk"].as_slice()) + .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": { + "credProps": { + "Rk": true, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field in `credProps`. + err = Error::duplicate_field("rk").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{ + \"credProps\": {{ + \"rk\": true, + \"rk\": true + }} + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `enabled`. + err = Error::invalid_type(Unexpected::Other("null"), &"a boolean") + .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": { + "prf": { + "enabled": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `enabled`. + err = Error::missing_field("enabled").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": { + "prf": {} + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `true` `enabled`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() + && reg + .client_extension_results + .prf + .is_some_and(|prf| prf.enabled)) + ); + // `false` `enabled`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": false, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() + && reg + .client_extension_results + .prf + .is_some_and(|prf| !prf.enabled)) + ); + // Invalid `enabled`. + err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") + .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": { + "prf": { + "enabled": 3u8 + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `results` with `enabled` `true`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": null, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() + && reg + .client_extension_results + .prf + .is_some_and(|prf| prf.enabled)) + ); + // `null` `results` with `enabled` `false`. + err = Error::custom( + "prf must not have 'results', including a null 'results', if 'enabled' is false", + ) + .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": { + "prf": { + "enabled": false, + "results": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field in `prf`. + err = Error::duplicate_field("enabled").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": {{ + \"enabled\": true, + \"enabled\": true + }} + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `first`. + err = Error::missing_field("first").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": { + "prf": { + "enabled": true, + "results": {}, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `first`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": { + "first": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() + && reg + .client_extension_results + .prf + .is_some_and(|prf| prf.enabled)) + ); + // `null` `second`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": { + "first": null, + "second": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() + && reg + .client_extension_results + .prf + .is_some_and(|prf| prf.enabled)) + ); + // Non-`null` `first`. + err = Error::invalid_type(Unexpected::Option, &"null") + .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": { + "prf": { + "enabled": true, + "results": { + "first": "" + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Non-`null` `second`. + err = Error::invalid_type(Unexpected::Option, &"null") + .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": { + "prf": { + "enabled": true, + "results": { + "first": null, + "second": "" + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown `prf` field. + err = Error::unknown_field("Results", ["enabled", "results"].as_slice()) + .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": { + "prf": { + "enabled": true, + "Results": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown `results` field. + err = Error::unknown_field("Second", ["first", "second"].as_slice()) + .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": { + "prf": { + "enabled": true, + "results": { + "first": null, + "Second": null + } + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field in `results`. + err = Error::duplicate_field("first").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<Registration>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": {{ + \"enabled\": true, + \"results\": {{ + \"first\": null, + \"first\": null + }} + }} + }}, + \"type\": \"public-key\" + }}" + ) + .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 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")] +#[test] +fn es256_registration_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let mut att_obj: [u8; 178] = [ + 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_24, + 148, + // `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, + // P-256 COSE key. + cbor::MAP_5, + KTY, + EC2, + ALG, + ES256, + // `crv`. + cbor::NEG_ONE, + // `P-256`. + cbor::ONE, + // `x`. + cbor::NEG_TWO, + cbor::BYTES_INFO_24, + 32, + // x-coordinate. 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, + // `y`. + cbor::NEG_THREE, + cbor::BYTES_INFO_24, + 32, + // y-coordinate. 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, + ]; + let key = P256Key::from_bytes( + &[ + 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, + 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, + ] + .into(), + ) + .unwrap() + .public_key(); + let enc_key = key.to_sec1_point(false); + let pub_key = key.to_public_key_der().unwrap(); + let att_obj_len = att_obj.len(); + let x_start = att_obj_len - 67; + let y_meta_start = x_start + 32; + let y_start = y_meta_start + 3; + att_obj[x_start..y_meta_start].copy_from_slice(enc_key.x().unwrap()); + att_obj[y_start..].copy_from_slice(enc_key.y().unwrap()); + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 148..]); + 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": -7i8, + "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::Es256).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 = P256PubKey::from_sec1_point(&P256Pt::from_affine_coordinates( + &[ + 66, 71, 188, 41, 125, 2, 226, 44, 148, 62, 63, 190, 172, 64, 33, 214, 6, 37, 148, 23, + 240, 235, 203, 84, 112, 219, 232, 197, 54, 182, 17, 235, + ] + .into(), + &[ + 22, 172, 123, 13, 170, 242, 217, 248, 193, 209, 206, 163, 92, 4, 162, 168, 113, 63, 2, + 117, 16, 223, 239, 196, 109, 179, 10, 130, 43, 213, 205, 92, + ] + .into(), + false, + )) + .unwrap(); + err = Error::invalid_value( + Unexpected::Bytes([0; 32].as_slice()), + &format!( + "DER-encoded public key to match the public key within the attestation object: P256(UncompressedP256PubKey({:?}, {:?}))", + &att_obj[x_start..y_meta_start], + &att_obj[y_start..], + ) + .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": -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()) + ); + // Missing `publicKey` when using EdDSA, ES256, or RS256. + err = Error::missing_field("publicKey").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` when using EdDSA, ES256, or RS256. + err = Error::invalid_type(Unexpected::Other("null"), &"publicKey") + .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 es384_registration_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let mut att_obj: [u8; 211] = [ + 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_24, + 181, + // `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, + // P-384 COSE key. + cbor::MAP_5, + KTY, + EC2, + ALG, + cbor::NEG_INFO_24, + ES384, + // `crv`. + cbor::NEG_ONE, + // `P-384`. + cbor::TWO, + // `x`. + cbor::NEG_TWO, + cbor::BYTES_INFO_24, + 48, + // x-coordinate. 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, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // `y`. + cbor::NEG_THREE, + cbor::BYTES_INFO_24, + 48, + // y-coordinate. 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, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ]; + let key = P384Key::from_bytes( + &[ + 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, 42, + 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, 32, 86, + 220, 68, 182, 11, 105, 223, 75, 70, + ] + .into(), + ) + .unwrap() + .public_key(); + let enc_key = key.to_sec1_point(false); + let pub_key = key.to_public_key_der().unwrap(); + let att_obj_len = att_obj.len(); + let x_start = att_obj_len - 99; + let y_meta_start = x_start + 48; + let y_start = y_meta_start + 3; + att_obj[x_start..y_meta_start].copy_from_slice(enc_key.x().unwrap()); + att_obj[y_start..].copy_from_slice(enc_key.y().unwrap()); + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 181..]); + 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": -35i8, + "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::Es256).as_str()), + &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).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": -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()) + ); + // 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 = P384PubKey::from_sec1_point(&P384Pt::from_affine_coordinates( + &[ + 192, 10, 27, 46, 66, 67, 80, 98, 33, 230, 156, 95, 1, 135, 150, 110, 64, 243, 22, 118, + 5, 255, 107, 44, 234, 111, 217, 105, 125, 114, 39, 7, 126, 2, 191, 111, 48, 93, 234, + 175, 18, 172, 59, 28, 97, 106, 178, 152, + ] + .into(), + &[ + 57, 36, 196, 12, 109, 129, 253, 115, 88, 154, 6, 43, 195, 85, 169, 5, 230, 51, 28, 205, + 142, 28, 150, 35, 24, 222, 170, 253, 14, 248, 84, 151, 109, 191, 152, 111, 222, 70, + 134, 247, 109, 171, 211, 33, 214, 217, 200, 111, + ] + .into(), + false, + )) + .unwrap(); + err = Error::invalid_value( + Unexpected::Bytes([0; 32].as_slice()), + &format!( + "DER-encoded public key to match the public key within the attestation object: P384(UncompressedP384PubKey({:?}, {:?}))", + &att_obj[x_start..y_meta_start], + &att_obj[y_start..], + ) + .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": -35i8, + "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": -35i8, + "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::Es384).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": -35i8, + "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::Es384).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")] +#[test] +fn rs256_registration_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let mut att_obj: [u8; 374] = [ + 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, + 1, + 87, + // `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, + // RSA COSE key. + cbor::MAP_4, + KTY, + RSA, + ALG, + cbor::NEG_INFO_25, + // RS256. + 1, + 0, + // `n`. + cbor::NEG_ONE, + cbor::BYTES_INFO_25, + 1, + 0, + // n. 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, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // `e`. + cbor::NEG_TWO, + cbor::BYTES | 3, + // e. + 1, + 0, + 1, + ]; + let n = [ + 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, 195, + 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, 185, 19, + 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, 100, 128, + 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, 145, 50, 174, + 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, 87, 145, 45, 32, + 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, 81, 217, 151, 162, + 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, 117, 248, 233, 151, + 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, 254, 81, 202, 64, 232, + 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, 108, 139, 253, 230, 80, + 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, 42, 198, 212, 23, 182, 222, + 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, 104, 68, 94, 198, 196, 35, 200, + 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, 72, 93, 53, 65, 111, 59, 242, 122, + 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, 153, 79, 0, 133, 78, 7, 218, 165, 241, + ]; + let e = 0x0001_0001u32; + let d = [ + 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, + 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, 35, + 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, 154, + 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, 6, 219, + 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, 82, 104, + 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, 216, 152, 94, + 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, 215, 107, 212, + 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, 140, 252, 248, 162, + 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, 249, 237, 203, 202, 48, 26, + 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, 31, 253, 55, 43, 158, 90, 248, + 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, 162, 60, 91, 99, 220, 51, 44, 85, + 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, 24, 245, 127, 122, 247, 152, 212, 75, + 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, + ]; + let p = BoxedUint::from_le_slice_vartime( + [ + 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, 69, + 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, 250, 195, + 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, 99, 76, 240, 14, + 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, 82, 98, 233, 236, + 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, 147, 179, 30, 62, 247, + 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, 168, 253, 228, 214, 54, 16, + 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, 24, 247, + ] + .as_slice(), + ); + let p_2 = BoxedUint::from_le_slice_vartime( + [ + 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, 85, + 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, 141, + 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, 39, 85, + 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, 250, 187, + 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, 112, 211, + 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, 173, 9, 236, + 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, 250, + ] + .as_slice(), + ); + let key = RsaPrivateKey::from_components( + BoxedUint::from_le_slice_vartime(n.as_slice()), + e.into(), + BoxedUint::from_le_slice_vartime(d.as_slice()), + vec![p, p_2], + ) + .unwrap() + .to_public_key(); + let pub_key = key.to_public_key_der().unwrap(); + let att_obj_len = att_obj.len(); + let n_start_idx = att_obj_len - 261; + let e_meta_start_idx = n_start_idx + 256; + // Correct and won't `panic`. + att_obj[n_start_idx..e_meta_start_idx] + .copy_from_slice(key.n().to_be_bytes_trimmed_vartime().as_ref()); + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + // Won't `panic`. + let b64_adata = base64url_nopad::encode(&att_obj[31..]); + 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": -257i16, + "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::Rs256).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 = RsaPrivateKey::from_components( + BoxedUint::from_le_slice_vartime( + [ + 175, 161, 161, 75, 52, 244, 72, 168, 29, 119, 33, 120, 3, 222, 231, 152, 222, 119, + 112, 83, 221, 237, 74, 174, 79, 216, 147, 251, 245, 94, 234, 114, 254, 21, 17, 254, + 8, 115, 75, 127, 150, 87, 59, 109, 230, 116, 85, 90, 11, 160, 63, 217, 9, 38, 187, + 250, 226, 183, 38, 164, 182, 218, 22, 19, 58, 189, 83, 219, 11, 144, 15, 99, 151, + 166, 46, 57, 17, 111, 189, 131, 142, 113, 85, 122, 188, 238, 52, 21, 116, 125, 102, + 195, 182, 165, 29, 156, 213, 182, 125, 156, 88, 56, 221, 2, 98, 43, 210, 115, 32, + 4, 105, 88, 181, 158, 207, 236, 162, 250, 253, 240, 72, 8, 253, 50, 220, 247, 76, + 170, 143, 68, 225, 231, 113, 64, 244, 17, 138, 162, 233, 33, 2, 67, 11, 223, 188, + 232, 152, 193, 20, 32, 243, 52, 64, 43, 2, 243, 8, 77, 150, 232, 109, 148, 95, 127, + 55, 71, 162, 34, 54, 83, 135, 52, 172, 191, 32, 42, 106, 43, 211, 206, 100, 104, + 110, 232, 5, 43, 120, 180, 166, 40, 144, 233, 239, 103, 134, 103, 255, 224, 138, + 184, 208, 137, 127, 36, 189, 143, 248, 201, 2, 218, 51, 232, 96, 30, 83, 124, 109, + 241, 23, 179, 247, 151, 238, 212, 204, 44, 43, 223, 148, 241, 172, 10, 235, 155, + 94, 68, 116, 24, 116, 191, 86, 53, 127, 35, 133, 198, 204, 59, 76, 110, 16, 1, 15, + 148, 135, 157, + ] + .as_slice(), + ), + 0x0001_0001u32.into(), + BoxedUint::from_le_slice_vartime( + [ + 129, 93, 123, 251, 104, 29, 84, 203, 116, 100, 75, 237, 111, 160, 12, 100, 172, 76, + 57, 178, 144, 235, 81, 61, 115, 243, 28, 40, 183, 22, 56, 150, 68, 38, 220, 62, + 233, 110, 48, 174, 35, 197, 244, 109, 148, 109, 36, 69, 69, 82, 225, 113, 175, 6, + 239, 27, 193, 101, 50, 239, 122, 102, 7, 46, 98, 79, 195, 116, 155, 158, 138, 147, + 51, 93, 24, 237, 246, 82, 14, 109, 144, 250, 239, 93, 63, 214, 96, 130, 226, 134, + 198, 145, 161, 11, 231, 97, 214, 180, 255, 95, 158, 88, 108, 254, 243, 177, 133, + 184, 92, 95, 148, 88, 55, 124, 245, 244, 84, 86, 4, 121, 44, 231, 97, 176, 190, 29, + 155, 40, 57, 69, 165, 80, 168, 9, 56, 43, 233, 6, 14, 157, 112, 223, 64, 88, 141, + 7, 65, 23, 64, 208, 6, 83, 61, 8, 182, 248, 126, 84, 179, 163, 80, 238, 90, 133, 4, + 14, 71, 177, 175, 27, 29, 151, 211, 108, 162, 195, 7, 157, 167, 86, 169, 3, 87, + 235, 89, 158, 237, 216, 31, 243, 197, 62, 5, 84, 131, 230, 186, 248, 49, 12, 93, + 244, 61, 135, 180, 17, 162, 241, 13, 115, 241, 138, 219, 98, 155, 166, 191, 63, 12, + 37, 1, 165, 178, 84, 200, 72, 80, 41, 77, 136, 217, 141, 246, 209, 31, 243, 159, + 71, 43, 246, 159, 182, 171, 116, 12, 3, 142, 235, 218, 164, 70, 90, 147, 238, 42, + 75, + ] + .as_slice(), + ), + vec![ + BoxedUint::from_le_slice_vartime( + [ + 215, 199, 110, 28, 64, 16, 16, 109, 106, 152, 150, 124, 52, 166, 121, 92, 242, + 13, 0, 69, 7, 152, 72, 172, 118, 63, 156, 180, 140, 39, 53, 29, 197, 224, 177, + 48, 41, 221, 102, 65, 17, 185, 55, 62, 219, 152, 227, 7, 78, 219, 14, 139, 71, + 204, 144, 152, 14, 39, 247, 244, 165, 224, 234, 60, 213, 74, 237, 30, 102, 177, + 242, 138, 168, 31, 122, 47, 206, 155, 225, 113, 103, 175, 152, 244, 27, 233, + 112, 223, 248, 38, 215, 178, 20, 244, 8, 121, 26, 11, 70, 122, 16, 85, 167, 87, + 64, 216, 228, 227, 173, 57, 250, 8, 221, 38, 12, 203, 212, 1, 112, 43, 72, 91, + 225, 97, 228, 57, 154, 193, + ] + .as_slice(), + ), + BoxedUint::from_le_slice_vartime( + [ + 233, 89, 204, 152, 31, 242, 8, 110, 38, 190, 111, 159, 105, 105, 45, 85, 15, + 244, 30, 250, 174, 226, 219, 111, 107, 191, 196, 135, 17, 123, 186, 167, 85, + 13, 120, 197, 159, 129, 78, 237, 152, 31, 230, 26, 229, 253, 197, 211, 105, + 204, 126, 142, 250, 55, 26, 172, 65, 160, 45, 6, 99, 86, 66, 238, 107, 6, 98, + 171, 93, 224, 201, 160, 31, 204, 82, 120, 228, 158, 238, 6, 190, 12, 150, 153, + 239, 95, 57, 71, 100, 239, 235, 155, 73, 200, 5, 225, 127, 185, 46, 48, 243, + 84, 33, 142, 17, 19, 20, 23, 215, 16, 114, 58, 211, 14, 73, 148, 168, 252, 159, + 252, 125, 57, 101, 211, 188, 12, 77, 208, + ] + .as_slice(), + ), + ], + ) + .unwrap() + .to_public_key(); + err = Error::invalid_value( + Unexpected::Bytes([0; 32].as_slice()), + &format!( + "DER-encoded public key to match the public key within the attestation object: Rsa(RsaPubKey({:?}, 65537))", + // Correct and won't `panic`. + &att_obj[n_start_idx..e_meta_start_idx], + ) + .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": -257i16, + "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` when using EdDSA, ES256, or RS256. + err = Error::missing_field("publicKey").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": -257i16, + "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` when using EdDSA, ES256, or RS256. + err = Error::invalid_type(Unexpected::Other("null"), &"publicKey") + .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": -257i16, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); +} diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod tests; #[cfg(doc)] use super::super::{super::request::register::CoseAlgorithmIdentifier, Challenge, CredentialId}; use super::{ @@ -433,11454 +435,3 @@ impl<'de> Deserialize<'de> for CustomRegistration { deserializer.deserialize_struct("CustomRegistration", FIELDS, CustomRegistrationVisitor) } } -#[cfg(test)] -mod tests { - use super::{ - super::{ - 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::{ - PublicKey as P256PubKey, Sec1Point as P256Pt, SecretKey as P256Key, - elliptic_curve::sec1::{FromSec1Point as _, ToSec1Point as _}, - }; - use p384::{PublicKey as P384PubKey, Sec1Point as P384Pt, SecretKey as P384Key}; - use rsa::{ - BoxedUint, RsaPrivateKey, - sha2::{Digest as _, Sha256}, - traits::PublicKeyParts as _, - }; - use serde::de::{Error as _, Unexpected}; - use serde_json::Error; - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] - #[expect( - clippy::cognitive_complexity, - clippy::too_many_lines, - reason = "a lot to test" - )] - #[test] - fn eddsa_registration_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let att_obj: [u8; 143] = [ - 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_24, - 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, - // Ed25519 COSE key. - cbor::MAP_4, - KTY, - OKP, - ALG, - EDDSA, - // `crv`. - cbor::NEG_ONE, - // `Ed25519`. - cbor::SIX, - // `x`. - cbor::NEG_TWO, - cbor::BYTES_INFO_24, - 32, - // Compressed y-coordinate. - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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 = VerifyingKey::from_bytes(&[1; 32]) - .unwrap() - .to_public_key_der() - .unwrap(); - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let att_obj_len = att_obj.len(); - let auth_data_start = att_obj_len - 113; - let b64_adata = base64url_nopad::encode(&att_obj[auth_data_start..]); - 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": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], - "publicKey": b64_key, - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "authenticatorAttachment": "cross-platform", - "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.count() == 6 - && matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - ) - && reg.0.client_extension_results.cred_props.is_none() - && reg.0.client_extension_results.prf.is_none() - ) - ); - // `id` and `rawId` mismatch. - let mut err = Error::invalid_value( - Unexpected::Bytes( - base64url_nopad::decode(b"ABABABABABABABABABABAA") - .unwrap() - .as_slice(), - ), - &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "ABABABABABABABABABABAA", - "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 `id`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "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(), - ); - // `null` `id`. - err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": null, - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": null, - "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 `rawId`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "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(), - ); - // `null` `rawId`. - err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": null, - "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()) - ); - // `id` and the credential id in authenticator data mismatch. - err = Error::invalid_value( - Unexpected::Bytes( - base64url_nopad - ::decode(b"ABABABABABABABABABABAA") - .unwrap() - .as_slice(), - ), - &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", [0u8; 16]).as_str(), - ) - .to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "ABABABABABABABABABABAA", - "rawId": "ABABABABABABABABABABAA", - "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()) - ); - // `authenticatorData` mismatches `authData` in attestation object. - let mut bad_auth = [0; 113]; - let bad_auth_len = bad_auth.len(); - bad_auth.copy_from_slice(&att_obj[auth_data_start..]); - bad_auth[bad_auth_len - 32..].copy_from_slice([0; 32].as_slice()); - err = Error::invalid_value( - Unexpected::Bytes(bad_auth.as_slice()), - &format!("authenticator data to match the authenticator data portion of attestation object: {:?}", &att_obj[att_obj_len - bad_auth_len..]).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": base64url_nopad::encode(bad_auth.as_slice()), - "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 `authenticatorData`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "transports": [], - "publicKey": b64_key, - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null `authenticatorData`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "transports": [], - "authenticatorData": null, - "publicKey": b64_key, - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `publicKeyAlgorithm` mismatch. - 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::Eddsa).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": -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()) - ); - // 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. - err = Error::invalid_value( - Unexpected::Bytes([0; 32].as_slice()), - &format!( - "DER-encoded public key to match the public key within the attestation object: Ed25519(Ed25519PubKey({:?}))", - &att_obj[att_obj_len - 32..], - ) - .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(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()), - "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 `publicKey`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "transports": [], - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `publicKey`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": null, - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Missing `transports`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "publicKey": b64_key, - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate `transports` are allowed. - assert!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "transports": ["usb", "usb"], - "publicKey": b64_key, - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.0.response.transports.count() == 1) - ); - // `null` `transports`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "transports": null, - "publicKey": b64_key, - "publicKeyAlgorithm": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Unknown `transports`. - err = Error::invalid_value( - Unexpected::Str("Usb"), - &"'ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'", - ) - .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": ["Usb"], - "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()) - ); - // `null` `authenticatorAttachment`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "authenticatorAttachment": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::None - )) - ); - // Unknown `authenticatorAttachment`. - err = Error::invalid_value( - Unexpected::Str("Platform"), - &"'platform' or 'cross-platform'", - ) - .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, - }, - "authenticatorAttachment": "Platform", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientDataJSON`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "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()) - ); - // `null` `clientDataJSON`. - err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": null, - "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 `attestationObject`. - err = Error::missing_field("attestationObject") - .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, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `attestationObject`. - err = Error::invalid_type( - Unexpected::Other("null"), - &"base64url-encoded attestation object", - ) - .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": null, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `response`. - err = Error::missing_field("response").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `response`. - err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty `response`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": {}, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientExtensionResults`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `clientExtensionResults`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": null, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Missing `type`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `type`. - err = Error::invalid_type(Unexpected::Other("null"), &"public-key") - .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": null - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Not exactly `public-type` `type`. - err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") - .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()) - ); - // `null`. - err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!(null).to_string().as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty. - err = Error::missing_field("response").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>(serde_json::json!({}).to_string().as_str()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field in `response`. - 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": -8i8, - "attestationObject": b64_aobj, - "foo": true, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field in `response`. - err = Error::duplicate_field("transports") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\", - \"transports\": [] - }}, - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field in `PublicKeyCredential`. - 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": -8i8, - "attestationObject": b64_aobj - }, - "clientExtensionResults": {}, - "type": "public-key", - "foo": true, - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field in `PublicKeyCredential`. - err = Error::duplicate_field("id").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Base case is correct. - assert!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "attestationObject": b64_aobj, - "authenticatorAttachment": "cross-platform", - "clientDataJSON": b64_cdata_json, - "clientExtensionResults": {}, - "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], - "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.count() == 6 - && matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - ) - && reg.0.client_extension_results.cred_props.is_none() - && reg.0.client_extension_results.prf.is_none() - ) - ); - // Missing `transports`. - err = Error::missing_field("transports").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "attestationObject": b64_aobj, - "authenticatorAttachment": "cross-platform", - "clientDataJSON": b64_cdata_json, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate `transports` are allowed. - assert!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "attestationObject": b64_aobj, - "authenticatorAttachment": "cross-platform", - "clientDataJSON": b64_cdata_json, - "clientExtensionResults": {}, - "transports": ["usb", "usb"], - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.0.response.transports.count() == 1) - ); - // `null` `transports`. - err = Error::invalid_type(Unexpected::Other("null"), &"AuthTransports") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": 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()) - ); - // Unknown `transports`. - err = Error::invalid_value( - Unexpected::Str("Usb"), - &"'ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'", - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "attestationObject": b64_aobj, - "authenticatorAttachment": "cross-platform", - "clientDataJSON": b64_cdata_json, - "clientExtensionResults": {}, - "transports": ["Usb"], - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `authenticatorAttachment`. - assert!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "attestationObject": b64_aobj, - "authenticatorAttachment": null, - "clientDataJSON": b64_cdata_json, - "clientExtensionResults": {}, - "transports": [], - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| matches!( - reg.0.authenticator_attachment, - AuthenticatorAttachment::None - )) - ); - // Unknown `authenticatorAttachment`. - err = Error::invalid_value( - Unexpected::Str("Platform"), - &"'platform' or 'cross-platform'", - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "attestationObject": b64_aobj, - "authenticatorAttachment": "Platform", - "clientDataJSON": b64_cdata_json, - "clientExtensionResults": {}, - "transports": [], - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientDataJSON`. - err = Error::missing_field("clientDataJSON") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "transports": [], - "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` `clientDataJSON`. - err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": null, - "transports": [], - "attestationObject": b64_aobj, - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `attestationObject`. - err = Error::missing_field("attestationObject") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": [], - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `attestationObject`. - err = Error::invalid_type( - Unexpected::Other("null"), - &"base64url-encoded attestation object", - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": [], - "attestationObject": null, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `clientExtensionResults`. - err = Error::missing_field("clientExtensionResults") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": [], - "attestationObject": b64_aobj, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `clientExtensionResults`. - err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": [], - "attestationObject": b64_aobj, - "clientExtensionResults": null, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `type`. - assert!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "attestationObject": b64_aobj, - "clientDataJSON": b64_cdata_json, - "clientExtensionResults": {}, - "transports": [] - }) - .to_string() - .as_str() - ) - .is_ok_and(|_| true) - ); - // `null` `type`. - err = Error::invalid_type(Unexpected::Other("null"), &"public-key") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "attestationObject": b64_aobj, - "clientDataJSON": b64_cdata_json, - "clientExtensionResults": {}, - "transports": [], - "type": null - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Not exactly `public-type` `type`. - err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": [], - "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`. - err = Error::invalid_type(Unexpected::Other("null"), &"CustomRegistration") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!(null).to_string().as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Empty. - err = Error::missing_field("attestationObject") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>(serde_json::json!({}).to_string().as_str()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown field. - err = Error::unknown_field( - "foo", - [ - "attestationObject", - "authenticatorAttachment", - "clientDataJSON", - "clientExtensionResults", - "transports", - "type", - ] - .as_slice(), - ) - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": [], - "attestationObject": b64_aobj, - "foo": true, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field. - err = Error::duplicate_field("transports") - .to_string() - .into_bytes(); - assert_eq!( - serde_json::from_str::<CustomRegistration>( - format!( - "{{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"transports\": [], - \"attestationObject\": \"{b64_aobj}\", - \"transports\": [] - \"clientExtensionResults\": {{}}, - \"type\": \"public-key\" - }}" - ) - .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")] - #[test] - fn client_extensions() { - let c_data_json = serde_json::json!({}).to_string(); - let att_obj: [u8; 143] = [ - 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_24, - 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, - // Ed25519 COSE key. - cbor::MAP_4, - KTY, - OKP, - ALG, - EDDSA, - // `crv`. - cbor::NEG_ONE, - // `Ed25519`. - cbor::SIX, - // `x`. - cbor::NEG_TWO, - cbor::BYTES_INFO_24, - 32, - // Compressed y-coordinate. - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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 = VerifyingKey::from_bytes(&[1; 32]) - .unwrap() - .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 auth_data_start = att_obj_len - 113; - let b64_adata = base64url_nopad::encode(&att_obj[auth_data_start..]); - 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": -8i8, - "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() - ) - ); - // `null` `credProps`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() - && reg.0.client_extension_results.prf.is_none()) - ); - // `null` `prf`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": null - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() - && reg.0.client_extension_results.prf.is_none()) - ); - // Unknown `clientExtensionResults`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "CredProps": { - "rk": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field. - let mut err = Error::duplicate_field("credProps").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{ - \"credProps\": null, - \"credProps\": null - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `rk`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg - .0 - .client_extension_results - .cred_props - .is_some_and(|props| props.rk.is_none()) - && reg.0.client_extension_results.prf.is_none()) - ); - // Missing `rk`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": {} - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg - .0 - .client_extension_results - .cred_props - .is_some_and(|props| props.rk.is_none()) - && reg.0.client_extension_results.prf.is_none()) - ); - // `true` rk`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg - .0 - .client_extension_results - .cred_props - .is_some_and(|props| props.rk.unwrap_or_default()) - && reg.0.client_extension_results.prf.is_none()) - ); - // `false` rk`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "rk": false - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg - .0 - .client_extension_results - .cred_props - .is_some_and(|props| props.rk.is_some_and(|rk| !rk)) - && reg.0.client_extension_results.prf.is_none()) - ); - // Invalid `rk`. - err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") - .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": { - "credProps": { - "rk": 3u8 - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown `credProps` field. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "credProps": { - "Rk": true, - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field in `credProps`. - err = Error::duplicate_field("rk").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{ - \"credProps\": {{ - \"rk\": true, - \"rk\": true - }} - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `enabled`. - err = Error::invalid_type(Unexpected::Other("null"), &"a boolean") - .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": { - "prf": { - "enabled": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `enabled`. - err = Error::missing_field("enabled").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": { - "prf": {} - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `true` `enabled`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() - && reg - .0 - .client_extension_results - .prf - .is_some_and(|prf| prf.enabled)) - ); - // `false` `enabled`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": false, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() - && reg - .0 - .client_extension_results - .prf - .is_some_and(|prf| !prf.enabled)) - ); - // Invalid `enabled`. - err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") - .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": { - "prf": { - "enabled": 3u8 - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `results` with `enabled` `true`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": null, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() - && reg - .0 - .client_extension_results - .prf - .is_some_and(|prf| prf.enabled)) - ); - // `null` `results` with `enabled` `false`. - err = Error::custom( - "prf must not have 'results', including a null 'results', if 'enabled' is false", - ) - .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": { - "prf": { - "enabled": false, - "results": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Duplicate field in `prf`. - err = Error::duplicate_field("enabled").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": {{ - \"enabled\": true, - \"enabled\": true - }} - }}, - \"type\": \"public-key\" - }}" - ) - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `first`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": {}, - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `first`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() - && reg - .0 - .client_extension_results - .prf - .is_some_and(|prf| prf.enabled)) - ); - // `null` `second`. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null, - "second": null - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() - && reg - .0 - .client_extension_results - .prf - .is_some_and(|prf| prf.enabled)) - ); - // Non-`null` `first`. - err = Error::invalid_type(Unexpected::Option, &"null") - .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": { - "prf": { - "enabled": true, - "results": { - "first": "" - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Non-`null` `second`. - err = Error::invalid_type(Unexpected::Option, &"null") - .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": { - "prf": { - "enabled": true, - "results": { - "first": null, - "second": "" - }, - } - }, - "type": "public-key" - }) - .to_string() - .as_str() - ) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Unknown `prf` field. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "Results": null - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Unknown `results` field. - 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": -8i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": { - "prf": { - "enabled": true, - "results": { - "first": null, - "Second": null - } - } - }, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Duplicate field in `results`. - err = Error::duplicate_field("first").to_string().into_bytes(); - assert_eq!( - serde_json::from_str::<RegistrationRelaxed>( - format!( - "{{ - \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"response\": {{ - \"clientDataJSON\": \"{b64_cdata_json}\", - \"authenticatorData\": \"{b64_adata}\", - \"transports\": [], - \"publicKey\": \"{b64_key}\", - \"publicKeyAlgorithm\": -8, - \"attestationObject\": \"{b64_aobj}\" - }}, - \"clientExtensionResults\": {{ - \"prf\": {{ - \"enabled\": true, - \"results\": {{ - \"first\": null, - \"first\": null - }} - }} - }}, - \"type\": \"public-key\" - }}" - ) - .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 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")] - #[test] - fn es256_registration_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let mut att_obj: [u8; 178] = [ - 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_24, - 148, - // `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, - // P-256 COSE key. - cbor::MAP_5, - KTY, - EC2, - ALG, - ES256, - // `crv`. - cbor::NEG_ONE, - // `P-256`. - cbor::ONE, - // `x`. - cbor::NEG_TWO, - cbor::BYTES_INFO_24, - 32, - // x-coordinate. 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, - // `y`. - cbor::NEG_THREE, - cbor::BYTES_INFO_24, - 32, - // y-coordinate. 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, - ]; - let key = P256Key::from_bytes( - &[ - 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, - 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, - ] - .into(), - ) - .unwrap() - .public_key(); - let enc_key = key.to_sec1_point(false); - let pub_key = key.to_public_key_der().unwrap(); - let att_obj_len = att_obj.len(); - let x_start = att_obj_len - 67; - let y_meta_start = x_start + 32; - let y_start = y_meta_start + 3; - att_obj[x_start..y_meta_start].copy_from_slice(enc_key.x().unwrap()); - att_obj[y_start..].copy_from_slice(enc_key.y().unwrap()); - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 148..]); - 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": -7i8, - "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::Es256).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 = P256PubKey::from_sec1_point(&P256Pt::from_affine_coordinates( - &[ - 66, 71, 188, 41, 125, 2, 226, 44, 148, 62, 63, 190, 172, 64, 33, 214, 6, 37, 148, - 23, 240, 235, 203, 84, 112, 219, 232, 197, 54, 182, 17, 235, - ] - .into(), - &[ - 22, 172, 123, 13, 170, 242, 217, 248, 193, 209, 206, 163, 92, 4, 162, 168, 113, 63, - 2, 117, 16, 223, 239, 196, 109, 179, 10, 130, 43, 213, 205, 92, - ] - .into(), - false, - )) - .unwrap(); - err = Error::invalid_value( - Unexpected::Bytes([0; 32].as_slice()), - &format!( - "DER-encoded public key to match the public key within the attestation object: P256(UncompressedP256PubKey({:?}, {:?}))", - &att_obj[x_start..y_meta_start], - &att_obj[y_start..], - ) - .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": -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()) - ); - // Missing `publicKey`. - drop( - 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(), - ); - // `null` `publicKey`. - drop( - 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(), - ); - // Base case is valid. - assert!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": [], - "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() - ) - ); - } - #[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")] - #[test] - fn es384_registration_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let mut att_obj: [u8; 211] = [ - 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_24, - 181, - // `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, - // P-384 COSE key. - cbor::MAP_5, - KTY, - EC2, - ALG, - cbor::NEG_INFO_24, - ES384, - // `crv`. - cbor::NEG_ONE, - // `P-384`. - cbor::TWO, - // `x`. - cbor::NEG_TWO, - cbor::BYTES_INFO_24, - 48, - // x-coordinate. 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // `y`. - cbor::NEG_THREE, - cbor::BYTES_INFO_24, - 48, - // y-coordinate. 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ]; - let key = P384Key::from_bytes( - &[ - 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, - 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, - 32, 86, 220, 68, 182, 11, 105, 223, 75, 70, - ] - .into(), - ) - .unwrap() - .public_key(); - let enc_key = key.to_sec1_point(false); - let pub_key = key.to_public_key_der().unwrap(); - let att_obj_len = att_obj.len(); - let x_start = att_obj_len - 99; - let y_meta_start = x_start + 48; - let y_start = y_meta_start + 3; - att_obj[x_start..y_meta_start].copy_from_slice(enc_key.x().unwrap()); - att_obj[y_start..].copy_from_slice(enc_key.y().unwrap()); - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 181..]); - 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": -35i8, - "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::Es256).as_str()), - &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).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": -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()) - ); - // 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 = P384PubKey::from_sec1_point(&P384Pt::from_affine_coordinates( - &[ - 192, 10, 27, 46, 66, 67, 80, 98, 33, 230, 156, 95, 1, 135, 150, 110, 64, 243, 22, - 118, 5, 255, 107, 44, 234, 111, 217, 105, 125, 114, 39, 7, 126, 2, 191, 111, 48, - 93, 234, 175, 18, 172, 59, 28, 97, 106, 178, 152, - ] - .into(), - &[ - 57, 36, 196, 12, 109, 129, 253, 115, 88, 154, 6, 43, 195, 85, 169, 5, 230, 51, 28, - 205, 142, 28, 150, 35, 24, 222, 170, 253, 14, 248, 84, 151, 109, 191, 152, 111, - 222, 70, 134, 247, 109, 171, 211, 33, 214, 217, 200, 111, - ] - .into(), - false, - )) - .unwrap(); - err = Error::invalid_value( - Unexpected::Bytes([0; 32].as_slice()), - &format!( - "DER-encoded public key to match the public key within the attestation object: P384(UncompressedP384PubKey({:?}, {:?}))", - &att_obj[x_start..y_meta_start], - &att_obj[y_start..], - ) - .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": -35i8, - "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`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "transports": [], - "publicKeyAlgorithm": -35i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `publicKeyAlgorithm` mismatch when `publicKey` does not exist. - 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::Es384).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": -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()) - ); - // `null` `publicKey`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": null, - "publicKeyAlgorithm": -35i8, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `publicKeyAlgorithm` mismatch when `publicKey` is null. - 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::Es384).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": -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()) - ); - // Base case is valid. - assert!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": [], - "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() - ) - ); - } - #[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")] - #[test] - fn rs256_registration_deserialize_data_mismatch() { - let c_data_json = serde_json::json!({}).to_string(); - let mut att_obj: [u8; 374] = [ - 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, - 1, - 87, - // `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, - // RSA COSE key. - cbor::MAP_4, - KTY, - RSA, - ALG, - cbor::NEG_INFO_25, - // RS256. - 1, - 0, - // `n`. - cbor::NEG_ONE, - cbor::BYTES_INFO_25, - 1, - 0, - // n. 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - // `e`. - cbor::NEG_TWO, - cbor::BYTES | 3, - // e. - 1, - 0, - 1, - ]; - let n = [ - 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, - 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, - 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, - 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, - 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, - 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, - 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, - 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, - 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, - 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, - 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, - 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, - 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, - 153, 79, 0, 133, 78, 7, 218, 165, 241, - ]; - let e = 0x0001_0001u32; - let d = [ - 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, - 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, - 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, - 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, - 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, - 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, - 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, - 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, - 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, - 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, - 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, - 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, - 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, - 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, - ]; - let p = BoxedUint::from_le_slice_vartime( - [ - 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, - 69, 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, - 250, 195, 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, - 99, 76, 240, 14, 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, - 82, 98, 233, 236, 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, - 147, 179, 30, 62, 247, 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, - 168, 253, 228, 214, 54, 16, 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, - 24, 247, - ] - .as_slice(), - ); - let p_2 = BoxedUint::from_le_slice_vartime( - [ - 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, - 85, 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, - 141, 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, - 39, 85, 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, - 250, 187, 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, - 112, 211, 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, - 173, 9, 236, 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, - 250, - ] - .as_slice(), - ); - let key = RsaPrivateKey::from_components( - BoxedUint::from_le_slice_vartime(n.as_slice()), - e.into(), - BoxedUint::from_le_slice_vartime(d.as_slice()), - vec![p, p_2], - ) - .unwrap() - .to_public_key(); - let pub_key = key.to_public_key_der().unwrap(); - let att_obj_len = att_obj.len(); - let n_start = att_obj_len - 261; - let e_start = n_start + 256; - att_obj[n_start..e_start].copy_from_slice(key.n().to_be_bytes().as_ref()); - let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 343..]); - 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": -257i16, - "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::Rs256).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 = RsaPrivateKey::from_components( - BoxedUint::from_le_slice_vartime( - [ - 175, 161, 161, 75, 52, 244, 72, 168, 29, 119, 33, 120, 3, 222, 231, 152, 222, - 119, 112, 83, 221, 237, 74, 174, 79, 216, 147, 251, 245, 94, 234, 114, 254, 21, - 17, 254, 8, 115, 75, 127, 150, 87, 59, 109, 230, 116, 85, 90, 11, 160, 63, 217, - 9, 38, 187, 250, 226, 183, 38, 164, 182, 218, 22, 19, 58, 189, 83, 219, 11, - 144, 15, 99, 151, 166, 46, 57, 17, 111, 189, 131, 142, 113, 85, 122, 188, 238, - 52, 21, 116, 125, 102, 195, 182, 165, 29, 156, 213, 182, 125, 156, 88, 56, 221, - 2, 98, 43, 210, 115, 32, 4, 105, 88, 181, 158, 207, 236, 162, 250, 253, 240, - 72, 8, 253, 50, 220, 247, 76, 170, 143, 68, 225, 231, 113, 64, 244, 17, 138, - 162, 233, 33, 2, 67, 11, 223, 188, 232, 152, 193, 20, 32, 243, 52, 64, 43, 2, - 243, 8, 77, 150, 232, 109, 148, 95, 127, 55, 71, 162, 34, 54, 83, 135, 52, 172, - 191, 32, 42, 106, 43, 211, 206, 100, 104, 110, 232, 5, 43, 120, 180, 166, 40, - 144, 233, 239, 103, 134, 103, 255, 224, 138, 184, 208, 137, 127, 36, 189, 143, - 248, 201, 2, 218, 51, 232, 96, 30, 83, 124, 109, 241, 23, 179, 247, 151, 238, - 212, 204, 44, 43, 223, 148, 241, 172, 10, 235, 155, 94, 68, 116, 24, 116, 191, - 86, 53, 127, 35, 133, 198, 204, 59, 76, 110, 16, 1, 15, 148, 135, 157, - ] - .as_slice(), - ), - 0x0001_0001u32.into(), - BoxedUint::from_le_slice_vartime( - [ - 129, 93, 123, 251, 104, 29, 84, 203, 116, 100, 75, 237, 111, 160, 12, 100, 172, - 76, 57, 178, 144, 235, 81, 61, 115, 243, 28, 40, 183, 22, 56, 150, 68, 38, 220, - 62, 233, 110, 48, 174, 35, 197, 244, 109, 148, 109, 36, 69, 69, 82, 225, 113, - 175, 6, 239, 27, 193, 101, 50, 239, 122, 102, 7, 46, 98, 79, 195, 116, 155, - 158, 138, 147, 51, 93, 24, 237, 246, 82, 14, 109, 144, 250, 239, 93, 63, 214, - 96, 130, 226, 134, 198, 145, 161, 11, 231, 97, 214, 180, 255, 95, 158, 88, 108, - 254, 243, 177, 133, 184, 92, 95, 148, 88, 55, 124, 245, 244, 84, 86, 4, 121, - 44, 231, 97, 176, 190, 29, 155, 40, 57, 69, 165, 80, 168, 9, 56, 43, 233, 6, - 14, 157, 112, 223, 64, 88, 141, 7, 65, 23, 64, 208, 6, 83, 61, 8, 182, 248, - 126, 84, 179, 163, 80, 238, 90, 133, 4, 14, 71, 177, 175, 27, 29, 151, 211, - 108, 162, 195, 7, 157, 167, 86, 169, 3, 87, 235, 89, 158, 237, 216, 31, 243, - 197, 62, 5, 84, 131, 230, 186, 248, 49, 12, 93, 244, 61, 135, 180, 17, 162, - 241, 13, 115, 241, 138, 219, 98, 155, 166, 191, 63, 12, 37, 1, 165, 178, 84, - 200, 72, 80, 41, 77, 136, 217, 141, 246, 209, 31, 243, 159, 71, 43, 246, 159, - 182, 171, 116, 12, 3, 142, 235, 218, 164, 70, 90, 147, 238, 42, 75, - ] - .as_slice(), - ), - vec![ - BoxedUint::from_le_slice_vartime( - [ - 215, 199, 110, 28, 64, 16, 16, 109, 106, 152, 150, 124, 52, 166, 121, 92, - 242, 13, 0, 69, 7, 152, 72, 172, 118, 63, 156, 180, 140, 39, 53, 29, 197, - 224, 177, 48, 41, 221, 102, 65, 17, 185, 55, 62, 219, 152, 227, 7, 78, 219, - 14, 139, 71, 204, 144, 152, 14, 39, 247, 244, 165, 224, 234, 60, 213, 74, - 237, 30, 102, 177, 242, 138, 168, 31, 122, 47, 206, 155, 225, 113, 103, - 175, 152, 244, 27, 233, 112, 223, 248, 38, 215, 178, 20, 244, 8, 121, 26, - 11, 70, 122, 16, 85, 167, 87, 64, 216, 228, 227, 173, 57, 250, 8, 221, 38, - 12, 203, 212, 1, 112, 43, 72, 91, 225, 97, 228, 57, 154, 193, - ] - .as_slice(), - ), - BoxedUint::from_le_slice_vartime( - [ - 233, 89, 204, 152, 31, 242, 8, 110, 38, 190, 111, 159, 105, 105, 45, 85, - 15, 244, 30, 250, 174, 226, 219, 111, 107, 191, 196, 135, 17, 123, 186, - 167, 85, 13, 120, 197, 159, 129, 78, 237, 152, 31, 230, 26, 229, 253, 197, - 211, 105, 204, 126, 142, 250, 55, 26, 172, 65, 160, 45, 6, 99, 86, 66, 238, - 107, 6, 98, 171, 93, 224, 201, 160, 31, 204, 82, 120, 228, 158, 238, 6, - 190, 12, 150, 153, 239, 95, 57, 71, 100, 239, 235, 155, 73, 200, 5, 225, - 127, 185, 46, 48, 243, 84, 33, 142, 17, 19, 20, 23, 215, 16, 114, 58, 211, - 14, 73, 148, 168, 252, 159, 252, 125, 57, 101, 211, 188, 12, 77, 208, - ] - .as_slice(), - ), - ], - ) - .unwrap() - .to_public_key(); - err = Error::invalid_value( - Unexpected::Bytes([0; 32].as_slice()), - &format!( - "DER-encoded public key to match the public key within the attestation object: Rsa(RsaPubKey({:?}, 65537))", - &att_obj[n_start..e_start], - ) - .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": -257i16, - "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`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "transports": [], - "publicKeyAlgorithm": -257i16, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // `null` `publicKey`. - drop( - serde_json::from_str::<RegistrationRelaxed>( - serde_json::json!({ - "id": "AAAAAAAAAAAAAAAAAAAAAA", - "rawId": "AAAAAAAAAAAAAAAAAAAAAA", - "response": { - "clientDataJSON": b64_cdata_json, - "authenticatorData": b64_adata, - "transports": [], - "publicKey": null, - "publicKeyAlgorithm": -257i16, - "attestationObject": b64_aobj, - }, - "clientExtensionResults": {}, - "type": "public-key" - }) - .to_string() - .as_str(), - ) - .unwrap(), - ); - // Base case is valid. - assert!( - serde_json::from_str::<CustomRegistration>( - serde_json::json!({ - "clientDataJSON": b64_cdata_json, - "transports": [], - "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() - ) - ); - } -} diff --git a/src/response/register/ser_relaxed/tests.rs b/src/response/register/ser_relaxed/tests.rs @@ -0,0 +1,11442 @@ +use super::{ + super::{ + 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::{ + PublicKey as P256PubKey, Sec1Point as P256Pt, SecretKey as P256Key, + elliptic_curve::sec1::{FromSec1Point as _, ToSec1Point as _}, +}; +use p384::{PublicKey as P384PubKey, Sec1Point as P384Pt, SecretKey as P384Key}; +use rsa::{ + BoxedUint, RsaPrivateKey, + sha2::{Digest as _, Sha256}, + traits::PublicKeyParts as _, +}; +use serde::de::{Error as _, Unexpected}; +use serde_json::Error; +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[expect(clippy::indexing_slicing, reason = "comments justify correctness")] +#[expect( + clippy::cognitive_complexity, + clippy::too_many_lines, + reason = "a lot to test" +)] +#[test] +fn eddsa_registration_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let att_obj: [u8; 143] = [ + 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_24, + 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, + // Ed25519 COSE key. + cbor::MAP_4, + KTY, + OKP, + ALG, + EDDSA, + // `crv`. + cbor::NEG_ONE, + // `Ed25519`. + cbor::SIX, + // `x`. + cbor::NEG_TWO, + cbor::BYTES_INFO_24, + 32, + // Compressed y-coordinate. + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 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 = VerifyingKey::from_bytes(&[1; 32]) + .unwrap() + .to_public_key_der() + .unwrap(); + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let att_obj_len = att_obj.len(); + let auth_data_start = att_obj_len - 113; + let b64_adata = base64url_nopad::encode(&att_obj[auth_data_start..]); + 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": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], + "publicKey": b64_key, + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "authenticatorAttachment": "cross-platform", + "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.count() == 6 + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none() + ) + ); + // `id` and `rawId` mismatch. + let mut err = Error::invalid_value( + Unexpected::Bytes( + base64url_nopad::decode(b"ABABABABABABABABABABAA") + .unwrap() + .as_slice(), + ), + &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "ABABABABABABABABABABAA", + "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 `id`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "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(), + ); + // `null` `id`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": null, + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": null, + "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 `rawId`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "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(), + ); + // `null` `rawId`. + err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": null, + "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()) + ); + // `id` and the credential id in authenticator data mismatch. + err = Error::invalid_value( + Unexpected::Bytes( + base64url_nopad::decode(b"ABABABABABABABABABABAA") + .unwrap() + .as_slice(), + ), + &format!( + "id, rawId, and the credential id in the attested credential data to all match: {:?}", + [0u8; 16] + ) + .as_str(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "ABABABABABABABABABABAA", + "rawId": "ABABABABABABABABABABAA", + "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()) + ); + // `authenticatorData` mismatches `authData` in attestation object. + let mut bad_auth = [0; 113]; + let bad_auth_len = bad_auth.len(); + bad_auth.copy_from_slice(&att_obj[auth_data_start..]); + bad_auth[bad_auth_len - 32..].copy_from_slice([0; 32].as_slice()); + err = Error::invalid_value( + Unexpected::Bytes(bad_auth.as_slice()), + &format!("authenticator data to match the authenticator data portion of attestation object: {:?}", &att_obj[att_obj_len - bad_auth_len..]).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": base64url_nopad::encode(bad_auth.as_slice()), + "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 `authenticatorData`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "transports": [], + "publicKey": b64_key, + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null `authenticatorData`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "transports": [], + "authenticatorData": null, + "publicKey": b64_key, + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `publicKeyAlgorithm` mismatch. + 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::Eddsa).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": -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()) + ); + // 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. + err = Error::invalid_value( + Unexpected::Bytes([0; 32].as_slice()), + &format!( + "DER-encoded public key to match the public key within the attestation object: Ed25519(Ed25519PubKey({:?}))", + &att_obj[att_obj_len - 32..], + ) + .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(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()), + "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 `publicKey`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "transports": [], + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `publicKey`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": null, + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Missing `transports`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "publicKey": b64_key, + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate `transports` are allowed. + assert!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "transports": ["usb", "usb"], + "publicKey": b64_key, + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.0.response.transports.count() == 1) + ); + // `null` `transports`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "transports": null, + "publicKey": b64_key, + "publicKeyAlgorithm": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Unknown `transports`. + err = Error::invalid_value( + Unexpected::Str("Usb"), + &"'ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'", + ) + .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": ["Usb"], + "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()) + ); + // `null` `authenticatorAttachment`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "authenticatorAttachment": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); + // Unknown `authenticatorAttachment`. + err = Error::invalid_value( + Unexpected::Str("Platform"), + &"'platform' or 'cross-platform'", + ) + .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, + }, + "authenticatorAttachment": "Platform", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientDataJSON`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "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()) + ); + // `null` `clientDataJSON`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": null, + "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 `attestationObject`. + err = Error::missing_field("attestationObject") + .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, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `attestationObject`. + err = Error::invalid_type( + Unexpected::Other("null"), + &"base64url-encoded attestation object", + ) + .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": null, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `response`. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `response`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty `response`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": {}, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientExtensionResults`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `clientExtensionResults`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": null, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Missing `type`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `type`. + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .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": null + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Not exactly `public-type` `type`. + err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") + .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()) + ); + // `null`. + err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>(serde_json::json!(null).to_string().as_str()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty. + err = Error::missing_field("response").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>(serde_json::json!({}).to_string().as_str()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field in `response`. + 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": -8i8, + "attestationObject": b64_aobj, + "foo": true, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field in `response`. + err = Error::duplicate_field("transports") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\", + \"transports\": [] + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field in `PublicKeyCredential`. + 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": -8i8, + "attestationObject": b64_aobj + }, + "clientExtensionResults": {}, + "type": "public-key", + "foo": true, + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field in `PublicKeyCredential`. + err = Error::duplicate_field("id").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Base case is correct. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": "cross-platform", + "clientDataJSON": b64_cdata_json, + "clientExtensionResults": {}, + "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], + "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.count() == 6 + && matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::CrossPlatform + ) + && reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none() + ) + ); + // Missing `transports`. + err = Error::missing_field("transports").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": "cross-platform", + "clientDataJSON": b64_cdata_json, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate `transports` are allowed. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": "cross-platform", + "clientDataJSON": b64_cdata_json, + "clientExtensionResults": {}, + "transports": ["usb", "usb"], + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.0.response.transports.count() == 1) + ); + // `null` `transports`. + err = Error::invalid_type(Unexpected::Other("null"), &"AuthTransports") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": 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()) + ); + // Unknown `transports`. + err = Error::invalid_value( + Unexpected::Str("Usb"), + &"'ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": "cross-platform", + "clientDataJSON": b64_cdata_json, + "clientExtensionResults": {}, + "transports": ["Usb"], + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `authenticatorAttachment`. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": null, + "clientDataJSON": b64_cdata_json, + "clientExtensionResults": {}, + "transports": [], + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| matches!( + reg.0.authenticator_attachment, + AuthenticatorAttachment::None + )) + ); + // Unknown `authenticatorAttachment`. + err = Error::invalid_value( + Unexpected::Str("Platform"), + &"'platform' or 'cross-platform'", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "authenticatorAttachment": "Platform", + "clientDataJSON": b64_cdata_json, + "clientExtensionResults": {}, + "transports": [], + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientDataJSON`. + err = Error::missing_field("clientDataJSON") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "transports": [], + "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` `clientDataJSON`. + err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": null, + "transports": [], + "attestationObject": b64_aobj, + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `attestationObject`. + err = Error::missing_field("attestationObject") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": [], + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `attestationObject`. + err = Error::invalid_type( + Unexpected::Other("null"), + &"base64url-encoded attestation object", + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": [], + "attestationObject": null, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `clientExtensionResults`. + err = Error::missing_field("clientExtensionResults") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": [], + "attestationObject": b64_aobj, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `clientExtensionResults`. + err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": [], + "attestationObject": b64_aobj, + "clientExtensionResults": null, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `type`. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "clientDataJSON": b64_cdata_json, + "clientExtensionResults": {}, + "transports": [] + }) + .to_string() + .as_str() + ) + .is_ok_and(|_| true) + ); + // `null` `type`. + err = Error::invalid_type(Unexpected::Other("null"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "attestationObject": b64_aobj, + "clientDataJSON": b64_cdata_json, + "clientExtensionResults": {}, + "transports": [], + "type": null + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Not exactly `public-type` `type`. + err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": [], + "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`. + err = Error::invalid_type(Unexpected::Other("null"), &"CustomRegistration") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>(serde_json::json!(null).to_string().as_str()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Empty. + err = Error::missing_field("attestationObject") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>(serde_json::json!({}).to_string().as_str()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown field. + err = Error::unknown_field( + "foo", + [ + "attestationObject", + "authenticatorAttachment", + "clientDataJSON", + "clientExtensionResults", + "transports", + "type", + ] + .as_slice(), + ) + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": [], + "attestationObject": b64_aobj, + "foo": true, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field. + err = Error::duplicate_field("transports") + .to_string() + .into_bytes(); + assert_eq!( + serde_json::from_str::<CustomRegistration>( + format!( + "{{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"transports\": [], + \"attestationObject\": \"{b64_aobj}\", + \"transports\": [] + \"clientExtensionResults\": {{}}, + \"type\": \"public-key\" + }}" + ) + .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")] +#[test] +fn client_extensions() { + let c_data_json = serde_json::json!({}).to_string(); + let att_obj: [u8; 143] = [ + 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_24, + 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, + // Ed25519 COSE key. + cbor::MAP_4, + KTY, + OKP, + ALG, + EDDSA, + // `crv`. + cbor::NEG_ONE, + // `Ed25519`. + cbor::SIX, + // `x`. + cbor::NEG_TWO, + cbor::BYTES_INFO_24, + 32, + // Compressed y-coordinate. + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 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 = VerifyingKey::from_bytes(&[1; 32]) + .unwrap() + .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 auth_data_start = att_obj_len - 113; + let b64_adata = base64url_nopad::encode(&att_obj[auth_data_start..]); + 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": -8i8, + "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() + ) + ); + // `null` `credProps`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); + // `null` `prf`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": null + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() + && reg.0.client_extension_results.prf.is_none()) + ); + // Unknown `clientExtensionResults`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "CredProps": { + "rk": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field. + let mut err = Error::duplicate_field("credProps").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{ + \"credProps\": null, + \"credProps\": null + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `rk`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg + .0 + .client_extension_results + .cred_props + .is_some_and(|props| props.rk.is_none()) + && reg.0.client_extension_results.prf.is_none()) + ); + // Missing `rk`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": {} + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg + .0 + .client_extension_results + .cred_props + .is_some_and(|props| props.rk.is_none()) + && reg.0.client_extension_results.prf.is_none()) + ); + // `true` rk`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg + .0 + .client_extension_results + .cred_props + .is_some_and(|props| props.rk.unwrap_or_default()) + && reg.0.client_extension_results.prf.is_none()) + ); + // `false` rk`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "rk": false + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg + .0 + .client_extension_results + .cred_props + .is_some_and(|props| props.rk.is_some_and(|rk| !rk)) + && reg.0.client_extension_results.prf.is_none()) + ); + // Invalid `rk`. + err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") + .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": { + "credProps": { + "rk": 3u8 + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown `credProps` field. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "credProps": { + "Rk": true, + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field in `credProps`. + err = Error::duplicate_field("rk").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{ + \"credProps\": {{ + \"rk\": true, + \"rk\": true + }} + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `enabled`. + err = Error::invalid_type(Unexpected::Other("null"), &"a boolean") + .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": { + "prf": { + "enabled": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `enabled`. + err = Error::missing_field("enabled").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": { + "prf": {} + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `true` `enabled`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() + && reg + .0 + .client_extension_results + .prf + .is_some_and(|prf| prf.enabled)) + ); + // `false` `enabled`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": false, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() + && reg + .0 + .client_extension_results + .prf + .is_some_and(|prf| !prf.enabled)) + ); + // Invalid `enabled`. + err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") + .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": { + "prf": { + "enabled": 3u8 + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `results` with `enabled` `true`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": null, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() + && reg + .0 + .client_extension_results + .prf + .is_some_and(|prf| prf.enabled)) + ); + // `null` `results` with `enabled` `false`. + err = Error::custom( + "prf must not have 'results', including a null 'results', if 'enabled' is false", + ) + .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": { + "prf": { + "enabled": false, + "results": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Duplicate field in `prf`. + err = Error::duplicate_field("enabled").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": {{ + \"enabled\": true, + \"enabled\": true + }} + }}, + \"type\": \"public-key\" + }}" + ) + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `first`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": {}, + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `first`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": { + "first": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() + && reg + .0 + .client_extension_results + .prf + .is_some_and(|prf| prf.enabled)) + ); + // `null` `second`. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": { + "first": null, + "second": null + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() + && reg + .0 + .client_extension_results + .prf + .is_some_and(|prf| prf.enabled)) + ); + // Non-`null` `first`. + err = Error::invalid_type(Unexpected::Option, &"null") + .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": { + "prf": { + "enabled": true, + "results": { + "first": "" + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Non-`null` `second`. + err = Error::invalid_type(Unexpected::Option, &"null") + .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": { + "prf": { + "enabled": true, + "results": { + "first": null, + "second": "" + }, + } + }, + "type": "public-key" + }) + .to_string() + .as_str() + ) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Unknown `prf` field. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "Results": null + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Unknown `results` field. + 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": -8i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": { + "prf": { + "enabled": true, + "results": { + "first": null, + "Second": null + } + } + }, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Duplicate field in `results`. + err = Error::duplicate_field("first").to_string().into_bytes(); + assert_eq!( + serde_json::from_str::<RegistrationRelaxed>( + format!( + "{{ + \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", + \"response\": {{ + \"clientDataJSON\": \"{b64_cdata_json}\", + \"authenticatorData\": \"{b64_adata}\", + \"transports\": [], + \"publicKey\": \"{b64_key}\", + \"publicKeyAlgorithm\": -8, + \"attestationObject\": \"{b64_aobj}\" + }}, + \"clientExtensionResults\": {{ + \"prf\": {{ + \"enabled\": true, + \"results\": {{ + \"first\": null, + \"first\": null + }} + }} + }}, + \"type\": \"public-key\" + }}" + ) + .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 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")] +#[test] +fn es256_registration_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let mut att_obj: [u8; 178] = [ + 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_24, + 148, + // `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, + // P-256 COSE key. + cbor::MAP_5, + KTY, + EC2, + ALG, + ES256, + // `crv`. + cbor::NEG_ONE, + // `P-256`. + cbor::ONE, + // `x`. + cbor::NEG_TWO, + cbor::BYTES_INFO_24, + 32, + // x-coordinate. 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, + // `y`. + cbor::NEG_THREE, + cbor::BYTES_INFO_24, + 32, + // y-coordinate. 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, + ]; + let key = P256Key::from_bytes( + &[ + 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115, + 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95, + ] + .into(), + ) + .unwrap() + .public_key(); + let enc_key = key.to_sec1_point(false); + let pub_key = key.to_public_key_der().unwrap(); + let att_obj_len = att_obj.len(); + let x_start = att_obj_len - 67; + let y_meta_start = x_start + 32; + let y_start = y_meta_start + 3; + att_obj[x_start..y_meta_start].copy_from_slice(enc_key.x().unwrap()); + att_obj[y_start..].copy_from_slice(enc_key.y().unwrap()); + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 148..]); + 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": -7i8, + "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::Es256).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 = P256PubKey::from_sec1_point(&P256Pt::from_affine_coordinates( + &[ + 66, 71, 188, 41, 125, 2, 226, 44, 148, 62, 63, 190, 172, 64, 33, 214, 6, 37, 148, 23, + 240, 235, 203, 84, 112, 219, 232, 197, 54, 182, 17, 235, + ] + .into(), + &[ + 22, 172, 123, 13, 170, 242, 217, 248, 193, 209, 206, 163, 92, 4, 162, 168, 113, 63, 2, + 117, 16, 223, 239, 196, 109, 179, 10, 130, 43, 213, 205, 92, + ] + .into(), + false, + )) + .unwrap(); + err = Error::invalid_value( + Unexpected::Bytes([0; 32].as_slice()), + &format!( + "DER-encoded public key to match the public key within the attestation object: P256(UncompressedP256PubKey({:?}, {:?}))", + &att_obj[x_start..y_meta_start], + &att_obj[y_start..], + ) + .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": -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()) + ); + // Missing `publicKey`. + drop( + 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(), + ); + // `null` `publicKey`. + drop( + 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(), + ); + // Base case is valid. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": [], + "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() + ) + ); +} +#[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")] +#[test] +fn es384_registration_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let mut att_obj: [u8; 211] = [ + 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_24, + 181, + // `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, + // P-384 COSE key. + cbor::MAP_5, + KTY, + EC2, + ALG, + cbor::NEG_INFO_24, + ES384, + // `crv`. + cbor::NEG_ONE, + // `P-384`. + cbor::TWO, + // `x`. + cbor::NEG_TWO, + cbor::BYTES_INFO_24, + 48, + // x-coordinate. 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, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // `y`. + cbor::NEG_THREE, + cbor::BYTES_INFO_24, + 48, + // y-coordinate. 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, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ]; + let key = P384Key::from_bytes( + &[ + 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232, 42, + 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1, 32, 86, + 220, 68, 182, 11, 105, 223, 75, 70, + ] + .into(), + ) + .unwrap() + .public_key(); + let enc_key = key.to_sec1_point(false); + let pub_key = key.to_public_key_der().unwrap(); + let att_obj_len = att_obj.len(); + let x_start = att_obj_len - 99; + let y_meta_start = x_start + 48; + let y_start = y_meta_start + 3; + att_obj[x_start..y_meta_start].copy_from_slice(enc_key.x().unwrap()); + att_obj[y_start..].copy_from_slice(enc_key.y().unwrap()); + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 181..]); + 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": -35i8, + "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::Es256).as_str()), + &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).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": -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()) + ); + // 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 = P384PubKey::from_sec1_point(&P384Pt::from_affine_coordinates( + &[ + 192, 10, 27, 46, 66, 67, 80, 98, 33, 230, 156, 95, 1, 135, 150, 110, 64, 243, 22, 118, + 5, 255, 107, 44, 234, 111, 217, 105, 125, 114, 39, 7, 126, 2, 191, 111, 48, 93, 234, + 175, 18, 172, 59, 28, 97, 106, 178, 152, + ] + .into(), + &[ + 57, 36, 196, 12, 109, 129, 253, 115, 88, 154, 6, 43, 195, 85, 169, 5, 230, 51, 28, 205, + 142, 28, 150, 35, 24, 222, 170, 253, 14, 248, 84, 151, 109, 191, 152, 111, 222, 70, + 134, 247, 109, 171, 211, 33, 214, 217, 200, 111, + ] + .into(), + false, + )) + .unwrap(); + err = Error::invalid_value( + Unexpected::Bytes([0; 32].as_slice()), + &format!( + "DER-encoded public key to match the public key within the attestation object: P384(UncompressedP384PubKey({:?}, {:?}))", + &att_obj[x_start..y_meta_start], + &att_obj[y_start..], + ) + .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": -35i8, + "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`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "transports": [], + "publicKeyAlgorithm": -35i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `publicKeyAlgorithm` mismatch when `publicKey` does not exist. + 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::Es384).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": -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()) + ); + // `null` `publicKey`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": null, + "publicKeyAlgorithm": -35i8, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `publicKeyAlgorithm` mismatch when `publicKey` is null. + 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::Es384).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": -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()) + ); + // Base case is valid. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": [], + "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() + ) + ); +} +#[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")] +#[test] +fn rs256_registration_deserialize_data_mismatch() { + let c_data_json = serde_json::json!({}).to_string(); + let mut att_obj: [u8; 374] = [ + 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, + 1, + 87, + // `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, + // RSA COSE key. + cbor::MAP_4, + KTY, + RSA, + ALG, + cbor::NEG_INFO_25, + // RS256. + 1, + 0, + // `n`. + cbor::NEG_ONE, + cbor::BYTES_INFO_25, + 1, + 0, + // n. 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, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // `e`. + cbor::NEG_TWO, + cbor::BYTES | 3, + // e. + 1, + 0, + 1, + ]; + let n = [ + 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107, 195, + 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206, 185, 19, + 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165, 100, 128, + 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204, 145, 50, 174, + 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64, 87, 145, 45, 32, + 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105, 81, 217, 151, 162, + 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55, 117, 248, 233, 151, + 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21, 254, 81, 202, 64, 232, + 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157, 108, 139, 253, 230, 80, + 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188, 42, 198, 212, 23, 182, 222, + 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56, 104, 68, 94, 198, 196, 35, 200, + 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13, 72, 93, 53, 65, 111, 59, 242, 122, + 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132, 153, 79, 0, 133, 78, 7, 218, 165, 241, + ]; + let e = 0x0001_0001u32; + let d = [ + 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0, + 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168, 35, + 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108, 154, + 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102, 6, 219, + 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247, 82, 104, + 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166, 216, 152, 94, + 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111, 215, 107, 212, + 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242, 140, 252, 248, 162, + 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212, 249, 237, 203, 202, 48, 26, + 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83, 31, 253, 55, 43, 158, 90, 248, + 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153, 162, 60, 91, 99, 220, 51, 44, 85, + 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142, 24, 245, 127, 122, 247, 152, 212, 75, + 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45, 50, 23, 3, 25, 253, 87, 227, 79, 119, 161, + ]; + let p = BoxedUint::from_le_slice_vartime( + [ + 215, 166, 5, 21, 11, 179, 41, 77, 198, 92, 165, 48, 77, 162, 42, 41, 206, 141, 60, 69, + 47, 164, 19, 92, 46, 72, 100, 238, 100, 53, 214, 197, 163, 185, 6, 140, 229, 250, 195, + 77, 8, 12, 5, 236, 178, 173, 86, 201, 43, 213, 165, 51, 108, 101, 161, 99, 76, 240, 14, + 234, 76, 197, 137, 53, 198, 168, 135, 205, 212, 198, 120, 29, 16, 82, 98, 233, 236, + 177, 12, 171, 141, 100, 107, 146, 33, 176, 125, 202, 172, 79, 147, 179, 30, 62, 247, + 206, 169, 19, 168, 114, 26, 73, 108, 178, 105, 84, 89, 191, 168, 253, 228, 214, 54, 16, + 212, 199, 111, 72, 3, 41, 247, 227, 165, 244, 32, 188, 24, 247, + ] + .as_slice(), + ); + let p_2 = BoxedUint::from_le_slice_vartime( + [ + 41, 25, 198, 240, 134, 206, 121, 57, 11, 5, 134, 192, 212, 77, 229, 197, 14, 78, 85, + 212, 190, 114, 179, 188, 21, 171, 174, 12, 104, 74, 15, 164, 136, 173, 62, 177, 141, + 213, 93, 102, 147, 83, 59, 124, 146, 59, 175, 213, 55, 27, 25, 248, 154, 29, 39, 85, + 50, 235, 134, 60, 203, 106, 186, 195, 190, 185, 71, 169, 142, 236, 92, 11, 250, 187, + 198, 8, 201, 184, 120, 178, 227, 87, 63, 243, 89, 227, 234, 184, 28, 252, 112, 211, + 193, 69, 23, 92, 5, 72, 93, 53, 69, 159, 73, 160, 105, 244, 249, 94, 214, 173, 9, 236, + 4, 255, 129, 11, 224, 140, 252, 168, 57, 143, 176, 241, 60, 219, 90, 250, + ] + .as_slice(), + ); + let key = RsaPrivateKey::from_components( + BoxedUint::from_le_slice_vartime(n.as_slice()), + e.into(), + BoxedUint::from_le_slice_vartime(d.as_slice()), + vec![p, p_2], + ) + .unwrap() + .to_public_key(); + let pub_key = key.to_public_key_der().unwrap(); + let att_obj_len = att_obj.len(); + let n_start = att_obj_len - 261; + let e_start = n_start + 256; + att_obj[n_start..e_start].copy_from_slice(key.n().to_be_bytes().as_ref()); + let b64_cdata_json = base64url_nopad::encode(c_data_json.as_bytes()); + let b64_adata = base64url_nopad::encode(&att_obj[att_obj_len - 343..]); + 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": -257i16, + "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::Rs256).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 = RsaPrivateKey::from_components( + BoxedUint::from_le_slice_vartime( + [ + 175, 161, 161, 75, 52, 244, 72, 168, 29, 119, 33, 120, 3, 222, 231, 152, 222, 119, + 112, 83, 221, 237, 74, 174, 79, 216, 147, 251, 245, 94, 234, 114, 254, 21, 17, 254, + 8, 115, 75, 127, 150, 87, 59, 109, 230, 116, 85, 90, 11, 160, 63, 217, 9, 38, 187, + 250, 226, 183, 38, 164, 182, 218, 22, 19, 58, 189, 83, 219, 11, 144, 15, 99, 151, + 166, 46, 57, 17, 111, 189, 131, 142, 113, 85, 122, 188, 238, 52, 21, 116, 125, 102, + 195, 182, 165, 29, 156, 213, 182, 125, 156, 88, 56, 221, 2, 98, 43, 210, 115, 32, + 4, 105, 88, 181, 158, 207, 236, 162, 250, 253, 240, 72, 8, 253, 50, 220, 247, 76, + 170, 143, 68, 225, 231, 113, 64, 244, 17, 138, 162, 233, 33, 2, 67, 11, 223, 188, + 232, 152, 193, 20, 32, 243, 52, 64, 43, 2, 243, 8, 77, 150, 232, 109, 148, 95, 127, + 55, 71, 162, 34, 54, 83, 135, 52, 172, 191, 32, 42, 106, 43, 211, 206, 100, 104, + 110, 232, 5, 43, 120, 180, 166, 40, 144, 233, 239, 103, 134, 103, 255, 224, 138, + 184, 208, 137, 127, 36, 189, 143, 248, 201, 2, 218, 51, 232, 96, 30, 83, 124, 109, + 241, 23, 179, 247, 151, 238, 212, 204, 44, 43, 223, 148, 241, 172, 10, 235, 155, + 94, 68, 116, 24, 116, 191, 86, 53, 127, 35, 133, 198, 204, 59, 76, 110, 16, 1, 15, + 148, 135, 157, + ] + .as_slice(), + ), + 0x0001_0001u32.into(), + BoxedUint::from_le_slice_vartime( + [ + 129, 93, 123, 251, 104, 29, 84, 203, 116, 100, 75, 237, 111, 160, 12, 100, 172, 76, + 57, 178, 144, 235, 81, 61, 115, 243, 28, 40, 183, 22, 56, 150, 68, 38, 220, 62, + 233, 110, 48, 174, 35, 197, 244, 109, 148, 109, 36, 69, 69, 82, 225, 113, 175, 6, + 239, 27, 193, 101, 50, 239, 122, 102, 7, 46, 98, 79, 195, 116, 155, 158, 138, 147, + 51, 93, 24, 237, 246, 82, 14, 109, 144, 250, 239, 93, 63, 214, 96, 130, 226, 134, + 198, 145, 161, 11, 231, 97, 214, 180, 255, 95, 158, 88, 108, 254, 243, 177, 133, + 184, 92, 95, 148, 88, 55, 124, 245, 244, 84, 86, 4, 121, 44, 231, 97, 176, 190, 29, + 155, 40, 57, 69, 165, 80, 168, 9, 56, 43, 233, 6, 14, 157, 112, 223, 64, 88, 141, + 7, 65, 23, 64, 208, 6, 83, 61, 8, 182, 248, 126, 84, 179, 163, 80, 238, 90, 133, 4, + 14, 71, 177, 175, 27, 29, 151, 211, 108, 162, 195, 7, 157, 167, 86, 169, 3, 87, + 235, 89, 158, 237, 216, 31, 243, 197, 62, 5, 84, 131, 230, 186, 248, 49, 12, 93, + 244, 61, 135, 180, 17, 162, 241, 13, 115, 241, 138, 219, 98, 155, 166, 191, 63, 12, + 37, 1, 165, 178, 84, 200, 72, 80, 41, 77, 136, 217, 141, 246, 209, 31, 243, 159, + 71, 43, 246, 159, 182, 171, 116, 12, 3, 142, 235, 218, 164, 70, 90, 147, 238, 42, + 75, + ] + .as_slice(), + ), + vec![ + BoxedUint::from_le_slice_vartime( + [ + 215, 199, 110, 28, 64, 16, 16, 109, 106, 152, 150, 124, 52, 166, 121, 92, 242, + 13, 0, 69, 7, 152, 72, 172, 118, 63, 156, 180, 140, 39, 53, 29, 197, 224, 177, + 48, 41, 221, 102, 65, 17, 185, 55, 62, 219, 152, 227, 7, 78, 219, 14, 139, 71, + 204, 144, 152, 14, 39, 247, 244, 165, 224, 234, 60, 213, 74, 237, 30, 102, 177, + 242, 138, 168, 31, 122, 47, 206, 155, 225, 113, 103, 175, 152, 244, 27, 233, + 112, 223, 248, 38, 215, 178, 20, 244, 8, 121, 26, 11, 70, 122, 16, 85, 167, 87, + 64, 216, 228, 227, 173, 57, 250, 8, 221, 38, 12, 203, 212, 1, 112, 43, 72, 91, + 225, 97, 228, 57, 154, 193, + ] + .as_slice(), + ), + BoxedUint::from_le_slice_vartime( + [ + 233, 89, 204, 152, 31, 242, 8, 110, 38, 190, 111, 159, 105, 105, 45, 85, 15, + 244, 30, 250, 174, 226, 219, 111, 107, 191, 196, 135, 17, 123, 186, 167, 85, + 13, 120, 197, 159, 129, 78, 237, 152, 31, 230, 26, 229, 253, 197, 211, 105, + 204, 126, 142, 250, 55, 26, 172, 65, 160, 45, 6, 99, 86, 66, 238, 107, 6, 98, + 171, 93, 224, 201, 160, 31, 204, 82, 120, 228, 158, 238, 6, 190, 12, 150, 153, + 239, 95, 57, 71, 100, 239, 235, 155, 73, 200, 5, 225, 127, 185, 46, 48, 243, + 84, 33, 142, 17, 19, 20, 23, 215, 16, 114, 58, 211, 14, 73, 148, 168, 252, 159, + 252, 125, 57, 101, 211, 188, 12, 77, 208, + ] + .as_slice(), + ), + ], + ) + .unwrap() + .to_public_key(); + err = Error::invalid_value( + Unexpected::Bytes([0; 32].as_slice()), + &format!( + "DER-encoded public key to match the public key within the attestation object: Rsa(RsaPubKey({:?}, 65537))", + &att_obj[n_start..e_start], + ) + .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": -257i16, + "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`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "transports": [], + "publicKeyAlgorithm": -257i16, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // `null` `publicKey`. + drop( + serde_json::from_str::<RegistrationRelaxed>( + serde_json::json!({ + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "rawId": "AAAAAAAAAAAAAAAAAAAAAA", + "response": { + "clientDataJSON": b64_cdata_json, + "authenticatorData": b64_adata, + "transports": [], + "publicKey": null, + "publicKeyAlgorithm": -257i16, + "attestationObject": b64_aobj, + }, + "clientExtensionResults": {}, + "type": "public-key" + }) + .to_string() + .as_str(), + ) + .unwrap(), + ); + // Base case is valid. + assert!( + serde_json::from_str::<CustomRegistration>( + serde_json::json!({ + "clientDataJSON": b64_cdata_json, + "transports": [], + "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() + ) + ); +} diff --git a/src/response/register/tests.rs b/src/response/register/tests.rs @@ -0,0 +1,421 @@ +use super::{ + super::{ + super::{ + AggErr, + request::{AsciiDomain, RpId}, + }, + auth::{AuthenticatorData, NonDiscoverableAuthenticatorAssertion}, + }, + AttestationFormat, AttestationObject, AuthDataContainer as _, AuthExtOutput as _, + AuthTransports, AuthenticatorAttestation, AuthenticatorExtensionOutput, + AuthenticatorExtensionOutputErr, Backup, CborSuccess, CredentialProtectionPolicy, + FourToSixtyThree, FromCbor as _, HmacSecret, Sig, UncompressedPubKey, + cbor::{ + BYTES, BYTES_INFO_24, MAP_1, MAP_2, MAP_3, MAP_4, SIMPLE_FALSE, SIMPLE_TRUE, TEXT_11, + TEXT_12, TEXT_14, + }, +}; +use ed25519_dalek::Verifier as _; +use p256::ecdsa::{DerSignature as P256Sig, SigningKey as P256Key}; +use rsa::sha2::{Digest as _, Sha256}; +#[expect(clippy::panic, reason = "OK in tests")] +#[expect( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::missing_asserts_for_indexing, + reason = "comments justifies correctness" +)] +fn hex_decode<const N: usize>(input: &[u8; N]) -> Vec<u8> { + /// Value to subtract from a lowercase hex digit. + const LOWER_OFFSET: u8 = b'a' - 10; + assert_eq!( + N & 1, + 0, + "hex_decode must be passed a reference to an array of even length" + ); + let mut data = Vec::with_capacity(N >> 1); + input.chunks_exact(2).fold((), |(), byte| { + // `byte.len() == 2`. + let mut hex = byte[0]; + let val = match hex { + // `Won't underflow`. + b'0'..=b'9' => hex - b'0', + // `Won't underflow`. + b'a'..=b'f' => hex - LOWER_OFFSET, + _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"), + } << 4u8; + // `byte.len() == 2`. + hex = byte[1]; + data.push( + val | match hex { + // `Won't underflow`. + b'0'..=b'9' => hex - b'0', + // `Won't underflow`. + b'a'..=b'f' => hex - LOWER_OFFSET, + _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"), + }, + ); + }); + data +} +/// <https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-none-es256> +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_in_result, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[test] +fn es256_test_vector() -> Result<(), AggErr> { + let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?); + let credential_private_key = + hex_decode(b"6e68e7a58484a3264f66b77f5d6dc5bc36a47085b615c9727ab334e8c369c2ee"); + let aaguid = hex_decode(b"8446ccb9ab1db374750b2367ff6f3a1f"); + let credential_id = + hex_decode(b"f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4"); + let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c4535513d3d227d"); + let attestation_object = hex_decode(b"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220"); + let key = *P256Key::from_slice(credential_private_key.as_slice()) + .unwrap() + .verifying_key(); + let enc_key = key.to_sec1_point(false); + let auth_attest = + AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0)); + let att_obj = + AttestationObject::from_data(auth_attest.attestation_object_and_c_data_hash.as_slice())?; + assert_eq!( + aaguid, + att_obj.data.auth_data.attested_credential_data.aaguid.0 + ); + assert_eq!( + credential_id, + att_obj + .data + .auth_data + .attested_credential_data + .credential_id + .0 + ); + assert!( + matches!(att_obj.data.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if **enc_key.x().unwrap() == *pub_key.0 && **enc_key.y().unwrap() == *pub_key.1) + ); + assert_eq!( + *att_obj.data.auth_data.rp_id_hash, + *Sha256::digest(rp_id.as_ref()) + ); + assert!(att_obj.data.auth_data.flags.user_present); + assert!(matches!(att_obj.data.attestation, AttestationFormat::None)); + let authenticator_data = + hex_decode(b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000"); + let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d"); + let signature = hex_decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87"); + let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user( + client_data_json_2, + authenticator_data, + signature, + ); + let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; + assert_eq!(*auth_data.rp_id_hash(), *Sha256::digest(rp_id.as_ref())); + assert!(auth_data.flags().user_present); + assert!(match att_obj.data.auth_data.flags.backup { + Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible), + Backup::Eligible => !matches!(auth_data.flags().backup, Backup::NotEligible), + Backup::Exists => matches!(auth_data.flags().backup, Backup::Exists), + }); + let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap(); + let mut msg = auth_assertion.authenticator_data().to_owned(); + msg.extend_from_slice(&Sha256::digest(auth_assertion.client_data_json())); + key.verify(msg.as_slice(), &sig).unwrap(); + Ok(()) +} +/// <https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-packed-self-es256> +#[expect( + clippy::panic_in_result_fn, + clippy::unwrap_in_result, + clippy::unwrap_used, + reason = "OK in tests" +)] +#[expect(clippy::indexing_slicing, reason = "comment justifies correctness")] +#[test] +fn es256_self_attest_test_vector() -> Result<(), AggErr> { + let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?); + let credential_private_key = + hex_decode(b"b4bbfa5d68e1693b6ef5a19a0e60ef7ee2cbcac81f7fec7006ac3a21e0c5116a"); + let aaguid = hex_decode(b"df850e09db6afbdfab51697791506cfc"); + let credential_id = + hex_decode(b"455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c"); + let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f3078594856673d3d227d"); + let attestation_object = hex_decode(b"a363666d74667061636b65646761747453746d74a263616c67266373696758483046022100ae045923ded832b844cae4d5fc864277c0dc114ad713e271af0f0d371bd3ac540221009077a088ed51a673951ad3ba2673d5029bab65b64f4ea67b234321f86fcfac5d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2"); + let key = *P256Key::from_slice(credential_private_key.as_slice()) + .unwrap() + .verifying_key(); + let enc_key = key.to_sec1_point(false); + let auth_attest = + AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0)); + let (att_obj, auth_idx) = AttestationObject::parse_data(auth_attest.attestation_object())?; + assert_eq!(aaguid, att_obj.auth_data.attested_credential_data.aaguid.0); + assert_eq!( + credential_id, + att_obj.auth_data.attested_credential_data.credential_id.0 + ); + assert!( + matches!(att_obj.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if **enc_key.x().unwrap() == *pub_key.0 && **enc_key.y().unwrap() == *pub_key.1) + ); + assert_eq!( + *att_obj.auth_data.rp_id_hash, + *Sha256::digest(rp_id.as_ref()) + ); + assert!(att_obj.auth_data.flags.user_present); + assert!(match att_obj.attestation { + AttestationFormat::None => false, + AttestationFormat::Packed(attest) => { + match attest.signature { + 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( + // Won't `panic` since `auth_idx` is returned from `AttestationObject::parse_data`. + &auth_attest.attestation_object_and_c_data_hash[auth_idx..], + &s, + ) + .is_ok() + } + } + } + }); + let authenticator_data = + hex_decode(b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000"); + let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d"); + let signature = hex_decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744"); + let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user( + client_data_json_2, + authenticator_data, + signature, + ); + let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; + assert_eq!(*auth_data.rp_id_hash(), *Sha256::digest(rp_id.as_ref())); + assert!(auth_data.flags().user_present); + assert!(match att_obj.auth_data.flags.backup { + Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible), + Backup::Eligible | Backup::Exists => + !matches!(auth_data.flags().backup, Backup::NotEligible), + }); + let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap(); + let mut msg = auth_assertion.authenticator_data().to_owned(); + msg.extend_from_slice(&Sha256::digest(auth_assertion.client_data_json())); + key.verify(msg.as_slice(), &sig).unwrap(); + Ok(()) +} +struct AuthExtOptions<'a> { + cred_protect: Option<u8>, + hmac_secret: Option<bool>, + min_pin_length: Option<u8>, + hmac_secret_mc: Option<&'a [u8]>, +} +#[expect( + clippy::panic, + clippy::unreachable, + reason = "want to crash when there is a bug" +)] +#[expect( + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "comments justify correctness" +)] +fn generate_auth_extensions(opts: &AuthExtOptions<'_>) -> Vec<u8> { + // Maxes at 4, so addition is clearly free from overflow. + let map_len = u8::from(opts.cred_protect.is_some()) + + u8::from(opts.hmac_secret.is_some()) + + u8::from(opts.min_pin_length.is_some()) + + u8::from(opts.hmac_secret_mc.is_some()); + let header = match map_len { + 0 => return Vec::new(), + 1 => MAP_1, + 2 => MAP_2, + 3 => MAP_3, + 4 => MAP_4, + _ => unreachable!("bug"), + }; + let mut cbor = Vec::with_capacity(128); + cbor.push(header); + if let Some(protect) = opts.cred_protect { + cbor.push(TEXT_11); + cbor.extend_from_slice(b"credProtect".as_slice()); + if protect >= 24 { + cbor.push(24); + } + cbor.push(protect); + } + if let Some(hmac) = opts.hmac_secret { + cbor.push(TEXT_11); + cbor.extend_from_slice(b"hmac-secret".as_slice()); + cbor.push(if hmac { SIMPLE_TRUE } else { SIMPLE_FALSE }); + } + if let Some(pin) = opts.min_pin_length { + cbor.push(TEXT_12); + cbor.extend_from_slice(b"minPinLength".as_slice()); + if pin >= 24 { + cbor.push(24); + } + cbor.push(pin); + } + if let Some(mc) = opts.hmac_secret_mc { + cbor.push(TEXT_14); + cbor.extend_from_slice(b"hmac-secret-mc".as_slice()); + match mc.len() { + len @ ..=23 => { + // `as` is clearly OK. + cbor.push(BYTES | len as u8); + } + len @ 24..=255 => { + cbor.push(BYTES_INFO_24); + // `as` is clearly OK. + cbor.push(len as u8); + } + _ => panic!( + "AuthExtOptions does not allow hmac_secret_mc to have length greater than 255" + ), + } + cbor.extend_from_slice(mc); + } + cbor +} +#[expect(clippy::panic_in_result_fn, reason = "not a problem for a test")] +#[expect(clippy::shadow_unrelated, reason = "struct destructuring is prefered")] +#[expect(clippy::too_many_lines, reason = "a lot to test")] +#[test] +fn auth_ext() -> Result<(), AuthenticatorExtensionOutputErr> { + let mut opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: None, + hmac_secret: None, + min_pin_length: None, + hmac_secret_mc: None, + }); + let CborSuccess { value, remaining } = + AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; + assert!(remaining.is_empty()); + assert!(value.missing()); + opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: None, + hmac_secret: None, + min_pin_length: None, + hmac_secret_mc: Some([0; 48].as_slice()), + }); + assert!( + AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( + |e| matches!(e, AuthenticatorExtensionOutputErr::Missing), + |_| false, + ) + ); + opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: None, + hmac_secret: Some(true), + min_pin_length: None, + hmac_secret_mc: Some([0; 48].as_slice()), + }); + let CborSuccess { value, remaining } = + AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; + assert!(remaining.is_empty()); + assert!( + matches!(value.cred_protect, CredentialProtectionPolicy::None) + && matches!(value.hmac_secret, HmacSecret::One) + && value.min_pin_length.is_none() + ); + opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: None, + hmac_secret: Some(false), + min_pin_length: None, + hmac_secret_mc: Some([0; 48].as_slice()), + }); + assert!( + AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( + |e| matches!(e, AuthenticatorExtensionOutputErr::Missing), + |_| false, + ) + ); + opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: None, + hmac_secret: Some(true), + min_pin_length: None, + hmac_secret_mc: Some([0; 49].as_slice()), + }); + assert!( + AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( + |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcValue), + |_| false, + ) + ); + opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: None, + hmac_secret: Some(true), + min_pin_length: None, + hmac_secret_mc: Some([0; 23].as_slice()), + }); + assert!( + AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( + |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcType), + |_| false, + ) + ); + opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: Some(1), + hmac_secret: Some(true), + min_pin_length: Some(5), + hmac_secret_mc: Some([0; 48].as_slice()), + }); + let CborSuccess { value, remaining } = + AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; + assert!(remaining.is_empty()); + assert!( + matches!( + value.cred_protect, + CredentialProtectionPolicy::UserVerificationOptional + ) && matches!(value.hmac_secret, HmacSecret::One) + && value + .min_pin_length + .is_some_and(|pin| pin == FourToSixtyThree::Five) + ); + opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: Some(0), + hmac_secret: None, + min_pin_length: None, + hmac_secret_mc: None, + }); + assert!( + AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( + |e| matches!(e, AuthenticatorExtensionOutputErr::CredProtectValue), + |_| false, + ) + ); + opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: None, + hmac_secret: None, + min_pin_length: Some(3), + hmac_secret_mc: None, + }); + assert!( + AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( + |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue), + |_| false, + ) + ); + opts = generate_auth_extensions(&AuthExtOptions { + cred_protect: None, + hmac_secret: None, + min_pin_length: Some(64), + hmac_secret_mc: None, + }); + assert!( + AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( + |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue), + |_| false, + ) + ); + Ok(()) +} diff --git a/src/response/ser_relaxed.rs b/src/response/ser_relaxed.rs @@ -1,4 +1,6 @@ extern crate alloc; +#[cfg(test)] +mod tests; #[cfg(doc)] use super::{Challenge, LimitedVerificationParser}; use super::{ @@ -408,602 +410,3 @@ impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfValuesRelaxed { .map(Self) } } -#[cfg(test)] -mod tests { - use super::{ClientDataJsonParser as _, Cow, RelaxedClientDataJsonParser}; - use serde::de::{Error as _, Unexpected}; - use serde_json::Error; - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[expect(clippy::little_endian_bytes, reason = "comments justify correctness")] - #[expect(clippy::too_many_lines, reason = "a lot to test")] - #[test] - fn relaxed_client_data_json() { - // Base case is correct. - let mut input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok_and(|c| { - c.cross_origin - && c.challenge.0 - // challenges are sent little-endian - == u128::from_le_bytes([ - 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, - ]) - && matches!(c.origin.0, Cow::Borrowed(o) if o == "https://example.com") - && c.top_origin.is_some_and( - |t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org"), - ) - }) - ); - // Base case is correct. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert!( - RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).is_ok_and(|c| { - c.cross_origin - && c.challenge.0 - // challenges are sent little-endian - == u128::from_le_bytes([ - 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, - ]) - && matches!(c.origin.0, Cow::Borrowed(o) if o == "https://example.com") - && c.top_origin.is_some_and( - |t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org"), - ) - }) - ); - // Unknown keys are allowed. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org", - "foo": true - }) - .to_string(); - drop(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).unwrap()); - // Duplicate keys are forbidden. - let mut input_str = "{ - \"challenge\": \"ABABABABABABABABABABAA\", - \"type\": \"webauthn.create\", - \"origin\": \"https://example.com\", - \"crossOrigin\": true, - \"topOrigin\": \"https://example.org\", - \"crossOrigin\": true - }"; - let mut err = Error::duplicate_field("crossOrigin") - .to_string() - .into_bytes(); - assert_eq!( - RelaxedClientDataJsonParser::<true>::parse(input_str.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `crossOrigin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": null, - "topOrigin": "https://example.org" - }) - .to_string(); - assert!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .is_ok_and(|c| !c.cross_origin) - ); - // Missing `crossOrigin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "topOrigin": "https://example.org" - }) - .to_string(); - assert!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .is_ok_and(|c| !c.cross_origin) - ); - // `null` `topOrigin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": null - }) - .to_string(); - assert!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .is_ok_and(|c| c.top_origin.is_none()) - ); - // Missing `topOrigin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - }) - .to_string(); - assert!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .is_ok_and(|c| c.top_origin.is_none()) - ); - // `null` `challenge`. - err = Error::invalid_type( - Unexpected::Other("null"), - &"base64 encoding of the 16-byte challenge in a URL safe way without padding", - ) - .to_string() - .into_bytes(); - input = serde_json::json!({ - "challenge": null, - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `challenge`. - err = Error::missing_field("challenge").to_string().into_bytes(); - input = serde_json::json!({ - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `type`. - err = Error::invalid_type( - Unexpected::Other("null"), - &"'webauthn.create' or 'webauthn.get'", - ) - .to_string() - .into_bytes(); - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": null, - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `type`. - err = Error::missing_field("type").to_string().into_bytes(); - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `origin`. - err = Error::invalid_type(Unexpected::Other("null"), &"OriginWrapper") - .to_string() - .into_bytes(); - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": null, - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `origin`. - err = Error::missing_field("origin").to_string().into_bytes(); - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Mismatched `type`. - err = Error::invalid_value(Unexpected::Str("webauthn.create"), &"webauthn.get") - .to_string() - .into_bytes(); - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Mismatched `type`. - err = Error::invalid_value(Unexpected::Str("webauthn.get"), &"webauthn.create") - .to_string() - .into_bytes(); - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `crossOrigin` can be `false` even when `topOrigin` exists. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": "https://example.com", - "crossOrigin": false, - "topOrigin": "https://example.org" - }) - .to_string(); - drop(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).unwrap()); - // `crossOrigin` can be `true` even when `topOrigin` does not exist. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": "https://example.com", - "crossOrigin": true, - }) - .to_string(); - drop(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).unwrap()); - // BOM is removed. - input_str = "\u{feff}{ - \"challenge\": \"ABABABABABABABABABABAA\", - \"type\": \"webauthn.create\", - \"origin\": \"https://example.com\", - \"crossOrigin\": true, - \"topOrigin\": \"https://example.org\" - }"; - drop(RelaxedClientDataJsonParser::<true>::parse(input_str.as_bytes()).unwrap()); - // Invalid Unicode is replaced. - let mut input_bytes = b"{ - \"challenge\": \"ABABABABABABABABABABAA\", - \"type\": \"webauthn.create\", - \"origin\": \"https://\xffexample.com\", - \"crossOrigin\": true, - \"topOrigin\": \"https://example.org\" - }" - .as_slice(); - assert!( - RelaxedClientDataJsonParser::<true>::parse(input_bytes).is_ok_and(|c| { - matches!(c.origin.0, Cow::Owned(o) if o == "https://\u{fffd}example.com") - }) - ); - // Escape characters are de-escaped. - input_bytes = b"{ - \"challenge\": \"ABABABABABABABABABABAA\", - \"type\": \"webauthn\\u002ecreate\", - \"origin\": \"https://examp\\\\le.com\", - \"crossOrigin\": true, - \"topOrigin\": \"https://example.org\" - }"; - assert!( - RelaxedClientDataJsonParser::<true>::parse(input_bytes).is_ok_and(|c| { - matches!(c.origin.0, Cow::Owned(o) if o == "https://examp\\le.com") - }) - ); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[expect(clippy::little_endian_bytes, reason = "comments justify correctness")] - #[expect(clippy::too_many_lines, reason = "a lot to test")] - #[test] - fn relaxed_challenge() { - // Base case is correct. - let mut input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert!( - RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok_and( - |c| { - // `Challenges` are sent in little-endian. - c.0 == u128::from_le_bytes([ - 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, - ]) - } - ) - ); - // Base case is correct. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert!( - RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok_and( - |c| { - // `Challenges` are sent in little-endian. - c.0 == u128::from_le_bytes([ - 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, - ]) - } - ) - ); - // Unknown keys are allowed. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org", - "foo": true - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); - // Duplicate keys are ignored. - let mut input_str = "{ - \"challenge\": \"ABABABABABABABABABABAA\", - \"type\": \"webauthn.create\", - \"origin\": \"https://example.com\", - \"crossOrigin\": true, - \"topOrigin\": \"https://example.org\", - \"crossOrigin\": true - }"; - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_str.as_bytes()).unwrap(); - // `null` `crossOrigin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": null, - "topOrigin": "https://example.org" - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); - // Missing `crossOrigin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "topOrigin": "https://example.org" - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); - // `null` `topOrigin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": null - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); - // Missing `topOrigin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); - // `null` `challenge`. - let mut err = Error::invalid_type( - Unexpected::Other("null"), - &"base64 encoding of the 16-byte challenge in a URL safe way without padding", - ) - .to_string() - .into_bytes(); - input = serde_json::json!({ - "challenge": null, - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // Missing `challenge`. - err = Error::missing_field("challenge").to_string().into_bytes(); - input = serde_json::json!({ - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - assert_eq!( - RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()) - .unwrap_err() - .to_string() - .into_bytes() - .get(..err.len()), - Some(err.as_slice()) - ); - // `null` `type`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": null, - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); - // Missing `type`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); - // `null` `origin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": null, - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); - // Missing `origin`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); - // Mismatched `type`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.create", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); - // Mismatched `type`. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": "https://example.com", - "crossOrigin": true, - "topOrigin": "https://example.org" - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); - // `crossOrigin` can be `false` even when `topOrigin` exists. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": "https://example.com", - "crossOrigin": false, - "topOrigin": "https://example.org" - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); - // `crossOrigin` can be `true` even when `topOrigin` does not exist. - input = serde_json::json!({ - "challenge": "ABABABABABABABABABABAA", - "type": "webauthn.get", - "origin": "https://example.com", - "crossOrigin": true, - }) - .to_string(); - _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); - // BOM is removed. - input_str = "\u{feff}{ - \"challenge\": \"ABABABABABABABABABABAA\", - \"type\": \"webauthn.create\", - \"origin\": \"https://example.com\", - \"crossOrigin\": true, - \"topOrigin\": \"https://example.org\" - }"; - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_str.as_bytes()).unwrap(); - // Invalid Unicode is replaced. - let mut input_bytes = b"{ - \"challenge\": \"ABABABABABABABABABABAA\", - \"type\": \"webauthn.create\", - \"origin\": \"https://\xffexample.com\", - \"crossOrigin\": true, - \"topOrigin\": \"https://example.org\" - }" - .as_slice(); - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_bytes).unwrap(); - // Escape characters are de-escaped. - input_bytes = b"{ - \"challenge\": \"ABABABABABABABABABABAA\", - \"type\": \"webauthn\\u002ecreate\", - \"origin\": \"https://examp\\\\le.com\", - \"crossOrigin\": true, - \"topOrigin\": \"https://example.org\" - }"; - _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_bytes).unwrap(); - } -} diff --git a/src/response/ser_relaxed/tests.rs b/src/response/ser_relaxed/tests.rs @@ -0,0 +1,583 @@ +use super::{ClientDataJsonParser as _, Cow, RelaxedClientDataJsonParser}; +use serde::de::{Error as _, Unexpected}; +use serde_json::Error; +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[expect(clippy::little_endian_bytes, reason = "comments justify correctness")] +#[expect(clippy::too_many_lines, reason = "a lot to test")] +#[test] +fn relaxed_client_data_json() { + // Base case is correct. + let mut input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok_and(|c| { + c.cross_origin + && c.challenge.0 + // challenges are sent little-endian + == u128::from_le_bytes([ + 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, + ]) + && matches!(c.origin.0, Cow::Borrowed(o) if o == "https://example.com") + && c.top_origin + .is_some_and(|t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org")) + }) + ); + // Base case is correct. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert!( + RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).is_ok_and(|c| { + c.cross_origin + && c.challenge.0 + // challenges are sent little-endian + == u128::from_le_bytes([ + 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, + ]) + && matches!(c.origin.0, Cow::Borrowed(o) if o == "https://example.com") + && c.top_origin + .is_some_and(|t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org")) + }) + ); + // Unknown keys are allowed. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org", + "foo": true + }) + .to_string(); + drop(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).unwrap()); + // Duplicate keys are forbidden. + let mut input_str = "{ + \"challenge\": \"ABABABABABABABABABABAA\", + \"type\": \"webauthn.create\", + \"origin\": \"https://example.com\", + \"crossOrigin\": true, + \"topOrigin\": \"https://example.org\", + \"crossOrigin\": true + }"; + let mut err = Error::duplicate_field("crossOrigin") + .to_string() + .into_bytes(); + assert_eq!( + RelaxedClientDataJsonParser::<true>::parse(input_str.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `crossOrigin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": null, + "topOrigin": "https://example.org" + }) + .to_string(); + assert!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok_and(|c| !c.cross_origin) + ); + // Missing `crossOrigin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "topOrigin": "https://example.org" + }) + .to_string(); + assert!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok_and(|c| !c.cross_origin) + ); + // `null` `topOrigin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": null + }) + .to_string(); + assert!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) + .is_ok_and(|c| c.top_origin.is_none()) + ); + // Missing `topOrigin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + }) + .to_string(); + assert!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) + .is_ok_and(|c| c.top_origin.is_none()) + ); + // `null` `challenge`. + err = Error::invalid_type( + Unexpected::Other("null"), + &"base64 encoding of the 16-byte challenge in a URL safe way without padding", + ) + .to_string() + .into_bytes(); + input = serde_json::json!({ + "challenge": null, + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `challenge`. + err = Error::missing_field("challenge").to_string().into_bytes(); + input = serde_json::json!({ + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `type`. + err = Error::invalid_type( + Unexpected::Other("null"), + &"'webauthn.create' or 'webauthn.get'", + ) + .to_string() + .into_bytes(); + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": null, + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `type`. + err = Error::missing_field("type").to_string().into_bytes(); + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `origin`. + err = Error::invalid_type(Unexpected::Other("null"), &"OriginWrapper") + .to_string() + .into_bytes(); + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": null, + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `origin`. + err = Error::missing_field("origin").to_string().into_bytes(); + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Mismatched `type`. + err = Error::invalid_value(Unexpected::Str("webauthn.create"), &"webauthn.get") + .to_string() + .into_bytes(); + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Mismatched `type`. + err = Error::invalid_value(Unexpected::Str("webauthn.get"), &"webauthn.create") + .to_string() + .into_bytes(); + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `crossOrigin` can be `false` even when `topOrigin` exists. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": "https://example.com", + "crossOrigin": false, + "topOrigin": "https://example.org" + }) + .to_string(); + drop(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).unwrap()); + // `crossOrigin` can be `true` even when `topOrigin` does not exist. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": "https://example.com", + "crossOrigin": true, + }) + .to_string(); + drop(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).unwrap()); + // BOM is removed. + input_str = "\u{feff}{ + \"challenge\": \"ABABABABABABABABABABAA\", + \"type\": \"webauthn.create\", + \"origin\": \"https://example.com\", + \"crossOrigin\": true, + \"topOrigin\": \"https://example.org\" + }"; + drop(RelaxedClientDataJsonParser::<true>::parse(input_str.as_bytes()).unwrap()); + // Invalid Unicode is replaced. + let mut input_bytes = b"{ + \"challenge\": \"ABABABABABABABABABABAA\", + \"type\": \"webauthn.create\", + \"origin\": \"https://\xffexample.com\", + \"crossOrigin\": true, + \"topOrigin\": \"https://example.org\" + }" + .as_slice(); + assert!( + RelaxedClientDataJsonParser::<true>::parse(input_bytes).is_ok_and(|c| { + matches!(c.origin.0, Cow::Owned(o) if o == "https://\u{fffd}example.com") + }) + ); + // Escape characters are de-escaped. + input_bytes = b"{ + \"challenge\": \"ABABABABABABABABABABAA\", + \"type\": \"webauthn\\u002ecreate\", + \"origin\": \"https://examp\\\\le.com\", + \"crossOrigin\": true, + \"topOrigin\": \"https://example.org\" + }"; + assert!( + RelaxedClientDataJsonParser::<true>::parse(input_bytes) + .is_ok_and(|c| { matches!(c.origin.0, Cow::Owned(o) if o == "https://examp\\le.com") }) + ); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[expect(clippy::little_endian_bytes, reason = "comments justify correctness")] +#[expect(clippy::too_many_lines, reason = "a lot to test")] +#[test] +fn relaxed_challenge() { + // Base case is correct. + let mut input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert!( + RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok_and(|c| { + // `Challenges` are sent in little-endian. + c.0 == u128::from_le_bytes([0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0]) + }) + ); + // Base case is correct. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert!( + RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok_and(|c| { + // `Challenges` are sent in little-endian. + c.0 == u128::from_le_bytes([0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0]) + }) + ); + // Unknown keys are allowed. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org", + "foo": true + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); + // Duplicate keys are ignored. + let mut input_str = "{ + \"challenge\": \"ABABABABABABABABABABAA\", + \"type\": \"webauthn.create\", + \"origin\": \"https://example.com\", + \"crossOrigin\": true, + \"topOrigin\": \"https://example.org\", + \"crossOrigin\": true + }"; + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_str.as_bytes()).unwrap(); + // `null` `crossOrigin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": null, + "topOrigin": "https://example.org" + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); + // Missing `crossOrigin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "topOrigin": "https://example.org" + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); + // `null` `topOrigin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": null + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); + // Missing `topOrigin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); + // `null` `challenge`. + let mut err = Error::invalid_type( + Unexpected::Other("null"), + &"base64 encoding of the 16-byte challenge in a URL safe way without padding", + ) + .to_string() + .into_bytes(); + input = serde_json::json!({ + "challenge": null, + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // Missing `challenge`. + err = Error::missing_field("challenge").to_string().into_bytes(); + input = serde_json::json!({ + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + assert_eq!( + RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()) + .unwrap_err() + .to_string() + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) + ); + // `null` `type`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": null, + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); + // Missing `type`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); + // `null` `origin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": null, + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); + // Missing `origin`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); + // Mismatched `type`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.create", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); + // Mismatched `type`. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": "https://example.com", + "crossOrigin": true, + "topOrigin": "https://example.org" + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); + // `crossOrigin` can be `false` even when `topOrigin` exists. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": "https://example.com", + "crossOrigin": false, + "topOrigin": "https://example.org" + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); + // `crossOrigin` can be `true` even when `topOrigin` does not exist. + input = serde_json::json!({ + "challenge": "ABABABABABABABABABABAA", + "type": "webauthn.get", + "origin": "https://example.com", + "crossOrigin": true, + }) + .to_string(); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); + // BOM is removed. + input_str = "\u{feff}{ + \"challenge\": \"ABABABABABABABABABABAA\", + \"type\": \"webauthn.create\", + \"origin\": \"https://example.com\", + \"crossOrigin\": true, + \"topOrigin\": \"https://example.org\" + }"; + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_str.as_bytes()).unwrap(); + // Invalid Unicode is replaced. + let mut input_bytes = b"{ + \"challenge\": \"ABABABABABABABABABABAA\", + \"type\": \"webauthn.create\", + \"origin\": \"https://\xffexample.com\", + \"crossOrigin\": true, + \"topOrigin\": \"https://example.org\" + }" + .as_slice(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_bytes).unwrap(); + // Escape characters are de-escaped. + input_bytes = b"{ + \"challenge\": \"ABABABABABABABABABABAA\", + \"type\": \"webauthn\\u002ecreate\", + \"origin\": \"https://examp\\\\le.com\", + \"crossOrigin\": true, + \"topOrigin\": \"https://example.org\" + }"; + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_bytes).unwrap(); +} diff --git a/src/response/tests.rs b/src/response/tests.rs @@ -0,0 +1,174 @@ +use super::{ClientDataJsonParser as _, CollectedClientDataErr, LimitedVerificationParser}; +#[test] +fn parse_string() { + assert!( + LimitedVerificationParser::<true>::parse_string(br#"abc""#) + .is_ok_and(|tup| { tup.0 == "abc" && tup.1 == b"" }) + ); + assert!( + LimitedVerificationParser::<false>::parse_string(br#"abc"23"#) + .is_ok_and(|tup| { tup.0 == "abc" && tup.1 == b"23" }) + ); + assert!( + LimitedVerificationParser::<true>::parse_string(br#"ab\"c"23"#) + .is_ok_and(|tup| { tup.0 == r#"ab"c"# && tup.1 == b"23" }) + ); + assert!( + LimitedVerificationParser::<false>::parse_string(br#"ab\\c"23"#) + .is_ok_and(|tup| { tup.0 == r"ab\c" && tup.1 == b"23" }) + ); + assert!( + LimitedVerificationParser::<true>::parse_string(br#"ab\u001fc"23"#) + .is_ok_and(|tup| { tup.0 == "ab\u{001f}c" && tup.1 == b"23" }) + ); + assert!( + LimitedVerificationParser::<false>::parse_string(br#"ab\u000dc"23"#) + .is_ok_and(|tup| { tup.0 == "ab\u{000d}c" && tup.1 == b"23" }) + ); + assert!( + LimitedVerificationParser::<true>::parse_string(b"\\\\\\\\\\\\a\\\\\\\\a\\\\\"") + .is_ok_and(|tup| { tup.0 == "\\\\\\a\\\\a\\" && tup.1.is_empty() }) + ); + assert!( + LimitedVerificationParser::<false>::parse_string(b"\\\\\\\\\\a\\\\\\\\a\\\\\"") + .is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString),) + ); + assert!( + LimitedVerificationParser::<true>::parse_string(br#"ab\u0020c"23"#) + .is_err_and(|err| matches!(err, CollectedClientDataErr::InvalidEscapedString),) + ); + assert!( + LimitedVerificationParser::<false>::parse_string(br#"ab\ac"23"#) + .is_err_and(|err| matches!(err, CollectedClientDataErr::InvalidEscapedString),) + ); + assert!( + LimitedVerificationParser::<true>::parse_string(br#"ab\""#) + .is_err_and(|err| matches!(err, CollectedClientDataErr::InvalidObject),) + ); + assert!( + LimitedVerificationParser::<false>::parse_string(br#"ab\u001Fc"23"#) + .is_err_and(|err| matches!(err, CollectedClientDataErr::InvalidEscapedString),) + ); + assert!( + LimitedVerificationParser::<true>::parse_string([0, b'"'].as_slice()) + .is_err_and(|err| matches!(err, CollectedClientDataErr::InvalidEscapedString),) + ); + assert!( + LimitedVerificationParser::<false>::parse_string([b'a', 255, b'"'].as_slice()) + .is_err_and(|err| matches!(err, CollectedClientDataErr::Utf8(_))) + ); + assert!( + LimitedVerificationParser::<true>::parse_string([b'a', b'"', 255].as_slice()) + .is_ok_and(|tup| tup.0 == "a" && tup.1 == [255]) + ); + assert!( + LimitedVerificationParser::<false>::parse_string(br#"""#) + .is_ok_and(|tup| tup.0.is_empty() && tup.1.is_empty()) + ); +} +#[expect(clippy::cognitive_complexity, reason = "a lot of things to test")] +#[test] +fn c_data_json() { + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,{}}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_some_and(|v| v == "bob"))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob",a}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_some_and(|v| v == "bob"))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"a}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Challenge))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0.is_empty() && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Type))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::ChallengeKey))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::OriginKey))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\\e.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\"e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\"e.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0013e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\u{0013}e.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\3e.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); + assert!( + LimitedVerificationParser::<true>::parse([].as_slice()) + .is_err_and(|e| matches!(e, CollectedClientDataErr::Len)) + ); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidStart))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::OriginKey))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::CrossOriginKey))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::CrossOrigin))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true","topOrigin":"https://abc.com"a}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<false>::parse(b"{\"type\":\"webauthn.get\",\"challenge\":\"AAAAAAAAAAAAAAAAAAAAAA\",\"origin\":\"https://example.com\",\"crossOrigin\":false,\xff}".as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_some_and(|v| v == "bob"))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Challenge))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0.is_empty() && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::Type))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::ChallengeKey))); + assert!( + LimitedVerificationParser::<false>::parse( + br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false}"# + .as_slice() + ) + .is_err_and(|e| matches!(e, CollectedClientDataErr::OriginKey)) + ); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\\e.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\"e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\"e.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0013e.com","crossOrigin":false}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\u{0013}e.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\3e.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString))); + assert!( + LimitedVerificationParser::<false>::parse([].as_slice()) + .is_err_and(|e| matches!(e, CollectedClientDataErr::Len)) + ); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidStart))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::OriginKey))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::CrossOriginKey))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::CrossOrigin))); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::InvalidObject))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"https://example.com"}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::TopOriginSameAsOrigin))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"foo":true}"#.as_slice()).is_ok_and(|val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); + assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challengE":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"foo":true}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::ChallengeKey))); + assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create"challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossorigin":false,"foo":true}"#.as_slice()).is_err_and(|e| matches!(e, CollectedClientDataErr::ChallengeKey))); +} +#[test] +fn c_data_challenge() { + assert!( + LimitedVerificationParser::<false>::get_sent_challenge([].as_slice()) + .is_err_and(|e| matches!(e, CollectedClientDataErr::Len)) + ); + assert!( + LimitedVerificationParser::<true>::get_sent_challenge([].as_slice()) + .is_err_and(|e| matches!(e, CollectedClientDataErr::Len)) + ); + assert!( + LimitedVerificationParser::<true>::get_sent_challenge( + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBB" + ) + .is_err_and(|e| matches!(e, CollectedClientDataErr::Challenge)) + ); + assert!( + LimitedVerificationParser::<false>::get_sent_challenge( + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBB" + ) + .is_err_and(|e| matches!(e, CollectedClientDataErr::Challenge)) + ); + assert!( + LimitedVerificationParser::<true>::get_sent_challenge( + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".as_slice() + ) + .is_ok_and(|c| c.0 == 0) + ); + assert!( + LimitedVerificationParser::<false>::get_sent_challenge( + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".as_slice() + ) + .is_ok_and(|c| c.0 == 0) + ); +}