commit 88c3f257fcf796e6642a254adbdb3c81cfc214f4
parent 385433ad3d5255e8a8efdb90b2ccbb8befec3b2f
Author: Zack Newman <zack@philomathiclife.com>
Date: Fri, 23 May 2025 13:14:22 -0600
new hashset. improve prf
Diffstat:
26 files changed, 3623 insertions(+), 1211 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -9,27 +9,74 @@ license = "MIT OR Apache-2.0"
name = "webauthn_rp"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/webauthn_rp/"
-rust-version = "1.86.0"
-version = "0.3.0"
+rust-version = "1.87.0"
+version = "0.4.0"
+
+[lints.rust]
+unknown_lints = { level = "deny", priority = -1 }
+future_incompatible = { level = "deny", priority = -1 }
+let_underscore = { level = "deny", priority = -1 }
+missing_docs = { level = "deny", priority = -1 }
+nonstandard_style = { level = "deny", priority = -1 }
+refining_impl_trait = { level = "deny", priority = -1 }
+rust_2018_compatibility = { level = "deny", priority = -1 }
+rust_2018_idioms = { level = "deny", priority = -1 }
+rust_2021_compatibility = { level = "deny", priority = -1 }
+rust_2024_compatibility = { level = "deny", priority = -1 }
+unsafe_code = { level = "deny", priority = -1 }
+unused = { 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 }
+nursery = { level = "deny", priority = -1 }
+pedantic = { level = "deny", priority = -1 }
+perf = { level = "deny", priority = -1 }
+restriction = { level = "deny", priority = -1 }
+style = { level = "deny", priority = -1 }
+suspicious = { level = "deny", priority = -1 }
+# Noisy, opinionated, and likely don't prevent bugs or improve APIs.
+arbitrary_source_item_ordering = "allow"
+blanket_clippy_restriction_lints = "allow"
+exhaustive_enums = "allow"
+exhaustive_structs = "allow"
+implicit_return = "allow"
+min_ident_chars = "allow"
+missing_trait_methods = "allow"
+module_name_repetitions = "allow"
+multiple_crate_versions = "allow"
+pub_with_shorthand = "allow"
+pub_use = "allow"
+question_mark_used = "allow"
+ref_patterns = "allow"
+return_and_then = "allow"
+self_named_module_files = "allow"
+single_call_fn = "allow"
+single_char_lifetime_names = "allow"
+unseparated_literal_suffix = "allow"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
-data-encoding = { version = "2.8.0", default-features = false }
+data-encoding = { version = "2.9.0", default-features = false }
ed25519-dalek = { version = "2.1.1", default-features = false, features = ["fast"] }
+hashbrown = { version = "0.15.3", 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 }
-rand = { version = "0.9.0", default-features = false, features = ["thread_rng"] }
+rand = { version = "0.9.1", 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.140", default-features = false, features = ["alloc"], optional = true }
url = { version = "2.5.4", default-features = false }
[dev-dependencies]
-data-encoding = { version = "2.8.0", default-features = false, features = ["alloc"] }
+data-encoding = { version = "2.9.0", default-features = false, features = ["alloc"] }
ed25519-dalek = { version = "2.1.1", 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"] }
diff --git a/README.md b/README.md
@@ -17,16 +17,18 @@ having said that, there are pre-defined serialization formats for "common" deplo
## `webauthn_rp` in action
```rust
+use core::convert;
use webauthn_rp::{
- AuthenticatedCredential, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
- DiscoverableCredentialRequestOptions, PublicKeyCredentialCreationOptions, RegisteredCredential,
- Registration, RegistrationServerState,
+ AuthenticatedCredential64, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
+ DiscoverableCredentialRequestOptions, PublicKeyCredentialCreationOptions64, RegisteredCredential64,
+ Registration, RegistrationServerState64,
+ hash::hash_set::FixedCapHashSet,
request::{
AsciiDomain, PublicKeyCredentialDescriptor, RpId,
auth::AuthenticationVerificationOptions,
register::{
- Nickname, PublicKeyCredentialUserEntity, RegistrationVerificationOptions,
- USER_HANDLE_MAX_LEN, UserHandle64, Username,
+ Nickname, PublicKeyCredentialUserEntity64, RegistrationVerificationOptions,
+ UserHandle64, Username,
},
},
response::{
@@ -35,8 +37,6 @@ use webauthn_rp::{
register::{CompressedPubKey, DynamicState, error::RegCeremonyErr},
},
};
-// These are available iff `serializable_server_state` is _not_ enabled.
-use webauthn_rp::request::{FixedCapHashSet, InsertResult};
use serde::de::{Deserialize, Deserializer};
use serde_json::Error as JsonErr;
/// The RP ID our application uses.
@@ -99,7 +99,7 @@ impl<'de: 'a + 'b, 'a, 'b> Deserialize<'de> for AccountReg<'a, 'b> {
/// already exist otherwise. This is similar to credential creation except a random `UserHandle64` is generated and
/// will be used for subsequent credential registrations.
fn start_account_creation(
- reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+ reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
) -> Result<Vec<u8>, AppErr> {
let rp_id = RpId::Domain(
AsciiDomain::try_from(RP_ID.to_owned())
@@ -107,19 +107,17 @@ fn start_account_creation(
);
let user_id = UserHandle64::new();
let (server, client) =
- PublicKeyCredentialCreationOptions::first_passkey_with_blank_user_info(
+ PublicKeyCredentialCreationOptions64::first_passkey_with_blank_user_info(
&rp_id, &user_id,
)
.start_ceremony()
.unwrap_or_else(|_e| {
- unreachable!("we don't manually mutate the options; thus this can't fail")
+ unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
});
- if matches!(
- reg_ceremonies.insert_or_replace_all_expired(server),
- InsertResult::Success
- ) {
+ if reg_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity)
+ {
Ok(serde_json::to_vec(&client)
- .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
+ .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState64::serialize")))
} else {
Err(AppErr::WebAuthnCeremonyCreation)
}
@@ -133,7 +131,7 @@ fn start_account_creation(
/// Note if this errors, then the user should be notified to delete the passkey created on their
/// authenticator.
fn finish_account_creation(
- reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+ reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
client_data: Vec<u8>,
) -> Result<(), AppErr> {
let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data.as_slice())?;
@@ -161,24 +159,22 @@ fn finish_account_creation(
/// the authenticator.
fn start_cred_registration(
user_id: &UserHandle64,
- reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+ reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
) -> Result<Vec<u8>, AppErr> {
let rp_id = RpId::Domain(
AsciiDomain::try_from(RP_ID.to_owned())
.unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
);
let (entity, creds) = select_user_info(user_id)?.ok_or_else(|| AppErr::NoAccount)?;
- let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, entity, creds)
+ let (server, client) = PublicKeyCredentialCreationOptions64::passkey(&rp_id, entity, creds)
.start_ceremony()
.unwrap_or_else(|_e| {
- unreachable!("we don't manually mutate the options; thus this won't error")
+ unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
});
- if matches!(
- reg_ceremonies.insert_or_replace_all_expired(server),
- InsertResult::Success
- ) {
+ if reg_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity)
+ {
Ok(serde_json::to_vec(&client)
- .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
+ .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState64::serialize")))
} else {
Err(AppErr::WebAuthnCeremonyCreation)
}
@@ -192,7 +188,7 @@ fn start_cred_registration(
/// Note if this errors, then the user should be notified to delete the passkey created on their
/// authenticator.
fn finish_cred_registration(
- reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+ reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
client_data: Vec<u8>,
) -> Result<(), AppErr> {
// `Registration::from_json_custom` is available iff `serde_relaxed` is enabled.
@@ -223,12 +219,10 @@ fn start_auth(
let (server, client) = DiscoverableCredentialRequestOptions::passkey(&rp_id)
.start_ceremony()
.unwrap_or_else(|_e| {
- unreachable!("we don't manually mutate the options; thus this won't error")
+ unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
});
- if matches!(
- auth_ceremonies.insert_or_replace_all_expired(server),
- InsertResult::Success
- ) {
+ if auth_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity)
+ {
Ok(serde_json::to_vec(&client).unwrap_or_else(|_e| {
unreachable!("bug in DiscoverableAuthenticationClientState::serialize")
}))
@@ -241,7 +235,7 @@ fn finish_auth(
auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
client_data: Vec<u8>,
) -> Result<(), AppErr> {
- // `Authentication::from_json_custom` is available iff `serde_relaxed` is enabled.
+ // `DiscoverableAuthentication64::from_json_custom` is available iff `serde_relaxed` is enabled.
let authentication =
DiscoverableAuthentication64::from_json_custom(client_data.as_slice())?;
let mut cred = select_credential(
@@ -250,7 +244,7 @@ fn finish_auth(
)?
.ok_or_else(|| AppErr::NoCredential)?;
if auth_ceremonies
- // `Authentication::challenge_relaxed` is available iff `serde_relaxed` is enabled.
+ // `DiscoverableAuthentication64::challenge_relaxed` is available iff `serde_relaxed` is enabled.
.take(&authentication.challenge_relaxed()?)
.ok_or(AppErr::MissingWebAuthnCeremony)?
.verify(
@@ -276,7 +270,7 @@ fn finish_auth(
/// `CredentialId`, or there already exists an account using the same `UserHandle64`.
fn insert_account(
account: &AccountReg<'_, '_>,
- cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
+ cred: RegisteredCredential64<'_>,
) -> Result<(), AppErr> {
// ⋮
}
@@ -289,7 +283,7 @@ fn select_user_info<'a>(
user_id: &'a UserHandle64,
) -> Result<
Option<(
- PublicKeyCredentialUserEntity<'static, 'static, 'a, USER_HANDLE_MAX_LEN>,
+ PublicKeyCredentialUserEntity64<'static, 'static, 'a>,
Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
)>,
AppErr,
@@ -300,9 +294,10 @@ fn select_user_info<'a>(
///
/// # Errors
///
-/// Errors iff writing `cred` errors or there already exists a credential using the same `CredentialId`.
+/// Errors iff writing `cred` errors, there already exists a credential using the same `CredentialId`,
+/// or there does not exist an account under the `UserHandle64`.
fn insert_credential(
- cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
+ cred: RegisteredCredential64<'_>,
) -> Result<(), AppErr> {
// ⋮
}
@@ -317,10 +312,9 @@ fn select_credential<'cred, 'user>(
user_id: &'user UserHandle64,
) -> Result<
Option<
- AuthenticatedCredential<
+ AuthenticatedCredential64<
'cred,
'user,
- USER_HANDLE_MAX_LEN,
CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>,
>,
>,
@@ -347,7 +341,6 @@ fn update_credential(
will occur.
### `bin`
-
Enables binary (de)serialization via `Encode` and `Decode`. Since registered credentials will almost always
have to be saved to persistent storage, _some_ form of (de)serialization is necessary. In the event `bin` is
unsuitable or only partially suitable (e.g., human-readable output is desired), one will need to enable
@@ -405,13 +398,14 @@ wants to accommodate non-conforming clients or clients that implement older vers
### `serializable_server_state`
Automatically enables [`bin`](#bin) in addition to `Encode` and `Decode` implementations for
-`RegistrationServerState` and `AuthenticationServerState`. Less accurate `SystemTime` is used instead of
-`Instant` for timeout enforcement. This should be enabled if you don't desire to use in-memory collections to
-store the instances of those types.
+`RegistrationServerState`, `DiscoverableAuthenticationServerState`, and
+`NonDiscoverableAuthenticationServerState`. Less accurate `SystemTime` is used instead of `Instant` for
+timeout enforcement. This should be enabled if you don't desire to use in-memory collections to store the instances
+of those types.
Note even when written to persistent storage, an application should still periodically remove expired ceremonies.
-If one is using a relational database (RDB); then one can achieve this by storing `ServerState::sent_challenge`,
-the `Vec` returned from `Encode::encode`, and `ServerState::expiration` and periodically remove all rows
+If one is using a relational database (RDB); then one can achieve this by storing `SentChallenge`,
+the `Vec` returned from `Encode::encode`, and `TimedCeremony::expiration` and periodically remove all rows
whose expiration exceeds the current date and time.
## Registration and authentication
@@ -435,13 +429,14 @@ It is for those reasons data like `RegistrationServerState` are not serializable
use of in-memory collections (e.g., `FixedCapHashSet`). 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 only 48 bytes on `x86_64-unknown-linux-gnu` platforms. To avoid issues
-related to incomplete ceremonies, RPs can periodically iterate the collection for expired ceremonies and remove
-such data. Other techniques can be employed as well to mitigate OOM, but they are application specific and
-out-of-scope. If this is undesirable, one can enable [`serializable_server_state`](#serializable_server_state)
-so that `RegistrationServerState` and `AuthenticationServerState` implement `Encode` and `Decode`. Another
-reason one may need to store this information persistently is for load-balancing purposes where the server that
-started the ceremony is not guaranteed to be the server that finishes the ceremony.
+`RegistrationServerState` instances are as small as 48 bytes on `x86_64-unknown-linux-gnu` platforms. To avoid
+issues related to incomplete ceremonies, RPs can periodically iterate the collection for expired ceremonies and
+remove such data. Other techniques can be employed as well to mitigate OOM, but they are application specific
+and out-of-scope. If this is undesirable, one can enable [`serializable_server_state`](#serializable_server_state)
+so that `RegistrationServerState`, `DiscoverableAuthenticationServerState`, and
+`NonDiscoverableAuthenticationServerState` implement `Encode` and `Decode`. Another reason one may need to
+store this information persistently is for load-balancing purposes where the server that started the ceremony is
+not guaranteed to be the server that finishes the ceremony.
## Supported signature algorithms
diff --git a/src/hash.rs b/src/hash.rs
@@ -0,0 +1,153 @@
+#[cfg(doc)]
+use super::{
+ hash::hash_set::FixedCapHashSet,
+ request::{
+ Challenge,
+ auth::{
+ DiscoverableAuthenticationServerState, DiscoverableCredentialRequestOptions,
+ NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
+ },
+ register::{PublicKeyCredentialCreationOptions, RegistrationServerState},
+ },
+};
+use core::hash::{BuildHasher, Hasher};
+pub use hashbrown;
+/// Fixed-capacity hash map.
+pub mod hash_map;
+/// Fixed-capacity hash set.
+pub mod hash_set;
+/// [`Hasher`] whose `write_*` methods simply store up to 64 bits of the passed argument _as is_ overwriting
+/// any previous state.
+///
+/// This is designed to only be used indirectly via a hash map whose keys are randomly generated on the server
+/// based on at least 64 bits—the size of the integer returned from [`Self::finish`]—of entropy.
+/// This makes this `Hasher` usable (and ideal) in only the most niche circumstances.
+///
+/// [`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
+/// 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
+/// only constructable via [`PublicKeyCredentialCreationOptions::start_ceremony`],
+/// [`DiscoverableCredentialRequestOptions::start_ceremony`], and
+/// [`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(Debug)]
+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`).
+impl Hasher for IdentityHasher {
+ /// Returns `0` if no `write_*` calls have been made; otherwise returns the result of the most recent
+ /// `write_*` call.
+ #[inline]
+ fn finish(&self) -> u64 {
+ self.0
+ }
+ /// Writes `i` to `self`.
+ #[inline]
+ fn write_u64(&mut self, i: u64) {
+ self.0 = i;
+ }
+ /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i8(&mut self, i: i8) {
+ self.write_u64(i as u64);
+ }
+ /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i16(&mut self, i: i16) {
+ self.write_u64(i as u64);
+ }
+ /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i32(&mut self, i: i32) {
+ self.write_u64(i as u64);
+ }
+ /// Redirects to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i64(&mut self, i: i64) {
+ self.write_u64(i as u64);
+ }
+ /// Truncates `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i128(&mut self, i: i128) {
+ 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) {
+ self.write_u64(u64::from(i));
+ }
+ /// Zero-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[inline]
+ fn write_u16(&mut self, i: u16) {
+ self.write_u64(u64::from(i));
+ }
+ /// Zero-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[inline]
+ fn write_u32(&mut self, i: u32) {
+ self.write_u64(u64::from(i));
+ }
+ /// Truncates `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_u128(&mut self, i: u128) {
+ 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")]
+ #[inline]
+ fn write(&mut self, bytes: &[u8]) {
+ if let Some(data) = bytes.get(..8) {
+ let mut val = [0; 8];
+ val.copy_from_slice(data);
+ self.write_u64(u64::from_ne_bytes(val));
+ }
+ }
+}
+/// [`BuildHasher`] of an [`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)]
+pub struct BuildIdentityHasher;
+impl BuildHasher for BuildIdentityHasher {
+ type Hasher = IdentityHasher;
+ #[inline]
+ fn build_hasher(&self) -> Self::Hasher {
+ IdentityHasher(0)
+ }
+}
diff --git a/src/hash/hash_map.rs b/src/hash/hash_map.rs
@@ -0,0 +1,364 @@
+use super::{super::request::TimedCeremony, BuildIdentityHasher};
+#[cfg(doc)]
+use core::hash::Hasher;
+use core::hash::{BuildHasher, Hash};
+use hashbrown::{
+ Equivalent,
+ 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`].
+///
+/// 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.,
+/// [`Self::insert_remove_all_expired`]) are provided that will automatically remove expired entries. Note the
+/// 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`].
+///
+/// [`Self::into`]: struct.FixedCapHashMap.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`.
+ #[inline]
+ #[must_use]
+ pub fn new(capacity: usize) -> Self {
+ Self(HashMap::with_capacity_and_hasher(
+ capacity,
+ BuildIdentityHasher,
+ ))
+ }
+}
+impl<K, V, S> FixedCapHashMap<K, V, S> {
+ /// [`HashMap::values_mut`].
+ #[inline]
+ pub fn values_mut(&mut self) -> ValuesMut<'_, K, V> {
+ self.0.values_mut()
+ }
+ /// [`HashMap::iter_mut`].
+ #[expect(
+ clippy::iter_without_into_iter,
+ reason = "re-export all mutable methods of HashMap"
+ )]
+ #[inline]
+ pub fn iter_mut(&mut self) -> IterMut<'_, K, V> {
+ self.0.iter_mut()
+ }
+ /// [`HashMap::clear`].
+ #[inline]
+ pub fn clear(&mut self) {
+ self.0.clear();
+ }
+ /// [`HashMap::drain`].
+ #[inline]
+ pub fn drain(&mut self) -> Drain<'_, K, V> {
+ self.0.drain()
+ }
+ /// [`HashMap::extract_if`].
+ #[inline]
+ 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`].
+ #[inline]
+ #[must_use]
+ pub fn with_hasher(capacity: usize, hasher: S) -> Self {
+ Self(HashMap::with_capacity_and_hasher(capacity, hasher))
+ }
+ /// [`HashMap::retain`].
+ #[inline]
+ pub fn retain<F: FnMut(&K, &mut V) -> bool>(&mut self, f: F) {
+ self.0.retain(f);
+ }
+}
+impl<K: TimedCeremony, V, S> FixedCapHashMap<K, V, S> {
+ /// Removes all expired ceremonies.
+ #[inline]
+ pub fn remove_expired_ceremonies(&mut self) {
+ // Even though it's more accurate to check the current `Instant` for each ceremony, we elect to capture
+ // the `Instant` we begin iteration for performance reasons. It's unlikely an appreciable amount of
+ // additional ceremonies would be removed.
+ #[cfg(not(feature = "serializable_server_state"))]
+ let now = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let now = SystemTime::now();
+ self.retain(|v, _| v.expiration() >= now);
+ }
+}
+impl<K: Eq + Hash, V, S: BuildHasher> FixedCapHashMap<K, V, S> {
+ /// [`HashMap::get_mut`].
+ #[inline]
+ pub fn get_mut<Q: Equivalent<K> + Hash + ?Sized>(&mut self, k: &Q) -> Option<&mut V> {
+ self.0.get_mut(k)
+ }
+ /// [`HashMap::get_key_value_mut`].
+ #[inline]
+ pub fn get_key_value_mut<Q: Equivalent<K> + Hash + ?Sized>(
+ &mut self,
+ k: &Q,
+ ) -> Option<(&K, &mut V)> {
+ self.0.get_key_value_mut(k)
+ }
+ /// [`HashMap::get_many_mut`].
+ #[inline]
+ pub fn get_many_mut<Q: Equivalent<K> + Hash + ?Sized, const N: usize>(
+ &mut self,
+ ks: [&Q; N],
+ ) -> [Option<&mut V>; N] {
+ self.0.get_many_mut(ks)
+ }
+ /// [`HashMap::get_many_key_value_mut`].
+ #[inline]
+ pub fn get_many_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)
+ }
+ /// [`HashMap::remove`].
+ #[inline]
+ pub fn remove<Q: Equivalent<K> + Hash + ?Sized>(&mut self, k: &Q) -> Option<V> {
+ self.0.remove(k)
+ }
+ /// [`HashMap::remove_entry`].
+ #[inline]
+ pub fn remove_entry<Q: Equivalent<K> + Hash + ?Sized>(&mut self, k: &Q) -> Option<(K, V)> {
+ self.0.remove_entry(k)
+ }
+ /// [`HashMap::try_insert`].
+ ///
+ /// `Ok(None)` is returned iff [`HashMap::len`] `==` [`HashMap::capacity`] and `key` does not already exist in
+ /// the map.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`HashMap::insert`] does.
+ #[inline]
+ pub fn try_insert(
+ &mut self,
+ key: K,
+ value: V,
+ ) -> Result<Option<&mut V>, OccupiedError<'_, K, V, S>> {
+ let full = self.0.len() == self.0.capacity();
+ match self.0.entry(key) {
+ Entry::Occupied(entry) => Err(OccupiedError { entry, value }),
+ Entry::Vacant(ent) => {
+ if full {
+ Ok(None)
+ } else {
+ Ok(Some(ent.insert(value)))
+ }
+ }
+ }
+ }
+ /// [`HashMap::insert`].
+ ///
+ /// `None` is returned iff [`HashMap::len`] `==` [`HashMap::capacity`] 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();
+ match self.0.entry(k) {
+ Entry::Occupied(mut ent) => Some(Some(ent.insert(v))),
+ Entry::Vacant(ent) => {
+ if full {
+ None
+ } else {
+ ent.insert(v);
+ Some(None)
+ }
+ }
+ }
+ }
+ /// [`HashMap::entry`].
+ ///
+ /// `None` is returned iff [`HashMap::len`] `==` [`HashMap::capacity`] 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();
+ match self.0.entry(key) {
+ ent @ Entry::Occupied(_) => Some(ent),
+ ent @ Entry::Vacant(_) => {
+ if full {
+ None
+ } else {
+ Some(ent)
+ }
+ }
+ }
+ }
+ /// [`HashMap::entry_ref`].
+ ///
+ /// `None` is returned iff [`HashMap::len`] `==` [`HashMap::capacity`] 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();
+ match self.0.entry_ref(key) {
+ ent @ EntryRef::Occupied(_) => Some(ent),
+ ent @ EntryRef::Vacant(_) => {
+ if full {
+ None
+ } else {
+ Some(ent)
+ }
+ }
+ }
+ }
+}
+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.
+ #[inline]
+ pub fn insert_remove_expired(&mut self, k: K, v: V) -> Option<Option<V>> {
+ if self.0.len() == self.0.capacity() {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let now = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let now = SystemTime::now();
+ if self
+ .0
+ .extract_if(|exp, _| exp.expiration() < now)
+ .next()
+ .is_some()
+ {
+ Some(self.0.insert(k, v))
+ } else if let Entry::Occupied(mut ent) = self.0.entry(k) {
+ Some(Some(ent.insert(v)))
+ } else {
+ None
+ }
+ } else {
+ Some(self.0.insert(k, v))
+ }
+ }
+ /// [`Self::insert`] except all expired ceremones are removed in the event there is no available capacity.
+ #[inline]
+ pub fn insert_remove_all_expired(&mut self, k: K, v: V) -> Option<Option<V>> {
+ if self.0.len() == self.0.capacity() {
+ self.remove_expired_ceremonies();
+ }
+ if self.0.len() == self.0.capacity() {
+ if let Entry::Occupied(mut ent) = self.0.entry(k) {
+ Some(Some(ent.insert(v)))
+ } else {
+ None
+ }
+ } else {
+ Some(self.0.insert(k, v))
+ }
+ }
+ /// [`Self::entry`] except the first expired ceremony is removed in the event there is no available capacity.
+ #[inline]
+ pub fn entry_remove_expired(&mut self, key: K) -> Option<Entry<'_, K, V, S>> {
+ if self.0.len() == self.0.capacity() {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let now = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let now = SystemTime::now();
+ if self
+ .0
+ .extract_if(|v, _| v.expiration() < now)
+ .next()
+ .is_some()
+ {
+ Some(self.0.entry(key))
+ } else if let ent @ Entry::Occupied(_) = self.0.entry(key) {
+ Some(ent)
+ } else {
+ None
+ }
+ } else {
+ Some(self.0.entry(key))
+ }
+ }
+ /// [`Self::entry`] except all expired ceremones are removed in the event there is no available capacity.
+ #[inline]
+ pub fn entry_remove_all_expired(&mut self, key: K) -> Option<Entry<'_, K, V, S>> {
+ if self.0.len() == self.0.capacity() {
+ self.remove_expired_ceremonies();
+ }
+ if self.0.len() == self.0.capacity() {
+ if let ent @ Entry::Occupied(_) = self.0.entry(key) {
+ Some(ent)
+ } else {
+ None
+ }
+ } else {
+ Some(self.0.entry(key))
+ }
+ }
+ /// [`Self::entry_ref`] except the first expired ceremony is removed in the event there is no available capacity.
+ #[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() {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let now = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let now = SystemTime::now();
+ if self
+ .0
+ .extract_if(|v, _| v.expiration() < now)
+ .next()
+ .is_some()
+ {
+ Some(self.0.entry_ref(key))
+ } else if let ent @ EntryRef::Occupied(_) = self.0.entry_ref(key) {
+ Some(ent)
+ } else {
+ None
+ }
+ } else {
+ Some(self.0.entry_ref(key))
+ }
+ }
+ /// [`Self::entry_ref`] except all expired ceremones are removed in the event there is no available capacity.
+ #[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() {
+ self.remove_expired_ceremonies();
+ }
+ if self.0.len() == self.0.capacity() {
+ if let ent @ EntryRef::Occupied(_) = self.0.entry_ref(key) {
+ Some(ent)
+ } else {
+ None
+ }
+ } else {
+ Some(self.0.entry_ref(key))
+ }
+ }
+}
+impl<K, V, S> AsRef<HashMap<K, V, S>> for FixedCapHashMap<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> {
+ #[inline]
+ fn from(value: FixedCapHashMap<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
@@ -0,0 +1,256 @@
+use super::{super::request::TimedCeremony, BuildIdentityHasher};
+#[cfg(doc)]
+use core::hash::Hasher;
+use core::hash::{BuildHasher, Hash};
+use hashbrown::{
+ Equivalent,
+ 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 maximum [`HashSet::capacity`].
+///
+/// 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.,
+/// [`Self::insert_remove_all_expired`]) are provided that will automatically remove expired values. Note the
+/// 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`].
+///
+/// [`Self::into`]: struct.FixedCapHashSet.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`.
+ #[inline]
+ #[must_use]
+ pub fn new(capacity: usize) -> Self {
+ Self(HashSet::with_capacity_and_hasher(
+ capacity,
+ BuildIdentityHasher,
+ ))
+ }
+}
+impl<T, S> FixedCapHashSet<T, S> {
+ /// [`HashSet::clear`].
+ #[inline]
+ pub fn clear(&mut self) {
+ self.0.clear();
+ }
+ /// [`HashSet::drain`].
+ #[inline]
+ pub fn drain(&mut self) -> Drain<'_, T> {
+ self.0.drain()
+ }
+ /// [`HashSet::extract_if`].
+ #[inline]
+ 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`].
+ #[inline]
+ #[must_use]
+ pub fn with_hasher(capacity: usize, hasher: S) -> Self {
+ Self(HashSet::with_capacity_and_hasher(capacity, hasher))
+ }
+ /// [`HashSet::retain`].
+ #[inline]
+ pub fn retain<F: FnMut(&T) -> bool>(&mut self, f: F) {
+ self.0.retain(f);
+ }
+}
+impl<T: TimedCeremony, S> FixedCapHashSet<T, S> {
+ /// Removes all expired ceremonies.
+ #[inline]
+ pub fn remove_expired_ceremonies(&mut self) {
+ // Even though it's more accurate to check the current `Instant` for each ceremony, we elect to capture
+ // the `Instant` we begin iteration for performance reasons. It's unlikely an appreciable amount of
+ // additional ceremonies would be removed.
+ #[cfg(not(feature = "serializable_server_state"))]
+ let now = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let now = SystemTime::now();
+ self.retain(|v| v.expiration() >= now);
+ }
+}
+impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> {
+ /// [`HashSet::get_or_insert`].
+ ///
+ /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] 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() {
+ self.0.get(&value)
+ } else {
+ Some(self.0.get_or_insert(value))
+ }
+ }
+ /// [`HashSet::get_or_insert_with`].
+ ///
+ /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] 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>(
+ &mut self,
+ value: &Q,
+ f: F,
+ ) -> Option<&T> {
+ if self.0.len() == self.0.capacity() {
+ self.0.get(value)
+ } else {
+ Some(self.0.get_or_insert_with(value, f))
+ }
+ }
+ /// [`HashSet::remove`].
+ #[inline]
+ pub fn remove<Q: Equivalent<T> + Hash + ?Sized>(&mut self, value: &Q) -> bool {
+ self.0.remove(value)
+ }
+ /// [`HashSet::take`].
+ #[inline]
+ pub fn take<Q: Equivalent<T> + Hash + ?Sized>(&mut self, value: &Q) -> Option<T> {
+ self.0.take(value)
+ }
+ /// [`HashSet::insert`].
+ ///
+ /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] 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();
+ if let Entry::Vacant(ent) = self.0.entry(value) {
+ if full {
+ None
+ } else {
+ ent.insert();
+ Some(true)
+ }
+ } else {
+ Some(false)
+ }
+ }
+ /// [`HashSet::replace`].
+ ///
+ /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] and `value` does not already exist in the
+ /// set.
+ #[inline]
+ pub fn replace(&mut self, value: T) -> Option<Option<T>> {
+ // Ideally we would use the Entry API to avoid searching multiple times, but one can't while also using
+ // `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() {
+ None
+ } else {
+ self.0.insert(value);
+ Some(None)
+ }
+ }
+ /// [`HashSet::entry`].
+ ///
+ /// `None` is returned iff [`HashSet::len`] `==` [`HashSet::capacity`] 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();
+ match self.0.entry(value) {
+ ent @ Entry::Occupied(_) => Some(ent),
+ ent @ Entry::Vacant(_) => {
+ if full {
+ None
+ } else {
+ Some(ent)
+ }
+ }
+ }
+ }
+}
+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.
+ #[inline]
+ pub fn insert_remove_expired(&mut self, value: T) -> Option<bool> {
+ if self.0.len() == self.0.capacity() {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let now = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let now = SystemTime::now();
+ if self.0.extract_if(|v| v.expiration() < now).next().is_some() {
+ Some(self.0.insert(value))
+ } else {
+ self.0.contains(&value).then_some(false)
+ }
+ } else {
+ Some(self.0.insert(value))
+ }
+ }
+ /// [`Self::insert`] except all expired ceremones are removed in the event there is no available capacity.
+ #[inline]
+ pub fn insert_remove_all_expired(&mut self, value: T) -> Option<bool> {
+ if self.0.len() == self.0.capacity() {
+ self.remove_expired_ceremonies();
+ }
+ if self.0.len() == self.0.capacity() {
+ 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.
+ #[inline]
+ pub fn entry_remove_expired(&mut self, value: T) -> Option<Entry<'_, T, S>> {
+ if self.0.len() == self.0.capacity() {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let now = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let now = SystemTime::now();
+ if self.0.extract_if(|v| v.expiration() < now).next().is_some() {
+ Some(self.0.entry(value))
+ } else if let ent @ Entry::Occupied(_) = self.0.entry(value) {
+ Some(ent)
+ } else {
+ None
+ }
+ } else {
+ Some(self.0.entry(value))
+ }
+ }
+ /// [`Self::entry`] except all expired ceremones are removed in the event there is no available capacity.
+ #[inline]
+ pub fn entry_remove_all_expired(&mut self, value: T) -> Option<Entry<'_, T, S>> {
+ if self.0.len() == self.0.capacity() {
+ self.remove_expired_ceremonies();
+ }
+ if self.0.len() == self.0.capacity() {
+ if let ent @ Entry::Occupied(_) = self.0.entry(value) {
+ Some(ent)
+ } else {
+ None
+ }
+ } else {
+ Some(self.0.entry(value))
+ }
+ }
+}
+impl<T, S> AsRef<HashSet<T, S>> for FixedCapHashSet<T, S> {
+ #[inline]
+ fn as_ref(&self) -> &HashSet<T, S> {
+ &self.0
+ }
+}
+impl<T, S> From<FixedCapHashSet<T, S>> for HashSet<T, S> {
+ #[inline]
+ fn from(value: FixedCapHashSet<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)
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -17,16 +17,18 @@
//! ## `webauthn_rp` in action
//!
//! ```no_run
+//! use core::convert;
//! use webauthn_rp::{
-//! AuthenticatedCredential, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
-//! DiscoverableCredentialRequestOptions, PublicKeyCredentialCreationOptions, RegisteredCredential,
-//! Registration, RegistrationServerState,
+//! AuthenticatedCredential64, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
+//! DiscoverableCredentialRequestOptions, PublicKeyCredentialCreationOptions64, RegisteredCredential64,
+//! Registration, RegistrationServerState64,
+//! hash::hash_set::FixedCapHashSet,
//! request::{
//! AsciiDomain, PublicKeyCredentialDescriptor, RpId,
//! auth::AuthenticationVerificationOptions,
//! register::{
-//! Nickname, PublicKeyCredentialUserEntity, RegistrationVerificationOptions,
-//! USER_HANDLE_MAX_LEN, UserHandle64, Username,
+//! Nickname, PublicKeyCredentialUserEntity64, RegistrationVerificationOptions,
+//! UserHandle64, Username,
//! },
//! },
//! response::{
@@ -35,9 +37,6 @@
//! register::{CompressedPubKey, DynamicState, error::RegCeremonyErr},
//! },
//! };
-//! # #[cfg(not(feature = "serializable_server_state"))]
-//! // These are available iff `serializable_server_state` is _not_ enabled.
-//! use webauthn_rp::request::{FixedCapHashSet, InsertResult};
//! # #[cfg(feature = "serde")]
//! use serde::de::{Deserialize, Deserializer};
//! # #[cfg(feature = "serde_relaxed")]
@@ -105,9 +104,9 @@
//! /// This only makes sense for greenfield deployments since account information (e.g., user name) would likely
//! /// already exist otherwise. This is similar to credential creation except a random `UserHandle64` is generated and
//! /// will be used for subsequent credential registrations.
-//! # #[cfg(all(feature = "serde_relaxed", not(feature = "serializable_server_state")))]
+//! # #[cfg(feature = "serde_relaxed")]
//! fn start_account_creation(
-//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
//! ) -> Result<Vec<u8>, AppErr> {
//! let rp_id = RpId::Domain(
//! AsciiDomain::try_from(RP_ID.to_owned())
@@ -115,19 +114,17 @@
//! );
//! let user_id = UserHandle64::new();
//! let (server, client) =
-//! PublicKeyCredentialCreationOptions::first_passkey_with_blank_user_info(
+//! PublicKeyCredentialCreationOptions64::first_passkey_with_blank_user_info(
//! &rp_id, &user_id,
//! )
//! .start_ceremony()
//! .unwrap_or_else(|_e| {
-//! unreachable!("we don't manually mutate the options; thus this can't fail")
+//! unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
//! });
-//! if matches!(
-//! reg_ceremonies.insert_or_replace_all_expired(server),
-//! InsertResult::Success
-//! ) {
+//! if reg_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity)
+//! {
//! Ok(serde_json::to_vec(&client)
-//! .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
+//! .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState64::serialize")))
//! } else {
//! Err(AppErr::WebAuthnCeremonyCreation)
//! }
@@ -140,9 +137,9 @@
//! ///
//! /// Note if this errors, then the user should be notified to delete the passkey created on their
//! /// authenticator.
-//! # #[cfg(all(feature = "serde_relaxed", not(feature = "serializable_server_state")))]
+//! # #[cfg(feature = "serde_relaxed")]
//! fn finish_account_creation(
-//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
//! client_data: Vec<u8>,
//! ) -> Result<(), AppErr> {
//! let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data.as_slice())?;
@@ -168,27 +165,25 @@
//! /// passkey. This is similar to account creation except we already have the user entity info and we need to
//! /// fetch the registered `PublicKeyCredentialDescriptor`s to avoid accidentally overwriting a passkey on
//! /// the authenticator.
-//! # #[cfg(all(feature = "serde_relaxed", not(feature = "serializable_server_state")))]
+//! # #[cfg(feature = "serde_relaxed")]
//! fn start_cred_registration(
//! user_id: &UserHandle64,
-//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
//! ) -> Result<Vec<u8>, AppErr> {
//! let rp_id = RpId::Domain(
//! AsciiDomain::try_from(RP_ID.to_owned())
//! .unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
//! );
//! let (entity, creds) = select_user_info(user_id)?.ok_or_else(|| AppErr::NoAccount)?;
-//! let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, entity, creds)
+//! let (server, client) = PublicKeyCredentialCreationOptions64::passkey(&rp_id, entity, creds)
//! .start_ceremony()
//! .unwrap_or_else(|_e| {
-//! unreachable!("we don't manually mutate the options; thus this won't error")
+//! unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
//! });
-//! if matches!(
-//! reg_ceremonies.insert_or_replace_all_expired(server),
-//! InsertResult::Success
-//! ) {
+//! if reg_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity)
+//! {
//! Ok(serde_json::to_vec(&client)
-//! .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
+//! .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState64::serialize")))
//! } else {
//! Err(AppErr::WebAuthnCeremonyCreation)
//! }
@@ -201,9 +196,9 @@
//! ///
//! /// Note if this errors, then the user should be notified to delete the passkey created on their
//! /// authenticator.
-//! # #[cfg(all(feature = "serde_relaxed", not(feature = "serializable_server_state")))]
+//! # #[cfg(feature = "serde_relaxed")]
//! fn finish_cred_registration(
-//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
//! client_data: Vec<u8>,
//! ) -> Result<(), AppErr> {
//! // `Registration::from_json_custom` is available iff `serde_relaxed` is enabled.
@@ -224,7 +219,7 @@
//! )
//! }
//! /// Starts the passkey authentication ceremony.
-//! # #[cfg(all(feature = "serde_relaxed", not(feature = "serializable_server_state")))]
+//! # #[cfg(feature = "serde_relaxed")]
//! fn start_auth(
//! auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
//! ) -> Result<Vec<u8>, AppErr> {
@@ -235,12 +230,10 @@
//! let (server, client) = DiscoverableCredentialRequestOptions::passkey(&rp_id)
//! .start_ceremony()
//! .unwrap_or_else(|_e| {
-//! unreachable!("we don't manually mutate the options; thus this won't error")
+//! unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
//! });
-//! if matches!(
-//! auth_ceremonies.insert_or_replace_all_expired(server),
-//! InsertResult::Success
-//! ) {
+//! if auth_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity)
+//! {
//! Ok(serde_json::to_vec(&client).unwrap_or_else(|_e| {
//! unreachable!("bug in DiscoverableAuthenticationClientState::serialize")
//! }))
@@ -249,12 +242,12 @@
//! }
//! }
//! /// Finishes the passkey authentication ceremony.
-//! # #[cfg(all(feature = "serde_relaxed", not(feature = "serializable_server_state")))]
+//! # #[cfg(feature = "serde_relaxed")]
//! fn finish_auth(
//! auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
//! client_data: Vec<u8>,
//! ) -> Result<(), AppErr> {
-//! // `Authentication::from_json_custom` is available iff `serde_relaxed` is enabled.
+//! // `DiscoverableAuthentication64::from_json_custom` is available iff `serde_relaxed` is enabled.
//! let authentication =
//! DiscoverableAuthentication64::from_json_custom(client_data.as_slice())?;
//! let mut cred = select_credential(
@@ -263,7 +256,7 @@
//! )?
//! .ok_or_else(|| AppErr::NoCredential)?;
//! if auth_ceremonies
-//! // `Authentication::challenge_relaxed` is available iff `serde_relaxed` is enabled.
+//! // `DiscoverableAuthentication64::challenge_relaxed` is available iff `serde_relaxed` is enabled.
//! .take(&authentication.challenge_relaxed()?)
//! .ok_or(AppErr::MissingWebAuthnCeremony)?
//! .verify(
@@ -289,7 +282,7 @@
//! /// `CredentialId`, or there already exists an account using the same `UserHandle64`.
//! fn insert_account(
//! account: &AccountReg<'_, '_>,
-//! cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
+//! cred: RegisteredCredential64<'_>,
//! ) -> Result<(), AppErr> {
//! // ⋮
//! # Ok(())
@@ -303,7 +296,7 @@
//! user_id: &'a UserHandle64,
//! ) -> Result<
//! Option<(
-//! PublicKeyCredentialUserEntity<'static, 'static, 'a, USER_HANDLE_MAX_LEN>,
+//! PublicKeyCredentialUserEntity64<'static, 'static, 'a>,
//! Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
//! )>,
//! AppErr,
@@ -315,9 +308,10 @@
//! ///
//! /// # Errors
//! ///
-//! /// Errors iff writing `cred` errors or there already exists a credential using the same `CredentialId`.
+//! /// Errors iff writing `cred` errors, there already exists a credential using the same `CredentialId`,
+//! /// or there does not exist an account under the `UserHandle64`.
//! fn insert_credential(
-//! cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
+//! cred: RegisteredCredential64<'_>,
//! ) -> Result<(), AppErr> {
//! // ⋮
//! # Ok(())
@@ -333,10 +327,9 @@
//! user_id: &'user UserHandle64,
//! ) -> Result<
//! Option<
-//! AuthenticatedCredential<
+//! AuthenticatedCredential64<
//! 'cred,
//! 'user,
-//! USER_HANDLE_MAX_LEN,
//! CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>,
//! >,
//! >,
@@ -430,7 +423,7 @@
//! of those types.
//!
//! Note even when written to persistent storage, an application should still periodically remove expired ceremonies.
-//! If one is using a relational database (RDB); then one can achieve this by storing [`TimedCeremony::sent_challenge`],
+//! If one is using a relational database (RDB); then one can achieve this by storing [`SentChallenge`],
//! the `Vec` returned from [`Encode::encode`], and [`TimedCeremony::expiration`] and periodically remove all rows
//! whose expiration exceeds the current date and time.
//!
@@ -518,50 +511,6 @@
//! [^note]: `panic`s related to memory allocations or stack overflow are possible since such issues are not
//! formally guarded against.
#![cfg_attr(docsrs, feature(doc_cfg))]
-#![deny(
- unknown_lints,
- future_incompatible,
- let_underscore,
- missing_docs,
- nonstandard_style,
- refining_impl_trait,
- rust_2018_compatibility,
- rust_2018_idioms,
- rust_2021_compatibility,
- rust_2024_compatibility,
- unsafe_code,
- unused,
- warnings,
- clippy::all,
- clippy::cargo,
- clippy::complexity,
- clippy::correctness,
- clippy::nursery,
- clippy::pedantic,
- clippy::perf,
- clippy::restriction,
- clippy::style,
- clippy::suspicious
-)]
-#![expect(
- clippy::arbitrary_source_item_ordering,
- clippy::blanket_clippy_restriction_lints,
- clippy::exhaustive_enums,
- clippy::exhaustive_structs,
- clippy::implicit_return,
- clippy::min_ident_chars,
- clippy::missing_trait_methods,
- clippy::multiple_crate_versions,
- clippy::pub_with_shorthand,
- clippy::pub_use,
- clippy::ref_patterns,
- clippy::return_and_then,
- clippy::self_named_module_files,
- clippy::single_call_fn,
- clippy::single_char_lifetime_names,
- clippy::unseparated_literal_suffix,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
#[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(feature = "serializable_server_state")]
@@ -577,24 +526,20 @@ use crate::request::{
use crate::response::error::CredentialIdErr;
#[cfg(feature = "serde_relaxed")]
use crate::response::ser_relaxed::SerdeJsonErr;
-#[cfg(feature = "bin")]
-use crate::{
- request::register::bin::{DecodeNicknameErr, DecodeUsernameErr},
- response::{
- bin::DecodeAuthTransportsErr,
- register::bin::{DecodeDynamicStateErr, DecodeStaticStateErr},
- },
-};
#[cfg(doc)]
use crate::{
+ hash::hash_set::FixedCapHashSet,
request::{
- AsciiDomain, DomainOrigin, FixedCapHashSet, Port, PublicKeyCredentialDescriptor, RpId,
- Scheme, TimedCeremony, Url,
+ AsciiDomain, DomainOrigin, Port, PublicKeyCredentialDescriptor, RpId, Scheme,
+ TimedCeremony, Url,
auth::{AllowedCredential, AllowedCredentials, PublicKeyCredentialRequestOptions},
- register::{CoseAlgorithmIdentifier, Nickname, PublicKeyCredentialUserEntity, Username},
+ register::{
+ CoseAlgorithmIdentifier, Nickname, PublicKeyCredentialUserEntity, UserHandle16,
+ UserHandle64, Username,
+ },
},
response::{
- CollectedClientData, Flag,
+ CollectedClientData, Flag, SentChallenge,
auth::{self, Authentication, DiscoverableAuthenticatorAssertion},
register::{
self, Aaguid, Attestation, AttestationObject, AttestedCredentialData,
@@ -603,12 +548,20 @@ use crate::{
},
},
};
+#[cfg(feature = "bin")]
+use crate::{
+ request::register::bin::{DecodeNicknameErr, DecodeUsernameErr},
+ response::{
+ bin::DecodeAuthTransportsErr,
+ register::bin::{DecodeDynamicStateErr, DecodeStaticStateErr},
+ },
+};
use crate::{
request::{
auth::error::{RequestOptionsErr, SecondFactorErr},
error::{AsciiDomainErr, DomainOriginParseErr, PortParseErr, SchemeParseErr, UrlErr},
register::{
- ResidentKeyRequirement, UserHandle,
+ ResidentKeyRequirement, USER_HANDLE_MAX_LEN, UserHandle,
error::{CreationOptionsErr, NicknameErr, UsernameErr},
},
},
@@ -633,6 +586,7 @@ use core::{
convert,
error::Error,
fmt::{self, Display, Formatter},
+ ops::Not,
};
#[cfg(all(doc, feature = "serde_relaxed"))]
use response::register::ser_relaxed::RegistrationRelaxed;
@@ -648,6 +602,8 @@ use std::time::{Instant, SystemTime};
#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
#[cfg(feature = "bin")]
pub mod bin;
+/// Contains functionality for fixed-capacity hash maps and sets.
+pub mod hash;
/// Functionality for starting ceremonies.
///
/// # What kind of credential should I create?
@@ -719,7 +675,10 @@ pub use crate::{
NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
},
register::{
- PublicKeyCredentialCreationOptions, RegistrationClientState, RegistrationServerState,
+ PublicKeyCredentialCreationOptions, PublicKeyCredentialCreationOptions16,
+ PublicKeyCredentialCreationOptions64, RegistrationClientState,
+ RegistrationClientState16, RegistrationClientState64, RegistrationServerState,
+ RegistrationServerState16, RegistrationServerState64,
},
},
response::{
@@ -731,22 +690,24 @@ pub use crate::{
register::Registration,
},
};
-/// Error returned in [`RegCeremonyErr::Credential`] and [`AuthCeremonyErr::Credential`] as well as
-/// from [`AuthenticatedCredential::new`].
+/// Error returned in [`RegCeremonyErr::Credential`] and [`AuthenticatedCredential::new`].
#[derive(Clone, Copy, Debug)]
pub enum CredentialErr {
/// Variant when [`CredentialProtectionPolicy::UserVerificationRequired`], but
/// [`DynamicState::user_verified`] is `false`.
CredProtectUserVerificationRequiredWithoutUserVerified,
- /// Variant when [`AuthenticatorExtensionOutput::hmac_secret`] is `Some(true)`, but
+ /// Variant when [`ClientExtensionsOutputs::prf`] is
+ /// `Some(AuthenticationExtensionsPRFOutputs { enabled: true })` and
/// [`DynamicState::user_verified`] is `false`.
- HmacSecretWithoutUserVerified,
+ PrfWithoutUserVerified,
/// Variant when [`AuthenticatorExtensionOutput::hmac_secret`] is `Some(true)`, but
- /// [`ClientExtensionsOutputs::prf`] is not `Some(AuthenticationExtensionsPRFOutputs { enabled: true })`.
+ /// [`ClientExtensionsOutputs::prf`] is `Some(AuthenticationExtensionsPRFOutputs { enabled: false })`
+ /// or `AuthenticatorExtensionOutput::hmac_secret` is `Some`, but
+ /// `ClientExtensionsOutputs::prf` is `None`.
HmacSecretWithoutPrf,
/// Variant when [`ClientExtensionsOutputs::prf`] is
/// `Some(AuthenticationExtensionsPRFOutputs { enabled: true })`, but
- /// [`AuthenticatorExtensionOutput::hmac_secret`] is not `Some(true)`.
+ /// [`AuthenticatorExtensionOutput::hmac_secret`] is `Some(false)`.
PrfWithoutHmacSecret,
/// Variant when [`ResidentKeyRequirement::Required`] was sent, but
/// [`CredentialPropertiesOutput::rk`] is `Some(false)`.
@@ -759,9 +720,7 @@ impl Display for CredentialErr {
Self::CredProtectUserVerificationRequiredWithoutUserVerified => {
"credProtect requires user verification, but the user is not verified"
}
- Self::HmacSecretWithoutUserVerified => {
- "hmac-secret is enabled, but the user is not verified"
- }
+ Self::PrfWithoutUserVerified => "prf is enabled, but the user is not verified",
Self::HmacSecretWithoutPrf => "hmac-secret was enabled but prf was not",
Self::PrfWithoutHmacSecret => "prf was enabled, but hmac-secret was not",
Self::ResidentKeyRequiredServerCredentialCreated => {
@@ -771,11 +730,11 @@ impl Display for CredentialErr {
}
}
impl Error for CredentialErr {}
-/// Checks if the `static_state` and `dynamic_state` are valid for a credential.
+/// Checks if the `static_state` and `dynamic_state` are valid.
///
/// # Errors
///
-/// Errors iff `static_state` or `dynamic_state` are invalid.
+/// Errors iff `static_state` or `dynamc_state` are invalid.
fn verify_static_and_dynamic_state<T>(
static_state: &StaticState<T>,
dynamic_state: DynamicState,
@@ -787,11 +746,43 @@ fn verify_static_and_dynamic_state<T>(
CredentialProtectionPolicy::UserVerificationRequired
) {
Err(CredentialErr::CredProtectUserVerificationRequiredWithoutUserVerified)
- } else if static_state.extensions.hmac_secret.unwrap_or_default() {
- Err(CredentialErr::HmacSecretWithoutUserVerified)
+ } else if static_state
+ .client_extension_results
+ .prf
+ .is_some_and(|prf| prf.enabled)
+ {
+ Err(CredentialErr::PrfWithoutUserVerified)
} else {
Ok(())
}
+ .and_then(|()| {
+ static_state.client_extension_results.prf.map_or_else(
+ || {
+ if static_state.extensions.hmac_secret.is_none() {
+ Ok(())
+ } else {
+ Err(CredentialErr::HmacSecretWithoutPrf)
+ }
+ },
+ |prf| {
+ if prf.enabled {
+ if static_state.extensions.hmac_secret.is_some_and(Not::not) {
+ Err(CredentialErr::PrfWithoutHmacSecret)
+ } else {
+ Ok(())
+ }
+ } else if static_state
+ .extensions
+ .hmac_secret
+ .is_some_and(convert::identity)
+ {
+ Err(CredentialErr::HmacSecretWithoutPrf)
+ } else {
+ Ok(())
+ }
+ },
+ )
+ })
}
/// Registered credential that needs to be saved server-side to perform future
/// [authentication ceremonies](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) with
@@ -927,48 +918,24 @@ impl<'reg, const USER_LEN: usize> RegisteredCredential<'reg, USER_LEN> {
metadata: Metadata<'a>,
) -> Result<Self, CredentialErr> {
verify_static_and_dynamic_state(&static_state, dynamic_state).and_then(|()| {
- // `verify_static_and_dynamic_state` already ensures that
- // `hmac-secret` is not `Some(true)` when `!dynamic_state.user_verified`;
- // thus we only need to check that one is not enabled without the other.
- if static_state.extensions.hmac_secret.unwrap_or_default() {
- if metadata
+ if !matches!(metadata.resident_key, ResidentKeyRequirement::Required)
+ || metadata
.client_extension_results
- .prf
- .is_some_and(|prf| prf.enabled)
- {
- Ok(())
- } else {
- Err(CredentialErr::HmacSecretWithoutPrf)
- }
- } else if metadata
- .client_extension_results
- .prf
- .is_some_and(|prf| prf.enabled)
+ .cred_props
+ .as_ref()
+ .is_none_or(|props| props.rk.is_none_or(convert::identity))
{
- Err(CredentialErr::PrfWithoutHmacSecret)
+ Ok(Self {
+ id,
+ transports,
+ user_id,
+ static_state,
+ dynamic_state,
+ metadata,
+ })
} else {
- Ok(())
+ Err(CredentialErr::ResidentKeyRequiredServerCredentialCreated)
}
- .and_then(|()| {
- if !matches!(metadata.resident_key, ResidentKeyRequirement::Required)
- || metadata
- .client_extension_results
- .cred_props
- .as_ref()
- .is_none_or(|props| props.rk.is_none_or(convert::identity))
- {
- Ok(Self {
- id,
- transports,
- user_id,
- static_state,
- dynamic_state,
- metadata,
- })
- } else {
- Err(CredentialErr::ResidentKeyRequiredServerCredentialCreated)
- }
- })
})
}
/// Returns the contained data consuming `self`.
@@ -1016,6 +983,10 @@ impl<'reg, const USER_LEN: usize> RegisteredCredential<'reg, USER_LEN> {
)
}
}
+/// `RegisteredCredential` based on a [`UserHandle64`].
+pub type RegisteredCredential64<'reg> = RegisteredCredential<'reg, USER_HANDLE_MAX_LEN>;
+/// `RegisteredCredential` based on a [`UserHandle16`].
+pub type RegisteredCredential16<'reg> = RegisteredCredential<'reg, 16>;
/// Credential used in authentication ceremonies.
///
/// Similar to [`RegisteredCredential`] except designed to only contain the necessary data to complete
@@ -1135,6 +1106,12 @@ impl<'cred, 'user, const USER_LEN: usize, PublicKey>
)
}
}
+/// `AuthenticatedCredential` based on a [`UserHandle64`].
+pub type AuthenticatedCredential64<'cred, 'user, PublicKey> =
+ AuthenticatedCredential<'cred, 'user, USER_HANDLE_MAX_LEN, PublicKey>;
+/// `AuthenticatedCredential` based on a [`UserHandle16`].
+pub type AuthenticatedCredential16<'cred, 'user, PublicKey> =
+ AuthenticatedCredential<'cred, 'user, 16, PublicKey>;
/// Convenience aggregate error that rolls up all errors into one.
#[derive(Debug)]
pub enum AggErr {
diff --git a/src/request.rs b/src/request.rs
@@ -1,5 +1,6 @@
#[cfg(doc)]
use super::{
+ hash::hash_set::FixedCapHashSet,
request::{
auth::{
AllowedCredential, AllowedCredentials, CredentialSpecificExtension,
@@ -21,8 +22,6 @@ use crate::{
CredentialId, Origin, Response, SentChallenge,
},
};
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-use core::hash::{BuildHasher, Hash, Hasher};
use core::{
borrow::Borrow,
fmt::{self, Display, Formatter},
@@ -30,10 +29,10 @@ use core::{
str::FromStr,
};
use rsa::sha2::{Digest as _, Sha256};
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+use std::time::Instant;
#[cfg(feature = "serializable_server_state")]
use std::time::SystemTime;
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-use std::{collections::HashSet, time::Instant};
use url::Url as Uri;
/// Contains functionality for beginning the
/// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony).
@@ -41,9 +40,9 @@ use url::Url as Uri;
/// # Examples
///
/// ```
-/// # #[cfg(not(feature = "serializable_server_state"))]
-/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
+/// # use core::convert;
/// # use webauthn_rp::{
+/// # hash::hash_set::FixedCapHashSet,
/// # request::{
/// # auth::{AllowedCredentials, DiscoverableCredentialRequestOptions, NonDiscoverableCredentialRequestOptions},
/// # register::UserHandle64,
@@ -52,16 +51,13 @@ use url::Url as Uri;
/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
/// # AggErr,
/// # };
-/// # #[cfg(not(feature = "serializable_server_state"))]
/// let mut ceremonies = FixedCapHashSet::new(128);
/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
/// let (server, client) = DiscoverableCredentialRequestOptions::passkey(&rp_id).start_ceremony()?;
-/// # #[cfg(not(feature = "serializable_server_state"))]
-/// assert!(matches!(
-/// ceremonies.insert_or_replace_all_expired(server),
-/// InsertResult::Success
-/// ));
-/// # #[cfg(all(feature = "custom", not(feature = "serializable_server_state")))]
+/// assert!(
+/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity)
+/// );
+/// # #[cfg(feature = "custom")]
/// let mut ceremonies_2 = FixedCapHashSet::new(128);
/// # #[cfg(feature = "serde")]
/// assert!(serde_json::to_string(&client).is_ok());
@@ -71,11 +67,10 @@ use url::Url as Uri;
/// # #[cfg(feature = "custom")]
/// let (server_2, client_2) =
/// NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?.start_ceremony()?;
-/// # #[cfg(all(feature = "custom", not(feature = "serializable_server_state")))]
-/// assert!(matches!(
-/// ceremonies_2.insert_or_replace_all_expired(server_2),
-/// InsertResult::Success
-/// ));
+/// # #[cfg(feature = "custom")]
+/// assert!(
+/// ceremonies_2.insert_remove_all_expired(server_2).map_or(false, convert::identity)
+/// );
/// # #[cfg(all(feature = "custom", feature = "serde"))]
/// assert!(serde_json::to_string(&client_2).is_ok());
/// /// Extract `UserHandle` from session cookie.
@@ -108,9 +103,9 @@ pub mod error;
/// # Examples
///
/// ```
-/// # #[cfg(not(feature = "serializable_server_state"))]
-/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
+/// # use core::convert;
/// # use webauthn_rp::{
+/// # hash::hash_set::FixedCapHashSet,
/// # request::{
/// # register::{
/// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64,
@@ -120,7 +115,7 @@ pub mod error;
/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
/// # AggErr,
/// # };
-/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom"))]
+/// # #[cfg(feature = "custom")]
/// let mut ceremonies = FixedCapHashSet::new(128);
/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
/// # #[cfg(feature = "custom")]
@@ -132,11 +127,10 @@ pub mod error;
/// # #[cfg(feature = "custom")]
/// let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, user.clone(), creds)
/// .start_ceremony()?;
-/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom"))]
-/// assert!(matches!(
-/// ceremonies.insert_or_replace_all_expired(server),
-/// InsertResult::Success
-/// ));
+/// # #[cfg(feature = "custom")]
+/// assert!(
+/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity)
+/// );
/// # #[cfg(all(feature = "serde", feature = "custom"))]
/// assert!(serde_json::to_string(&client).is_ok());
/// # #[cfg(feature = "custom")]
@@ -144,11 +138,10 @@ pub mod error;
/// # #[cfg(feature = "custom")]
/// let (server_2, client_2) =
/// PublicKeyCredentialCreationOptions::second_factor(&rp_id, user, creds_2).start_ceremony()?;
-/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom"))]
-/// assert!(matches!(
-/// ceremonies.insert_or_replace_all_expired(server_2),
-/// InsertResult::Success
-/// ));
+/// # #[cfg(feature = "custom")]
+/// assert!(
+/// ceremonies.insert_remove_all_expired(server_2).map_or(false, convert::identity)
+/// );
/// # #[cfg(all(feature = "serde", feature = "custom"))]
/// assert!(serde_json::to_string(&client_2).is_ok());
/// /// Extract `UserHandle` from session cookie or storage if this is not the first credential registered.
@@ -205,7 +198,6 @@ pub struct Challenge(u128);
impl Challenge {
// This won't `panic` since 4/3 of 16 is less than `usize::MAX`.
/// The number of bytes a `Challenge` takes to encode in base64url.
- #[expect(clippy::unwrap_used, reason = "we want to crash when there is a bug")]
pub(super) const BASE64_LEN: usize = super::base64url_nopad_len(16).unwrap();
/// Generates a random `Challenge`.
///
@@ -915,7 +907,7 @@ impl<'a: 'b + 'c, 'b, 'c> TryFrom<&'a str> for DomainOrigin<'b, 'c> {
}
/// [`PublicKeyCredentialDescriptor`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptor)
/// associated with a registered credential.
-#[derive(Debug)]
+#[derive(Clone, Debug)]
pub struct PublicKeyCredentialDescriptor<T> {
/// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-id).
pub id: CredentialId<T>,
@@ -1498,154 +1490,7 @@ trait Ceremony<const USER_LEN: usize, const DISCOVERABLE: bool> {
}
}
/// `300_000` milliseconds is equal to five minutes.
-#[expect(
- clippy::unwrap_used,
- reason = "clearly correct, Option::unwrap is const, and better than unsafe"
-)]
pub(super) const THREE_HUNDRED_THOUSAND: NonZeroU32 = NonZeroU32::new(300_000).unwrap();
-
-/// [`Hasher`] whose `write_*` methods simply store up to 64 bits of the passed argument _as is_ overwriting
-/// any previous state.
-///
-/// This is designed to only be used indirectly via a hash map whose keys are randomly generated on the server
-/// based on at least 64 bits—the size of the integer returned from [`Self::finish`]—of entropy.
-/// This makes this `Hasher` usable (and ideal) in only the most niche circumstances.
-///
-/// [`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
-/// 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
-/// only constructable via [`PublicKeyCredentialCreationOptions::start_ceremony`],
-/// [`DiscoverableCredentialRequestOptions::start_ceremony`], and
-/// [`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).
-#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-#[derive(Debug)]
-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`).
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-impl Hasher for IdentityHasher {
- /// Returns `0` if no `write_*` calls have been made; otherwise returns the result of the most recent
- /// `write_*` call.
- #[inline]
- fn finish(&self) -> u64 {
- self.0
- }
- /// Writes `i` to `self`.
- #[inline]
- fn write_u64(&mut self, i: u64) {
- self.0 = i;
- }
- /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
- #[expect(
- clippy::as_conversions,
- clippy::cast_sign_loss,
- reason = "we simply need to convert into a u64 in a deterministic way"
- )]
- #[inline]
- fn write_i8(&mut self, i: i8) {
- self.write_u64(i as u64);
- }
- /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
- #[expect(
- clippy::as_conversions,
- clippy::cast_sign_loss,
- reason = "we simply need to convert into a u64 in a deterministic way"
- )]
- #[inline]
- fn write_i16(&mut self, i: i16) {
- self.write_u64(i as u64);
- }
- /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
- #[expect(
- clippy::as_conversions,
- clippy::cast_sign_loss,
- reason = "we simply need to convert into a u64 in a deterministic way"
- )]
- #[inline]
- fn write_i32(&mut self, i: i32) {
- self.write_u64(i as u64);
- }
- /// Redirects to [`Self::write_u64`].
- #[expect(
- clippy::as_conversions,
- clippy::cast_sign_loss,
- reason = "we simply need to convert into a u64 in a deterministic way"
- )]
- #[inline]
- fn write_i64(&mut self, i: i64) {
- self.write_u64(i as u64);
- }
- /// Truncates `i` to a [`u64`] before redirecting to [`Self::write_u64`].
- #[expect(
- clippy::as_conversions,
- clippy::cast_possible_truncation,
- clippy::cast_sign_loss,
- reason = "we simply need to convert into a u64 in a deterministic way"
- )]
- #[inline]
- fn write_i128(&mut self, i: i128) {
- 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) {
- self.write_u64(u64::from(i));
- }
- /// Zero-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
- #[inline]
- fn write_u16(&mut self, i: u16) {
- self.write_u64(u64::from(i));
- }
- /// Zero-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
- #[inline]
- fn write_u32(&mut self, i: u32) {
- self.write_u64(u64::from(i));
- }
- /// Truncates `i` to a [`u64`] before redirecting to [`Self::write_u64`].
- #[expect(
- clippy::as_conversions,
- clippy::cast_possible_truncation,
- reason = "we simply need to convert into a u64 in a deterministic way"
- )]
- #[inline]
- fn write_u128(&mut self, i: u128) {
- 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")]
- #[inline]
- fn write(&mut self, bytes: &[u8]) {
- if let Some(data) = bytes.get(..8) {
- let mut val = [0; 8];
- val.copy_from_slice(data);
- self.write_u64(u64::from_ne_bytes(val));
- }
- }
-}
-/// [`BuildHasher`] of an [`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.
-#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-#[derive(Clone, Copy, Debug)]
-pub struct BuildIdentityHasher;
-#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
-#[cfg(not(feature = "serializable_server_state"))]
-impl BuildHasher for BuildIdentityHasher {
- type Hasher = IdentityHasher;
- #[inline]
- fn build_hasher(&self) -> Self::Hasher {
- IdentityHasher(0)
- }
-}
/// "Ceremonies" stored on the server that expire after a certain duration.
///
/// Types like [`RegistrationServerState`] and [`DiscoverableAuthenticationServerState`] are based on [`Challenge`]s
@@ -1660,249 +1505,6 @@ pub trait TimedCeremony {
/// Returns the `SystemTime` the ceremony expires.
#[cfg(all(not(doc), feature = "serializable_server_state"))]
fn expiration(&self) -> SystemTime;
- /// Returns the `SentChallenge`.
- fn sent_challenge(&self) -> SentChallenge;
-}
-/// Fixed-capacity hash set that only inserts items when there is available capacity.
-///
-/// This should only be used if _both_ of the following conditions are met:
-///
-/// * Application is already protected from memory exhaustion attacks (e.g., users must be connected
-/// via VPN).
-/// * There are legitimate reasons for an in-memory collection to grow unbounded.
-///
-/// The first point is necessary; otherwise an attacker could trivially slow down the application
-/// by causing repeated inserts forcing repeated calls to functions like [`Self::retain`]. The second
-/// point is necessary; otherwise any in-memory collection would suffice.
-///
-/// When `T` is a [`TimedCeremony`], there are legitimate reasons why a ceremony will never finish (e.g.,
-/// an outage could kill a user's connection after starting a ceremony). The longer the application
-/// runs the more such instances occur to the point where the in-memory collection is full of expired
-/// ceremonies. Since this should rarely occur and as long as [`Self::capacity`] is appropriate,
-/// [`Self::insert`] should almost always succeed; however very rarely there will be a point when
-/// one will have to [`Self::remove_expired_ceremonies`]. A vast majority of the time a user
-/// will complete the ceremony which requires ownership of the `TimedCeremony` which in turn requires
-/// [`Self::take`] which will add an available slot.
-#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-#[derive(Debug)]
-pub struct FixedCapHashSet<T, S = BuildIdentityHasher>(HashSet<T, S>);
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-impl<T> FixedCapHashSet<T, BuildIdentityHasher> {
- /// Creates an empty `FixedCapHashSet` with at least the specified capacity.
- ///
- /// The hash set will be able to hold at least `capacity` elements without reallocating. This method is allowed
- /// to allocate for more elements than `capacity`.
- #[inline]
- #[must_use]
- pub fn new(capacity: usize) -> Self {
- Self(HashSet::with_capacity_and_hasher(
- capacity,
- BuildIdentityHasher,
- ))
- }
-}
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-impl<T, S> FixedCapHashSet<T, S> {
- /// Creates an empty `FixedCapHashSet` with at least the specified capacity, using `hasher` to hash the keys.
- ///
- /// The hash set will be able to hold at least `capacity` elements without reallocating. This method is allowed
- /// to allocate for more elements than `capacity`.
- #[inline]
- #[must_use]
- pub fn new_with_hasher(capacity: usize, hasher: S) -> Self {
- Self(HashSet::with_capacity_and_hasher(capacity, hasher))
- }
- /// Returns the immutable capacity.
- ///
- /// This number is a lower bound; the `FixedCapHashSet` might be able to hold more, but is guaranteed to be
- /// able to hold at least this many.
- #[inline]
- #[must_use]
- pub fn capacity(&self) -> usize {
- self.0.capacity()
- }
- /// Clears the set, removing all values.
- #[inline]
- pub fn clear(&mut self) {
- self.0.clear();
- }
- /// Returns the number of elements in the set.
- #[inline]
- #[must_use]
- pub fn len(&self) -> usize {
- self.0.len()
- }
- /// Returns `true` iff the set contains no elements.
- #[inline]
- #[must_use]
- pub fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
- /// Retains only the elements specified by the predicate.
- ///
- /// In other words, remove all elements `e` for which `f(&e)` returns `false`. The elements are visited in
- /// unsorted (and unspecified) order.
- #[inline]
- pub fn retain<F>(&mut self, f: F)
- where
- F: FnMut(&T) -> bool,
- {
- self.0.retain(f);
- }
-}
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-impl<T: TimedCeremony, S> FixedCapHashSet<T, S> {
- /// Removes all expired ceremonies.
- #[inline]
- pub fn remove_expired_ceremonies(&mut self) {
- // Even though it's more accurate to check the current `Instant` for each ceremony, we elect to capture
- // the `Instant` we begin iteration for performance reasons. It's unlikely an appreciable amount of
- // additional ceremonies would be removed.
- let now = Instant::now();
- self.retain(|v| v.expiration() >= now);
- }
-}
-/// Result returned from [`FixedCapHashSet::insert`], [`FixedCapHashSet::insert_or_replace_expired`], and
-/// [`FixedCapHashSet::insert_or_replace_all_expired`].
-#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-#[derive(Clone, Copy, Debug)]
-pub enum InsertResult {
- /// Value was successfully inserted.
- Success,
- /// Value was not inserted since the capacity was full.
- CapacityFull,
- /// Value was not inserted since the value already existed.
- ///
- /// When the keys are based on [`Challenge`]s, this should almost never occur since `Challenge`s
- /// are 16 bytes of random data.
- Duplicate,
-}
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> {
- /// Returns `true` iff the set contains a value.
- ///
- /// The value may be any borrowed form of the set's value type, but `Hash` and `Eq` on the borrowed form _must_
- /// match those for the value type.
- #[inline]
- #[must_use]
- pub fn contains<Q>(&self, value: &Q) -> bool
- where
- T: Borrow<Q>,
- Q: Eq + Hash + ?Sized,
- {
- self.0.contains(value)
- }
- /// Returns a reference to the value in the set, if any, that is equal to the given value.
- ///
- /// The value may be any borrowed form of the set's value type, but `Hash` and `Eq` on the borrowed form _must_
- /// match those for the value type.
- #[inline]
- #[must_use]
- pub fn get<Q>(&self, value: &Q) -> Option<&T>
- where
- T: Borrow<Q>,
- Q: Eq + Hash + ?Sized,
- {
- self.0.get(value)
- }
- /// Removes a value from the set. Returns whether the value was present in the set.
- ///
- /// The value may be any borrowed form of the set's value type, but `Hash` and `Eq` on the borrowed form _must_
- /// match those for the value type.
- #[inline]
- pub fn remove<Q>(&mut self, value: &Q) -> bool
- where
- T: Borrow<Q>,
- Q: Eq + Hash + ?Sized,
- {
- self.0.remove(value)
- }
- /// Removes and returns the value in the set, if any, that is equal to the given one.
- ///
- /// The value may be any borrowed form of the set's value type, but `Hash` and `Eq` on the borrowed form _must_
- /// match those for the value type.
- #[inline]
- pub fn take<Q>(&mut self, value: &Q) -> Option<T>
- where
- T: Borrow<Q>,
- Q: Eq + Hash + ?Sized,
- {
- self.0.take(value)
- }
- /// Adds `value` to the set iff [`Self::capacity`] `>` [`Self::len`].
- #[inline]
- pub fn insert(&mut self, value: T) -> InsertResult {
- if self.len() == self.capacity() {
- InsertResult::CapacityFull
- } else if self.0.insert(value) {
- InsertResult::Success
- } else {
- InsertResult::Duplicate
- }
- }
-}
-#[cfg(any(doc, not(feature = "serializable_server_state")))]
-impl<T: Borrow<SentChallenge> + Eq + Hash + TimedCeremony, S: BuildHasher> FixedCapHashSet<T, S> {
- /// Adds a ceremony to the set.
- ///
- /// This will only insert `value` iff [`Self::capacity`] `>` [`Self::len`]. When [`Self::len`] `==`
- /// [`Self::capacity`], this will iterate items in the set until an expired ceremony is encountered; at which
- /// point, the expired ceremony will be removed before `value` is inserted. In the event no expired ceremonies
- /// exist, [`InsertResult::CapacityFull`] will be returned.
- ///
- /// If one wants to avoid the potentially expensive operation of iterating the set for an expired ceremony,
- /// call [`Self::insert`]. Alternatively one can call [`Self::insert_or_replace_all_expired`] to avoid
- /// repeatedly iterating the hash set once its capacity is full.
- #[inline]
- pub fn insert_or_replace_expired(&mut self, value: T) -> InsertResult {
- if self.len() == self.capacity() {
- let now = Instant::now();
- self.0
- .iter()
- .try_fold((), |(), v| {
- if v.expiration() < now {
- Err(v.sent_challenge())
- } else {
- Ok(())
- }
- })
- .map_or_else(
- |chall| {
- self.remove(&chall);
- if self.0.insert(value) {
- InsertResult::Success
- } else {
- InsertResult::Duplicate
- }
- },
- |()| InsertResult::CapacityFull,
- )
- } else if self.0.insert(value) {
- InsertResult::Success
- } else {
- InsertResult::Duplicate
- }
- }
- /// Adds a ceremony to the set.
- ///
- /// This will only insert `value` iff [`Self::capacity`] `>` [`Self::len`]. When [`Self::len`] `==`
- /// [`Self::capacity`], this will [`Self::remove_expired_ceremonies`] before `value` is inserted. In the event
- /// no expired ceremonies exist, [`InsertResult::CapacityFull`] will be returned.
- #[inline]
- pub fn insert_or_replace_all_expired(&mut self, value: T) -> InsertResult {
- if self.len() == self.capacity() {
- self.remove_expired_ceremonies();
- }
- if self.len() == self.capacity() {
- InsertResult::CapacityFull
- } else if self.0.insert(value) {
- InsertResult::Success
- } else {
- InsertResult::Duplicate
- }
- }
}
#[cfg(test)]
mod tests {
@@ -1919,9 +1521,9 @@ mod tests {
register::{
AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation,
AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputs,
- CompressedP256PubKey, CompressedP384PubKey, CompressedPubKey,
- CredentialProtectionPolicy, DynamicState, Ed25519PubKey, Registration,
- RsaPubKey, StaticState, UncompressedPubKey,
+ ClientExtensionsOutputsStaticState, CompressedP256PubKey, CompressedP384PubKey,
+ CompressedPubKey, CredentialProtectionPolicy, DynamicState, Ed25519PubKey,
+ Registration, RsaPubKey, StaticState, UncompressedPubKey,
},
},
},
@@ -1933,7 +1535,7 @@ mod tests {
Extension as AuthExt, NonDiscoverableCredentialRequestOptions, PrfInputOwned,
},
register::{
- CredProtect, Extension as RegExt, PublicKeyCredentialCreationOptions,
+ CredProtect, Extension as RegExt, FourToSixtyThree, PublicKeyCredentialCreationOptions,
PublicKeyCredentialUserEntity, RegistrationVerificationOptions, UserHandle,
},
};
@@ -1970,7 +1572,7 @@ mod tests {
const CBOR_TRUE: u8 = CBOR_SIMPLE | 21;
#[test]
#[cfg(feature = "custom")]
- fn ed25519_reg() -> Result<(), AggErr> {
+ fn eddsa_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
@@ -1985,8 +1587,15 @@ mod tests {
opts.challenge = Challenge(0);
opts.extensions = RegExt {
cred_props: None,
- cred_protect: CredProtect::UserVerificationRequired(ExtensionInfo::RequireEnforceValue),
- min_pin_length: Some((10, ExtensionInfo::RequireEnforceValue)),
+ cred_protect: CredProtect::UserVerificationRequired(
+ false,
+ ExtensionInfo::RequireEnforceValue,
+ ),
+ min_pin_length: Some((
+ FourToSixtyThree::new(10)
+ .unwrap_or_else(|| unreachable!("bug in FourToSixyThree::new")),
+ ExtensionInfo::RequireEnforceValue,
+ )),
prf: Some(ExtensionInfo::RequireEnforceValue),
};
let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
@@ -2313,7 +1922,7 @@ mod tests {
}
#[test]
#[cfg(feature = "custom")]
- fn ed25519_auth() -> Result<(), AggErr> {
+ fn eddsa_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let mut creds = AllowedCredentials::with_capacity(1);
creds.push(AllowedCredential {
@@ -2511,6 +2120,9 @@ mod tests {
cred_protect: CredentialProtectionPolicy::None,
hmac_secret: Some(true),
},
+ client_extension_results: ClientExtensionsOutputsStaticState {
+ prf: Some(AuthenticationExtensionsPrfOutputs { enabled: true }),
+ }
},
DynamicState {
user_verified: true,
@@ -2525,7 +2137,7 @@ mod tests {
}
#[test]
#[cfg(feature = "custom")]
- fn p256_reg() -> Result<(), AggErr> {
+ fn es256_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
@@ -2783,7 +2395,7 @@ mod tests {
}
#[test]
#[cfg(feature = "custom")]
- fn p256_auth() -> Result<(), AggErr> {
+ fn es256_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
opts.0.challenge = Challenge(0);
@@ -2879,6 +2491,7 @@ mod tests {
cred_protect: CredentialProtectionPolicy::None,
hmac_secret: None,
},
+ client_extension_results: ClientExtensionsOutputsStaticState { prf: None }
},
DynamicState {
user_verified: true,
@@ -2893,7 +2506,7 @@ mod tests {
}
#[test]
#[cfg(feature = "custom")]
- fn p384_reg() -> Result<(), AggErr> {
+ fn es384_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
@@ -3186,7 +2799,7 @@ mod tests {
}
#[test]
#[cfg(feature = "custom")]
- fn p384_auth() -> Result<(), AggErr> {
+ fn es384_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
opts.0.challenge = Challenge(0);
@@ -3283,6 +2896,7 @@ mod tests {
cred_protect: CredentialProtectionPolicy::None,
hmac_secret: None,
},
+ client_extension_results: ClientExtensionsOutputsStaticState { prf: None }
},
DynamicState {
user_verified: true,
@@ -3297,7 +2911,7 @@ mod tests {
}
#[test]
#[cfg(feature = "custom")]
- fn rsa_reg() -> Result<(), AggErr> {
+ fn rs256_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
@@ -3799,7 +3413,7 @@ mod tests {
}
#[test]
#[cfg(feature = "custom")]
- fn rsa_auth() -> Result<(), AggErr> {
+ fn rs256_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
opts.0.challenge = Challenge(0);
@@ -3946,6 +3560,7 @@ mod tests {
cred_protect: CredentialProtectionPolicy::None,
hmac_secret: None,
},
+ client_extension_results: ClientExtensionsOutputsStaticState { prf: None }
},
DynamicState {
user_verified: true,
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -3,7 +3,10 @@ use super::{
super::response::{
Backup, CollectedClientData, Flag,
auth::AuthenticatorData,
- register::{DynamicState, StaticState},
+ register::{
+ AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState,
+ DynamicState, StaticState,
+ },
},
AsciiDomain, DomainOrigin, Url,
register::{self, PublicKeyCredentialCreationOptions},
@@ -18,7 +21,7 @@ use super::{
HmacSecret, NonDiscoverableAuthentication,
error::{AuthCeremonyErr, ExtensionErr, OneOrTwo},
},
- register::CompressedPubKey,
+ register::{CompressedPubKey, CredentialProtectionPolicy},
},
},
BackupReq, Ceremony, CeremonyOptions, Challenge, CredentialId, Credentials, ExtensionReq, Hint,
@@ -85,9 +88,10 @@ impl SignatureCounterEnforcement {
/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
///
/// This is only applicable if
-/// [`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)
-/// is `true` when registering a new credential with [`register::Extension::prf`] and
/// [`PublicKeyCredentialRequestOptions::user_verification`] is [`UserVerificationRequirement::Required`].
+/// Additionally [`AuthenticatorExtensionOutputStaticState::hmac_secret`] must either be `None` or `Some(true)`
+/// and [`ClientExtensionsOutputsStaticState::prf`] must be `None` or
+/// `Some(AuthenticationExtensionsPrfOutputs { enabled: true })`.
///
/// Unlike the spec, it is forbidden for
/// [the decrypted outputs](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs) to be
@@ -103,7 +107,7 @@ impl SignatureCounterEnforcement {
///
/// For the owned analog, see [`PrfInputOwned`].
///
-/// When relying on [`DiscoverableCredentialRequestOptions`]), one will likely use a static PRF input for _all_
+/// When relying on [`DiscoverableCredentialRequestOptions`], one will likely use a static PRF input for _all_
/// credentials since rolling over PRF inputs is not feasible. One uses this type for such a thing. In other words,
/// `'a` will likely be `'static` and [`Self::second`] will likely be `None`.
#[derive(Clone, Copy, Debug)]
@@ -113,19 +117,28 @@ pub struct PrfInput<'a> {
/// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second).
pub second: Option<&'a [u8]>,
/// Response requirements.
+ ///
+ /// Note this is only applicable for authenticators that implement the
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension)
+ /// extension since the data is encrypted and is part of the [`AuthenticatorData`].
pub ext_info: ExtensionReq,
}
/// Owned version of [`PrfInput`].
///
/// 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(Debug)]
+#[derive(Clone, Debug)]
pub struct PrfInputOwned {
/// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first).
pub first: Vec<u8>,
/// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second).
pub second: Option<Vec<u8>>,
- /// Response requirements.
+ ///
+ /// Note this is only applicable for authenticators that implement the
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension)
+ /// extension since the data is encrypted and is part of the [`AuthenticatorData`].
pub ext_info: ExtensionReq,
}
/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client.
@@ -139,7 +152,7 @@ pub struct Extension<'prf> {
}
/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client that
/// are credential-specific which among other things implies a non-discoverable request.
-#[derive(Debug, Default)]
+#[derive(Clone, Debug, Default)]
pub struct CredentialSpecificExtension {
/// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension).
///
@@ -148,7 +161,7 @@ pub struct CredentialSpecificExtension {
}
/// Registered credential used in
/// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
-#[derive(Debug)]
+#[derive(Clone, Debug)]
pub struct AllowedCredential {
/// The registered credential.
pub credential: PublicKeyCredentialDescriptor<Vec<u8>>,
@@ -164,8 +177,14 @@ impl From<PublicKeyCredentialDescriptor<Vec<u8>>> for AllowedCredential {
}
}
}
+impl From<AllowedCredential> for PublicKeyCredentialDescriptor<Vec<u8>> {
+ #[inline]
+ fn from(credential: AllowedCredential) -> Self {
+ credential.credential
+ }
+}
/// Queue of unique [`AllowedCredential`]s.
-#[derive(Debug, Default)]
+#[derive(Clone, Debug, Default)]
pub struct AllowedCredentials {
/// Allowed credentials.
creds: Vec<AllowedCredential>,
@@ -281,6 +300,29 @@ impl From<&AllowedCredentials> for Vec<CredInfo> {
})
}
}
+impl From<AllowedCredentials> for Vec<PublicKeyCredentialDescriptor<Vec<u8>>> {
+ #[inline]
+ fn from(value: AllowedCredentials) -> Self {
+ let mut creds = Self::with_capacity(value.creds.len());
+ value.creds.into_iter().fold((), |(), cred| {
+ creds.push(cred.credential);
+ });
+ creds
+ }
+}
+impl From<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>> for AllowedCredentials {
+ #[inline]
+ fn from(value: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>) -> Self {
+ let mut creds = Self::with_capacity(value.len());
+ value.into_iter().fold((), |(), credential| {
+ creds.push(AllowedCredential {
+ credential,
+ extension: CredentialSpecificExtension { prf: None },
+ });
+ });
+ creds
+ }
+}
/// Helper that verifies the overlap of [`DiscoverableCredentialRequestOptions::start_ceremony`] and
/// [`DiscoverableAuthenticationServerState::decode`].
fn validate_discoverable_options_helper(
@@ -403,11 +445,11 @@ impl<'rp_id, 'prf> NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
pub const fn options(&mut self) -> &mut PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
&mut self.options
}
- /// Returns the `slice` of [`AllowedCredential`]s.
+ /// Returns a reference to the [`AllowedCredential`]s.
#[inline]
#[must_use]
- pub fn allow_credentials(&self) -> &[AllowedCredential] {
- self.allow_credentials.as_ref()
+ pub const fn allow_credentials(&self) -> &AllowedCredentials {
+ &self.allow_credentials
}
/// Creates a `NonDiscoverableCredentialRequestOptions` containing
/// [`PublicKeyCredentialRequestOptions::second_factor`] and the passed [`AllowedCredentials`].
@@ -630,8 +672,37 @@ impl<'rp_id, 'prf> NonDiscoverableAuthenticationClientState<'rp_id, 'prf> {
&self.0
}
}
-/// `PrfInput` and `PrfInputOwned` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`]
-/// in an in-memory collection.
+/// The possible combinations of an [`AuthenticatedCredential`]'s [`StaticState`]'s
+/// `extensions.hmac_secret` and `client_extension_results.prf`.
+///
+/// Note we ensure in `crate::verify_static_and_dynamic_state` that `hmac_secret` does not exist when
+/// `prf` does not exist, `hmac_secret` does not exist or is `false` when `prf` is `false`, or
+/// `hmac_secret` does not exist or is `true` when `prf` is `true`.
+#[derive(Clone, Copy)]
+enum CredPrf {
+ /// No `prf` or `hmac_secret`.
+ None,
+ /// `prf.enabled` is `false` but there is no `hmac_secret`.
+ FalseNoHmac,
+ /// `prf.enabled` and `hmac_secret` are `false`.
+ FalseFalseHmac,
+ /// `prf.enabled` is `true` but there is no `hmac_secret`.
+ TrueNoHmac,
+ /// `prf.enabled` and `hmac_secret` are `true`.
+ TrueTrueHmac,
+}
+impl CredPrf {
+ /// Returns `true` iff `self` is allowed to have an `HmacSecret` response.
+ ///
+ /// Note many authenticators allow PRF to be used during authentication even when not requested during
+ /// registration even for authenticators (e.g., CTAP-based ones) that implement PRF on top of the `hmac-secret`
+ /// extension; thus we allow `Self::None` and `Self::TrueNoHmac`.
+ const fn is_prf_capable(self) -> bool {
+ matches!(self, Self::None | Self::TrueNoHmac | Self::TrueTrueHmac)
+ }
+}
+/// `PrfInput` and `PrfInputOwned` without the actual data sent to reduce memory usage when storing
+/// [`DiscoverableAuthenticationServerState`] in an in-memory collection.
#[derive(Clone, Copy, Debug)]
enum ServerPrfInfo {
/// `PrfInput::second` was `None`.
@@ -647,72 +718,95 @@ impl ServerPrfInfo {
}
}
/// Validates `val` based on the passed arguments.
+ ///
+ /// It's not possible to request the PRF extension without sending `UserVerificationRequirement::Required`;
+ /// thus `user_verified` will always be `true` when sending PRF; otherwise ceremony validation will error.
+ /// However when we _don't_ send the PRF extension _and_ we don't error on an unsolicited response, it's
+ /// possible to receive an `HmacSecret` without the user having been verified; thus we only ensure
+ /// `user_verified` is true when we don't error on unsolicted responses _and_ we didn't send the PRF extension.
fn validate(
val: Option<Self>,
- prf_capable: bool,
+ user_verified: bool,
+ cred_prf: CredPrf,
hmac: HmacSecret,
err_unsolicited: bool,
) -> Result<(), ExtensionErr> {
match hmac {
- HmacSecret::None => {
- if prf_capable {
- val.map_or(Ok(()), |input| {
- if matches!(input.ext_info(), ExtensionReq::Allow) {
+ HmacSecret::None => val.map_or(Ok(()), |input| {
+ if matches!(input.ext_info(), ExtensionReq::Allow) {
+ if cred_prf.is_prf_capable() {
+ Ok(())
+ } else {
+ Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
+ }
+ } else {
+ match cred_prf {
+ CredPrf::None | CredPrf::TrueNoHmac => Ok(()),
+ CredPrf::FalseNoHmac | CredPrf::FalseFalseHmac => {
+ Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
+ }
+ CredPrf::TrueTrueHmac => Err(ExtensionErr::MissingHmacSecret),
+ }
+ }
+ }),
+ HmacSecret::One => val.map_or_else(
+ || {
+ if err_unsolicited {
+ Err(ExtensionErr::ForbiddenHmacSecret)
+ } else if cred_prf.is_prf_capable() {
+ if user_verified {
Ok(())
} else {
- Err(ExtensionErr::MissingHmacSecret)
+ Err(ExtensionErr::UserNotVerifiedHmacSecret)
}
- })
- } else {
- // We check if the PRF extension was requested on an incapable credential;
- // if so, we error.
- val.map_or(Ok(()), |_| Err(ExtensionErr::HmacSecretForPrfIncapableCred))
- }
- }
- HmacSecret::One => {
- if prf_capable {
- val.map_or_else(
- || {
- if err_unsolicited {
- Err(ExtensionErr::ForbiddenHmacSecret)
- } else {
- Ok(())
- }
- },
- |input| match input {
- Self::One(_) => Ok(()),
- Self::Two(_) => Err(ExtensionErr::InvalidHmacSecretValue(
- OneOrTwo::Two,
- OneOrTwo::One,
- )),
- },
- )
- } else {
- Err(ExtensionErr::HmacSecretForPrfIncapableCred)
- }
- }
- HmacSecret::Two => {
- if prf_capable {
- val.map_or_else(
- || {
- if err_unsolicited {
- Err(ExtensionErr::ForbiddenHmacSecret)
- } else {
- Ok(())
- }
- },
- |input| match input {
- Self::One(_) => Err(ExtensionErr::InvalidHmacSecretValue(
- OneOrTwo::One,
- OneOrTwo::Two,
- )),
- Self::Two(_) => Ok(()),
- },
- )
- } else {
- Err(ExtensionErr::HmacSecretForPrfIncapableCred)
- }
- }
+ } else {
+ Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
+ }
+ },
+ |info| {
+ if matches!(info, Self::One(_)) {
+ if cred_prf.is_prf_capable() {
+ Ok(())
+ } else {
+ Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
+ }
+ } else {
+ Err(ExtensionErr::InvalidHmacSecretValue(
+ OneOrTwo::Two,
+ OneOrTwo::One,
+ ))
+ }
+ },
+ ),
+ HmacSecret::Two => val.map_or_else(
+ || {
+ if err_unsolicited {
+ Err(ExtensionErr::ForbiddenHmacSecret)
+ } else if cred_prf.is_prf_capable() {
+ if user_verified {
+ Ok(())
+ } else {
+ Err(ExtensionErr::UserNotVerifiedHmacSecret)
+ }
+ } else {
+ Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
+ }
+ },
+ |info| {
+ if matches!(info, Self::Two(_)) {
+ if cred_prf.is_prf_capable() {
+ Ok(())
+ } else {
+ Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
+ }
+ } else {
+ Err(ExtensionErr::InvalidHmacSecretValue(
+ OneOrTwo::One,
+ OneOrTwo::Two,
+ ))
+ }
+ },
+ ),
}
}
}
@@ -786,13 +880,15 @@ impl ServerExtensionInfo {
/// Note that this MUST only be called internally by `auth::validate_extensions`.
fn validate_extensions(
self,
+ user_verified: bool,
auth_ext: AuthenticatorExtensionOutput,
error_unsolicited: bool,
- prf_capable: bool,
+ cred_prf: CredPrf,
) -> Result<(), ExtensionErr> {
ServerPrfInfo::validate(
self.prf,
- prf_capable,
+ user_verified,
+ cred_prf,
auth_ext.hmac_secret,
error_unsolicited,
)
@@ -801,15 +897,16 @@ impl ServerExtensionInfo {
/// Validates the extensions.
fn validate_extensions(
ext: ServerExtensionInfo,
+ user_verified: bool,
cred_ext: Option<ServerCredSpecificExtensionInfo>,
auth_ext: AuthenticatorExtensionOutput,
error_unsolicited: bool,
- prf_capable: bool,
+ cred_prf: CredPrf,
) -> Result<(), ExtensionErr> {
cred_ext.map_or_else(
|| {
// No client-specific extensions, so we can simply focus on `ext`.
- ext.validate_extensions(auth_ext, error_unsolicited, prf_capable)
+ ext.validate_extensions(user_verified, auth_ext, error_unsolicited, cred_prf)
},
|c_ext| {
// Must carefully process each extension based on overlap and which gets priority over the other.
@@ -817,7 +914,8 @@ fn validate_extensions(
|| {
ServerPrfInfo::validate(
ext.prf,
- prf_capable,
+ user_verified,
+ cred_prf,
auth_ext.hmac_secret,
error_unsolicited,
)
@@ -825,7 +923,8 @@ fn validate_extensions(
|_| {
ServerPrfInfo::validate(
c_ext.prf,
- prf_capable,
+ user_verified,
+ cred_prf,
auth_ext.hmac_secret,
error_unsolicited,
)
@@ -1053,7 +1152,7 @@ impl DiscoverableAuthenticationServerState {
if cred.user_id == response.response.user_handle() {
// Step 6 item 2.
if cred.id == response.raw_id {
- self.0.verify(rp_id, response, cred, options, None)
+ self.0.verify(true, rp_id, response, cred, options, None)
} else {
Err(AuthCeremonyErr::CredentialIdMismatch)
}
@@ -1147,7 +1246,7 @@ impl NonDiscoverableAuthenticationServerState {
// Step 6 item 1.
if c.id == cred.id {
self.state
- .verify(rp_id, response, cred, options, Some(c.ext))
+ .verify(false, rp_id, response, cred, options, Some(c.ext))
} else {
Err(AuthCeremonyErr::CredentialIdMismatch)
}
@@ -1214,6 +1313,7 @@ impl AuthenticationServerState {
RsaKey: AsRef<[u8]>,
>(
self,
+ discoverable: bool,
rp_id: &RpId,
response: &'a Authentication<USER_LEN, DISCOVERABLE>,
cred: &mut AuthenticatedCredential<
@@ -1278,13 +1378,30 @@ impl AuthenticationServerState {
response.authenticator_attachment,
)
.and_then(|auth_attachment| {
+ let flags = auth_data.flags();
// Step 23.
validate_extensions(
self.extensions,
+ flags.user_verified,
cred_ext,
auth_data.extensions(),
options.error_on_unsolicited_extensions,
- cred.static_state.extensions.hmac_secret.unwrap_or_default(),
+ cred.static_state.client_extension_results.prf.map_or(
+ CredPrf::None,
+ |prf| {
+ if prf.enabled {
+ cred.static_state
+ .extensions
+ .hmac_secret
+ .map_or(CredPrf::TrueNoHmac, |_| CredPrf::TrueTrueHmac)
+ } else {
+ cred.static_state
+ .extensions
+ .hmac_secret
+ .map_or(CredPrf::FalseNoHmac, |_| CredPrf::FalseFalseHmac)
+ }
+ },
+ ),
)
.map_err(AuthCeremonyErr::Extension)
.and_then(|()| {
@@ -1293,7 +1410,6 @@ impl AuthenticationServerState {
.sig_counter_enforcement
.validate(cred.dynamic_state.sign_count, auth_data.sign_count())
.and_then(|sig_counter| {
- let flags = auth_data.flags();
let prev_dyn_state = cred.dynamic_state;
// Step 24 item 2.
cred.dynamic_state.backup = flags.backup;
@@ -1305,15 +1421,27 @@ impl AuthenticationServerState {
cred.dynamic_state.sign_count = sig_counter;
cred.dynamic_state.authenticator_attachment = auth_attachment;
// Step 25.
- crate::verify_static_and_dynamic_state(
- &cred.static_state,
- cred.dynamic_state,
- )
- .map_err(|e| {
+ if flags.user_verified {
+ Ok(())
+ } else {
+ match cred.static_state.extensions.cred_protect {
+ CredentialProtectionPolicy::None | CredentialProtectionPolicy::UserVerificationOptional => Ok(()),
+ CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => {
+ if discoverable {
+ Err(AuthCeremonyErr::DiscoverableCredProtectCredentialIdList)
+ } else {
+ Ok(())
+ }
+ }
+ CredentialProtectionPolicy::UserVerificationRequired => {
+ Err(AuthCeremonyErr::UserNotVerifiedCredProtectRequired)
+ }
+ }
+ }.inspect_err(|_| {
cred.dynamic_state = prev_dyn_state;
- AuthCeremonyErr::Credential(e)
+ }).map(|()| {
+ prev_dyn_state != cred.dynamic_state
})
- .map(|()| prev_dyn_state != cred.dynamic_state)
})
})
})
@@ -1338,10 +1466,6 @@ impl TimedCeremony for AuthenticationServerState {
fn expiration(&self) -> SystemTime {
self.expiration
}
- #[inline]
- fn sent_challenge(&self) -> SentChallenge {
- self.challenge
- }
}
impl TimedCeremony for DiscoverableAuthenticationServerState {
#[cfg(any(doc, not(feature = "serializable_server_state")))]
@@ -1354,10 +1478,6 @@ impl TimedCeremony for DiscoverableAuthenticationServerState {
fn expiration(&self) -> SystemTime {
self.0.expiration()
}
- #[inline]
- fn sent_challenge(&self) -> SentChallenge {
- self.0.sent_challenge()
- }
}
impl TimedCeremony for NonDiscoverableAuthenticationServerState {
#[cfg(any(doc, not(feature = "serializable_server_state")))]
@@ -1370,10 +1490,6 @@ impl TimedCeremony for NonDiscoverableAuthenticationServerState {
fn expiration(&self) -> SystemTime {
self.state.expiration()
}
- #[inline]
- fn sent_challenge(&self) -> SentChallenge {
- self.state.sent_challenge()
- }
}
impl<const USER_LEN: usize, const DISCOVERABLE: bool> Ceremony<USER_LEN, DISCOVERABLE>
for AuthenticationServerState
@@ -1527,28 +1643,77 @@ impl Ord for NonDiscoverableAuthenticationServerState {
}
#[cfg(test)]
mod tests {
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ use super::{
+ super::super::{
+ AuthenticatedCredential, DynamicState, StaticState, UserHandle,
+ response::{
+ Backup,
+ auth::{DiscoverableAuthenticatorAssertion, HmacSecret},
+ register::{
+ AuthenticationExtensionsPrfOutputs, AuthenticatorExtensionOutputStaticState,
+ ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, Ed25519PubKey,
+ },
+ },
+ },
+ AuthCeremonyErr, AuthenticationVerificationOptions, AuthenticatorAttachment,
+ AuthenticatorAttachmentEnforcement, CompressedPubKey, DiscoverableAuthentication,
+ ExtensionErr, OneOrTwo, PrfInput, SignatureCounterEnforcement,
+ };
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
+ use super::{
+ super::{super::AggErr, Challenge, CredentialId, RpId, UserVerificationRequirement},
+ DiscoverableCredentialRequestOptions, ExtensionReq,
+ };
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
use super::{
super::{
- super::{
- AggErr,
- bin::{Decode as _, Encode as _},
- },
+ super::bin::{Decode as _, Encode as _},
AsciiDomain, AuthTransports,
},
- AllowedCredential, AllowedCredentials, Challenge, CredentialId,
- CredentialSpecificExtension, Credentials as _, DiscoverableAuthenticationServerState,
- DiscoverableCredentialRequestOptions, Extension, ExtensionReq,
- NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
- PrfInputOwned, PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement,
+ AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Credentials as _,
+ DiscoverableAuthenticationServerState, Extension, NonDiscoverableAuthenticationServerState,
+ NonDiscoverableCredentialRequestOptions, PrfInputOwned, PublicKeyCredentialDescriptor,
};
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ use ed25519_dalek::{Signer as _, SigningKey};
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
use rsa::sha2::{Digest as _, Sha256};
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_BYTES: u8 = 0b010_00000;
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_TEXT: u8 = 0b011_00000;
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_MAP: u8 = 0b101_00000;
#[test]
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
@@ -1745,4 +1910,422 @@ mod tests {
);
Ok(())
}
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ struct TestResponseOptions {
+ user_verified: bool,
+ hmac: HmacSecret,
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ enum PrfCredOptions {
+ None,
+ FalseNoHmac,
+ FalseHmacFalse,
+ TrueNoHmac,
+ TrueHmacTrue,
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ struct TestCredOptions {
+ cred_protect: CredentialProtectionPolicy,
+ prf: PrfCredOptions,
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ enum PrfUvOptions {
+ /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise
+ /// `UserVerificationRequirement::Preferred` is used.
+ None(bool),
+ Prf(PrfInput<'static>),
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ struct TestRequestOptions {
+ error_unsolicited: bool,
+ prf_uv: PrfUvOptions,
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ struct TestOptions {
+ request: TestRequestOptions,
+ response: TestResponseOptions,
+ cred: TestCredOptions,
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn generate_client_data_json() -> Vec<u8> {
+ let mut json = Vec::with_capacity(256);
+ json.extend_from_slice(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice());
+ json
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn generate_authenticator_data_public_key_sig(
+ opts: TestResponseOptions,
+ ) -> (
+ Vec<u8>,
+ CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>,
+ Vec<u8>,
+ ) {
+ let mut authenticator_data = Vec::with_capacity(256);
+ authenticator_data.extend_from_slice(
+ [
+ // RP ID HASH.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // FLAGS.
+ // UP, UV, AT, and ED (right-to-left).
+ 0b0000_0001
+ | if opts.user_verified {
+ 0b0000_0100
+ } else {
+ 0b0000_0000
+ }
+ | if matches!(opts.hmac, HmacSecret::None) {
+ 0
+ } else {
+ 0b1000_0000
+ },
+ // COUNTER.
+ // 0 as 32-bit big endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ authenticator_data[..32]
+ .copy_from_slice(Sha256::digest("example.com".as_bytes()).as_slice());
+ match opts.hmac {
+ HmacSecret::None => {}
+ HmacSecret::One => {
+ authenticator_data.extend_from_slice(
+ [
+ CBOR_MAP | 1,
+ // CBOR text of length 11.
+ CBOR_TEXT | 11,
+ b'h',
+ b'm',
+ b'a',
+ b'c',
+ b'-',
+ b's',
+ b'e',
+ b'c',
+ b'r',
+ b'e',
+ b't',
+ CBOR_BYTES | 24,
+ 48,
+ ]
+ .as_slice(),
+ );
+ authenticator_data.extend_from_slice([0; 48].as_slice());
+ }
+ HmacSecret::Two => {
+ authenticator_data.extend_from_slice(
+ [
+ CBOR_MAP | 1,
+ // CBOR text of length 11.
+ CBOR_TEXT | 11,
+ b'h',
+ b'm',
+ b'a',
+ b'c',
+ b'-',
+ b's',
+ b'e',
+ b'c',
+ b'r',
+ b'e',
+ b't',
+ CBOR_BYTES | 24,
+ 80,
+ ]
+ .as_slice(),
+ );
+ authenticator_data.extend_from_slice([0; 80].as_slice());
+ }
+ }
+ let len = authenticator_data.len();
+ authenticator_data
+ .extend_from_slice(Sha256::digest(generate_client_data_json().as_slice()).as_slice());
+ let sig_key = SigningKey::from_bytes(&[0; 32]);
+ let sig = sig_key.sign(authenticator_data.as_slice()).to_vec();
+ authenticator_data.truncate(len);
+ (
+ authenticator_data,
+ CompressedPubKey::Ed25519(Ed25519PubKey::from(sig_key.verifying_key().to_bytes())),
+ sig,
+ )
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn validate(options: TestOptions) -> Result<(), AggErr> {
+ let rp_id = RpId::Domain("example.com".to_owned().try_into()?);
+ let user = UserHandle::from([0; 1]);
+ let (authenticator_data, credential_public_key, signature) =
+ generate_authenticator_data_public_key_sig(options.response);
+ let credential_id = CredentialId::try_from(vec![0; 16])?;
+ let authentication = DiscoverableAuthentication::new(
+ credential_id.clone(),
+ DiscoverableAuthenticatorAssertion::new(
+ generate_client_data_json(),
+ authenticator_data,
+ signature,
+ user,
+ ),
+ AuthenticatorAttachment::None,
+ );
+ let auth_opts = AuthenticationVerificationOptions::<'static, 'static, &str, &str> {
+ allowed_origins: [].as_slice(),
+ allowed_top_origins: None,
+ auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Update(false),
+ backup_requirement: None,
+ error_on_unsolicited_extensions: options.request.error_unsolicited,
+ sig_counter_enforcement: SignatureCounterEnforcement::Fail,
+ update_uv: false,
+ #[cfg(feature = "serde_relaxed")]
+ client_data_json_relaxed: false,
+ };
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
+ opts.0.challenge = Challenge(0);
+ opts.0.user_verification = UserVerificationRequirement::Preferred;
+ match options.request.prf_uv {
+ PrfUvOptions::None(required) => {
+ if required {
+ opts.0.user_verification = UserVerificationRequirement::Required;
+ };
+ }
+ PrfUvOptions::Prf(input) => {
+ opts.0.user_verification = UserVerificationRequirement::Required;
+ opts.0.extensions.prf = Some(input);
+ }
+ }
+ let mut cred = AuthenticatedCredential::new(
+ (&credential_id).into(),
+ &user,
+ StaticState {
+ credential_public_key,
+ extensions: AuthenticatorExtensionOutputStaticState {
+ cred_protect: options.cred.cred_protect,
+ hmac_secret: match options.cred.prf {
+ PrfCredOptions::None
+ | PrfCredOptions::FalseNoHmac
+ | PrfCredOptions::TrueNoHmac => None,
+ PrfCredOptions::FalseHmacFalse => Some(false),
+ PrfCredOptions::TrueHmacTrue => Some(true),
+ },
+ },
+ client_extension_results: ClientExtensionsOutputsStaticState {
+ prf: match options.cred.prf {
+ PrfCredOptions::None => None,
+ PrfCredOptions::FalseNoHmac | PrfCredOptions::FalseHmacFalse => {
+ Some(AuthenticationExtensionsPrfOutputs { enabled: false })
+ }
+ PrfCredOptions::TrueNoHmac | PrfCredOptions::TrueHmacTrue => {
+ Some(AuthenticationExtensionsPrfOutputs { enabled: true })
+ }
+ },
+ },
+ },
+ DynamicState {
+ user_verified: true,
+ backup: Backup::NotEligible,
+ sign_count: 0,
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ )?;
+ opts.start_ceremony()?
+ .0
+ .verify(&rp_id, &authentication, &mut cred, &auth_opts)
+ .map_err(AggErr::AuthCeremony)
+ .map(|_| ())
+ }
+ /// Test all, and only, possible `UserNotVerified` errors.
+ /// 4 * 5 * 3 * 2 * 5 = 600 tests.
+ /// We ignore this due to how long it takes (around 4 seconds or so).
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[ignore]
+ #[test]
+ fn test_uv_required_err() {
+ const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [
+ CredentialProtectionPolicy::None,
+ CredentialProtectionPolicy::UserVerificationOptional,
+ CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
+ CredentialProtectionPolicy::UserVerificationRequired,
+ ];
+ const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [
+ PrfCredOptions::None,
+ PrfCredOptions::FalseNoHmac,
+ PrfCredOptions::FalseHmacFalse,
+ PrfCredOptions::TrueNoHmac,
+ PrfCredOptions::TrueHmacTrue,
+ ];
+ const ALL_HMAC_OPTIONS: [HmacSecret; 3] =
+ [HmacSecret::None, HmacSecret::One, HmacSecret::Two];
+ const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true];
+ const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [
+ PrfUvOptions::None(true),
+ PrfUvOptions::Prf(PrfInput {
+ first: [].as_slice(),
+ second: None,
+ ext_info: ExtensionReq::Require,
+ }),
+ PrfUvOptions::Prf(PrfInput {
+ first: [].as_slice(),
+ second: None,
+ ext_info: ExtensionReq::Allow,
+ }),
+ PrfUvOptions::Prf(PrfInput {
+ first: [].as_slice(),
+ second: Some([].as_slice()),
+ ext_info: ExtensionReq::Require,
+ }),
+ PrfUvOptions::Prf(PrfInput {
+ first: [].as_slice(),
+ second: Some([].as_slice()),
+ ext_info: ExtensionReq::Allow,
+ }),
+ ];
+ for cred_protect in ALL_CRED_PROTECT_OPTIONS {
+ for prf in ALL_PRF_CRED_OPTIONS {
+ for hmac in ALL_HMAC_OPTIONS {
+ for error_unsolicited in ALL_UNSOLICIT_OPTIONS {
+ for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS {
+ assert!(validate(TestOptions {
+ request: TestRequestOptions {
+ error_unsolicited,
+ prf_uv,
+ },
+ response: TestResponseOptions {
+ user_verified: false,
+ hmac,
+ },
+ cred: TestCredOptions { cred_protect, prf, },
+ }).map_or_else(|err| matches!(err, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::UserNotVerified)), |_| false));
+ }
+ }
+ }
+ }
+ }
+ }
+ /// Test all, and only, possible `UserNotVerified` errors.
+ /// 4 * 5 * 2 * 2 = 80 tests.
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[test]
+ fn test_forbidden_hmac() {
+ const ALL_CRED_PROTECT_OPTIONS: [CredentialProtectionPolicy; 4] = [
+ CredentialProtectionPolicy::None,
+ CredentialProtectionPolicy::UserVerificationOptional,
+ CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
+ CredentialProtectionPolicy::UserVerificationRequired,
+ ];
+ const ALL_PRF_CRED_OPTIONS: [PrfCredOptions; 5] = [
+ PrfCredOptions::None,
+ PrfCredOptions::FalseNoHmac,
+ PrfCredOptions::FalseHmacFalse,
+ PrfCredOptions::TrueNoHmac,
+ PrfCredOptions::TrueHmacTrue,
+ ];
+ const ALL_HMAC_OPTIONS: [HmacSecret; 2] = [HmacSecret::One, HmacSecret::Two];
+ const ALL_UV_OPTIONS: [bool; 2] = [false, true];
+ for cred_protect in ALL_CRED_PROTECT_OPTIONS {
+ for prf in ALL_PRF_CRED_OPTIONS {
+ for hmac in ALL_HMAC_OPTIONS {
+ for user_verified in ALL_UV_OPTIONS {
+ assert!(validate(TestOptions {
+ request: TestRequestOptions {
+ error_unsolicited: true,
+ prf_uv: PrfUvOptions::None(false),
+ },
+ response: TestResponseOptions {
+ user_verified,
+ hmac,
+ },
+ cred: TestCredOptions { cred_protect, prf, },
+ }).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));
+ }
+ }
+ }
+ }
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[test]
+ fn test_prf() -> Result<(), AggErr> {
+ let mut opts = TestOptions {
+ request: TestRequestOptions {
+ error_unsolicited: false,
+ prf_uv: PrfUvOptions::Prf(PrfInput {
+ first: [].as_slice(),
+ second: None,
+ ext_info: ExtensionReq::Allow,
+ }),
+ },
+ response: TestResponseOptions {
+ user_verified: true,
+ hmac: HmacSecret::None,
+ },
+ cred: TestCredOptions {
+ cred_protect: CredentialProtectionPolicy::None,
+ prf: PrfCredOptions::None,
+ },
+ };
+ validate(opts)?;
+ opts.request.prf_uv = PrfUvOptions::Prf(PrfInput {
+ first: [].as_slice(),
+ second: None,
+ ext_info: 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));
+ opts.response.hmac = HmacSecret::One;
+ opts.request.prf_uv = PrfUvOptions::Prf(PrfInput {
+ first: [].as_slice(),
+ second: None,
+ ext_info: ExtensionReq::Allow,
+ });
+ opts.cred.prf = PrfCredOptions::TrueNoHmac;
+ validate(opts)?;
+ 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));
+ 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::PrfRequestedForPrfIncapableCred))), |_| false));
+ 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));
+ Ok(())
+ }
}
diff --git a/src/request/auth/ser_server_state.rs b/src/request/auth/ser_server_state.rs
@@ -1,8 +1,3 @@
-#![expect(
- clippy::question_mark_used,
- clippy::unseparated_literal_suffix,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
use super::{
super::super::bin::{
Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible,
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -6,8 +6,8 @@ use super::{
AuthenticatorAttachment,
register::{
Attestation, AttestationFormat, AuthenticatorExtensionOutput,
- AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputs,
- CredentialProtectionPolicy, Registration, UncompressedPubKey,
+ ClientExtensionsOutputs, CredentialProtectionPolicy, Registration,
+ UncompressedPubKey,
error::{ExtensionErr, RegCeremonyErr},
},
},
@@ -49,7 +49,7 @@ pub mod error;
/// Contains functionality to serialize data to a client.
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
#[cfg(feature = "serde")]
-mod ser;
+pub(crate) 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")]
@@ -64,15 +64,24 @@ pub enum CredProtect {
/// Request
/// [`userVerificationOptional`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptional)
/// but allow any.
- UserVerificationOptional(ExtensionInfo),
+ ///
+ /// The `bool` corresponds to
+ /// [`enforceCredentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#dom-authenticationextensionsclientinputs-enforcecredentialprotectionpolicy).
+ UserVerificationOptional(bool, ExtensionInfo),
/// Request
/// [`userVerificationOptionalWithCredentialIDList`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptionalwithcredentialidlist);
/// and when enforcing the value, disallow [`CredentialProtectionPolicy::UserVerificationOptional`].
- UserVerificationOptionalWithCredentialIdList(ExtensionInfo),
+ ///
+ /// The `bool` corresponds to
+ /// [`enforceCredentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#dom-authenticationextensionsclientinputs-enforcecredentialprotectionpolicy).
+ UserVerificationOptionalWithCredentialIdList(bool, ExtensionInfo),
/// Request
/// [`userVerificationRequired`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationrequired);
/// and when enforcing the value, only allow [`CredentialProtectionPolicy::UserVerificationRequired`].
- UserVerificationRequired(ExtensionInfo),
+ ///
+ /// The `bool` corresponds to
+ /// [`enforceCredentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#dom-authenticationextensionsclientinputs-enforcecredentialprotectionpolicy).
+ UserVerificationRequired(bool, ExtensionInfo),
}
impl CredProtect {
/// Validates `other` is allowed based on `self`.
@@ -86,7 +95,7 @@ impl CredProtect {
const fn validate(self, other: CredentialProtectionPolicy) -> Result<(), ExtensionErr> {
match self {
Self::None => Ok(()),
- Self::UserVerificationOptional(info) => {
+ Self::UserVerificationOptional(_, info) => {
if matches!(other, CredentialProtectionPolicy::None) {
if matches!(
info,
@@ -100,7 +109,7 @@ impl CredProtect {
Ok(())
}
}
- Self::UserVerificationOptionalWithCredentialIdList(info) => match info {
+ Self::UserVerificationOptionalWithCredentialIdList(_, info) => match info {
ExtensionInfo::RequireEnforceValue => match other {
CredentialProtectionPolicy::None => Err(ExtensionErr::MissingCredProtect),
CredentialProtectionPolicy::UserVerificationOptional => {
@@ -125,7 +134,7 @@ impl CredProtect {
}
ExtensionInfo::AllowDontEnforceValue => Ok(()),
},
- Self::UserVerificationRequired(info) => match info {
+ Self::UserVerificationRequired(_, info) => match info {
ExtensionInfo::RequireEnforceValue => match other {
CredentialProtectionPolicy::None => Err(ExtensionErr::MissingCredProtect),
CredentialProtectionPolicy::UserVerificationOptional
@@ -162,14 +171,14 @@ impl PartialEq for CredProtect {
fn eq(&self, other: &Self) -> bool {
match *self {
Self::None => matches!(other, Self::None),
- Self::UserVerificationOptional(info) => {
- matches!(*other, Self::UserVerificationOptional(info2) if info == info2)
+ Self::UserVerificationOptional(enforce, info) => {
+ matches!(*other, Self::UserVerificationOptional(enforce2, info2) if enforce == enforce2 && info == info2)
}
- Self::UserVerificationOptionalWithCredentialIdList(info) => {
- matches!(*other, Self::UserVerificationOptionalWithCredentialIdList(info2) if info == info2)
+ Self::UserVerificationOptionalWithCredentialIdList(enforce, info) => {
+ matches!(*other, Self::UserVerificationOptionalWithCredentialIdList(enforce2, info2) if enforce == enforce2 && info == info2)
}
- Self::UserVerificationRequired(info) => {
- matches!(*other, Self::UserVerificationRequired(info2) if info == info2)
+ Self::UserVerificationRequired(enforce, info) => {
+ matches!(*other, Self::UserVerificationRequired(enforce2, info2) if enforce == enforce2 && info == info2)
}
}
}
@@ -179,15 +188,21 @@ impl Display for CredProtect {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::None => f.write_str("do not sent a credProtect request"),
- Self::UserVerificationOptional(info) => {
- write!(f, "request user verification optional and {info}")
+ Self::UserVerificationOptional(enforce, info) => {
+ write!(
+ f,
+ "request user verification optional with enforcement {enforce} and {info}"
+ )
}
- Self::UserVerificationOptionalWithCredentialIdList(info) => write!(
+ Self::UserVerificationOptionalWithCredentialIdList(enforce, info) => write!(
f,
- "user verification optional with credential ID list and {info}"
+ "user verification optional with credential ID list with enforcement {enforce} and {info}"
),
- Self::UserVerificationRequired(info) => {
- write!(f, "user verification required and {info}")
+ Self::UserVerificationRequired(enforce, info) => {
+ write!(
+ f,
+ "user verification required with enforcement {enforce} and {info}"
+ )
}
}
}
@@ -196,19 +211,50 @@ impl Display for CredProtect {
/// as defined in RFC 8266.
///
/// Note [string truncation](https://www.w3.org/TR/webauthn-3/#sctn-strings-truncation) is allowed, so one may
-/// want to enforce their own length requirements in the event [`Self::MAX_LEN`] is too long.
+/// want to enforce [`Self::RECOMMENDED_MAX_LEN`].
#[derive(Clone, Debug)]
pub struct Nickname<'a>(Cow<'a, str>);
-impl Nickname<'_> {
+impl<'a> Nickname<'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 `Nickname` 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<'a>(self) -> Nickname<'a> {
+ pub fn into_owned<'b>(self) -> Nickname<'b> {
Nickname(Cow::Owned(self.0.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` violates [RFC 8266 § 2.3](https://www.rfc-editor.org/rfc/rfc8266.html#section-2.3)
+ /// or the resulting length exceeds [`Self::RECOMMENDED_MAX_LEN`].
+ #[inline]
+ pub fn with_recommended_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, NicknameErr> {
+ precis_profiles::Nickname::new()
+ .enforce(value)
+ .map_err(|_e| NicknameErr::Rfc8266)
+ .and_then(|val| {
+ if val.len() <= Self::RECOMMENDED_MAX_LEN {
+ Ok(Self(val))
+ } else {
+ Err(NicknameErr::Len)
+ }
+ })
+ }
+ /// 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)
+ /// or the resulting length exceeds [`Self::MAX_LEN`].
+ #[inline]
+ pub fn with_max_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, NicknameErr> {
+ Self::try_from(value)
+ }
}
impl AsRef<str> for Nickname<'_> {
#[inline]
@@ -222,6 +268,15 @@ impl Borrow<str> for Nickname<'_> {
self.0.as_ref()
}
}
+impl<'a: 'b, 'b> From<&'a Nickname<'_>> for Nickname<'b> {
+ #[inline]
+ fn from(value: &'a Nickname<'_>) -> Self {
+ match value.0 {
+ Cow::Borrowed(val) => Self(Cow::Borrowed(val)),
+ Cow::Owned(ref val) => Self(Cow::Borrowed(val.as_str())),
+ }
+ }
+}
impl<'a: 'b, 'b> From<Nickname<'a>> for Cow<'b, str> {
#[inline]
fn from(value: Nickname<'a>) -> Self {
@@ -280,24 +335,92 @@ impl TryFrom<String> for Nickname<'_> {
Self::try_from(Cow::Owned(value))
}
}
+impl PartialEq<Nickname<'_>> for Nickname<'_> {
+ #[inline]
+ fn eq(&self, other: &Nickname<'_>) -> bool {
+ self.0 == other.0
+ }
+}
+impl PartialEq<&Nickname<'_>> for Nickname<'_> {
+ #[inline]
+ fn eq(&self, other: &&Nickname<'_>) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<Nickname<'_>> for &Nickname<'_> {
+ #[inline]
+ fn eq(&self, other: &Nickname<'_>) -> bool {
+ **self == *other
+ }
+}
+impl Eq for Nickname<'_> {}
+impl Hash for Nickname<'_> {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.0.hash(state);
+ }
+}
+impl PartialOrd<Nickname<'_>> for Nickname<'_> {
+ #[inline]
+ fn partial_cmp(&self, other: &Nickname<'_>) -> Option<Ordering> {
+ self.0.partial_cmp(&other.0)
+ }
+}
+impl Ord for Nickname<'_> {
+ #[inline]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.0.cmp(&other.0)
+ }
+}
/// String returned from the
/// [UsernameCasePreserved Enforcement rule](https://www.rfc-editor.org/rfc/rfc8265#section-3.4.3) as defined in
/// RFC 8265.
///
/// Note [string truncation](https://www.w3.org/TR/webauthn-3/#sctn-strings-truncation) is allowed, so one may
-/// want to enforce their own length requirements in the event [`Self::MAX_LEN`] is too long.
+/// want to enforce [`Self::RECOMMENDED_MAX_LEN`].
#[derive(Clone, Debug)]
pub struct Username<'a>(Cow<'a, str>);
-impl Username<'_> {
+impl<'a> Username<'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 `Username` 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<'a>(self) -> Username<'a> {
+ pub fn into_owned<'b>(self) -> Username<'b> {
Username(Cow::Owned(self.0.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` violates [RFC 8265 § 3.4.3](https://www.rfc-editor.org/rfc/rfc8265.html#section-3.4.3)
+ /// or the resulting length exceeds [`Self::RECOMMENDED_MAX_LEN`].
+ #[inline]
+ pub fn with_recommended_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, UsernameErr> {
+ UsernameCasePreserved::default()
+ .enforce(value)
+ .map_err(|_e| UsernameErr::Rfc8265)
+ .and_then(|val| {
+ if val.len() <= Self::RECOMMENDED_MAX_LEN {
+ Ok(Self(val))
+ } else {
+ Err(UsernameErr::Len)
+ }
+ })
+ }
+ /// 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)
+ /// or the resulting length exceeds [`Self::MAX_LEN`].
+ #[inline]
+ pub fn with_max_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, UsernameErr> {
+ Self::try_from(value)
+ }
}
impl AsRef<str> for Username<'_> {
#[inline]
@@ -365,6 +488,52 @@ impl<'a: 'b, 'b> From<Username<'a>> for Cow<'b, str> {
value.0
}
}
+impl<'a: 'b, 'b> From<&'a Username<'_>> for Username<'b> {
+ #[inline]
+ fn from(value: &'a Username<'_>) -> Self {
+ match value.0 {
+ Cow::Borrowed(val) => Self(Cow::Borrowed(val)),
+ Cow::Owned(ref val) => Self(Cow::Borrowed(val.as_str())),
+ }
+ }
+}
+impl PartialEq<Username<'_>> for Username<'_> {
+ #[inline]
+ fn eq(&self, other: &Username<'_>) -> bool {
+ self.0 == other.0
+ }
+}
+impl PartialEq<&Username<'_>> for Username<'_> {
+ #[inline]
+ fn eq(&self, other: &&Username<'_>) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<Username<'_>> for &Username<'_> {
+ #[inline]
+ fn eq(&self, other: &Username<'_>) -> bool {
+ **self == *other
+ }
+}
+impl Eq for Username<'_> {}
+impl Hash for Username<'_> {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.0.hash(state);
+ }
+}
+impl PartialOrd<Username<'_>> for Username<'_> {
+ #[inline]
+ fn partial_cmp(&self, other: &Username<'_>) -> Option<Ordering> {
+ self.0.partial_cmp(&other.0)
+ }
+}
+impl Ord for Username<'_> {
+ #[inline]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.0.cmp(&other.0)
+ }
+}
/// [`COSEAlgorithmIdentifier`](https://www.w3.org/TR/webauthn-3/#typedefdef-cosealgorithmidentifier).
///
/// Note the order of variants is the following:
@@ -375,15 +544,19 @@ impl<'a: 'b, 'b> From<Username<'a>> for Cow<'b, str> {
/// that contains `Self::Eddsa` will prioritize it over all others.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum CoseAlgorithmIdentifier {
- /// [EdDSA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms) with the added requirement that
- /// Ed25519 be used for `crv` parameter [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier).
+ /// [EdDSA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+ ///
+ /// Note that Ed25519 must be used for the `crv` parameter
+ /// [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier).
Eddsa,
- /// [ES256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms) with the added requirements that
- /// P-256 be used for `crv` parameter and the uncompressed form must be used
+ /// [ES256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+ ///
+ /// Note the uncompressed form must be used and P-256 must be used for the `crv` parameter
/// [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier).
Es256,
- /// [ES384](https://www.iana.org/assignments/cose/cose.xhtml#algorithms) with the added requirements that
- /// P-384 be used for `crv` parameter and the uncompressed form must be used
+ /// [ES384](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+ ///
+ /// Note the uncompressed form must be used and P-384 must be used for the `crv` parameter
/// [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier).
Es384,
/// [RS256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
@@ -416,7 +589,7 @@ impl PartialEq<CoseAlgorithmIdentifier> for &CoseAlgorithmIdentifier {
#[derive(Clone, Copy, Debug)]
pub struct CoseAlgorithmIdentifiers(u8);
impl CoseAlgorithmIdentifiers {
- /// Contains all [`CoseAlgorithmIdentifiers`].
+ /// Contains all [`CoseAlgorithmIdentifier`]s.
pub const ALL: Self = Self(0)
.add(CoseAlgorithmIdentifier::Eddsa)
.add(CoseAlgorithmIdentifier::Es256)
@@ -472,6 +645,53 @@ impl PartialEq for CoseAlgorithmIdentifiers {
self.0 == other.0
}
}
+/// `newtype` of `u8` bound inclusively between [`Self::MIN`] and [`Self::MAX`].
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct FourToSixtyThree(u8);
+impl FourToSixtyThree {
+ /// Minimum inner value.
+ const MIN_INNER: u8 = 4;
+ /// Maximum inner value.
+ const MAX_INNER: u8 = 63;
+ /// Minimum value.
+ pub const MIN: Self = Self(Self::MIN_INNER);
+ /// Maximum value.
+ pub const MAX: Self = Self(Self::MAX_INNER);
+ /// Returns `Self` iff `val` is inclusively between [`Self::MIN`] and [`Self::MAX`].
+ #[inline]
+ #[must_use]
+ pub const fn new(val: u8) -> Option<Self> {
+ match val {
+ Self::MIN_INNER..=Self::MAX_INNER => Some(Self(val)),
+ _ => None,
+ }
+ }
+ /// Returns the contained value.
+ #[inline]
+ #[must_use]
+ pub const fn value(self) -> u8 {
+ self.0
+ }
+}
+impl Display for FourToSixtyThree {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+impl From<FourToSixtyThree> for u8 {
+ #[inline]
+ fn from(value: FourToSixtyThree) -> Self {
+ value.0
+ }
+}
+impl Default for FourToSixtyThree {
+ /// Returns [`Self::MIN`].
+ #[inline]
+ fn default() -> Self {
+ Self::MIN
+ }
+}
/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client.
#[derive(Clone, Copy, Debug, Default)]
pub struct Extension {
@@ -496,25 +716,28 @@ pub struct Extension {
/// When the value is enforced, that corresponds to
/// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension)
/// in [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) set to a value at least as large
- /// as the contained `u8`.
- pub min_pin_length: Option<(u8, ExtensionInfo)>,
+ /// as the contained `FourToSixtyThree`.
+ pub min_pin_length: Option<(FourToSixtyThree, ExtensionInfo)>,
/// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension).
///
/// When the value is enforced, that corresponds to
- /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled) and
- /// [`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)
- /// in [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) set to `true`. In contrast
- /// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) must not exist,
- /// be `null`, or be an
+ /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled) set to `true`.
+ /// In contrast [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results)
+ /// must not exist, be `null`, or be an
/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues)
- /// such that [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is `null` and
- /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) does not exist or is
- /// `null`. This is to ensure the decrypted outputs stay on the client.
+ /// such that [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is `null`
+ /// and [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) does not
+ /// exist or is `null`. This is to ensure the decrypted outputs stay on the client.
///
/// Note for
/// [CTAP 2.2](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
/// this is only used to instruct the authenticator to create the necessary `hmac-secret` since it does not
- /// currently support PRF evaluation at creation time. This also requires [`UserVerificationRequirement::Required`].
+ /// currently support PRF evaluation at creation time. This also requires
+ /// [`UserVerificationRequirement::Required`].
+ ///
+ /// While many authenticators will be able to use the `prf` extension during authentication when this is not
+ /// passed during registration, it is recommended to still pass this during registration in the event the
+ /// authenticator does require it during registration.
pub prf: Option<ExtensionInfo>,
}
impl Extension {
@@ -564,15 +787,18 @@ impl Extension {
Err(ExtensionErr::ForbiddenMinPinLength)
} else {
// Pretend to set `minPinLength`, so we can check `prf`.
- self.min_pin_length = Some((0, ExtensionInfo::RequireEnforceValue));
+ self.min_pin_length =
+ Some((FourToSixtyThree::MIN, ExtensionInfo::RequireEnforceValue));
self.validate_unsolicited(client_ext, auth_ext)
}
} else if !matches!(auth_ext.cred_protect, CredentialProtectionPolicy::None) {
Err(ExtensionErr::ForbiddenCredProtect)
} else {
// Pretend to set `credProtect`, so we can check `minPinLength` and `prf` extensions.
- self.cred_protect =
- CredProtect::UserVerificationOptional(ExtensionInfo::RequireEnforceValue);
+ self.cred_protect = CredProtect::UserVerificationOptional(
+ false,
+ ExtensionInfo::RequireEnforceValue,
+ );
self.validate_unsolicited(client_ext, auth_ext)
}
} else if client_ext.cred_props.is_some() {
@@ -638,10 +864,7 @@ impl Extension {
| ExtensionInfo::RequireDontEnforceValue
) {
if client_ext.prf.is_some() {
- auth_ext
- .hmac_secret
- .ok_or(ExtensionErr::MissingHmacSecret)
- .map(|_| ())
+ Ok(())
} else {
Err(ExtensionErr::MissingPrf)
}
@@ -732,8 +955,28 @@ pub const USER_HANDLE_MAX_LEN: usize = 64;
pub const USER_HANDLE_MIN_LEN: usize = 1;
/// A [user handle](https://www.w3.org/TR/webauthn-3/#user-handle) that is made up of
/// [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] bytes.
-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct UserHandle<const LEN: usize>([u8; LEN]);
+impl<const LEN: usize> UserHandle<LEN> {
+ /// Returns the contained data as a `slice`.
+ #[inline]
+ #[must_use]
+ pub const fn as_slice(&self) -> &[u8] {
+ self.0.as_slice()
+ }
+ /// Returns the contained data as a shared reference to the array.
+ #[inline]
+ #[must_use]
+ pub const fn as_array(&self) -> &[u8; LEN] {
+ &self.0
+ }
+ /// Returns the contained data.
+ #[inline]
+ #[must_use]
+ pub const fn into_array(self) -> [u8; LEN] {
+ self.0
+ }
+}
/// Implements [`Default`] for [`UserHandle`] of array of length of the passed `usize` literal.
///
/// Only [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] inclusively are allowed to be passed.
@@ -789,13 +1032,37 @@ where
impl<const LEN: usize> AsRef<[u8]> for UserHandle<LEN> {
#[inline]
fn as_ref(&self) -> &[u8] {
- self.0.as_slice()
+ self.as_slice()
}
}
impl<const LEN: usize> Borrow<[u8]> for UserHandle<LEN> {
#[inline]
fn borrow(&self) -> &[u8] {
- self.0.as_slice()
+ self.as_slice()
+ }
+}
+impl<const LEN: usize> PartialEq<&Self> for UserHandle<LEN> {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl<const LEN: usize> PartialEq<UserHandle<LEN>> for &UserHandle<LEN> {
+ #[inline]
+ fn eq(&self, other: &UserHandle<LEN>) -> bool {
+ **self == *other
+ }
+}
+impl<const LEN: usize> From<UserHandle<LEN>> for [u8; LEN] {
+ #[inline]
+ fn from(value: UserHandle<LEN>) -> Self {
+ value.into_array()
+ }
+}
+impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<LEN>> for &'b [u8; LEN] {
+ #[inline]
+ fn from(value: &'a UserHandle<LEN>) -> Self {
+ value.as_array()
}
}
/// `UserHandle` that is based on the [spec recommendation](https://www.w3.org/TR/webauthn-3/#user-handle).
@@ -805,6 +1072,71 @@ pub type UserHandle64 = UserHandle<USER_HANDLE_MAX_LEN>;
/// While not the recommended size like [`UserHandle64`], 16 bytes is common for many deployments since
/// it's the same size as [Universally Unique IDentifiers (UUIDs)](https://www.rfc-editor.org/rfc/rfc9562).
pub type UserHandle16 = UserHandle<16>;
+impl UserHandle16 {
+ /// Same as [`Self::new`] except 6 bits of metadata is encoded to conform with
+ /// [UUID Version 4](https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-4).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::UserHandle16;
+ /// assert!(UserHandle16::new_uuid_v4().is_uuid_v4());
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn new_uuid_v4() -> Self {
+ let mut this = Self::new();
+ // The first 4 bits of the 6th octet (0-based index) represents the UUID version (i.e., 4).
+ // We first 0-out the version bits retaining the other 4 bits, then set the version bits to 4.
+ this.0[6] = (this.0[6] & 0x0F) | 0x40;
+ // The first 2 bits of the 8th octet (0-based index) represents the UUID variant (i.e., 8,9,A,B)
+ // which is defined to be 2.
+ // We first 0-out the variant bits retaining the other 6 bits, then set the variant bits to 2.
+ this.0[8] = (this.0[8] & 0x3F) | 0x80;
+ this
+ }
+ /// Returns `true` iff `self` is a valid
+ /// [UUID Version 4](https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-4).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::UserHandle16;
+ /// assert!(UserHandle16::new_uuid_v4().is_uuid_v4());
+ /// let mut user = UserHandle16::new_uuid_v4().into_array();
+ /// user[6] = 255;
+ /// # #[cfg(feature = "custom")]
+ /// assert!(!UserHandle16::from(user).is_uuid_v4());
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn is_uuid_v4(&self) -> bool {
+ // The first 4 bits of the 6th octet (0-based index) represents the UUID version (i.e., 4).
+ // The first 2 bits of the 8th octet (0-based index) represents the UUID variant (i.e., 8,9,A,B) which
+ // is defined to be 2.
+ self.0[6] >> 4 == 0x4 && self.0[8] >> 6 == 0x2
+ }
+ /// Returns `Some` containing `uuid_v4` iff `uuid_v4` is a valid
+ /// [UUID Version 4](https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-4).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::UserHandle16;
+ /// let mut user = UserHandle16::new_uuid_v4().into_array();
+ /// assert!(UserHandle16::from_uuid_v4(user).is_some());
+ /// user[8] = 255;
+ /// assert!(UserHandle16::from_uuid_v4(user).is_none());
+ /// ```
+ #[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
+ #[cfg(feature = "custom")]
+ #[inline]
+ #[must_use]
+ pub fn from_uuid_v4(uuid_v4: [u8; 16]) -> Option<Self> {
+ let this = Self(uuid_v4);
+ this.is_uuid_v4().then_some(this)
+ }
+}
/// [The `PublicKeyCredentialUserEntity`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity)
/// sent to the client.
#[derive(Clone, Debug)]
@@ -851,6 +1183,12 @@ impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<LEN>>
}
}
}
+/// `PublicKeyCredentialUserEntity` based on a [`UserHandle64`].
+pub type PublicKeyCredentialUserEntity64<'name, 'display_name, 'id> =
+ PublicKeyCredentialUserEntity<'name, 'display_name, 'id, USER_HANDLE_MAX_LEN>;
+/// `PublicKeyCredentialUserEntity` based on a [`UserHandle16`].
+pub type PublicKeyCredentialUserEntity16<'name, 'display_name, 'id> =
+ PublicKeyCredentialUserEntity<'name, 'display_name, 'id, 16>;
/// [`ResidentKeyRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement) sent to the client.
#[derive(Clone, Copy, Debug)]
pub enum ResidentKeyRequirement {
@@ -1140,7 +1478,7 @@ const fn validate_options_helper(
Err(CreationOptionsErr::PrfWithoutUserVerification)
} else if matches!(
extensions.cred_protect,
- CredProtect::UserVerificationRequired(_)
+ CredProtect::UserVerificationRequired(_, _)
) {
Err(CreationOptionsErr::CredProtectRequiredWithoutUserVerification)
} else {
@@ -1195,7 +1533,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
///
/// Creates a `PublicKeyCredentialCreationOptions` that requires the authenticator to create a client-side
/// discoverable credential enforcing any form of user verification. A five-minute timeout is set.
- /// [`Extension::cred_protect`] with [`CredProtect::UserVerificationRequired`] and
+ /// [`Extension::cred_protect`] with [`CredProtect::UserVerificationRequired`] with `false` and
/// [`ExtensionInfo::AllowEnforceValue`] is used. [`Self::mediation`] is
/// [`CredentialMediationRequirement::Optional`].
///
@@ -1241,6 +1579,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
extensions: Extension {
cred_props: None,
cred_protect: CredProtect::UserVerificationRequired(
+ false,
ExtensionInfo::AllowEnforceValue,
),
min_pin_length: None,
@@ -1283,7 +1622,9 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
///
/// Creates a `PublicKeyCredentialCreationOptions` that prefers the authenticator to create a server-side
/// credential without requiring user verification. A five-minute timeout is set. [`Extension::cred_props`]
- /// is [`ExtensionReq::Allow`]. [`Self::mediation`] is [`CredentialMediationRequirement::Optional`].
+ /// is [`ExtensionReq::Allow`]. [`Extension::cred_protect`] is
+ /// [`CredProtect::UserVerificationOptionalWithCredentialIdList`] with `false` and
+ /// [`ExtensionInfo::AllowEnforceValue`]. [`Self::mediation`] is [`CredentialMediationRequirement::Optional`].
///
/// Note some authenticators require user verification during credential registration (e.g.,
/// [CTAP 2.0 authenticators](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#authenticatorMakeCredential)).
@@ -1328,7 +1669,10 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
let mut opts = Self::passkey(rp_id, user, exclude_credentials);
opts.authenticator_selection = AuthenticatorSelectionCriteria::second_factor();
opts.extensions.cred_props = Some(ExtensionReq::Allow);
- opts.extensions.cred_protect = CredProtect::None;
+ opts.extensions.cred_protect = CredProtect::UserVerificationOptionalWithCredentialIdList(
+ false,
+ ExtensionInfo::AllowEnforceValue,
+ );
opts
}
/// Convenience function for [`Self::second_factor`] passing an empty `Vec`.
@@ -1415,6 +1759,18 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
})
}
}
+/// `PublicKeyCredentialCreationOptions` based on a [`UserHandle64`].
+pub type PublicKeyCredentialCreationOptions64<'rp_id, 'user_name, 'user_display_name, 'user_id> =
+ PublicKeyCredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ USER_HANDLE_MAX_LEN,
+ >;
+/// `PublicKeyCredentialCreationOptions` based on a [`UserHandle16`].
+pub type PublicKeyCredentialCreationOptions16<'rp_id, 'user_name, 'user_display_name, 'user_id> =
+ PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_id, 16>;
/// Container of a [`PublicKeyCredentialCreationOptions`] that has been used to start the registration ceremony.
/// This gets sent to the client ASAP.
#[derive(Debug)]
@@ -1469,6 +1825,12 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
&self.0
}
}
+/// `RegistrationClientState` based on a [`UserHandle64`].
+pub type RegistrationClientState64<'rp_id, 'user_name, 'user_display_name, 'user_id> =
+ RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_id, USER_HANDLE_MAX_LEN>;
+/// `RegistrationClientState` based on a [`UserHandle16`].
+pub type RegistrationClientState16<'rp_id, 'user_name, 'user_display_name, 'user_id> =
+ RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_id, 16>;
/// Additional verification options to perform in [`RegistrationServerState::verify`].
#[derive(Clone, Copy, Debug)]
pub struct RegistrationVerificationOptions<'origins, 'top_origins, O, T> {
@@ -1655,11 +2017,10 @@ impl<const USER_LEN: usize> RegistrationServerState<USER_LEN> {
StaticState {
credential_public_key: attested_credential_data
.credential_public_key,
- extensions:
- AuthenticatorExtensionOutputStaticState {
- cred_protect: extensions.cred_protect,
- hmac_secret: extensions.hmac_secret,
- },
+ extensions: extensions.into(),
+ client_extension_results: response
+ .client_extension_results
+ .into(),
},
DynamicState {
user_verified: flags.user_verified,
@@ -1682,7 +2043,8 @@ impl<const USER_LEN: usize> RegistrationServerState<USER_LEN> {
aaguid: attested_credential_data.aaguid,
extensions: extensions.into(),
client_extension_results: response
- .client_extension_results,
+ .client_extension_results
+ .into(),
resident_key: self
.authenticator_selection
.resident_key,
@@ -1721,10 +2083,6 @@ impl<const USER_LEN: usize> TimedCeremony for RegistrationServerState<USER_LEN>
fn expiration(&self) -> SystemTime {
self.expiration
}
- #[inline]
- fn sent_challenge(&self) -> SentChallenge {
- self.challenge
- }
}
impl<const USER_LEN: usize> Ceremony<USER_LEN, false> for RegistrationServerState<USER_LEN> {
type R = Registration;
@@ -1788,41 +2146,119 @@ impl<const USER_LEN: usize> Ord for RegistrationServerState<USER_LEN> {
self.challenge.cmp(&other.challenge)
}
}
+/// `RegistrationServerState` based on a [`UserHandle64`].
+pub type RegistrationServerState64 = RegistrationServerState<USER_HANDLE_MAX_LEN>;
+/// `RegistrationServerState` based on a [`UserHandle16`].
+pub type RegistrationServerState16 = RegistrationServerState<16>;
#[cfg(test)]
mod tests {
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
+ use super::{
+ super::{super::AggErr, ExtensionInfo},
+ Challenge, CredProtect, FourToSixtyThree, PublicKeyCredentialCreationOptions, RpId,
+ UserHandle,
+ };
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
use super::{
super::{
+ super::bin::{Decode as _, Encode as _},
+ AsciiDomain,
+ },
+ Extension, PublicKeyCredentialUserEntity, RegistrationServerState,
+ };
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ use super::{
+ super::{
super::{
- AggErr,
- bin::{Decode as _, Encode as _},
+ CredentialErr,
+ response::register::{
+ AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation,
+ ClientExtensionsOutputs, CredentialPropertiesOutput,
+ CredentialProtectionPolicy,
+ },
},
- AsciiDomain,
+ AuthTransports,
},
- Challenge, CredProtect, Extension, ExtensionInfo, PublicKeyCredentialCreationOptions,
- PublicKeyCredentialUserEntity, RegistrationServerState, RpId, UserHandle,
+ AuthenticatorAttachment, BackupReq, ExtensionErr, ExtensionReq, RegCeremonyErr,
+ Registration, RegistrationVerificationOptions, UserVerificationRequirement,
};
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
use ed25519_dalek::{Signer as _, SigningKey};
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
use rsa::sha2::{Digest as _, Sha256};
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_UINT: u8 = 0b000_00000;
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_NEG: u8 = 0b001_00000;
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_BYTES: u8 = 0b010_00000;
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_TEXT: u8 = 0b011_00000;
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_MAP: u8 = 0b101_00000;
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_SIMPLE: u8 = 0b111_00000;
- #[cfg(all(feature = "custom", feature = "serializable_server_state"))]
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ const CBOR_FALSE: u8 = CBOR_SIMPLE | 20;
+ #[cfg(all(
+ feature = "custom",
+ any(
+ feature = "serializable_server_state",
+ not(any(feature = "bin", feature = "serde"))
+ )
+ ))]
const CBOR_TRUE: u8 = CBOR_SIMPLE | 21;
#[test]
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
- fn ed25519_reg_ser() -> Result<(), AggErr> {
+ fn eddsa_reg_ser() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0; 1]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
@@ -1837,8 +2273,15 @@ mod tests {
opts.challenge = Challenge(0);
opts.extensions = Extension {
cred_props: None,
- cred_protect: CredProtect::UserVerificationRequired(ExtensionInfo::RequireEnforceValue),
- min_pin_length: Some((10, ExtensionInfo::RequireEnforceValue)),
+ cred_protect: CredProtect::UserVerificationRequired(
+ false,
+ ExtensionInfo::RequireEnforceValue,
+ ),
+ min_pin_length: Some((
+ FourToSixtyThree::new(10)
+ .unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new")),
+ ExtensionInfo::RequireEnforceValue,
+ )),
prf: Some(ExtensionInfo::RequireEnforceValue),
};
let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
@@ -2156,4 +2599,701 @@ mod tests {
);
Ok(())
}
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ struct TestResponseOptions {
+ user_verified: bool,
+ cred_protect: CredentialProtectionPolicy,
+ prf: Option<bool>,
+ hmac: Option<bool>,
+ min_pin: Option<FourToSixtyThree>,
+ cred_props: Option<Option<bool>>,
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ enum PrfUvOptions {
+ /// `true` iff `UserVerificationRequirement::Required` should be used; otherwise
+ /// `UserVerificationRequirement::Preferred` is used.
+ None(bool),
+ Prf(ExtensionInfo),
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ struct TestRequestOptions {
+ error_unsolicited: bool,
+ protect: CredProtect,
+ prf_uv: PrfUvOptions,
+ props: Option<ExtensionReq>,
+ pin: Option<(FourToSixtyThree, ExtensionInfo)>,
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ #[derive(Clone, Copy)]
+ struct TestOptions {
+ request: TestRequestOptions,
+ response: TestResponseOptions,
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn generate_client_data_json() -> Vec<u8> {
+ let mut json = Vec::with_capacity(256);
+ json.extend_from_slice(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice());
+ json
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn generate_attestation_object(options: TestResponseOptions) -> Vec<u8> {
+ let mut attestation_object = Vec::with_capacity(256);
+ attestation_object.extend_from_slice(
+ [
+ CBOR_MAP | 3,
+ CBOR_TEXT | 3,
+ b'f',
+ b'm',
+ b't',
+ CBOR_TEXT | 4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ CBOR_TEXT | 7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ CBOR_MAP,
+ CBOR_TEXT | 8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ CBOR_BYTES | 24,
+ // Length.
+ 113 + if matches!(options.cred_protect, CredentialProtectionPolicy::None) {
+ if options.hmac.is_some() {
+ 14 + options.min_pin.map_or(0, |_| 14)
+ } else {
+ options.min_pin.map_or(0, |_| 15)
+ }
+ } else {
+ 14 + options.hmac.map_or(0, |_| 13) + options.min_pin.map_or(0, |_| 14)
+ },
+ // RP ID HASH.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // FLAGS.
+ // UP, UV, AT, and ED (right-to-left).
+ 0b0100_0001
+ | if options.user_verified {
+ 0b0000_0100
+ } else {
+ 0b0000_0000
+ }
+ | if matches!(options.cred_protect, CredentialProtectionPolicy::None)
+ && options.hmac.is_none()
+ && options.min_pin.is_none()
+ {
+ 0
+ } else {
+ 0b1000_0000
+ },
+ // COUNTER.
+ // 0 as 32-bit big endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ // AAGUID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // L.
+ // CREDENTIAL ID length is 16 as 16-bit big endian.
+ 0,
+ 16,
+ // CREDENTIAL ID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ CBOR_MAP | 4,
+ // COSE kty.
+ CBOR_UINT | 1,
+ // COSE OKP.
+ CBOR_UINT | 1,
+ // COSE alg.
+ CBOR_UINT | 3,
+ // COSE Eddsa.
+ CBOR_NEG | 7,
+ // COSE OKP crv.
+ CBOR_NEG,
+ // COSE Ed25519.
+ CBOR_UINT | 6,
+ // COSE OKP x.
+ CBOR_NEG | 1,
+ CBOR_BYTES | 24,
+ // Length is 32.
+ 32,
+ // Compressed-y coordinate.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ attestation_object[30..62]
+ .copy_from_slice(Sha256::digest("example.com".as_bytes()).as_slice());
+ if matches!(options.cred_protect, CredentialProtectionPolicy::None) {
+ if options.hmac.is_some() {
+ if options.min_pin.is_some() {
+ attestation_object.push(CBOR_MAP | 2);
+ } else {
+ attestation_object.push(CBOR_MAP | 1);
+ }
+ } else if options.min_pin.is_some() {
+ attestation_object.push(CBOR_MAP | 1);
+ }
+ } else {
+ attestation_object.extend_from_slice(
+ [
+ CBOR_MAP | 1 + u8::from(options.hmac.is_some()) + u8::from(options.min_pin.is_some()),
+ // CBOR text of length 11.
+ CBOR_TEXT | 11,
+ b'c',
+ b'r',
+ b'e',
+ b'd',
+ b'P',
+ b'r',
+ b'o',
+ b't',
+ b'e',
+ b'c',
+ b't',
+ match options.cred_protect { CredentialProtectionPolicy::None => unreachable!("bug"), CredentialProtectionPolicy::UserVerificationOptional => 1, CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => 2, CredentialProtectionPolicy::UserVerificationRequired => 3, },
+ ].as_slice()
+ )
+ }
+ options.hmac.map(|h| {
+ attestation_object.extend_from_slice(
+ [
+ // CBOR text of length 11.
+ CBOR_TEXT | 11,
+ b'h',
+ b'm',
+ b'a',
+ b'c',
+ b'-',
+ b's',
+ b'e',
+ b'c',
+ b'r',
+ b'e',
+ b't',
+ if h { CBOR_TRUE } else { CBOR_FALSE },
+ ]
+ .as_slice(),
+ );
+ });
+ options.min_pin.map(|p| {
+ assert!(p.value() <= 23, "bug");
+ attestation_object.extend_from_slice(
+ [
+ // CBOR text of length 12.
+ CBOR_TEXT | 12,
+ b'm',
+ b'i',
+ b'n',
+ b'P',
+ b'i',
+ b'n',
+ b'L',
+ b'e',
+ b'n',
+ b'g',
+ b't',
+ b'h',
+ CBOR_UINT | p.value(),
+ ]
+ .as_slice(),
+ );
+ });
+ attestation_object
+ }
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn validate(options: TestOptions) -> Result<(), AggErr> {
+ let rp_id = RpId::Domain("example.com".to_owned().try_into()?);
+ let registration = Registration::new(
+ AuthenticatorAttestation::new(
+ generate_client_data_json(),
+ generate_attestation_object(options.response),
+ AuthTransports::NONE,
+ ),
+ AuthenticatorAttachment::None,
+ ClientExtensionsOutputs {
+ cred_props: options
+ .response
+ .cred_props
+ .map(|rk| CredentialPropertiesOutput { rk }),
+ prf: options
+ .response
+ .prf
+ .map(|enabled| AuthenticationExtensionsPrfOutputs { enabled }),
+ },
+ );
+ let reg_opts = RegistrationVerificationOptions::<'static, 'static, &str, &str> {
+ allowed_origins: [].as_slice(),
+ allowed_top_origins: None,
+ backup_requirement: BackupReq::None,
+ error_on_unsolicited_extensions: options.request.error_unsolicited,
+ require_authenticator_attachment: false,
+ #[cfg(feature = "serde_relaxed")]
+ client_data_json_relaxed: false,
+ };
+ let user = UserHandle::from([0; 1]);
+ let mut opts =
+ PublicKeyCredentialCreationOptions::first_passkey_with_blank_user_info(&rp_id, &user);
+ opts.challenge = Challenge(0);
+ opts.authenticator_selection.user_verification = UserVerificationRequirement::Preferred;
+ match options.request.prf_uv {
+ PrfUvOptions::None(required) => {
+ if required
+ || matches!(
+ options.request.protect,
+ CredProtect::UserVerificationRequired(_, _)
+ )
+ {
+ opts.authenticator_selection.user_verification =
+ UserVerificationRequirement::Required;
+ }
+ }
+ PrfUvOptions::Prf(info) => {
+ opts.authenticator_selection.user_verification =
+ UserVerificationRequirement::Required;
+ opts.extensions.prf = Some(info);
+ }
+ }
+ opts.extensions.cred_protect = options.request.protect;
+ opts.extensions.cred_props = options.request.props;
+ opts.extensions.min_pin_length = options.request.pin;
+ opts.start_ceremony()?
+ .0
+ .verify(&rp_id, ®istration, ®_opts)
+ .map_err(AggErr::RegCeremony)
+ .map(|_| ())
+ }
+ /// Test all, and only, possible `UserNotVerified` errors.
+ /// 4 * 3 * 3 * 2 * 13 * 5 * 3 * 5 * 4 * 4 = 1,123,200 tests.
+ /// We ignore this due to how long it takes (around 30 seconds or so).
+ #[test]
+ #[ignore]
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn test_uv_required_err() {
+ const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [
+ CredentialProtectionPolicy::None,
+ CredentialProtectionPolicy::UserVerificationOptional,
+ CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
+ CredentialProtectionPolicy::UserVerificationRequired,
+ ];
+ const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)];
+ const ALL_HMAC_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)];
+ const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true];
+ const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [
+ CredProtect::None,
+ CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue),
+ CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue),
+ CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue),
+ CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue),
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ false,
+ ExtensionInfo::RequireEnforceValue,
+ ),
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ true,
+ ExtensionInfo::RequireDontEnforceValue,
+ ),
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ false,
+ ExtensionInfo::AllowEnforceValue,
+ ),
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ true,
+ ExtensionInfo::AllowDontEnforceValue,
+ ),
+ CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue),
+ CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue),
+ CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue),
+ CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue),
+ ];
+ const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [
+ PrfUvOptions::None(true),
+ PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue),
+ PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue),
+ PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue),
+ PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue),
+ ];
+ const ALL_PROPS_OPTIONS: [Option<ExtensionReq>; 3] =
+ [None, Some(ExtensionReq::Require), Some(ExtensionReq::Allow)];
+ const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [
+ None,
+ Some((
+ FourToSixtyThree::new(5).unwrap(),
+ ExtensionInfo::RequireEnforceValue,
+ )),
+ Some((
+ FourToSixtyThree::new(5).unwrap(),
+ ExtensionInfo::RequireDontEnforceValue,
+ )),
+ Some((
+ FourToSixtyThree::new(5).unwrap(),
+ ExtensionInfo::AllowEnforceValue,
+ )),
+ Some((
+ FourToSixtyThree::new(5).unwrap(),
+ ExtensionInfo::AllowDontEnforceValue,
+ )),
+ ];
+ const ALL_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 4] =
+ [None, Some(None), Some(Some(false)), Some(Some(true))];
+ const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [
+ None,
+ Some(FourToSixtyThree::MIN),
+ Some(FourToSixtyThree::new(5).unwrap()),
+ Some(FourToSixtyThree::new(6).unwrap()),
+ ];
+ for cred_protect in ALL_CRED_PROTECTION_OPTIONS {
+ for prf in ALL_PRF_OPTIONS {
+ for hmac in ALL_HMAC_OPTIONS {
+ for cred_props in ALL_CRED_PROPS_OPTIONS {
+ for min_pin in ALL_MIN_PIN_OPTIONS {
+ for error_unsolicited in ALL_UNSOLICIT_OPTIONS {
+ for protect in ALL_CRED_PROTECT_OPTIONS {
+ for prf_uv in ALL_NOT_FALSE_PRF_UV_OPTIONS {
+ for props in ALL_PROPS_OPTIONS {
+ for pin in ALL_PIN_OPTIONS {
+ assert!(validate(TestOptions {
+ request: TestRequestOptions {
+ error_unsolicited,
+ protect,
+ prf_uv,
+ props,
+ pin,
+ },
+ response: TestResponseOptions {
+ user_verified: false,
+ hmac,
+ cred_protect,
+ prf,
+ min_pin,
+ cred_props,
+ },
+ }).map_or_else(|err| matches!(err, AggErr::RegCeremony(reg_err) if matches!(reg_err, RegCeremonyErr::UserNotVerified)), |_| false));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ /// Test all, and only, possible `ForbiddenCredProps` errors.
+ /// 4 * 3 * 3 * 2 * 13 * 6 * 5 * 3 * 4 = 336,960
+ /// -
+ /// 4 * 3 * 3 * 4 * 6 * 5 * 3 * 4 = 51,840
+ /// -
+ /// 4 * 3 * 3 * 13 * 5 * 5 * 3 * 4 = 140,400
+ /// +
+ /// 4 * 3 * 3 * 4 * 5 * 5 * 3 * 4 = 43,200
+ /// =
+ /// 187,920 total tests.
+ /// We ignore this due to how long it takes (around 6 seconds or so).
+ #[test]
+ #[ignore]
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn test_forbidden_cred_props() {
+ const ALL_CRED_PROTECTION_OPTIONS: [CredentialProtectionPolicy; 4] = [
+ CredentialProtectionPolicy::None,
+ CredentialProtectionPolicy::UserVerificationOptional,
+ CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
+ CredentialProtectionPolicy::UserVerificationRequired,
+ ];
+ const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)];
+ const ALL_HMAC_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)];
+ const ALL_UV_OPTIONS: [bool; 2] = [false, true];
+ const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [
+ CredProtect::None,
+ CredProtect::UserVerificationOptional(false, ExtensionInfo::RequireEnforceValue),
+ CredProtect::UserVerificationOptional(true, ExtensionInfo::RequireDontEnforceValue),
+ CredProtect::UserVerificationOptional(false, ExtensionInfo::AllowEnforceValue),
+ CredProtect::UserVerificationOptional(true, ExtensionInfo::AllowDontEnforceValue),
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ false,
+ ExtensionInfo::RequireEnforceValue,
+ ),
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ true,
+ ExtensionInfo::RequireDontEnforceValue,
+ ),
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ false,
+ ExtensionInfo::AllowEnforceValue,
+ ),
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ true,
+ ExtensionInfo::AllowDontEnforceValue,
+ ),
+ CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue),
+ CredProtect::UserVerificationRequired(true, ExtensionInfo::RequireDontEnforceValue),
+ CredProtect::UserVerificationRequired(false, ExtensionInfo::AllowEnforceValue),
+ CredProtect::UserVerificationRequired(true, ExtensionInfo::AllowDontEnforceValue),
+ ];
+ const ALL_PRF_UV_OPTIONS: [PrfUvOptions; 6] = [
+ PrfUvOptions::None(false),
+ PrfUvOptions::None(true),
+ PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue),
+ PrfUvOptions::Prf(ExtensionInfo::RequireDontEnforceValue),
+ PrfUvOptions::Prf(ExtensionInfo::AllowEnforceValue),
+ PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue),
+ ];
+ const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [
+ None,
+ Some((
+ FourToSixtyThree::new(5).unwrap(),
+ ExtensionInfo::RequireEnforceValue,
+ )),
+ Some((
+ FourToSixtyThree::new(5).unwrap(),
+ ExtensionInfo::RequireDontEnforceValue,
+ )),
+ Some((
+ FourToSixtyThree::new(5).unwrap(),
+ ExtensionInfo::AllowEnforceValue,
+ )),
+ Some((
+ FourToSixtyThree::new(5).unwrap(),
+ ExtensionInfo::AllowDontEnforceValue,
+ )),
+ ];
+ const ALL_NON_EMPTY_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 3] =
+ [Some(None), Some(Some(false)), Some(Some(true))];
+ const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [
+ None,
+ Some(FourToSixtyThree::MIN),
+ Some(FourToSixtyThree::new(5).unwrap()),
+ Some(FourToSixtyThree::new(6).unwrap()),
+ ];
+ for cred_protect in ALL_CRED_PROTECTION_OPTIONS {
+ for prf in ALL_PRF_OPTIONS {
+ for hmac in ALL_HMAC_OPTIONS {
+ for cred_props in ALL_NON_EMPTY_CRED_PROPS_OPTIONS {
+ for min_pin in ALL_MIN_PIN_OPTIONS {
+ for user_verified in ALL_UV_OPTIONS {
+ for protect in ALL_CRED_PROTECT_OPTIONS {
+ for prf_uv in ALL_PRF_UV_OPTIONS {
+ for pin in ALL_PIN_OPTIONS {
+ if user_verified
+ || (!matches!(
+ protect,
+ CredProtect::UserVerificationRequired(_, _)
+ ) && matches!(prf_uv, PrfUvOptions::None(uv) if !uv))
+ {
+ assert!(validate(TestOptions {
+ request: TestRequestOptions {
+ error_unsolicited: true,
+ protect,
+ prf_uv,
+ props: None,
+ pin,
+ },
+ response: TestResponseOptions {
+ user_verified,
+ hmac,
+ cred_protect,
+ prf,
+ 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));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ #[test]
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn test_prf() -> Result<(), AggErr> {
+ let mut opts = TestOptions {
+ request: TestRequestOptions {
+ error_unsolicited: false,
+ protect: CredProtect::None,
+ prf_uv: PrfUvOptions::Prf(ExtensionInfo::RequireEnforceValue),
+ props: None,
+ pin: None,
+ },
+ response: TestResponseOptions {
+ user_verified: true,
+ hmac: None,
+ cred_protect: CredentialProtectionPolicy::None,
+ prf: Some(true),
+ min_pin: None,
+ cred_props: None,
+ },
+ };
+ 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));
+ opts.response.hmac = Some(false);
+ 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));
+ opts.request.prf_uv = PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue);
+ opts.response.hmac = Some(true);
+ 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));
+ opts.response.hmac = Some(false);
+ 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));
+ 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));
+ opts.response.prf = Some(false);
+ validate(opts)?;
+ opts.request.prf_uv = PrfUvOptions::None(false);
+ opts.response.user_verified = false;
+ opts.response.hmac = None;
+ 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::PrfWithoutUserVerified))), |_| false));
+ opts.response.prf = None;
+ validate(opts)?;
+ Ok(())
+ }
+ #[test]
+ #[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
+ fn test_cred_protect() -> Result<(), AggErr> {
+ let mut opts = TestOptions {
+ request: TestRequestOptions {
+ error_unsolicited: false,
+ protect: CredProtect::UserVerificationRequired(
+ false,
+ ExtensionInfo::RequireEnforceValue,
+ ),
+ prf_uv: PrfUvOptions::None(false),
+ props: None,
+ pin: None,
+ },
+ response: TestResponseOptions {
+ user_verified: true,
+ hmac: None,
+ cred_protect: CredentialProtectionPolicy::UserVerificationRequired,
+ prf: None,
+ min_pin: None,
+ cred_props: None,
+ },
+ };
+ 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));
+ 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));
+ Ok(())
+ }
}
diff --git a/src/request/register/error.rs b/src/request/register/error.rs
@@ -16,7 +16,8 @@ pub enum NicknameErr {
/// Error returned when the [Nickname Enforcement rule](https://www.rfc-editor.org/rfc/rfc8266#section-2.3)
/// fails.
Rfc8266,
- /// Error returned when the length of the transformed string would exceed [`Nickname::MAX_LEN`].
+ /// Error returned when the length of the transformed string would exceed [`Nickname::MAX_LEN`] or
+ /// [`Nickname::RECOMMENDED_MAX_LEN`].
Len,
}
impl Display for NicknameErr {
@@ -35,14 +36,15 @@ pub enum UsernameErr {
/// Error returned when the
/// [UsernameCasePreserved Enforcement rule](https://www.rfc-editor.org/rfc/rfc8265#section-3.4.3) fails.
Rfc8265,
- /// Error returned when the length of the transformed string would exceed [`Nickname::MAX_LEN`].
+ /// Error returned when the length of the transformed string would exceed [`Username::MAX_LEN`] or
+ /// [`Username::RECOMMENDED_MAX_LEN`].
Len,
}
impl Display for UsernameErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
- Self::Rfc8265 => "nickname does not conform to RFC 8265",
+ Self::Rfc8265 => "username does not conform to RFC 8265",
Self::Len => "length of username is too long",
})
}
diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs
@@ -2,7 +2,7 @@ extern crate alloc;
use super::{
AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria,
CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, CrossPlatformHint, Extension,
- ExtensionInfo, Hint, Nickname, PlatformHint, PublicKeyCredentialUserEntity,
+ Hint, Nickname, PlatformHint, PublicKeyCredentialUserEntity,
RegistrationClientState, ResidentKeyRequirement, RpId, UserHandle, Username,
};
use alloc::borrow::Cow;
@@ -80,15 +80,12 @@ impl Serialize for CoseAlgorithmIdentifier {
.serialize_struct("PublicKeyCredentialParameters", 2)
.and_then(|mut ser| {
ser.serialize_field("type", "public-key").and_then(|()| {
- ser.serialize_field(
- "alg",
- &match *self {
- Self::Eddsa => EDDSA,
- Self::Es256 => ES256,
- Self::Es384 => ES384,
- Self::Rs256 => RS256,
- },
- )
+ ser.serialize_field("alg", &match *self {
+ Self::Eddsa => EDDSA,
+ Self::Es256 => ES256,
+ Self::Es384 => ES384,
+ Self::Rs256 => RS256,
+ })
.and_then(|()| ser.end())
})
})
@@ -113,17 +110,17 @@ impl Serialize for CoseAlgorithmIdentifiers {
/// # Ok::<_, serde_json::Error>(())
/// ```
#[expect(
- clippy::as_conversions,
- reason = "u8::count_ones returns a u32, and it's always going to fit in a usize"
+ clippy::arithmetic_side_effects,
+ reason = "comment justifies correctness"
)]
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
- // `self.0.count_ones()` is at most 4 which is guaranteed to be a valid `usize`.
+ // At most we add `1` four times which clearly cannot overflow or `usize`.
serializer
- .serialize_seq(Some(self.0.count_ones() as usize))
+ .serialize_seq(Some(usize::from(self.contains(CoseAlgorithmIdentifier::Eddsa)) + usize::from(self.contains(CoseAlgorithmIdentifier::Es256)) + usize::from(self.contains(CoseAlgorithmIdentifier::Es384)) + usize::from(self.contains(CoseAlgorithmIdentifier::Es384))))
.and_then(|mut ser| {
if self.contains(CoseAlgorithmIdentifier::Eddsa) {
ser.serialize_element(&CoseAlgorithmIdentifier::Eddsa)
@@ -176,7 +173,8 @@ impl Serialize for PublicKeyCredentialRpEntity<'_> {
})
}
}
-impl<const LEN: usize> Serialize for UserHandle<LEN> {
+// We implement this separately from `user_serialize` for proper documentation and example purposes.
+impl Serialize for UserHandle<1> {
/// Serializes `self` to conform with
/// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentityjson-id).
///
@@ -197,10 +195,39 @@ impl<const LEN: usize> Serialize for UserHandle<LEN> {
where
S: Serializer,
{
- serializer.serialize_str(BASE64URL_NOPAD.encode(self.0.as_ref()).as_str())
+ serializer.serialize_str(BASE64URL_NOPAD.encode_mut_str(self.0.as_slice(), [0; crate::base64url_nopad_len(1).unwrap()].as_mut_slice()))
}
}
-impl<const LEN: usize> Serialize for PublicKeyCredentialUserEntity<'_, '_, '_, LEN> {
+/// Implements [`Serialize`] for [`UserHandle`] of array of length of the passed `usize` literal.
+///
+/// Only [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] inclusively are allowed to be passed.
+macro_rules! user_serialize {
+ ( $( $x:literal),* ) => {
+ $(
+impl Serialize for UserHandle<$x> {
+ /// See [`UserHandle::serialize`].
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+
+ serializer.serialize_str(BASE64URL_NOPAD.encode_mut_str(self.0.as_slice(), [0; crate::base64url_nopad_len($x).unwrap()].as_mut_slice()))
+ }
+}
+ )*
+ };
+}
+// MUST only pass `2`–[`USER_HANDLE_MAX_LEN`] inclusively.
+user_serialize!(
+ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
+ 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+ 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64
+);
+impl<const LEN: usize> Serialize for PublicKeyCredentialUserEntity<'_, '_, '_, LEN>
+where
+ UserHandle<LEN>: Serialize,
+{
/// Serializes `self` to conform with
/// [`PublicKeyCredentialUserEntityJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentityjson).
///
@@ -448,18 +475,18 @@ impl Serialize for Extension {
///
/// ```
/// # use webauthn_rp::request::{
- /// # register::{CredProtect, Extension},
+ /// # register::{CredProtect, Extension, FourToSixtyThree},
/// # ExtensionInfo, ExtensionReq,
/// # };
/// assert_eq!(serde_json::to_string(&Extension::default())?, r#"{}"#);
/// assert_eq!(
/// serde_json::to_string(&Extension {
/// cred_props: Some(ExtensionReq::Allow),
- /// cred_protect: CredProtect::UserVerificationRequired(ExtensionInfo::RequireEnforceValue),
- /// min_pin_length: Some((16, ExtensionInfo::AllowDontEnforceValue)),
+ /// cred_protect: CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue),
+ /// min_pin_length: Some((FourToSixtyThree::new(16).unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new")), ExtensionInfo::AllowDontEnforceValue)),
/// prf: Some(ExtensionInfo::AllowEnforceValue)
/// })?,
- /// r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":true,"minPinLength":true,"prf":{}}"#
+ /// r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{}}"#
/// );
/// # Ok::<_, serde_json::Error>(())
/// ```
@@ -497,7 +524,7 @@ impl Serialize for Extension {
if matches!(self.cred_protect, CredProtect::None) {
Ok(())
} else {
- let ext_info;
+ let enforce_policy;
// [`credProtect`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension)
// is serialized by serializing its fields directly and not as a map of fields.
ser.serialize_field(
@@ -506,18 +533,18 @@ impl Serialize for Extension {
CredProtect::None => unreachable!(
"Extensions is incorrectly serializing credProtect"
),
- CredProtect::UserVerificationOptional(info) => {
- ext_info = info;
+ CredProtect::UserVerificationOptional(enforce, _) => {
+ enforce_policy = enforce;
"userVerificationOptional"
}
CredProtect::UserVerificationOptionalWithCredentialIdList(
- info,
+ enforce, _,
) => {
- ext_info = info;
+ enforce_policy = enforce;
"userVerificationOptionalWithCredentialIDList"
}
- CredProtect::UserVerificationRequired(info) => {
- ext_info = info;
+ CredProtect::UserVerificationRequired(enforce, _) => {
+ enforce_policy = enforce;
"userVerificationRequired"
}
},
@@ -525,11 +552,7 @@ impl Serialize for Extension {
.and_then(|()| {
ser.serialize_field(
"enforceCredentialProtectionPolicy",
- &matches!(
- ext_info,
- ExtensionInfo::RequireEnforceValue
- | ExtensionInfo::AllowEnforceValue
- ),
+ &enforce_policy,
)
})
}
@@ -546,7 +569,10 @@ impl Serialize for Extension {
})
}
}
-impl<const USER_LEN: usize> Serialize for RegistrationClientState<'_, '_, '_, '_, USER_LEN> {
+impl<'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> Serialize for RegistrationClientState<'_, 'user_name, 'user_display_name, 'user_id, USER_LEN>
+where
+ PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>: Serialize,
+{
/// Serializes `self` to conform with
/// [`PublicKeyCredentialCreationOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptionsjson).
///
@@ -558,7 +584,7 @@ impl<const USER_LEN: usize> Serialize for RegistrationClientState<'_, '_, '_, '_
/// # use webauthn_rp::{
/// # request::{
/// # register::{
- /// # UserHandle64, AuthenticatorAttachmentReq, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
+ /// # FourToSixtyThree, UserHandle64, AuthenticatorAttachmentReq, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
/// # },
/// # AsciiDomain, ExtensionInfo, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
/// # },
@@ -585,7 +611,7 @@ impl<const USER_LEN: usize> Serialize for RegistrationClientState<'_, '_, '_, '_
/// let user_handle = UserHandle64::new();
/// let mut options = PublicKeyCredentialCreationOptions::passkey(&rp_id, PublicKeyCredentialUserEntity { name: "pierre.de.fermat".try_into()?, id: &user_handle, display_name: Some("Pierre de Fermat".try_into()?) }, creds);
/// options.authenticator_selection.authenticator_attachment = AuthenticatorAttachmentReq::None(Hint::SecurityKey);
- /// options.extensions.min_pin_length = Some((16, ExtensionInfo::RequireEnforceValue));
+ /// options.extensions.min_pin_length = Some((FourToSixtyThree::new(16).unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new")), ExtensionInfo::RequireEnforceValue));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap();
/// let json = serde_json::json!({
@@ -639,7 +665,7 @@ impl<const USER_LEN: usize> Serialize for RegistrationClientState<'_, '_, '_, '_
/// ],
/// "extensions":{
/// "credentialProtectionPolicy":"userVerificationRequired",
- /// "enforceCredentialProtectionPolicy":true,
+ /// "enforceCredentialProtectionPolicy":false,
/// "minPinLength":true
/// }
/// }).to_string();
@@ -877,7 +903,7 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifier {
match v {
EDDSA => Ok(CoseAlgorithmIdentifier::Eddsa),
ES256 => Ok(CoseAlgorithmIdentifier::Es256),
- ES384 => Ok(CoseAlgorithmIdentifier::Es384),
+ ES384=> Ok(CoseAlgorithmIdentifier::Es384),
RS256 => Ok(CoseAlgorithmIdentifier::Rs256),
_ => Err(E::invalid_value(
Unexpected::Signed(i64::from(v)),
diff --git a/src/request/register/ser_server_state.rs b/src/request/register/ser_server_state.rs
@@ -1,7 +1,3 @@
-#![expect(
- clippy::unseparated_literal_suffix,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
use super::{
super::super::bin::{
Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible as _,
@@ -133,16 +129,19 @@ impl EncodeBuffer for CredProtect {
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
match *self {
Self::None => 0u8.encode_into_buffer(buffer),
- Self::UserVerificationOptional(info) => {
+ Self::UserVerificationOptional(enforce, info) => {
1u8.encode_into_buffer(buffer);
+ enforce.encode_into_buffer(buffer);
info.encode_into_buffer(buffer);
}
- Self::UserVerificationOptionalWithCredentialIdList(info) => {
+ Self::UserVerificationOptionalWithCredentialIdList(enforce, info) => {
2u8.encode_into_buffer(buffer);
+ enforce.encode_into_buffer(buffer);
info.encode_into_buffer(buffer);
}
- Self::UserVerificationRequired(info) => {
+ Self::UserVerificationRequired(enforce, info) => {
3u8.encode_into_buffer(buffer);
+ enforce.encode_into_buffer(buffer);
info.encode_into_buffer(buffer);
}
}
@@ -153,10 +152,18 @@ impl<'a> DecodeBuffer<'a> for CredProtect {
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
u8::decode_from_buffer(data).and_then(|val| match val {
0 => Ok(Self::None),
- 1 => ExtensionInfo::decode_from_buffer(data).map(Self::UserVerificationOptional),
- 2 => ExtensionInfo::decode_from_buffer(data)
- .map(Self::UserVerificationOptionalWithCredentialIdList),
- 3 => ExtensionInfo::decode_from_buffer(data).map(Self::UserVerificationRequired),
+ 1 => bool::decode_from_buffer(data).and_then(|enforce| {
+ ExtensionInfo::decode_from_buffer(data)
+ .map(|info| Self::UserVerificationOptional(enforce, info))
+ }),
+ 2 => bool::decode_from_buffer(data).and_then(|enforce| {
+ ExtensionInfo::decode_from_buffer(data)
+ .map(|info| Self::UserVerificationOptionalWithCredentialIdList(enforce, info))
+ }),
+ 3 => bool::decode_from_buffer(data).and_then(|enforce| {
+ ExtensionInfo::decode_from_buffer(data)
+ .map(|info| Self::UserVerificationRequired(enforce, info))
+ }),
_ => Err(EncDecErr),
})
}
diff --git a/src/request/ser.rs b/src/request/ser.rs
@@ -19,7 +19,6 @@ impl Serialize for Challenge {
/// assert_eq!(serde_json::to_string(&Challenge::new())?.len(), 24);
/// # Ok::<_, serde_json::Error>(())
/// ```
- #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
#[expect(
clippy::little_endian_bytes,
reason = "SentChallenge::deserialize and Challenge::serialize need to be consistent across architectures"
@@ -29,13 +28,10 @@ impl Serialize for Challenge {
where
S: Serializer,
{
- let mut data = [0; Self::BASE64_LEN];
- BASE64URL_NOPAD.encode_mut(self.0.to_le_bytes().as_slice(), data.as_mut_slice());
- serializer.serialize_str(
- str::from_utf8(data.as_slice())
- // There is a bug, so crash and burn.
- .unwrap_or_else(|_| unreachable!("there is a bug in Challenge::serialize")),
- )
+ serializer.serialize_str(BASE64URL_NOPAD.encode_mut_str(
+ self.0.to_le_bytes().as_slice(),
+ [0; Self::BASE64_LEN].as_mut_slice(),
+ ))
}
}
impl Serialize for RpId {
diff --git a/src/response.rs b/src/response.rs
@@ -29,12 +29,12 @@ use ser_relaxed::SerdeJsonErr;
/// # Examples
///
/// ```no_run
+/// # use core::convert;
/// # use data_encoding::BASE64URL_NOPAD;
-/// # #[cfg(not(feature = "serializable_server_state"))]
-/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
/// # use webauthn_rp::{
+/// # hash::hash_set::FixedCapHashSet,
/// # request::{auth::{error::RequestOptionsErr, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, AsciiDomain, BackupReq, RpId},
-/// # response::{auth::{error::AuthCeremonyErr, DiscoverableAuthentication64}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKey, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId},
+/// # response::{auth::{error::AuthCeremonyErr, DiscoverableAuthentication64}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKey, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId},
/// # AuthenticatedCredential, CredentialErr
/// # };
/// # #[derive(Debug)]
@@ -79,15 +79,12 @@ use ser_relaxed::SerdeJsonErr;
/// # Self::AuthCeremony(value)
/// # }
/// # }
-/// # #[cfg(not(feature = "serializable_server_state"))]
/// let mut ceremonies = FixedCapHashSet::new(128);
/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
/// let (server, client) = DiscoverableCredentialRequestOptions::passkey(&rp_id).start_ceremony()?;
-/// # #[cfg(not(feature = "serializable_server_state"))]
-/// assert!(matches!(
-/// ceremonies.insert_or_replace_all_expired(server),
-/// InsertResult::Success
-/// ));
+/// assert!(
+/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity)
+/// );
/// # #[cfg(feature = "serde")]
/// let authentication = serde_json::from_str::<DiscoverableAuthentication64>(get_authentication_json(client).as_str())?;
/// # #[cfg(feature = "serde")]
@@ -96,7 +93,7 @@ use ser_relaxed::SerdeJsonErr;
/// let (static_state, dynamic_state) = get_credential(authentication.raw_id(), &user_handle).ok_or(E::UnknownCredential)?;
/// # #[cfg(all(feature = "custom", feature = "serde"))]
/// let mut cred = AuthenticatedCredential::new(authentication.raw_id(), &user_handle, static_state, dynamic_state)?;
-/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom", feature = "serde"))]
+/// # #[cfg(all(feature = "custom", feature = "serde"))]
/// if ceremonies.take(&authentication.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, &authentication, &mut cred, &AuthenticationVerificationOptions::<&str, &str>::default())? {
/// update_cred(authentication.raw_id(), cred.dynamic_state());
/// }
@@ -126,7 +123,7 @@ use ser_relaxed::SerdeJsonErr;
/// /// Gets the `AuthenticatedCredential` parts associated with `id` and `user_handle` from the database.
/// fn get_credential(id: CredentialId<&[u8]>, user_handle: &UserHandle64) -> Option<(StaticState<CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>>, DynamicState)> {
/// // ⋮
-/// # Some((StaticState { credential_public_key: CompressedPubKey::Ed25519(Ed25519PubKey::from([0; 32])), extensions: AuthenticatorExtensionOutputStaticState { cred_protect: CredentialProtectionPolicy::UserVerificationRequired, hmac_secret: None, } }, DynamicState { user_verified: true, backup: Backup::NotEligible, sign_count: 1, authenticator_attachment: AuthenticatorAttachment::None }))
+/// # Some((StaticState { credential_public_key: CompressedPubKey::Ed25519(Ed25519PubKey::from([0; 32])), extensions: AuthenticatorExtensionOutputStaticState { cred_protect: CredentialProtectionPolicy::UserVerificationRequired, hmac_secret: None, }, client_extension_results: ClientExtensionsOutputsStaticState { prf: None, }, }, DynamicState { user_verified: true, backup: Backup::NotEligible, sign_count: 1, authenticator_attachment: AuthenticatorAttachment::None }))
/// }
/// /// Updates the current `DynamicState` associated with `id` in the database to
/// /// `dyn_state`.
@@ -155,10 +152,10 @@ pub mod error;
/// # Examples
///
/// ```no_run
+/// # use core::convert;
/// # use data_encoding::BASE64URL_NOPAD;
-/// # #[cfg(not(feature = "serializable_server_state"))]
-/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
/// # use webauthn_rp::{
+/// # hash::hash_set::FixedCapHashSet,
/// # request::{register::{error::CreationOptionsErr, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomain, PublicKeyCredentialDescriptor, RpId},
/// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData},
/// # RegisteredCredential
@@ -197,7 +194,7 @@ pub mod error;
/// # Self::RegCeremony(value)
/// # }
/// # }
-/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom"))]
+/// # #[cfg(feature = "custom")]
/// let mut ceremonies = FixedCapHashSet::new(128);
/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
/// # #[cfg(feature = "custom")]
@@ -208,15 +205,14 @@ pub mod error;
/// let creds = get_registered_credentials(user_handle);
/// # #[cfg(feature = "custom")]
/// let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, user, creds).start_ceremony()?;
-/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom"))]
-/// assert!(matches!(
-/// ceremonies.insert_or_replace_all_expired(server),
-/// InsertResult::Success
-/// ));
+/// # #[cfg(feature = "custom")]
+/// assert!(
+/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity)
+/// );
/// # #[cfg(all(feature = "serde_relaxed", feature = "custom"))]
/// let registration = serde_json::from_str::<Registration>(get_registration_json(client).as_str())?;
/// let ver_opts = RegistrationVerificationOptions::<&str, &str>::default();
-/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom", feature = "serde_relaxed"))]
+/// # #[cfg(all(feature = "custom", feature = "serde_relaxed"))]
/// insert_cred(ceremonies.take(®istration.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, ®istration, &ver_opts)?);
/// /// Extract `UserHandle` from session cookie if this is not the first credential registered.
/// # #[cfg(feature = "custom")]
@@ -592,6 +588,18 @@ impl<T: Hash> Hash for CredentialId<T> {
self.0.hash(state);
}
}
+impl<T: PartialOrd<T2>, T2: PartialOrd<T>> PartialOrd<CredentialId<T>> for CredentialId<T2> {
+ #[inline]
+ fn partial_cmp(&self, other: &CredentialId<T>) -> Option<Ordering> {
+ self.0.partial_cmp(&other.0)
+ }
+}
+impl<T: Ord> Ord for CredentialId<T> {
+ #[inline]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.0.cmp(&other.0)
+ }
+}
// We define a separate type to ensure challenges sent to the client are always randomly generated;
// otherwise one could deserialize arbitrary data into a `Challenge`.
/// Copy of [`Challenge`] sent back from the client.
diff --git a/src/response/auth.rs b/src/response/auth.rs
@@ -3,18 +3,17 @@ use self::{
super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr},
ser_relaxed::{AuthenticationRelaxed, CustomAuthentication},
};
-#[cfg(all(doc, feature = "serde_relaxed"))]
-use super::super::request::FixedCapHashSet;
#[cfg(doc)]
use super::super::{
AuthenticatedCredential, RegisteredCredential, StaticState,
+ hash::hash_set::FixedCapHashSet,
request::{
Challenge,
auth::{
- DiscoverableAuthenticationServerState, NonDiscoverableAuthenticationServerState,
- PublicKeyCredentialRequestOptions,
+ CredentialSpecificExtension, DiscoverableAuthenticationServerState, Extension,
+ NonDiscoverableAuthenticationServerState, PublicKeyCredentialRequestOptions,
},
- register::{UserHandle16, UserHandle64},
+ register::{self, UserHandle16, UserHandle64},
},
};
use super::{
@@ -22,7 +21,7 @@ use super::{
AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment,
CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor,
LimitedVerificationParser, ParsedAuthData, Response, SentChallenge,
- auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr},
+ auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr, MissingUserHandleErr},
cbor,
error::CollectedClientDataErr,
register::CompressedPubKey,
@@ -48,13 +47,35 @@ pub(super) mod ser;
#[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).
+///
+/// This is only relevant when [`Extension::prf`] or [`CredentialSpecificExtension::prf`] is `Some` and for
+/// authenticators that implement [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) on top of
+/// `hmac-secret`.
+///
+/// Note while many authenticators that implement `prf` don't require `prf` to have been sent during registration
+/// (i.e., [`register::Extension::prf`]), it is recommended to do so for those authenticators that do require it.
#[derive(Clone, Copy, Debug)]
pub enum HmacSecret {
/// No `hmac-secret` response.
+ ///
+ /// Either [`Extension::prf`] was not sent, the credential is not PRF-capable, or the authenticator does not use
+ /// the
+ /// [`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)
+ /// extension.
None,
/// One encrypted `hmac-secret`.
+ ///
+ /// [`Extension::prf`] was sent with one PRF input for a PRF-capable credential whose authenticator implements
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) on top of the
+ /// [`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)
+ /// extension.
One,
/// Two encrypted `hmac-secret`s.
+ ///
+ /// [`Extension::prf`] was sent with two PRF inputs for a PRF-capable credential whose authenticator implements
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) on top of the
+ /// [`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)
+ /// extension.
Two,
}
impl FromCbor<'_> for HmacSecret {
@@ -299,7 +320,7 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool>
/// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
#[inline]
#[must_use]
- pub fn client_data_json(&self) -> &[u8] {
+ pub const fn client_data_json(&self) -> &[u8] {
self.client_data_json.as_slice()
}
/// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata).
@@ -320,7 +341,7 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool>
/// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature).
#[inline]
#[must_use]
- pub fn signature(&self) -> &[u8] {
+ pub const fn signature(&self) -> &[u8] {
self.signature.as_slice()
}
/// Constructs an instance of `Self` with the contained data.
@@ -533,6 +554,39 @@ pub type DiscoverableAuthenticatorAssertion<const USER_LEN: usize> =
/// `AuthenticatorAssertion` with an optional `UserHandle`.
pub type NonDiscoverableAuthenticatorAssertion<const USER_LEN: usize> =
AuthenticatorAssertion<USER_LEN, false>;
+impl<const USER_LEN: usize> From<DiscoverableAuthenticatorAssertion<USER_LEN>>
+ for NonDiscoverableAuthenticatorAssertion<USER_LEN>
+{
+ #[inline]
+ fn from(value: DiscoverableAuthenticatorAssertion<USER_LEN>) -> Self {
+ Self {
+ client_data_json: value.client_data_json,
+ authenticator_data_and_c_data_hash: value.authenticator_data_and_c_data_hash,
+ signature: value.signature,
+ user_handle: value.user_handle,
+ }
+ }
+}
+impl<const USER_LEN: usize> TryFrom<NonDiscoverableAuthenticatorAssertion<USER_LEN>>
+ for DiscoverableAuthenticatorAssertion<USER_LEN>
+{
+ type Error = MissingUserHandleErr;
+ #[inline]
+ fn try_from(
+ value: NonDiscoverableAuthenticatorAssertion<USER_LEN>,
+ ) -> Result<Self, MissingUserHandleErr> {
+ if value.user_handle.is_some() {
+ Ok(Self {
+ client_data_json: value.client_data_json,
+ authenticator_data_and_c_data_hash: value.authenticator_data_and_c_data_hash,
+ signature: value.signature,
+ user_handle: value.user_handle,
+ })
+ } else {
+ Err(MissingUserHandleErr)
+ }
+ }
+}
/// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for authentication ceremonies.
#[expect(
clippy::field_scoped_visibility_modifiers,
@@ -673,6 +727,33 @@ pub type DiscoverableAuthentication64 = Authentication<USER_HANDLE_MAX_LEN, true
pub type DiscoverableAuthentication16 = Authentication<16, true>;
/// `Authentication` with an optional [`UserHandle`].
pub type NonDiscoverableAuthentication<const USER_LEN: usize> = Authentication<USER_LEN, false>;
+impl<const USER_LEN: usize> From<DiscoverableAuthentication<USER_LEN>>
+ for NonDiscoverableAuthentication<USER_LEN>
+{
+ #[inline]
+ fn from(value: DiscoverableAuthentication<USER_LEN>) -> Self {
+ Self {
+ raw_id: value.raw_id,
+ response: value.response.into(),
+ authenticator_attachment: value.authenticator_attachment,
+ }
+ }
+}
+impl<const USER_LEN: usize> TryFrom<NonDiscoverableAuthentication<USER_LEN>>
+ for DiscoverableAuthentication<USER_LEN>
+{
+ type Error = MissingUserHandleErr;
+ #[inline]
+ fn try_from(
+ value: NonDiscoverableAuthentication<USER_LEN>,
+ ) -> Result<Self, MissingUserHandleErr> {
+ value.response.try_into().map(|response| Self {
+ raw_id: value.raw_id,
+ response,
+ authenticator_attachment: value.authenticator_attachment,
+ })
+ }
+}
/// `Authentication` with an optional [`UserHandle64`].
pub type NonDiscoverableAuthentication64 = Authentication<USER_HANDLE_MAX_LEN, false>;
/// `Authentication` with an optional [`UserHandle16`].
diff --git a/src/response/auth/error.rs b/src/response/auth/error.rs
@@ -1,9 +1,8 @@
#[cfg(feature = "serde_relaxed")]
use super::super::SerdeJsonErr;
use super::super::{
- super::{CredentialErr, CredentialId},
- AuthRespErr, AuthenticatorDataErr as AuthDataErr, CeremonyErr, CollectedClientDataErr,
- PubKeyErr, RpId,
+ super::CredentialId, AuthRespErr, AuthenticatorDataErr as AuthDataErr, CeremonyErr,
+ CollectedClientDataErr, PubKeyErr, RpId,
};
#[cfg(doc)]
use super::{
@@ -14,16 +13,18 @@ use super::{
BackupReq, UserVerificationRequirement,
auth::{
AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions,
- DiscoverableAuthenticationServerState, Extension,
+ CredentialSpecificExtension, DiscoverableAuthenticationServerState,
+ DiscoverableCredentialRequestOptions, Extension,
NonDiscoverableAuthenticationServerState, PublicKeyCredentialRequestOptions,
},
},
},
Backup,
+ register::CredentialProtectionPolicy,
},
Authentication, AuthenticatorAssertion, AuthenticatorAttachment, AuthenticatorData,
AuthenticatorExtensionOutput, CollectedClientData, CompressedPubKey, Flag, HmacSecret,
- Signature,
+ Signature, UserHandle,
};
use core::{
convert::Infallible,
@@ -150,28 +151,40 @@ impl Display for OneOrTwo {
/// Error in [`AuthCeremonyErr::Extension`].
#[derive(Clone, Copy, Debug)]
pub enum ExtensionErr {
+ /// [`AuthenticatorExtensionOutput::hmac_secret`] was sent from the client, but [`Flag::user_verified`]
+ /// was `false`.
+ ///
+ /// Note this is only possible iff [`PublicKeyCredentialRequestOptions::user_verification`] is not
+ /// [`UserVerificationRequirement::Required`], [`Extension::prf`] is `None`,
+ /// [`CredentialSpecificExtension::prf`] is `None`, and
+ /// [`AuthenticationVerificationOptions::error_on_unsolicited_extensions`] is `false`.
+ UserNotVerifiedHmacSecret,
/// [`AuthenticatorExtensionOutput::hmac_secret`] was sent from the client but was not supposed to be.
ForbiddenHmacSecret,
- /// [`AuthenticatorExtensionOutput::hmac_secret`] was sent from the client for a credential that is not PRF
- /// capable.
- HmacSecretForPrfIncapableCred,
- /// [`Extension::prf`] was requested, but the required response was not sent back.
+ /// [`Extension::prf`] was requested for a credential that does not support it.
+ PrfRequestedForPrfIncapableCred,
+ /// [`Extension::prf`] was requested for a PRF-capable credential that is based on the
+ /// [`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)
+ /// extension, but the required response was not sent back.
MissingHmacSecret,
/// [`Extension::prf`] was requested with the first number of PRF inputs, but the second number of
/// [`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)
- /// outputs were sent.
+ /// outputs were sent for a PRF-capable credential that is based on the `hmac-secret` extension.
InvalidHmacSecretValue(OneOrTwo, OneOrTwo),
}
impl Display for ExtensionErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
+ Self::UserNotVerifiedHmacSecret => {
+ f.write_str("user was not verified but hmac-secret info was sent from the client")
+ }
Self::ForbiddenHmacSecret => {
f.write_str("hmac-secret info was sent from the client, but it is not allowed")
}
- Self::HmacSecretForPrfIncapableCred => f.write_str(
- "hmac-secret info was sent from the client for a PRF-incapable credential",
- ),
+ Self::PrfRequestedForPrfIncapableCred => {
+ f.write_str("prf extension was requested for a credential that is not PRF-capable")
+ }
Self::MissingHmacSecret => f.write_str("hmac-secret was not sent from the client"),
Self::InvalidHmacSecretValue(sent, recv) => write!(
f,
@@ -242,8 +255,12 @@ pub enum AuthCeremonyErr {
/// [`AllowedCredentials`] is empty (i.e., a discoverable request was issued), but
/// [`AuthenticatorAssertion::user_handle`] was [`None`].
MissingUserHandle,
- /// Variant returned when [`AuthenticatedCredential`] cannot be updated due to invalid state.
- Credential(CredentialErr),
+ /// [`Flag::user_verified`] was `false`, but the credential has
+ /// [`CredentialProtectionPolicy::UserVerificationRequired`].
+ UserNotVerifiedCredProtectRequired,
+ /// [`DiscoverableCredentialRequestOptions`] was sent but the credential has
+ /// [`CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList`].
+ DiscoverableCredProtectCredentialIdList,
}
impl Display for AuthCeremonyErr {
#[inline]
@@ -279,7 +296,8 @@ impl Display for AuthCeremonyErr {
Self::CredentialIdMismatch => f.write_str("the credential ID does not match"),
Self::NoMatchingAllowedCredential => f.write_str("none of the credentials used to start the non-discoverable request have the same Credential ID as the credential used to finish the ceremony"),
Self::MissingUserHandle => f.write_str("the credential used to finish the ceremony did not have a user handle despite a discoverable request being issued"),
- Self::Credential(err) => err.fmt(f),
+ Self::UserNotVerifiedCredProtectRequired => f.write_str("the credential requires user verification, but the user was not verified"),
+ Self::DiscoverableCredProtectCredentialIdList => f.write_str("the credential requires user verification or to be used for non-discoverable requests, but a discoverable request was used and the user was not verified"),
}
}
}
@@ -296,3 +314,13 @@ pub struct UnknownCredentialOptions<'rp, 'cred> {
/// [`credentialId`](https://www.w3.org/TR/webauthn-3/#dictdef-unknowncredentialoptions-credentialid).
pub credential_id: CredentialId<&'cred [u8]>,
}
+/// Error when a [`UserHandle`] does not exist that is required to.
+#[derive(Debug)]
+pub struct MissingUserHandleErr;
+impl Display for MissingUserHandleErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("user handle does not exist")
+ }
+}
+impl Error for MissingUserHandleErr {}
diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs
@@ -1,7 +1,3 @@
-#![expect(
- clippy::question_mark_used,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
use super::{
super::{
super::response::ser::{Base64DecodedVal, PublicKeyCredential},
diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs
@@ -1,7 +1,3 @@
-#![expect(
- clippy::question_mark_used,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
#[cfg(doc)]
use super::super::{Challenge, CredentialId};
use super::{
diff --git a/src/response/register.rs b/src/response/register.rs
@@ -3,15 +3,12 @@ use self::{
super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr},
ser_relaxed::{CustomRegistration, RegistrationRelaxed},
};
-#[cfg(all(doc, feature = "serde_relaxed"))]
-use super::super::request::FixedCapHashSet;
#[cfg(all(doc, feature = "bin"))]
+use super::super::bin::{Decode, Encode};
+#[cfg(feature = "bin")]
+use super::register::bin::{AaguidOwned, MetadataOwned};
use super::{
- super::bin::{Decode, Encode},
- register::bin::MetadataOwned,
-};
-use super::{
- super::request::register::ResidentKeyRequirement,
+ super::request::register::{FourToSixtyThree, ResidentKeyRequirement},
AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthTransports,
AuthenticatorAttachment, Backup, CborSuccess, ClientDataJsonParser as _, CollectedClientData,
CredentialId, Flag, FromCbor, LimitedVerificationParser, ParsedAuthData, Response,
@@ -28,8 +25,9 @@ use super::{
use super::{
super::{
AuthenticatedCredential, RegisteredCredential,
+ hash::hash_set::FixedCapHashSet,
request::{
- BackupReq, Challenge,
+ BackupReq, Challenge, UserVerificationRequirement,
auth::{AuthenticationVerificationOptions, PublicKeyCredentialRequestOptions},
register::{Extension, RegistrationServerState},
},
@@ -105,7 +103,7 @@ pub struct AuthenticatorExtensionOutput {
/// [`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).
pub hmac_secret: Option<bool>,
/// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension).
- pub min_pin_length: Option<u8>,
+ pub min_pin_length: Option<FourToSixtyThree>,
}
impl AuthExtOutput for AuthenticatorExtensionOutput {
fn missing(self) -> bool {
@@ -128,7 +126,7 @@ pub struct AuthenticatorExtensionOutputStaticState {
#[derive(Clone, Copy, Debug)]
pub struct AuthenticatorExtensionOutputMetadata {
/// [`AuthenticatorExtensionOutput::min_pin_length`].
- pub min_pin_length: Option<u8>,
+ pub min_pin_length: Option<FourToSixtyThree>,
}
impl From<AuthenticatorExtensionOutput> for AuthenticatorExtensionOutputMetadata {
#[inline]
@@ -244,8 +242,8 @@ impl FromCbor<'_> for HmacSecret {
enum MinPinLength {
/// No `minPinLength` extension.
None,
- /// `minPinLength` with the value of the contained `u8`.
- Val(u8),
+ /// `minPinLength` with the value of the contained `FourToSixtyThree`.
+ Val(FourToSixtyThree),
}
impl FromCbor<'_> for MinPinLength {
type Err = AuthenticatorExtensionOutputErr;
@@ -277,19 +275,25 @@ impl FromCbor<'_> for MinPinLength {
.split_first()
.ok_or(AuthenticatorExtensionOutputErr::Len)
.and_then(|(&key_len, remaining)| match key_len.cmp(&24) {
- Ordering::Less => Ok(CborSuccess {
- value: Self::Val(key_len),
- remaining,
- }),
+ Ordering::Less => FourToSixtyThree::new(key_len)
+ .ok_or(AuthenticatorExtensionOutputErr::MinPinLengthValue)
+ .map(|val| CborSuccess {
+ value: Self::Val(val),
+ remaining,
+ }),
Ordering::Equal => remaining
.split_first()
.ok_or(AuthenticatorExtensionOutputErr::Len)
.and_then(|(&key_24, rem)| {
if key_24 > 23 {
- Ok(CborSuccess {
- value: Self::Val(key_24),
- remaining: rem,
- })
+ FourToSixtyThree::new(key_24)
+ .ok_or(
+ AuthenticatorExtensionOutputErr::MinPinLengthValue,
+ )
+ .map(|val| CborSuccess {
+ value: Self::Val(val),
+ remaining: rem,
+ })
} else {
Err(AuthenticatorExtensionOutputErr::MinPinLengthValue)
}
@@ -512,6 +516,12 @@ impl Ed25519PubKey<&[u8]> {
)
.map_err(|_e| PubKeyErr::Ed25519)
}
+ /// Transforms `self` into an "owned" version.
+ #[inline]
+ #[must_use]
+ pub fn into_owned(self) -> Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> {
+ Ed25519PubKey(*Ed25519PubKey::<&[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>::from(self).0)
+ }
}
impl Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> {
/// Validates `self` is in fact a valid Ed25519 public key.
@@ -647,6 +657,18 @@ impl<'a> UncompressedP256PubKey<'a> {
// `self.1.len() == 32`, so this won't `panic`.
self.1[31] & 1 == 1
}
+ /// Transforms `self` into the compressed version that owns the data.
+ #[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
+ #[inline]
+ #[must_use]
+ pub fn into_compressed(
+ self,
+ ) -> CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]> {
+ CompressedP256PubKey {
+ x: self.0.try_into().unwrap_or_else(|_e| unreachable!("there is a bug in UncompressedP256PubKey that allows for the x-coordinate to not be 32 bytes in length")),
+ y_is_odd: self.y_is_odd(),
+ }
+ }
}
impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP256PubKey<'b> {
type Error = UncompressedP256PubKeyErr;
@@ -894,6 +916,18 @@ impl<'a> UncompressedP384PubKey<'a> {
// `self.1.len() == 48`, so this won't `panic`.
self.1[47] & 1 == 1
}
+ /// Transforms `self` into the compressed version that owns the data.
+ #[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
+ #[inline]
+ #[must_use]
+ pub fn into_compressed(
+ self,
+ ) -> CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]> {
+ CompressedP384PubKey {
+ x: self.0.try_into().unwrap_or_else(|_e| unreachable!("there is a bug in UncompressedP384PubKey that allows for the x-coordinate to not be 48 bytes in length")),
+ y_is_odd: self.y_is_odd(),
+ }
+ }
}
impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP384PubKey<'b> {
type Error = UncompressedP384PubKeyErr;
@@ -1165,6 +1199,14 @@ impl<T: AsRef<[u8]>> RsaPubKey<T> {
.map(RsaVerKey::new)
}
}
+impl RsaPubKey<&[u8]> {
+ /// Transforms `self` into an "owned" version.
+ #[inline]
+ #[must_use]
+ pub fn into_owned(self) -> RsaPubKey<Vec<u8>> {
+ RsaPubKey(self.0.to_owned(), self.1)
+ }
+}
impl<'a: 'b, 'b> TryFrom<(&'a [u8], u32)> for RsaPubKey<&'b [u8]> {
type Error = RsaPubKeyErr;
/// The first item is the big-endian modulus, and the second item is the exponent.
@@ -1636,6 +1678,24 @@ impl UncompressedPubKey<'_> {
Self::Rsa(k) => k.validate(),
}
}
+ /// Transforms `self` into the compressed version that owns the data.
+ #[inline]
+ #[must_use]
+ pub fn into_compressed(
+ self,
+ ) -> CompressedPubKey<
+ [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
+ [u8; <NistP256 as Curve>::FieldBytesSize::INT],
+ [u8; <NistP384 as Curve>::FieldBytesSize::INT],
+ Vec<u8>,
+ > {
+ match self {
+ Self::Ed25519(key) => CompressedPubKey::Ed25519(key.into_owned()),
+ Self::P256(key) => CompressedPubKey::P256(key.into_compressed()),
+ Self::P384(key) => CompressedPubKey::P384(key.into_compressed()),
+ Self::Rsa(key) => CompressedPubKey::Rsa(key.into_owned()),
+ }
+ }
}
impl PartialEq<UncompressedPubKey<'_>> for UncompressedPubKey<'_> {
#[inline]
@@ -1839,7 +1899,6 @@ pub struct AttestedCredentialData<'a> {
/// [`credentialPublicKey`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialpublickey).
pub credential_public_key: UncompressedPubKey<'a>,
}
-
impl<'a> FromCbor<'a> for AttestedCredentialData<'a> {
type Err = AttestedCredentialDataErr;
#[expect(clippy::big_endian_bytes, reason = "CBOR integers are big-endian")]
@@ -1925,6 +1984,27 @@ impl<'a> AuthenticatorData<'a> {
self.extensions
}
}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> {
+ type Error = AuthenticatorDataErr;
+ /// Deserializes `value` based on the
+ /// [authenticator data structure](https://www.w3.org/TR/webauthn-3/#table-authData).
+ #[expect(
+ clippy::panic_in_result_fn,
+ reason = "we want to crash when there is a bug"
+ )]
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ Self::from_cbor(value)
+ .map_err(AuthenticatorDataErr::from)
+ .map(|success| {
+ assert!(
+ success.remaining.is_empty(),
+ "there is a bug in AuthenticatorData::from_cbor"
+ );
+ success.value
+ })
+ }
+}
impl<'a> AuthData<'a> for AuthenticatorData<'a> {
type UpBitErr = Infallible;
type CredData = AttestedCredentialData<'a>;
@@ -1957,27 +2037,6 @@ impl<'a> AuthData<'a> for AuthenticatorData<'a> {
self.flags
}
}
-impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> {
- type Error = AuthenticatorDataErr;
- /// Deserializes `value` based on the
- /// [authenticator data structure](https://www.w3.org/TR/webauthn-3/#table-authData).
- #[expect(
- clippy::panic_in_result_fn,
- reason = "we want to crash when there is a bug"
- )]
- #[inline]
- fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
- Self::from_cbor(value)
- .map_err(AuthenticatorDataErr::from)
- .map(|success| {
- assert!(
- success.remaining.is_empty(),
- "there is a bug in AuthenticatorData::from_cbor"
- );
- success.value
- })
- }
-}
/// [None](https://www.w3.org/TR/webauthn-3/#sctn-none-attestation).
struct NoneAttestation;
impl FromCbor<'_> for NoneAttestation {
@@ -2226,7 +2285,7 @@ impl<'a> FromCbor<'a> for PackedAttestation<'a> {
fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
/// Parses `data` as packed attestation up until `x5c` _without_ the `cbor::MAP_2` or `cbor::MAP_3` header.
fn parse_to_cert_chain(data: &[u8]) -> Result<CborSuccess<'_, Sig<'_>>, AttestationErr> {
- // {"alg":CoseAlgId,"sig":sig_bytes...}
+ // {"alg":CoseAlgorithmIdentifier,"sig":sig_bytes...}
/// "alg" key.
const ALG: [u8; 4] = [cbor::TEXT_3, b'a', b'l', b'g'];
/// "sig" key.
@@ -2348,7 +2407,7 @@ impl<'a> FromCbor<'a> for AttestationFormat<'a> {
type Err = AttestationErr;
fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
// Note we assume that cbor starts _after_ `cbor::MAP_3`.
- // {"fmt":"none"|"packed", "attStmt": NoneAttestation|PackedAttestation, ...}.
+ // {"fmt":"none"|"packed", "attStmt": NoneAttestation|CborPacked, ...}.
/// "fmt" key.
const FMT: [u8; 4] = [cbor::TEXT_3, b'f', b'm', b't'];
/// "none" value.
@@ -2415,34 +2474,11 @@ impl<'a> FromCbor<'a> for AttestationFormat<'a> {
})
}
}
-/// [Attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object).
-#[derive(Debug)]
-pub struct AttestationObject<'a> {
- /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids).
- attestation: AttestationFormat<'a>,
- /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
- auth_data: AuthenticatorData<'a>,
-}
impl<'a> AttestationObject<'a> {
- /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids).
- #[inline]
- #[must_use]
- pub const fn attestation(&self) -> AttestationFormat<'a> {
- self.attestation
- }
- /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
- #[inline]
- #[must_use]
- pub const fn auth_data(&self) -> &AuthenticatorData<'a> {
- &self.auth_data
- }
/// Deserializes `data` based on the
/// [attestation object layout](https://www.w3.org/TR/webauthn-3/#attestation-object)
/// returning [`Self`] and the index within `data` that the authenticator data portion
/// begins.
- ///
- /// This mainly exists to unify [`Self::from_data`], [`Self::try_from`], and
- /// [`ser::AuthenticatorAttestationVisitor::visit_map`].
#[expect(
clippy::panic_in_result_fn,
reason = "we want to crash when there is a bug"
@@ -2456,7 +2492,7 @@ impl<'a> AttestationObject<'a> {
/// `authData` key.
const AUTH_DATA_KEY: [u8; 9] =
[cbor::TEXT_8, b'a', b'u', b't', b'h', b'D', b'a', b't', b'a'];
- // {"fmt":<AttestationFormat>, "attStmt":<AttestationStatement>, "authData":<AuthenticatorData>}.
+ // {"fmt":<AttestationFormat>, "attStmt":<AttestationObject>, "authData":<AuthentcatorData>}.
data.split_first().ok_or(AttestationObjectErr::Len).and_then(|(map, map_rem)| {
if *map == cbor::MAP_3 {
AttestationFormat::from_cbor(map_rem).map_err(AttestationObjectErr::Attestation).and_then(|att| {
@@ -2534,6 +2570,37 @@ impl<'a> AttestationObject<'a> {
})
}
}
+/// [Attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object).
+#[derive(Debug)]
+pub struct AttestationObject<'a> {
+ /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids).
+ attestation: AttestationFormat<'a>,
+ /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+ auth_data: AuthenticatorData<'a>,
+}
+impl<'a> AttestationObject<'a> {
+ /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids).
+ #[inline]
+ #[must_use]
+ pub const fn attestation(&self) -> AttestationFormat<'a> {
+ self.attestation
+ }
+ /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+ #[inline]
+ #[must_use]
+ pub const fn auth_data(&self) -> &AuthenticatorData<'a> {
+ &self.auth_data
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AttestationObject<'b> {
+ type Error = AttestationObjectErr;
+ /// Deserializes `value` based on the
+ /// [attestation object layout](https://www.w3.org/TR/webauthn-3/#attestation-object).
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ Self::parse_data(value).map(|(val, _)| val)
+ }
+}
impl<'a> AuthDataContainer<'a> for AttestationObject<'a> {
type Auth = AuthenticatorData<'a>;
type Err = AttestationObjectErr;
@@ -2548,15 +2615,6 @@ impl<'a> AuthDataContainer<'a> for AttestationObject<'a> {
&self.auth_data
}
}
-impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AttestationObject<'b> {
- type Error = AttestationObjectErr;
- /// Deserializes `value` based on the
- /// [attestation object layout](https://www.w3.org/TR/webauthn-3/#attestation-object).
- #[inline]
- fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
- Self::parse_data(value).map(|(val, _)| val)
- }
-}
/// [`AuthenticatorAttestationResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse).
#[derive(Debug)]
pub struct AuthenticatorAttestation {
@@ -2572,7 +2630,7 @@ impl AuthenticatorAttestation {
/// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
#[inline]
#[must_use]
- pub fn client_data_json(&self) -> &[u8] {
+ pub const fn client_data_json(&self) -> &[u8] {
self.client_data_json.as_slice()
}
/// [attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object).
@@ -2716,6 +2774,34 @@ pub struct ClientExtensionsOutputs {
/// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf).
pub prf: Option<AuthenticationExtensionsPrfOutputs>,
}
+/// [`ClientExtensionsOutputs`] extensions that are saved in [`Metadata`] because they are purely informative
+/// and not used during authentication ceremonies.
+#[derive(Clone, Copy, Debug)]
+pub struct ClientExtensionsOutputsMetadata {
+ /// [`ClientExtensionsOutputs::cred_props`].
+ pub cred_props: Option<CredentialPropertiesOutput>,
+}
+/// [`ClientExtensionsOutputs`] extensions that are saved in [`StaticState`] because they are used during
+/// authentication ceremonies.
+#[derive(Clone, Copy, Debug)]
+pub struct ClientExtensionsOutputsStaticState {
+ /// [`ClientExtensionsOutputs::prf`].
+ pub prf: Option<AuthenticationExtensionsPrfOutputs>,
+}
+impl From<ClientExtensionsOutputs> for ClientExtensionsOutputsMetadata {
+ #[inline]
+ fn from(value: ClientExtensionsOutputs) -> Self {
+ Self {
+ cred_props: value.cred_props,
+ }
+ }
+}
+impl From<ClientExtensionsOutputs> for ClientExtensionsOutputsStaticState {
+ #[inline]
+ fn from(value: ClientExtensionsOutputs) -> Self {
+ Self { prf: value.prf }
+ }
+}
/// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for registration ceremonies.
#[expect(
clippy::field_scoped_visibility_modifiers,
@@ -2859,7 +2945,7 @@ pub struct Metadata<'a> {
pub extensions: AuthenticatorExtensionOutputMetadata,
/// [`getClientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults)
/// output during registration that is never used during authentication ceremonies.
- pub client_extension_results: ClientExtensionsOutputs,
+ pub client_extension_results: ClientExtensionsOutputsMetadata,
/// `ResidentKeyRequirement` sent during registration.
pub resident_key: ResidentKeyRequirement,
}
@@ -2875,8 +2961,7 @@ impl Metadata<'_> {
/// "min_pin_length": null | <8-bit unsigned integer without leading 0s>
/// },
/// "client_extension_results": {
- /// "cred_props": null | CredPropsJSON,
- /// "prf": null | PrfJSON
+ /// "cred_props": null | CredPropsJSON
/// },
/// "resident_key": "required" | "discouraged" | "preferred"
/// }
@@ -2901,10 +2986,10 @@ impl Metadata<'_> {
/// ```
/// # use core::str::FromStr;
/// # use webauthn_rp::{
- /// # request::register::ResidentKeyRequirement,
+ /// # request::register::{FourToSixtyThree, ResidentKeyRequirement},
/// # response::register::{
/// # Aaguid, Attestation,
- /// # AuthenticatorExtensionOutputMetadata, ClientExtensionsOutputs, CredentialPropertiesOutput,
+ /// # AuthenticatorExtensionOutputMetadata, ClientExtensionsOutputsMetadata, CredentialPropertiesOutput,
/// # Metadata,
/// # },
/// # };
@@ -2912,13 +2997,12 @@ impl Metadata<'_> {
/// attestation: Attestation::None,
/// aaguid: Aaguid::try_from([15; 16].as_slice())?,
/// extensions: AuthenticatorExtensionOutputMetadata {
- /// min_pin_length: Some(16),
+ /// min_pin_length: Some(FourToSixtyThree::new(16).unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new"))),
/// },
- /// client_extension_results: ClientExtensionsOutputs {
+ /// client_extension_results: ClientExtensionsOutputsMetadata {
/// cred_props: Some(CredentialPropertiesOutput {
/// rk: Some(true),
/// }),
- /// prf: None,
/// },
/// resident_key: ResidentKeyRequirement::Required
/// };
@@ -2931,8 +3015,7 @@ impl Metadata<'_> {
/// "client_extension_results": {
/// "cred_props": {
/// "rk": true
- /// },
- /// "prf": null
+ /// }
/// },
/// "resident_key": "required"
/// });
@@ -2946,19 +3029,15 @@ impl Metadata<'_> {
clippy::integer_division_remainder_used,
reason = "comments justify their correctness"
)]
- #[expect(
- clippy::else_if_without_else,
- reason = "don't want an empty else branch"
- )]
#[inline]
#[must_use]
pub fn into_json(self) -> String {
- // Maximum capacity needed is not _that_ much larger than the minimum, 184. An example is the
+ // Maximum capacity needed is not _that_ much larger than the minimum, 173. An example is the
// following:
// `{"attestation":"none","aaguid":"00000000000000000000000000000000","extensions":{"min_pin_length":null},"client_extension_results":{"cred_props":{"rk":false},"prf":{"enabled":false}},"resident_key":"discouraged"}`.
// We use a raw `Vec` instead of a `String` since we need to transform some binary values into ASCII which
// is easier to do as bytes.
- let mut buffer = Vec::with_capacity(211);
+ let mut buffer = Vec::with_capacity(187);
buffer.extend_from_slice(br#"{"attestation":"#);
buffer.extend_from_slice(match self.attestation {
Attestation::None => br#""none","aaguid":""#,
@@ -2983,26 +3062,17 @@ impl Metadata<'_> {
None => buffer.extend_from_slice(b"null"),
Some(pin) => {
// Clearly correct.
- let dig_1 = pin / 100;
+ let dig_1 = pin.value() / 10;
// Clearly correct.
- let dig_2 = (pin % 100) / 10;
- // Clearly correct.
- let dig_3 = pin % 10;
+ let dig_2 = pin.value() % 10;
if dig_1 > 0 {
// We simply add the appropriate offset which is `b'0` for decimal digits.
// Overflow cannot occur since this maxes at `b'9'`.
buffer.push(dig_1 + b'0');
- // We simply add the appropriate offset which is `b'0` for decimal digits.
- // Overflow cannot occur since this maxes at `b'9'`.
- buffer.push(dig_2 + b'0');
- } else if dig_2 > 0 {
- // We simply add the appropriate offset which is `b'0` for decimal digits.
- // Overflow cannot occur since this maxes at `b'9'`.
- buffer.push(dig_2 + b'0');
}
// We simply add the appropriate offset which is `b'0` for decimal digits.
// Overflow cannot occur since this maxes at `b'9'`.
- buffer.push(dig_3 + b'0');
+ buffer.push(dig_2 + b'0');
}
}
buffer.extend_from_slice(br#"},"client_extension_results":{"cred_props":"#);
@@ -3016,17 +3086,6 @@ impl Metadata<'_> {
}
}
}
- buffer.extend_from_slice(br#","prf":"#);
- match self.client_extension_results.prf {
- None => buffer.extend_from_slice(b"null"),
- Some(prf) => {
- buffer.extend_from_slice(if prf.enabled {
- br#"{"enabled":true}"#
- } else {
- br#"{"enabled":false}"#
- });
- }
- }
buffer.extend_from_slice(br#"},"resident_key":"#);
buffer.extend_from_slice(match self.resident_key {
ResidentKeyRequirement::Required => br#""required"}"#,
@@ -3038,6 +3097,25 @@ impl Metadata<'_> {
// is valid UTF-8.
unsafe { String::from_utf8_unchecked(buffer) }
}
+ /// 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]
+ pub fn into_owned(self) -> MetadataOwned {
+ MetadataOwned {
+ attestation: self.attestation,
+ aaguid: AaguidOwned(self.aaguid.0.try_into().unwrap_or_else(|_e| {
+ unreachable!(
+ "there is a bug in Metadata that allows AAGUID to not have length of 16"
+ )
+ })),
+ extensions: self.extensions,
+ client_extension_results: self.client_extension_results,
+ resident_key: self.resident_key,
+ }
+ }
}
/// [`RegisteredCredential`] and [`AuthenticatedCredential`] static state.
///
@@ -3049,6 +3127,45 @@ pub struct StaticState<PublicKey> {
/// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) output during registration that are
/// used during authentication ceremonies.
pub extensions: AuthenticatorExtensionOutputStaticState,
+ /// [`getClientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults)
+ /// output during registration that are used during authentication ceremonies.
+ pub client_extension_results: ClientExtensionsOutputsStaticState,
+}
+impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8]>>
+ From<&'a StaticState<CompressedPubKey<T, T2, T3, T4>>>
+ for StaticState<CompressedPubKey<&'b [u8], &'b [u8], &'b [u8], &'b [u8]>>
+{
+ #[inline]
+ fn from(value: &'a StaticState<CompressedPubKey<T, T2, T3, T4>>) -> Self {
+ Self {
+ credential_public_key: (&value.credential_public_key).into(),
+ extensions: value.extensions,
+ client_extension_results: value.client_extension_results,
+ }
+ }
+}
+/// `StaticState` with an uncompressed [`Self::credential_public_key`].
+pub type StaticStateUncompressed<'a> = StaticState<UncompressedPubKey<'a>>;
+/// `StaticState` with a compressed [`Self::credential_public_key`].
+pub type StaticStateCompressed = StaticState<
+ CompressedPubKey<
+ [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
+ [u8; <NistP256 as Curve>::FieldBytesSize::INT],
+ [u8; <NistP384 as Curve>::FieldBytesSize::INT],
+ Vec<u8>,
+ >,
+>;
+impl StaticStateUncompressed<'_> {
+ /// Transforms `self` into `StaticState` that contains the compressed version of the public key.
+ #[inline]
+ #[must_use]
+ pub fn into_compressed(self) -> StaticStateCompressed {
+ StaticStateCompressed {
+ credential_public_key: self.credential_public_key.into_compressed(),
+ extensions: self.extensions,
+ client_extension_results: self.client_extension_results,
+ }
+ }
}
/// [`RegisteredCredential`] and [`AuthenticatedCredential`] dynamic state.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -3056,7 +3173,11 @@ pub struct DynamicState {
/// [`UV`](https://www.w3.org/TR/webauthn-3/#authdata-flags-uv).
///
/// Once this is `true`, it will remain `true`. It will be set to `true` when `false` iff
- /// [`Flag::user_verified`] and [`AuthenticationVerificationOptions::update_uv`].
+ /// [`Flag::user_verified`] and [`AuthenticationVerificationOptions::update_uv`]. In other words, this only
+ /// means that the user has been verified _at some point in the past_; but it does _not_ mean the user has
+ /// most-recently been verified this is because user verification is a _ceremony-specific_ property. To
+ /// enforce user verification for all ceremonies, [`UserVerificationRequirement::Required`] must always be
+ /// sent.
pub user_verified: bool,
/// This can only be updated if [`BackupReq`] allows for it.
pub backup: Backup,
diff --git a/src/response/register/bin.rs b/src/response/register/bin.rs
@@ -1,17 +1,13 @@
-#![expect(
- clippy::unseparated_literal_suffix,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
use super::{
super::super::bin::{
Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible as _,
},
Aaguid, Attestation, AuthenticationExtensionsPrfOutputs, AuthenticatorAttachment,
AuthenticatorExtensionOutputMetadata, AuthenticatorExtensionOutputStaticState, Backup,
- ClientExtensionsOutputs, CompressedP256PubKey, CompressedP384PubKey, CompressedPubKey,
- CredentialPropertiesOutput, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, Metadata,
- ResidentKeyRequirement, RsaPubKey, StaticState, UncompressedP256PubKey, UncompressedP384PubKey,
- UncompressedPubKey,
+ ClientExtensionsOutputsMetadata, ClientExtensionsOutputsStaticState, CompressedP256PubKey,
+ CompressedP384PubKey, CompressedPubKey, CredentialPropertiesOutput, CredentialProtectionPolicy,
+ DynamicState, Ed25519PubKey, FourToSixtyThree, Metadata, ResidentKeyRequirement, RsaPubKey,
+ StaticState, UncompressedP256PubKey, UncompressedP384PubKey, UncompressedPubKey,
};
use core::{
convert::Infallible,
@@ -19,8 +15,8 @@ use core::{
fmt::{self, Display, Formatter},
};
use p256::{
- elliptic_curve::{generic_array::typenum::ToInt as _, Curve},
NistP256,
+ elliptic_curve::{Curve, generic_array::typenum::ToInt as _},
};
use p384::NistP384;
impl EncodeBuffer for CredentialProtectionPolicy {
@@ -289,11 +285,22 @@ impl<'a> DecodeBuffer<'a> for AaguidOwned {
<[u8; super::AAGUID_LEN]>::decode_from_buffer(data).map(Self)
}
}
+impl EncodeBuffer for FourToSixtyThree {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.value().encode_into_buffer(buffer);
+ }
+}
impl EncodeBuffer for AuthenticatorExtensionOutputMetadata {
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
self.min_pin_length.encode_into_buffer(buffer);
}
}
+impl<'a> DecodeBuffer<'a> for FourToSixtyThree {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| Self::new(val).ok_or(EncDecErr))
+ }
+}
impl<'a> DecodeBuffer<'a> for AuthenticatorExtensionOutputMetadata {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
@@ -322,18 +329,26 @@ impl<'a> DecodeBuffer<'a> for AuthenticationExtensionsPrfOutputs {
bool::decode_from_buffer(data).map(|enabled| Self { enabled })
}
}
-impl EncodeBuffer for ClientExtensionsOutputs {
+impl EncodeBuffer for ClientExtensionsOutputsMetadata {
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
self.cred_props.encode_into_buffer(buffer);
+ }
+}
+impl EncodeBuffer for ClientExtensionsOutputsStaticState {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
self.prf.encode_into_buffer(buffer);
}
}
-impl<'a> DecodeBuffer<'a> for ClientExtensionsOutputs {
+impl<'a> DecodeBuffer<'a> for ClientExtensionsOutputsMetadata {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- Option::decode_from_buffer(data).and_then(|cred_props| {
- Option::decode_from_buffer(data).map(|prf| Self { cred_props, prf })
- })
+ Option::decode_from_buffer(data).map(|cred_props| Self { cred_props })
+ }
+}
+impl<'a> DecodeBuffer<'a> for ClientExtensionsOutputsStaticState {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ Option::decode_from_buffer(data).map(|prf| Self { prf })
}
}
impl Encode for Metadata<'_> {
@@ -348,9 +363,9 @@ impl Encode for Metadata<'_> {
// * 1 for `Attestation`
// * 16 for `Aaguid`.
// * 1 or 2 for `AuthenticatorExtensionOutputMetadata` where we assume 1 is the most common
- // * 2–5 for `ClientExtensionsOutputs` where we assume 2 is the most common
+ // * 1–3 for `ClientExtensionsOutputsMetadata` where we assume 1 is the most common
// * 1 for `ResidentKeyRequirement`
- let mut buffer = Vec::with_capacity(1 + 16 + 1 + 2 + 1);
+ let mut buffer = Vec::with_capacity(1 + 16 + 1 + 1 + 1);
self.attestation.encode_into_buffer(&mut buffer);
self.aaguid.encode_into_buffer(&mut buffer);
self.extensions.encode_into_buffer(&mut buffer);
@@ -370,7 +385,7 @@ pub struct MetadataOwned {
/// [`Metadata::extensions`].
pub extensions: AuthenticatorExtensionOutputMetadata,
/// [`Metadata::client_extension_results`].
- pub client_extension_results: ClientExtensionsOutputs,
+ pub client_extension_results: ClientExtensionsOutputsMetadata,
/// [`Metadata::resident_key`].
pub resident_key: ResidentKeyRequirement,
}
@@ -430,7 +445,7 @@ impl Decode for MetadataOwned {
AuthenticatorExtensionOutputMetadata::decode_from_buffer(&mut input)
.map_err(|_e| DecodeMetadataOwnedErr::Extensions)
.and_then(|extensions| {
- ClientExtensionsOutputs::decode_from_buffer(&mut input)
+ ClientExtensionsOutputsMetadata::decode_from_buffer(&mut input)
.map_err(|_e| DecodeMetadataOwnedErr::ClientExtensionResults)
.and_then(|client_extension_results| {
ResidentKeyRequirement::decode_from_buffer(&mut input)
@@ -469,7 +484,7 @@ impl Encode for StaticState<UncompressedPubKey<'_>> {
#[inline]
fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
let mut buffer = Vec::with_capacity(
- // The maximum value is 1 + 2 + 2048 + 4 + 1 + 1 + 1 = 2058 so overflow cannot happen.
+ // The maximum value is 1 + 2 + 2048 + 4 + 1 + 1 + 1 + 1 + 1 = 2060 so overflow cannot happen.
// `key.0.len() <= MAX_RSA_N_BYTES` which is 2048.
match self.credential_public_key {
UncompressedPubKey::Ed25519(_) => 33,
@@ -478,10 +493,14 @@ impl Encode for StaticState<UncompressedPubKey<'_>> {
UncompressedPubKey::Rsa(key) => 1 + 2 + key.0.len() + 4,
} + 1
+ 1
- + usize::from(self.extensions.hmac_secret.is_some()),
+ + usize::from(self.extensions.hmac_secret.is_some())
+ + 1
+ + usize::from(self.client_extension_results.prf.is_some()),
);
self.credential_public_key.encode_into_buffer(&mut buffer);
self.extensions.encode_into_buffer(&mut buffer);
+ self.client_extension_results
+ .encode_into_buffer(&mut buffer);
Ok(buffer)
}
}
@@ -492,6 +511,8 @@ pub enum DecodeStaticStateErr {
CredentialPublicKey,
/// Variant returned when [`StaticState::extensions`] could not be decoded.
Extensions,
+ /// Variant returned when [`StaticState::client_extension_results`] could not be decoded.
+ ClientExtensionResults,
/// Variant returned when there was trailing data after decoding a [`StaticState`].
TrailingData,
}
@@ -501,6 +522,7 @@ impl Display for DecodeStaticStateErr {
f.write_str(match *self {
Self::CredentialPublicKey => "credential_public_key could not be decoded",
Self::Extensions => "extensions could not be decoded",
+ Self::ClientExtensionResults => "client_extension_results could not be decoded",
Self::TrailingData => "there was trailing data after decoding a StaticState",
})
}
@@ -527,14 +549,19 @@ impl Decode
AuthenticatorExtensionOutputStaticState::decode_from_buffer(&mut input)
.map_err(|_e| DecodeStaticStateErr::Extensions)
.and_then(|extensions| {
- if input.is_empty() {
- Ok(Self {
- credential_public_key,
- extensions,
+ ClientExtensionsOutputsStaticState::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeStaticStateErr::ClientExtensionResults)
+ .and_then(|client_extension_results| {
+ if input.is_empty() {
+ Ok(Self {
+ credential_public_key,
+ extensions,
+ client_extension_results,
+ })
+ } else {
+ Err(DecodeStaticStateErr::TrailingData)
+ }
})
- } else {
- Err(DecodeStaticStateErr::TrailingData)
- }
})
})
}
diff --git a/src/response/register/error.rs b/src/response/register/error.rs
@@ -3,30 +3,30 @@ use super::super::SerdeJsonErr;
#[cfg(doc)]
use super::{
super::super::{
+ RegisteredCredential,
request::{
+ BackupReq, CredentialMediationRequirement, UserVerificationRequirement,
register::{
AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, Extension,
PublicKeyCredentialCreationOptions, RegistrationServerState,
RegistrationVerificationOptions,
},
- BackupReq, CredentialMediationRequirement, UserVerificationRequirement,
},
- RegisteredCredential,
},
Aaguid, Attestation, AttestationObject, AttestedCredentialData, AuthenticatorAttachment,
AuthenticatorAttestation, AuthenticatorData, AuthenticatorExtensionOutput, Backup,
ClientExtensionsOutputs, CollectedClientData, CompressedP256PubKey, CompressedP384PubKey,
- Ed25519PubKey, Ed25519Signature, Flag, Metadata, PackedAttestation, RsaPubKey,
- UncompressedP256PubKey, UncompressedP384PubKey, UncompressedPubKey, MAX_RSA_N_BYTES, MIN_RSA_E,
- MIN_RSA_N_BYTES,
+ Ed25519PubKey, Ed25519Signature, Flag, MAX_RSA_N_BYTES, MIN_RSA_E, MIN_RSA_N_BYTES, Metadata,
+ PackedAttestation, RsaPubKey, UncompressedP256PubKey, UncompressedP384PubKey,
+ UncompressedPubKey,
};
use super::{
super::{
- super::{request::register::CredProtect, CredentialErr},
- error::{CollectedClientDataErr, CredentialIdErr},
+ super::{CredentialErr, request::register::CredProtect},
AuthRespErr, AuthenticatorDataErr as AuthDataErr, CeremonyErr,
+ error::{CollectedClientDataErr, CredentialIdErr},
},
- CredentialProtectionPolicy,
+ CredentialProtectionPolicy, FourToSixtyThree,
};
use core::{
convert::Infallible,
@@ -476,8 +476,6 @@ pub enum ExtensionErr {
MissingPrf,
/// [`Extension::cred_protect`] was requested, but the required response was not sent back.
MissingCredProtect,
- /// [`Extension::prf`] was requested, but the required response was not sent back.
- MissingHmacSecret,
/// [`Extension::min_pin_length`] was requested, but the required response was not sent back.
MissingMinPinLength,
/// [`Extension::cred_protect`] was requested with the first policy, but the second policy was sent back.
@@ -492,7 +490,7 @@ pub enum ExtensionErr {
/// [`Extension::min_pin_length`] was requested, but
/// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension)
/// was sent set to the second value which is strictly less than the required first value.
- InvalidMinPinLength(u8, u8),
+ InvalidMinPinLength(FourToSixtyThree, FourToSixtyThree),
}
impl Display for ExtensionErr {
#[inline]
@@ -516,7 +514,6 @@ impl Display for ExtensionErr {
Self::MissingCredProps => f.write_str("credProps was not sent from the client"),
Self::MissingPrf => f.write_str("prf was not sent from the client"),
Self::MissingCredProtect => f.write_str("credProtect was not sent from the client"),
- Self::MissingHmacSecret => f.write_str("hmac-secret was not sent from the client"),
Self::MissingMinPinLength => f.write_str("minPinLength was not sent from the client"),
Self::InvalidCredProtectValue(sent, rec) => write!(
f,
diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs
@@ -1,7 +1,3 @@
-#![expect(
- clippy::question_mark_used,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
use super::{
super::{
super::request::register::CoseAlgorithmIdentifier,
@@ -926,22 +922,24 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
// used.
attested_info.as_ref().map_or_else(
|| AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
- if matches!(att_obj.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P384(_)) {
- // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
- // is the start of the raw authenticator data which itself contains the raw Credential ID.
- Ok(Some((CoseAlgorithmIdentifier::Es384, auth_idx, auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
- } else {
- Err(Error::missing_field(PUBLIC_KEY))
+ match att_obj.auth_data.attested_credential_data.credential_public_key {
+ UncompressedPubKey::P384(_) => {
+ // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
+ // is the start of the raw authenticator data which itself contains the raw Credential ID.
+ Ok(Some((CoseAlgorithmIdentifier::Es384, auth_idx, auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
+ }
+ UncompressedPubKey::Ed25519(_) | UncompressedPubKey::P256(_) | UncompressedPubKey::Rsa(_) => Err(Error::missing_field(PUBLIC_KEY)),
}
}),
- |&(ref attested_data, cred_id_start)| if matches!(attested_data.credential_public_key, UncompressedPubKey::P384(_)) {
- // Overflow won't occur since this is correct. This is correct since we successfully parsed
- // `AttestedCredentialData` and calculated `cred_id_start` from it.
- Ok(Some((CoseAlgorithmIdentifier::Es384, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
- } else if flag {
- Err(Error::invalid_type(Unexpected::Other("null"), &format!("{PUBLIC_KEY} to be a base64url-encoded DER-encoded SubjectPublicKeyInfo").as_str()))
- } else {
- Err(Error::missing_field(PUBLIC_KEY))
+ |&(ref attested_data, cred_id_start)| {
+ match attested_data.credential_public_key {
+ UncompressedPubKey::P384(_) => {
+ // Overflow won't occur since this is correct. This is correct since we successfully parsed
+ // `AttestedCredentialData` and calculated `cred_id_start` from it.
+ Ok(Some((CoseAlgorithmIdentifier::Es384, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
+ }
+ UncompressedPubKey::Ed25519(_) | UncompressedPubKey::P256(_) | UncompressedPubKey::Rsa(_) => if flag { Err(Error::invalid_type(Unexpected::Other("null"), &format!("{PUBLIC_KEY} to be a base64url-encoded DER-encoded SubjectPublicKeyInfo").as_str())) } else { Err(Error::missing_field(PUBLIC_KEY)) },
+ }
}
)
}
@@ -951,28 +949,35 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
attested_info.as_ref().map_or_else(
|| AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
if key == att_obj.auth_data.attested_credential_data.credential_public_key {
+ let alg = match att_obj.auth_data.attested_credential_data.credential_public_key {
+ UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
+ UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
+ UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
+ UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
+ };
// This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
// is the start of the raw authenticator data which itself contains the raw Credential ID.
- Ok((auth_idx, auth_idx+ att_obj.auth_data.attested_credential_data.credential_id.0.len()))
+ Ok(Some((alg, auth_idx, auth_idx+ att_obj.auth_data.attested_credential_data.credential_id.0.len())))
} else {
Err(Error::invalid_value(Unexpected::Bytes(der.0.as_slice()), &format!("DER-encoded public key to match the public key within the attestation object: {:?}", att_obj.auth_data.attested_credential_data.credential_public_key).as_str()))
}
}),
|&(ref attested_data, cred_id_start)| {
if key == attested_data.credential_public_key {
+ let alg = match attested_data.credential_public_key {
+ UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
+ UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
+ UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
+ UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
+ };
// Overflow won't occur since this is correct. This is correct since we successfully parsed
// `AttestedCredentialData` and calculated `cred_id_start` from it.
- Ok((cred_id_start, cred_id_start + attested_data.credential_id.0.len()))
+ Ok(Some((alg, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
} else {
Err(Error::invalid_value(Unexpected::Bytes(der.0.as_slice()), &format!("DER-encoded public key to match the public key within the attestation object: {:?}", attested_data.credential_public_key).as_str()))
}
}
- ).map(|(start, last)| Some((match key {
- UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
- UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
- UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
- UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
- }, start, last)))
+ )
})
}
).and_then(|cred_key_alg_cred_info| {
@@ -1065,9 +1070,10 @@ impl<'de> Deserialize<'de> for AuthenticatorAttestation {
/// is deserialized via [`AuthTransports::deserialize`]; the decoded `publicKey` is parsed according to the
/// applicable DER-encoded ASN.1 `SubjectPublicKeyInfo` schema;
/// [`publicKeyAlgorithm`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-publickeyalgorithm)
- /// is deserialized via [`CoseAlgorithmIdentifier::deserialize`]; all `required` fields in the
- /// `AuthenticatorAttestationResponseJSON` Web IDL `dictionary` exist (and must not be `null`); `publicKey` exists when
- /// Ed25519, P-256 with SHA-256, or RSASSA-PKCS1-v1_5 with SHA-256 is used (and must not be `null`)
+ /// is deserialized according to
+ /// [`CoseAlgorithmIdentifier`](https://www.w3.org/TR/webauthn-3/#typedefdef-cosealgorithmidentifier); all `required`
+ /// fields in the `AuthenticatorAttestationResponseJSON` Web IDL `dictionary` exist (and must not be `null`); `publicKey`
+ /// exists when Ed25519, P-256 with SHA-256, or RSASSA-PKCS1-v1_5 with SHA-256 is used (and must not be `null`)
/// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#sctn-public-key-easy); the `publicKeyAlgorithm` aligns
/// with
/// [`credentialPublicKey`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialpublickey)
@@ -1176,7 +1182,7 @@ impl<'de> Deserialize<'de> for CredentialPropertiesOutput {
}
impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfOutputs {
/// Deserializes a `struct` based on
- /// [`AuthenticationExtensionsPRFOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs).
+ /// [`AuthenticationExtensionsPRFOutputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputsjson).
///
/// Note unknown and duplicate keys are forbidden;
/// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled)
@@ -1561,7 +1567,7 @@ mod tests {
b't',
b'a',
cbor::BYTES_INFO_24,
- 113,
+ 115,
// `rpIdHash`.
0,
0,
@@ -2242,7 +2248,7 @@ mod tests {
// Unknown `transports`.
err = Error::invalid_value(
Unexpected::Str("Usb"),
- &"'ble', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'",
+ &"'ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'",
)
.to_string()
.into_bytes();
@@ -4583,7 +4589,7 @@ mod tests {
);
// `publicKeyAlgorithm` mismatch.
let mut err = Error::invalid_value(
- Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).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,7 +4603,7 @@ mod tests {
"authenticatorData": b64_adata,
"transports": [],
"publicKey": b64_key,
- "publicKeyAlgorithm": -8,
+ "publicKeyAlgorithm": -7,
"attestationObject": b64_aobj,
},
"clientExtensionResults": {},
@@ -4737,7 +4743,7 @@ mod tests {
);
// `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
err = Error::invalid_value(
- Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).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();
@@ -4750,7 +4756,7 @@ mod tests {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"transports": [],
- "publicKeyAlgorithm": -8,
+ "publicKeyAlgorithm": -7,
"attestationObject": b64_aobj,
},
"clientExtensionResults": {},
@@ -4788,7 +4794,7 @@ mod tests {
);
// `publicKeyAlgorithm` mismatch when `publicKey` is null.
err = Error::invalid_value(
- Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).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,7 +4808,7 @@ mod tests {
"authenticatorData": b64_adata,
"transports": [],
"publicKey": null,
- "publicKeyAlgorithm": -8,
+ "publicKeyAlgorithm": -7,
"attestationObject": b64_aobj,
},
"clientExtensionResults": {},
diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs
@@ -1,7 +1,3 @@
-#![expect(
- clippy::question_mark_used,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
#[cfg(doc)]
use super::super::{super::request::register::CoseAlgorithmIdentifier, Challenge, CredentialId};
use super::{
@@ -1122,7 +1118,7 @@ mod tests {
// Unknown `transports`.
err = Error::invalid_value(
Unexpected::Str("Usb"),
- &"'ble', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'",
+ &"'ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'",
)
.to_string()
.into_bytes();
@@ -1716,7 +1712,7 @@ mod tests {
// Unknown `transports`.
err = Error::invalid_value(
Unexpected::Str("Usb"),
- &"'ble', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'",
+ &"'ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'",
)
.to_string()
.into_bytes();
@@ -3827,7 +3823,7 @@ mod tests {
);
// `publicKeyAlgorithm` mismatch.
let mut err = Error::invalid_value(
- Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).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();
@@ -3841,7 +3837,7 @@ mod tests {
"authenticatorData": b64_adata,
"transports": [],
"publicKey": b64_key,
- "publicKeyAlgorithm": -8,
+ "publicKeyAlgorithm": -7,
"attestationObject": b64_aobj,
},
"clientExtensionResults": {},
@@ -3939,13 +3935,8 @@ mod tests {
},
"clientExtensionResults": {},
"type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .unwrap_err().to_string().into_bytes()[..err.len()],
- err
- );
+ }).to_string().as_str()
+ ).unwrap_err().to_string().into_bytes()[..err.len()], err);
// Missing `publicKey`.
assert!(
serde_json::from_str::<RegistrationRelaxed>(
@@ -4732,8 +4723,8 @@ mod tests {
.to_string()
.as_str()
)
- .unwrap_err().to_string().into_bytes()[..err.len()],
- err
+ .unwrap_err().to_string().into_bytes()[..err.len()],
+ err
);
// Missing `publicKey`.
assert!(
diff --git a/src/response/ser.rs b/src/response/ser.rs
@@ -1,7 +1,3 @@
-#![expect(
- clippy::question_mark_used,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
extern crate alloc;
use super::{
AllAcceptedCredentialsOptions, AuthTransports, AuthenticatorAttachment, AuthenticatorTransport,
@@ -62,6 +58,8 @@ impl<'de> Deserialize<'de> for AuthenticatorTransport {
/// Deserializes [`prim@str`] based on
/// [`AuthenticatorTransport`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport).
///
+ /// Note `"cable"` is also supported and will be interpreted as [`Self::Hybrid`].
+ ///
/// # Examples
///
/// ```
@@ -90,9 +88,11 @@ impl<'de> Deserialize<'de> for AuthenticatorTransport {
where
E: Error,
{
+ /// Legacy version of [`Self::Hybrid`].
+ const CA_BLE: &str = "cable";
match v {
BLE => Ok(AuthenticatorTransport::Ble),
- HYBRID => Ok(AuthenticatorTransport::Hybrid),
+ CA_BLE | HYBRID => Ok(AuthenticatorTransport::Hybrid),
INTERNAL => Ok(AuthenticatorTransport::Internal),
NFC => Ok(AuthenticatorTransport::Nfc),
SMART_CARD => Ok(AuthenticatorTransport::SmartCard),
@@ -100,7 +100,7 @@ impl<'de> Deserialize<'de> for AuthenticatorTransport {
_ => Err(E::invalid_value(
Unexpected::Str(v),
&format!(
- "'{BLE}', '{HYBRID}', '{INTERNAL}', '{NFC}', '{SMART_CARD}', or '{USB}'"
+ "'{BLE}', '{CA_BLE}', '{HYBRID}', '{INTERNAL}', '{NFC}', '{SMART_CARD}', or '{USB}'"
)
.as_str(),
)),
@@ -805,7 +805,11 @@ where
)
}
}
-impl<const USER_LEN: usize> Serialize for AllAcceptedCredentialsOptions<'_, '_, USER_LEN> {
+use super::UserHandle;
+impl<const USER_LEN: usize> Serialize for AllAcceptedCredentialsOptions<'_, '_, USER_LEN>
+where
+ UserHandle<USER_LEN>: Serialize,
+{
/// Serializes `self` to conform with
/// [`AllAcceptedCredentialsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions).
///
@@ -865,7 +869,10 @@ impl<const USER_LEN: usize> Serialize for AllAcceptedCredentialsOptions<'_, '_,
})
}
}
-impl<const LEN: usize> Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_, LEN> {
+impl<const LEN: usize> Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_, LEN>
+where
+ UserHandle<LEN>: Serialize,
+{
/// Serializes `self` to conform with
/// [`CurrentUserDetailsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions).
///
diff --git a/src/response/ser_relaxed.rs b/src/response/ser_relaxed.rs
@@ -1,18 +1,13 @@
-#![expect(
- clippy::pub_use,
- clippy::question_mark_used,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
extern crate alloc;
+#[cfg(doc)]
+use super::{Challenge, LimitedVerificationParser};
use super::{
+ ClientDataJsonParser, CollectedClientData, Origin, SentChallenge,
ser::{
AuthenticationExtensionsPrfValues, AuthenticationExtensionsPrfValuesVisitor,
PRF_VALUES_FIELDS,
},
- ClientDataJsonParser, CollectedClientData, Origin, SentChallenge,
};
-#[cfg(doc)]
-use super::{Challenge, LimitedVerificationParser};
use alloc::borrow::Cow;
use core::{
fmt::{self, Formatter},
@@ -70,11 +65,7 @@ impl<const R: bool> ClientDataJsonParser for RelaxedClientDataJsonParser<R> {
serde_json::from_slice::<CDataJsonHelper<'_, R>>(json.split_at_checked(BOM.len()).map_or(
json,
|(bom, rem)| {
- if bom == BOM {
- rem
- } else {
- json
- }
+ if bom == BOM { rem } else { json }
},
))
.map(|val| val.0)
@@ -87,11 +78,7 @@ impl<const R: bool> ClientDataJsonParser for RelaxedClientDataJsonParser<R> {
serde_json::from_slice::<Chall>(json.split_at_checked(BOM.len()).map_or(
json,
|(bom, rem)| {
- if bom == BOM {
- rem
- } else {
- json
- }
+ if bom == BOM { rem } else { json }
},
))
.map(|c| c.0)
@@ -226,7 +213,10 @@ impl<'de: 'a, 'a, const R: bool> Visitor<'de> for RelaxedHelper<'a, R> {
impl Visitor<'_> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- write!(formatter, "'{TYPE}', '{CHALLENGE}', '{ORIGIN}', '{CROSS_ORIGIN}', or '{TOP_ORIGIN}'")
+ write!(
+ formatter,
+ "'{TYPE}', '{CHALLENGE}', '{ORIGIN}', '{CROSS_ORIGIN}', or '{TOP_ORIGIN}'"
+ )
}
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
@@ -524,8 +514,10 @@ mod tests {
"topOrigin": "https://example.org"
})
.to_string();
- assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
- .map_or(false, |c| !c.cross_origin));
+ assert!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .map_or(false, |c| !c.cross_origin)
+ );
// Missing `crossOrigin`.
let input = serde_json::json!({
"challenge": "ABABABABABABABABABABAA",
@@ -534,8 +526,10 @@ mod tests {
"topOrigin": "https://example.org"
})
.to_string();
- assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
- .map_or(false, |c| !c.cross_origin));
+ assert!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .map_or(false, |c| !c.cross_origin)
+ );
// `null` `topOrigin`.
let input = serde_json::json!({
"challenge": "ABABABABABABABABABABAA",
@@ -545,8 +539,10 @@ mod tests {
"topOrigin": null
})
.to_string();
- assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
- .map_or(false, |c| c.top_origin.is_none()));
+ assert!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .map_or(false, |c| c.top_origin.is_none())
+ );
// Missing `topOrigin`.
let input = serde_json::json!({
"challenge": "ABABABABABABABABABABAA",
@@ -555,8 +551,10 @@ mod tests {
"crossOrigin": true,
})
.to_string();
- assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
- .map_or(false, |c| c.top_origin.is_none()));
+ assert!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .map_or(false, |c| c.top_origin.is_none())
+ );
// `null` `challenge`.
err = Error::invalid_type(
Unexpected::Other("null"),