webauthn_rp

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

commit 18befe4fbcacf8eb89fafca076dabca42e489526
parent d35fd32bad2cec478105a246c0ce4dc443d3523e
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon, 12 Jan 2026 17:16:29 -0700

fix hashset. address test lints

Diffstat:
MCargo.toml | 38+++++++++++++++++++++++++-------------
MREADME.md | 17++++++++++-------
Msrc/hash.rs | 35+++++++++++++++++++++++++++++------
Msrc/hash/hash_map.rs | 185++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/hash/hash_set.rs | 193++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/lib.rs | 114++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/request.rs | 152+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/request/auth.rs | 39++++++++++++++++++++-------------------
Msrc/request/auth/ser.rs | 291++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/request/register.rs | 402+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/request/register/bin.rs | 43+++++++++++++++++++++++--------------------
Msrc/request/register/ser.rs | 1123++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/request/ser.rs | 3---
Msrc/response.rs | 184+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/response/auth.rs | 15++++-----------
Msrc/response/auth/error.rs | 1-
Msrc/response/auth/ser.rs | 349+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/response/auth/ser_relaxed.rs | 479++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/response/custom.rs | 24++++++++++++------------
Msrc/response/register.rs | 121+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/response/register/error.rs | 1-
Msrc/response/register/ser.rs | 989+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/response/register/ser_relaxed.rs | 1256+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/response/ser.rs | 6+++---
Msrc/response/ser_relaxed.rs | 247+++++++++++++++++++++++++++++++++++++++++--------------------------------------
25 files changed, 3702 insertions(+), 2605 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -10,7 +10,7 @@ name = "webauthn_rp" readme = "README.md" repository = "https://git.philomathiclife.com/repos/webauthn_rp/" rust-version = "1.89.0" -version = "0.4.0" +version = "0.4.0+spec-3" [lints.rust] ambiguous_negative_literals = { level = "deny", priority = -1 } @@ -44,9 +44,11 @@ rust_2021_compatibility = { level = "deny", priority = -1 } rust_2024_compatibility = { level = "deny", priority = -1 } single_use_lifetimes = { level = "deny", priority = -1 } #supertrait_item_shadowing_definition = { level = "deny", priority = -1 } +#supertrait_item_shadowing_usage = { level = "deny", priority = -1 } trivial_casts = { level = "deny", priority = -1 } trivial_numeric_casts = { level = "deny", priority = -1 } unit_bindings = { level = "deny", priority = -1 } +unknown_or_malformed_diagnostic_attributes = { level = "deny", priority = -1 } unnameable_types = { level = "deny", priority = -1 } #unqualified_local_imports = { level = "deny", priority = -1 } unreachable_pub = { level = "deny", priority = -1 } @@ -62,7 +64,6 @@ variant_size_differences = { level = "deny", priority = -1 } warnings = { level = "deny", priority = -1 } [lints.clippy] -all = { level = "deny", priority = -1 } cargo = { level = "deny", priority = -1 } complexity = { level = "deny", priority = -1 } correctness = { level = "deny", priority = -1 } @@ -81,7 +82,6 @@ implicit_return = "allow" min_ident_chars = "allow" missing_trait_methods = "allow" module_name_repetitions = "allow" -option_option = "allow" pub_use = "allow" pub_with_shorthand = "allow" question_mark_used = "allow" @@ -95,27 +95,39 @@ unseparated_literal_suffix = "allow" [package.metadata.docs.rs] all-features = true -rustdoc-args = ["--cfg", "docsrs"] +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "aarch64-apple-darwin", + "aarch64-pc-windows-msvc", + "aarch64-unknown-linux-gnu", + "i686-pc-windows-msvc", + "i686-unknown-linux-gnu", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-msvc", + "x86_64-unknown-freebsd", + "x86_64-unknown-linux-musl", + "x86_64-unknown-netbsd" +] [dependencies] -base64url_nopad = { version = "0.1.0", default-features = false } +base64url_nopad = { version = "0.1.2", default-features = false } ed25519-dalek = { version = "2.2.0", default-features = false } -hashbrown = { version = "0.15.4", default-features = false } +hashbrown = { version = "0.16.1", default-features = false } p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] } p384 = { version = "0.13.1", default-features = false, features = ["ecdsa"] } -precis-profiles = { version = "0.1.12", default-features = false } +precis-profiles = { version = "0.1.13", default-features = false } rand = { version = "0.9.2", default-features = false, features = ["thread_rng"] } -rsa = { version = "0.9.8", default-features = false, features = ["sha2"] } -serde = { version = "1.0.219", default-features = false, features = ["alloc"], optional = true } -serde_json = { version = "1.0.141", default-features = false, features = ["alloc"], optional = true } -url = { version = "2.5.4", default-features = false } +rsa = { version = "0.9.10", default-features = false, features = ["sha2"] } +serde = { version = "1.0.228", default-features = false, features = ["alloc"], optional = true } +serde_json = { version = "1.0.149", default-features = false, features = ["alloc"], optional = true } +url = { version = "2.5.8", default-features = false } [dev-dependencies] -base64url_nopad = { version = "0.1.0", default-features = false, features = ["alloc"] } +base64url_nopad = { version = "0.1.2", default-features = false, features = ["alloc"] } ed25519-dalek = { version = "2.2.0", default-features = false, features = ["alloc", "pkcs8"] } p256 = { version = "0.13.2", default-features = false, features = ["pem"] } p384 = { version = "0.13.1", default-features = false, features = ["pkcs8"] } -serde_json = { version = "1.0.141", default-features = false, features = ["preserve_order"] } +serde_json = { version = "1.0.149", default-features = false, features = ["preserve_order"] } ### FEATURES ################################################################# diff --git a/README.md b/README.md @@ -499,11 +499,14 @@ at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -Before any PR is sent, `cargo clippy` and `cargo t` should be run _for each possible combination of "features"_ -using stable Rust. One easy way to achieve this is by building `ci` and invoking it with no commands in the -`webauthn_rp` directory or sub-directories. You can fetch `ci` via `git clone https://git.philomathiclife.com/repos/ci`, -and it can be built with `cargo build --release`. Additionally, -`RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be built. +Before any PR is sent, `cargo clippy --all-targets`, `cargo test --all-targets -- --include-ignored`, and +`cargo test --doc` should be run _for each possible combination of "features"_ using the stable and MSRV toolchains. +One easy way to achieve this is by invoking [`ci-cargo`](https://crates.io/crates/ci-cargo) as +`ci-cargo clippy --all-targets test --all-targets --include-ignored --ignore-compile-errors` in the `webauthn_rp` +directory. + +Last, `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be +built. ### Status @@ -511,8 +514,8 @@ This package is actively maintained and will conform to the [latest WebAuthn API version](https://www.w3.org/TR/webauthn-3/). Previous versions will not be supported—excluding bug fixes of course—however functionality will exist to facilitate the migration process from the previous version. -The crate is only tested on `x86_64-unknown-linux-gnu` and `x86_64-unknown-openbsd` targets, but it should work -on most platforms. +The crate is only tested on the `x86_64-unknown-linux-gnu`, `x86_64-unknown-openbsd`, and `aarch64-apple-darwin` +targets; but it should work on most platforms. [^note]: `panic`s related to memory allocations or stack overflow are possible since such issues are not formally guarded against. diff --git a/src/hash.rs b/src/hash.rs @@ -1,6 +1,6 @@ #[cfg(doc)] use super::{ - hash::hash_set::FixedCapHashSet, + hash::hash_set::MaxLenHashSet, request::{ Challenge, auth::{ @@ -12,9 +12,9 @@ use super::{ }; use core::hash::{BuildHasher, Hasher}; pub use hashbrown; -/// Fixed-capacity hash map. +/// Hash map with an immutable maximum length that allocates exactly once. pub mod hash_map; -/// Fixed-capacity hash set. +/// Hash set with an immutable maximum length that allocates exactly once. pub mod hash_set; /// [`Hasher`] whose `write_*` methods simply store up to 64 bits of the passed argument _as is_ overwriting /// any previous state. @@ -25,7 +25,7 @@ pub mod hash_set; /// /// [`RegistrationServerState`], [`DiscoverableAuthenticationServerState`], and /// [`NonDiscoverableAuthenticationServerState`] implement [`Hash`] by simply writing the -/// contained [`Challenge`]; thus when they are stored in a hashed collection (e.g., [`FixedCapHashSet`]), one can +/// contained [`Challenge`]; thus when they are stored in a hashed collection (e.g., [`MaxLenHashSet`]), one can /// optimize without fear by using this `Hasher` since `Challenge`s are immutable and can only ever be created on /// the server via [`Challenge::new`] (and equivalently [`Challenge::default`]). `RegistrationServerState`, /// `DiscoverableAuthenticationServerState`, and `NonDiscoverableAuthenticationServerState` are also immutable and @@ -34,7 +34,7 @@ pub mod hash_set; /// [`NonDiscoverableCredentialRequestOptions::start_ceremony`] respectively. Since `Challenge` is already based on /// a random `u128`, other `Hasher`s will be slower and likely produce lower-quality hashes (and never /// higher quality). -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub struct IdentityHasher(u64); // Note it is _not_ required for `write_*` methods to do the same thing as other `write_*` methods // (e.g., `Self::write_u64` may not be the same thing as 8 calls to `Self::write_u8`). @@ -101,6 +101,18 @@ impl Hasher for IdentityHasher { fn write_i128(&mut self, i: i128) { self.write_u64(i as u64); } + /// Redirects to [`Self::write_u64`] on 64-bit platforms. + /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`] on platforms of less than 64 bits. + /// Truncates `i` to a [`u64`] before redirecting to [`Self::write_u64`] on platforms of more than 64 bits. + #[expect( + clippy::as_conversions, + clippy::cast_sign_loss, + reason = "we simply need to convert into a u64 in a deterministic way" + )] + #[inline] + fn write_isize(&mut self, i: isize) { + self.write_u64(i as u64); + } /// Zero-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`]. #[inline] fn write_u8(&mut self, i: u8) { @@ -126,6 +138,17 @@ impl Hasher for IdentityHasher { fn write_u128(&mut self, i: u128) { self.write_u64(i as u64); } + /// Redirects to [`Self::write_u64`] on 64-bit platforms. + /// Zero-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`] on platforms of less than 64 bits. + /// Truncates `i` to a [`u64`] before redirecting to [`Self::write_u64`] on platforms of more than 64 bits. + #[expect( + clippy::as_conversions, + reason = "we simply need to convert into a u64 in a deterministic way" + )] + #[inline] + fn write_usize(&mut self, i: usize) { + self.write_u64(i as u64); + } /// This does nothing iff `bytes.len() < 8`; otherwise the first 8 bytes are converted /// to a [`u64`] that is written via [`Self::write_u64`]; #[expect(clippy::host_endian_bytes, reason = "endianness does not matter")] @@ -142,7 +165,7 @@ impl Hasher for IdentityHasher { /// /// This MUST only be used with hash maps with keys that are randomly generated on the server based on at least 64 /// bits of entropy. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub struct BuildIdentityHasher; impl BuildHasher for BuildIdentityHasher { type Hasher = IdentityHasher; diff --git a/src/hash/hash_map.rs b/src/hash/hash_map.rs @@ -3,14 +3,18 @@ use super::{super::request::TimedCeremony, BuildIdentityHasher}; use core::hash::Hasher; use core::hash::{BuildHasher, Hash}; use hashbrown::{ - Equivalent, + Equivalent, TryReserveError, hash_map::{Drain, Entry, EntryRef, ExtractIf, HashMap, IterMut, OccupiedError, ValuesMut}, }; #[cfg(not(feature = "serializable_server_state"))] use std::time::Instant; #[cfg(feature = "serializable_server_state")] use std::time::SystemTime; -/// `newtype` around [`HashMap`] that has maximum [`HashMap::capacity`]. +/// [`HashMap`] that has maximum [`HashMap::capacity`] and length and allocates exactly once. +/// +/// Note due to how `HashMap` removes entries, it's possible to insert an entry after removing an entry and cause +/// a new allocation. To avoid this, we ensure that the allocated capacity is at least twice the size of +/// the requested maximum length. /// /// This is useful in situations when the underlying entries are expected to be removed, and one wants to ensure the /// map does not grow unbounded. When `K` is a [`TimedCeremony`], helper methods (e.g., @@ -18,24 +22,47 @@ use std::time::SystemTime; /// intended use case is for `K` to be based on a server-side randomly generated value; thus the default [`Hasher`] /// is [`BuildIdentityHasher`]. In the event this is not true, one MUST use a more appropriate `Hasher`. /// -/// Only the mutable methods of `HashMap` are re-defined in order to ensure the capacity never grows. For all -/// other methods, first call [`Self::as_ref`] or [`Self::into`]. +/// Only the mutable methods of `HashMap` are re-defined in order to ensure [`Self::max_len`] is never exceeded. +/// For all other methods, first call [`Self::as_ref`] or [`Self::into`]. /// -/// [`Self::into`]: struct.FixedCapHashMap.html#impl-Into<U>-for-T +/// [`Self::into`]: struct.MaxLenHashMap.html#impl-Into<U>-for-T #[derive(Debug)] -pub struct FixedCapHashMap<K, V, S = BuildIdentityHasher>(HashMap<K, V, S>); -impl<K, V> FixedCapHashMap<K, V, BuildIdentityHasher> { - /// [`HashMap::with_capacity_and_hasher`] using `capacity` and `BuildIdentityHasher`. +pub struct MaxLenHashMap<K, V, S = BuildIdentityHasher>(HashMap<K, V, S>, usize); +impl<K, V> MaxLenHashMap<K, V, BuildIdentityHasher> { + /// [`HashMap::with_capacity_and_hasher`] using `2 * max_len` and `BuildIdentityHasher`. + /// + /// Note since the actual capacity allocated may exceed the requested capacity, [`Self::max_len`] may exceed + /// `max_len`. + /// + /// # Panics + /// + /// `panic`s if `max_len > usize::MAX / 2`. Note since [`HashMap::with_capacity_and_hasher`] `panic`s + /// for much smaller values than `usize::MAX / 2`—even when `K` and `V` are zero-sized types (ZSTs)—this is not + /// an additional `panic` than what would already occur. The only difference is the message reported. #[inline] #[must_use] - pub fn new(capacity: usize) -> Self { - Self(HashMap::with_capacity_and_hasher( - capacity, - BuildIdentityHasher, - )) + pub fn new(max_len: usize) -> Self { + Self::with_hasher(max_len, BuildIdentityHasher) } } -impl<K, V, S> FixedCapHashMap<K, V, S> { +impl<K, V, S> MaxLenHashMap<K, V, S> { + /// Capacity we allocate. + /// + /// # Errors + /// + /// Errors iff `max_len > usize::MAX / 2`. + const fn requested_capacity(max_len: usize) -> Result<usize, TryReserveError> { + if max_len <= usize::MAX >> 1u8 { + Ok(max_len << 1u8) + } else { + Err(TryReserveError::CapacityOverflow) + } + } + /// Returns the immutable maximum length allowed by `self`. + #[inline] + pub const fn max_len(&self) -> usize { + self.1 + } /// [`HashMap::values_mut`]. #[inline] pub fn values_mut(&mut self) -> ValuesMut<'_, K, V> { @@ -65,11 +92,26 @@ impl<K, V, S> FixedCapHashMap<K, V, S> { pub fn extract_if<F: FnMut(&K, &mut V) -> bool>(&mut self, f: F) -> ExtractIf<'_, K, V, F> { self.0.extract_if(f) } - /// [`HashMap::with_capacity_and_hasher`]. + /// [`HashMap::with_capacity_and_hasher`] using `2 * max_len` and `hasher`. + /// + /// Note since the actual capacity allocated may exceed the requested capacity, [`Self::max_len`] may exceed + /// `max_len`. + /// + /// # Panics + /// + /// `panic`s if `max_len > usize::MAX / 2`. Note since [`HashMap::with_capacity_and_hasher`] `panic`s + /// for much smaller values than `usize::MAX / 2`—even when `K` and `V` are zero-sized types (ZSTs)—this is not + /// an additional `panic` than what would already occur. The only difference is the message reported. + #[expect( + clippy::expect_used, + reason = "purpose of this function is to panic if the hash map cannot be allocated" + )] #[inline] #[must_use] - pub fn with_hasher(capacity: usize, hasher: S) -> Self { - Self(HashMap::with_capacity_and_hasher(capacity, hasher)) + pub fn with_hasher(max_len: usize, hasher: S) -> Self { + let map = HashMap::with_capacity_and_hasher(Self::requested_capacity(max_len).expect("HashMap::with_hasher must be passed a maximum length that does not exceed usize::MAX / 2"), hasher); + let len = map.capacity() >> 1u8; + Self(map, len) } /// [`HashMap::retain`]. #[inline] @@ -77,7 +119,7 @@ impl<K, V, S> FixedCapHashMap<K, V, S> { self.0.retain(f); } } -impl<K: TimedCeremony, V, S> FixedCapHashMap<K, V, S> { +impl<K: TimedCeremony, V, S> MaxLenHashMap<K, V, S> { /// Removes all expired ceremonies. #[inline] pub fn remove_expired_ceremonies(&mut self) { @@ -90,8 +132,35 @@ impl<K: TimedCeremony, V, S> FixedCapHashMap<K, V, S> { let now = SystemTime::now(); self.retain(|v, _| v.expiration() >= now); } + /// Removes the first encountered expired ceremony. + #[inline] + pub fn remove_first_expired_ceremony(&mut self) { + #[cfg(not(feature = "serializable_server_state"))] + let now = Instant::now(); + #[cfg(feature = "serializable_server_state")] + let now = SystemTime::now(); + drop(self.0.extract_if(|k, _| k.expiration() < now).next()); + } } -impl<K: Eq + Hash, V, S: BuildHasher> FixedCapHashMap<K, V, S> { +impl<K: Eq + Hash, V, S: BuildHasher> MaxLenHashMap<K, V, S> { + /// [`HashMap::with_hasher`] using `hasher` followed by [`HashMap::try_reserve`] using `2 * max_len`. + /// + /// Note since the actual capacity allocated may exceed the requested capacity, [`Self::max_len`] may exceed + /// `max_len`. + /// + /// # Errors + /// + /// Errors iff `max_len > usize::MAX / 2` or [`HashMap::try_reserve`] does. + #[inline] + pub fn try_with_hasher(max_len: usize, hasher: S) -> Result<Self, TryReserveError> { + Self::requested_capacity(max_len).and_then(|additional| { + let mut set = HashMap::with_hasher(hasher); + set.try_reserve(additional).map(|()| { + let len = set.capacity() >> 1u8; + Self(set, len) + }) + }) + } /// [`HashMap::get_mut`]. #[inline] pub fn get_mut<Q: Equivalent<K> + Hash + ?Sized>(&mut self, k: &Q) -> Option<&mut V> { @@ -105,21 +174,21 @@ impl<K: Eq + Hash, V, S: BuildHasher> FixedCapHashMap<K, V, S> { ) -> Option<(&K, &mut V)> { self.0.get_key_value_mut(k) } - /// [`HashMap::get_many_mut`]. + /// [`HashMap::get_disjoint_mut`]. #[inline] - pub fn get_many_mut<Q: Equivalent<K> + Hash + ?Sized, const N: usize>( + pub fn get_disjoint_mut<Q: Equivalent<K> + Hash + ?Sized, const N: usize>( &mut self, ks: [&Q; N], ) -> [Option<&mut V>; N] { - self.0.get_many_mut(ks) + self.0.get_disjoint_mut(ks) } - /// [`HashMap::get_many_key_value_mut`]. + /// [`HashMap::get_disjoint_key_value_mut`]. #[inline] - pub fn get_many_key_value_mut<Q: Equivalent<K> + Hash + ?Sized, const N: usize>( + pub fn get_disjoint_key_value_mut<Q: Equivalent<K> + Hash + ?Sized, const N: usize>( &mut self, ks: [&Q; N], ) -> [Option<(&K, &mut V)>; N] { - self.0.get_many_key_value_mut(ks) + self.0.get_disjoint_key_value_mut(ks) } /// [`HashMap::remove`]. #[inline] @@ -133,7 +202,7 @@ impl<K: Eq + Hash, V, S: BuildHasher> FixedCapHashMap<K, V, S> { } /// [`HashMap::try_insert`]. /// - /// `Ok(None)` is returned iff [`HashMap::len`] `==` [`HashMap::capacity`] and `key` does not already exist in + /// `Ok(None)` is returned iff [`HashMap::len`] `==` [`Self::max_len`] and `key` does not already exist in /// the map. /// /// # Errors @@ -145,7 +214,7 @@ impl<K: Eq + Hash, V, S: BuildHasher> FixedCapHashMap<K, V, S> { key: K, value: V, ) -> Result<Option<&mut V>, OccupiedError<'_, K, V, S>> { - let full = self.0.len() == self.0.capacity(); + let full = self.0.len() == self.1; match self.0.entry(key) { Entry::Occupied(entry) => Err(OccupiedError { entry, value }), Entry::Vacant(ent) => { @@ -159,11 +228,11 @@ impl<K: Eq + Hash, V, S: BuildHasher> FixedCapHashMap<K, V, S> { } /// [`HashMap::insert`]. /// - /// `None` is returned iff [`HashMap::len`] `==` [`HashMap::capacity`] and `key` does not already exist in the + /// `None` is returned iff [`HashMap::len`] `==` [`Self::max_len`] and `key` does not already exist in the /// map. #[inline] pub fn insert(&mut self, k: K, v: V) -> Option<Option<V>> { - let full = self.0.len() == self.0.capacity(); + let full = self.0.len() == self.1; match self.0.entry(k) { Entry::Occupied(mut ent) => Some(Some(ent.insert(v))), Entry::Vacant(ent) => { @@ -178,11 +247,11 @@ impl<K: Eq + Hash, V, S: BuildHasher> FixedCapHashMap<K, V, S> { } /// [`HashMap::entry`]. /// - /// `None` is returned iff [`HashMap::len`] `==` [`HashMap::capacity`] and `key` does not already exist in the + /// `None` is returned iff [`HashMap::len`] `==` [`Self::max_len`] and `key` does not already exist in the /// map. #[inline] pub fn entry(&mut self, key: K) -> Option<Entry<'_, K, V, S>> { - let full = self.0.len() == self.0.capacity(); + let full = self.0.len() == self.1; match self.0.entry(key) { ent @ Entry::Occupied(_) => Some(ent), ent @ Entry::Vacant(_) => { @@ -196,14 +265,14 @@ impl<K: Eq + Hash, V, S: BuildHasher> FixedCapHashMap<K, V, S> { } /// [`HashMap::entry_ref`]. /// - /// `None` is returned iff [`HashMap::len`] `==` [`HashMap::capacity`] and `key` does not already exist in the + /// `None` is returned iff [`HashMap::len`] `==` [`Self::max_len`] and `key` does not already exist in the /// map. #[inline] pub fn entry_ref<'a, 'b, Q: Equivalent<K> + Hash + ?Sized>( &'a mut self, key: &'b Q, ) -> Option<EntryRef<'a, 'b, K, Q, V, S>> { - let full = self.0.len() == self.0.capacity(); + let full = self.0.len() == self.1; match self.0.entry_ref(key) { ent @ EntryRef::Occupied(_) => Some(ent), ent @ EntryRef::Vacant(_) => { @@ -216,11 +285,12 @@ impl<K: Eq + Hash, V, S: BuildHasher> FixedCapHashMap<K, V, S> { } } } -impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> FixedCapHashMap<K, V, S> { - /// [`Self::insert`] except the first expired ceremony is removed in the event there is no available capacity. +impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> MaxLenHashMap<K, V, S> { + /// [`Self::insert`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] + /// entries have been added. #[inline] pub fn insert_remove_expired(&mut self, k: K, v: V) -> Option<Option<V>> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { #[cfg(not(feature = "serializable_server_state"))] let now = Instant::now(); #[cfg(feature = "serializable_server_state")] @@ -241,13 +311,14 @@ impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> FixedCapHashMap<K, V, S> { Some(self.0.insert(k, v)) } } - /// [`Self::insert`] except all expired ceremones are removed in the event there is no available capacity. + /// [`Self::insert`] except all expired ceremonies are removed in the event [`Self::max_len`] entries have + /// been added. #[inline] pub fn insert_remove_all_expired(&mut self, k: K, v: V) -> Option<Option<V>> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { self.remove_expired_ceremonies(); } - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { if let Entry::Occupied(mut ent) = self.0.entry(k) { Some(Some(ent.insert(v))) } else { @@ -257,10 +328,11 @@ impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> FixedCapHashMap<K, V, S> { Some(self.0.insert(k, v)) } } - /// [`Self::entry`] except the first expired ceremony is removed in the event there is no available capacity. + /// [`Self::entry`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] + /// entries have been added. #[inline] pub fn entry_remove_expired(&mut self, key: K) -> Option<Entry<'_, K, V, S>> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { #[cfg(not(feature = "serializable_server_state"))] let now = Instant::now(); #[cfg(feature = "serializable_server_state")] @@ -281,13 +353,14 @@ impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> FixedCapHashMap<K, V, S> { Some(self.0.entry(key)) } } - /// [`Self::entry`] except all expired ceremones are removed in the event there is no available capacity. + /// [`Self::entry`] except all expired ceremonies are removed in the event [`Self::max_len`] entries have + /// been added. #[inline] pub fn entry_remove_all_expired(&mut self, key: K) -> Option<Entry<'_, K, V, S>> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { self.remove_expired_ceremonies(); } - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { if let ent @ Entry::Occupied(_) = self.0.entry(key) { Some(ent) } else { @@ -297,13 +370,14 @@ impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> FixedCapHashMap<K, V, S> { Some(self.0.entry(key)) } } - /// [`Self::entry_ref`] except the first expired ceremony is removed in the event there is no available capacity. + /// [`Self::entry_ref`] except the first encoutered expired ceremony is removed in the event [`Self::max_len`] + /// entries have been added. #[inline] pub fn entry_ref_remove_expired<'a, 'b, Q: Equivalent<K> + Hash + ?Sized>( &'a mut self, key: &'b Q, ) -> Option<EntryRef<'a, 'b, K, Q, V, S>> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { #[cfg(not(feature = "serializable_server_state"))] let now = Instant::now(); #[cfg(feature = "serializable_server_state")] @@ -324,16 +398,17 @@ impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> FixedCapHashMap<K, V, S> { Some(self.0.entry_ref(key)) } } - /// [`Self::entry_ref`] except all expired ceremones are removed in the event there is no available capacity. + /// [`Self::entry_ref`] except all expired ceremonies are removed in the event [`Self::max_len`] entries have + /// been added. #[inline] pub fn entry_ref_remove_all_expired<'a, 'b, Q: Equivalent<K> + Hash + ?Sized>( &'a mut self, key: &'b Q, ) -> Option<EntryRef<'a, 'b, K, Q, V, S>> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { self.remove_expired_ceremonies(); } - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { if let ent @ EntryRef::Occupied(_) = self.0.entry_ref(key) { Some(ent) } else { @@ -344,21 +419,15 @@ impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> FixedCapHashMap<K, V, S> { } } } -impl<K, V, S> AsRef<HashMap<K, V, S>> for FixedCapHashMap<K, V, S> { +impl<K, V, S> AsRef<HashMap<K, V, S>> for MaxLenHashMap<K, V, S> { #[inline] fn as_ref(&self) -> &HashMap<K, V, S> { &self.0 } } -impl<K, V, S> From<FixedCapHashMap<K, V, S>> for HashMap<K, V, S> { +impl<K, V, S> From<MaxLenHashMap<K, V, S>> for HashMap<K, V, S> { #[inline] - fn from(value: FixedCapHashMap<K, V, S>) -> Self { + fn from(value: MaxLenHashMap<K, V, S>) -> Self { value.0 } } -impl<K, V, S> From<HashMap<K, V, S>> for FixedCapHashMap<K, V, S> { - #[inline] - fn from(value: HashMap<K, V, S>) -> Self { - Self(value) - } -} diff --git a/src/hash/hash_set.rs b/src/hash/hash_set.rs @@ -3,14 +3,18 @@ use super::{super::request::TimedCeremony, BuildIdentityHasher}; use core::hash::Hasher; use core::hash::{BuildHasher, Hash}; use hashbrown::{ - Equivalent, + Equivalent, TryReserveError, hash_set::{Drain, Entry, ExtractIf, HashSet}, }; #[cfg(not(feature = "serializable_server_state"))] use std::time::Instant; #[cfg(feature = "serializable_server_state")] use std::time::SystemTime; -/// `newtype` around [`HashSet`] that has maximum [`HashSet::capacity`]. +/// [`HashSet`] that has maximum [`HashSet::capacity`] and length and allocates exactly once. +/// +/// Note due to how `HashSet` removes values, it's possible to insert a value after removing a value and cause +/// a new allocation. To avoid this, we ensure that the allocated capacity is at least twice the size of +/// the requested maximum length. /// /// This is useful in situations when the underlying values are expected to be removed, and one wants to ensure the /// set does not grow unbounded. When `T` is a [`TimedCeremony`], helper methods (e.g., @@ -18,24 +22,47 @@ use std::time::SystemTime; /// intended use case is for `T` to be based on a server-side randomly generated value; thus the default [`Hasher`] /// is [`BuildIdentityHasher`]. In the event this is not true, one MUST use a more appropriate `Hasher`. /// -/// Only the mutable methods of `HashSet` are re-defined in order to ensure the capacity never grows. For all -/// other methods, first call [`Self::as_ref`] or [`Self::into`]. +/// Only the mutable methods of `HashSet` are re-defined in order to ensure [`Self::max_len`] is never exceeded. +/// For all other methods, first call [`Self::as_ref`] or [`Self::into`]. /// -/// [`Self::into`]: struct.FixedCapHashSet.html#impl-Into<U>-for-T +/// [`Self::into`]: struct.MaxLenHashSet.html#impl-Into<U>-for-T #[derive(Debug)] -pub struct FixedCapHashSet<T, S = BuildIdentityHasher>(HashSet<T, S>); -impl<T> FixedCapHashSet<T, BuildIdentityHasher> { - /// [`HashSet::with_capacity_and_hasher`] using `capacity` and `BuildIdentityHasher`. +pub struct MaxLenHashSet<T, S = BuildIdentityHasher>(HashSet<T, S>, usize); +impl<T> MaxLenHashSet<T, BuildIdentityHasher> { + /// [`HashSet::with_capacity_and_hasher`] using `2 * max_len` and `BuildIdentityHasher`. + /// + /// Note since the actual capacity allocated may exceed the requested capacity, [`Self::max_len`] may exceed + /// `max_len`. + /// + /// # Panics + /// + /// `panic`s if `max_len > usize::MAX / 2`. Note since [`HashSet::with_capacity_and_hasher`] `panic`s + /// for much smaller values than `usize::MAX / 2`—even when `T` is a zero-sized type (ZST)—this is not an + /// additional `panic` than what would already occur. The only difference is the message reported. #[inline] #[must_use] - pub fn new(capacity: usize) -> Self { - Self(HashSet::with_capacity_and_hasher( - capacity, - BuildIdentityHasher, - )) + pub fn new(max_len: usize) -> Self { + Self::with_hasher(max_len, BuildIdentityHasher) } } -impl<T, S> FixedCapHashSet<T, S> { +impl<T, S> MaxLenHashSet<T, S> { + /// Capacity we allocate. + /// + /// # Errors + /// + /// Errors iff `max_len > usize::MAX / 2`. + const fn requested_capacity(max_len: usize) -> Result<usize, TryReserveError> { + if max_len <= usize::MAX >> 1u8 { + Ok(max_len << 1u8) + } else { + Err(TryReserveError::CapacityOverflow) + } + } + /// Returns the immutable maximum length allowed by `self`. + #[inline] + pub const fn max_len(&self) -> usize { + self.1 + } /// [`HashSet::clear`]. #[inline] pub fn clear(&mut self) { @@ -51,11 +78,26 @@ impl<T, S> FixedCapHashSet<T, S> { pub fn extract_if<F: FnMut(&T) -> bool>(&mut self, f: F) -> ExtractIf<'_, T, F> { self.0.extract_if(f) } - /// [`HashSet::with_capacity_and_hasher`]. + /// [`HashSet::with_capacity_and_hasher`] using `2 * max_len` and `hasher`. + /// + /// Note since the actual capacity allocated may exceed the requested capacity, [`Self::max_len`] may exceed + /// `max_len`. + /// + /// # Panics + /// + /// `panic`s if `max_len > usize::MAX / 2`. Note since [`HashSet::with_capacity_and_hasher`] `panic`s + /// for much smaller values than `usize::MAX / 2`—even when `T` is a zero-sized type (ZST)—this is not an + /// additional `panic` than what would already occur. The only difference is the message reported. + #[expect( + clippy::expect_used, + reason = "purpose of this function is to panic if the hash set cannot be allocated" + )] #[inline] #[must_use] - pub fn with_hasher(capacity: usize, hasher: S) -> Self { - Self(HashSet::with_capacity_and_hasher(capacity, hasher)) + pub fn with_hasher(max_len: usize, hasher: S) -> Self { + let set = HashSet::with_capacity_and_hasher(Self::requested_capacity(max_len).expect("HashSet::with_hasher must be passed a maximum length that does not exceed usize::MAX / 2"), hasher); + let len = set.capacity() >> 1u8; + Self(set, len) } /// [`HashSet::retain`]. #[inline] @@ -63,7 +105,7 @@ impl<T, S> FixedCapHashSet<T, S> { self.0.retain(f); } } -impl<T: TimedCeremony, S> FixedCapHashSet<T, S> { +impl<T: TimedCeremony, S> MaxLenHashSet<T, S> { /// Removes all expired ceremonies. #[inline] pub fn remove_expired_ceremonies(&mut self) { @@ -76,15 +118,42 @@ impl<T: TimedCeremony, S> FixedCapHashSet<T, S> { let now = SystemTime::now(); self.retain(|v| v.expiration() >= now); } + /// Removes the first encountered expired ceremony. + #[inline] + pub fn remove_first_expired_ceremony(&mut self) { + #[cfg(not(feature = "serializable_server_state"))] + let now = Instant::now(); + #[cfg(feature = "serializable_server_state")] + let now = SystemTime::now(); + drop(self.0.extract_if(|v| v.expiration() < now).next()); + } } -impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> { +impl<T: Eq + Hash, S: BuildHasher> MaxLenHashSet<T, S> { + /// [`HashSet::with_hasher`] using `hasher` followed by [`HashSet::try_reserve`] using `2 * max_len`. + /// + /// Note since the actual capacity allocated may exceed the requested capacity, [`Self::max_len`] may exceed + /// `max_len`. + /// + /// # Errors + /// + /// Errors iff `max_len > usize::MAX / 2` or [`HashSet::try_reserve`] does. + #[inline] + pub fn try_with_hasher(max_len: usize, hasher: S) -> Result<Self, TryReserveError> { + Self::requested_capacity(max_len).and_then(|additional| { + let mut set = HashSet::with_hasher(hasher); + set.try_reserve(additional).map(|()| { + let len = set.capacity() >> 1u8; + Self(set, len) + }) + }) + } /// [`HashSet::get_or_insert`]. /// - /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] and `value` does not already exist in the + /// `None` is returned iff [`HashSet::len`] `==` [`Self::max_len`] and `value` does not already exist in the /// set. #[inline] pub fn get_or_insert(&mut self, value: T) -> Option<&T> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { self.0.get(&value) } else { Some(self.0.get_or_insert(value)) @@ -92,7 +161,7 @@ impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> { } /// [`HashSet::get_or_insert_with`]. /// - /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] and `value` does not already exist in the + /// `None` is returned iff [`HashSet::len`] `==` [`Self::max_len`] and `value` does not already exist in the /// set. #[inline] pub fn get_or_insert_with<Q: Equivalent<T> + Hash + ?Sized, F: FnOnce(&Q) -> T>( @@ -100,7 +169,7 @@ impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> { value: &Q, f: F, ) -> Option<&T> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { self.0.get(value) } else { Some(self.0.get_or_insert_with(value, f)) @@ -118,11 +187,11 @@ impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> { } /// [`HashSet::insert`]. /// - /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] and `value` does not already exist in the + /// `None` is returned iff [`HashSet::len`] `==` [`Self::max_len`] and `value` does not already exist in the /// set. #[inline] pub fn insert(&mut self, value: T) -> Option<bool> { - let full = self.0.len() == self.0.capacity(); + let full = self.0.len() == self.1; if let Entry::Vacant(ent) = self.0.entry(value) { if full { None @@ -136,7 +205,7 @@ impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> { } /// [`HashSet::replace`]. /// - /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] and `value` does not already exist in the + /// `None` is returned iff [`HashSet::len`] `==` [`Self::max_len`] and `value` does not already exist in the /// set. #[inline] pub fn replace(&mut self, value: T) -> Option<Option<T>> { @@ -144,7 +213,7 @@ impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> { // `replace` since there is no `OccupiedEntry::replace`. if self.0.contains(&value) { Some(self.0.replace(value)) - } else if self.0.len() == self.0.capacity() { + } else if self.0.len() == self.1 { None } else { _ = self.0.insert(value); @@ -153,11 +222,11 @@ impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> { } /// [`HashSet::entry`]. /// - /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] and `value` does not already exist in the + /// `None` is returned iff [`HashSet::len`] `==` [`Self::max_len`] and `value` does not already exist in the /// set. #[inline] pub fn entry(&mut self, value: T) -> Option<Entry<'_, T, S>> { - let full = self.0.len() == self.0.capacity(); + let full = self.0.len() == self.1; match self.0.entry(value) { ent @ Entry::Occupied(_) => Some(ent), ent @ Entry::Vacant(_) => { @@ -170,11 +239,12 @@ impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> { } } } -impl<T: Eq + Hash + TimedCeremony, S: BuildHasher> FixedCapHashSet<T, S> { - /// [`Self::insert`] except the first expired ceremony is removed in the event there is no available capacity. +impl<T: Eq + Hash + TimedCeremony, S: BuildHasher> MaxLenHashSet<T, S> { + /// [`Self::insert`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] + /// items have been added. #[inline] pub fn insert_remove_expired(&mut self, value: T) -> Option<bool> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { #[cfg(not(feature = "serializable_server_state"))] let now = Instant::now(); #[cfg(feature = "serializable_server_state")] @@ -188,22 +258,24 @@ impl<T: Eq + Hash + TimedCeremony, S: BuildHasher> FixedCapHashSet<T, S> { Some(self.0.insert(value)) } } - /// [`Self::insert`] except all expired ceremones are removed in the event there is no available capacity. + /// [`Self::insert`] except all expired ceremones are removed in the event [`Self::max_len`] items have + /// been added. #[inline] pub fn insert_remove_all_expired(&mut self, value: T) -> Option<bool> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { self.remove_expired_ceremonies(); } - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { self.0.contains(&value).then_some(false) } else { Some(self.0.insert(value)) } } - /// [`Self::entry`] except the first expired ceremony is removed in the event there is no available capacity. + /// [`Self::entry`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] + /// items have been added. #[inline] pub fn entry_remove_expired(&mut self, value: T) -> Option<Entry<'_, T, S>> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { #[cfg(not(feature = "serializable_server_state"))] let now = Instant::now(); #[cfg(feature = "serializable_server_state")] @@ -219,13 +291,14 @@ impl<T: Eq + Hash + TimedCeremony, S: BuildHasher> FixedCapHashSet<T, S> { Some(self.0.entry(value)) } } - /// [`Self::entry`] except all expired ceremones are removed in the event there is no available capacity. + /// [`Self::entry`] except all expired ceremones are removed in the event [`Self::max_len`] items have + /// been added. #[inline] pub fn entry_remove_all_expired(&mut self, value: T) -> Option<Entry<'_, T, S>> { - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { self.remove_expired_ceremonies(); } - if self.0.len() == self.0.capacity() { + if self.0.len() == self.1 { if let ent @ Entry::Occupied(_) = self.0.entry(value) { Some(ent) } else { @@ -236,27 +309,21 @@ impl<T: Eq + Hash + TimedCeremony, S: BuildHasher> FixedCapHashSet<T, S> { } } } -impl<T, S> AsRef<HashSet<T, S>> for FixedCapHashSet<T, S> { +impl<T, S> AsRef<HashSet<T, S>> for MaxLenHashSet<T, S> { #[inline] fn as_ref(&self) -> &HashSet<T, S> { &self.0 } } -impl<T, S> From<FixedCapHashSet<T, S>> for HashSet<T, S> { +impl<T, S> From<MaxLenHashSet<T, S>> for HashSet<T, S> { #[inline] - fn from(value: FixedCapHashSet<T, S>) -> Self { + fn from(value: MaxLenHashSet<T, S>) -> Self { value.0 } } -impl<T, S> From<HashSet<T, S>> for FixedCapHashSet<T, S> { - #[inline] - fn from(value: HashSet<T, S>) -> Self { - Self(value) - } -} #[cfg(test)] mod tests { - use super::{Equivalent, FixedCapHashSet, TimedCeremony}; + use super::{Equivalent, MaxLenHashSet, TimedCeremony}; use core::hash::{Hash, Hasher}; #[cfg(not(feature = "serializable_server_state"))] use std::time::Instant; @@ -309,28 +376,32 @@ mod tests { } #[test] fn hash_set_insert_removed() { - let mut set = FixedCapHashSet::new(8); + 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..cap { - assert_eq!(set.as_ref().capacity(), cap); + for i in 0..max_len { + assert!(set.as_ref().capacity() <= cap); cer.id = i; assert_eq!(set.insert(cer), Some(true)); } - assert_eq!(set.as_ref().capacity(), cap); - assert_eq!(set.as_ref().len(), cap); - for i in 0..cap { + 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), Some(true)); - assert_eq!(set.as_ref().capacity(), cap); - assert_eq!(set.as_ref().len(), cap); + assert!(set.as_ref().capacity() <= cap); + assert_eq!(set.as_ref().len(), max_len); let mut counter = 0; - for i in 0..cap { + for i in 0..max_len { counter += usize::from(set.as_ref().contains(&i)); } - assert_eq!(counter, cap - 1); - assert!(set.as_ref().contains(&cap)); + assert_eq!(counter, max_len - 1); + assert!(set.as_ref().contains(&(max_len - 1))); } } diff --git a/src/lib.rs b/src/lib.rs @@ -16,18 +16,18 @@ //! //! ## `webauthn_rp` in action //! -//! ```no_run +//! ``` //! use core::convert; //! use webauthn_rp::{ //! AuthenticatedCredential64, DiscoverableAuthentication64, DiscoverableAuthenticationServerState, //! DiscoverableCredentialRequestOptions, CredentialCreationOptions64, RegisteredCredential64, //! Registration, RegistrationServerState64, -//! hash::hash_set::FixedCapHashSet, +//! hash::hash_set::MaxLenHashSet, //! request::{ //! PublicKeyCredentialDescriptor, RpId, //! auth::AuthenticationVerificationOptions, //! register::{ -//! Nickname, PublicKeyCredentialUserEntity64, RegistrationVerificationOptions, +//! DisplayName, PublicKeyCredentialUserEntity64, RegistrationVerificationOptions, //! UserHandle64, Username, //! }, //! }, @@ -91,7 +91,7 @@ //! struct AccountReg<'a, 'b> { //! registration: Registration, //! user_name: Username<'a>, -//! user_display_name: Nickname<'b>, +//! user_display_name: DisplayName<'b>, //! } //! # #[cfg(feature = "serde")] //! impl<'de: 'a + 'b, 'a, 'b> Deserialize<'de> for AccountReg<'a, 'b> { @@ -110,12 +110,14 @@ //! /// will be used for subsequent credential registrations. //! # #[cfg(feature = "serde_relaxed")] //! fn start_account_creation( -//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>, +//! reg_ceremonies: &mut MaxLenHashSet<RegistrationServerState64>, //! ) -> Result<Vec<u8>, AppErr> { //! let user_id = UserHandle64::new(); +//! let user_name = Username::try_from("blank").unwrap(); +//! let user_display_name = DisplayName::Blank; //! let (server, client) = -//! CredentialCreationOptions64::first_passkey_with_blank_user_info( -//! RP_ID, &user_id, +//! CredentialCreationOptions64::passkey( +//! RP_ID, PublicKeyCredentialUserEntity64 { id: &user_id, name: user_name, display_name: user_display_name, }, Vec::new() //! ) //! .start_ceremony() //! .unwrap_or_else(|_e| { @@ -139,7 +141,7 @@ //! /// authenticator. //! # #[cfg(feature = "serde_relaxed")] //! fn finish_account_creation( -//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>, +//! reg_ceremonies: &mut MaxLenHashSet<RegistrationServerState64>, //! client_data: &[u8], //! ) -> Result<(), AppErr> { //! let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data)?; @@ -165,7 +167,7 @@ //! # #[cfg(feature = "serde_relaxed")] //! fn start_cred_registration( //! user_id: &UserHandle64, -//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>, +//! reg_ceremonies: &mut MaxLenHashSet<RegistrationServerState64>, //! ) -> Result<Vec<u8>, AppErr> { //! let (entity, creds) = select_user_info(user_id)?.ok_or(AppErr::NoAccount)?; //! let (server, client) = CredentialCreationOptions64::passkey(RP_ID, entity, creds) @@ -191,7 +193,7 @@ //! /// authenticator. //! # #[cfg(feature = "serde_relaxed")] //! fn finish_cred_registration( -//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>, +//! reg_ceremonies: &mut MaxLenHashSet<RegistrationServerState64>, //! client_data: &[u8], //! ) -> Result<(), AppErr> { //! // `Registration::from_json_custom` is available iff `serde_relaxed` is enabled. @@ -211,7 +213,7 @@ //! /// Starts the passkey authentication ceremony. //! # #[cfg(feature = "serde_relaxed")] //! fn start_auth( -//! auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>, +//! auth_ceremonies: &mut MaxLenHashSet<DiscoverableAuthenticationServerState>, //! ) -> Result<Vec<u8>, AppErr> { //! let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID) //! .start_ceremony() @@ -230,7 +232,7 @@ //! /// Finishes the passkey authentication ceremony. //! # #[cfg(feature = "serde_relaxed")] //! fn finish_auth( -//! auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>, +//! auth_ceremonies: &mut MaxLenHashSet<DiscoverableAuthenticationServerState>, //! client_data: &[u8], //! ) -> Result<(), AppErr> { //! // `DiscoverableAuthentication64::from_json_custom` is available iff `serde_relaxed` is enabled. @@ -275,11 +277,11 @@ //! /// # Errors //! /// //! /// Errors iff fetching the data errors. -//! fn select_user_info( +//! fn select_user_info<'name, 'display_name>( //! user_id: &UserHandle64, //! ) -> Result< //! Option<( -//! PublicKeyCredentialUserEntity64<'static, 'static, '_>, +//! PublicKeyCredentialUserEntity64<'name, 'display_name, '_>, //! Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, //! )>, //! AppErr, @@ -428,7 +430,7 @@ //! increasing. If data resides in memory, a monotonic [`Instant`] can be used instead. //! //! It is for those reasons data like [`RegistrationServerState`] are not serializable by default and require the -//! use of in-memory collections (e.g., [`FixedCapHashSet`]). To better ensure OOM is not a concern, RPs should set +//! use of in-memory collections (e.g., [`MaxLenHashSet`]). To better ensure OOM is not a concern, RPs should set //! reasonable timeouts. Since ceremonies can only be completed by moving data (e.g., //! [`RegistrationServerState::verify`]), ceremony completion is guaranteed to free up the memory used— //! `RegistrationServerState` instances are as small as 48 bytes on `x86_64-unknown-linux-gnu` platforms. To avoid @@ -500,6 +502,14 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(not(any(feature = "custom", all(feature = "bin", feature = "serde"))))] compile_error!("'custom' must be enabled or both 'bin' and 'serde' must be enabled"); +#[cfg(all(doc, feature = "serde"))] +use crate::request::register::ser::{ + PublicKeyCredentialCreationOptionsOwned, PublicKeyCredentialUserEntityOwned, +}; +#[cfg(feature = "serde")] +use crate::request::register::ser::{ + PublicKeyCredentialCreationOptionsOwnedErr, PublicKeyCredentialUserEntityOwnedErr, +}; #[cfg(feature = "serializable_server_state")] use crate::request::{ auth::ser_server_state::{ @@ -515,13 +525,13 @@ use crate::response::error::CredentialIdErr; use crate::response::ser_relaxed::SerdeJsonErr; #[cfg(doc)] use crate::{ - hash::hash_set::FixedCapHashSet, + hash::hash_set::MaxLenHashSet, request::{ AsciiDomain, DomainOrigin, Port, PublicKeyCredentialDescriptor, RpId, Scheme, TimedCeremony, Url, auth::{AllowedCredential, AllowedCredentials, PublicKeyCredentialRequestOptions}, register::{ - CoseAlgorithmIdentifier, Nickname, PublicKeyCredentialCreationOptions, + CoseAlgorithmIdentifier, DisplayName, Nickname, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle16, UserHandle64, Username, }, }, @@ -537,7 +547,7 @@ use crate::{ }; #[cfg(feature = "bin")] use crate::{ - request::register::bin::{DecodeNicknameErr, DecodeUsernameErr}, + request::register::bin::{DecodeDisplayNameErr, DecodeUsernameErr}, response::{ bin::DecodeAuthTransportsErr, register::bin::{DecodeDynamicStateErr, DecodeStaticStateErr}, @@ -586,10 +596,9 @@ use std::time::SystemTimeError; #[cfg(doc)] use std::time::{Instant, SystemTime}; /// Contains functionality to (de)serialize data to a data store. -#[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] pub mod bin; -/// Contains functionality for fixed-capacity hash maps and sets. +/// Contains functionality for maximum-length hash maps and sets that allocate exactly once. pub mod hash; /// Functionality for starting ceremonies. /// @@ -1046,7 +1055,6 @@ impl<'cred, 'user, const USER_LEN: usize, PublicKey> /// Errors iff the passed arguments are invalid. Read [`CredentialErr`] /// for more information. #[expect(single_use_lifetimes, reason = "false positive")] - #[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))] #[cfg(any(feature = "bin", feature = "custom"))] #[inline] pub fn new<'a: 'cred, 'b: 'user>( @@ -1147,67 +1155,61 @@ pub enum AggErr { CollectedClientData(CollectedClientDataErr), /// Variant when [`CollectedClientData::from_client_data_json_relaxed`] errors or any of the [`Deserialize`] /// implementations error when relying on [`Deserializer`] or [`StreamDeserializer`]. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] SerdeJson(SerdeJsonErr), /// Variant when [`Aaguid::try_from`] errors. Aaguid(AaguidErr), /// Variant when [`AuthTransports::decode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] DecodeAuthTransports(DecodeAuthTransportsErr), /// Variant when [`StaticState::decode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] DecodeStaticState(DecodeStaticStateErr), /// Variant when [`DynamicState::decode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] DecodeDynamicState(DecodeDynamicStateErr), - /// Variant when [`Nickname::decode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] + /// Variant when [`DisplayName::decode`] errors. #[cfg(feature = "bin")] - DecodeNickname(DecodeNicknameErr), + DecodeDisplayName(DecodeDisplayNameErr), /// Variant when [`Username::decode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] DecodeUsername(DecodeUsernameErr), /// Variant when [`RegistrationServerState::decode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] DecodeRegistrationServerState(DecodeRegistrationServerStateErr), /// Variant when [`DiscoverableAuthenticationServerState::decode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] DecodeDiscoverableAuthenticationServerState(DecodeDiscoverableAuthenticationServerStateErr), /// Variant when [`NonDiscoverableAuthenticationServerState::decode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] DecodeNonDiscoverableAuthenticationServerState( DecodeNonDiscoverableAuthenticationServerStateErr, ), /// Variant when [`RegistrationServerState::encode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] EncodeRegistrationServerState(SystemTimeError), /// Variant when [`DiscoverableAuthenticationServerState::encode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] EncodeDiscoverableAuthenticationServerState(SystemTimeError), /// Variant when [`NonDiscoverableAuthenticationServerState::encode`] errors. - #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] EncodeNonDiscoverableAuthenticationServerState( EncodeNonDiscoverableAuthenticationServerStateErr, ), /// Variant when [`AuthenticatedCredential::new`] errors. - #[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))] #[cfg(any(feature = "bin", feature = "custom"))] Credential(CredentialErr), /// Variant when [`CredentialId::try_from`] or [`CredentialId::decode`] errors. - #[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))] #[cfg(any(feature = "bin", feature = "custom"))] CredentialId(CredentialIdErr), + /// Variant when [`PublicKeyCredentialUserEntityOwned`] errors when converted into a + /// [`PublicKeyCredentialUserEntity`]. + #[cfg(feature = "serde")] + PublicKeyCredentialUserEntityOwned(PublicKeyCredentialUserEntityOwnedErr), + /// Variant when [`PublicKeyCredentialCreationOptionsOwned`] errors when converted into a + /// [`PublicKeyCredentialCreationOptions`]. + #[cfg(feature = "serde")] + PublicKeyCredentialCreationOptionsOwned(PublicKeyCredentialCreationOptionsOwnedErr), } impl From<AsciiDomainErr> for AggErr { #[inline] @@ -1305,7 +1307,6 @@ impl From<CollectedClientDataErr> for AggErr { Self::CollectedClientData(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] impl From<SerdeJsonErr> for AggErr { #[inline] @@ -1319,7 +1320,6 @@ impl From<AaguidErr> for AggErr { Self::Aaguid(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] impl From<DecodeAuthTransportsErr> for AggErr { #[inline] @@ -1327,7 +1327,6 @@ impl From<DecodeAuthTransportsErr> for AggErr { Self::DecodeAuthTransports(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] impl From<DecodeStaticStateErr> for AggErr { #[inline] @@ -1335,7 +1334,6 @@ impl From<DecodeStaticStateErr> for AggErr { Self::DecodeStaticState(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] impl From<DecodeDynamicStateErr> for AggErr { #[inline] @@ -1343,15 +1341,13 @@ impl From<DecodeDynamicStateErr> for AggErr { Self::DecodeDynamicState(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] -impl From<DecodeNicknameErr> for AggErr { +impl From<DecodeDisplayNameErr> for AggErr { #[inline] - fn from(value: DecodeNicknameErr) -> Self { - Self::DecodeNickname(value) + fn from(value: DecodeDisplayNameErr) -> Self { + Self::DecodeDisplayName(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] impl From<DecodeUsernameErr> for AggErr { #[inline] @@ -1359,7 +1355,6 @@ impl From<DecodeUsernameErr> for AggErr { Self::DecodeUsername(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] impl From<DecodeRegistrationServerStateErr> for AggErr { #[inline] @@ -1367,7 +1362,6 @@ impl From<DecodeRegistrationServerStateErr> for AggErr { Self::DecodeRegistrationServerState(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] impl From<DecodeDiscoverableAuthenticationServerStateErr> for AggErr { #[inline] @@ -1375,7 +1369,6 @@ impl From<DecodeDiscoverableAuthenticationServerStateErr> for AggErr { Self::DecodeDiscoverableAuthenticationServerState(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] impl From<DecodeNonDiscoverableAuthenticationServerStateErr> for AggErr { #[inline] @@ -1383,7 +1376,6 @@ impl From<DecodeNonDiscoverableAuthenticationServerStateErr> for AggErr { Self::DecodeNonDiscoverableAuthenticationServerState(value) } } -#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] impl From<EncodeNonDiscoverableAuthenticationServerStateErr> for AggErr { #[inline] @@ -1391,7 +1383,6 @@ impl From<EncodeNonDiscoverableAuthenticationServerStateErr> for AggErr { Self::EncodeNonDiscoverableAuthenticationServerState(value) } } -#[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))] #[cfg(any(feature = "bin", feature = "custom"))] impl From<CredentialErr> for AggErr { #[inline] @@ -1399,7 +1390,6 @@ impl From<CredentialErr> for AggErr { Self::Credential(value) } } -#[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))] #[cfg(any(feature = "bin", feature = "custom"))] impl From<CredentialIdErr> for AggErr { #[inline] @@ -1407,6 +1397,20 @@ impl From<CredentialIdErr> for AggErr { Self::CredentialId(value) } } +#[cfg(feature = "serde")] +impl From<PublicKeyCredentialUserEntityOwnedErr> for AggErr { + #[inline] + fn from(value: PublicKeyCredentialUserEntityOwnedErr) -> Self { + Self::PublicKeyCredentialUserEntityOwned(value) + } +} +#[cfg(feature = "serde")] +impl From<PublicKeyCredentialCreationOptionsOwnedErr> for AggErr { + #[inline] + fn from(value: PublicKeyCredentialCreationOptionsOwnedErr) -> Self { + Self::PublicKeyCredentialCreationOptionsOwned(value) + } +} impl Display for AggErr { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -1437,7 +1441,7 @@ impl Display for AggErr { #[cfg(feature = "bin")] Self::DecodeDynamicState(err) => err.fmt(f), #[cfg(feature = "bin")] - Self::DecodeNickname(err) => err.fmt(f), + Self::DecodeDisplayName(err) => err.fmt(f), #[cfg(feature = "bin")] Self::DecodeUsername(err) => err.fmt(f), #[cfg(feature = "serializable_server_state")] @@ -1456,6 +1460,10 @@ impl Display for AggErr { Self::Credential(err) => err.fmt(f), #[cfg(any(feature = "bin", feature = "custom"))] Self::CredentialId(err) => err.fmt(f), + #[cfg(feature = "serde")] + Self::PublicKeyCredentialUserEntityOwned(err) => err.fmt(f), + #[cfg(feature = "serde")] + Self::PublicKeyCredentialCreationOptionsOwned(err) => err.fmt(f), } } } diff --git a/src/request.rs b/src/request.rs @@ -1,6 +1,6 @@ #[cfg(doc)] use super::{ - hash::hash_set::FixedCapHashSet, + hash::hash_set::MaxLenHashSet, request::{ auth::{ AllowedCredential, AllowedCredentials, CredentialSpecificExtension, @@ -44,7 +44,7 @@ use url::Url as Uri; /// ``` /// # use core::convert; /// # use webauthn_rp::{ -/// # hash::hash_set::FixedCapHashSet, +/// # hash::hash_set::MaxLenHashSet, /// # request::{ /// # auth::{AllowedCredentials, DiscoverableCredentialRequestOptions, NonDiscoverableCredentialRequestOptions}, /// # register::UserHandle64, @@ -54,13 +54,13 @@ use url::Url as Uri; /// # AggErr, /// # }; /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); -/// let mut ceremonies = FixedCapHashSet::new(128); +/// let mut ceremonies = MaxLenHashSet::new(128); /// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?; /// assert!( /// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) /// ); /// # #[cfg(feature = "custom")] -/// let mut ceremonies_2 = FixedCapHashSet::new(128); +/// let mut ceremonies_2 = MaxLenHashSet::new(128); /// # #[cfg(feature = "serde")] /// assert!(serde_json::to_string(&client).is_ok()); /// let user_handle = get_user_handle(); @@ -107,10 +107,10 @@ pub mod error; /// ``` /// # use core::convert; /// # use webauthn_rp::{ -/// # hash::hash_set::FixedCapHashSet, +/// # hash::hash_set::MaxLenHashSet, /// # request::{ /// # register::{ -/// # CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64, +/// # CredentialCreationOptions, DisplayName, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64, /// # }, /// # PublicKeyCredentialDescriptor, RpId /// # }, @@ -119,7 +119,7 @@ pub mod error; /// # }; /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); /// # #[cfg(feature = "custom")] -/// let mut ceremonies = FixedCapHashSet::new(128); +/// let mut ceremonies = MaxLenHashSet::new(128); /// # #[cfg(feature = "custom")] /// let user_handle = get_user_handle(); /// # #[cfg(feature = "custom")] @@ -164,7 +164,7 @@ pub mod error; /// # Ok(PublicKeyCredentialUserEntity { /// # name: "foo".try_into()?, /// # id: user, -/// # display_name: None, +/// # display_name: DisplayName::Blank, /// # }) /// } /// /// Fetch the `PublicKeyCredentialDescriptor`s associated with `user`. @@ -181,12 +181,10 @@ pub mod error; /// ``` pub mod register; /// Contains functionality to serialize data to a client. -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] #[cfg(feature = "serde")] mod ser; /// Contains functionality to (de)serialize data needed for [`RegistrationServerState`], /// [`DiscoverableAuthenticationServerState`], and [`NonDiscoverableAuthenticationServerState`] to a data store. -#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] pub(super) mod ser_server_state; // `Challenge` must _never_ be constructable directly or indirectly; thus its tuple field must always be private, @@ -688,7 +686,7 @@ impl RpId { } /// Validates `hash` is the same as the SHA-256 hash of `self`. fn validate_rp_id_hash<E>(&self, hash: &[u8]) -> Result<(), CeremonyErr<E>> { - if hash == Sha256::digest(self.as_ref()).as_slice() { + if *hash == *Sha256::digest(self.as_ref()) { Ok(()) } else { Err(CeremonyErr::RpIdHashMismatch) @@ -1668,7 +1666,6 @@ pub trait TimedCeremony { /// Returns the `Instant` the ceremony expires. /// /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. - #[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))] #[cfg(any(doc, not(feature = "serializable_server_state")))] fn expiration(&self) -> Instant; /// Returns the `SystemTime` the ceremony expires. @@ -1732,7 +1729,7 @@ mod tests { }, }, }, - Challenge, Credentials, ExtensionInfo, ExtensionReq, PrfInput, + Challenge, Credentials as _, ExtensionInfo, ExtensionReq, PrfInput, PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement, auth::{ AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions, @@ -1740,12 +1737,13 @@ mod tests { Extension as AuthExt, NonDiscoverableCredentialRequestOptions, PrfInputOwned, }, register::{ - CredProtect, CredentialCreationOptions, Extension as RegExt, FourToSixtyThree, - PublicKeyCredentialUserEntity, RegistrationVerificationOptions, UserHandle, + CredProtect, CredentialCreationOptions, DisplayName, Extension as RegExt, + FourToSixtyThree, PublicKeyCredentialUserEntity, RegistrationVerificationOptions, + UserHandle, }, }; #[cfg(feature = "custom")] - use ed25519_dalek::{Signer, SigningKey}; + use ed25519_dalek::{Signer as _, SigningKey}; #[cfg(feature = "custom")] use p256::{ ecdsa::{DerSignature as P256DerSig, SigningKey as P256Key}, @@ -1757,9 +1755,9 @@ mod tests { use rsa::{ BigUint, RsaPrivateKey, pkcs1v15::SigningKey as RsaKey, - sha2::{Digest, Sha256}, - signature::{Keypair, SignatureEncoding}, - traits::PublicKeyParts, + sha2::{Digest as _, Sha256}, + signature::{Keypair as _, SignatureEncoding as _}, + traits::PublicKeyParts as _, }; use serde_json as _; #[cfg(feature = "custom")] @@ -1809,10 +1807,13 @@ mod tests { 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("λ.com").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> { @@ -1822,7 +1823,7 @@ mod tests { PublicKeyCredentialUserEntity { name: "foo".try_into()?, id: &id, - display_name: None, + display_name: DisplayName::Blank, }, Vec::new(), ); @@ -2135,13 +2136,11 @@ mod tests { ] .as_slice(), ); - attestation_object - .extend_from_slice(Sha256::digest(client_data_json.as_slice()).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()).as_slice()); + 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()); @@ -2164,6 +2163,9 @@ mod tests { )?.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> { @@ -2333,10 +2335,8 @@ mod tests { ] .as_slice(), ); - authenticator_data[..32] - .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice()); - authenticator_data - .extend_from_slice(Sha256::digest(client_data_json.as_slice()).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); @@ -2378,6 +2378,14 @@ mod tests { )?); 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> { @@ -2387,7 +2395,7 @@ mod tests { PublicKeyCredentialUserEntity { name: "foo".try_into()?, id: &id, - display_name: None, + display_name: DisplayName::Blank, }, Vec::new(), ); @@ -2601,8 +2609,7 @@ mod tests { ] .as_slice(), ); - attestation_object[30..62] - .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).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, @@ -2632,9 +2639,16 @@ mod tests { }, }, &RegistrationVerificationOptions::<&str, &str>::default(), - )?.static_state.credential_public_key, UncompressedPubKey::P256(k) if k.x() == x.as_slice() && k.y() == y.as_slice())); + )?.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> { @@ -2691,10 +2705,8 @@ mod tests { ] .as_slice(), ); - authenticator_data[..32] - .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice()); - authenticator_data - .extend_from_slice(Sha256::digest(client_data_json.as_slice()).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, @@ -2745,6 +2757,14 @@ mod tests { )?); 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> { @@ -2754,7 +2774,7 @@ mod tests { PublicKeyCredentialUserEntity { name: "foo".try_into()?, id: &id, - display_name: None, + display_name: DisplayName::Blank, }, Vec::new(), ); @@ -3002,8 +3022,7 @@ mod tests { ] .as_slice(), ); - attestation_object[30..62] - .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).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, @@ -3034,9 +3053,16 @@ mod tests { }, }, &RegistrationVerificationOptions::<&str, &str>::default(), - )?.static_state.credential_public_key, UncompressedPubKey::P384(k) if k.x() == x.as_slice() && k.y() == y.as_slice())); + )?.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> { @@ -3093,10 +3119,8 @@ mod tests { ] .as_slice(), ); - authenticator_data[..32] - .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice()); - authenticator_data - .extend_from_slice(Sha256::digest(client_data_json.as_slice()).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, @@ -3148,6 +3172,15 @@ mod tests { )?); 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> { @@ -3157,7 +3190,7 @@ mod tests { PublicKeyCredentialUserEntity { name: "foo".try_into()?, id: &id, - display_name: None, + display_name: DisplayName::Blank, }, Vec::new(), ); @@ -3564,8 +3597,7 @@ mod tests { ] .as_slice(), ); - attestation_object[31..63] - .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).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, @@ -3582,7 +3614,7 @@ mod tests { 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 = 65537; + 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, @@ -3635,8 +3667,8 @@ mod tests { .unwrap(), ) .verifying_key(); - let n = rsa_key.as_ref().n().to_bytes_be(); - attestation_object[113..369].copy_from_slice(n.as_slice()); + let n_other = rsa_key.as_ref().n().to_bytes_be(); + attestation_object[113..369].copy_from_slice(n_other.as_slice()); assert!(matches!(opts.start_ceremony()?.0.verify( RP_ID, &Registration { @@ -3652,9 +3684,17 @@ mod tests { }, }, &RegistrationVerificationOptions::<&str, &str>::default(), - )?.static_state.credential_public_key, UncompressedPubKey::Rsa(k) if *k.n() == n.as_slice() && k.e() == e)); + )?.static_state.credential_public_key, UncompressedPubKey::Rsa(k) if *k.n() == n_other.as_slice() && 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> { @@ -3711,10 +3751,8 @@ mod tests { ] .as_slice(), ); - authenticator_data[..32] - .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice()); - authenticator_data - .extend_from_slice(Sha256::digest(client_data_json.as_slice()).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, @@ -3731,7 +3769,7 @@ mod tests { 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 = 65537; + 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, diff --git a/src/request/auth.rs b/src/request/auth.rs @@ -43,12 +43,10 @@ use std::time::SystemTime; /// Contains error types. pub mod error; /// Contains functionality to serialize data to a client. -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] #[cfg(feature = "serde")] pub mod ser; /// Contains functionality to (de)serialize [`DiscoverableAuthenticationServerState`] and /// [`NonDiscoverableAuthenticationServerState`] to a data store. -#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] pub mod ser_server_state; /// Controls how [signature counter](https://www.w3.org/TR/webauthn-3/#signature-counter) is enforced. @@ -89,7 +87,7 @@ impl SignatureCounterEnforcement { /// /// When relying on [`NonDiscoverableCredentialRequestOptions`], it's recommended to use credential-specific PRF /// inputs that are continuously rolled over. One uses this type for such a thing. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct PrfInputOwned { /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first). pub first: Vec<u8>, @@ -1029,7 +1027,6 @@ pub struct AuthenticationVerificationOptions<'origins, 'top_origins, O, T> { /// Dictates what happens when [`AuthenticatorData::sign_count`] is not updated to a strictly greater value. pub sig_counter_enforcement: SignatureCounterEnforcement, /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] pub client_data_json_relaxed: bool, } @@ -1654,6 +1651,7 @@ mod tests { 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> { @@ -1749,6 +1747,9 @@ mod tests { 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::type_complexity, reason = "fine")] + #[expect(clippy::too_many_lines, reason = "a lot to test")] #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] fn generate_authenticator_data_public_key_sig( opts: TestResponseOptions, @@ -1816,8 +1817,7 @@ mod tests { ] .as_slice(), ); - authenticator_data[..32] - .copy_from_slice(Sha256::digest("example.com".as_bytes()).as_slice()); + authenticator_data[..32].copy_from_slice(&Sha256::digest(b"example.com")); match opts.hmac { HmacSecret::None => {} HmacSecret::One => { @@ -1871,7 +1871,7 @@ mod tests { } let len = authenticator_data.len(); authenticator_data - .extend_from_slice(Sha256::digest(generate_client_data_json().as_slice()).as_slice()); + .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); @@ -1916,7 +1916,7 @@ mod tests { PrfUvOptions::None(required) => { if required { opts.public_key.user_verification = UserVerificationRequirement::Required; - }; + } } PrfUvOptions::Prf(input) => { opts.public_key.user_verification = UserVerificationRequirement::Required; @@ -1967,9 +1967,9 @@ mod tests { /// 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] + #[ignore = "slow"] #[test] - fn test_uv_required_err() { + fn uv_required_err() { const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ CredentialProtectionPolicy::None, CredentialProtectionPolicy::UserVerificationOptional, @@ -2032,7 +2032,7 @@ mod tests { hmac, }, cred: TestCredOptions { cred_protect, prf, }, - }).map_or_else(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::UserNotVerified)), |_| false)); + }).is_err_and(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::UserNotVerified)))); } } } @@ -2043,7 +2043,7 @@ mod tests { /// 4 * 5 * 2 * 2 = 80 tests. #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] #[test] - fn test_forbidden_hmac() { + fn forbidden_hmac() { const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [ CredentialProtectionPolicy::None, CredentialProtectionPolicy::UserVerificationOptional, @@ -2073,15 +2073,16 @@ mod tests { hmac, }, cred: TestCredOptions { cred_protect, prf, }, - }).map_or_else(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenHmacSecret))), |_| false)); + }).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 test_prf() -> Result<(), AggErr> { + fn prf() -> Result<(), AggErr> { let mut opts = TestOptions { request: TestRequestOptions { error_unsolicited: false, @@ -2111,7 +2112,7 @@ mod tests { ExtensionReq::Require, )); opts.cred.prf = PrfCredOptions::TrueHmacTrue; - assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::MissingHmacSecret))), |_| false)); + 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 { @@ -2121,16 +2122,16 @@ mod tests { ExtensionReq::Allow, )); opts.cred.prf = PrfCredOptions::TrueNoHmac; - assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential))), |_| false)); + 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).map_or_else(|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)))), |_| false)); + 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).map_or_else(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::HmacSecretForNonHmacSecretCredential))), |_| false)); + 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).map_or_else(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::UserNotVerifiedHmacSecret))), |_| false)); + 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,18 +1,14 @@ -#[cfg(doc)] -use super::ExtensionReq; use super::{ - super::{ - super::response::ser::Null, - ser::{DEFAULT_RP_ID, PrfHelper}, - }, + super::{super::response::ser::Null, ser::PrfHelper}, AllowedCredential, AllowedCredentials, Challenge, CredentialMediationRequirement, Credentials as _, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, - Extension, FIVE_MINUTES, Hint, NonDiscoverableAuthenticationClientState, + Extension, ExtensionReq, FIVE_MINUTES, Hint, NonDiscoverableAuthenticationClientState, NonDiscoverableCredentialRequestOptions, PrfInput, PrfInputOwned, PublicKeyCredentialRequestOptions, RpId, UserVerificationRequirement, }; use core::{ - fmt::{self, Formatter}, + error::Error as E, + fmt::{self, Display, Formatter}, num::NonZeroU32, }; use serde::{ @@ -525,23 +521,34 @@ pub struct ExtensionOwned { /// See [`Extension::prf`]. pub prf: Option<PrfInputOwned>, } -impl<'a: 'prf_first + 'prf_second, 'prf_first, 'prf_second> From<&'a ExtensionOwned> - for Extension<'prf_first, 'prf_second> -{ +impl ExtensionOwned { + /// Returns an `Extension` based on `self`. #[inline] - fn from(value: &'a ExtensionOwned) -> Self { - Self { - prf: value.prf.as_ref().map(|input| { + #[must_use] + pub fn as_extension(&self) -> Extension<'_, '_> { + Extension { + prf: self.prf.as_ref().map(|prf| { ( PrfInput { - first: input.first.as_slice(), - second: input.second.as_deref(), + first: &prf.first, + second: prf.second.as_deref(), }, - input.ext_req, + prf.ext_req, ) }), } } + /// Returns an `Extension` based on `self` and `prf`. + /// + /// Note `prf` is used _unconditionally_ regardless if [`Self::prf`] is `Some`. + #[inline] + #[must_use] + pub const fn with_prf<'prf_first, 'prf_second>( + &self, + prf: (PrfInput<'prf_first, 'prf_second>, ExtensionReq), + ) -> Extension<'prf_first, 'prf_second> { + Extension { prf: Some(prf) } + } } impl<'de> Deserialize<'de> for ExtensionOwned { /// Deserializes a `struct` according to the following pseudo-schema: @@ -650,13 +657,25 @@ impl<'de> Deserialize<'de> for ExtensionOwned { deserializer.deserialize_struct("ExtensionOwned", FIELDS, ExtensionOwnedVisitor) } } -/// Similar to [`PublicKeyCredentialRequestOptions`] except the fields are based on owned data. +/// Error returned by [`PublicKeyCredentialRequestOptionsOwned::as_options`] when +/// [`PublicKeyCredentialRequestOptionsOwned::rp_id`] is `None`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PublicKeyCredentialRequestOptionsOwnedErr; +impl Display for PublicKeyCredentialRequestOptionsOwnedErr { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("request options did not have an RP ID") + } +} +impl E for PublicKeyCredentialRequestOptionsOwnedErr {} +/// Similar to [`PublicKeyCredentialRequestOptions`] except the fields are based on owned data, and +/// [`Self::rp_id`] is optional. /// /// This is primarily useful to assist [`ClientCredentialRequestOptions::deserialize`], #[derive(Debug)] pub struct PublicKeyCredentialRequestOptionsOwned { /// See [`PublicKeyCredentialRequestOptions::rp_id`]. - pub rp_id: RpId, + pub rp_id: Option<RpId>, /// See [`PublicKeyCredentialRequestOptions::timeout`]. pub timeout: NonZeroU32, /// See [`PublicKeyCredentialRequestOptions::user_verification`]. @@ -667,18 +686,93 @@ pub struct PublicKeyCredentialRequestOptionsOwned { pub extensions: ExtensionOwned, } impl PublicKeyCredentialRequestOptionsOwned { - /// Creates a `PublicKeyCredentialRequestOptions` based on the contained data and randomly-generated - /// [`Challenge`]. + /// Returns a `PublicKeyCredentialRequestOptions` based on `self`. + /// + /// # Errors + /// + /// Errors iff [`Self::rp_id`] is `None`. + #[inline] + pub fn as_options( + &self, + ) -> Result< + PublicKeyCredentialRequestOptions<'_, '_, '_>, + PublicKeyCredentialRequestOptionsOwnedErr, + > { + self.rp_id + .as_ref() + .ok_or(PublicKeyCredentialRequestOptionsOwnedErr) + .map(|rp_id| PublicKeyCredentialRequestOptions { + challenge: Challenge::new(), + timeout: self.timeout, + rp_id, + user_verification: self.user_verification, + hints: self.hints, + extensions: self.extensions.as_extension(), + }) + } + /// Returns a `PublicKeyCredentialRequestOptions` based on `self` and `rp_id`. + /// + /// Note `rp_id` is used _unconditionally_ regardless if [`Self::rp_id`] is `Some`. + #[inline] + #[must_use] + pub fn with_rp_id<'rp_id>( + &self, + rp_id: &'rp_id RpId, + ) -> PublicKeyCredentialRequestOptions<'rp_id, '_, '_> { + PublicKeyCredentialRequestOptions { + challenge: Challenge::new(), + timeout: self.timeout, + rp_id, + user_verification: self.user_verification, + hints: self.hints, + extensions: self.extensions.as_extension(), + } + } + /// Returns a `PublicKeyCredentialRequestOptions` based on `self`, `exclude_credentials`, and `extensions`. + /// + /// Note `extensions` is used _unconditionally_ regardless of what [`Self::extensions`] is. + /// + /// # Errors + /// + /// Errors iff [`Self::rp_id`] is `None`. + #[inline] + pub fn with_extensions<'prf_first, 'prf_second>( + &self, + extensions: Extension<'prf_first, 'prf_second>, + ) -> Result< + PublicKeyCredentialRequestOptions<'_, 'prf_first, 'prf_second>, + PublicKeyCredentialRequestOptionsOwnedErr, + > { + self.rp_id + .as_ref() + .ok_or(PublicKeyCredentialRequestOptionsOwnedErr) + .map(|rp_id| PublicKeyCredentialRequestOptions { + challenge: Challenge::new(), + timeout: self.timeout, + rp_id, + user_verification: self.user_verification, + hints: self.hints, + extensions, + }) + } + /// Returns a `PublicKeyCredentialRequestOptions` based on `self`, `rp_id`, and `extensions`. + /// + /// Note `rp_id` and `extensions` are used _unconditionally_ regardless if [`Self::rp_id`] is `Some` or what + /// [`Self::extensions`] is. #[inline] #[must_use] - pub fn into_options(&self) -> PublicKeyCredentialRequestOptions<'_, '_, '_> { + pub fn with_rp_id_and_extensions<'rp_id, 'prf_first, 'prf_second>( + &self, + rp_id: &'rp_id RpId, + extensions: Extension<'prf_first, 'prf_second>, + ) -> PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { PublicKeyCredentialRequestOptions { - rp_id: &self.rp_id, challenge: Challenge::new(), timeout: self.timeout, + rp_id, user_verification: self.user_verification, hints: self.hints, - extensions: (&self.extensions).into(), + extensions, } } } @@ -686,7 +780,7 @@ impl Default for PublicKeyCredentialRequestOptionsOwned { #[inline] fn default() -> Self { Self { - rp_id: DEFAULT_RP_ID, + rp_id: None, timeout: FIVE_MINUTES, user_verification: UserVerificationRequirement::Preferred, hints: Hint::default(), @@ -706,13 +800,9 @@ impl<'de> Deserialize<'de> for PublicKeyCredentialRequestOptionsOwned { /// exists, it must be `null` or empty. /// /// If [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-timeout) exists, - /// it must be `null` or positive. + /// it must be `null` or positive. If `timeout` is missing or is `null`, then [`FIVE_MINUTES`] will be used. /// - /// In the event there is no RP ID defined, the value `"example.invalid"` will be used. - /// - /// For any field that does not exist or is `null`, the corresponding [`Default`] `impl` will be used. For - /// `user_verification`, [`UserVerificationRequirement::Preferred`] will be used. For `timeout`, - /// [`FIVE_MINUTES`] will be used. + /// If `userVerification` is missing or is `null`, then [`UserVerificationRequirement::Required`] will be used. /// /// Unknown or duplicate fields lead to an error. #[expect(clippy::too_many_lines, reason = "131 lines is fine")] @@ -797,7 +887,7 @@ impl<'de> Deserialize<'de> for PublicKeyCredentialRequestOptionsOwned { if rp.is_some() { return Err(Error::duplicate_field(RP_ID)); } - rp = map.next_value::<Option<RpId>>().map(Some)?; + rp = map.next_value::<Option<_>>().map(Some)?; } Field::UserVerification => { if user_veri.is_some() { @@ -838,7 +928,7 @@ impl<'de> Deserialize<'de> for PublicKeyCredentialRequestOptionsOwned { } } Ok(PublicKeyCredentialRequestOptionsOwned { - rp_id: rp.flatten().unwrap_or(DEFAULT_RP_ID), + rp_id: rp.flatten(), user_verification: user_veri .flatten() .unwrap_or(UserVerificationRequirement::Preferred), @@ -870,10 +960,7 @@ impl<'de> Deserialize<'de> for PublicKeyCredentialRequestOptionsOwned { /// /// It's common to tailor an authentication ceremony based on a user's environment. The options that should be /// used are then sent to the server. To facilitate this, [`Self::deserialize`] can be used to deserialize the data -/// sent from the client. Upon successful deserialization, [`Self::into_discoverable_options`] and -/// [`Self::into_non_discoverable_options`] can then be used to construct the -/// appropriate [`DiscoverableCredentialRequestOptions`] and [`NonDiscoverableCredentialRequestOptions`] -/// respectively. +/// sent from the client. /// /// Note one may want to change some of the [`Extension`] data since [`ExtensionReq::Allow`] is unconditionally /// used. Read [`ExtensionOwned::deserialize`] for more information. @@ -889,43 +976,6 @@ pub struct ClientCredentialRequestOptions { /// See [`NonDiscoverableCredentialRequestOptions::options`]. pub public_key: PublicKeyCredentialRequestOptionsOwned, } -impl ClientCredentialRequestOptions { - /// Creates a `DiscoverableCredentialRequestOptions` based on the contained data where - /// [`DiscoverableCredentialRequestOptions::public_key`] is constructed via - /// [`PublicKeyCredentialRequestOptionsOwned::into_options`]. - #[inline] - #[must_use] - pub fn into_discoverable_options(&self) -> DiscoverableCredentialRequestOptions<'_, '_, '_> { - DiscoverableCredentialRequestOptions { - mediation: self.mediation, - public_key: self.public_key.into_options(), - } - } - /// Creates a `NonDiscoverableCredentialRequestOptions` based on the contained data where - /// [`NonDiscoverableCredentialRequestOptions::options`] is constructed via - /// [`PublicKeyCredentialRequestOptionsOwned::into_options`]. - #[inline] - #[must_use] - pub fn into_non_discoverable_options( - &self, - allow_credentials: AllowedCredentials, - ) -> NonDiscoverableCredentialRequestOptions<'_, '_, '_> { - NonDiscoverableCredentialRequestOptions { - mediation: self.mediation, - options: self.public_key.into_options(), - allow_credentials, - } - } -} -impl Default for ClientCredentialRequestOptions { - #[inline] - fn default() -> Self { - Self { - mediation: CredentialMediationRequirement::default(), - public_key: PublicKeyCredentialRequestOptionsOwned::default(), - } - } -} impl<'de> Deserialize<'de> for ClientCredentialRequestOptions { /// Deserializes a `struct` according to the following pseudo-schema: /// @@ -1027,29 +1077,38 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions { mod test { use super::{ super::ExtensionReq, ClientCredentialRequestOptions, CredentialMediationRequirement, - DEFAULT_RP_ID, ExtensionOwned, FIVE_MINUTES, Hint, NonZeroU32, - PublicKeyCredentialRequestOptionsOwned, UserVerificationRequirement, + ExtensionOwned, FIVE_MINUTES, Hint, 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()[..56], - *"unknown field `bob`, expected `mediation` or `publicKey`" + 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()[..27], *"duplicate field `mediation`"); - let mut options = serde_json::from_str::<ClientCredentialRequestOptions>(r#"{}"#)?; + 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_eq!(options.public_key.rp_id, DEFAULT_RP_ID); + assert!(options.public_key.rp_id.is_none()); assert_eq!(options.public_key.timeout, FIVE_MINUTES); assert!(matches!( options.public_key.user_verification, @@ -1064,7 +1123,7 @@ mod test { options.mediation, CredentialMediationRequirement::Required )); - assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID); + assert!(options.public_key.rp_id.is_none()); assert_eq!(options.public_key.timeout, FIVE_MINUTES); assert!(matches!( options.public_key.user_verification, @@ -1073,7 +1132,7 @@ mod test { assert!(matches!(options.public_key.hints, Hint::None)); assert!(options.public_key.extensions.prf.is_none()); options = serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"publicKey":{}}"#)?; - assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID); + assert!(options.public_key.rp_id.is_none()); assert_eq!(options.public_key.timeout, FIVE_MINUTES); assert!(matches!( options.public_key.user_verification, @@ -1088,7 +1147,12 @@ mod test { options.mediation, CredentialMediationRequirement::Conditional )); - assert_eq!(options.public_key.rp_id.as_ref(), "example.com"); + 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, @@ -1099,55 +1163,63 @@ mod test { .public_key .extensions .prf - .map_or(false, |prf| prf.first.is_empty() + .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()[..130], - *"unknown field `bob`, expected one of `rpId`, `userVerification`, `challenge`, `timeout`, `allowCredentials`, `hints`, `extensions`" + 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()[..22], *"duplicate field `rpId`"); + 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()[..41], - *"invalid type: Option value, expected null" + 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()[..19], *"trailing characters"); + 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()[..50], - *"invalid value: integer `0`, expected a nonzero u32" + 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()[..59], - *"invalid value: integer `4294967296`, expected a nonzero u32" + err.to_string().get(..59), + Some("invalid value: integer `4294967296`, expected a nonzero u32") ); - let mut key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{}"#)?; - assert_eq!(key.rp_id, DEFAULT_RP_ID); + 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, @@ -1158,7 +1230,7 @@ mod test { key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( r#"{"rpId":null,"timeout":null,"allowCredentials":null,"userVerification":null,"extensions":null,"hints":null,"challenge":null}"#, )?; - assert_eq!(key.rp_id, DEFAULT_RP_ID); + assert!(key.rp_id.is_none()); assert_eq!(key.timeout, FIVE_MINUTES); assert!(matches!( key.user_verification, @@ -1182,14 +1254,14 @@ mod test { 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_eq!(key.rp_id.as_ref(), "example.com"); + 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!(matches!(key.hints, Hint::SecurityKey)); - assert!(key.extensions.prf.map_or(false, |prf| prf.first.is_empty() + 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>( @@ -1198,32 +1270,37 @@ mod test { 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()[..35], - *"unknown field `bob`, expected `prf`" + 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()[..21], *"duplicate field `prf`"); + 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()[..51], - *"invalid type: null, expected base64url-encoded data" + 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.map_or(false, |prf| prf.first.is_empty() + 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>(r#"{}"#)?; + ext = serde_json::from_str::<ExtensionOwned>("{}")?; assert!(ext.prf.is_none()); Ok(()) } diff --git a/src/request/register.rs b/src/request/register.rs @@ -41,21 +41,17 @@ use std::time::Instant; #[cfg(any(doc, feature = "serializable_server_state"))] use std::time::SystemTime; /// Contains functionality to (de)serialize data to a data store. -#[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] pub mod bin; /// Contains functionality that needs to be accessible when `bin` or `serde` are not enabled. -#[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] mod custom; /// Contains error types. pub mod error; /// Contains functionality to (de)serialize data to a client. -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] #[cfg(feature = "serde")] pub mod ser; /// Contains functionality to (de)serialize [`RegistrationServerState`] to a data store. -#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))] #[cfg(feature = "serializable_server_state")] pub mod ser_server_state; /// Used by [`Extension::cred_protect`] to enforce the [`CredentialProtectionPolicy`] sent by the client via @@ -235,6 +231,7 @@ impl<'a> Nickname<'a> { }) } /// Same as [`Self::try_from`]. + /// /// # Errors /// /// Errors iff `value` violates [RFC 8266 § 2.3](https://www.rfc-editor.org/rfc/rfc8266.html#section-2.3) @@ -361,6 +358,167 @@ impl Ord for Nickname<'_> { self.0.cmp(&other.0) } } +/// Name intended to be displayed to a user. +#[derive(Clone, Debug)] +pub enum DisplayName<'a> { + /// A blank string. + Blank, + /// A non-blank string conforming to RFC 8266. + Nickname(Nickname<'a>), +} +impl<'a> DisplayName<'a> { + /// The maximum allowed length. + pub const MAX_LEN: usize = 1023; + /// The recommended maximum length to allow. + pub const RECOMMENDED_MAX_LEN: usize = 64; + /// Returns a `DisplayName` that consumes `self`. When `self` owns the data, the data is simply moved; + /// when the data is borrowed, then it is cloned into an owned instance. + #[inline] + #[must_use] + pub fn into_owned<'b>(self) -> DisplayName<'b> { + match self { + Self::Blank => DisplayName::Blank, + Self::Nickname(val) => DisplayName::Nickname(val.into_owned()), + } + } + /// Same as [`Self::with_max_len`] except the length must not exceed [`Self::RECOMMENDED_MAX_LEN`] instead of + /// [`Self::MAX_LEN`]. + /// + /// # Errors + /// + /// Errors iff `value` is not empty and [`Nickname::with_recommended_len`] errors. + #[expect(single_use_lifetimes, reason = "false positive")] + #[inline] + pub fn with_recommended_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, NicknameErr> { + if value.is_empty() { + Ok(Self::Blank) + } else { + Nickname::with_recommended_len(value).map(Self::Nickname) + } + } + /// Same as [`Self::try_from`]. + /// + /// # Errors + /// + /// Errors iff `value` is not empty and [`Nickname::with_max_len`] errors. + #[expect(single_use_lifetimes, reason = "false positive")] + #[inline] + pub fn with_max_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, NicknameErr> { + Self::try_from(value) + } +} +impl AsRef<str> for DisplayName<'_> { + #[inline] + fn as_ref(&self) -> &str { + match *self { + Self::Blank => "", + Self::Nickname(ref val) => val.as_ref(), + } + } +} +impl Borrow<str> for DisplayName<'_> { + #[inline] + fn borrow(&self) -> &str { + self.as_ref() + } +} +impl<'a: 'b, 'b> From<&'a DisplayName<'_>> for DisplayName<'b> { + #[inline] + fn from(value: &'a DisplayName<'_>) -> Self { + match *value { + DisplayName::Blank => Self::Blank, + DisplayName::Nickname(ref val) => Self::Nickname(val.into()), + } + } +} +impl<'a: 'b, 'b> From<DisplayName<'a>> for Cow<'b, str> { + #[inline] + fn from(value: DisplayName<'a>) -> Self { + match value { + DisplayName::Blank => Cow::Borrowed(""), + DisplayName::Nickname(val) => val.into(), + } + } +} +impl<'a: 'b, 'b> TryFrom<Cow<'a, str>> for DisplayName<'b> { + type Error = NicknameErr; + /// # Examples + /// + /// ``` + /// # use std::borrow::Cow; + /// # use webauthn_rp::request::register::{error::NicknameErr, DisplayName}; + /// assert_eq!( + /// DisplayName::try_from(Cow::Borrowed(""))?.as_ref(), + /// "" + /// ); + /// assert_eq!( + /// DisplayName::try_from(Cow::Borrowed("Sir Isaac Newton"))?.as_ref(), + /// "Sir Isaac Newton" + /// ); + /// # Ok::<_, NicknameErr>(()) + /// ``` + #[inline] + fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> { + if value.is_empty() { + Ok(Self::Blank) + } else { + Nickname::try_from(value).map(Self::Nickname) + } + } +} +impl<'a: 'b, 'b> TryFrom<&'a str> for DisplayName<'b> { + type Error = NicknameErr; + /// Same as [`DisplayName::try_from`] except the input is a `str`. + #[inline] + fn try_from(value: &'a str) -> Result<Self, Self::Error> { + Self::try_from(Cow::Borrowed(value)) + } +} +impl TryFrom<String> for DisplayName<'_> { + type Error = NicknameErr; + /// Same as [`DisplayName::try_from`] except the input is a `String`. + #[inline] + fn try_from(value: String) -> Result<Self, Self::Error> { + Self::try_from(Cow::Owned(value)) + } +} +impl PartialEq<DisplayName<'_>> for DisplayName<'_> { + #[inline] + fn eq(&self, other: &DisplayName<'_>) -> bool { + self.as_ref() == other.as_ref() + } +} +impl PartialEq<&DisplayName<'_>> for DisplayName<'_> { + #[inline] + fn eq(&self, other: &&DisplayName<'_>) -> bool { + *self == **other + } +} +impl PartialEq<DisplayName<'_>> for &DisplayName<'_> { + #[inline] + fn eq(&self, other: &DisplayName<'_>) -> bool { + **self == *other + } +} +impl Eq for DisplayName<'_> {} +impl Hash for DisplayName<'_> { + #[inline] + fn hash<H: Hasher>(&self, state: &mut H) { + self.as_ref().hash(state); + } +} +impl PartialOrd<DisplayName<'_>> for DisplayName<'_> { + #[inline] + fn partial_cmp(&self, other: &DisplayName<'_>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} +impl Ord for DisplayName<'_> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.as_ref().cmp(other.as_ref()) + } +} /// String returned from the /// [UsernameCasePreserved Enforcement rule](https://www.rfc-editor.org/rfc/rfc8265#section-3.4.3) as defined in /// RFC 8265. @@ -403,6 +561,7 @@ impl<'a> Username<'a> { }) } /// Same as [`Self::try_from`]. + /// /// # Errors /// /// Errors iff `value` violates [RFC 8265 § 3.4.3](https://www.rfc-editor.org/rfc/rfc8265.html#section-3.4.3) @@ -412,12 +571,6 @@ impl<'a> Username<'a> { pub fn with_max_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, UsernameErr> { Self::try_from(value) } - /// Returns `Self` containing `"blank"`. - #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] - fn blank() -> Self { - Self::try_from("blank") - .unwrap_or_else(|_e| unreachable!("'blank' is no longer a valid Username")) - } } impl AsRef<str> for Username<'_> { #[inline] @@ -1109,7 +1262,6 @@ impl UserHandle16 { /// user[8] = 255; /// assert!(UserHandle16::from_uuid_v4(user).is_none()); /// ``` - #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] #[inline] #[must_use] @@ -1127,40 +1279,7 @@ pub struct PublicKeyCredentialUserEntity<'name, 'display_name, 'id, const LEN: u /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id). pub id: &'id UserHandle<LEN>, /// [`displayName`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname). - /// - /// `None` iff the display name should be the empty string. - pub display_name: Option<Nickname<'display_name>>, -} -impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<LEN>> - for PublicKeyCredentialUserEntity<'_, '_, 'b, LEN> -{ - /// Returns a `PublicKeyCredentialUserEntity` with [`Self::name`] set to `"blank"`, - /// [`Self::id`] set to `value`, and [`Self::display_name`] set to `None`. - /// - /// One should let users set their own user name and user display name; however this can technically happen - /// _after_ successfully completing the registration ceremony since this information is not used during - /// [`RegistrationServerState::verify`]. For example the client can send such information along with - /// [`Registration`]. - /// - /// # Examples - /// - /// ``` - /// # use webauthn_rp::request::register::{PublicKeyCredentialUserEntity, UserHandle64}; - /// let user_handle = UserHandle64::new(); - /// let entity = PublicKeyCredentialUserEntity::from(&user_handle); - /// assert_eq!("blank", entity.name.as_ref()); - /// assert_eq!(user_handle, *entity.id); - /// assert!(entity.display_name.is_none()); - /// # Ok::<_, webauthn_rp::AggErr>(()) - /// ``` - #[inline] - fn from(value: &'a UserHandle<LEN>) -> Self { - Self { - name: Username::blank(), - id: value, - display_name: None, - } - } + pub display_name: DisplayName<'display_name>, } /// `PublicKeyCredentialUserEntity` based on a [`UserHandle64`]. pub type PublicKeyCredentialUserEntity64<'name, 'display_name, 'id> = @@ -1289,7 +1408,7 @@ impl AuthenticatorAttachmentReq { } } /// [`AuthenticatorSelectionCriteria`](https://www.w3.org/TR/webauthn-3/#dictionary-authenticatorSelection). -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct AuthenticatorSelectionCriteria { /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment). pub authenticator_attachment: AuthenticatorAttachmentReq, @@ -1391,14 +1510,6 @@ impl AuthenticatorSelectionCriteria { .validate(require_auth_attachment, auth_attachment) } } -#[cfg(test)] -impl PartialEq for AuthenticatorSelectionCriteria { - fn eq(&self, other: &Self) -> bool { - self.authenticator_attachment == other.authenticator_attachment - && self.resident_key == other.resident_key - && self.user_verification == other.user_verification - } -} /// Helper that verifies the overlap of [`CredentialCreationOptions::start_ceremony`] and /// [`RegistrationServerState::decode`]. const fn validate_options_helper( @@ -1493,35 +1604,6 @@ impl< ), } } - /// Convenience function for [`Self::passkey`] passing an empty `Vec`. - /// - /// This MUST only be used when this is the first credential for a user. - #[expect(single_use_lifetimes, reason = "false positive")] - #[inline] - #[must_use] - pub fn first_passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( - rp_id: &'a RpId, - user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, - ) -> Self { - Self::passkey(rp_id, user, Vec::new()) - } - /// Convenience function for [`Self::first_passkey`] passing [`PublicKeyCredentialUserEntity::from`] applied - /// to `user_id` for `user`. - /// - /// This MUST only be used when user information is provided _after_ registration (e.g., when the client - /// sends user name and user display name along with [`Registration`]). - /// - /// Because user information is likely known for existing accounts, this will often only be called during - /// greenfield deployments. - #[expect(single_use_lifetimes, reason = "false positive")] - #[inline] - #[must_use] - pub fn first_passkey_with_blank_user_info<'a: 'rp_id, 'b: 'user_id>( - rp_id: &'a RpId, - user_id: &'b UserHandle<USER_LEN>, - ) -> Self { - Self::first_passkey(rp_id, user_id.into()) - } /// Sets [`Self::mediation`] to [`CredentialMediationRequirement::default`] and /// [`Self::public_key`] to [`PublicKeyCredentialCreationOptions::second_factor`]. #[expect(single_use_lifetimes, reason = "false positive")] @@ -1542,18 +1624,6 @@ impl< ); opts } - /// Convenience function for [`Self::second_factor`] passing an empty `Vec`. - /// - /// This MUST only be used when this is the first credential for a user. - #[expect(single_use_lifetimes, reason = "false positive")] - #[inline] - #[must_use] - pub fn first_second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( - rp_id: &'a RpId, - user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, - ) -> Self { - Self::second_factor(rp_id, user, Vec::new()) - } /// Begins the [registration ceremony](https://www.w3.org/TR/webauthn-3/#registration-ceremony) consuming /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `RegistrationClientState` MUST be /// sent ASAP. In order to complete registration, the returned `RegistrationServerState` MUST be saved so that @@ -1581,7 +1651,7 @@ impl< /// PublicKeyCredentialUserEntity { /// name: "bernard.riemann".try_into()?, /// id: &UserHandle64::new(), - /// display_name: Some("Georg Friedrich Bernhard Riemann".try_into()?) + /// display_name: "Georg Friedrich Bernhard Riemann".try_into()?, /// }, /// Vec::new() /// ).start_ceremony()?.0.expiration() > Instant::now() @@ -1746,7 +1816,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> /// PublicKeyCredentialUserEntity { /// name: "archimedes.of.syracuse".try_into()?, /// id: &UserHandle64::new(), - /// display_name: Some("Αρχιμήδης ο Συρακούσιος".try_into()?), + /// display_name: "Αρχιμήδης ο Συρακούσιος".try_into()?, /// }, /// Vec::new() /// ) @@ -1781,35 +1851,6 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> }, } } - /// Convenience function for [`Self::passkey`] passing an empty `Vec`. - /// - /// This MUST only be used when this is the first credential for a user. - #[expect(single_use_lifetimes, reason = "false positive")] - #[inline] - #[must_use] - pub fn first_passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( - rp_id: &'a RpId, - user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, - ) -> Self { - Self::passkey(rp_id, user, Vec::new()) - } - /// Convenience function for [`Self::first_passkey`] passing [`PublicKeyCredentialUserEntity::from`] applied - /// to `user_id` for `user`. - /// - /// This MUST only be used when user information is provided _after_ registration (e.g., when the client - /// sends user name and user display name along with [`Registration`]). - /// - /// Because user information is likely known for existing accounts, this will often only be called during - /// greenfield deployments. - #[expect(single_use_lifetimes, reason = "false positive")] - #[inline] - #[must_use] - pub fn first_passkey_with_blank_user_info<'a: 'rp_id, 'b: 'user_id>( - rp_id: &'a RpId, - user_id: &'b UserHandle<USER_LEN>, - ) -> Self { - Self::first_passkey(rp_id, user_id.into()) - } /// Deployments that want to incorporate a "something a user has" factor into a larger multi-factor /// authentication (MFA) setup. Specifically deployments that are _not_ userless or passwordless. It /// is important `exclude_credentials` contains the information for _all_ [`RegisteredCredential`]s registered @@ -1845,7 +1886,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> /// PublicKeyCredentialUserEntity { /// name: "carl.gauss".try_into()?, /// id: &UserHandle64::new(), - /// display_name: Some("Johann Carl Friedrich Gauß".try_into()?), + /// display_name: "Johann Carl Friedrich Gauß".try_into()?, /// }, /// Vec::new() /// ) @@ -1872,18 +1913,6 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> ); opts } - /// Convenience function for [`Self::second_factor`] passing an empty `Vec`. - /// - /// This MUST only be used when this is the first credential for a user. - #[expect(single_use_lifetimes, reason = "false positive")] - #[inline] - #[must_use] - pub fn first_second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>( - rp_id: &'a RpId, - user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>, - ) -> Self { - Self::second_factor(rp_id, user, Vec::new()) - } } /// `PublicKeyCredentialCreationOptions` based on a [`UserHandle64`]. pub type PublicKeyCredentialCreationOptions64< @@ -1975,7 +2004,7 @@ impl< /// PublicKeyCredentialUserEntity { /// name: "david.hilbert".try_into()?, /// id: &UserHandle64::new(), - /// display_name: Some("David Hilbert".try_into()?) + /// display_name: "David Hilbert".try_into()?, /// }, /// Vec::new() /// ) @@ -2062,7 +2091,6 @@ pub struct RegistrationVerificationOptions<'origins, 'top_origins, O, T> { /// [`AuthenticatorAttachment`] must be sent iff `true`. pub require_authenticator_attachment: bool, /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] pub client_data_json_relaxed: bool, } @@ -2643,8 +2671,8 @@ mod tests { ))] use super::{ super::{super::AggErr, ExtensionInfo}, - Challenge, CredProtect, CredentialCreationOptions, FourToSixtyThree, PrfInput, RpId, - UserHandle, + Challenge, CredProtect, CredentialCreationOptions, DisplayName, FourToSixtyThree, PrfInput, + PublicKeyCredentialUserEntity, RpId, UserHandle, }; #[cfg(all(feature = "custom", feature = "serializable_server_state"))] use super::{ @@ -2652,7 +2680,7 @@ mod tests { super::bin::{Decode as _, Encode as _}, AsciiDomain, }, - Extension, PublicKeyCredentialUserEntity, RegistrationServerState, + Extension, RegistrationServerState, }; #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] use super::{ @@ -2668,7 +2696,7 @@ mod tests { AuthTransports, }, AuthenticatorAttachment, BackupReq, ExtensionErr, ExtensionReq, RegCeremonyErr, - Registration, RegistrationVerificationOptions, UserVerificationRequirement, + Registration, RegistrationVerificationOptions, UserVerificationRequirement, Username, }; #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] use rsa::sha2::{Digest as _, Sha256}; @@ -2688,6 +2716,7 @@ mod tests { 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> { @@ -2698,7 +2727,7 @@ mod tests { PublicKeyCredentialUserEntity { name: "foo".try_into()?, id: &id, - display_name: None, + display_name: DisplayName::Blank, }, Vec::new(), ); @@ -2735,6 +2764,7 @@ mod tests { 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"))))] @@ -2766,6 +2796,17 @@ mod tests { 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); @@ -2801,6 +2842,7 @@ mod tests { 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) @@ -2973,28 +3015,30 @@ mod tests { ] .as_slice(), ); - attestation_object[30..62] - .copy_from_slice(Sha256::digest("example.com".as_bytes()).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) + 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)), + | (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)), + | (1 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two))), ); } } else { attestation_object.extend_from_slice( [ - 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()), + // 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', @@ -3008,9 +3052,10 @@ mod tests { 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( @@ -3093,6 +3138,7 @@ mod tests { } attestation_object } + #[expect(clippy::unwrap_in_result, clippy::unwrap_used, reason = "OK in tests")] #[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()?); @@ -3124,7 +3170,15 @@ mod tests { client_data_json_relaxed: false, }; let user = UserHandle::from([0; 1]); - let mut opts = CredentialCreationOptions::first_passkey_with_blank_user_info(&rp_id, &user); + let mut opts = CredentialCreationOptions::passkey( + &rp_id, + PublicKeyCredentialUserEntity { + id: &user, + name: Username::try_from("blank").unwrap(), + display_name: DisplayName::Blank, + }, + Vec::new(), + ); opts.public_key.challenge = Challenge(0); opts.public_key.authenticator_selection.user_verification = UserVerificationRequirement::Preferred; @@ -3164,10 +3218,11 @@ mod tests { /// 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] + #[ignore = "slow"] #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn test_uv_required_err() { + fn uv_required_err() { const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ CredentialProtectionPolicy::None, CredentialProtectionPolicy::UserVerificationOptional, @@ -3229,6 +3284,7 @@ mod tests { 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] = [ @@ -3263,7 +3319,7 @@ mod tests { min_pin, cred_props, }, - }).map_or_else(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::UserNotVerified)), |_| false)); + }).is_err_and(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::UserNotVerified)))); } } } @@ -3286,10 +3342,11 @@ mod tests { /// = /// 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] + #[ignore = "slow"] #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn test_forbidden_cred_props() { + fn forbidden_cred_props() { const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [ CredentialProtectionPolicy::None, CredentialProtectionPolicy::UserVerificationOptional, @@ -3350,6 +3407,7 @@ mod tests { 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] = [ @@ -3383,13 +3441,13 @@ mod tests { }, response: TestResponseOptions { user_verified, - hmac, cred_protect, prf, + hmac, min_pin, cred_props, }, - }).map_or_else(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenCredProps))), |_| false)); + }).is_err_and(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::ForbiddenCredProps))))); } } } @@ -3401,9 +3459,10 @@ mod tests { } } } + #[expect(clippy::panic_in_result_fn, reason = "OK in tests")] #[test] #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))] - fn test_prf() -> Result<(), AggErr> { + fn prf() -> Result<(), AggErr> { let mut opts = TestOptions { request: TestRequestOptions { error_unsolicited: false, @@ -3423,33 +3482,34 @@ mod tests { }; validate(opts)?; opts.response.prf = Some(false); - assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidPrfValue))), |_| 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).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue))), |_| 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::InvalidHmacSecretValue))))); opts.request.prf_uv = PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue); opts.response.hmac = HmacSecret::Enabled; opts.response.prf = None; - assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))), |_| false)); + 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).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))), |_| false)); + 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).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::PrfWithoutHmacSecret))), |_| false)); + 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).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutUserVerified))), |_| false)); + 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 test_cred_protect() -> Result<(), AggErr> { + fn cred_protect() -> Result<(), AggErr> { let mut opts = TestOptions { request: TestRequestOptions { error_unsolicited: false, @@ -3473,12 +3533,12 @@ mod tests { validate(opts)?; opts.response.cred_protect = CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; - assert!(validate(opts).map_or_else(|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)))), |_| 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::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).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::CredProtectUserVerificationRequiredWithoutUserVerified))), |_| false)); + 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/bin.rs b/src/request/register/bin.rs @@ -1,7 +1,7 @@ extern crate alloc; use super::{ super::super::bin::{Decode, Encode}, - Nickname, NicknameErr, UserHandle, Username, UsernameErr, + DisplayName, Nickname, NicknameErr, UserHandle, Username, UsernameErr, }; use alloc::borrow::Cow; use core::{ @@ -31,7 +31,7 @@ where Ok(Self(input)) } } -impl Encode for Nickname<'_> { +impl Encode for DisplayName<'_> { type Output<'a> = &'a str where @@ -42,41 +42,44 @@ impl Encode for Nickname<'_> { Ok(self.as_ref()) } } -/// Error returned from [`Nickname::decode`]. +/// Error returned from [`DisplayName::decode`]. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum DecodeNicknameErr { +pub enum DecodeDisplayNameErr { /// Variant returned when the encoded data could not be decoded - /// into a [`Nickname`]. + /// into a [`DisplayName`]. Nickname(NicknameErr), - /// Variant returned when the [`Nickname`] was not encoded + /// Variant returned when the [`DisplayName`] was not encoded /// into its canonical form. NotCanonical, } -impl Display for DecodeNicknameErr { +impl Display for DecodeDisplayNameErr { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { Self::Nickname(e) => e.fmt(f), - Self::NotCanonical => f.write_str("Nickname was not encoded in its canonical form"), + Self::NotCanonical => f.write_str("DisplayName was not encoded in its canonical form"), } } } -impl Error for DecodeNicknameErr {} -impl<'b> Decode for Nickname<'b> { +impl Error for DecodeDisplayNameErr {} +impl<'b> Decode for DisplayName<'b> { type Input<'a> = &'b str; - type Err = DecodeNicknameErr; + type Err = DecodeDisplayNameErr; #[inline] fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> { - match Nickname::try_from(input).map_err(DecodeNicknameErr::Nickname) { - Ok(v) => match v.0 { - Cow::Borrowed(name) => { - if name == input { - Ok(Self(Cow::Borrowed(input))) - } else { - Err(DecodeNicknameErr::NotCanonical) + match DisplayName::try_from(input).map_err(DecodeDisplayNameErr::Nickname) { + Ok(v) => match v { + DisplayName::Blank => Ok(Self::Blank), + DisplayName::Nickname(name) => match name.0 { + Cow::Borrowed(val) => { + if val == input { + Ok(Self::Nickname(Nickname(Cow::Borrowed(input)))) + } else { + Err(DecodeDisplayNameErr::NotCanonical) + } } - } - Cow::Owned(_) => Err(DecodeNicknameErr::NotCanonical), + Cow::Owned(_) => Err(DecodeDisplayNameErr::NotCanonical), + }, }, Err(e) => Err(e), } diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs @@ -3,12 +3,12 @@ use super::{ super::{ super::response::ser::{Null, Type}, auth::PrfInputOwned, - ser::{DEFAULT_RP_ID, PrfHelper}, + ser::PrfHelper, }, AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, Challenge, CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, CredentialCreationOptions, - CredentialMediationRequirement, CrossPlatformHint, Extension, ExtensionInfo, ExtensionReq, - FIVE_MINUTES, FourToSixtyThree, Hint, Nickname, PlatformHint, PrfInput, + CredentialMediationRequirement, CrossPlatformHint, DisplayName, Extension, ExtensionInfo, + ExtensionReq, FIVE_MINUTES, FourToSixtyThree, Hint, Nickname, PlatformHint, PrfInput, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, RegistrationClientState, ResidentKeyRequirement, RpId, UserHandle, UserVerificationRequirement, Username, @@ -20,7 +20,8 @@ use alloc::borrow::Cow; use core::str::FromStr; use core::{ convert, - fmt::{self, Formatter}, + error::Error as E, + fmt::{self, Display, Formatter}, marker::PhantomData, num::NonZeroU32, str, @@ -50,6 +51,27 @@ impl Serialize for Nickname<'_> { serializer.serialize_str(self.0.as_ref()) } } +impl Serialize for DisplayName<'_> { + /// Serializes `self` as a [`prim@str`]. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::register::DisplayName; + /// assert_eq!( + /// serde_json::to_string(&DisplayName::try_from("Terence Tao")?).unwrap(), + /// r#""Terence Tao""# + /// ); + /// # Ok::<_, webauthn_rp::AggErr>(()) + /// ``` + #[inline] + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(self.as_ref()) + } +} impl Serialize for Username<'_> { /// Serializes `self` as a [`prim@str`]. /// @@ -264,7 +286,7 @@ where /// # Examples /// /// ``` - /// # use webauthn_rp::request::register::{PublicKeyCredentialUserEntity, UserHandle}; + /// # use webauthn_rp::request::register::{DisplayName, PublicKeyCredentialUserEntity, UserHandle}; /// # #[cfg(feature = "custom")] /// // We create this manually purely for example. One should almost always /// // randomly generate this (e.g., `UserHandle::new`). @@ -274,7 +296,7 @@ where /// serde_json::to_string(&PublicKeyCredentialUserEntity { /// name: "georg.cantor".try_into()?, /// id: &id, - /// display_name: Some("Гео́рг Ка́нтор".try_into()?), + /// display_name: "Гео́рг Ка́нтор".try_into()?, /// }).unwrap(), /// r#"{"name":"georg.cantor","id":"AA","displayName":"Гео́рг Ка́нтор"}"# /// ); @@ -285,7 +307,7 @@ where /// serde_json::to_string(&PublicKeyCredentialUserEntity { /// name: "georg.cantor".try_into()?, /// id: &id, - /// display_name: None, + /// display_name: DisplayName::Blank, /// }).unwrap(), /// r#"{"name":"georg.cantor","id":"AA","displayName":""}"# /// ); @@ -301,11 +323,8 @@ where .and_then(|mut ser| { ser.serialize_field(NAME, &self.name).and_then(|()| { ser.serialize_field(ID, &self.id).and_then(|()| { - ser.serialize_field( - DISPLAY_NAME, - self.display_name.as_ref().map_or("", |val| val.as_ref()), - ) - .and_then(|()| ser.end()) + ser.serialize_field(DISPLAY_NAME, &self.display_name) + .and_then(|()| ser.end()) }) }) }) @@ -840,7 +859,7 @@ where /// creds.push(PublicKeyCredentialDescriptor { id, transports }); /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); /// let user_handle = UserHandle64::new(); - /// let mut options = CredentialCreationOptions::passkey(&rp_id, PublicKeyCredentialUserEntity { name: "pierre.de.fermat".try_into()?, id: &user_handle, display_name: Some("Pierre de Fermat".try_into()?) }, creds); + /// let mut options = CredentialCreationOptions::passkey(&rp_id, PublicKeyCredentialUserEntity { name: "pierre.de.fermat".try_into()?, id: &user_handle, display_name: "Pierre de Fermat".try_into()?, }, creds); /// options.public_key.authenticator_selection.authenticator_attachment = AuthenticatorAttachmentReq::None(Hint::SecurityKey); /// options.public_key.extensions.min_pin_length = Some((FourToSixtyThree::Sixteen, ExtensionInfo::RequireEnforceValue)); /// # #[cfg(all(feature = "bin", feature = "custom"))] @@ -965,6 +984,53 @@ impl<'de: 'a, 'a> Deserialize<'de> for Nickname<'a> { deserializer.deserialize_str(NicknameVisitor(PhantomData)) } } +impl<'de: 'a, 'a> Deserialize<'de> for DisplayName<'a> { + /// Deserializes [`prim@str`] and parses it according to [`Self::try_from`]. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::register::DisplayName; + /// assert_eq!( + /// serde_json::from_str::<DisplayName>(r#""Alexander Grothendieck""#)?.as_ref(), + /// "Alexander Grothendieck" + /// ); + /// # Ok::<_, serde_json::Error>(()) + ///``` + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `DisplayName`. + struct DisplayNameVisitor<'b>(PhantomData<fn() -> &'b ()>); + impl<'d: 'b, 'b> Visitor<'d> for DisplayNameVisitor<'b> { + type Value = DisplayName<'b>; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("DisplayName") + } + fn visit_borrowed_str<E>(self, v: &'d str) -> Result<Self::Value, E> + where + E: Error, + { + DisplayName::try_from(v).map_err(E::custom) + } + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + if v.is_empty() { + Ok(DisplayName::Blank) + } else { + Nickname::try_from(v).map_err(E::custom).map(|name| { + DisplayName::Nickname(Nickname(Cow::Owned(name.0.into_owned()))) + }) + } + } + } + deserializer.deserialize_str(DisplayNameVisitor(PhantomData)) + } +} impl<'de: 'a, 'a> Deserialize<'de> for Username<'a> { /// Deserializes [`prim@str`] and parses it according to [`Self::try_from`]. /// @@ -1116,7 +1182,7 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifier { /// Helper to deserialize `PublicKeyCredentialRpEntity` with an optional `RpId`. /// /// Used in [`ClientCredentialCreationOptions::deserialize`]. -struct PublicKeyCredentialRpEntityHelper(RpId); +struct PublicKeyCredentialRpEntityHelper(Option<RpId>); impl<'de> Deserialize<'de> for PublicKeyCredentialRpEntityHelper { /// Conforms to the following schema: /// @@ -1127,7 +1193,8 @@ impl<'de> Deserialize<'de> for PublicKeyCredentialRpEntityHelper { /// } /// ``` /// - /// None of the fields are required. + /// None of the fields are required, and missing fields are interpreted the same as fields + /// with `null` values. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, @@ -1226,9 +1293,7 @@ impl<'de> Deserialize<'de> for PublicKeyCredentialRpEntityHelper { } } } - Ok(PublicKeyCredentialRpEntityHelper( - id.flatten().unwrap_or(DEFAULT_RP_ID), - )) + Ok(PublicKeyCredentialRpEntityHelper(id.flatten())) } } /// Fields for `PublicKeyCredentialRpEntityHelper`. @@ -1240,48 +1305,131 @@ impl<'de> Deserialize<'de> for PublicKeyCredentialRpEntityHelper { ) } } -/// Similar to [`PublicKeyCredentialUserEntity`] except the [`UserHandle`] is owned. +/// Similar to [`PublicKeyCredentialUserEntity`] except the [`UserHandle`] is owned, and all fields are +/// optional. /// /// This is primarily useful to assist [`ClientCredentialCreationOptions::deserialize`]. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct PublicKeyCredentialUserEntityOwned<'name, 'display_name, const LEN: usize> { /// See [`PublicKeyCredentialUserEntity::name`]. - pub name: Username<'name>, + pub name: Option<Username<'name>>, /// See [`PublicKeyCredentialUserEntity::id`]. - pub id: UserHandle<LEN>, + pub id: Option<UserHandle<LEN>>, /// See [`PublicKeyCredentialUserEntity::display_name`]. - pub display_name: Option<Nickname<'display_name>>, + pub display_name: Option<DisplayName<'display_name>>, } -impl<'a: 'name + 'display_name + 'id, 'name, 'display_name, 'id, const LEN: usize> - From<&'a PublicKeyCredentialUserEntityOwned<'_, '_, LEN>> - for PublicKeyCredentialUserEntity<'name, 'display_name, 'id, LEN> -{ +/// Error returned when converting a [`PublicKeyCredentialUserEntityOwned`] into a +/// [`PublicKeyCredentialUserEntity`] (e.g., via [`PublicKeyCredentialUserEntityOwned::with_id`]). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PublicKeyCredentialUserEntityOwnedErr { + /// Variant returned when [`PublicKeyCredentialUserEntityOwned::name`] is `None`. + MissingName, + /// Variant returned when [`PublicKeyCredentialUserEntityOwned::id`] is `None`. + MissingId, + /// Variant returned when [`PublicKeyCredentialUserEntityOwned::display_name`] is `None`. + MissingDisplayName, +} +impl Display for PublicKeyCredentialUserEntityOwnedErr { #[inline] - fn from(value: &'a PublicKeyCredentialUserEntityOwned<'_, '_, LEN>) -> Self { - Self { - name: (&value.name).into(), - id: &value.id, - display_name: value.display_name.as_ref().map(Into::into), - } + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::MissingName => "user entity info did not have a username", + Self::MissingId => "user entity info did not have a user handle", + Self::MissingDisplayName => "user entity info did not have a user display name", + }) } } -impl<const LEN: usize> Default for PublicKeyCredentialUserEntityOwned<'_, '_, LEN> -where - UserHandle<LEN>: Default, -{ +impl E for PublicKeyCredentialUserEntityOwnedErr {} +impl<const LEN: usize> PublicKeyCredentialUserEntityOwned<'_, '_, LEN> { + /// Returns a `PublicKeyCredentialUserEntity` based on `self`. + /// + /// # Errors + /// + /// Errors iff any of the fields in `self` are `None`. #[inline] - fn default() -> Self { - Self { - name: Username::blank(), - id: UserHandle::default(), - display_name: None, - } + pub fn as_entity( + &self, + ) -> Result<PublicKeyCredentialUserEntity<'_, '_, '_, LEN>, PublicKeyCredentialUserEntityOwnedErr> + { + self.name + .as_ref() + .ok_or(PublicKeyCredentialUserEntityOwnedErr::MissingName) + .and_then(|username| { + self.id + .as_ref() + .ok_or(PublicKeyCredentialUserEntityOwnedErr::MissingId) + .and_then(|id| { + self.display_name + .as_ref() + .ok_or(PublicKeyCredentialUserEntityOwnedErr::MissingDisplayName) + .map(|display| PublicKeyCredentialUserEntity { + name: username.into(), + id, + display_name: display.into(), + }) + }) + }) + } + /// Returns a `PublicKeyCredentialUserEntity` based on `self` and `id`. + /// + /// Note `id` is used _unconditionally_ regardless if [`Self::id`] is `Some`. + /// + /// # Errors + /// + /// Errors iff [`Self::name`] or [`Self::display_name`] are `None`. + #[inline] + pub fn with_id<'id>( + &self, + id: &'id UserHandle<LEN>, + ) -> Result< + PublicKeyCredentialUserEntity<'_, '_, 'id, LEN>, + PublicKeyCredentialUserEntityOwnedErr, + > { + self.name + .as_ref() + .ok_or(PublicKeyCredentialUserEntityOwnedErr::MissingName) + .and_then(|username| { + self.display_name + .as_ref() + .ok_or(PublicKeyCredentialUserEntityOwnedErr::MissingDisplayName) + .map(|display| PublicKeyCredentialUserEntity { + name: username.into(), + id, + display_name: display.into(), + }) + }) + } + /// Returns a `PublicKeyCredentialUserEntity` based on `self`, `name`, and `display_name`. + /// + /// Note `name` and `display_name` are used _unconditionally_ regardless if [`Self::name`] or + /// [`Self::display_name`] are `Some`. + /// + /// # Errors + /// + /// Errors iff [`Self::id`] is `None`. + #[inline] + pub fn with_name_and_display_name<'name, 'display_name>( + &self, + name: Username<'name>, + display_name: DisplayName<'display_name>, + ) -> Result< + PublicKeyCredentialUserEntity<'name, 'display_name, '_, LEN>, + PublicKeyCredentialUserEntityOwnedErr, + > { + self.id + .as_ref() + .ok_or(PublicKeyCredentialUserEntityOwnedErr::MissingId) + .map(|id| PublicKeyCredentialUserEntity { + name, + id, + display_name, + }) } } impl<'de: 'name + 'display_name, 'name, 'display_name, const LEN: usize> Deserialize<'de> for PublicKeyCredentialUserEntityOwned<'name, 'display_name, LEN> where - UserHandle<LEN>: Default, + UserHandle<LEN>: Deserialize<'de>, { /// Deserializes a `struct` according to /// [`PublicKeyCredentialUserEntityJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentityjson). @@ -1292,26 +1440,21 @@ where /// [`name`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentityjson-name) is deserialized /// according to [`Username::deserialize`], and /// [`displayName`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentityjson-displayname) is - /// deserialized according to [`Nickname::deserialize`] where `""` is deserialized to `None` (since - /// blank strings are not valid `Nickname`s). + /// deserialized according to [`DisplayName::deserialize`]. /// - /// In the event `id` does not exist, a randomly generated `UserHandle` will be used. In the event `name` - /// does not exist, `"blank"` will be used. In the event `displayName` does not exist, `None` will - /// be used. - /// - /// Unknown or duplicate fields lead to an error. + /// Unknown or duplicate fields lead to an error. Missing fields are interpreted the same as if the field + /// were assigned `null`. /// /// # Examples /// /// ``` /// # use webauthn_rp::request::register::ser::PublicKeyCredentialUserEntityOwned; /// let val = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 16>>(r#"{"name":"paul.erdos","displayName":"Erdős Pál"}"#)?; - /// assert_eq!(val.name.as_ref(), "paul.erdos"); - /// assert_eq!(val.display_name.as_ref().map(|v| v.as_ref()), Some("Erdős Pál")); - /// assert_ne!(val.id.as_slice(), [0; 16]); + /// assert!(val.name.is_some_and(|name| name.as_ref() == "paul.erdos")); + /// assert!(val.display_name.is_some_and(|display| display.as_ref() == "Erdős Pál")); + /// assert!(val.id.is_none()); /// # Ok::<_, serde_json::Error>(()) /// ``` - #[expect(clippy::too_many_lines, reason = "122 is fine")] #[inline] fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where @@ -1324,13 +1467,12 @@ where impl<'d: 'a + 'b, 'a, 'b, const L: usize> Visitor<'d> for PublicKeyCredentialUserEntityOwnedVisitor<'a, 'b, L> where - UserHandle<L>: Default, + UserHandle<L>: Deserialize<'d>, { type Value = PublicKeyCredentialUserEntityOwned<'a, 'b, L>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str("PublicKeyCredentialUserEntityOwned") } - #[expect(clippy::too_many_lines, reason = "102 is fine")] fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> where A: MapAccess<'d>, @@ -1371,48 +1513,6 @@ where deserializer.deserialize_identifier(FieldVisitor) } } - /// Helper to deserialize `displayName`. - struct DisplayName<'e>(Option<Nickname<'e>>); - impl<'e: 'f, 'f> Deserialize<'e> for DisplayName<'f> { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'e>, - { - /// `Visitor` for `DisplayName`. - struct DisplayNameVisitor<'g>(PhantomData<fn() -> &'g ()>); - impl<'g: 'h, 'h> Visitor<'g> for DisplayNameVisitor<'h> { - type Value = DisplayName<'h>; - fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - formatter.write_str("User display name") - } - fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> - where - E: Error, - { - if v.is_empty() { - Ok(DisplayName(None)) - } else { - Nickname::try_from(v).map_err(E::custom).map(|name| { - DisplayName(Some(Nickname(Cow::Owned(name.0.into_owned())))) - }) - } - } - fn visit_borrowed_str<E>(self, v: &'g str) -> Result<Self::Value, E> - where - E: Error, - { - if v.is_empty() { - Ok(DisplayName(None)) - } else { - Nickname::try_from(v) - .map_err(E::custom) - .map(|n| DisplayName(Some(n))) - } - } - } - deserializer.deserialize_str(DisplayNameVisitor(PhantomData)) - } - } let mut user_handle = None; let mut username = None; let mut display = None; @@ -1434,15 +1534,13 @@ where if display.is_some() { return Err(Error::duplicate_field(DISPLAY_NAME)); } - display = map - .next_value::<Option<DisplayName<'_>>>() - .map(|n| n.map_or_else(|| Some(None), |disp| Some(disp.0)))?; + display = map.next_value::<Option<_>>().map(Some)?; } } } Ok(PublicKeyCredentialUserEntityOwned { - id: user_handle.flatten().unwrap_or_default(), - name: username.flatten().unwrap_or_else(Username::blank), + id: user_handle.flatten(), + name: username.flatten(), display_name: display.flatten(), }) } @@ -1816,18 +1914,8 @@ impl<'de> Deserialize<'de> for AuthenticatorSelectionCriteria { /// [`requireResidentKey`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-requireresidentkey) /// must be consistent (i.e., `requireResidentKey` iff `residentKey` is [`ResidentKeyRequirement::Required`]). /// - /// `residentKey` defaults to [`ResidentKeyRequirement::Discouraged`] when it is `null` or does not exist - /// unless `requireResidentKey` is `true` in which case it is `ResidentKeyRequirement::Required`. - /// - /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification) - /// is [`UserVerificationRequirement::Preferred`] if it does not exist or is `null`. - /// - /// If - /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment) - /// does not exist or is `null`, then [`AuthenticatorAttachmentReq::None`] will be used containing - /// [`Hint::None`]. - /// - /// Unknown or duplicate fields lead to an error. + /// Missing and `null` fields default to the corresponding [`Default`] value. Unknown and duplicate fields + /// lead to an error. /// /// # Examples /// @@ -2058,6 +2146,36 @@ impl<'de> Deserialize<'de> for AttestationFormats { deserializer.deserialize_seq(AttestationFormatsVisitor) } } +impl<'de> Deserialize<'de> for FourToSixtyThree { + /// Deserializes a `u8` based on [`Self::from_u8`]. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::register::FourToSixtyThree; + /// # use serde_json::Error; + /// assert_eq!(serde_json::from_str::<FourToSixtyThree>("4")?, FourToSixtyThree::Four); + /// assert_eq!(serde_json::from_str::<FourToSixtyThree>("63")?, FourToSixtyThree::SixtyThree); + /// assert!(serde_json::from_str::<FourToSixtyThree>("0").is_err()); + /// assert!(serde_json::from_str::<FourToSixtyThree>("3").is_err()); + /// assert!(serde_json::from_str::<FourToSixtyThree>("64").is_err()); + /// # Ok::<_, Error>(()) + /// ``` + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + u8::deserialize(deserializer).and_then(|val| { + Self::from_u8(val).ok_or_else(|| { + Error::invalid_value( + Unexpected::Unsigned(u64::from(val)), + &"integer inclusively between 4 and 63", + ) + }) + }) + } +} /// Similar to [`Extension`] except [`PrfInputOwned`] is used. /// /// This is primarily useful to assist [`ClientCredentialCreationOptions::deserialize`]. @@ -2072,26 +2190,48 @@ pub struct ExtensionOwned { /// See [`Extension::prf`]. pub prf: Option<PrfInputOwned>, } -impl<'a: 'prf_first + 'prf_second, 'prf_first, 'prf_second> From<&'a ExtensionOwned> - for Extension<'prf_first, 'prf_second> -{ +impl ExtensionOwned { + /// Returns an `Extension` based on `self`. + /// + /// Note [`PrfInputOwned::ext_req`] is converted into an [`ExtensionInfo`] such that the value is enforced. #[inline] - fn from(value: &'a ExtensionOwned) -> Self { - Self { - cred_props: value.cred_props, - cred_protect: value.cred_protect, - min_pin_length: value.min_pin_length, - prf: value.prf.as_ref().map(|input| { + #[must_use] + pub fn as_extension(&self) -> Extension<'_, '_> { + Extension { + cred_props: self.cred_props, + cred_protect: self.cred_protect, + min_pin_length: self.min_pin_length, + prf: self.prf.as_ref().map(|prf| { ( PrfInput { - first: input.first.as_slice(), - second: input.second.as_deref(), + first: &prf.first, + second: prf.second.as_deref(), + }, + if matches!(prf.ext_req, ExtensionReq::Require) { + ExtensionInfo::RequireEnforceValue + } else { + ExtensionInfo::AllowEnforceValue }, - ExtensionInfo::AllowEnforceValue, ) }), } } + /// Returns an `Extension` based on `self` and `prf`. + /// + /// Note `prf` is used _unconditionally_ regardless if [`Self::prf`] is `Some`. + #[inline] + #[must_use] + pub const fn with_prf<'prf_first, 'prf_second>( + &self, + prf: (PrfInput<'prf_first, 'prf_second>, ExtensionInfo), + ) -> Extension<'prf_first, 'prf_second> { + Extension { + cred_props: self.cred_props, + cred_protect: self.cred_protect, + min_pin_length: self.min_pin_length, + prf: Some(prf), + } + } } impl<'de> Deserialize<'de> for ExtensionOwned { /// Deserializes a `struct` according to the following pseudo-schema: @@ -2338,7 +2478,8 @@ impl<'de> Deserialize<'de> for ExtensionOwned { deserializer.deserialize_struct("ExtensionOwned", FIELDS, ExtensionOwnedVisitor) } } -/// Similar to [`PublicKeyCredentialCreationOptions`] except the fields are based on owned data. +/// Similar to [`PublicKeyCredentialCreationOptions`] except the fields are based on owned data, and +/// [`Self::rp_id`] is optional. /// /// This is primarily useful to assist [`ClientCredentialCreationOptions::deserialize`]. #[derive(Debug)] @@ -2348,7 +2489,7 @@ pub struct PublicKeyCredentialCreationOptionsOwned< const USER_LEN: usize, > { /// See [`PublicKeyCredentialCreationOptions::rp_id`]. - pub rp_id: RpId, + pub rp_id: Option<RpId>, /// See [`PublicKeyCredentialCreationOptions::user`]. pub user: PublicKeyCredentialUserEntityOwned<'user_name, 'user_display_name, USER_LEN>, /// See [`PublicKeyCredentialCreationOptions::pub_key_cred_params`]. @@ -2360,41 +2501,330 @@ pub struct PublicKeyCredentialCreationOptionsOwned< /// See [`PublicKeyCredentialCreationOptions::extensions`]. pub extensions: ExtensionOwned, } +/// Error returned when converting a [`PublicKeyCredentialCreationOptionsOwned`] into a +/// [`PublicKeyCredentialCreationOptions`] (e.g., via [`PublicKeyCredentialCreationOptionsOwned::with_rp_id`]). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PublicKeyCredentialCreationOptionsOwnedErr { + /// Variant returned when [`PublicKeyCredentialCreationOptionsOwned::rp_id`] is `None`. + MissingRpId, + /// Variant returned when [`PublicKeyCredentialCreationOptionsOwned::user`] cannot be converted into a + /// a [`PublicKeyCredentialCreationOptions`]. + UserEntity(PublicKeyCredentialUserEntityOwnedErr), +} +impl Display for PublicKeyCredentialCreationOptionsOwnedErr { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::MissingRpId => f.write_str("creation options did not have an RP ID"), + Self::UserEntity(err) => err.fmt(f), + } + } +} +impl E for PublicKeyCredentialCreationOptionsOwnedErr {} +impl From<PublicKeyCredentialUserEntityOwnedErr> for PublicKeyCredentialCreationOptionsOwnedErr { + #[inline] + fn from(value: PublicKeyCredentialUserEntityOwnedErr) -> Self { + Self::UserEntity(value) + } +} impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER_LEN> { - /// Creates a `PublicKeyCredentialCreationOptions` based on the contained data and randomly-generated - /// [`Challenge`]. + /// Returns a `PublicKeyCredentialCreationOptions` based on `self` and `exclude_credentials`. + /// + /// # Errors + /// + /// Errors iff [`Self::rp_id`] is `None` or [`PublicKeyCredentialUserEntityOwned::as_entity`] errors. + #[inline] + pub fn as_options( + &self, + exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, + ) -> Result< + PublicKeyCredentialCreationOptions<'_, '_, '_, '_, '_, '_, USER_LEN>, + PublicKeyCredentialCreationOptionsOwnedErr, + > { + self.rp_id + .as_ref() + .ok_or(PublicKeyCredentialCreationOptionsOwnedErr::MissingRpId) + .and_then(|rp_id| { + self.user + .as_entity() + .map_err(PublicKeyCredentialCreationOptionsOwnedErr::UserEntity) + .map(|user| PublicKeyCredentialCreationOptions { + rp_id, + user, + challenge: Challenge::new(), + pub_key_cred_params: self.pub_key_cred_params, + timeout: self.timeout, + exclude_credentials, + authenticator_selection: self.authenticator_selection, + extensions: self.extensions.as_extension(), + }) + }) + } + /// Returns a `PublicKeyCredentialCreationOptions` based on `self`, `exclude_credentials`, and `rp_id`. + /// + /// Note `rp_id` is used _unconditionally_ regardless if [`Self::rp_id`] is `Some`. + /// + /// # Errors + /// + /// Errors iff [`PublicKeyCredentialUserEntityOwned::as_entity`] errors. + #[inline] + pub fn with_rp_id<'rp_id>( + &self, + exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, + rp_id: &'rp_id RpId, + ) -> Result< + PublicKeyCredentialCreationOptions<'rp_id, '_, '_, '_, '_, '_, USER_LEN>, + PublicKeyCredentialCreationOptionsOwnedErr, + > { + self.user + .as_entity() + .map_err(PublicKeyCredentialCreationOptionsOwnedErr::UserEntity) + .map(|user| PublicKeyCredentialCreationOptions { + rp_id, + user, + challenge: Challenge::new(), + pub_key_cred_params: self.pub_key_cred_params, + timeout: self.timeout, + exclude_credentials, + authenticator_selection: self.authenticator_selection, + extensions: self.extensions.as_extension(), + }) + } + /// Returns a `PublicKeyCredentialCreationOptions` based on `self`, `exclude_credentials`, and `user`. + /// + /// Note `user` is used _unconditionally_ regardless of what [`Self::user`] is. + /// + /// # Errors + /// + /// Errors iff [`Self::rp_id`] is `None`. + #[inline] + pub fn with_user<'user_name, 'user_display_name, 'user_id>( + &self, + exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, + user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>, + ) -> Result< + PublicKeyCredentialCreationOptions< + '_, + 'user_name, + 'user_display_name, + 'user_id, + '_, + '_, + USER_LEN, + >, + PublicKeyCredentialCreationOptionsOwnedErr, + > { + self.rp_id + .as_ref() + .ok_or(PublicKeyCredentialCreationOptionsOwnedErr::MissingRpId) + .map(|rp_id| PublicKeyCredentialCreationOptions { + rp_id, + user, + challenge: Challenge::new(), + pub_key_cred_params: self.pub_key_cred_params, + timeout: self.timeout, + exclude_credentials, + authenticator_selection: self.authenticator_selection, + extensions: self.extensions.as_extension(), + }) + } + /// Returns a `PublicKeyCredentialCreationOptions` based on `self`, `exclude_credentials`, and `extensions`. + /// + /// Note `extensions` is used _unconditionally_ regardless of what [`Self::extensions`] is. + /// + /// # Errors + /// + /// Errors iff [`Self::rp_id`] is `None` or [`PublicKeyCredentialUserEntityOwned::as_entity`] errors. + #[inline] + pub fn with_extensions<'prf_first, 'prf_second>( + &self, + exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, + extensions: Extension<'prf_first, 'prf_second>, + ) -> Result< + PublicKeyCredentialCreationOptions<'_, '_, '_, '_, 'prf_first, 'prf_second, USER_LEN>, + PublicKeyCredentialCreationOptionsOwnedErr, + > { + self.rp_id + .as_ref() + .ok_or(PublicKeyCredentialCreationOptionsOwnedErr::MissingRpId) + .and_then(|rp_id| { + self.user + .as_entity() + .map_err(PublicKeyCredentialCreationOptionsOwnedErr::UserEntity) + .map(|user| PublicKeyCredentialCreationOptions { + rp_id, + user, + challenge: Challenge::new(), + pub_key_cred_params: self.pub_key_cred_params, + timeout: self.timeout, + exclude_credentials, + authenticator_selection: self.authenticator_selection, + extensions, + }) + }) + } + /// Returns a `PublicKeyCredentialCreationOptions` based on `self`, `exclude_credentials`, `rp_id`, and `user`. + /// + /// Note `rp_id` and `user` are used _unconditionally_ regardless if [`Self::rp_id`] is `Some` or what + /// [`Self::user`] is. + #[inline] + #[must_use] + pub fn with_rp_id_and_user<'rp_id, 'user_name, 'user_display_name, 'user_id>( + &self, + exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, + rp_id: &'rp_id RpId, + user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>, + ) -> PublicKeyCredentialCreationOptions< + 'rp_id, + 'user_name, + 'user_display_name, + 'user_id, + '_, + '_, + USER_LEN, + > { + PublicKeyCredentialCreationOptions { + rp_id, + user, + challenge: Challenge::new(), + pub_key_cred_params: self.pub_key_cred_params, + timeout: self.timeout, + exclude_credentials, + authenticator_selection: self.authenticator_selection, + extensions: self.extensions.as_extension(), + } + } + /// Returns a `PublicKeyCredentialCreationOptions` based on `self`, `exclude_credentials`, `rp_id`, and + /// `extensions`. + /// + /// Note `rp_id` and `extensions` are used _unconditionally_ regardless if [`Self::rp_id`] is `Some` or what + /// [`Self::extensions`] is. + /// + /// # Errors + /// + /// Errors iff [`PublicKeyCredentialUserEntityOwned::as_entity`] errors. + #[inline] + pub fn with_rp_id_and_extensions<'rp_id, 'prf_first, 'prf_second>( + &self, + exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, + rp_id: &'rp_id RpId, + extensions: Extension<'prf_first, 'prf_second>, + ) -> Result< + PublicKeyCredentialCreationOptions<'rp_id, '_, '_, '_, 'prf_first, 'prf_second, USER_LEN>, + PublicKeyCredentialCreationOptionsOwnedErr, + > { + self.user + .as_entity() + .map_err(PublicKeyCredentialCreationOptionsOwnedErr::UserEntity) + .map(|user| PublicKeyCredentialCreationOptions { + rp_id, + user, + challenge: Challenge::new(), + pub_key_cred_params: self.pub_key_cred_params, + timeout: self.timeout, + exclude_credentials, + authenticator_selection: self.authenticator_selection, + extensions, + }) + } + /// Returns a `PublicKeyCredentialCreationOptions` based on `self`, `exclude_credentials`, `user`, and + /// `extensions`. + /// + /// Note `user` and `extensions` are used _unconditionally_ regardless of what the values of [`Self::user`] + /// or [`Self::extensions`] are. + /// + /// # Errors + /// + /// Errors iff [`Self::rp_id`] is `None`. + #[inline] + pub fn with_user_and_extensions< + 'user_name, + 'user_display_name, + 'user_id, + 'prf_first, + 'prf_second, + >( + &self, + exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, + user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>, + extensions: Extension<'prf_first, 'prf_second>, + ) -> Result< + PublicKeyCredentialCreationOptions< + '_, + 'user_name, + 'user_display_name, + 'user_id, + 'prf_first, + 'prf_second, + USER_LEN, + >, + PublicKeyCredentialCreationOptionsOwnedErr, + > { + self.rp_id + .as_ref() + .ok_or(PublicKeyCredentialCreationOptionsOwnedErr::MissingRpId) + .map(|rp_id| PublicKeyCredentialCreationOptions { + rp_id, + user, + challenge: Challenge::new(), + pub_key_cred_params: self.pub_key_cred_params, + timeout: self.timeout, + exclude_credentials, + authenticator_selection: self.authenticator_selection, + extensions, + }) + } + /// Returns a `PublicKeyCredentialCreationOptions` based on `self`, `exclude_credentials`, `rp_id`, `user`, + /// and `extensions`. + /// + /// Note `rp_id`, `user`, and `extensions` are used _unconditionally_ regardless if [`Self::rp_id`] is `Some` + /// or what the values of [`Self::user`] and [`Self::extensions`] are. #[inline] #[must_use] - pub fn into_options( + pub fn with_rp_id_user_and_extensions< + 'rp_id, + 'user_name, + 'user_display_name, + 'user_id, + 'prf_first, + 'prf_second, + >( &self, exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, - ) -> PublicKeyCredentialCreationOptions<'_, '_, '_, '_, '_, '_, USER_LEN> { + rp_id: &'rp_id RpId, + user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>, + extensions: Extension<'prf_first, 'prf_second>, + ) -> PublicKeyCredentialCreationOptions< + 'rp_id, + 'user_name, + 'user_display_name, + 'user_id, + 'prf_first, + 'prf_second, + USER_LEN, + > { PublicKeyCredentialCreationOptions { - rp_id: &self.rp_id, - user: (&self.user).into(), + rp_id, + user, challenge: Challenge::new(), pub_key_cred_params: self.pub_key_cred_params, timeout: self.timeout, exclude_credentials, authenticator_selection: self.authenticator_selection, - extensions: (&self.extensions).into(), + extensions, } } } -impl<'user_name, 'user_display_name, const USER_LEN: usize> Default - for PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN> -where - PublicKeyCredentialUserEntityOwned<'user_name, 'user_display_name, USER_LEN>: Default, -{ +impl<const USER_LEN: usize> Default for PublicKeyCredentialCreationOptionsOwned<'_, '_, USER_LEN> { #[inline] fn default() -> Self { Self { - rp_id: DEFAULT_RP_ID, + rp_id: None, user: PublicKeyCredentialUserEntityOwned::default(), pub_key_cred_params: CoseAlgorithmIdentifiers::default(), timeout: FIVE_MINUTES, authenticator_selection: AuthenticatorSelectionCriteria { - authenticator_attachment: AuthenticatorAttachmentReq::default(), + authenticator_attachment: AuthenticatorAttachmentReq::None(Hint::None), resident_key: ResidentKeyRequirement::Discouraged, user_verification: UserVerificationRequirement::Preferred, }, @@ -2406,8 +2836,7 @@ impl<'de: 'user_name + 'user_display_name, 'user_name, 'user_display_name, const Deserialize<'de> for PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN> where - UserHandle<USER_LEN>: Default, - PublicKeyCredentialUserEntityOwned<'user_name, 'user_display_name, USER_LEN>: Default, + PublicKeyCredentialUserEntityOwned<'user_name, 'user_display_name, USER_LEN>: Deserialize<'de>, { /// Deserializes a `struct` based on /// [`PublicKeyCredentialCreationOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptionsjson). @@ -2429,14 +2858,9 @@ where /// exists, it must be `null`, empty, or `["none"]`. /// /// If [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-timeout) exists, - /// it must be `null` or positive. - /// - /// In the event there is no RP ID defined, the value `"example.invalid"` will be used. + /// it must be `null` or positive. When it does not exist or is `null`, [`FIVE_MINUTES`] will be used. /// - /// For any field that does not exist or is `null`, the corresponding [`Default`] `impl` will be used. For - /// [`AuthenticatorSelectionCriteria`], `AuthenticatorAttachmentReq::None(Hint::None)`, - /// [`ResidentKeyRequirement::Discouraged`], and [`UserVerificationRequirement::Preferred`] will be used. - /// For `timeout`, [`FIVE_MINUTES`] will be used. + /// Fields that are missing or `null` will be replaced with their corresponding [`Default`] value. /// /// Unknown or duplicate fields lead to an error. #[expect(clippy::too_many_lines, reason = "want to keep logic internal")] @@ -2452,8 +2876,7 @@ where impl<'d: 'a + 'b, 'a, 'b, const LEN: usize> Visitor<'d> for PublicKeyCredentialCreationOptionsOwnedVisitor<'a, 'b, LEN> where - UserHandle<LEN>: Default, - PublicKeyCredentialUserEntityOwned<'a, 'b, LEN>: Default, + PublicKeyCredentialUserEntityOwned<'a, 'b, LEN>: Deserialize<'d>, { type Value = PublicKeyCredentialCreationOptionsOwned<'a, 'b, LEN>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { @@ -2546,7 +2969,8 @@ where } rp = map .next_value::<Option<PublicKeyCredentialRpEntityHelper>>() - .map(|opt| Some(opt.map(|val| val.0)))?; + .map(|opt| opt.map(|val| val.0)) + .map(Some)?; } Field::User => { if user_info.is_some() { @@ -2696,16 +3120,14 @@ where } } Ok(PublicKeyCredentialCreationOptionsOwned { - rp_id: rp.flatten().unwrap_or(DEFAULT_RP_ID), + rp_id: rp.flatten().flatten(), user: user_info.flatten().unwrap_or_default(), pub_key_cred_params: params.flatten().unwrap_or_default(), timeout: time.flatten().unwrap_or(FIVE_MINUTES), - authenticator_selection: auth.unwrap_or_else(|| { - AuthenticatorSelectionCriteria { - authenticator_attachment: AuthenticatorAttachmentReq::default(), - resident_key: ResidentKeyRequirement::Discouraged, - user_verification: UserVerificationRequirement::Preferred, - } + authenticator_selection: auth.unwrap_or(AuthenticatorSelectionCriteria { + authenticator_attachment: AuthenticatorAttachmentReq::None(Hint::None), + resident_key: ResidentKeyRequirement::Discouraged, + user_verification: UserVerificationRequirement::Preferred, }), extensions: ext.flatten().unwrap_or_default(), }) @@ -2742,15 +3164,7 @@ where /// [`CredProtect::UserVerificationRequired`] which can typically only be used when /// [`UserVerificationRequirement::Required`] is requested since many user agents error otherwise. /// -/// To facilitate this, [`Self::deserialize`] can be used to deserialize the data sent from the client. Upon -/// successful deserialization, [`Self::into_options`] can then be used to construct the appropriate -/// [`CredentialCreationOptions`]. -/// -/// Note one may want to change some of the [`Extension`] data since [`ExtensionInfo::AllowEnforceValue`] and -/// [`ExtensionReq::Allow`] are unconditionally used. Read [`ExtensionOwned::deserialize`] for more information. -/// -/// Additionally, one may want to change the value of [`PublicKeyCredentialCreationOptions::rp_id`] since -/// `"example.invalid"` is used in the event the RP ID was not supplied. +/// To facilitate this, [`Self::deserialize`] can be used to deserialize the data sent from the client. #[derive(Debug)] pub struct ClientCredentialCreationOptions<'user_name, 'user_display_name, const USER_LEN: usize> { /// See [`CredentialCreationOptions::mediation`]. @@ -2759,40 +3173,11 @@ pub struct ClientCredentialCreationOptions<'user_name, 'user_display_name, const pub public_key: PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN>, } -impl<const USER_LEN: usize> ClientCredentialCreationOptions<'_, '_, USER_LEN> { - /// Creates a `CredentialCreationOptions` based on the contained data where - /// [`CredentialCreationOptions::public_key`] is constructed via - /// [`PublicKeyCredentialCreationOptionsOwned::into_options`]. - #[inline] - #[must_use] - pub fn into_options( - &self, - exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, - ) -> CredentialCreationOptions<'_, '_, '_, '_, '_, '_, USER_LEN> { - CredentialCreationOptions { - mediation: self.mediation, - public_key: self.public_key.into_options(exclude_credentials), - } - } -} -impl<'user_name, 'user_display_name, const USER_LEN: usize> Default - for ClientCredentialCreationOptions<'user_name, 'user_display_name, USER_LEN> -where - PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN>: Default, -{ - #[inline] - fn default() -> Self { - Self { - mediation: CredentialMediationRequirement::default(), - public_key: PublicKeyCredentialCreationOptionsOwned::default(), - } - } -} impl<'de: 'user_name + 'user_display_name, 'user_name, 'user_display_name, const USER_LEN: usize> Deserialize<'de> for ClientCredentialCreationOptions<'user_name, 'user_display_name, USER_LEN> where - UserHandle<USER_LEN>: Default, - PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN>: Default, + PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN>: + Deserialize<'de>, { /// Deserializes a `struct` according to the following pseudo-schema: /// @@ -2820,8 +3205,7 @@ where impl<'d: 'a + 'b, 'a, 'b, const LEN: usize> Visitor<'d> for ClientCredentialCreationOptionsVisitor<'a, 'b, LEN> where - UserHandle<LEN>: Default, - PublicKeyCredentialCreationOptionsOwned<'a, 'b, LEN>: Default, + PublicKeyCredentialCreationOptionsOwned<'a, 'b, LEN>: Deserialize<'d>, { type Value = ClientCredentialCreationOptions<'a, 'b, LEN>; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { @@ -2902,34 +3286,47 @@ mod test { use super::{ AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, ClientCredentialCreationOptions, CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, - CredProtect, CredentialMediationRequirement, CrossPlatformHint, DEFAULT_RP_ID, - ExtensionInfo, ExtensionOwned, ExtensionReq, FIVE_MINUTES, FourToSixtyThree, Hint, - NonZeroU32, PlatformHint, PublicKeyCredentialCreationOptionsOwned, - PublicKeyCredentialUserEntityOwned, ResidentKeyRequirement, UserVerificationRequirement, + CredProtect, CredentialMediationRequirement, CrossPlatformHint, ExtensionInfo, + ExtensionOwned, ExtensionReq, FIVE_MINUTES, FourToSixtyThree, Hint, NonZeroU32, + PlatformHint, 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()[..56], - *"unknown field `bob`, expected `mediation` or `publicKey`" + 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()[..27], *"duplicate field `mediation`"); - let mut options = - serde_json::from_str::<ClientCredentialCreationOptions<'_, '_, 1>>(r#"{}"#)?; + 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_eq!(options.public_key.rp_id, DEFAULT_RP_ID); - assert_eq!(options.public_key.user.name.as_ref(), "blank"); + 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, @@ -2961,8 +3358,9 @@ mod test { options.mediation, CredentialMediationRequirement::Required )); - assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID); - assert_eq!(options.public_key.user.name.as_ref(), "blank"); + 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, @@ -2990,8 +3388,9 @@ mod test { options = serde_json::from_str::<ClientCredentialCreationOptions<'_, '_, 1>>( r#"{"publicKey":{}}"#, )?; - assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID); - assert_eq!(options.public_key.user.name.as_ref(), "blank"); + 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, @@ -3017,20 +3416,38 @@ mod test { 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":"AA"},"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}}"#, + 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_eq!(options.public_key.rp_id.as_ref(), "example.com"); - assert_eq!(options.public_key.user.name.as_ref(), "bob"); + 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.as_ref() == "bob") + ); assert!( options .public_key .user .display_name - .map_or(false, |name| name.as_ref() == "Bob") + .is_some_and(|val| val.as_ref() == "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, @@ -3057,7 +3474,7 @@ mod test { .public_key .extensions .cred_props - .map_or(false, |req| matches!(req, ExtensionReq::Allow)) + .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)) @@ -3067,7 +3484,7 @@ mod test { .public_key .extensions .min_pin_length - .map_or(false, |min| min.0 == FourToSixtyThree::Four + .is_some_and(|min| min.0 == FourToSixtyThree::Four && matches!(min.1, ExtensionInfo::AllowEnforceValue)) ); assert!( @@ -3075,12 +3492,22 @@ mod test { .public_key .extensions .prf - .map_or(false, |prf| prf.first.is_empty() + .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>>( @@ -3088,75 +3515,88 @@ mod test { ) .unwrap_err(); assert_eq!( - err.to_string()[..201], - *"unknown field `bob`, expected one of `rp`, `user`, `challenge`, `pubKeyCredParams`, `timeout`, `excludeCredentials`, `authenticatorSelection`, `hints`, `extensions`, `attestation`, `attestationFormats`" + 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()[..29], *"duplicate field `attestation`"); + assert_eq!( + err.to_string().get(..29), + Some("duplicate field `attestation`") + ); err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>( r#"{"authenticatorSelection":{"authenticatorAttachment":"platform"},"hints":["client-device", "security-key"]}"#, ).unwrap_err(); assert_eq!( - err.to_string()[..96], - *"'platform' authenticator attachment modality must coincide with no hints or 'client-device' hint" + err.to_string().get(..96), + Some( + "'platform' authenticator attachment modality must coincide with no hints or 'client-device' hint" + ) ); err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>( r#"{"challenge":"AAAAAAAAAAAAAAAAAAAAAA"}"#, ) .unwrap_err(); assert_eq!( - err.to_string()[..41], - *"invalid type: Option value, expected null" + 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()[..19], *"trailing characters"); + 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()[..27], *"invalid value: string \"foo\""); + 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()[..96], - *"attestationFormats must be an empty sequence or contain exactly one string whose value is 'none'" + 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()[..42], - *"invalid value: string \"foo\", expected none" + 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()[..50], - *"invalid value: integer `0`, expected a nonzero u32" + 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()[..59], - *"invalid value: integer `4294967296`, expected a nonzero u32" + err.to_string().get(..59), + Some("invalid value: integer `4294967296`, expected a nonzero u32") ); let mut key = - serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(r#"{}"#)?; - assert_eq!(key.rp_id, DEFAULT_RP_ID); - assert_eq!(key.user.name.as_ref(), "blank"); + 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); @@ -3178,8 +3618,9 @@ mod test { 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_eq!(key.rp_id, DEFAULT_RP_ID); - assert_eq!(key.user.name.as_ref(), "blank"); + 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); @@ -3201,8 +3642,9 @@ mod test { key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>( r#"{"rp":{},"user":{},"excludeCredentials":[],"attestationFormats":[],"authenticatorSelection":{},"extensions":{},"pubKeyCredParams":[],"hints":[]}"#, )?; - assert_eq!(key.rp_id, DEFAULT_RP_ID); - assert_eq!(key.user.name.as_ref(), "blank"); + 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!( @@ -3223,8 +3665,9 @@ mod test { 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_eq!(key.rp_id, DEFAULT_RP_ID); - assert_eq!(key.user.name.as_ref(), "blank"); + 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!( @@ -3243,15 +3686,16 @@ mod test { 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":"AA"},"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}"#, + 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_eq!(key.rp_id.as_ref(), "example.com"); - assert_eq!(key.user.name.as_ref(), "bob"); + assert!(key.rp_id.is_some_and(|val| val.as_ref() == "example.com")); + assert!(key.user.name.is_some_and(|val| val.as_ref() == "bob")); assert!( key.user .display_name - .map_or(false, |name| name.as_ref() == "Bob") + .is_some_and(|val| val.as_ref() == "Bob") ); + assert!(key.user.id.is_some_and(|val| val.as_ref() == [1; 1])); assert_eq!( key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL @@ -3275,7 +3719,7 @@ mod test { assert!( key.extensions .cred_props - .map_or(false, |req| matches!(req, ExtensionReq::Allow)) + .is_some_and(|req| matches!(req, ExtensionReq::Allow)) ); assert!( matches!(key.extensions.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue)) @@ -3283,10 +3727,10 @@ mod test { assert!( key.extensions .min_pin_length - .map_or(false, |min| min.0 == FourToSixtyThree::Four + .is_some_and(|min| min.0 == FourToSixtyThree::Four && matches!(min.1, ExtensionInfo::AllowEnforceValue)) ); - assert!(key.extensions.prf.map_or(false, |prf| prf.first.is_empty() + 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>>( @@ -3295,47 +3739,62 @@ mod test { 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()[..138], - *"unknown field `bob`, expected one of `credProps`, `credentialProtectionPolicy`, `enforceCredentialProtectionPolicy`, `minPinLength`, `prf`" + 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()[..27], *"duplicate field `credProps`"); + 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()[..84], - *"'enforceCredentialProtectionPolicy' must not exist when 'credentialProtectionPolicy'" + 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()[..103], - *"'enforceCredentialProtectionPolicy' must be null or not exist when 'credentialProtectionPolicy' is null" + 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 - .map_or(false, |props| matches!(props, ExtensionReq::Allow)) + .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 - .map_or(false, |min| min.0 == FourToSixtyThree::Four + .is_some_and(|min| min.0 == FourToSixtyThree::Four && matches!(min.1, ExtensionInfo::AllowEnforceValue)) ); - assert!(ext.prf.map_or(false, |prf| prf.first.is_empty() + 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>( @@ -3345,7 +3804,7 @@ mod test { 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#"{}"#)?; + 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()); @@ -3366,6 +3825,11 @@ mod test { ); 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>>( @@ -3373,74 +3837,92 @@ mod test { ) .unwrap_err(); assert_eq!( - err.to_string()[..64], - *"unknown field `bob`, expected one of `id`, `name`, `displayName`" + 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()[..22], *"duplicate field `name`"); + assert_eq!(err.to_string().get(..22), Some("duplicate field `name`")); let mut user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 1>>( - r#"{"id":"AA","name":"bob","displayName":"Bob"}"#, + r#"{"id":"AQ","name":"bob","displayName":"Bob"}"#, )?; - assert_eq!(user.id.as_slice(), [0; 1].as_slice()); - assert_eq!(user.name.as_ref(), "bob"); - assert_eq!(user.display_name.as_ref().map(|v| v.as_ref()), Some("Bob")); + assert!( + user.id + .is_some_and(|val| val.as_slice() == [1; 1].as_slice()) + ); + assert!(user.name.is_some_and(|val| val.as_ref() == "bob")); + assert!(user.display_name.is_some_and(|val| val.as_ref() == "Bob")); user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 1>>( r#"{"id":null,"name":null,"displayName":null}"#, )?; - assert_eq!(user.name.as_ref(), "blank"); + assert!(user.name.is_none()); assert!(user.display_name.is_none()); - user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 1>>(r#"{}"#)?; - assert_eq!(user.name.as_ref(), "blank"); + 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>(r#"null"#).unwrap_err(); + let mut err = serde_json::from_str::<AuthenticatorSelectionCriteria>("null").unwrap_err(); assert_eq!( - err.to_string()[..59], - *"invalid type: null, expected AuthenticatorSelectionCriteria" + 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()[..62], - *"'residentKey' is 'required', but 'requireResidentKey' is false" + 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()[..65], - *"'residentKey' is not 'required', but 'requireResidentKey' is true" + 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()[..84], - *"invalid value: string \"prefered\", expected 'required', 'discouraged', or 'preferred'" + 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()[..119], - *"unknown field `bob`, expected one of `authenticatorAttachment`, `residentKey`, `requireResidentKey`, `userVerification`" + 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()[..36], - *"duplicate field `requireResidentKey`" + 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"}"#, @@ -3470,7 +3952,7 @@ mod test { crit.user_verification, UserVerificationRequirement::Preferred )); - crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{}"#)?; + crit = serde_json::from_str::<AuthenticatorSelectionCriteria>("{}")?; assert!( matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) ); @@ -3561,69 +4043,74 @@ mod test { )); 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>(r#"null"#).unwrap_err(); + let mut err = serde_json::from_str::<CoseAlgorithmIdentifiers>("null").unwrap_err(); assert_eq!( - err.to_string()[..53], - *"invalid type: null, expected CoseAlgorithmIdentifiers" + err.to_string().get(..53), + Some("invalid type: null, expected CoseAlgorithmIdentifiers") ); - err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[null]"#).unwrap_err(); + err = serde_json::from_str::<CoseAlgorithmIdentifiers>("[null]").unwrap_err(); assert_eq!( - err.to_string()[..37], - *"invalid type: null, expected PubParam" + err.to_string().get(..37), + Some("invalid type: null, expected PubParam") ); - err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{}]"#).unwrap_err(); - assert_eq!(err.to_string()[..19], *"missing field `alg`"); + 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()[..45], - *"unknown field `foo`, expected `type` or `alg`" + 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()[..21], *"duplicate field `alg`"); + 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()[..52], - *"invalid type: null, expected CoseAlgorithmIdentifier" + 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()[..39], - *"invalid type: null, expected public-key" + 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()[..58], - *"invalid value: integer `-6`, expected -8, -7, -35, or -257" + err.to_string().get(..58), + Some("invalid value: integer `-6`, expected -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()[..49], - *"pubKeyCredParams contained duplicate Es256 values" + 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()[..63], - *"pubKeyCredParams contained EdDSA, but it wasn't the first value" + err.to_string().get(..63), + Some("pubKeyCredParams contained EdDSA, but it wasn't the first value") ); let mut alg = serde_json::from_str::<CoseAlgorithmIdentifiers>( r#"[{"type":"public-key","alg":-8},{"alg":-7}]"#, @@ -3632,7 +4119,7 @@ mod test { assert!(alg.contains(CoseAlgorithmIdentifier::Es256)); assert!(!alg.contains(CoseAlgorithmIdentifier::Es384)); assert!(!alg.contains(CoseAlgorithmIdentifier::Rs256)); - alg = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[]"#)?; + alg = serde_json::from_str::<CoseAlgorithmIdentifiers>("[]")?; assert!(alg.contains(CoseAlgorithmIdentifier::Eddsa)); assert!(alg.contains(CoseAlgorithmIdentifier::Es256)); assert!(alg.contains(CoseAlgorithmIdentifier::Es384)); diff --git a/src/request/ser.rs b/src/request/ser.rs @@ -944,6 +944,3 @@ impl<'e> Deserialize<'e> for PrfHelper { deserializer.deserialize_struct("Prf", FIELDS, PrfHelperVisitor) } } -/// Default RP ID to use containing the value `"example.invalid"` when an RP ID is not sent. -pub(super) const DEFAULT_RP_ID: RpId = - RpId::StaticDomain(AsciiDomainStatic::new("example.invalid").unwrap()); diff --git a/src/response.rs b/src/response.rs @@ -30,7 +30,7 @@ use ser_relaxed::SerdeJsonErr; /// ```no_run /// # use core::convert; /// # use webauthn_rp::{ -/// # hash::hash_set::FixedCapHashSet, +/// # hash::hash_set::MaxLenHashSet, /// # request::{auth::{error::InvalidTimeout, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, BackupReq, RpId}, /// # response::{auth::{error::AuthCeremonyErr, DiscoverableAuthentication64}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKeyOwned, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId}, /// # AuthenticatedCredential, CredentialErr @@ -72,7 +72,7 @@ use ser_relaxed::SerdeJsonErr; /// # } /// # } /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); -/// let mut ceremonies = FixedCapHashSet::new(128); +/// let mut ceremonies = MaxLenHashSet::new(128); /// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?; /// assert!( /// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) @@ -126,14 +126,12 @@ use ser_relaxed::SerdeJsonErr; /// ``` pub mod auth; /// Contains functionality to (de)serialize data to a data store. -#[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] pub mod bin; /// Contains constants useful for /// [CTAP2 canonical CBOR encoding form](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#ctap2-canonical-cbor-encoding-form). mod cbor; /// Contains functionality that needs to be accessible when `bin` or `serde` are not enabled. -#[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] pub mod custom; /// Contains error types. @@ -146,8 +144,8 @@ pub mod error; /// ```no_run /// # use core::convert; /// # use webauthn_rp::{ -/// # hash::hash_set::FixedCapHashSet, -/// # request::{register::{error::CreationOptionsErr, CredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, PublicKeyCredentialDescriptor, RpId}, +/// # hash::hash_set::MaxLenHashSet, +/// # request::{register::{error::CreationOptionsErr, CredentialCreationOptions, DisplayName, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, PublicKeyCredentialDescriptor, RpId}, /// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData}, /// # RegisteredCredential /// # }; @@ -181,7 +179,7 @@ pub mod error; /// # } /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); /// # #[cfg(feature = "custom")] -/// let mut ceremonies = FixedCapHashSet::new(128); +/// let mut ceremonies = MaxLenHashSet::new(128); /// # #[cfg(feature = "custom")] /// let user_handle = get_user_handle(); /// # #[cfg(feature = "custom")] @@ -217,7 +215,7 @@ pub mod error; /// # PublicKeyCredentialUserEntity { /// # name: "foo".try_into().unwrap(), /// # id: user, -/// # display_name: None, +/// # display_name: DisplayName::Blank, /// # } /// } /// /// Send `RegistrationClientState` and receive `Registration` JSON from client. @@ -255,11 +253,9 @@ pub mod error; /// ``` pub mod register; /// Contains functionality to (de)serialize data to/from a client. -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] #[cfg(feature = "serde")] pub(crate) mod ser; /// Contains functionality to deserialize data from a client in a "relaxed" way. -#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] pub mod ser_relaxed; /// [Backup eligibility](https://www.w3.org/TR/webauthn-3/#backup-eligibility) and @@ -319,11 +315,9 @@ impl AuthenticatorTransport { pub struct AuthTransports(u8); impl AuthTransports { /// An empty `AuthTransports`. - #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] pub const NONE: Self = Self::new(); /// An `AuthTransports` containing all possible [`AuthenticatorTransport`]s. - #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] pub const ALL: Self = Self::all(); /// Construct an empty `AuthTransports`. @@ -411,7 +405,6 @@ impl AuthTransports { /// 6 /// ); /// ``` - #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] #[inline] #[must_use] @@ -437,7 +430,6 @@ impl AuthTransports { /// 0 /// ); /// ``` - #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] #[inline] #[must_use] @@ -853,7 +845,6 @@ impl<'a> CollectedClientData<'a> { /// # Ok::<_, SerdeJsonErr>(()) /// ``` #[expect(single_use_lifetimes, reason = "false positive")] - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] #[inline] pub fn from_client_data_json_relaxed<'b: 'a, const REGISTRATION: bool>(json: &'b [u8]) -> Result<Self, SerdeJsonErr> { @@ -1784,130 +1775,125 @@ impl<const REG: bool> FromCbor<'_> for HmacSecretGet<REG> { } #[cfg(test)] mod tests { - use super::{CollectedClientDataErr, ClientDataJsonParser, LimitedVerificationParser}; + use super::{CollectedClientDataErr, ClientDataJsonParser as _, LimitedVerificationParser}; #[test] fn parse_string() { assert!(LimitedVerificationParser::<true>::parse_string(br#"abc""#) - .map_or(false, |tup| { tup.0 == "abc" && tup.1 == br#""# })); + .is_ok_and(|tup| { tup.0 == "abc" && tup.1 == b"" })); assert!(LimitedVerificationParser::<false>::parse_string(br#"abc"23"#) - .map_or(false, |tup| { tup.0 == "abc" && tup.1 == br#"23"# })); + .is_ok_and(|tup| { tup.0 == "abc" && tup.1 == b"23" })); assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\"c"23"#) - .map_or(false, |tup| { tup.0 == r#"ab"c"# && tup.1 == br#"23"# })); + .is_ok_and(|tup| { tup.0 == r#"ab"c"# && tup.1 == b"23" })); assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\\c"23"#) - .map_or(false, |tup| { tup.0 == r#"ab\c"# && tup.1 == br#"23"# })); + .is_ok_and(|tup| { tup.0 == r"ab\c" && tup.1 == b"23" })); assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\u001fc"23"#) - .map_or(false, |tup| { tup.0 == "ab\u{001f}c" && tup.1 == br#"23"# })); + .is_ok_and(|tup| { tup.0 == "ab\u{001f}c" && tup.1 == b"23" })); assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\u000dc"23"#) - .map_or(false, |tup| { tup.0 == "ab\u{000d}c" && tup.1 == br#"23"# })); + .is_ok_and(|tup| { tup.0 == "ab\u{000d}c" && tup.1 == b"23" })); assert!( - LimitedVerificationParser::<true>::parse_string(b"\\\\\\\\\\\\a\\\\\\\\a\\\\\"").map_or(false, |tup| { + 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\\\\\"").map_or_else( + LimitedVerificationParser::<false>::parse_string(b"\\\\\\\\\\a\\\\\\\\a\\\\\"").is_err_and( |e| matches!(e, CollectedClientDataErr::InvalidEscapedString), - |_| false ) ); - assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\u0020c"23"#).map_or_else( + assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\u0020c"23"#).is_err_and( |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), - |_| false )); - assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\ac"23"#).map_or_else( + assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\ac"23"#).is_err_and( |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), - |_| false )); - assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\""#).map_or_else( + assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\""#).is_err_and( |err| matches!(err, CollectedClientDataErr::InvalidObject), - |_| false )); - assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\u001Fc"23"#).map_or_else( + assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\u001Fc"23"#).is_err_and( |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), - |_| false )); - assert!(LimitedVerificationParser::<true>::parse_string([0, b'"'].as_slice()).map_or_else( + assert!(LimitedVerificationParser::<true>::parse_string([0, b'"'].as_slice()).is_err_and( |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), - |_| false )); assert!(LimitedVerificationParser::<false>::parse_string([b'a', 255, b'"'].as_slice()) - .map_or_else(|err| matches!(err, CollectedClientDataErr::Utf8(_)), |_| false)); - assert!(LimitedVerificationParser::<true>::parse_string([b'a', b'"', 255].as_slice()).is_ok()); + .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#"""#).map_or(false, |tup| tup.0.is_empty() && tup.1.is_empty()) + 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()).map_or(false, |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()).map_or(false, |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()).map_or(false, |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()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.map_or(false, |v| v == "bob"))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob",a}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.map_or(false, |v| v == "bob"))); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).map_or(false, |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()).map_or_else(|e| matches!(e, CollectedClientDataErr::Type), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).map_or(false, |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()).map_or(false, |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()).map_or(false, |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()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); + 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()) - .map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidStart), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOriginKey), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOrigin), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true","topOrigin":"https://abc.com"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or(false, |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()).map_or(false, |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()).map_or(false, |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()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.map_or(false, |v| v == "bob"))); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).map_or(false, |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()).map_or_else(|e| matches!(e, CollectedClientDataErr::Type), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false)); + .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() ) - .map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).map_or(false, |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()).map_or(false, |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()).map_or(false, |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()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); + .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()) - .map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidStart), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOriginKey), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOrigin), |_| false)); - assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"https://example.com"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::TopOriginSameAsOrigin), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"foo":true}"#.as_slice()).map_or(false, |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()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false)); - assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create"challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossorigin":false,"foo":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false)); + .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()).map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false)); - assert!(LimitedVerificationParser::<true>::get_sent_challenge([].as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false)); - assert!(LimitedVerificationParser::<true>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBB").map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false)); - assert!(LimitedVerificationParser::<false>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBB").map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false)); - assert!(LimitedVerificationParser::<true>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".as_slice()).map_or(false, |c| c.0 == 0)); - assert!(LimitedVerificationParser::<false>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".as_slice()).map_or(false, |c| c.0 == 0)); + 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.rs b/src/response/auth.rs @@ -6,7 +6,7 @@ use self::{ #[cfg(doc)] use super::super::{ AuthenticatedCredential, RegisteredCredential, StaticState, - hash::hash_set::FixedCapHashSet, + hash::hash_set::MaxLenHashSet, request::{ Challenge, auth::{ @@ -40,11 +40,9 @@ use serde::Deserialize; /// Contains error types. pub mod error; /// Contains functionality to deserialize data from a client. -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] #[cfg(feature = "serde")] pub(super) mod ser; /// Contains functionality to deserialize data from a client in a "relaxed" way. -#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] pub mod ser_relaxed; /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension). @@ -309,8 +307,7 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> signature: Vec<u8>, user_handle: Option<UserHandle<USER_LEN>>, ) -> Self { - authenticator_data - .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); + authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice())); Self { client_data_json, authenticator_data_and_c_data_hash: authenticator_data, @@ -579,7 +576,6 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, D self.authenticator_attachment } /// Constructs an `Authentication`. - #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] #[inline] #[must_use] @@ -597,7 +593,7 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, D /// Returns the associated `SentChallenge`. /// /// This is useful when wanting to extract the corresponding [`DiscoverableAuthenticationServerState`] - /// or [`NonDiscoverableAuthenticationServerState`] from an in-memory collection (e.g., [`FixedCapHashSet`]) or + /// or [`NonDiscoverableAuthenticationServerState`] from an in-memory collection (e.g., [`MaxLenHashSet`]) or /// storage. /// /// Note if [`CollectedClientData::from_client_data_json`] returns `Ok`, then this will return `Ok` @@ -618,7 +614,7 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, D /// /// This is useful when wanting to extract the corresponding [`DiscoverableAuthenticationServerState`] /// or [`NonDiscoverableAuthenticationServerState`] from an in-memory collection (e.g., - /// [`FixedCapHashSet`]) or storage. + /// [`MaxLenHashSet`]) or storage. /// /// Note if [`CollectedClientData::from_client_data_json_relaxed`] returns `Ok`, then this will return /// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse @@ -630,7 +626,6 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, D /// a leading U+FEFF and replacing any sequences of invalid UTF-8 code units with U+FFFD or /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) does not exist /// or is not a base64url-encoded [`Challenge`]. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] #[inline] pub fn challenge_relaxed(&self) -> Result<SentChallenge, SerdeJsonErr> { @@ -643,7 +638,6 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, D /// # Errors /// /// Errors iff [`AuthenticationRelaxed::deserialize`] does. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] #[inline] pub fn from_json_relaxed<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr> @@ -658,7 +652,6 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, D /// # Errors /// /// Errors iff [`CustomAuthentication::deserialize`] does. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] #[inline] pub fn from_json_custom<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr> diff --git a/src/response/auth/error.rs b/src/response/auth/error.rs @@ -206,7 +206,6 @@ pub enum AuthCeremonyErr { CollectedClientData(CollectedClientDataErr), /// [`AuthenticatorAssertion::client_data_json`] could not be parsed by /// [`CollectedClientData::from_client_data_json_relaxed`]. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] CollectedClientDataRelaxed(SerdeJsonErr), /// [`AuthenticatorAssertion::authenticator_data`] could not be parsed into an diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs @@ -453,6 +453,13 @@ mod tests { 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(); @@ -498,10 +505,11 @@ mod tests { 0, 0, ]; - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); + 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>>( @@ -509,7 +517,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -521,24 +529,26 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |auth| auth.response.client_data_json - == c_data_json.as_bytes() - && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data - && auth.response.authenticator_data_and_c_data_hash[37..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && matches!( - auth.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - )) + .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("ABABABABABABABABABABAA".as_bytes()) + base64url_nopad::decode(b"ABABABABABABABABABABAA") .unwrap() .as_slice(), ), - &format!("id and rawId to match: CredentialId({:?})", [0; 16]).as_str(), + &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), ) .to_string() .into_bytes(); @@ -548,7 +558,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "ABABABABABABABABABABAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -562,8 +572,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // missing `id`. err = Error::missing_field("id").to_string().into_bytes(); @@ -572,7 +583,7 @@ mod tests { serde_json::json!({ "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -586,8 +597,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `id`. err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") @@ -599,7 +611,7 @@ mod tests { "id": null, "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -612,8 +624,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // missing `rawId`. err = Error::missing_field("rawId").to_string().into_bytes(); @@ -622,7 +635,7 @@ mod tests { serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -635,8 +648,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `rawId`. err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") @@ -648,7 +662,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": null, "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -661,8 +675,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `authenticatorData`. err = Error::missing_field("authenticatorData") @@ -674,7 +689,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "signature": b64_sig, "userHandle": b64_user, }, @@ -686,8 +701,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `authenticatorData`. err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") @@ -699,7 +715,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": null, "signature": b64_sig, "userHandle": b64_user, @@ -712,8 +728,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `signature`. err = Error::missing_field("signature").to_string().into_bytes(); @@ -723,7 +740,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "userHandle": b64_user, }, @@ -735,8 +752,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `signature`. err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") @@ -748,7 +766,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": null, "userHandle": b64_user, @@ -761,17 +779,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `userHandle`. - assert!( + drop( serde_json::from_str::<NonDiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, }, @@ -779,18 +798,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `userHandle`. - assert!( + drop( serde_json::from_str::<NonDiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": null, @@ -799,9 +818,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `authenticatorAttachment`. assert!( @@ -810,7 +829,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -822,7 +841,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |auth| matches!( + .is_ok_and(|auth| matches!( auth.authenticator_attachment, AuthenticatorAttachment::None )) @@ -840,7 +859,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -854,8 +873,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientDataJSON`. err = Error::missing_field("clientDataJSON") @@ -879,8 +899,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientDataJSON`. err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") @@ -905,8 +926,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `response`. err = Error::missing_field("response").to_string().into_bytes(); @@ -923,8 +945,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `response`. err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAssertion") @@ -944,8 +967,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty `response`. err = Error::missing_field("clientDataJSON") @@ -965,8 +989,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientExtensionResults`. err = Error::missing_field("clientExtensionResults") @@ -978,7 +1003,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -990,8 +1015,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientExtensionResults`. err = Error::invalid_type( @@ -1006,7 +1032,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1019,8 +1045,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `type`. err = Error::missing_field("type").to_string().into_bytes(); @@ -1030,7 +1057,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1042,8 +1069,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `type`. err = Error::invalid_type(Unexpected::Other("null"), &"public-key") @@ -1055,7 +1083,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1068,8 +1096,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") @@ -1081,7 +1110,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1094,8 +1123,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null`. err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") @@ -1107,8 +1137,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty. err = Error::missing_field("response").to_string().into_bytes(); @@ -1118,8 +1149,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field in `response`. err = Error::unknown_field( @@ -1140,7 +1172,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1154,8 +1186,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field in `response`. err = Error::duplicate_field("userHandle") @@ -1168,7 +1201,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\", @@ -1183,8 +1216,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field in `PublicKeyCredential`. err = Error::unknown_field( @@ -1207,7 +1241,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1221,8 +1255,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field in `PublicKeyCredential`. err = Error::duplicate_field("id").to_string().into_bytes(); @@ -1234,7 +1269,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\" @@ -1248,14 +1283,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 = [ + let auth_data: [u8; 37] = [ // `rpIdHash`. 0, 0, @@ -1297,7 +1336,8 @@ mod tests { 0, 0, ]; - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); + 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()); @@ -1308,7 +1348,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1320,24 +1360,26 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |auth| auth.response.client_data_json - == c_data_json.as_bytes() - && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data - && auth.response.authenticator_data_and_c_data_hash[37..] - == *Sha256::digest(c_data_json.as_bytes()).as_slice() - && matches!( - auth.authenticator_attachment, - AuthenticatorAttachment::CrossPlatform - )) + .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`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1348,9 +1390,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Unknown `clientExtensionResults`. let mut err = Error::unknown_field("Prf", ["prf"].as_slice()) @@ -1362,7 +1404,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1377,8 +1419,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field. err = Error::duplicate_field("prf").to_string().into_bytes(); @@ -1389,7 +1432,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\" @@ -1405,17 +1448,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `results`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1428,9 +1472,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field in `prf`. err = Error::duplicate_field("results").to_string().into_bytes(); @@ -1441,7 +1485,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\" @@ -1459,8 +1503,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `first`. err = Error::missing_field("first").to_string().into_bytes(); @@ -1470,7 +1515,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1487,17 +1532,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `first`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1512,18 +1558,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `second`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1539,9 +1585,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Non-`null` `first`. err = Error::invalid_type(Unexpected::Option, &"null") @@ -1553,7 +1599,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1572,8 +1618,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Non-`null` `second`. err = Error::invalid_type(Unexpected::Option, &"null") @@ -1585,7 +1632,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1605,8 +1652,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `prf` field. err = Error::unknown_field("enabled", ["results"].as_slice()) @@ -1618,7 +1666,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1636,8 +1684,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `results` field. err = Error::unknown_field("Second", ["first", "second"].as_slice()) @@ -1649,7 +1698,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1669,8 +1718,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field in `results`. err = Error::duplicate_field("first").to_string().into_bytes(); @@ -1681,7 +1731,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\" @@ -1701,8 +1751,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 @@ -468,10 +468,17 @@ mod tests { 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 = [ + let auth_data: [u8; 37] = [ // `rpIdHash`. 0, 0, @@ -513,7 +520,7 @@ mod tests { 0, 0, ]; - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); + 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()); @@ -524,7 +531,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -536,11 +543,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |auth| auth.0.response.client_data_json + .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()).as_slice() + == *Sha256::digest(c_data_json.as_bytes()) && matches!( auth.0.authenticator_attachment, AuthenticatorAttachment::CrossPlatform @@ -549,11 +556,11 @@ mod tests { // `id` and `rawId` mismatch. let mut err = Error::invalid_value( Unexpected::Bytes( - base64url_nopad::decode("ABABABABABABABABABABAA".as_bytes()) + base64url_nopad::decode(b"ABABABABABABABABABABAA") .unwrap() .as_slice(), ), - &format!("id and rawId to match: CredentialId({:?})", [0; 16]).as_str(), + &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), ) .to_string() .into_bytes(); @@ -563,7 +570,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "ABABABABABABABABABABAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -577,8 +584,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // missing `id`. err = Error::missing_field("id").to_string().into_bytes(); @@ -587,7 +595,7 @@ mod tests { serde_json::json!({ "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -601,8 +609,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `id`. err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") @@ -614,7 +623,7 @@ mod tests { "id": null, "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -627,16 +636,17 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // missing `rawId`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -645,9 +655,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `rawId`. err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") @@ -659,7 +669,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": null, "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -672,8 +682,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `authenticatorData`. err = Error::missing_field("authenticatorData") @@ -685,7 +696,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "signature": b64_sig, "userHandle": b64_user, }, @@ -697,8 +708,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `authenticatorData`. err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") @@ -710,7 +722,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": null, "signature": b64_sig, "userHandle": b64_user, @@ -723,8 +735,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `signature`. err = Error::missing_field("signature").to_string().into_bytes(); @@ -734,7 +747,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "userHandle": b64_user, }, @@ -746,8 +759,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `signature`. err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") @@ -759,7 +773,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": null, "userHandle": b64_user, @@ -772,17 +786,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `userHandle`. - assert!( + drop( serde_json::from_str::<NonDiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, }, @@ -790,18 +805,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `userHandle`. - assert!( + drop( serde_json::from_str::<NonDiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": null, @@ -810,9 +825,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `authenticatorAttachment`. assert!( @@ -821,7 +836,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -833,7 +848,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |auth| matches!( + .is_ok_and(|auth| matches!( auth.0.authenticator_attachment, AuthenticatorAttachment::None )) @@ -851,7 +866,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -865,8 +880,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientDataJSON`. err = Error::missing_field("clientDataJSON") @@ -890,8 +906,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientDataJSON`. err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") @@ -916,8 +933,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `response`. err = Error::missing_field("response").to_string().into_bytes(); @@ -934,8 +952,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `response`. err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAssertion") @@ -955,8 +974,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty `response`. err = Error::missing_field("clientDataJSON") @@ -976,17 +996,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientExtensionResults`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -994,18 +1015,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `clientExtensionResults`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1014,18 +1035,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Missing `type`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1033,9 +1054,9 @@ mod tests { "clientExtensionResults": {}, }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `type`. err = Error::invalid_type(Unexpected::Other("null"), &"public-key") @@ -1047,7 +1068,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1060,8 +1081,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") @@ -1073,7 +1095,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1086,8 +1108,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null`. err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") @@ -1099,8 +1122,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty. err = Error::missing_field("response").to_string().into_bytes(); @@ -1110,17 +1134,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field in `response`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1130,9 +1155,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field in `response`. err = Error::duplicate_field("userHandle") @@ -1145,7 +1170,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\", @@ -1160,17 +1185,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field in `PublicKeyCredential`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1180,9 +1206,9 @@ mod tests { "foo": true, }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field in `PublicKeyCredential`. err = Error::duplicate_field("id").to_string().into_bytes(); @@ -1194,7 +1220,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\" @@ -1208,15 +1234,16 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1227,11 +1254,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |auth| auth.0.response.client_data_json + .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()).as_slice() + == *Sha256::digest(c_data_json.as_bytes()) && matches!( auth.0.authenticator_attachment, AuthenticatorAttachment::CrossPlatform @@ -1242,7 +1269,7 @@ mod tests { assert_eq!( serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1255,8 +1282,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `id`. err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") @@ -1266,7 +1294,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": null, - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1278,8 +1306,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `authenticatorData`. err = Error::missing_field("authenticatorData") @@ -1289,7 +1318,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "signature": b64_sig, "userHandle": b64_user, "clientExtensionResults": {}, @@ -1300,8 +1329,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `authenticatorData`. err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData") @@ -1311,7 +1341,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": null, "signature": b64_sig, "userHandle": b64_user, @@ -1323,8 +1353,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `signature`. err = Error::missing_field("signature").to_string().into_bytes(); @@ -1332,7 +1363,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "userHandle": b64_user, "clientExtensionResults": {}, @@ -1343,8 +1374,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `signature`. err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") @@ -1354,7 +1386,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": null, "userHandle": b64_user, @@ -1366,31 +1398,32 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `userHandle`. - assert!( + drop( serde_json::from_str::<NonDiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `userHandle`. - assert!( + drop( serde_json::from_str::<NonDiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": null, @@ -1398,16 +1431,16 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `authenticatorAttachment`. assert!( serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1418,7 +1451,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |auth| matches!( + .is_ok_and(|auth| matches!( auth.0.authenticator_attachment, AuthenticatorAttachment::None )) @@ -1434,7 +1467,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1447,8 +1480,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientDataJSON`. err = Error::missing_field("clientDataJSON") @@ -1469,8 +1503,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientDataJSON`. err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") @@ -1492,8 +1527,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty. err = Error::missing_field("authenticatorData") @@ -1505,8 +1541,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientExtensionResults`. err = Error::missing_field("clientExtensionResults") @@ -1516,7 +1553,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1527,8 +1564,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientExtensionResults`. err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs") @@ -1538,7 +1576,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1550,23 +1588,24 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); - assert!( + drop( serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, "clientExtensionResults": {}, }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `type`. err = Error::invalid_type(Unexpected::Other("null"), &"public-key") @@ -1576,7 +1615,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1588,8 +1627,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") @@ -1599,7 +1639,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1611,8 +1651,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null`. err = Error::invalid_type(Unexpected::Other("null"), &"CustomAuthentication") @@ -1624,8 +1665,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field. err = Error::unknown_field( @@ -1648,7 +1690,7 @@ mod tests { serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1661,8 +1703,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field. err = Error::duplicate_field("userHandle") @@ -1673,7 +1716,7 @@ mod tests { format!( "{{ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\", @@ -1687,14 +1730,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 = [ + let auth_data: [u8; 37] = [ // `rpIdHash`. 0, 0, @@ -1736,7 +1783,7 @@ mod tests { 0, 0, ]; - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); + 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()); @@ -1747,7 +1794,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1759,24 +1806,24 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |auth| auth.0.response.client_data_json + .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()).as_slice() + == *Sha256::digest(c_data_json.as_bytes()) && matches!( auth.0.authenticator_attachment, AuthenticatorAttachment::CrossPlatform )) ); // `null` `prf`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1787,18 +1834,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Unknown `clientExtensionResults`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1809,9 +1856,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field. let mut err = Error::duplicate_field("prf").to_string().into_bytes(); @@ -1822,7 +1869,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\" @@ -1838,17 +1885,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `results`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1861,9 +1909,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field in `prf`. err = Error::duplicate_field("results").to_string().into_bytes(); @@ -1874,7 +1922,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\" @@ -1892,17 +1940,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `first`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1915,18 +1964,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `first`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1941,18 +1990,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `second`. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -1968,9 +2017,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Non-`null` `first`. err = Error::invalid_type(Unexpected::Option, &"null") @@ -1982,7 +2031,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -2001,8 +2050,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Non-`null` `second`. err = Error::invalid_type(Unexpected::Option, &"null") @@ -2014,7 +2064,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -2034,8 +2084,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `enabled` is still not allowed. err = Error::unknown_field("enabled", ["results"].as_slice()) @@ -2047,7 +2098,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -2065,17 +2116,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `prf` field. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -2089,18 +2141,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Unknown `results` field. - assert!( + drop( serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "signature": b64_sig, "userHandle": b64_user, @@ -2116,9 +2168,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field in `results`. err = Error::duplicate_field("first").to_string().into_bytes(); @@ -2129,7 +2181,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"signature\": \"{b64_sig}\", \"userHandle\": \"{b64_user}\" @@ -2149,8 +2201,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); } } diff --git a/src/response/custom.rs b/src/response/custom.rs @@ -141,32 +141,32 @@ mod tests { assert_eq!(iter.len(), 6); assert!( iter.next() - .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Ble)) + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Ble)) ); assert_eq!(iter.len(), 5); assert!( iter.next() - .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Hybrid)) + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Hybrid)) ); assert_eq!(iter.len(), 4); assert!( iter.next_back() - .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Usb)) + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Usb)) ); assert_eq!(iter.len(), 3); - assert!(iter.next().map_or(false, |tran| matches!( - tran, - AuthenticatorTransport::Internal - ))); + assert!( + iter.next() + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::Internal)) + ); assert_eq!(iter.len(), 2); - assert!(iter.next_back().map_or(false, |tran| matches!( - tran, - AuthenticatorTransport::SmartCard - ))); + assert!( + iter.next_back() + .is_some_and(|tran| matches!(tran, AuthenticatorTransport::SmartCard)) + ); assert_eq!(iter.len(), 1); assert!( iter.next() - .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Nfc)) + .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 @@ -25,7 +25,7 @@ use super::{ use super::{ super::{ AuthenticatedCredential, RegisteredCredential, - hash::hash_set::FixedCapHashSet, + hash::hash_set::MaxLenHashSet, request::{ BackupReq, Challenge, UserVerificationRequirement, auth::{AuthenticationVerificationOptions, PublicKeyCredentialRequestOptions}, @@ -57,17 +57,14 @@ use rsa::{ #[cfg(all(doc, feature = "serde_relaxed"))] use serde::Deserialize; /// Contains functionality to (de)serialize data to a data store. -#[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] pub mod bin; /// Contains error types. pub mod error; /// Contains functionality to deserialize data from a client. -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] #[cfg(feature = "serde")] mod ser; /// Contains functionality to deserialize data from a client in a "relaxed" way. -#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] pub mod ser_relaxed; /// [`credentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#dom-authenticationextensionsclientinputs-credentialprotectionpolicy). @@ -2931,8 +2928,7 @@ impl AuthenticatorAttestation { mut attestation_object: Vec<u8>, transports: AuthTransports, ) -> Self { - attestation_object - .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); + attestation_object.extend_from_slice(&Sha256::digest(client_data_json.as_slice())); Self { client_data_json, attestation_object_and_c_data_hash: attestation_object, @@ -3105,7 +3101,6 @@ impl Registration { self.client_extension_results } /// Constructs a `Registration` based on the passed arguments. - #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] #[cfg(feature = "custom")] #[inline] #[must_use] @@ -3123,7 +3118,7 @@ impl Registration { /// Returns the associated `SentChallenge`. /// /// This is useful when wanting to extract the corresponding [`RegistrationServerState`] from - /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage. + /// an in-memory collection (e.g., [`MaxLenHashSet`]) or storage. /// /// Note if [`CollectedClientData::from_client_data_json`] returns `Ok`, then this will return /// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse @@ -3142,7 +3137,7 @@ impl Registration { /// Returns the associated `SentChallenge`. /// /// This is useful when wanting to extract the corresponding [`RegistrationServerState`] from - /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage. + /// an in-memory collection (e.g., [`MaxLenHashSet`]) or storage. /// /// Note if [`CollectedClientData::from_client_data_json_relaxed`] returns `Ok`, then this will return /// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse @@ -3154,7 +3149,6 @@ impl Registration { /// a leading U+FEFF and replacing any sequences of invalid UTF-8 code units with U+FFFD or /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) does not exist /// or is not a base64url-encoded [`Challenge`]. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] #[inline] pub fn challenge_relaxed(&self) -> Result<SentChallenge, SerdeJsonErr> { @@ -3167,7 +3161,6 @@ impl Registration { /// # Errors /// /// Errors iff [`RegistrationRelaxed::deserialize`] does. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] #[inline] pub fn from_json_relaxed(json: &[u8]) -> Result<Self, SerdeJsonErr> { @@ -3178,7 +3171,6 @@ impl Registration { /// # Errors /// /// Errors iff [`CustomRegistration::deserialize`] does. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] #[inline] pub fn from_json_custom(json: &[u8]) -> Result<Self, SerdeJsonErr> { @@ -3368,7 +3360,6 @@ impl Metadata<'_> { } /// Transforms `self` into an "owned" version. #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] - #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] #[cfg(feature = "bin")] #[inline] #[must_use] @@ -3488,6 +3479,13 @@ mod tests { 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; @@ -3498,16 +3496,22 @@ mod tests { ); 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"), - } << 4; + } << 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"), }, @@ -3515,7 +3519,13 @@ mod tests { }); data } - /// https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-none-es256 + /// <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())?); @@ -3549,11 +3559,11 @@ mod tests { .0 ); assert!( - matches!(att_obj.data.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if enc_key.x().unwrap().as_slice() == pub_key.0 && enc_key.y().unwrap().as_slice() == pub_key.1) + 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()).as_slice() + *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)); @@ -3568,10 +3578,7 @@ mod tests { signature, ); let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; - assert_eq!( - auth_data.rp_id_hash(), - Sha256::digest(rp_id.as_ref()).as_slice() - ); + 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), @@ -3580,11 +3587,18 @@ mod tests { }); 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()).as_slice()); - assert!(key.verify(msg.as_slice(), &sig).is_ok()); + 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 + /// <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())?); @@ -3608,11 +3622,11 @@ mod tests { 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().as_slice() == pub_key.0 && enc_key.y().unwrap().as_slice() == pub_key.1) + 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()).as_slice() + *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 { @@ -3623,6 +3637,7 @@ mod tests { 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, ) @@ -3642,10 +3657,7 @@ mod tests { signature, ); let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; - assert_eq!( - auth_data.rp_id_hash(), - Sha256::digest(rp_id.as_ref()).as_slice() - ); + 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), @@ -3654,8 +3666,8 @@ mod tests { }); 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()).as_slice()); - assert!(key.verify(msg.as_slice(), &sig).is_ok()); + msg.extend_from_slice(&Sha256::digest(auth_assertion.client_data_json())); + key.verify(msg.as_slice(), &sig).unwrap(); Ok(()) } struct AuthExtOptions<'a> { @@ -3664,7 +3676,19 @@ mod tests { min_pin_length: Option<u8>, hmac_secret_mc: Option<&'a [u8]>, } - fn generate_auth_extensions(opts: AuthExtOptions<'_>) -> Vec<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()) @@ -3705,10 +3729,12 @@ mod tests { 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!( @@ -3719,9 +3745,12 @@ mod tests { } 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 test_auth_ext() -> Result<(), AuthenticatorExtensionOutputErr> { - let opts = generate_auth_extensions(AuthExtOptions { + fn auth_ext() -> Result<(), AuthenticatorExtensionOutputErr> { + let mut opts = generate_auth_extensions(&AuthExtOptions { cred_protect: None, hmac_secret: None, min_pin_length: None, @@ -3731,7 +3760,7 @@ mod tests { AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; assert!(remaining.is_empty()); assert!(value.missing()); - let opts = generate_auth_extensions(AuthExtOptions { + opts = generate_auth_extensions(&AuthExtOptions { cred_protect: None, hmac_secret: None, min_pin_length: None, @@ -3743,7 +3772,7 @@ mod tests { |_| false, ) ); - let opts = generate_auth_extensions(AuthExtOptions { + opts = generate_auth_extensions(&AuthExtOptions { cred_protect: None, hmac_secret: Some(true), min_pin_length: None, @@ -3757,7 +3786,7 @@ mod tests { && matches!(value.hmac_secret, HmacSecret::One) && value.min_pin_length.is_none() ); - let opts = generate_auth_extensions(AuthExtOptions { + opts = generate_auth_extensions(&AuthExtOptions { cred_protect: None, hmac_secret: Some(false), min_pin_length: None, @@ -3769,7 +3798,7 @@ mod tests { |_| false, ) ); - let opts = generate_auth_extensions(AuthExtOptions { + opts = generate_auth_extensions(&AuthExtOptions { cred_protect: None, hmac_secret: Some(true), min_pin_length: None, @@ -3781,7 +3810,7 @@ mod tests { |_| false, ) ); - let opts = generate_auth_extensions(AuthExtOptions { + opts = generate_auth_extensions(&AuthExtOptions { cred_protect: None, hmac_secret: Some(true), min_pin_length: None, @@ -3793,7 +3822,7 @@ mod tests { |_| false, ) ); - let opts = generate_auth_extensions(AuthExtOptions { + opts = generate_auth_extensions(&AuthExtOptions { cred_protect: Some(1), hmac_secret: Some(true), min_pin_length: Some(5), @@ -3811,7 +3840,7 @@ mod tests { .min_pin_length .is_some_and(|pin| pin == FourToSixtyThree::Five) ); - let opts = generate_auth_extensions(AuthExtOptions { + opts = generate_auth_extensions(&AuthExtOptions { cred_protect: Some(0), hmac_secret: None, min_pin_length: None, @@ -3823,7 +3852,7 @@ mod tests { |_| false, ) ); - let opts = generate_auth_extensions(AuthExtOptions { + opts = generate_auth_extensions(&AuthExtOptions { cred_protect: None, hmac_secret: None, min_pin_length: Some(3), @@ -3835,7 +3864,7 @@ mod tests { |_| false, ) ); - let opts = generate_auth_extensions(AuthExtOptions { + opts = generate_auth_extensions(&AuthExtOptions { cred_protect: None, hmac_secret: None, min_pin_length: Some(64), diff --git a/src/response/register/error.rs b/src/response/register/error.rs @@ -556,7 +556,6 @@ pub enum RegCeremonyErr { CollectedClientData(CollectedClientDataErr), /// [`AuthenticatorAttestation::client_data_json`] could not be parsed by /// [`CollectedClientData::from_client_data_json_relaxed`]. - #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] #[cfg(feature = "serde_relaxed")] CollectedClientDataRelaxed(SerdeJsonErr), /// [`AuthenticatorAttestation::attestation_object`] could not be parsed into diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs @@ -1389,9 +1389,9 @@ mod tests { Registration, RsaPubKey, UncompressedP256PubKey, UncompressedP384PubKey, cbor, }, CoseAlgorithmIdentifier, - spki::SubjectPublicKeyInfo, + spki::SubjectPublicKeyInfo as _, }; - use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey}; + use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey as _}; use p256::{ EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key, elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, @@ -1400,10 +1400,11 @@ mod tests { use rsa::{ BigUint, RsaPrivateKey, sha2::{Digest as _, Sha256}, - traits::PublicKeyParts, + traits::PublicKeyParts as _, }; use serde::de::{Error as _, Unexpected}; use serde_json::Error; + #[expect(clippy::unwrap_used, reason = "OK in tests")] #[test] fn ed25519_spki() { assert!( @@ -1414,9 +1415,10 @@ mod tests { .unwrap() .as_bytes() ) - .map_or(false, |k| k.0 == [1; 32]) + .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( @@ -1430,13 +1432,11 @@ mod tests { .public_key(); let enc_key = key.to_encoded_point(false); assert!( - UncompressedP256PubKey::from_der(key.to_public_key_der().unwrap().as_bytes()).map_or( - false, - |k| k.0 == enc_key.x().unwrap().as_slice() - && k.1 == enc_key.y().unwrap().as_slice() - ) + 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( @@ -1451,13 +1451,11 @@ mod tests { .public_key(); let enc_key = key.to_encoded_point(false); assert!( - UncompressedP384PubKey::from_der(key.to_public_key_der().unwrap().as_bytes()).map_or( - false, - |k| k.0 == enc_key.x().unwrap().as_slice() - && k.1 == enc_key.y().unwrap().as_slice() - ) + 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 = [ @@ -1476,7 +1474,7 @@ mod tests { 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 = 65537u32; + 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, @@ -1529,14 +1527,20 @@ mod tests { .to_public_key(); assert!( RsaPubKey::from_der(key.to_public_key_der().unwrap().as_bytes()) - .map_or(false, |k| k.0 == key.n().to_bytes_be() - && BigUint::from(k.1) == *key.e()) + .is_ok_and(|k| k.0 == key.n().to_bytes_be() && BigUint::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 = [ + let att_obj: [u8; 143] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -1696,8 +1700,10 @@ mod tests { .unwrap() .to_public_key_der() .unwrap(); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 113..]); + 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. @@ -1707,11 +1713,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "authenticatorAttachment": "cross-platform", @@ -1721,28 +1727,28 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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("ABABABABABABABABABABAA".as_bytes()) + base64url_nopad::decode(b"ABABABABABABABABABABAA") .unwrap() .as_slice(), ), - &format!("id and rawId to match: CredentialId({:?})", [0; 16]).as_str(), + &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), ) .to_string() .into_bytes(); @@ -1752,11 +1758,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "ABABABABABABABABABABAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1767,8 +1773,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // missing `id`. err = Error::missing_field("id").to_string().into_bytes(); @@ -1777,11 +1784,11 @@ mod tests { serde_json::json!({ "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1792,8 +1799,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `id`. err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") @@ -1805,11 +1813,11 @@ mod tests { "id": null, "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1820,8 +1828,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // missing `rawId`. err = Error::missing_field("rawId").to_string().into_bytes(); @@ -1830,11 +1839,11 @@ mod tests { serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1845,8 +1854,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `rawId`. err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") @@ -1858,11 +1868,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": null, "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1873,18 +1883,19 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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("ABABABABABABABABABABAA".as_bytes()) + ::decode(b"ABABABABABABABABABABAA") .unwrap() .as_slice(), ), - &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", [0; 16]).as_str(), + &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!( @@ -1893,11 +1904,11 @@ mod tests { "id": "ABABABABABABABABABABAA", "rawId": "ABABABABABABABABABABAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1908,16 +1919,17 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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[att_obj.len() - 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(), + &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!( @@ -1926,11 +1938,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": base64url_nopad::encode(bad_auth.as_slice()), "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1941,8 +1953,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `authenticatorData`. err = Error::missing_field("authenticatorData") @@ -1954,10 +1967,10 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1968,8 +1981,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `authenticatorData`. err = Error::invalid_type(Unexpected::Other("null"), &"authenticatorData") @@ -1981,11 +1995,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "authenticatorData": null, "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1996,12 +2010,13 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `publicKeyAlgorithm` mismatch. err = Error::invalid_value( - Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()), + 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(); @@ -2011,11 +2026,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2026,8 +2041,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKeyAlgorithm`. err = Error::missing_field("publicKeyAlgorithm") @@ -2039,7 +2055,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -2053,8 +2069,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `publicKeyAlgorithm`. err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm") @@ -2066,7 +2083,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -2081,8 +2098,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `publicKey` mismatch. err = Error::invalid_value( @@ -2099,11 +2117,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "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": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2112,8 +2130,8 @@ mod tests { .to_string() .as_str() ) - .unwrap_err().to_string().into_bytes()[..err.len()], - err + .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(); @@ -2123,10 +2141,10 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2137,8 +2155,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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") @@ -2150,11 +2169,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2165,8 +2184,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `transports`. err = Error::missing_field("transports").to_string().into_bytes(); @@ -2176,10 +2196,10 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2190,8 +2210,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate `transports` are allowed. assert!( @@ -2200,11 +2221,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": ["usb", "usb"], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2213,7 +2234,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg.response.transports.count() == 1) + .is_ok_and(|reg| reg.response.transports.count() == 1) ); // `null` `transports`. err = Error::invalid_type(Unexpected::Other("null"), &"transports") @@ -2225,11 +2246,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": null, "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2240,8 +2261,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `transports`. err = Error::invalid_value( @@ -2256,11 +2278,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": ["Usb"], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2271,8 +2293,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `authenticatorAttachment`. assert!( @@ -2281,11 +2304,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "authenticatorAttachment": null, @@ -2295,10 +2318,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| matches!( - reg.authenticator_attachment, - AuthenticatorAttachment::None - )) + .is_ok_and(|reg| matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)) ); // Unknown `authenticatorAttachment`. err = Error::invalid_value( @@ -2313,11 +2333,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "authenticatorAttachment": "Platform", @@ -2329,8 +2349,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientDataJSON`. err = Error::missing_field("clientDataJSON") @@ -2345,7 +2366,7 @@ mod tests { "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2356,8 +2377,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientDataJSON`. err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") @@ -2373,7 +2395,7 @@ mod tests { "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2384,8 +2406,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `attestationObject`. err = Error::missing_field("attestationObject") @@ -2397,11 +2420,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, }, "clientExtensionResults": {}, "type": "public-key" @@ -2411,8 +2434,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `attestationObject`. err = Error::invalid_type( @@ -2427,11 +2451,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": null, }, "clientExtensionResults": {}, @@ -2442,8 +2466,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `response`. err = Error::missing_field("response").to_string().into_bytes(); @@ -2460,8 +2485,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `response`. err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation") @@ -2481,8 +2507,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty `response`. err = Error::missing_field("clientDataJSON") @@ -2502,8 +2529,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientExtensionResults`. err = Error::missing_field("clientExtensionResults") @@ -2515,11 +2543,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "type": "public-key" @@ -2529,8 +2557,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientExtensionResults`. err = Error::invalid_type( @@ -2545,11 +2574,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": null, @@ -2560,8 +2589,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `type`. err = Error::missing_field("type").to_string().into_bytes(); @@ -2571,11 +2601,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2585,8 +2615,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `type`. err = Error::invalid_type(Unexpected::Other("null"), &"public-key") @@ -2598,11 +2629,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2613,8 +2644,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") @@ -2626,11 +2658,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2641,8 +2673,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null`. err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") @@ -2652,8 +2685,9 @@ mod tests { serde_json::from_str::<Registration>(serde_json::json!(null).to_string().as_str()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty. err = Error::missing_field("response").to_string().into_bytes(); @@ -2661,8 +2695,9 @@ mod tests { serde_json::from_str::<Registration>(serde_json::json!({}).to_string().as_str()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field in `response`. err = Error::unknown_field( @@ -2685,11 +2720,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, "foo": true, }, @@ -2701,8 +2736,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field in `response`. err = Error::duplicate_field("transports") @@ -2715,7 +2751,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -2732,8 +2768,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field in `PublicKeyCredential`. err = Error::unknown_field( @@ -2756,11 +2793,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj }, "clientExtensionResults": {}, @@ -2772,8 +2809,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field in `PublicKeyCredential`. err = Error::duplicate_field("id").to_string().into_bytes(); @@ -2785,7 +2823,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -2801,14 +2839,22 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 = [ + let att_obj: [u8; 143] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -2968,7 +3014,7 @@ mod tests { .unwrap() .to_public_key_der() .unwrap(); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); + 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()); @@ -2979,11 +3025,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2992,16 +3038,16 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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!( @@ -3010,11 +3056,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3025,10 +3071,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() && reg.client_extension_results.prf.is_none()) ); // `null` `prf`. @@ -3038,11 +3081,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3053,10 +3096,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() && reg.client_extension_results.prf.is_none()) ); // Unknown `clientExtensionResults`. @@ -3069,11 +3109,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3088,8 +3128,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field. err = Error::duplicate_field("credProps").to_string().into_bytes(); @@ -3100,7 +3141,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -3118,8 +3159,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `rk`. assert!( @@ -3128,11 +3170,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3145,10 +3187,10 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg + .is_ok_and(|reg| reg .client_extension_results .cred_props - .map_or(false, |props| props.rk.is_none()) + .is_some_and(|props| props.rk.is_none()) && reg.client_extension_results.prf.is_none()) ); // Missing `rk`. @@ -3158,11 +3200,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3173,10 +3215,10 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg + .is_ok_and(|reg| reg .client_extension_results .cred_props - .map_or(false, |props| props.rk.is_none()) + .is_some_and(|props| props.rk.is_none()) && reg.client_extension_results.prf.is_none()) ); // `true` rk`. @@ -3186,11 +3228,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3203,10 +3245,10 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg + .is_ok_and(|reg| reg .client_extension_results .cred_props - .map_or(false, |props| props.rk.unwrap_or_default()) + .is_some_and(|props| props.rk.unwrap_or_default()) && reg.client_extension_results.prf.is_none()) ); // `false` rk`. @@ -3216,11 +3258,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3233,10 +3275,10 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg + .is_ok_and(|reg| reg .client_extension_results .cred_props - .map_or(false, |props| props.rk.map_or(false, |rk| !rk)) + .is_some_and(|props| props.rk.is_some_and(|rk| !rk)) && reg.client_extension_results.prf.is_none()) ); // Invalid `rk`. @@ -3249,16 +3291,16 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { "credProps": { - "rk": 3 + "rk": 3u8 } }, "type": "public-key" @@ -3268,8 +3310,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `credProps` field. err = Error::unknown_field("Rk", ["rk"].as_slice()) @@ -3281,11 +3324,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3300,8 +3343,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field in `credProps`. err = Error::duplicate_field("rk").to_string().into_bytes(); @@ -3312,7 +3356,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -3332,8 +3376,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `enabled`. err = Error::invalid_type(Unexpected::Other("null"), &"a boolean") @@ -3345,11 +3390,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3364,8 +3409,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `enabled`. err = Error::missing_field("enabled").to_string().into_bytes(); @@ -3375,11 +3421,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3392,8 +3438,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `true` `enabled`. assert!( @@ -3402,11 +3449,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3419,14 +3466,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() && reg .client_extension_results .prf - .map_or(false, |prf| prf.enabled)) + .is_some_and(|prf| prf.enabled)) ); // `false` `enabled`. assert!( @@ -3435,11 +3479,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3452,14 +3496,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() && reg .client_extension_results .prf - .map_or(false, |prf| !prf.enabled)) + .is_some_and(|prf| !prf.enabled)) ); // Invalid `enabled`. err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") @@ -3471,16 +3512,16 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { "prf": { - "enabled": 3 + "enabled": 3u8 } }, "type": "public-key" @@ -3490,8 +3531,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `results` with `enabled` `true`. assert!( @@ -3500,11 +3542,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3518,14 +3560,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() && reg .client_extension_results .prf - .map_or(false, |prf| prf.enabled)) + .is_some_and(|prf| prf.enabled)) ); // `null` `results` with `enabled` `false`. err = Error::custom( @@ -3539,11 +3578,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3559,8 +3598,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field in `prf`. err = Error::duplicate_field("enabled").to_string().into_bytes(); @@ -3571,7 +3611,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -3591,8 +3631,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `first`. err = Error::missing_field("first").to_string().into_bytes(); @@ -3602,11 +3643,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3622,8 +3663,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `first`. assert!( @@ -3632,11 +3674,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3652,14 +3694,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() && reg .client_extension_results .prf - .map_or(false, |prf| prf.enabled)) + .is_some_and(|prf| prf.enabled)) ); // `null` `second`. assert!( @@ -3668,11 +3707,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3689,14 +3728,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.client_extension_results.cred_props.is_none() && reg .client_extension_results .prf - .map_or(false, |prf| prf.enabled)) + .is_some_and(|prf| prf.enabled)) ); // Non-`null` `first`. err = Error::invalid_type(Unexpected::Option, &"null") @@ -3708,11 +3744,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3730,8 +3766,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Non-`null` `second`. err = Error::invalid_type(Unexpected::Option, &"null") @@ -3743,11 +3780,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3766,8 +3803,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `prf` field. err = Error::unknown_field("Results", ["enabled", "results"].as_slice()) @@ -3779,11 +3817,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3799,8 +3837,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `results` field. err = Error::unknown_field("Second", ["first", "second"].as_slice()) @@ -3812,11 +3851,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3835,8 +3874,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field in `results`. err = Error::duplicate_field("first").to_string().into_bytes(); @@ -3847,7 +3887,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -3870,14 +3910,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 = [ + let mut att_obj: [u8; 178] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -4082,10 +4126,12 @@ mod tests { let enc_key = key.to_encoded_point(false); let pub_key = key.to_public_key_der().unwrap(); let att_obj_len = att_obj.len(); - att_obj[att_obj_len - 67..att_obj_len - 35] - .copy_from_slice(enc_key.x().unwrap().as_slice()); - att_obj[att_obj_len - 32..].copy_from_slice(enc_key.y().unwrap().as_slice()); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); + 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()); @@ -4096,11 +4142,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4109,20 +4155,20 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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()), + 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(); @@ -4132,11 +4178,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4147,8 +4193,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKeyAlgorithm`. err = Error::missing_field("publicKeyAlgorithm") @@ -4160,7 +4207,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -4174,8 +4221,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `publicKeyAlgorithm`. err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm") @@ -4187,7 +4235,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -4202,8 +4250,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `publicKey` mismatch. let bad_pub_key = P256PubKey::from_encoded_point(&P256Pt::from_affine_coordinates( @@ -4224,8 +4273,8 @@ mod tests { Unexpected::Bytes([0; 32].as_slice()), &format!( "DER-encoded public key to match the public key within the attestation object: P256(UncompressedP256PubKey({:?}, {:?}))", - &att_obj[att_obj.len() - 67..att_obj.len() - 35], - &att_obj[att_obj.len() - 32..], + &att_obj[x_start..y_meta_start], + &att_obj[y_start..], ) .as_str(), ) @@ -4235,11 +4284,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4248,8 +4297,8 @@ mod tests { .to_string() .as_str() ) - .unwrap_err().to_string().into_bytes()[..err.len()], - err + .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(); @@ -4259,10 +4308,10 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4273,8 +4322,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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") @@ -4286,11 +4336,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4301,14 +4351,22 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 = [ + let mut att_obj: [u8; 211] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -4547,11 +4605,13 @@ mod tests { let enc_key = key.to_encoded_point(false); let pub_key = key.to_public_key_der().unwrap(); let att_obj_len = att_obj.len(); - att_obj[att_obj_len - 99..att_obj_len - 51] - .copy_from_slice(enc_key.x().unwrap().as_slice()); - att_obj[att_obj_len - 48..].copy_from_slice(enc_key.y().unwrap().as_slice()); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 181..]); + 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. @@ -4561,11 +4621,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -35, + "publicKeyAlgorithm": -35i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4574,20 +4634,20 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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()), + 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(); @@ -4597,11 +4657,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4612,8 +4672,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKeyAlgorithm`. err = Error::missing_field("publicKeyAlgorithm") @@ -4625,7 +4686,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -4639,8 +4700,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `publicKeyAlgorithm`. err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm") @@ -4652,7 +4714,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -4667,8 +4729,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `publicKey` mismatch. let bad_pub_key = P384PubKey::from_encoded_point(&P384Pt::from_affine_coordinates( @@ -4691,8 +4754,8 @@ mod tests { Unexpected::Bytes([0; 32].as_slice()), &format!( "DER-encoded public key to match the public key within the attestation object: P384(UncompressedP384PubKey({:?}, {:?}))", - &att_obj[att_obj.len() - 99..att_obj.len() - 51], - &att_obj[att_obj.len() - 48..], + &att_obj[x_start..y_meta_start], + &att_obj[y_start..], ) .as_str(), ) @@ -4702,11 +4765,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), - "publicKeyAlgorithm": -35, + "publicKeyAlgorithm": -35i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4715,8 +4778,8 @@ mod tests { .to_string() .as_str() ) - .unwrap_err().to_string().into_bytes()[..err.len()], - err + .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!( @@ -4725,10 +4788,10 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -35, + "publicKeyAlgorithm": -35i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4741,7 +4804,7 @@ mod tests { ); // `publicKeyAlgorithm` mismatch when `publicKey` does not exist. err = Error::invalid_value( - Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()), + 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(); @@ -4751,10 +4814,10 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4765,8 +4828,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `publicKey` is allowed when not using EdDSA, ES256, or RS256. assert!( @@ -4775,11 +4839,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -35, + "publicKeyAlgorithm": -35i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4792,7 +4856,7 @@ mod tests { ); // `publicKeyAlgorithm` mismatch when `publicKey` is null. err = Error::invalid_value( - Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()), + 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(); @@ -4802,11 +4866,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4817,14 +4881,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 = [ + let mut att_obj: [u8; 374] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -5228,7 +5296,7 @@ mod tests { 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 = 65537u32; + 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, @@ -5281,10 +5349,13 @@ mod tests { .to_public_key(); let pub_key = key.to_public_key_der().unwrap(); let att_obj_len = att_obj.len(); - att_obj[att_obj_len - 261..att_obj_len - 5] - .copy_from_slice(key.n().to_bytes_be().as_slice()); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 343..]); + 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_bytes_be().as_slice()); + 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. @@ -5294,11 +5365,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -257, + "publicKeyAlgorithm": -257i16, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -5307,20 +5378,20 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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()), + 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(); @@ -5330,11 +5401,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -5345,8 +5416,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKeyAlgorithm`. err = Error::missing_field("publicKeyAlgorithm") @@ -5358,7 +5430,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -5372,8 +5444,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `publicKeyAlgorithm`. err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm") @@ -5385,7 +5458,7 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -5400,8 +5473,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `publicKey` mismatch. let bad_pub_key = RsaPrivateKey::from_components( @@ -5425,7 +5499,7 @@ mod tests { ] .as_slice(), ), - 65537u32.into(), + 0x0001_0001u32.into(), BigUint::from_bytes_le( [ 129, 93, 123, 251, 104, 29, 84, 203, 116, 100, 75, 237, 111, 160, 12, 100, 172, @@ -5481,7 +5555,8 @@ mod tests { 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[att_obj.len() - 261..att_obj.len() - 5], + // Correct and won't `panic`. + &att_obj[n_start_idx..e_meta_start_idx], ) .as_str(), ) @@ -5491,11 +5566,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), - "publicKeyAlgorithm": -257, + "publicKeyAlgorithm": -257i16, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -5504,8 +5579,8 @@ mod tests { .to_string() .as_str() ) - .unwrap_err().to_string().into_bytes()[..err.len()], - err + .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(); @@ -5515,10 +5590,10 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -257, + "publicKeyAlgorithm": -257i16, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -5529,8 +5604,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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") @@ -5542,11 +5618,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -257, + "publicKeyAlgorithm": -257i16, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -5557,8 +5633,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 @@ -442,7 +442,7 @@ mod tests { }, CustomRegistration, RegistrationRelaxed, }; - use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey}; + use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey as _}; use p256::{ EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key, elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, @@ -451,14 +451,21 @@ mod tests { use rsa::{ BigUint, RsaPrivateKey, sha2::{Digest as _, Sha256}, - traits::PublicKeyParts, + 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 = [ + let att_obj: [u8; 143] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -618,8 +625,10 @@ mod tests { .unwrap() .to_public_key_der() .unwrap(); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 113..]); + 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. @@ -629,11 +638,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "authenticatorAttachment": "cross-platform", @@ -643,28 +652,28 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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("ABABABABABABABABABABAA".as_bytes()) + base64url_nopad::decode(b"ABABABABABABABABABABAA") .unwrap() .as_slice(), ), - &format!("id and rawId to match: CredentialId({:?})", [0; 16]).as_str(), + &format!("id and rawId to match: CredentialId({:?})", [0u8; 16]).as_str(), ) .to_string() .into_bytes(); @@ -674,11 +683,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "ABABABABABABABABABABAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -689,29 +698,30 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // missing `id`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `id`. err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") @@ -727,7 +737,7 @@ mod tests { "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -738,29 +748,30 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `rawId`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `rawId`. err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId") @@ -772,11 +783,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": null, "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -787,18 +798,19 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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("ABABABABABABABABABABAA".as_bytes()) + ::decode(b"ABABABABABABABABABABAA") .unwrap() .as_slice(), ), - &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", [0; 16]).as_str(), + &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!( @@ -807,11 +819,11 @@ mod tests { "id": "ABABABABABABABABABABAA", "rawId": "ABABABABABABABABABABAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -822,16 +834,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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[att_obj.len() - 113..]); - bad_auth[113 - 32..].copy_from_slice([0; 32].as_slice()); + 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(), + &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!( @@ -840,11 +854,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": base64url_nopad::encode(bad_auth.as_slice()), "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -855,55 +869,56 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `authenticatorData`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null `authenticatorData`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "authenticatorData": null, "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `publicKeyAlgorithm` mismatch. err = Error::invalid_value( - Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()), + 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(); @@ -913,11 +928,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -928,17 +943,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKeyAlgorithm`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -948,18 +964,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `publicKeyAlgorithm`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -970,16 +986,16 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .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..], + &att_obj[att_obj_len - 32..], ) .as_str(), ) @@ -989,11 +1005,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "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": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1002,72 +1018,72 @@ mod tests { .to_string() .as_str() ) - .unwrap_err().to_string().into_bytes()[..err.len()], - err + .unwrap_err().to_string().into_bytes().get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKey`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `publicKey`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Missing `transports`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate `transports` are allowed. assert!( @@ -1076,11 +1092,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": ["usb", "usb"], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1089,29 +1105,29 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg.0.response.transports.count() == 1) + .is_ok_and(|reg| reg.0.response.transports.count() == 1) ); // `null` `transports`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": null, "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Unknown `transports`. err = Error::invalid_value( @@ -1126,11 +1142,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": ["Usb"], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1141,8 +1157,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `authenticatorAttachment`. assert!( @@ -1151,11 +1168,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "authenticatorAttachment": null, @@ -1165,7 +1182,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| matches!( + .is_ok_and(|reg| matches!( reg.0.authenticator_attachment, AuthenticatorAttachment::None )) @@ -1183,11 +1200,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "authenticatorAttachment": "Platform", @@ -1199,8 +1216,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientDataJSON`. err = Error::missing_field("clientDataJSON") @@ -1215,7 +1233,7 @@ mod tests { "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1226,8 +1244,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientDataJSON`. err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") @@ -1243,7 +1262,7 @@ mod tests { "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1254,8 +1273,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `attestationObject`. err = Error::missing_field("attestationObject") @@ -1267,11 +1287,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, }, "clientExtensionResults": {}, "type": "public-key" @@ -1281,8 +1301,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `attestationObject`. err = Error::invalid_type( @@ -1297,11 +1318,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": null, }, "clientExtensionResults": {}, @@ -1312,8 +1333,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `response`. err = Error::missing_field("response").to_string().into_bytes(); @@ -1330,8 +1352,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `response`. err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation") @@ -1351,8 +1374,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty `response`. err = Error::missing_field("clientDataJSON") @@ -1372,72 +1396,73 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientExtensionResults`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `clientExtensionResults`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": null, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Missing `type`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `type`. err = Error::invalid_type(Unexpected::Other("null"), &"public-key") @@ -1449,11 +1474,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1464,8 +1489,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") @@ -1477,11 +1503,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -1492,8 +1518,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null`. err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential") @@ -1505,8 +1532,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty. err = Error::missing_field("response").to_string().into_bytes(); @@ -1514,21 +1542,22 @@ mod tests { serde_json::from_str::<RegistrationRelaxed>(serde_json::json!({}).to_string().as_str()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field in `response`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, "foo": true, }, @@ -1536,9 +1565,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field in `response`. err = Error::duplicate_field("transports") @@ -1551,7 +1580,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -1568,21 +1597,22 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field in `PublicKeyCredential`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj }, "clientExtensionResults": {}, @@ -1590,9 +1620,9 @@ mod tests { "foo": true, }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field in `PublicKeyCredential`. err = Error::duplicate_field("id").to_string().into_bytes(); @@ -1604,7 +1634,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -1620,8 +1650,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Base case is correct. assert!( @@ -1629,7 +1660,7 @@ mod tests { serde_json::json!({ "attestationObject": b64_aobj, "authenticatorAttachment": "cross-platform", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "clientExtensionResults": {}, "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"], "type": "public-key" @@ -1637,19 +1668,19 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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(); @@ -1658,7 +1689,7 @@ mod tests { serde_json::json!({ "attestationObject": b64_aobj, "authenticatorAttachment": "cross-platform", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "clientExtensionResults": {}, "type": "public-key" }) @@ -1667,8 +1698,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate `transports` are allowed. assert!( @@ -1676,7 +1708,7 @@ mod tests { serde_json::json!({ "attestationObject": b64_aobj, "authenticatorAttachment": "cross-platform", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "clientExtensionResults": {}, "transports": ["usb", "usb"], "type": "public-key" @@ -1684,7 +1716,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg.0.response.transports.count() == 1) + .is_ok_and(|reg| reg.0.response.transports.count() == 1) ); // `null` `transports`. err = Error::invalid_type(Unexpected::Other("null"), &"AuthTransports") @@ -1693,7 +1725,7 @@ mod tests { assert_eq!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": null, "attestationObject": b64_aobj, "clientExtensionResults": {}, @@ -1704,8 +1736,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `transports`. err = Error::invalid_value( @@ -1719,7 +1752,7 @@ mod tests { serde_json::json!({ "attestationObject": b64_aobj, "authenticatorAttachment": "cross-platform", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "clientExtensionResults": {}, "transports": ["Usb"], "type": "public-key" @@ -1729,8 +1762,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `authenticatorAttachment`. assert!( @@ -1738,7 +1772,7 @@ mod tests { serde_json::json!({ "attestationObject": b64_aobj, "authenticatorAttachment": null, - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "clientExtensionResults": {}, "transports": [], "type": "public-key" @@ -1746,7 +1780,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| matches!( + .is_ok_and(|reg| matches!( reg.0.authenticator_attachment, AuthenticatorAttachment::None )) @@ -1763,7 +1797,7 @@ mod tests { serde_json::json!({ "attestationObject": b64_aobj, "authenticatorAttachment": "Platform", - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "clientExtensionResults": {}, "transports": [], "type": "public-key" @@ -1773,8 +1807,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientDataJSON`. err = Error::missing_field("clientDataJSON") @@ -1793,8 +1828,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientDataJSON`. err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data") @@ -1812,8 +1848,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `attestationObject`. err = Error::missing_field("attestationObject") @@ -1822,7 +1859,7 @@ mod tests { assert_eq!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "clientExtensionResults": {}, "type": "public-key" @@ -1832,8 +1869,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `attestationObject`. err = Error::invalid_type( @@ -1845,7 +1883,7 @@ mod tests { assert_eq!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "attestationObject": null, "clientExtensionResults": {}, @@ -1856,8 +1894,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `clientExtensionResults`. err = Error::missing_field("clientExtensionResults") @@ -1866,7 +1905,7 @@ mod tests { assert_eq!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "attestationObject": b64_aobj, "type": "public-key" @@ -1876,8 +1915,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `clientExtensionResults`. err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs") @@ -1886,7 +1926,7 @@ mod tests { assert_eq!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "attestationObject": b64_aobj, "clientExtensionResults": null, @@ -1897,22 +1937,23 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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, + "clientDataJSON": b64_cdata_json, "clientExtensionResults": {}, "transports": [] }) .to_string() .as_str() ) - .map_or(false, |_| true) + .is_ok_and(|_| true) ); // `null` `type`. err = Error::invalid_type(Unexpected::Other("null"), &"public-key") @@ -1922,7 +1963,7 @@ mod tests { serde_json::from_str::<CustomRegistration>( serde_json::json!({ "attestationObject": b64_aobj, - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "clientExtensionResults": {}, "transports": [], "type": null @@ -1932,8 +1973,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Not exactly `public-type` `type`. err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key") @@ -1942,7 +1984,7 @@ mod tests { assert_eq!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "attestationObject": b64_aobj, "clientExtensionResults": {}, @@ -1953,8 +1995,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null`. err = Error::invalid_type(Unexpected::Other("null"), &"CustomRegistration") @@ -1966,8 +2009,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Empty. err = Error::missing_field("attestationObject") @@ -1977,8 +2021,9 @@ mod tests { serde_json::from_str::<CustomRegistration>(serde_json::json!({}).to_string().as_str()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown field. err = Error::unknown_field( @@ -1998,7 +2043,7 @@ mod tests { assert_eq!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "attestationObject": b64_aobj, "foo": true, @@ -2010,8 +2055,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field. err = Error::duplicate_field("transports") @@ -2021,7 +2067,7 @@ mod tests { serde_json::from_str::<CustomRegistration>( format!( "{{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"transports\": [], \"attestationObject\": \"{b64_aobj}\", \"transports\": [] @@ -2033,14 +2079,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 = [ + let att_obj: [u8; 143] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -2200,8 +2250,10 @@ mod tests { .unwrap() .to_public_key_der() .unwrap(); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 113..]); + 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. @@ -2211,11 +2263,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -2224,19 +2276,19 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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!( @@ -2245,11 +2297,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2260,11 +2312,7 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() && reg.0.client_extension_results.prf.is_none()) ); // `null` `prf`. @@ -2274,11 +2322,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2289,25 +2337,21 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() && reg.0.client_extension_results.prf.is_none()) ); // Unknown `clientExtensionResults`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2318,9 +2362,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field. let mut err = Error::duplicate_field("credProps").to_string().into_bytes(); @@ -2331,7 +2375,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -2349,8 +2393,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `rk`. assert!( @@ -2359,11 +2404,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2376,11 +2421,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg + .is_ok_and(|reg| reg .0 .client_extension_results .cred_props - .map_or(false, |props| props.rk.is_none()) + .is_some_and(|props| props.rk.is_none()) && reg.0.client_extension_results.prf.is_none()) ); // Missing `rk`. @@ -2390,11 +2435,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2405,11 +2450,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg + .is_ok_and(|reg| reg .0 .client_extension_results .cred_props - .map_or(false, |props| props.rk.is_none()) + .is_some_and(|props| props.rk.is_none()) && reg.0.client_extension_results.prf.is_none()) ); // `true` rk`. @@ -2419,11 +2464,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2436,11 +2481,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg + .is_ok_and(|reg| reg .0 .client_extension_results .cred_props - .map_or(false, |props| props.rk.unwrap_or_default()) + .is_some_and(|props| props.rk.unwrap_or_default()) && reg.0.client_extension_results.prf.is_none()) ); // `false` rk`. @@ -2450,11 +2495,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2467,11 +2512,11 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg + .is_ok_and(|reg| reg .0 .client_extension_results .cred_props - .map_or(false, |props| props.rk.map_or(false, |rk| !rk)) + .is_some_and(|props| props.rk.is_some_and(|rk| !rk)) && reg.0.client_extension_results.prf.is_none()) ); // Invalid `rk`. @@ -2484,16 +2529,16 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { "credProps": { - "rk": 3 + "rk": 3u8 } }, "type": "public-key" @@ -2503,21 +2548,22 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `credProps` field. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2528,9 +2574,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field in `credProps`. err = Error::duplicate_field("rk").to_string().into_bytes(); @@ -2541,7 +2587,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -2561,8 +2607,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `enabled`. err = Error::invalid_type(Unexpected::Other("null"), &"a boolean") @@ -2574,11 +2621,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2593,8 +2640,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `enabled`. err = Error::missing_field("enabled").to_string().into_bytes(); @@ -2604,11 +2652,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2621,8 +2669,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `true` `enabled`. assert!( @@ -2631,11 +2680,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2648,16 +2697,12 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() && reg .0 .client_extension_results .prf - .map_or(false, |prf| prf.enabled)) + .is_some_and(|prf| prf.enabled)) ); // `false` `enabled`. assert!( @@ -2666,11 +2711,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2683,16 +2728,12 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() && reg .0 .client_extension_results .prf - .map_or(false, |prf| !prf.enabled)) + .is_some_and(|prf| !prf.enabled)) ); // Invalid `enabled`. err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean") @@ -2704,16 +2745,16 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { "prf": { - "enabled": 3 + "enabled": 3u8 } }, "type": "public-key" @@ -2723,8 +2764,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `results` with `enabled` `true`. assert!( @@ -2733,11 +2775,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2751,16 +2793,12 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() && reg .0 .client_extension_results .prf - .map_or(false, |prf| prf.enabled)) + .is_some_and(|prf| prf.enabled)) ); // `null` `results` with `enabled` `false`. err = Error::custom( @@ -2774,11 +2812,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2794,8 +2832,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Duplicate field in `prf`. err = Error::duplicate_field("enabled").to_string().into_bytes(); @@ -2806,7 +2845,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -2826,21 +2865,22 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `first`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2852,9 +2892,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `first`. assert!( @@ -2863,11 +2903,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2883,16 +2923,12 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() && reg .0 .client_extension_results .prf - .map_or(false, |prf| prf.enabled)) + .is_some_and(|prf| prf.enabled)) ); // `null` `second`. assert!( @@ -2901,11 +2937,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2922,16 +2958,12 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |reg| reg - .0 - .client_extension_results - .cred_props - .is_none() + .is_ok_and(|reg| reg.0.client_extension_results.cred_props.is_none() && reg .0 .client_extension_results .prf - .map_or(false, |prf| prf.enabled)) + .is_some_and(|prf| prf.enabled)) ); // Non-`null` `first`. err = Error::invalid_type(Unexpected::Option, &"null") @@ -2943,11 +2975,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -2965,8 +2997,9 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Non-`null` `second`. err = Error::invalid_type(Unexpected::Option, &"null") @@ -2978,11 +3011,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3001,21 +3034,22 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Unknown `prf` field. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3027,22 +3061,22 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Unknown `results` field. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": { @@ -3057,9 +3091,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Duplicate field in `results`. err = Error::duplicate_field("first").to_string().into_bytes(); @@ -3070,7 +3104,7 @@ mod tests { \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\", \"response\": {{ - \"clientDataJSON\": \"{b64_cdata}\", + \"clientDataJSON\": \"{b64_cdata_json}\", \"authenticatorData\": \"{b64_adata}\", \"transports\": [], \"publicKey\": \"{b64_key}\", @@ -3093,14 +3127,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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 = [ + let mut att_obj: [u8; 178] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -3305,11 +3343,13 @@ mod tests { let enc_key = key.to_encoded_point(false); let pub_key = key.to_public_key_der().unwrap(); let att_obj_len = att_obj.len(); - att_obj[att_obj_len - 67..att_obj_len - 35] - .copy_from_slice(enc_key.x().unwrap().as_slice()); - att_obj[att_obj_len - 32..].copy_from_slice(enc_key.y().unwrap().as_slice()); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 148..]); + 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. @@ -3319,11 +3359,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -3332,23 +3372,23 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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()), + 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(); @@ -3358,11 +3398,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -3373,17 +3413,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKeyAlgorithm`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -3393,18 +3434,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `publicKeyAlgorithm`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -3415,9 +3456,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `publicKey` mismatch. let bad_pub_key = P256PubKey::from_encoded_point(&P256Pt::from_affine_coordinates( @@ -3438,8 +3479,8 @@ mod tests { Unexpected::Bytes([0; 32].as_slice()), &format!( "DER-encoded public key to match the public key within the attestation object: P256(UncompressedP256PubKey({:?}, {:?}))", - &att_obj[att_obj.len() - 67..att_obj.len() - 35], - &att_obj[att_obj.len() - 32..], + &att_obj[x_start..y_meta_start], + &att_obj[y_start..], ) .as_str(), ) @@ -3449,11 +3490,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -3462,57 +3503,57 @@ mod tests { .to_string() .as_str() ) - .unwrap_err().to_string().into_bytes()[..err.len()], - err + .unwrap_err().to_string().into_bytes().get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKey`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `publicKey`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Base case is valid. assert!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "attestationObject": b64_aobj, "clientExtensionResults": {}, @@ -3521,25 +3562,28 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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 = [ + let mut att_obj: [u8; 211] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -3778,11 +3822,13 @@ mod tests { let enc_key = key.to_encoded_point(false); let pub_key = key.to_public_key_der().unwrap(); let att_obj_len = att_obj.len(); - att_obj[att_obj_len - 99..att_obj_len - 51] - .copy_from_slice(enc_key.x().unwrap().as_slice()); - att_obj[att_obj_len - 48..].copy_from_slice(enc_key.y().unwrap().as_slice()); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 181..]); + 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. @@ -3792,11 +3838,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -35, + "publicKeyAlgorithm": -35i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -3805,23 +3851,23 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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()), + 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(); @@ -3831,11 +3877,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -7, + "publicKeyAlgorithm": -7i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -3846,17 +3892,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKeyAlgorithm`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -3866,18 +3913,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `publicKeyAlgorithm`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -3888,9 +3935,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `publicKey` mismatch. let bad_pub_key = P384PubKey::from_encoded_point(&P384Pt::from_affine_coordinates( @@ -3913,8 +3960,8 @@ mod tests { Unexpected::Bytes([0; 32].as_slice()), &format!( "DER-encoded public key to match the public key within the attestation object: P384(UncompressedP384PubKey({:?}, {:?}))", - &att_obj[att_obj.len() - 99..att_obj.len() - 51], - &att_obj[att_obj.len() - 48..], + &att_obj[x_start..y_meta_start], + &att_obj[y_start..], ) .as_str(), ) @@ -3924,41 +3971,41 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), - "publicKeyAlgorithm": -35, + "publicKeyAlgorithm": -35i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }).to_string().as_str() - ).unwrap_err().to_string().into_bytes()[..err.len()], err); + ).unwrap_err().to_string().into_bytes().get(..err.len()), Some(err.as_slice())); // Missing `publicKey`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -35, + "publicKeyAlgorithm": -35i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `publicKeyAlgorithm` mismatch when `publicKey` does not exist. err = Error::invalid_value( - Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), + 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(); @@ -3968,10 +4015,10 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -3982,34 +4029,35 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `publicKey`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -35, + "publicKeyAlgorithm": -35i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `publicKeyAlgorithm` mismatch when `publicKey` is null. err = Error::invalid_value( - Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()), + 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(); @@ -4019,11 +4067,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4034,14 +4082,15 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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, + "clientDataJSON": b64_cdata_json, "transports": [], "attestationObject": b64_aobj, "clientExtensionResults": {}, @@ -4050,25 +4099,28 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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 = [ + let mut att_obj: [u8; 374] = [ cbor::MAP_3, cbor::TEXT_3, b'f', @@ -4472,7 +4524,7 @@ mod tests { 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 = 65537u32; + 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, @@ -4525,10 +4577,11 @@ mod tests { .to_public_key(); let pub_key = key.to_public_key_der().unwrap(); let att_obj_len = att_obj.len(); - att_obj[att_obj_len - 261..att_obj_len - 5] - .copy_from_slice(key.n().to_bytes_be().as_slice()); - let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes()); - let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 343..]); + 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_bytes_be().as_slice()); + 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. @@ -4538,11 +4591,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -257, + "publicKeyAlgorithm": -257i16, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4551,23 +4604,23 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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()), + 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(); @@ -4577,11 +4630,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, - "publicKeyAlgorithm": -8, + "publicKeyAlgorithm": -8i8, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4592,17 +4645,18 @@ mod tests { ) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKeyAlgorithm`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -4612,18 +4666,18 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `publicKeyAlgorithm`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": b64_key, @@ -4634,9 +4688,9 @@ mod tests { "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `publicKey` mismatch. let bad_pub_key = RsaPrivateKey::from_components( @@ -4660,7 +4714,7 @@ mod tests { ] .as_slice(), ), - 65537u32.into(), + 0x0001_0001u32.into(), BigUint::from_bytes_le( [ 129, 93, 123, 251, 104, 29, 84, 203, 116, 100, 75, 237, 111, 160, 12, 100, 172, @@ -4716,7 +4770,7 @@ mod tests { 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[att_obj.len() - 261..att_obj.len() - 5], + &att_obj[n_start..e_start], ) .as_str(), ) @@ -4726,11 +4780,11 @@ mod tests { "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), - "publicKeyAlgorithm": -257, + "publicKeyAlgorithm": -257i16, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, @@ -4739,57 +4793,57 @@ mod tests { .to_string() .as_str() ) - .unwrap_err().to_string().into_bytes()[..err.len()], - err + .unwrap_err().to_string().into_bytes().get(..err.len()), + Some(err.as_slice()) ); // Missing `publicKey`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], - "publicKeyAlgorithm": -257, + "publicKeyAlgorithm": -257i16, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // `null` `publicKey`. - assert!( + drop( serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ "id": "AAAAAAAAAAAAAAAAAAAAAA", "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "authenticatorData": b64_adata, "transports": [], "publicKey": null, - "publicKeyAlgorithm": -257, + "publicKeyAlgorithm": -257i16, "attestationObject": b64_aobj, }, "clientExtensionResults": {}, "type": "public-key" }) .to_string() - .as_str() + .as_str(), ) - .is_ok() + .unwrap(), ); // Base case is valid. assert!( serde_json::from_str::<CustomRegistration>( serde_json::json!({ - "clientDataJSON": b64_cdata, + "clientDataJSON": b64_cdata_json, "transports": [], "attestationObject": b64_aobj, "clientExtensionResults": {}, @@ -4798,19 +4852,19 @@ mod tests { .to_string() .as_str() ) - .map_or(false, |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()).as_slice() - && 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()) + .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/ser.rs b/src/response/ser.rs @@ -861,15 +861,15 @@ where /// # #[cfg(feature = "bin")] /// # use webauthn_rp::bin::Decode; /// # use webauthn_rp::{ - /// # request::{register::{Nickname, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MIN_LEN, Username}, AsciiDomain, RpId}, + /// # request::{register::{DisplayName, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MIN_LEN, Username}, AsciiDomain, RpId}, /// # response::CurrentUserDetailsOptions, /// # AggErr, /// # }; /// /// Retrieves the `PublicKeyCredentialUserEntity` info associated with `user_id` from the database. /// # #[cfg(feature = "bin")] - /// fn get_user_info(user_id: UserHandle<USER_HANDLE_MIN_LEN>) -> Result<(Username<'static>, Option<Nickname<'static>>), AggErr> { + /// fn get_user_info(user_id: UserHandle<USER_HANDLE_MIN_LEN>) -> Result<(Username<'static>, DisplayName<'static>), AggErr> { /// // ⋮ - /// # Ok((Username::decode("foo").unwrap(), Some(Nickname::decode("foo").unwrap()))) + /// # Ok((Username::decode("foo").unwrap(), DisplayName::decode("foo").unwrap())) /// } /// /// Retrieves the `UserHandle` from a session cookie. /// # #[cfg(feature = "custom")] diff --git a/src/response/ser_relaxed.rs b/src/response/ser_relaxed.rs @@ -17,11 +17,9 @@ use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpec #[cfg(doc)] use serde_json::de; /// Category returned by [`SerdeJsonErr::classify`]. -#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] pub use serde_json::error::Category; /// Error returned by [`CollectedClientData::from_client_data_json_relaxed`] or any of the [`Deserialize`] /// implementations when relying on [`de::Deserializer`] or [`de::StreamDeserializer`]. -#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] pub use serde_json::error::Error as SerdeJsonErr; /// "Relaxed" [`ClientDataJsonParser`]. /// @@ -290,23 +288,13 @@ impl<'de: 'a, 'a, const R: bool> Visitor<'de> for RelaxedHelper<'a, R> { { Ok(OriginWrapper(Origin(String::from_utf8_lossy(v)))) } - #[expect(unsafe_code, reason = "safety comment justifies its use")] fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E> where E: Error, { - Ok(OriginWrapper(Origin( - match String::from_utf8_lossy(v.as_slice()) { - Cow::Borrowed(_) => { - // SAFETY: - // `String::from_utf8_lossy` returns `Cow::Borrowed` iff the input was valid - // UTF-8. - let val = unsafe { String::from_utf8_unchecked(v) }; - Cow::Owned(val) - } - Cow::Owned(val) => Cow::Owned(val), - }, - ))) + Ok(OriginWrapper(Origin(Cow::Owned( + String::from_utf8_lossy(v.as_slice()).into_owned(), + )))) } fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> where @@ -422,13 +410,16 @@ impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfValuesRelaxed { } #[cfg(test)] mod tests { - use super::{ClientDataJsonParser, Cow, RelaxedClientDataJsonParser}; + 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 input = serde_json::json!({ + let mut input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -437,21 +428,21 @@ mod tests { }) .to_string(); assert!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).map_or(false, |c| { + 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.map_or( - false, + && c.top_origin.is_some_and( |t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org"), ) }) ); // Base case is correct. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": "https://example.com", @@ -460,21 +451,21 @@ mod tests { }) .to_string(); assert!( - RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).map_or(false, |c| { + 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.map_or( - false, + && c.top_origin.is_some_and( |t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org"), ) }) ); // Unknown keys are allowed. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -483,9 +474,9 @@ mod tests { "foo": true }) .to_string(); - assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok()); + drop(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).unwrap()); // Duplicate keys are forbidden. - let input = "{ + let mut input_str = "{ \"challenge\": \"ABABABABABABABABABABAA\", \"type\": \"webauthn.create\", \"origin\": \"https://example.com\", @@ -497,14 +488,15 @@ mod tests { .to_string() .into_bytes(); assert_eq!( - RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) + RelaxedClientDataJsonParser::<true>::parse(input_str.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `crossOrigin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -514,10 +506,10 @@ mod tests { .to_string(); assert!( RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .map_or(false, |c| !c.cross_origin) + .is_ok_and(|c| !c.cross_origin) ); // Missing `crossOrigin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -526,10 +518,10 @@ mod tests { .to_string(); assert!( RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .map_or(false, |c| !c.cross_origin) + .is_ok_and(|c| !c.cross_origin) ); // `null` `topOrigin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -539,10 +531,10 @@ mod tests { .to_string(); assert!( RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .map_or(false, |c| c.top_origin.is_none()) + .is_ok_and(|c| c.top_origin.is_none()) ); // Missing `topOrigin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -551,7 +543,7 @@ mod tests { .to_string(); assert!( RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) - .map_or(false, |c| c.top_origin.is_none()) + .is_ok_and(|c| c.top_origin.is_none()) ); // `null` `challenge`. err = Error::invalid_type( @@ -560,7 +552,7 @@ mod tests { ) .to_string() .into_bytes(); - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": null, "type": "webauthn.create", "origin": "https://example.com", @@ -572,12 +564,13 @@ mod tests { RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `challenge`. err = Error::missing_field("challenge").to_string().into_bytes(); - let input = serde_json::json!({ + input = serde_json::json!({ "type": "webauthn.create", "origin": "https://example.com", "crossOrigin": true, @@ -588,8 +581,9 @@ mod tests { RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `type`. err = Error::invalid_type( @@ -598,7 +592,7 @@ mod tests { ) .to_string() .into_bytes(); - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": null, "origin": "https://example.com", @@ -610,12 +604,13 @@ mod tests { RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `type`. err = Error::missing_field("type").to_string().into_bytes(); - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "origin": "https://example.com", "crossOrigin": true, @@ -626,14 +621,15 @@ mod tests { RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `origin`. err = Error::invalid_type(Unexpected::Other("null"), &"OriginWrapper") .to_string() .into_bytes(); - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": null, @@ -645,12 +641,13 @@ mod tests { RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `origin`. err = Error::missing_field("origin").to_string().into_bytes(); - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "crossOrigin": true, @@ -661,14 +658,15 @@ mod tests { RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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(); - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -680,14 +678,15 @@ mod tests { RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .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(); - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": "https://example.com", @@ -699,11 +698,12 @@ mod tests { RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `crossOrigin` can be `false` even when `topOrigin` exists. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": "https://example.com", @@ -711,40 +711,41 @@ mod tests { "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).is_ok()); + drop(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).unwrap()); // `crossOrigin` can be `true` even when `topOrigin` does not exist. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": "https://example.com", "crossOrigin": true, }) .to_string(); - assert!(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).is_ok()); + drop(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).unwrap()); // BOM is removed. - let input = "\u{feff}{ + input_str = "\u{feff}{ \"challenge\": \"ABABABABABABABABABABAA\", \"type\": \"webauthn.create\", \"origin\": \"https://example.com\", \"crossOrigin\": true, \"topOrigin\": \"https://example.org\" }"; - assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok()); + drop(RelaxedClientDataJsonParser::<true>::parse(input_str.as_bytes()).unwrap()); // Invalid Unicode is replaced. - let input = b"{ + 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.as_slice()).map_or(false, |c| { + 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. - let input = b"{ + input_bytes = b"{ \"challenge\": \"ABABABABABABABABABABAA\", \"type\": \"webauthn\\u002ecreate\", \"origin\": \"https://examp\\\\le.com\", @@ -752,15 +753,18 @@ mod tests { \"topOrigin\": \"https://example.org\" }"; assert!( - RelaxedClientDataJsonParser::<true>::parse(input.as_slice()).map_or(false, |c| { + 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 input = serde_json::json!({ + let mut input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -769,9 +773,9 @@ mod tests { }) .to_string(); assert!( - RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).map_or( - false, + 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, ]) @@ -779,7 +783,7 @@ mod tests { ) ); // Base case is correct. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": "https://example.com", @@ -788,9 +792,9 @@ mod tests { }) .to_string(); assert!( - RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).map_or( - false, + 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, ]) @@ -798,7 +802,7 @@ mod tests { ) ); // Unknown keys are allowed. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -807,9 +811,9 @@ mod tests { "foo": true }) .to_string(); - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); // Duplicate keys are ignored. - let input = "{ + let mut input_str = "{ \"challenge\": \"ABABABABABABABABABABAA\", \"type\": \"webauthn.create\", \"origin\": \"https://example.com\", @@ -817,9 +821,9 @@ mod tests { \"topOrigin\": \"https://example.org\", \"crossOrigin\": true }"; - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_str.as_bytes()).unwrap(); // `null` `crossOrigin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -827,18 +831,18 @@ mod tests { "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); // Missing `crossOrigin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); // `null` `topOrigin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -846,16 +850,16 @@ mod tests { "topOrigin": null }) .to_string(); - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); // Missing `topOrigin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", "crossOrigin": true, }) .to_string(); - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); // `null` `challenge`. let mut err = Error::invalid_type( Unexpected::Other("null"), @@ -863,7 +867,7 @@ mod tests { ) .to_string() .into_bytes(); - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": null, "type": "webauthn.create", "origin": "https://example.com", @@ -875,12 +879,13 @@ mod tests { RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // Missing `challenge`. err = Error::missing_field("challenge").to_string().into_bytes(); - let input = serde_json::json!({ + input = serde_json::json!({ "type": "webauthn.create", "origin": "https://example.com", "crossOrigin": true, @@ -891,11 +896,12 @@ mod tests { RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()) .unwrap_err() .to_string() - .into_bytes()[..err.len()], - err + .into_bytes() + .get(..err.len()), + Some(err.as_slice()) ); // `null` `type`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": null, "origin": "https://example.com", @@ -903,18 +909,18 @@ mod tests { "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); // Missing `type`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "origin": "https://example.com", "crossOrigin": true, "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); // `null` `origin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": null, @@ -922,18 +928,18 @@ mod tests { "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); // Missing `origin`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "crossOrigin": true, "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); // Mismatched `type`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.create", "origin": "https://example.com", @@ -941,9 +947,9 @@ mod tests { "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); // Mismatched `type`. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": "https://example.com", @@ -951,9 +957,9 @@ mod tests { "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).unwrap(); // `crossOrigin` can be `false` even when `topOrigin` exists. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": "https://example.com", @@ -961,42 +967,43 @@ mod tests { "topOrigin": "https://example.org" }) .to_string(); - assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); // `crossOrigin` can be `true` even when `topOrigin` does not exist. - let input = serde_json::json!({ + input = serde_json::json!({ "challenge": "ABABABABABABABABABABAA", "type": "webauthn.get", "origin": "https://example.com", "crossOrigin": true, }) .to_string(); - assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).unwrap(); // BOM is removed. - let input = "\u{feff}{ + input_str = "\u{feff}{ \"challenge\": \"ABABABABABABABABABABAA\", \"type\": \"webauthn.create\", \"origin\": \"https://example.com\", \"crossOrigin\": true, \"topOrigin\": \"https://example.org\" }"; - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_str.as_bytes()).unwrap(); // Invalid Unicode is replaced. - let input = b"{ + let mut input_bytes = b"{ \"challenge\": \"ABABABABABABABABABABAA\", \"type\": \"webauthn.create\", \"origin\": \"https://\xffexample.com\", \"crossOrigin\": true, \"topOrigin\": \"https://example.org\" - }"; - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_slice()).is_ok()); + }" + .as_slice(); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_bytes).unwrap(); // Escape characters are de-escaped. - let input = b"{ + input_bytes = b"{ \"challenge\": \"ABABABABABABABABABABAA\", \"type\": \"webauthn\\u002ecreate\", \"origin\": \"https://examp\\\\le.com\", \"crossOrigin\": true, \"topOrigin\": \"https://example.org\" }"; - assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_slice()).is_ok()); + _ = RelaxedClientDataJsonParser::<true>::get_sent_challenge(input_bytes).unwrap(); } }