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:
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();
}
}