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:
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, ®istration, ®_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, ®istration, ®_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)
+ );
+}