commit 78b64db0e7571cf9fb3f8db2ecc500447f6a50bc
parent 1d95b0f6f7b1a68e29404a5dcd679b573d460618
Author: Zack Newman <zack@philomathiclife.com>
Date: Tue, 8 Jul 2025 13:26:39 -0600
static ascii domain
Diffstat:
12 files changed, 594 insertions(+), 244 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -9,25 +9,31 @@ license = "MIT OR Apache-2.0"
name = "webauthn_rp"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/webauthn_rp/"
-rust-version = "1.87.0"
+rust-version = "1.88.0"
version = "0.4.0"
[lints.rust]
ambiguous_negative_literals = { level = "deny", priority = -1 }
closure_returning_async_block = { level = "deny", priority = -1 }
+deprecated_safe = { level = "deny", priority = -1 }
deref_into_dyn_supertrait = { level = "deny", priority = -1 }
ffi_unwind_calls = { level = "deny", priority = -1 }
future_incompatible = { level = "deny", priority = -1 }
+#fuzzy_provenance_casts = { level = "deny", priority = -1 }
impl_trait_redundant_captures = { level = "deny", priority = -1 }
-keyword-idents = { level = "deny", priority = -1 }
+keyword_idents = { level = "deny", priority = -1 }
let_underscore = { level = "deny", priority = -1 }
linker_messages = { level = "deny", priority = -1 }
+#lossy_provenance_casts = { level = "deny", priority = -1 }
macro_use_extern_crate = { level = "deny", priority = -1 }
meta_variable_misuse = { level = "deny", priority = -1 }
missing_copy_implementations = { level = "deny", priority = -1 }
missing_debug_implementations = { level = "deny", priority = -1 }
missing_docs = { level = "deny", priority = -1 }
+#multiple_supertrait_upcastable = { level = "deny", priority = -1 }
+#must_not_suspend = { level = "deny", priority = -1 }
non_ascii_idents = { level = "deny", priority = -1 }
+#non_exhaustive_omitted_patterns = { level = "deny", priority = -1 }
nonstandard_style = { level = "deny", priority = -1 }
redundant_imports = { level = "deny", priority = -1 }
redundant_lifetimes = { level = "deny", priority = -1 }
@@ -36,12 +42,13 @@ 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 }
-single-use-lifetimes = { level = "deny", priority = -1 }
+single_use_lifetimes = { level = "deny", priority = -1 }
+#supertrait_item_shadowing_definition = { level = "deny", priority = -1 }
trivial_casts = { level = "deny", priority = -1 }
trivial_numeric_casts = { level = "deny", priority = -1 }
unit_bindings = { level = "deny", priority = -1 }
-unknown_lints = { level = "deny", priority = -1 }
unnameable_types = { level = "deny", priority = -1 }
+#unqualified_local_imports = { level = "deny", priority = -1 }
unreachable_pub = { level = "deny", priority = -1 }
unsafe_code = { level = "deny", priority = -1 }
unstable_features = { level = "deny", priority = -1 }
@@ -74,8 +81,8 @@ implicit_return = "allow"
min_ident_chars = "allow"
missing_trait_methods = "allow"
module_name_repetitions = "allow"
-pub_with_shorthand = "allow"
pub_use = "allow"
+pub_with_shorthand = "allow"
question_mark_used = "allow"
ref_patterns = "allow"
return_and_then = "allow"
diff --git a/README.md b/README.md
@@ -20,11 +20,11 @@ having said that, there are pre-defined serialization formats for "common" deplo
use core::convert;
use webauthn_rp::{
AuthenticatedCredential64, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
- DiscoverableCredentialRequestOptions, PublicKeyCredentialCreationOptions64, RegisteredCredential64,
+ DiscoverableCredentialRequestOptions, CredentialCreationOptions64, RegisteredCredential64,
Registration, RegistrationServerState64,
hash::hash_set::FixedCapHashSet,
request::{
- AsciiDomain, PublicKeyCredentialDescriptor, RpId,
+ AsciiDomainStatic, PublicKeyCredentialDescriptor, RpId,
auth::AuthenticationVerificationOptions,
register::{
Nickname, PublicKeyCredentialUserEntity64, RegistrationVerificationOptions,
@@ -34,13 +34,17 @@ use webauthn_rp::{
response::{
CredentialId,
auth::error::AuthCeremonyErr,
- register::{CompressedPubKey, DynamicState, error::RegCeremonyErr},
+ register::{CompressedPubKeyOwned, DynamicState, error::RegCeremonyErr},
},
};
use serde::de::{Deserialize, Deserializer};
use serde_json::Error as JsonErr;
/// The RP ID our application uses.
-const RP_ID: &str = "example.com";
+const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+/// The registration verification options.
+const REG_OPTS: &RegistrationVerificationOptions::<'static, 'static, &'static str, &'static str> = &RegistrationVerificationOptions::new();
+/// The authentication verification options.
+const AUTH_OPTS: &AuthenticationVerificationOptions::<'static, 'static, &'static str, &'static str> = &AuthenticationVerificationOptions::new();
/// Error we return in our application when a function fails.
enum AppErr {
/// WebAuthn registration ceremony failed.
@@ -101,14 +105,10 @@ impl<'de: 'a + 'b, 'a, 'b> Deserialize<'de> for AccountReg<'a, 'b> {
fn start_account_creation(
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 user_id = UserHandle64::new();
let (server, client) =
- PublicKeyCredentialCreationOptions64::first_passkey_with_blank_user_info(
- &rp_id, &user_id,
+ CredentialCreationOptions64::first_passkey_with_blank_user_info(
+ RP_ID, &user_id,
)
.start_ceremony()
.unwrap_or_else(|_e| {
@@ -132,9 +132,9 @@ fn start_account_creation(
/// authenticator.
fn finish_account_creation(
reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
- client_data: Vec<u8>,
+ client_data: &[u8],
) -> Result<(), AppErr> {
- let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data.as_slice())?;
+ let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data)?;
insert_account(
&account,
reg_ceremonies
@@ -142,12 +142,9 @@ fn finish_account_creation(
.take(&account.registration.challenge_relaxed()?)
.ok_or(AppErr::MissingWebAuthnCeremony)?
.verify(
- &RpId::Domain(
- AsciiDomain::try_from(RP_ID.to_owned())
- .unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
- ),
+ RP_ID,
&account.registration,
- &RegistrationVerificationOptions::<&str, &str>::default(),
+ REG_OPTS,
)?,
)
}
@@ -161,12 +158,8 @@ fn start_cred_registration(
user_id: &UserHandle64,
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) = PublicKeyCredentialCreationOptions64::passkey(&rp_id, entity, creds)
+ let (entity, creds) = select_user_info(user_id)?.ok_or(AppErr::NoAccount)?;
+ let (server, client) = CredentialCreationOptions64::passkey(RP_ID, entity, creds)
.start_ceremony()
.unwrap_or_else(|_e| {
unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
@@ -189,22 +182,19 @@ fn start_cred_registration(
/// authenticator.
fn finish_cred_registration(
reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
- client_data: Vec<u8>,
+ client_data: &[u8],
) -> Result<(), AppErr> {
// `Registration::from_json_custom` is available iff `serde_relaxed` is enabled.
- let registration = Registration::from_json_custom(client_data.as_slice())?;
+ let registration = Registration::from_json_custom(client_data)?;
insert_credential(
reg_ceremonies
// `Registration::challenge_relaxed` is available iff `serde_relaxed` is enabled.
.take(®istration.challenge_relaxed()?)
.ok_or(AppErr::MissingWebAuthnCeremony)?
.verify(
- &RpId::Domain(
- AsciiDomain::try_from(RP_ID.to_owned())
- .unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
- ),
+ RP_ID,
®istration,
- &RegistrationVerificationOptions::<&str, &str>::default(),
+ REG_OPTS,
)?,
)
}
@@ -212,11 +202,7 @@ fn finish_cred_registration(
fn start_auth(
auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
) -> 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 (server, client) = DiscoverableCredentialRequestOptions::passkey(&rp_id)
+ let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID)
.start_ceremony()
.unwrap_or_else(|_e| {
unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
@@ -233,28 +219,25 @@ fn start_auth(
/// Finishes the passkey authentication ceremony.
fn finish_auth(
auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
- client_data: Vec<u8>,
+ client_data: &[u8],
) -> Result<(), AppErr> {
// `DiscoverableAuthentication64::from_json_custom` is available iff `serde_relaxed` is enabled.
let authentication =
- DiscoverableAuthentication64::from_json_custom(client_data.as_slice())?;
+ DiscoverableAuthentication64::from_json_custom(client_data)?;
let mut cred = select_credential(
authentication.raw_id(),
authentication.response().user_handle(),
)?
- .ok_or_else(|| AppErr::NoCredential)?;
+ .ok_or(AppErr::NoCredential)?;
if auth_ceremonies
// `DiscoverableAuthentication64::challenge_relaxed` is available iff `serde_relaxed` is enabled.
.take(&authentication.challenge_relaxed()?)
.ok_or(AppErr::MissingWebAuthnCeremony)?
.verify(
- &RpId::Domain(
- AsciiDomain::try_from(RP_ID.to_owned())
- .unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
- ),
+ RP_ID,
&authentication,
&mut cred,
- &AuthenticationVerificationOptions::<&str, &str>::default(),
+ AUTH_OPTS,
)?
{
update_credential(cred.id(), cred.dynamic_state())
@@ -279,11 +262,11 @@ fn insert_account(
/// # Errors
///
/// Errors iff fetching the data errors.
-fn select_user_info<'a>(
- user_id: &'a UserHandle64,
+fn select_user_info(
+ user_id: &UserHandle64,
) -> Result<
Option<(
- PublicKeyCredentialUserEntity64<'static, 'static, 'a>,
+ PublicKeyCredentialUserEntity64<'static, 'static, '_>,
Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
)>,
AppErr,
@@ -315,7 +298,7 @@ fn select_credential<'cred, 'user>(
AuthenticatedCredential64<
'cred,
'user,
- CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>,
+ CompressedPubKeyOwned,
>,
>,
AppErr,
diff --git a/src/hash/hash_set.rs b/src/hash/hash_set.rs
@@ -10,7 +10,7 @@ use hashbrown::{
use std::time::Instant;
#[cfg(feature = "serializable_server_state")]
use std::time::SystemTime;
-/// `newtype` around [`HashSet`] that maximum [`HashSet::capacity`].
+/// `newtype` around [`HashSet`] that has 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.,
@@ -254,3 +254,83 @@ impl<T, S> From<HashSet<T, S>> for FixedCapHashSet<T, S> {
Self(value)
}
}
+#[cfg(test)]
+mod tests {
+ use super::{Equivalent, FixedCapHashSet, TimedCeremony};
+ use core::hash::{Hash, Hasher};
+ #[cfg(not(feature = "serializable_server_state"))]
+ use std::time::Instant;
+ #[cfg(feature = "serializable_server_state")]
+ use std::time::SystemTime;
+ #[derive(Clone, Copy)]
+ struct Ceremony {
+ id: usize,
+ #[cfg(not(feature = "serializable_server_state"))]
+ exp: Instant,
+ #[cfg(feature = "serializable_server_state")]
+ exp: SystemTime,
+ }
+ impl Default for Ceremony {
+ fn default() -> Self {
+ Self {
+ id: 0,
+ #[cfg(not(feature = "serializable_server_state"))]
+ exp: Instant::now(),
+ #[cfg(feature = "serializable_server_state")]
+ exp: SystemTime::now(),
+ }
+ }
+ }
+ impl PartialEq for Ceremony {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ }
+ }
+ impl Eq for Ceremony {}
+ impl Hash for Ceremony {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.id.hash(state);
+ }
+ }
+ impl TimedCeremony for Ceremony {
+ #[cfg(not(feature = "serializable_server_state"))]
+ fn expiration(&self) -> Instant {
+ self.exp
+ }
+ #[cfg(feature = "serializable_server_state")]
+ fn expiration(&self) -> SystemTime {
+ self.exp
+ }
+ }
+ impl Equivalent<Ceremony> for usize {
+ fn equivalent(&self, key: &Ceremony) -> bool {
+ *self == key.id
+ }
+ }
+ #[test]
+ fn hash_set_insert_removed() {
+ let mut set = FixedCapHashSet::new(8);
+ let cap = set.as_ref().capacity();
+ let mut cer = Ceremony::default();
+ for i in 0..cap {
+ assert_eq!(set.as_ref().capacity(), cap);
+ cer.id = i;
+ assert_eq!(set.insert(cer), Some(true));
+ }
+ assert_eq!(set.as_ref().capacity(), cap);
+ assert_eq!(set.as_ref().len(), cap);
+ for i in 0..cap {
+ assert!(set.as_ref().contains(&i));
+ }
+ cer.id = cap;
+ assert_eq!(set.insert_remove_expired(cer), Some(true));
+ assert_eq!(set.as_ref().capacity(), cap);
+ assert_eq!(set.as_ref().len(), cap);
+ let mut counter = 0;
+ for i in 0..cap {
+ counter += usize::from(set.as_ref().contains(&i));
+ }
+ assert_eq!(counter, cap - 1);
+ assert!(set.as_ref().contains(&cap));
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -24,7 +24,7 @@
//! Registration, RegistrationServerState64,
//! hash::hash_set::FixedCapHashSet,
//! request::{
-//! AsciiDomain, PublicKeyCredentialDescriptor, RpId,
+//! AsciiDomainStatic, PublicKeyCredentialDescriptor, RpId,
//! auth::AuthenticationVerificationOptions,
//! register::{
//! Nickname, PublicKeyCredentialUserEntity64, RegistrationVerificationOptions,
@@ -34,7 +34,7 @@
//! response::{
//! CredentialId,
//! auth::error::AuthCeremonyErr,
-//! register::{CompressedPubKey, DynamicState, error::RegCeremonyErr},
+//! register::{CompressedPubKeyOwned, DynamicState, error::RegCeremonyErr},
//! },
//! };
//! # #[cfg(feature = "serde")]
@@ -42,7 +42,11 @@
//! # #[cfg(feature = "serde_relaxed")]
//! use serde_json::Error as JsonErr;
//! /// The RP ID our application uses.
-//! const RP_ID: &str = "example.com";
+//! const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+//! /// The registration verification options.
+//! const REG_OPTS: &RegistrationVerificationOptions::<'static, 'static, &'static str, &'static str> = &RegistrationVerificationOptions::new();
+//! /// The authentication verification options.
+//! const AUTH_OPTS: &AuthenticationVerificationOptions::<'static, 'static, &'static str, &'static str> = &AuthenticationVerificationOptions::new();
//! /// Error we return in our application when a function fails.
//! enum AppErr {
//! /// WebAuthn registration ceremony failed.
@@ -108,14 +112,10 @@
//! fn start_account_creation(
//! 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 user_id = UserHandle64::new();
//! let (server, client) =
//! CredentialCreationOptions64::first_passkey_with_blank_user_info(
-//! &rp_id, &user_id,
+//! RP_ID, &user_id,
//! )
//! .start_ceremony()
//! .unwrap_or_else(|_e| {
@@ -140,9 +140,9 @@
//! # #[cfg(feature = "serde_relaxed")]
//! fn finish_account_creation(
//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
-//! client_data: Vec<u8>,
+//! client_data: &[u8],
//! ) -> Result<(), AppErr> {
-//! let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data.as_slice())?;
+//! let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data)?;
//! insert_account(
//! &account,
//! reg_ceremonies
@@ -150,12 +150,9 @@
//! .take(&account.registration.challenge_relaxed()?)
//! .ok_or(AppErr::MissingWebAuthnCeremony)?
//! .verify(
-//! &RpId::Domain(
-//! AsciiDomain::try_from(RP_ID.to_owned())
-//! .unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
-//! ),
+//! RP_ID,
//! &account.registration,
-//! &RegistrationVerificationOptions::<&str, &str>::default(),
+//! REG_OPTS,
//! )?,
//! )
//! }
@@ -170,12 +167,8 @@
//! user_id: &UserHandle64,
//! 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) = CredentialCreationOptions64::passkey(&rp_id, entity, creds)
+//! let (entity, creds) = select_user_info(user_id)?.ok_or(AppErr::NoAccount)?;
+//! let (server, client) = CredentialCreationOptions64::passkey(RP_ID, entity, creds)
//! .start_ceremony()
//! .unwrap_or_else(|_e| {
//! unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
@@ -199,22 +192,19 @@
//! # #[cfg(feature = "serde_relaxed")]
//! fn finish_cred_registration(
//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
-//! client_data: Vec<u8>,
+//! client_data: &[u8],
//! ) -> Result<(), AppErr> {
//! // `Registration::from_json_custom` is available iff `serde_relaxed` is enabled.
-//! let registration = Registration::from_json_custom(client_data.as_slice())?;
+//! let registration = Registration::from_json_custom(client_data)?;
//! insert_credential(
//! reg_ceremonies
//! // `Registration::challenge_relaxed` is available iff `serde_relaxed` is enabled.
//! .take(®istration.challenge_relaxed()?)
//! .ok_or(AppErr::MissingWebAuthnCeremony)?
//! .verify(
-//! &RpId::Domain(
-//! AsciiDomain::try_from(RP_ID.to_owned())
-//! .unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
-//! ),
+//! RP_ID,
//! ®istration,
-//! &RegistrationVerificationOptions::<&str, &str>::default(),
+//! REG_OPTS,
//! )?,
//! )
//! }
@@ -223,11 +213,7 @@
//! fn start_auth(
//! auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
//! ) -> 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 (server, client) = DiscoverableCredentialRequestOptions::passkey(&rp_id)
+//! let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID)
//! .start_ceremony()
//! .unwrap_or_else(|_e| {
//! unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
@@ -245,28 +231,25 @@
//! # #[cfg(feature = "serde_relaxed")]
//! fn finish_auth(
//! auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
-//! client_data: Vec<u8>,
+//! client_data: &[u8],
//! ) -> Result<(), AppErr> {
//! // `DiscoverableAuthentication64::from_json_custom` is available iff `serde_relaxed` is enabled.
//! let authentication =
-//! DiscoverableAuthentication64::from_json_custom(client_data.as_slice())?;
+//! DiscoverableAuthentication64::from_json_custom(client_data)?;
//! let mut cred = select_credential(
//! authentication.raw_id(),
//! authentication.response().user_handle(),
//! )?
-//! .ok_or_else(|| AppErr::NoCredential)?;
+//! .ok_or(AppErr::NoCredential)?;
//! if auth_ceremonies
//! // `DiscoverableAuthentication64::challenge_relaxed` is available iff `serde_relaxed` is enabled.
//! .take(&authentication.challenge_relaxed()?)
//! .ok_or(AppErr::MissingWebAuthnCeremony)?
//! .verify(
-//! &RpId::Domain(
-//! AsciiDomain::try_from(RP_ID.to_owned())
-//! .unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
-//! ),
+//! RP_ID,
//! &authentication,
//! &mut cred,
-//! &AuthenticationVerificationOptions::<&str, &str>::default(),
+//! AUTH_OPTS,
//! )?
//! {
//! update_credential(cred.id(), cred.dynamic_state())
@@ -292,11 +275,11 @@
//! /// # Errors
//! ///
//! /// Errors iff fetching the data errors.
-//! fn select_user_info<'a>(
-//! user_id: &'a UserHandle64,
+//! fn select_user_info(
+//! user_id: &UserHandle64,
//! ) -> Result<
//! Option<(
-//! PublicKeyCredentialUserEntity64<'static, 'static, 'a>,
+//! PublicKeyCredentialUserEntity64<'static, 'static, '_>,
//! Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
//! )>,
//! AppErr,
@@ -330,7 +313,7 @@
//! AuthenticatedCredential64<
//! 'cred,
//! 'user,
-//! CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>,
+//! CompressedPubKeyOwned,
//! >,
//! >,
//! AppErr,
diff --git a/src/request.rs b/src/request.rs
@@ -46,14 +46,14 @@ use url::Url as Uri;
/// # request::{
/// # auth::{AllowedCredentials, DiscoverableCredentialRequestOptions, NonDiscoverableCredentialRequestOptions},
/// # register::UserHandle64,
-/// # AsciiDomain, Credentials, PublicKeyCredentialDescriptor, RpId,
+/// # AsciiDomainStatic, Credentials, PublicKeyCredentialDescriptor, RpId,
/// # },
/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
/// # AggErr,
/// # };
+/// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
/// 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()?;
+/// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?;
/// assert!(
/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity)
/// );
@@ -66,7 +66,7 @@ use url::Url as Uri;
/// let creds = get_registered_credentials(&user_handle)?;
/// # #[cfg(feature = "custom")]
/// let (server_2, client_2) =
-/// NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?.start_ceremony()?;
+/// NonDiscoverableCredentialRequestOptions::second_factor(RP_ID, creds)?.start_ceremony()?;
/// # #[cfg(feature = "custom")]
/// assert!(
/// ceremonies_2.insert_remove_all_expired(server_2).map_or(false, convert::identity)
@@ -110,14 +110,14 @@ pub mod error;
/// # register::{
/// # CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64,
/// # },
-/// # AsciiDomain, PublicKeyCredentialDescriptor, RpId
+/// # AsciiDomainStatic, PublicKeyCredentialDescriptor, RpId
/// # },
/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
/// # AggErr,
/// # };
+/// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
/// # #[cfg(feature = "custom")]
/// let mut ceremonies = FixedCapHashSet::new(128);
-/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
/// # #[cfg(feature = "custom")]
/// let user_handle = get_user_handle();
/// # #[cfg(feature = "custom")]
@@ -125,7 +125,7 @@ pub mod error;
/// # #[cfg(feature = "custom")]
/// let creds = get_registered_credentials(&user_handle)?;
/// # #[cfg(feature = "custom")]
-/// let (server, client) = CredentialCreationOptions::passkey(&rp_id, user.clone(), creds)
+/// let (server, client) = CredentialCreationOptions::passkey(RP_ID, user.clone(), creds)
/// .start_ceremony()?;
/// # #[cfg(feature = "custom")]
/// assert!(
@@ -137,7 +137,7 @@ pub mod error;
/// let creds_2 = get_registered_credentials(&user_handle)?;
/// # #[cfg(feature = "custom")]
/// let (server_2, client_2) =
-/// CredentialCreationOptions::second_factor(&rp_id, user, creds_2).start_ceremony()?;
+/// CredentialCreationOptions::second_factor(RP_ID, user, creds_2).start_ceremony()?;
/// # #[cfg(feature = "custom")]
/// assert!(
/// ceremonies.insert_remove_all_expired(server_2).map_or(false, convert::identity)
@@ -230,6 +230,22 @@ impl Challenge {
pub const fn as_data(&self) -> u128 {
self.0
}
+ /// Returns the contained `u128` as a little-endian `array` consuming `self`.
+ #[inline]
+ #[must_use]
+ pub const fn into_array(self) -> [u8; 16] {
+ self.as_array()
+ }
+ /// Returns the contained `u128` as a little-endian `array`.
+ #[expect(
+ clippy::little_endian_bytes,
+ reason = "Challenge and SentChallenge need to be compatible, and we need to ensure the data is sent and received in the same order"
+ )]
+ #[inline]
+ #[must_use]
+ pub const fn as_array(&self) -> [u8; 16] {
+ self.0.to_le_bytes()
+ }
}
impl Default for Challenge {
/// Same as [`Self::new`].
@@ -250,6 +266,18 @@ impl From<&Challenge> for u128 {
value.0
}
}
+impl From<Challenge> for [u8; 16] {
+ #[inline]
+ fn from(value: Challenge) -> Self {
+ value.into_array()
+ }
+}
+impl From<&Challenge> for [u8; 16] {
+ #[inline]
+ fn from(value: &Challenge) -> Self {
+ value.as_array()
+ }
+}
/// A [domain](https://url.spec.whatwg.org/#concept-domain) in representation format consisting of only and any
/// ASCII.
///
@@ -257,6 +285,8 @@ impl From<&Challenge> for u128 {
/// label must have length inclusively between 1 and 63, and the total length of the domain must be at most 253
/// when a trailing `'.'` does not exist; otherwise the max length is 254. The root domain (i.e., `'.'`) is not
/// allowed.
+///
+/// Note if the domain is a `&'static str`, then use [`AsciiDomainStatic`] instead.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AsciiDomain(String);
impl AsciiDomain {
@@ -265,8 +295,11 @@ impl AsciiDomain {
/// # Examples
///
/// ```
- /// # use webauthn_rp::request::{error::AsciiDomainErr, AsciiDomain};
+ /// # use webauthn_rp::request::{AsciiDomain, error::AsciiDomainErr};
/// let mut dom = AsciiDomain::try_from("example.com.".to_owned())?;
+ /// assert_eq!(dom.as_ref(), "example.com.");
+ /// dom.remove_trailing_dot();
+ /// assert_eq!(dom.as_ref(), "example.com");
/// dom.remove_trailing_dot();
/// assert_eq!(dom.as_ref(), "example.com");
/// # Ok::<_, AsciiDomainErr>(())
@@ -278,7 +311,7 @@ impl AsciiDomain {
.0
.as_bytes()
.last()
- .unwrap_or_else(|| unreachable!("there is a bug in AsciiDomain::try_from"))
+ .unwrap_or_else(|| unreachable!("there is a bug in AsciiDomain::from_slice"))
== b'.'
{
_ = self.0.pop();
@@ -315,7 +348,7 @@ impl PartialEq<AsciiDomain> for &AsciiDomain {
**self == *other
}
}
-impl TryFrom<String> for AsciiDomain {
+impl TryFrom<Vec<u8>> for AsciiDomain {
type Error = AsciiDomainErr;
/// Verifies `value` is an ASCII domain in representation format converting any uppercase ASCII into
/// lowercase.
@@ -327,67 +360,71 @@ impl TryFrom<String> for AsciiDomain {
/// (e.g., [`AsciiDomain::remove_trailing_dot`]). Because this allows any ASCII, one may want to ensure `value`
/// is not an IP address.
///
+ /// # Errors
+ ///
+ /// Errors iff `value` is not a valid ASCII domain.
+ ///
/// # Examples
///
/// ```
/// # use webauthn_rp::request::{error::AsciiDomainErr, AsciiDomain};
/// // Root `'.'` is not removed if it exists.
- /// assert_ne!("example.com", AsciiDomain::try_from("example.com.".to_owned())?.as_ref());
+ /// assert_ne!("example.com", AsciiDomain::try_from(b"example.com.".to_vec())?.as_ref());
/// // Root domain (i.e., `'.'`) is not allowed.
- /// assert!(AsciiDomain::try_from(".".to_owned()).is_err());
+ /// assert!(AsciiDomain::try_from(vec![b'.']).is_err());
/// // Uppercase is transformed into lowercase.
- /// assert_eq!("example.com", AsciiDomain::try_from("ExAmPle.CoM".to_owned())?.as_ref());
+ /// assert_eq!("example.com", AsciiDomain::try_from(b"ExAmPle.CoM".to_vec())?.as_ref());
/// // The only ASCII character not allowed in a domain label is `'.'` as it is used exclusively to delimit
/// // labels.
- /// assert_eq!("\x00", AsciiDomain::try_from("\x00".to_owned())?.as_ref());
+ /// assert_eq!("\x00", AsciiDomain::try_from(b"\x00".to_vec())?.as_ref());
/// // Empty labels are not allowed.
- /// assert!(AsciiDomain::try_from("example..com".to_owned()).is_err());
+ /// assert!(AsciiDomain::try_from(b"example..com".to_vec()).is_err());
/// // Labels cannot have length greater than 63.
/// let mut long_label = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned();
/// assert_eq!(long_label.len(), 64);
- /// assert!(AsciiDomain::try_from(long_label.clone()).is_err());
+ /// assert!(AsciiDomain::try_from(long_label.clone().into_bytes()).is_err());
/// long_label.pop();
- /// assert_eq!(long_label, AsciiDomain::try_from(long_label.clone())?.as_ref());
+ /// assert_eq!(long_label, AsciiDomain::try_from(long_label.clone().into_bytes())?.as_ref());
/// // The maximum length of a domain is 254 if a trailing `'.'` exists; otherwise the max length is 253.
/// let mut long_domain = format!("{long_label}.{long_label}.{long_label}.{long_label}");
/// long_domain.pop();
/// long_domain.push('.');
/// assert_eq!(long_domain.len(), 255);
- /// assert!(AsciiDomain::try_from(long_domain.clone()).is_err());
+ /// assert!(AsciiDomain::try_from(long_domain.clone().into_bytes()).is_err());
/// long_domain.pop();
/// long_domain.pop();
/// long_domain.push('.');
/// assert_eq!(long_domain.len(), 254);
- /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone())?.as_ref());
+ /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone().into_bytes())?.as_ref());
/// long_domain.pop();
/// long_domain.push('a');
/// assert_eq!(long_domain.len(), 254);
- /// assert!(AsciiDomain::try_from(long_domain.clone()).is_err());
+ /// assert!(AsciiDomain::try_from(long_domain.clone().into_bytes()).is_err());
/// long_domain.pop();
/// assert_eq!(long_domain.len(), 253);
- /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone())?.as_ref());
+ /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone().into_bytes())?.as_ref());
/// // Only ASCII is allowed; thus if a domain needs to be Punycode-encoded, then it must be _before_ calling
/// // this function.
- /// assert!(AsciiDomain::try_from("λ.com".to_owned()).is_err());
- /// assert_eq!("xn--wxa.com", AsciiDomain::try_from("xn--wxa.com".to_owned())?.as_ref());
+ /// assert!(AsciiDomain::try_from("λ.com".to_owned().into_bytes()).is_err());
+ /// assert_eq!("xn--wxa.com", AsciiDomain::try_from(b"xn--wxa.com".to_vec())?.as_ref());
/// # Ok::<_, AsciiDomainErr>(())
/// ```
- #[expect(
- unsafe_code,
- reason = "need to transform uppercase ASCII into lowercase"
- )]
+ #[expect(unsafe_code, reason = "comment justifies correctness")]
#[expect(
clippy::arithmetic_side_effects,
- reason = "comment justifies its correctness"
+ reason = "comments justify correctness"
)]
#[inline]
- fn try_from(mut value: String) -> Result<Self, Self::Error> {
- value
- .as_bytes()
+ fn try_from(mut value: Vec<u8>) -> Result<Self, Self::Error> {
+ /// Value to add to an uppercase ASCII `u8` to get the lowercase version.
+ const DIFF: u8 = b'a' - b'A';
+ let bytes = value.as_slice();
+ bytes
+ .as_ref()
.last()
.ok_or(AsciiDomainErr::Empty)
.and_then(|b| {
- let len = value.len();
+ let len = bytes.len();
if *b == b'.' {
if len == 1 {
Err(AsciiDomainErr::RootDomain)
@@ -403,13 +440,9 @@ impl TryFrom<String> for AsciiDomain {
}
})
.and_then(|()| {
- // SAFETY:
- // The only possible mutation we perform is converting uppercase ASCII into lowercase which
- // is entirely safe since ASCII is a subset of UTF-8, and ASCII characters are always encoded
- // as a single UTF-8 code unit.
- let utf8 = unsafe { value.as_bytes_mut() };
- utf8.iter_mut()
- .try_fold(0u8, |label_len, byt| {
+ value
+ .iter_mut()
+ .try_fold(0u8, |mut label_len, byt| {
let b = *byt;
if b == b'.' {
if label_len == 0 {
@@ -419,18 +452,156 @@ impl TryFrom<String> for AsciiDomain {
}
} else if label_len == 63 {
Err(AsciiDomainErr::LabelLen)
- } else if b.is_ascii() {
- *byt = b.to_ascii_lowercase();
- // This won't overflow since `label_len < 63`.
- Ok(label_len + 1)
} else {
- Err(AsciiDomainErr::NotAscii)
+ // We know `label_len` is less than 63, thus this won't overflow.
+ label_len += 1;
+ match *byt {
+ // Non-uppercase ASCII is allowed and doesn't need to be converted.
+ ..b'A' | b'['..=0x7F => Ok(label_len),
+ // Uppercase ASCII is allowed but needs to be transformed into lowercase.
+ b'A'..=b'Z' => {
+ // Lowercase ASCII is a contiguous block starting from `b'a'` as is uppercase
+ // ASCII which starts from `b'A'` with uppercase ASCII coming before; thus we
+ // simply need to shift by a fixed amount.
+ *byt += DIFF;
+ Ok(label_len)
+ }
+ // Non-ASCII is disallowed.
+ 0x80.. => Err(AsciiDomainErr::NotAscii),
+ }
}
})
- .map(|_| Self(value))
+ .map(|_| {
+ // SAFETY:
+ // We just verified `value` only contains ASCII; thus this is safe.
+ let utf8 = unsafe { String::from_utf8_unchecked(value) };
+ Self(utf8)
+ })
})
}
}
+impl TryFrom<String> for AsciiDomain {
+ type Error = AsciiDomainErr;
+ /// Same as [`Self::try_from`] except `value` is a `String`.
+ #[inline]
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ Self::try_from(value.into_bytes())
+ }
+}
+/// Similar to [`AsciiDomain`] except the contained data is a `&'static str`.
+///
+/// Since [`Self::new`] and [`Option::unwrap`] are `const fn`s, one can define a global `const` or `static`
+/// variable that represents the RP ID.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct AsciiDomainStatic(&'static str);
+impl AsciiDomainStatic {
+ /// Returns the contained `str`.
+ #[inline]
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ self.0
+ }
+ /// Verifies `domain` is a valid lowercase ASCII domain returning `None` when not valid or when
+ /// uppercase ASCII exists.
+ ///
+ /// Read [`AsciiDomain`] for more information about what constitutes a valid domain.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{AsciiDomainStatic, RpId};
+ /// /// RP ID of our application.
+ /// const RP_IP: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+ /// ```
+ #[expect(
+ clippy::arithmetic_side_effects,
+ reason = "comment justifies correctness"
+ )]
+ #[expect(
+ clippy::else_if_without_else,
+ reason = "part of if branch and else branch are the same"
+ )]
+ #[inline]
+ #[must_use]
+ pub const fn new(domain: &'static str) -> Option<Self> {
+ let mut utf8 = domain.as_bytes();
+ if let Some(lst) = utf8.last() {
+ let len = utf8.len();
+ if *lst == b'.' {
+ if len == 1 || len > 254 {
+ return None;
+ }
+ } else if len > 253 {
+ return None;
+ }
+ let mut label_len = 0;
+ while let [first, ref rest @ ..] = *utf8 {
+ if first == b'.' {
+ if label_len == 0 {
+ return None;
+ }
+ label_len = 0;
+ } else if label_len == 63 {
+ return None;
+ } else {
+ match first {
+ // Any non-uppercase ASCII is allowed.
+ // We know `label_len` is less than 63, so this won't overflow.
+ ..b'A' | b'['..=0x7F => label_len += 1,
+ // Uppercase ASCII and non-ASCII are disallowed.
+ b'A'..=b'Z' | 0x80.. => return None,
+ }
+ }
+ utf8 = rest;
+ }
+ Some(Self(domain))
+ } else {
+ None
+ }
+ }
+}
+impl AsRef<str> for AsciiDomainStatic {
+ #[inline]
+ fn as_ref(&self) -> &str {
+ self.as_str()
+ }
+}
+impl Borrow<str> for AsciiDomainStatic {
+ #[inline]
+ fn borrow(&self) -> &str {
+ self.as_str()
+ }
+}
+impl From<AsciiDomainStatic> for &'static str {
+ #[inline]
+ fn from(value: AsciiDomainStatic) -> Self {
+ value.0
+ }
+}
+impl From<AsciiDomainStatic> for String {
+ #[inline]
+ fn from(value: AsciiDomainStatic) -> Self {
+ value.0.to_owned()
+ }
+}
+impl From<AsciiDomainStatic> for AsciiDomain {
+ #[inline]
+ fn from(value: AsciiDomainStatic) -> Self {
+ Self(value.0.to_owned())
+ }
+}
+impl PartialEq<&Self> for AsciiDomainStatic {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<AsciiDomainStatic> for &AsciiDomainStatic {
+ #[inline]
+ fn eq(&self, other: &AsciiDomainStatic) -> bool {
+ **self == *other
+ }
+}
/// The output of the [URL serializer](https://url.spec.whatwg.org/#concept-url-serializer).
///
/// The returned URL must consist of a [scheme](https://url.spec.whatwg.org/#concept-url-scheme) and
@@ -495,6 +666,11 @@ pub enum RpId {
/// and will likely be relaxed in a [future version](https://github.com/w3c/webauthn/issues/2206); thus
/// any ASCII domain is allowed.
Domain(AsciiDomain),
+ /// Similar to [`Self::Domain`] except the ASCII domain is static.
+ ///
+ /// Since [`AsciiDomainStatic::new`] is a `const fn`, one can define a `const` or `static` global variable
+ /// the contains the RP ID.
+ StaticDomain(AsciiDomainStatic),
/// A URL with only scheme and path.
Url(Url),
}
@@ -513,6 +689,7 @@ impl AsRef<str> for RpId {
fn as_ref(&self) -> &str {
match *self {
Self::Domain(ref dom) => dom.as_ref(),
+ Self::StaticDomain(dom) => dom.as_str(),
Self::Url(ref url) => url.as_ref(),
}
}
@@ -522,6 +699,7 @@ impl Borrow<str> for RpId {
fn borrow(&self) -> &str {
match *self {
Self::Domain(ref dom) => dom.borrow(),
+ Self::StaticDomain(dom) => dom.as_str(),
Self::Url(ref url) => url.borrow(),
}
}
@@ -531,6 +709,7 @@ impl From<RpId> for String {
fn from(value: RpId) -> Self {
match value {
RpId::Domain(dom) => dom.into(),
+ RpId::StaticDomain(dom) => dom.into(),
RpId::Url(url) => url.into(),
}
}
@@ -547,6 +726,24 @@ impl PartialEq<RpId> for &RpId {
**self == *other
}
}
+impl From<AsciiDomain> for RpId {
+ #[inline]
+ fn from(value: AsciiDomain) -> Self {
+ Self::Domain(value)
+ }
+}
+impl From<AsciiDomainStatic> for RpId {
+ #[inline]
+ fn from(value: AsciiDomainStatic) -> Self {
+ Self::StaticDomain(value)
+ }
+}
+impl From<Url> for RpId {
+ #[inline]
+ fn from(value: Url) -> Self {
+ Self::Url(value)
+ }
+}
/// A URI scheme. This can be used to make
/// [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) more convenient.
#[derive(Clone, Copy, Debug, Default)]
@@ -1402,6 +1599,9 @@ trait Ceremony<const USER_LEN: usize, const DISCOVERABLE: bool> {
// Steps 9 and 12 of the registration and authentication ceremonies
// respectively.
RpId::Url(ref url) => url == client_data_json.origin,
+ RpId::StaticDomain(dom) => {
+ DomainOrigin::new(dom.0) == client_data_json.origin
+ }
} {
Ok(())
} else {
@@ -1539,6 +1739,7 @@ impl PartialEq for PrfInput<'_, '_> {
}
#[cfg(test)]
mod tests {
+ use super::AsciiDomainStatic;
#[cfg(feature = "custom")]
use super::{
super::{
@@ -1558,7 +1759,7 @@ mod tests {
},
},
},
- AsciiDomain, Challenge, Credentials, ExtensionInfo, ExtensionReq, PrfInput,
+ Challenge, Credentials, ExtensionInfo, ExtensionReq, PrfInput,
PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement,
auth::{
AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions,
@@ -1603,12 +1804,48 @@ mod tests {
#[cfg(feature = "custom")]
const CBOR_TRUE: u8 = CBOR_SIMPLE | 21;
#[test]
+ fn ascii_domain_static() {
+ /// No trailing dot, max label length, max domain length.
+ const LONG: AsciiDomainStatic = AsciiDomainStatic::new(
+ "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww",
+ )
+ .unwrap();
+ /// Trailing dot, min label length, max domain length.
+ const LONG_TRAILING: AsciiDomainStatic = AsciiDomainStatic::new("w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.").unwrap();
+ /// Single character domain.
+ const SHORT: AsciiDomainStatic = AsciiDomainStatic::new("w").unwrap();
+ let long_label = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww";
+ assert_eq!(long_label.len(), 63);
+ let mut long = format!("{long_label}.{long_label}.{long_label}.{long_label}");
+ _ = long.pop();
+ _ = long.pop();
+ assert_eq!(LONG.0.len(), 253);
+ assert_eq!(LONG.0, long.as_str());
+ let trailing = "w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.";
+ assert_eq!(LONG_TRAILING.0.len(), 254);
+ assert_eq!(LONG_TRAILING.0, trailing);
+ assert_eq!(SHORT.0.len(), 1);
+ assert_eq!(SHORT.0, "w");
+ assert!(AsciiDomainStatic::new("www.Example.com").is_none());
+ assert!(AsciiDomainStatic::new("").is_none());
+ assert!(AsciiDomainStatic::new(".").is_none());
+ assert!(AsciiDomainStatic::new("www..c").is_none());
+ let too_long_label = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww";
+ assert_eq!(too_long_label.len(), 64);
+ assert!(AsciiDomainStatic::new(too_long_label).is_none());
+ let dom_254_no_trailing_dot = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww";
+ assert_eq!(dom_254_no_trailing_dot.len(), 254);
+ assert!(AsciiDomainStatic::new(dom_254_no_trailing_dot).is_none());
+ assert!(AsciiDomainStatic::new("λ.com").is_none());
+ }
+ #[cfg(feature = "custom")]
+ const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+ #[test]
#[cfg(feature = "custom")]
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 = CredentialCreationOptions::passkey(
- &rp_id,
+ RP_ID,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
id: &id,
@@ -1935,13 +2172,13 @@ mod tests {
let ver_key = sig_key.verifying_key();
let pub_key = ver_key.as_bytes();
attestation_object[107..139]
- .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice());
attestation_object[188..220].copy_from_slice(pub_key);
let sig = sig_key.sign(&attestation_object[107..]);
attestation_object[32..96].copy_from_slice(sig.to_bytes().as_slice());
attestation_object.truncate(261);
assert!(matches!(opts.start_ceremony()?.0.verify(
- &rp_id,
+ RP_ID,
&Registration {
response: AuthenticatorAttestation::new(
client_data_json,
@@ -1961,7 +2198,6 @@ mod tests {
#[test]
#[cfg(feature = "custom")]
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 {
credential: PublicKeyCredentialDescriptor {
@@ -1976,7 +2212,7 @@ mod tests {
}),
},
});
- let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?;
+ let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(RP_ID, creds)?;
opts.options().user_verification = UserVerificationRequirement::Required;
opts.options().challenge = Challenge(0);
opts.options().extensions = AuthExt { prf: None };
@@ -2129,14 +2365,14 @@ mod tests {
.as_slice(),
);
authenticator_data[..32]
- .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice());
authenticator_data
.extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
let ed_priv = SigningKey::from([0; 32]);
let sig = ed_priv.sign(authenticator_data.as_slice()).to_vec();
authenticator_data.truncate(132);
assert!(!opts.start_ceremony()?.0.verify(
- &rp_id,
+ RP_ID,
&NonDiscoverableAuthentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
response: NonDiscoverableAuthenticatorAssertion::with_user(
@@ -2176,10 +2412,9 @@ mod tests {
#[test]
#[cfg(feature = "custom")]
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 = CredentialCreationOptions::passkey(
- &rp_id,
+ RP_ID,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
id: &id,
@@ -2398,7 +2633,7 @@ mod tests {
.as_slice(),
);
attestation_object[30..62]
- .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice());
let p256_key = P256Key::from_bytes(
&[
137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115,
@@ -2414,7 +2649,7 @@ mod tests {
attestation_object[111..143].copy_from_slice(x);
attestation_object[146..].copy_from_slice(y);
assert!(matches!(opts.start_ceremony()?.0.verify(
- &rp_id,
+ RP_ID,
&Registration {
response: AuthenticatorAttestation::new(
client_data_json,
@@ -2434,8 +2669,7 @@ mod tests {
#[test]
#[cfg(feature = "custom")]
fn es256_auth() -> Result<(), AggErr> {
- let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(RP_ID);
opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
@@ -2489,7 +2723,7 @@ mod tests {
.as_slice(),
);
authenticator_data[..32]
- .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice());
authenticator_data
.extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
let p256_key = P256Key::from_bytes(
@@ -2504,7 +2738,7 @@ mod tests {
let pub_key = p256_key.verifying_key().to_encoded_point(true);
authenticator_data.truncate(37);
assert!(!opts.start_ceremony()?.0.verify(
- &rp_id,
+ RP_ID,
&DiscoverableAuthentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
response: DiscoverableAuthenticatorAssertion::new(
@@ -2545,10 +2779,9 @@ mod tests {
#[test]
#[cfg(feature = "custom")]
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 = CredentialCreationOptions::passkey(
- &rp_id,
+ RP_ID,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
id: &id,
@@ -2801,7 +3034,7 @@ mod tests {
.as_slice(),
);
attestation_object[30..62]
- .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice());
let p384_key = P384Key::from_bytes(
&[
158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232,
@@ -2818,7 +3051,7 @@ mod tests {
attestation_object[112..160].copy_from_slice(x);
attestation_object[163..].copy_from_slice(y);
assert!(matches!(opts.start_ceremony()?.0.verify(
- &rp_id,
+ RP_ID,
&Registration {
response: AuthenticatorAttestation::new(
client_data_json,
@@ -2838,8 +3071,7 @@ mod tests {
#[test]
#[cfg(feature = "custom")]
fn es384_auth() -> Result<(), AggErr> {
- let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(RP_ID);
opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
@@ -2893,7 +3125,7 @@ mod tests {
.as_slice(),
);
authenticator_data[..32]
- .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice());
authenticator_data
.extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
let p384_key = P384Key::from_bytes(
@@ -2909,7 +3141,7 @@ mod tests {
let pub_key = p384_key.verifying_key().to_encoded_point(true);
authenticator_data.truncate(37);
assert!(!opts.start_ceremony()?.0.verify(
- &rp_id,
+ RP_ID,
&DiscoverableAuthentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
response: DiscoverableAuthenticatorAssertion::new(
@@ -2950,10 +3182,9 @@ mod tests {
#[test]
#[cfg(feature = "custom")]
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 = CredentialCreationOptions::passkey(
- &rp_id,
+ RP_ID,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
id: &id,
@@ -3365,7 +3596,7 @@ mod tests {
.as_slice(),
);
attestation_object[31..63]
- .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice());
let n = [
111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107,
195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206,
@@ -3438,7 +3669,7 @@ mod tests {
let n = rsa_key.as_ref().n().to_bytes_be();
attestation_object[113..369].copy_from_slice(n.as_slice());
assert!(matches!(opts.start_ceremony()?.0.verify(
- &rp_id,
+ RP_ID,
&Registration {
response: AuthenticatorAttestation::new(
client_data_json,
@@ -3458,8 +3689,7 @@ mod tests {
#[test]
#[cfg(feature = "custom")]
fn rs256_auth() -> Result<(), AggErr> {
- let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(RP_ID);
opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
@@ -3513,7 +3743,7 @@ mod tests {
.as_slice(),
);
authenticator_data[..32]
- .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ .copy_from_slice(Sha256::digest(RP_ID.as_ref().as_bytes()).as_slice());
authenticator_data
.extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
let n = [
@@ -3588,7 +3818,7 @@ mod tests {
let sig = rsa_key.sign(authenticator_data.as_slice()).to_vec();
authenticator_data.truncate(37);
assert!(!opts.start_ceremony()?.0.verify(
- &rp_id,
+ RP_ID,
&DiscoverableAuthentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
response: DiscoverableAuthenticatorAssertion::new(
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -8,7 +8,7 @@ use super::{
DynamicState, StaticState,
},
},
- AsciiDomain, DomainOrigin, Url,
+ AsciiDomain, AsciiDomainStatic, DomainOrigin, Url,
register::{self, PublicKeyCredentialCreationOptions},
};
use super::{
@@ -365,6 +365,25 @@ impl<'rp_id, 'prf_first, 'prf_second>
)
})
}
+ /// Same as [`Self::start_ceremony`] except the raw challenge is returned instead of
+ /// [`DiscoverableAuthenticationClientState`].
+ ///
+ /// Note this is useful when one configures the authentication ceremony client-side and only needs the
+ /// server-generated challenge. It's of course essential that `self` is configured exactly the same as
+ /// how it is configured client-side. See
+ /// [`challengeURL`](https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-challengeURL) for more
+ /// information.
+ ///
+ /// # Errors
+ ///
+ /// Read [`Self::start_ceremony`].
+ #[inline]
+ pub fn start_ceremony_challenge_only(
+ self,
+ ) -> Result<(DiscoverableAuthenticationServerState, [u8; 16]), InvalidTimeout> {
+ self.start_ceremony()
+ .map(|(server, client)| (server, client.0.public_key.challenge.into_array()))
+ }
}
/// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions)
/// to send to the client when authenticating non-discoverable credententials.
@@ -987,9 +1006,10 @@ pub struct AuthenticationVerificationOptions<'origins, 'top_origins, O, T> {
/// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
///
/// When this is empty, the origin that will be used will be based on the [`RpId`] passed to
- /// [`DiscoverableAuthenticationServerState::verify`] or [`NonDiscoverableAuthenticationServerState::verify`]. If
- /// [`RpId::Domain`], then the [`DomainOrigin`] returned from passing [`AsciiDomain::as_ref`] to
- /// [`DomainOrigin::new`] will be used; otherwise the [`Url`] in [`RpId::Url`] will be used.
+ /// [`DiscoverableAuthenticationServerState::verify`] or [`NonDiscoverableAuthenticationServerState::verify`].
+ /// If [`RpId::Domain`] or [`RpId::StaticDomain`], then the [`DomainOrigin`] returned from passing
+ /// [`AsciiDomain::as_ref`] and [`AsciiDomainStatic::as_str`] to [`DomainOrigin::new`] respectively will be
+ /// used; otherwise the [`Url`] in [`RpId::Url`] will be used.
pub allowed_origins: &'origins [O],
/// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin)
/// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
@@ -1019,27 +1039,37 @@ pub struct AuthenticationVerificationOptions<'origins, 'top_origins, O, T> {
#[cfg(feature = "serde_relaxed")]
pub client_data_json_relaxed: bool,
}
-impl<O, T> Default for AuthenticationVerificationOptions<'_, '_, O, T> {
+impl<O, T> AuthenticationVerificationOptions<'_, '_, O, T> {
/// Returns `Self` such that [`Self::allowed_origins`] is empty, [`Self::allowed_top_origins`] is `None`,
/// [`Self::backup_requirement`] is `None`, [`Self::error_on_unsolicited_extensions`] is `true`,
/// [`Self::auth_attachment_enforcement`] is [`AuthenticatorAttachmentEnforcement::default`],
/// [`Self::update_uv`] is `false`, [`Self::sig_counter_enforcement`] is
/// [`SignatureCounterEnforcement::default`], and [`Self::client_data_json_relaxed`] is `true`.
+ ///
+ /// Note `O` and `T` should implement `PartialEq<Origin<'_>>` (e.g., `&str`).
#[inline]
- fn default() -> Self {
+ #[must_use]
+ pub const fn new() -> Self {
Self {
- allowed_origins: &[],
+ allowed_origins: [].as_slice(),
allowed_top_origins: None,
backup_requirement: None,
error_on_unsolicited_extensions: true,
- auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::default(),
+ auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::Ignore(false),
update_uv: false,
- sig_counter_enforcement: SignatureCounterEnforcement::default(),
+ sig_counter_enforcement: SignatureCounterEnforcement::Fail,
#[cfg(feature = "serde_relaxed")]
client_data_json_relaxed: true,
}
}
}
+impl<O, T> Default for AuthenticationVerificationOptions<'_, '_, O, T> {
+ /// Same as [`Self::new`].
+ #[inline]
+ fn default() -> Self {
+ Self::new()
+ }
+}
// This is essentially the `DiscoverableCredentialRequestOptions` used to create it; however to reduce
// memory usage, we remove all unnecessary data making an instance of this 48 bytes in size
// `x86_64-unknown-linux-gnu` platforms.
diff --git a/src/request/error.rs b/src/request/error.rs
@@ -7,7 +7,7 @@ use core::{
fmt::{self, Display, Formatter},
num::ParseIntError,
};
-/// Error returned by [`AsciiDomain::try_from`] when the `String` is not a valid ASCII domain.
+/// Error returned by [`AsciiDomain::try_from`] when the `Vec` is not a valid ASCII domain.
#[derive(Clone, Copy, Debug)]
pub enum AsciiDomainErr {
/// Variant returned when the domain is empty.
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -19,7 +19,9 @@ use super::{
};
#[cfg(doc)]
use crate::{
- request::{AsciiDomain, DomainOrigin, Url, auth::PublicKeyCredentialRequestOptions},
+ request::{
+ AsciiDomain, AsciiDomainStatic, DomainOrigin, Url, auth::PublicKeyCredentialRequestOptions,
+ },
response::{AuthTransports, AuthenticatorTransport, Backup, CollectedClientData},
};
use alloc::borrow::Cow;
@@ -1913,9 +1915,9 @@ pub struct RegistrationVerificationOptions<'origins, 'top_origins, O, T> {
/// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
///
/// When this is empty, the origin that will be used will be based on
- /// the [`RpId`] passed to [`RegistrationServerState::verify`]. If [`RpId::Domain`], then the [`DomainOrigin`] returned from
- /// passing [`AsciiDomain::as_ref`] to [`DomainOrigin::new`] will be used; otherwise the [`Url`] in
- /// [`RpId::Url`] will be used.
+ /// the [`RpId`] passed to [`RegistrationServerState::verify`]. If [`RpId::Domain`] or [`RpId::StaticDomain`],
+ /// then the [`DomainOrigin`] returned from passing [`AsciiDomain::as_ref`] and [`AsciiDomainStatic::as_str`]
+ /// to [`DomainOrigin::new`] respectively will be used; otherwise the [`Url`] in [`RpId::Url`] will be used.
pub allowed_origins: &'origins [O],
/// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin)
/// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
@@ -1935,11 +1937,29 @@ pub struct RegistrationVerificationOptions<'origins, 'top_origins, O, T> {
#[cfg(feature = "serde_relaxed")]
pub client_data_json_relaxed: bool,
}
-impl<O, T> Default for RegistrationVerificationOptions<'_, '_, O, T> {
+impl<O, T> RegistrationVerificationOptions<'_, '_, O, T> {
/// Returns `Self` such that [`Self::allowed_origins`] is empty, [`Self::allowed_top_origins`] is `None`,
/// [`Self::backup_requirement`] is [`BackupReq::None`], [`Self::error_on_unsolicited_extensions`] is `true`,
/// [`Self::require_authenticator_attachment`] is `false`, and [`Self::client_data_json_relaxed`] is
/// `true`.
+ ///
+ /// Note `O` and `T` should implement `PartialEq<Origin<'_>>` (e.g., `&str`).
+ #[inline]
+ #[must_use]
+ pub const fn new() -> Self {
+ Self {
+ allowed_origins: [].as_slice(),
+ allowed_top_origins: None,
+ backup_requirement: BackupReq::None,
+ error_on_unsolicited_extensions: true,
+ require_authenticator_attachment: false,
+ #[cfg(feature = "serde_relaxed")]
+ client_data_json_relaxed: true,
+ }
+ }
+}
+impl<O, T> Default for RegistrationVerificationOptions<'_, '_, O, T> {
+ /// Same as [`Self::new`].
#[inline]
fn default() -> Self {
Self {
@@ -2212,7 +2232,7 @@ impl PartialEq for ServerExtensionInfo {
}
// This is essentially the `PublicKeyCredentialCreationOptions` used to create it; however to reduce
// memory usage, we remove all unnecessary data making an instance of this 48 bytes in size on
-// `x86_64-unknown-linux-gnu` platforms.
+// `x86_64-unknown-linux-gnu` platforms when `USER_LEN` is `USER_HANDLE_MIN_LEN`.
/// State needed to be saved when beginning the registration ceremony.
///
/// Saves the necessary information associated with the [`CredentialCreationOptions`] used to create it
diff --git a/src/request/ser.rs b/src/request/ser.rs
@@ -48,8 +48,7 @@ impl Serialize for Challenge {
/// Serializes `self` to conform with
/// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-challenge).
///
- /// Specifically `self` is interpreted as a little-endian array of 16 bytes that is then transformed into a
- /// base64url-encoded string.
+ /// Specifically [`Self::as_array`] is transformed into a base64url-encoded string.
///
/// # Examples
///
@@ -59,17 +58,13 @@ impl Serialize for Challenge {
/// assert_eq!(serde_json::to_string(&Challenge::new())?.len(), 24);
/// # Ok::<_, serde_json::Error>(())
/// ```
- #[expect(
- clippy::little_endian_bytes,
- reason = "SentChallenge::deserialize and Challenge::serialize need to be consistent across architectures"
- )]
#[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.to_le_bytes().as_slice(),
+ self.as_array().as_slice(),
[0; Self::BASE64_LEN].as_mut_slice(),
))
}
diff --git a/src/response.rs b/src/response.rs
@@ -33,8 +33,8 @@ use ser_relaxed::SerdeJsonErr;
/// # use data_encoding::BASE64URL_NOPAD;
/// # use webauthn_rp::{
/// # hash::hash_set::FixedCapHashSet,
-/// # request::{auth::{error::InvalidTimeout, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, AsciiDomain, BackupReq, RpId},
-/// # response::{auth::{error::AuthCeremonyErr, DiscoverableAuthentication64}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKey, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId},
+/// # request::{auth::{error::InvalidTimeout, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, AsciiDomainStatic, BackupReq, RpId},
+/// # response::{auth::{error::AuthCeremonyErr, DiscoverableAuthentication64}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKeyOwned, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId},
/// # AuthenticatedCredential, CredentialErr
/// # };
/// # #[derive(Debug)]
@@ -79,9 +79,9 @@ use ser_relaxed::SerdeJsonErr;
/// # Self::AuthCeremony(value)
/// # }
/// # }
+/// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
/// 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()?;
+/// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?;
/// assert!(
/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity)
/// );
@@ -94,7 +94,7 @@ use ser_relaxed::SerdeJsonErr;
/// # #[cfg(all(feature = "custom", feature = "serde"))]
/// let mut cred = AuthenticatedCredential::new(authentication.raw_id(), &user_handle, static_state, dynamic_state)?;
/// # #[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())? {
+/// 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());
/// }
/// /// Send `DiscoverableAuthenticationClientState` and receive `DiscoverableAuthentication64` JSON from client.
@@ -121,9 +121,9 @@ use ser_relaxed::SerdeJsonErr;
/// # }).to_string()
/// }
/// /// 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)> {
+/// fn get_credential(id: CredentialId<&[u8]>, user_handle: &UserHandle64) -> Option<(StaticState<CompressedPubKeyOwned>, DynamicState)> {
/// // ⋮
-/// # 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 }))
+/// # Some((StaticState { credential_public_key: CompressedPubKeyOwned::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`.
@@ -156,7 +156,7 @@ pub mod error;
/// # use data_encoding::BASE64URL_NOPAD;
/// # use webauthn_rp::{
/// # hash::hash_set::FixedCapHashSet,
-/// # request::{register::{error::CreationOptionsErr, CredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomain, PublicKeyCredentialDescriptor, RpId},
+/// # request::{register::{error::CreationOptionsErr, CredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomainStatic, PublicKeyCredentialDescriptor, RpId},
/// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData},
/// # RegisteredCredential
/// # };
@@ -194,9 +194,9 @@ pub mod error;
/// # Self::RegCeremony(value)
/// # }
/// # }
+/// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
/// # #[cfg(feature = "custom")]
/// let mut ceremonies = FixedCapHashSet::new(128);
-/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
/// # #[cfg(feature = "custom")]
/// let user_handle = get_user_handle();
/// # #[cfg(feature = "custom")]
@@ -204,7 +204,7 @@ pub mod error;
/// # #[cfg(feature = "custom")]
/// let creds = get_registered_credentials(user_handle);
/// # #[cfg(feature = "custom")]
-/// let (server, client) = CredentialCreationOptions::passkey(&rp_id, user, creds).start_ceremony()?;
+/// let (server, client) = CredentialCreationOptions::passkey(RP_ID, user, creds).start_ceremony()?;
/// # #[cfg(feature = "custom")]
/// assert!(
/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity)
@@ -213,7 +213,7 @@ pub mod error;
/// let registration = serde_json::from_str::<Registration>(get_registration_json(client).as_str())?;
/// let ver_opts = RegistrationVerificationOptions::<&str, &str>::default();
/// # #[cfg(all(feature = "custom", feature = "serde_relaxed"))]
-/// insert_cred(ceremonies.take(®istration.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, ®istration, &ver_opts)?);
+/// 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")]
/// fn get_user_handle() -> UserHandle64 {
@@ -606,6 +606,34 @@ impl<T: Ord> Ord for CredentialId<T> {
/// Copy of [`Challenge`] sent back from the client.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SentChallenge(pub u128);
+impl SentChallenge {
+ /// Transforms `value` into a `SentChallenge` by interpreting `value` as a
+ /// little-endian `u128`.
+ #[expect(clippy::little_endian_bytes, reason = "Challenge and SentChallenge need to be compatible, and we need to ensure the data is sent and received in the same order")]
+ #[inline]
+ #[must_use]
+ pub const fn from_array(value: [u8; 16]) -> Self {
+ Self(u128::from_le_bytes(value))
+ }
+ /// Transforms `value` into a `SentChallenge`.
+ #[inline]
+ #[must_use]
+ pub const fn from_challenge(value: Challenge) -> Self {
+ Self(value.into_data())
+ }
+}
+impl From<Challenge> for SentChallenge {
+ #[inline]
+ fn from(value: Challenge) -> Self {
+ Self::from_challenge(value)
+ }
+}
+impl From<[u8; 16]> for SentChallenge {
+ #[inline]
+ fn from(value: [u8; 16]) -> Self {
+ Self::from_array(value)
+ }
+}
impl PartialEq<&Self> for SentChallenge {
#[inline]
fn eq(&self, other: &&Self) -> bool {
diff --git a/src/response/register.rs b/src/response/register.rs
@@ -1466,27 +1466,23 @@ impl<'a: 'b, 'b> TryFrom<(&'a [u8], u32)> for RsaPubKey<&'b [u8]> {
let zeros = fst.leading_zeros() as usize;
if zeros == 8 {
Err(RsaPubKeyErr::NLeading0)
- } else if let Some(bits) = n.len().checked_mul(8) {
- if (MIN_RSA_N_BITS..=MAX_RSA_N_BITS)
- // `bits` is at least 8 since `n.len()` is at least 1; thus underflow cannot occur.
- .contains(&(bits - zeros))
+ // `bits` is at least 8 since `n.len()` is at least 1; thus underflow cannot occur.
+ } else if let Some(bits) = n.len().checked_mul(8)
+ && (MIN_RSA_N_BITS..=MAX_RSA_N_BITS).contains(&(bits - zeros))
+ {
+ // We know `n` is not empty, so this won't `panic`.
+ if n.last()
+ .unwrap_or_else(|| unreachable!("there is a bug in RsaPubKey::try_from"))
+ & 1
+ == 0
{
- // We know `n` is not empty, so this won't `panic`.
- if n.last()
- .unwrap_or_else(|| unreachable!("there is a bug in RsaPubKey::try_from"))
- & 1
- == 0
- {
- Err(RsaPubKeyErr::NEven)
- } else if e < MIN_RSA_E {
- Err(RsaPubKeyErr::ESize)
- } else if e & 1 == 0 {
- Err(RsaPubKeyErr::EEven)
- } else {
- Ok(Self(n, e))
- }
+ Err(RsaPubKeyErr::NEven)
+ } else if e < MIN_RSA_E {
+ Err(RsaPubKeyErr::ESize)
+ } else if e & 1 == 0 {
+ Err(RsaPubKeyErr::EEven)
} else {
- Err(RsaPubKeyErr::NSize)
+ Ok(Self(n, e))
}
} else {
Err(RsaPubKeyErr::NSize)
@@ -1994,6 +1990,15 @@ pub enum CompressedPubKey<T, T2, T3, T4> {
/// An alleged RSA public key.
Rsa(RsaPubKey<T4>),
}
+/// `CompressedPubKey` that owns the key data.
+pub type CompressedPubKeyOwned = CompressedPubKey<
+ [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
+ [u8; <NistP256 as Curve>::FieldBytesSize::INT],
+ [u8; <NistP384 as Curve>::FieldBytesSize::INT],
+ Vec<u8>,
+>;
+/// `CompressedPubKey` that borrows the key data.
+pub type CompressedPubKeyBorrowed<'a> = CompressedPubKey<&'a [u8], &'a [u8], &'a [u8], &'a [u8]>;
impl CompressedPubKey<&[u8], &[u8], &[u8], &[u8]> {
/// Validates `self` is in fact a valid public key.
///
@@ -3410,15 +3415,8 @@ impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8
}
/// `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>,
- >,
->;
+/// `StaticState` with a compressed [`Self::credential_public_key`] that owns the key data.
+pub type StaticStateCompressed = StaticState<CompressedPubKeyOwned>;
impl StaticStateUncompressed<'_> {
/// Transforms `self` into `StaticState` that contains the compressed version of the public key.
#[inline]
diff --git a/src/response/ser.rs b/src/response/ser.rs
@@ -428,10 +428,6 @@ impl<'de> Deserialize<'de> for SentChallenge {
clippy::panic_in_result_fn,
reason = "we want to crash when there is a bug"
)]
- #[expect(
- clippy::little_endian_bytes,
- reason = "SentChallenge::deserialize and Challenge::serialize need to be consistent across architectures"
- )]
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: Error,
@@ -443,7 +439,7 @@ impl<'de> Deserialize<'de> for SentChallenge {
.map_err(|err| E::custom(err.error))
.map(|len| {
assert_eq!(len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut");
- SentChallenge(u128::from_le_bytes(data))
+ SentChallenge::from_array(data)
})
} else {
Err(E::invalid_value(