commit bb3d74891d8acff41a64501a29fd9e94ebf8a14b
parent 2acae1c69f2fddd68a09e6bdf5b7a9b349f09c39
Author: Zack Newman <zack@philomathiclife.com>
Date: Wed, 2 Apr 2025 17:13:08 -0600
make userhandle array-based, split authentication into discoverable and not
Diffstat:
22 files changed, 2344 insertions(+), 1531 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -21,7 +21,7 @@ data-encoding = { version = "2.8.0", default-features = false }
ed25519-dalek = { version = "2.1.1", default-features = false, features = ["fast"] }
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.11", default-features = false }
+precis-profiles = { version = "0.1.12", default-features = false }
rand = { version = "0.9.0", 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 }
diff --git a/README.md b/README.md
@@ -14,6 +14,328 @@ to adapt to native applications as well. It achieves this by not assuming how da
having said that, there are pre-defined serialization formats for "common" deployments which can be used when
[`serde`](#serde) is enabled.
+## `webauthn_rp` in action
+
+```rust
+use webauthn_rp::{
+ AuthenticatedCredential, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
+ DiscoverableCredentialRequestOptions, PublicKeyCredentialCreationOptions, RegisteredCredential,
+ Registration, RegistrationServerState,
+ request::{
+ AsciiDomain, PublicKeyCredentialDescriptor, RpId,
+ auth::AuthenticationVerificationOptions,
+ register::{
+ Nickname, PublicKeyCredentialUserEntity, RegistrationVerificationOptions,
+ USER_HANDLE_MAX_LEN, UserHandle64, Username,
+ },
+ },
+ response::{
+ CredentialId,
+ auth::error::AuthCeremonyErr,
+ 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.
+const RP_ID: &str = "example.com";
+/// Error we return in our application when a function fails.
+enum AppErr {
+ /// WebAuthn registration ceremony failed.
+ RegCeremony(RegCeremonyErr),
+ /// WebAuthn authentication ceremony failed.
+ AuthCeremony(AuthCeremonyErr),
+ /// Unable to insert a WebAuthn ceremony.
+ WebAuthnCeremonyCreation,
+ /// WebAuthn ceremony does not exist; thus the ceremony could not be completed.
+ MissingWebAuthnCeremony,
+ /// General error related to JSON deserialization.
+ Json(JsonErr),
+ /// No account exists associated with a particular `UserHandle64`.
+ NoAccount,
+ /// No credential exists associated with a particular `CredentialId`.
+ NoCredential,
+ /// `CredentialId` exists but the associated `UserHandle64` does not match.
+ CredentialUserIdMismatch,
+}
+impl From<JsonErr> for AppErr {
+ fn from(value: JsonErr) -> Self {
+ Self::Json(value)
+ }
+}
+impl From<RegCeremonyErr> for AppErr {
+ fn from(value: RegCeremonyErr) -> Self {
+ Self::RegCeremony(value)
+ }
+}
+impl From<AuthCeremonyErr> for AppErr {
+ fn from(value: AuthCeremonyErr) -> Self {
+ Self::AuthCeremony(value)
+ }
+}
+/// First-time account creation.
+///
+/// This gets sent from the user after an account is created on their side. The registration ceremony
+/// still has to be successfully completed for the account to be created server side. In the event of an error,
+/// the user should delete the created passkey since it won't be usable.
+struct AccountReg<'a, 'b> {
+ registration: Registration,
+ user_name: Username<'a>,
+ user_display_name: Nickname<'b>,
+}
+impl<'de: 'a + 'b, 'a, 'b> Deserialize<'de> for AccountReg<'a, 'b> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ // ⋮
+ }
+}
+/// Starts account creation.
+///
+/// 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.
+fn start_account_creation(
+ reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+) -> 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) =
+ PublicKeyCredentialCreationOptions::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")
+ });
+ if matches!(
+ reg_ceremonies.insert_or_replace_all_expired(server),
+ InsertResult::Success
+ ) {
+ Ok(serde_json::to_vec(&client)
+ .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
+ } else {
+ Err(AppErr::WebAuthnCeremonyCreation)
+ }
+}
+/// Finishes account creation.
+///
+/// Pending a successful registration ceremony, a new account associated with the randomly generated
+/// `UserHandle64` will be created with a corresponding passkey entry. This passkey will be used to
+/// log into the application.
+///
+/// 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>>,
+ client_data: Vec<u8>,
+) -> Result<(), AppErr> {
+ let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data.as_slice())?;
+ insert_account(
+ &account,
+ reg_ceremonies
+ .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")),
+ ),
+ &account.registration,
+ &RegistrationVerificationOptions::<&str, &str>::default(),
+ )?,
+ )
+}
+/// Starts passkey registration.
+///
+/// This is used for _existing_ accounts where the user is already logged in and wants to register another
+/// 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.
+fn start_cred_registration(
+ user_id: &UserHandle64,
+ reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+) -> 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)
+ .start_ceremony()
+ .unwrap_or_else(|_e| {
+ unreachable!("we don't manually mutate the options; thus this won't error")
+ });
+ if matches!(
+ reg_ceremonies.insert_or_replace_all_expired(server),
+ InsertResult::Success
+ ) {
+ Ok(serde_json::to_vec(&client)
+ .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
+ } else {
+ Err(AppErr::WebAuthnCeremonyCreation)
+ }
+}
+/// Finishes passkey registration.
+///
+/// Pending a successful registration ceremony, a new credential associated with the `UserHandle64`
+/// will be created. This passkey can then be used to log into the application just like any other registered
+/// passkey.
+///
+/// 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>>,
+ client_data: Vec<u8>,
+) -> Result<(), AppErr> {
+ let registration = Registration::from_json_custom(client_data.as_slice())?;
+ insert_credential(
+ reg_ceremonies
+ .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")),
+ ),
+ ®istration,
+ &RegistrationVerificationOptions::<&str, &str>::default(),
+ )?,
+ )
+}
+/// Starts the passkey authentication ceremony.
+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)
+ .start_ceremony()
+ .unwrap_or_else(|_e| {
+ unreachable!("we don't manually mutate the options; thus this won't error")
+ });
+ if matches!(
+ auth_ceremonies.insert_or_replace_all_expired(server),
+ InsertResult::Success
+ ) {
+ Ok(serde_json::to_vec(&client).unwrap_or_else(|_e| {
+ unreachable!("bug in DiscoverableAuthenticationClientState::serialize")
+ }))
+ } else {
+ Err(AppErr::WebAuthnCeremonyCreation)
+ }
+}
+/// Finishes the passkey authentication ceremony.
+fn finish_auth(
+ auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
+ client_data: Vec<u8>,
+) -> Result<(), AppErr> {
+ let authentication =
+ DiscoverableAuthentication64::from_json_custom(client_data.as_slice())?;
+ let mut cred = select_credential(
+ authentication.raw_id(),
+ authentication.response().user_handle(),
+ )?
+ .ok_or_else(|| AppErr::NoCredential)?;
+ if auth_ceremonies
+ .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")),
+ ),
+ &authentication,
+ &mut cred,
+ &AuthenticationVerificationOptions::<&str, &str>::default(),
+ )?
+ {
+ update_credential(cred.id(), cred.dynamic_state())
+ } else {
+ Ok(())
+ }
+}
+/// Writes `account` and `cred` to storage.
+///
+/// # Errors
+///
+/// Errors iff writing `account` or `cred` errors, there already exists a credential using the same
+/// `CredentialId`, or there already exists an account using the same `UserHandle64`.
+fn insert_account(
+ account: &AccountReg<'_, '_>,
+ cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
+) -> Result<(), AppErr> {
+ // ⋮
+}
+/// Fetches the user info and registered credentials associated with `user_id`.
+///
+/// # Errors
+///
+/// Errors iff fetching the data errors.
+fn select_user_info<'a>(
+ user_id: &'a UserHandle64,
+) -> Result<
+ Option<(
+ PublicKeyCredentialUserEntity<'static, 'static, 'a, USER_HANDLE_MAX_LEN>,
+ Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ )>,
+ AppErr,
+> {
+ // ⋮
+}
+/// Writes `cred` to storage.
+///
+/// # Errors
+///
+/// Errors iff writing `cred` errors or there already exists a credential using the same `CredentialId`.
+fn insert_credential(
+ cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
+) -> Result<(), AppErr> {
+ // ⋮
+}
+/// Fetches the `AuthenticatedCredential` associated with `cred_id` ensuring `user_id` matches the
+/// `UserHandle64` associated with the account.
+///
+/// # Errors
+///
+/// Errors iff fetching the data errors or the `user_id` does not match the stored `UserHandle64`.
+fn select_credential<'cred, 'user>(
+ cred_id: CredentialId<&'cred [u8]>,
+ user_id: &'user UserHandle64,
+) -> Result<
+ Option<
+ AuthenticatedCredential<
+ 'cred,
+ 'user,
+ USER_HANDLE_MAX_LEN,
+ CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>,
+ >,
+ >,
+ AppErr,
+> {
+ // ⋮
+}
+/// Overwrites the current `DynamicState` associated with `cred_id` with `dynamic_state`.
+///
+/// # Errors
+///
+/// Errors iff writing errors or `cred_id` does not exist.
+fn update_credential(
+ cred_id: CredentialId<&[u8]>,
+ dynamic_state: DynamicState,
+) -> Result<(), AppErr> {
+ // ⋮
+}
+```
+
## Cargo "features"
[`custom`](#custom) or both [`bin`](#bin) and [`serde`](#serde) must be enabled; otherwise a `compile_error`
diff --git a/src/lib.rs b/src/lib.rs
@@ -14,6 +14,346 @@
//! having said that, there are pre-defined serialization formats for "common" deployments which can be used when
//! [`serde`](#serde) is enabled.
//!
+//! ## `webauthn_rp` in action
+//!
+//! ```no_run
+//! use webauthn_rp::{
+//! AuthenticatedCredential, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
+//! DiscoverableCredentialRequestOptions, PublicKeyCredentialCreationOptions, RegisteredCredential,
+//! Registration, RegistrationServerState,
+//! request::{
+//! AsciiDomain, PublicKeyCredentialDescriptor, RpId,
+//! auth::AuthenticationVerificationOptions,
+//! register::{
+//! Nickname, PublicKeyCredentialUserEntity, RegistrationVerificationOptions,
+//! USER_HANDLE_MAX_LEN, UserHandle64, Username,
+//! },
+//! },
+//! response::{
+//! CredentialId,
+//! auth::error::AuthCeremonyErr,
+//! 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")]
+//! use serde_json::Error as JsonErr;
+//! /// The RP ID our application uses.
+//! const RP_ID: &str = "example.com";
+//! /// Error we return in our application when a function fails.
+//! enum AppErr {
+//! /// WebAuthn registration ceremony failed.
+//! RegCeremony(RegCeremonyErr),
+//! /// WebAuthn authentication ceremony failed.
+//! AuthCeremony(AuthCeremonyErr),
+//! /// Unable to insert a WebAuthn ceremony.
+//! WebAuthnCeremonyCreation,
+//! /// WebAuthn ceremony does not exist; thus the ceremony could not be completed.
+//! MissingWebAuthnCeremony,
+//! /// General error related to JSON deserialization.
+//! # #[cfg(feature = "serde_relaxed")]
+//! Json(JsonErr),
+//! /// No account exists associated with a particular `UserHandle64`.
+//! NoAccount,
+//! /// No credential exists associated with a particular `CredentialId`.
+//! NoCredential,
+//! /// `CredentialId` exists but the associated `UserHandle64` does not match.
+//! CredentialUserIdMismatch,
+//! }
+//! # #[cfg(feature = "serde_relaxed")]
+//! impl From<JsonErr> for AppErr {
+//! fn from(value: JsonErr) -> Self {
+//! Self::Json(value)
+//! }
+//! }
+//! impl From<RegCeremonyErr> for AppErr {
+//! fn from(value: RegCeremonyErr) -> Self {
+//! Self::RegCeremony(value)
+//! }
+//! }
+//! impl From<AuthCeremonyErr> for AppErr {
+//! fn from(value: AuthCeremonyErr) -> Self {
+//! Self::AuthCeremony(value)
+//! }
+//! }
+//! /// First-time account creation.
+//! ///
+//! /// This gets sent from the user after an account is created on their side. The registration ceremony
+//! /// still has to be successfully completed for the account to be created server side. In the event of an error,
+//! /// the user should delete the created passkey since it won't be usable.
+//! struct AccountReg<'a, 'b> {
+//! registration: Registration,
+//! user_name: Username<'a>,
+//! user_display_name: Nickname<'b>,
+//! }
+//! # #[cfg(feature = "serde")]
+//! impl<'de: 'a + 'b, 'a, 'b> Deserialize<'de> for AccountReg<'a, 'b> {
+//! fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+//! where
+//! D: Deserializer<'de>,
+//! {
+//! // ⋮
+//! # panic!("");
+//! }
+//! }
+//! /// Starts account creation.
+//! ///
+//! /// 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")))]
+//! fn start_account_creation(
+//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+//! ) -> 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) =
+//! PublicKeyCredentialCreationOptions::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")
+//! });
+//! if matches!(
+//! reg_ceremonies.insert_or_replace_all_expired(server),
+//! InsertResult::Success
+//! ) {
+//! Ok(serde_json::to_vec(&client)
+//! .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
+//! } else {
+//! Err(AppErr::WebAuthnCeremonyCreation)
+//! }
+//! }
+//! /// Finishes account creation.
+//! ///
+//! /// Pending a successful registration ceremony, a new account associated with the randomly generated
+//! /// `UserHandle64` will be created with a corresponding passkey entry. This passkey will be used to
+//! /// log into the application.
+//! ///
+//! /// 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")))]
+//! fn finish_account_creation(
+//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+//! client_data: Vec<u8>,
+//! ) -> Result<(), AppErr> {
+//! let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data.as_slice())?;
+//! insert_account(
+//! &account,
+//! reg_ceremonies
+//! .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")),
+//! ),
+//! &account.registration,
+//! &RegistrationVerificationOptions::<&str, &str>::default(),
+//! )?,
+//! )
+//! }
+//! /// Starts passkey registration.
+//! ///
+//! /// This is used for _existing_ accounts where the user is already logged in and wants to register another
+//! /// 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")))]
+//! fn start_cred_registration(
+//! user_id: &UserHandle64,
+//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+//! ) -> 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)
+//! .start_ceremony()
+//! .unwrap_or_else(|_e| {
+//! unreachable!("we don't manually mutate the options; thus this won't error")
+//! });
+//! if matches!(
+//! reg_ceremonies.insert_or_replace_all_expired(server),
+//! InsertResult::Success
+//! ) {
+//! Ok(serde_json::to_vec(&client)
+//! .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize")))
+//! } else {
+//! Err(AppErr::WebAuthnCeremonyCreation)
+//! }
+//! }
+//! /// Finishes passkey registration.
+//! ///
+//! /// Pending a successful registration ceremony, a new credential associated with the `UserHandle64`
+//! /// will be created. This passkey can then be used to log into the application just like any other registered
+//! /// passkey.
+//! ///
+//! /// 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")))]
+//! fn finish_cred_registration(
+//! reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState<USER_HANDLE_MAX_LEN>>,
+//! client_data: Vec<u8>,
+//! ) -> Result<(), AppErr> {
+//! let registration = Registration::from_json_custom(client_data.as_slice())?;
+//! insert_credential(
+//! reg_ceremonies
+//! .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")),
+//! ),
+//! ®istration,
+//! &RegistrationVerificationOptions::<&str, &str>::default(),
+//! )?,
+//! )
+//! }
+//! /// Starts the passkey authentication ceremony.
+//! # #[cfg(all(feature = "serde_relaxed", not(feature = "serializable_server_state")))]
+//! 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)
+//! .start_ceremony()
+//! .unwrap_or_else(|_e| {
+//! unreachable!("we don't manually mutate the options; thus this won't error")
+//! });
+//! if matches!(
+//! auth_ceremonies.insert_or_replace_all_expired(server),
+//! InsertResult::Success
+//! ) {
+//! Ok(serde_json::to_vec(&client).unwrap_or_else(|_e| {
+//! unreachable!("bug in DiscoverableAuthenticationClientState::serialize")
+//! }))
+//! } else {
+//! Err(AppErr::WebAuthnCeremonyCreation)
+//! }
+//! }
+//! /// Finishes the passkey authentication ceremony.
+//! # #[cfg(all(feature = "serde_relaxed", not(feature = "serializable_server_state")))]
+//! fn finish_auth(
+//! auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
+//! client_data: Vec<u8>,
+//! ) -> Result<(), AppErr> {
+//! let authentication =
+//! DiscoverableAuthentication64::from_json_custom(client_data.as_slice())?;
+//! let mut cred = select_credential(
+//! authentication.raw_id(),
+//! authentication.response().user_handle(),
+//! )?
+//! .ok_or_else(|| AppErr::NoCredential)?;
+//! if auth_ceremonies
+//! .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")),
+//! ),
+//! &authentication,
+//! &mut cred,
+//! &AuthenticationVerificationOptions::<&str, &str>::default(),
+//! )?
+//! {
+//! update_credential(cred.id(), cred.dynamic_state())
+//! } else {
+//! Ok(())
+//! }
+//! }
+//! /// Writes `account` and `cred` to storage.
+//! ///
+//! /// # Errors
+//! ///
+//! /// Errors iff writing `account` or `cred` errors, there already exists a credential using the same
+//! /// `CredentialId`, or there already exists an account using the same `UserHandle64`.
+//! fn insert_account(
+//! account: &AccountReg<'_, '_>,
+//! cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
+//! ) -> Result<(), AppErr> {
+//! // ⋮
+//! # Ok(())
+//! }
+//! /// Fetches the user info and registered credentials associated with `user_id`.
+//! ///
+//! /// # Errors
+//! ///
+//! /// Errors iff fetching the data errors.
+//! fn select_user_info<'a>(
+//! user_id: &'a UserHandle64,
+//! ) -> Result<
+//! Option<(
+//! PublicKeyCredentialUserEntity<'static, 'static, 'a, USER_HANDLE_MAX_LEN>,
+//! Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+//! )>,
+//! AppErr,
+//! > {
+//! // ⋮
+//! # Ok(None)
+//! }
+//! /// Writes `cred` to storage.
+//! ///
+//! /// # Errors
+//! ///
+//! /// Errors iff writing `cred` errors or there already exists a credential using the same `CredentialId`.
+//! fn insert_credential(
+//! cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>,
+//! ) -> Result<(), AppErr> {
+//! // ⋮
+//! # Ok(())
+//! }
+//! /// Fetches the `AuthenticatedCredential` associated with `cred_id` ensuring `user_id` matches the
+//! /// `UserHandle64` associated with the account.
+//! ///
+//! /// # Errors
+//! ///
+//! /// Errors iff fetching the data errors or the `user_id` does not match the stored `UserHandle64`.
+//! fn select_credential<'cred, 'user>(
+//! cred_id: CredentialId<&'cred [u8]>,
+//! user_id: &'user UserHandle64,
+//! ) -> Result<
+//! Option<
+//! AuthenticatedCredential<
+//! 'cred,
+//! 'user,
+//! USER_HANDLE_MAX_LEN,
+//! CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>,
+//! >,
+//! >,
+//! AppErr,
+//! > {
+//! // ⋮
+//! # Ok(None)
+//! }
+//! /// Overwrites the current `DynamicState` associated with `cred_id` with `dynamic_state`.
+//! ///
+//! /// # Errors
+//! ///
+//! /// Errors iff writing errors or `cred_id` does not exist.
+//! fn update_credential(
+//! cred_id: CredentialId<&[u8]>,
+//! dynamic_state: DynamicState,
+//! ) -> Result<(), AppErr> {
+//! // ⋮
+//! # Ok(())
+//! }
+//! ```
+//!
//! ## Cargo "features"
//!
//! [`custom`](#custom) or both [`bin`](#bin) and [`serde`](#serde) must be enabled; otherwise a [`compile_error`]
@@ -79,13 +419,14 @@
//! ### `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 [`TimedCeremony::sent_challenge`],
+//! 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
@@ -109,13 +450,14 @@
//! 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
//!
@@ -219,7 +561,9 @@ compile_error!("'custom' must be enabled or both 'bin' and 'serde' must be enabl
#[cfg(feature = "serializable_server_state")]
use crate::request::{
auth::ser_server_state::{
- DecodeAuthenticationServerStateErr, EncodeAuthenticationServerStateErr,
+ DecodeDiscoverableAuthenticationServerStateErr,
+ DecodeNonDiscoverableAuthenticationServerStateErr,
+ EncodeNonDiscoverableAuthenticationServerStateErr,
},
register::ser_server_state::DecodeRegistrationServerStateErr,
};
@@ -239,13 +583,13 @@ use crate::{
use crate::{
request::{
AsciiDomain, DomainOrigin, FixedCapHashSet, Port, PublicKeyCredentialDescriptor, RpId,
- Scheme, ServerState, Url,
- auth::{AllowedCredential, AllowedCredentials},
- register::{CoseAlgorithmIdentifier, Nickname, Username},
+ Scheme, TimedCeremony, Url,
+ auth::{AllowedCredential, AllowedCredentials, PublicKeyCredentialRequestOptions},
+ register::{CoseAlgorithmIdentifier, Nickname, PublicKeyCredentialUserEntity, Username},
},
response::{
CollectedClientData, Flag,
- auth::{self, AuthenticatorAssertion},
+ auth::{self, Authentication, DiscoverableAuthenticatorAssertion},
register::{
self, Aaguid, Attestation, AttestationObject, AttestedCredentialData,
AuthenticatorExtensionOutput, ClientExtensionsOutputs, CompressedPubKey,
@@ -259,7 +603,7 @@ use crate::{
error::{AsciiDomainErr, DomainOriginParseErr, PortParseErr, SchemeParseErr, UrlErr},
register::{
ResidentKeyRequirement, UserHandle,
- error::{CreationOptionsErr, NicknameErr, UserHandleErr, UsernameErr},
+ error::{CreationOptionsErr, NicknameErr, UsernameErr},
},
},
response::{
@@ -346,8 +690,7 @@ pub mod bin;
///
/// Two other reasons one may prefer to construct client-side credentials is richer support for extensions (e.g.,
/// [`largeBlobKey`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-largeBlobKey-extension)
-/// for CTAP 2.2 authenticators) and the ability to use both discoverable and nondiscoverable requests (i.e.,
-/// [`PublicKeyCredentialRequestOptions::allow_credentials`] is empty and not empty respectively). The former is not
+/// for CTAP 2.2 authenticators) and the ability to use both discoverable and nondiscoverable requests. The former is not
/// relevant for this library—at least currently—since the only extensions supported are applicable for both
/// client-side and server-side credentials. The latter can be important especially if an RP wants the ability to
/// seamlessly transition from a username and password scheme to a userless and passwordless one in the future.
@@ -365,7 +708,9 @@ pub mod response;
pub use crate::{
request::{
auth::{
- AuthenticationClientState, AuthenticationServerState, PublicKeyCredentialRequestOptions,
+ DiscoverableAuthenticationClientState, DiscoverableAuthenticationServerState,
+ DiscoverableCredentialRequestOptions, NonDiscoverableAuthenticationClientState,
+ NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
},
register::{
PublicKeyCredentialCreationOptions, RegistrationClientState, RegistrationServerState,
@@ -373,7 +718,9 @@ pub use crate::{
},
response::{
auth::{
- Authentication, PasskeyAuthentication, PasskeyAuthentication16, PasskeyAuthentication64,
+ DiscoverableAuthentication, DiscoverableAuthentication16, DiscoverableAuthentication64,
+ NonDiscoverableAuthentication, NonDiscoverableAuthentication16,
+ NonDiscoverableAuthentication64,
},
register::Registration,
},
@@ -450,28 +797,29 @@ fn verify_static_and_dynamic_state<T>(
/// * [`CredentialId`]
/// * MUST be globally unique, and it will likely be easier to enforce such uniqueness when it's separate.
/// * Fetching the [`AuthenticatedCredential`] by [`Authentication::raw_id`] when completing the
-/// authentication ceremony via [`AuthenticationServerState::verify`] will likely be easier than alternatives.
+/// authentication ceremony via [`DiscoverableAuthenticationServerState::verify`] or
+/// [`NonDiscoverableAuthenticationServerState::verify`] will likely be easier than alternatives.
/// * [`AuthTransports`]
/// * Fetching [`CredentialId`]s and associated `AuthTransports` by [`UserHandle`] will likely make credential
/// registration easier since one should set [`PublicKeyCredentialCreationOptions::exclude_credentials`] to
/// the [`PublicKeyCredentialDescriptor`]s belonging to a `UserHandle` in order to avoid accidentally
/// overwriting an existing credential on the authenticator.
/// * Fetching `CredentialId`s and associated `AuthTransports` by `UserHandle` will likely make starting
-/// authentication ceremonies easier for non-discoverable requests (i.e., setting
-/// [`PublicKeyCredentialRequestOptions::allow_credentials`] to a non-empty [`AllowedCredentials`]).
+/// authentication ceremonies easier for [`NonDiscoverableCredentialRequestOptions`].
/// * [`UserHandle`]
-/// * Fetching the [`AuthenticatedCredential`] by [`Authentication::raw_id`] must also coincide with
-/// verifying the associated `UserHandle` matches [`AuthenticatorAssertion::user_handle`] when `Some`.
+/// * Fetching the [`AuthenticatedCredential`] by [`DiscoverableAuthentication::raw_id`] must also coincide with
+/// verifying the associated `UserHandle` matches [`DiscoverableAuthenticatorAssertion::user_handle`].
/// * Fetching [`CredentialId`]s and associated [`AuthTransports`] by `UserHandle` will likely make credential
/// registration easier since one should set [`PublicKeyCredentialCreationOptions::exclude_credentials`] to
/// the [`PublicKeyCredentialDescriptor`]s belonging to a `UserHandle` in order to avoid accidentally
/// overwriting an existing credential on the authenticator.
/// * Fetching `CredentialId`s and associated `AuthTransports` by `UserHandle` will likely make starting
-/// authentication ceremonies easier for non-discoverable requests (i.e., setting
-/// [`PublicKeyCredentialRequestOptions::allow_credentials`] to a non-empty [`AllowedCredentials`]).
+/// authentication ceremonies easier for [`NonDiscoverableCredentialRequestOptions`].
/// * [`DynamicState`]
/// * `DynamicState` is the only part that is ever updated after a successful authentication ceremony
-/// via [`AuthenticationServerState::verify`]. It being separate allows for smaller and quicker updates.
+/// via [`DiscoverableAuthenticationServerState::verify`] or
+/// [`NonDiscoverableAuthenticationServerState::verify`]. It being separate allows for smaller and quicker
+/// updates.
/// * [`Metadata`]
/// * Informative data that is never used during authentication ceremonies; consequently, one may wish to
/// not even save this information.
@@ -488,13 +836,13 @@ fn verify_static_and_dynamic_state<T>(
///
/// When registering a credential, [`AttestedCredentialData::aaguid`], [`AttestedCredentialData::credential_id`],
/// and [`AttestedCredentialData::credential_public_key`] will be the sources for [`Metadata::aaguid`],
-/// [`Self::id`], and [`StaticState::credential_public_key`] respectively. Additionally, there must be some way for
-/// the RP to know what `UserHandle` the [`Registration`] is associated with (e.g., a session cookie); thus the
-/// source of [`Self::user_id`] is the `UserHandle` passed to [`RegistrationServerState::verify`].
+/// [`Self::id`], and [`StaticState::credential_public_key`] respectively. The [`PublicKeyCredentialUserEntity::id`]
+/// associated with the [`PublicKeyCredentialCreationOptions`] used to create the `RegisteredCredential` via
+/// [`RegistrationServerState::verify`] will be the source for [`Self::user_id`].
///
/// The only way to create this is via `RegistrationServerState::verify`.
#[derive(Debug)]
-pub struct RegisteredCredential<'reg, 'user> {
+pub struct RegisteredCredential<'reg, const USER_LEN: usize> {
/// The credential ID.
///
/// For client-side credentials, this is a unique identifier; but for server-side
@@ -506,7 +854,7 @@ pub struct RegisteredCredential<'reg, 'user> {
///
/// Unlike [`Self::id`] which is globally unique for an RP, this is unique up to "user" (i.e.,
/// multiple [`CredentialId`]s will often exist for the same `UserHandle`).
- user_id: UserHandle<&'user [u8]>,
+ user_id: UserHandle<USER_LEN>,
/// Immutable state returned during registration.
static_state: StaticState<UncompressedPubKey<'reg>>,
/// State that can change during authentication ceremonies.
@@ -514,7 +862,7 @@ pub struct RegisteredCredential<'reg, 'user> {
/// Metadata.
metadata: Metadata<'reg>,
}
-impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
+impl<'reg, const USER_LEN: usize> RegisteredCredential<'reg, USER_LEN> {
/// The credential ID.
///
/// For client-side credentials, this is a unique identifier; but for server-side
@@ -536,8 +884,8 @@ impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
/// multiple [`CredentialId`]s will often exist for the same `UserHandle`).
#[inline]
#[must_use]
- pub const fn user_id(&self) -> UserHandle<&'user [u8]> {
- self.user_id
+ pub const fn user_id(&self) -> &UserHandle<USER_LEN> {
+ &self.user_id
}
/// Immutable state returned during registration.
#[inline]
@@ -564,10 +912,10 @@ impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
/// Errors iff the passed arguments are invalid. Read [`CredentialErr`]
/// for more information.
#[inline]
- fn new<'a: 'reg, 'b: 'user>(
+ fn new<'a: 'reg>(
id: CredentialId<&'a [u8]>,
transports: AuthTransports,
- user_id: UserHandle<&'b [u8]>,
+ user_id: UserHandle<USER_LEN>,
static_state: StaticState<UncompressedPubKey<'a>>,
dynamic_state: DynamicState,
metadata: Metadata<'a>,
@@ -618,10 +966,6 @@ impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
})
}
/// Returns the contained data consuming `self`.
- #[expect(
- clippy::type_complexity,
- reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
- )]
#[inline]
#[must_use]
pub const fn into_parts(
@@ -629,7 +973,7 @@ impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
) -> (
CredentialId<&'reg [u8]>,
AuthTransports,
- UserHandle<&'user [u8]>,
+ UserHandle<USER_LEN>,
StaticState<UncompressedPubKey<'reg>>,
DynamicState,
Metadata<'reg>,
@@ -644,10 +988,6 @@ impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
)
}
/// Returns the contained data.
- #[expect(
- clippy::type_complexity,
- reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
- )]
#[inline]
#[must_use]
pub const fn as_parts(
@@ -655,7 +995,7 @@ impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
) -> (
CredentialId<&'reg [u8]>,
AuthTransports,
- UserHandle<&'user [u8]>,
+ &UserHandle<USER_LEN>,
StaticState<UncompressedPubKey<'reg>>,
DynamicState,
Metadata<'reg>,
@@ -663,7 +1003,7 @@ impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
(
self.id,
self.transports,
- self.user_id,
+ &self.user_id,
self.static_state,
self.dynamic_state,
self.metadata,
@@ -677,17 +1017,16 @@ impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
/// [`StaticState::credential_public_key`] is [`CompressedPubKey`] that can own or borrow its data, [`Self::id`] is
/// based on the [`CredentialId`] passed to [`Self::new`] which itself must be from [`Authentication::raw_id`], and
/// [`Self::user_id`] is based on the [`UserHandle`] passed to [`Self::new`] which itself must be the value in
-/// persistent storage associated with the `CredentialId`. When [`AuthenticatorAssertion::user_handle`] is `Some`,
-/// this can be used for `Self::user_id` so long as it matches the value in persistent storage. Note it MUST be
-/// `Some` when using discoverable requests (i.e., [`PublicKeyCredentialRequestOptions::allow_credentials`] is
-/// empty); and when using non-discoverable requests (i.e., `PublicKeyCredentialRequestOptions::allow_credentials`
-/// is non-empty), one should already have the user handle (e.g., in a session cookie) which can also be used.
+/// persistent storage associated with the `CredentialId`.
+///
+/// When [`DiscoverableAuthentication`] is used, one can use [`DiscoverableAuthenticatorAssertion::user_handle`]
+/// for `Self::user_id` so long as it matches the value in persistent storage.
///
/// Note `PublicKey` should be `CompressedPubKey` for this to be useful.
///
/// The only way to create this is via `Self::new`.
#[derive(Debug)]
-pub struct AuthenticatedCredential<'cred, 'user, PublicKey> {
+pub struct AuthenticatedCredential<'cred, 'user, const USER_LEN: usize, PublicKey> {
/// The credential ID.
///
/// For client-side credentials, this is a unique identifier; but for server-side
@@ -697,13 +1036,15 @@ pub struct AuthenticatedCredential<'cred, 'user, PublicKey> {
///
/// Unlike [`Self::id`] which is globally unique for an RP, this is unique up to "user" (i.e.,
/// multiple [`CredentialId`]s will often exist for the same `UserHandle`).
- user_id: UserHandle<&'user [u8]>,
+ user_id: &'user UserHandle<USER_LEN>,
/// Immutable state returned during registration.
static_state: StaticState<PublicKey>,
/// State that can change during authentication ceremonies.
dynamic_state: DynamicState,
}
-impl<'cred, 'user, PublicKey> AuthenticatedCredential<'cred, 'user, PublicKey> {
+impl<'cred, 'user, const USER_LEN: usize, PublicKey>
+ AuthenticatedCredential<'cred, 'user, USER_LEN, PublicKey>
+{
/// The credential ID.
///
/// For client-side credentials, this is a unique identifier; but for server-side
@@ -719,7 +1060,7 @@ impl<'cred, 'user, PublicKey> AuthenticatedCredential<'cred, 'user, PublicKey> {
/// multiple [`CredentialId`]s will often exist for the same `UserHandle`).
#[inline]
#[must_use]
- pub const fn user_id(&self) -> UserHandle<&'user [u8]> {
+ pub const fn user_id(&self) -> &'user UserHandle<USER_LEN> {
self.user_id
}
/// Immutable state returned during registration.
@@ -745,7 +1086,7 @@ impl<'cred, 'user, PublicKey> AuthenticatedCredential<'cred, 'user, PublicKey> {
#[inline]
pub fn new<'a: 'cred, 'b: 'user>(
id: CredentialId<&'a [u8]>,
- user_id: UserHandle<&'b [u8]>,
+ user_id: &'user UserHandle<USER_LEN>,
static_state: StaticState<PublicKey>,
dynamic_state: DynamicState,
) -> Result<Self, CredentialErr> {
@@ -757,34 +1098,26 @@ impl<'cred, 'user, PublicKey> AuthenticatedCredential<'cred, 'user, PublicKey> {
})
}
/// Returns the contained data consuming `self`.
- #[expect(
- clippy::type_complexity,
- reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
- )]
#[inline]
#[must_use]
pub fn into_parts(
self,
) -> (
CredentialId<&'cred [u8]>,
- UserHandle<&'user [u8]>,
+ &'user UserHandle<USER_LEN>,
StaticState<PublicKey>,
DynamicState,
) {
(self.id, self.user_id, self.static_state, self.dynamic_state)
}
/// Returns the contained data.
- #[expect(
- clippy::type_complexity,
- reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
- )]
#[inline]
#[must_use]
pub const fn as_parts(
&self,
) -> (
CredentialId<&'cred [u8]>,
- UserHandle<&'user [u8]>,
+ &'user UserHandle<USER_LEN>,
&StaticState<PublicKey>,
DynamicState,
) {
@@ -809,21 +1142,22 @@ pub enum AggErr {
DomainOrigin(DomainOriginParseErr),
/// Variant when [`Port::from_str`] errors.
Port(PortParseErr),
- /// Variant when [`PublicKeyCredentialRequestOptions::start_ceremony`] errors.
+ /// Variant when [`DiscoverableCredentialRequestOptions::start_ceremony`] or
+ /// [`NonDiscoverableCredentialRequestOptions::start_ceremony`]
+ /// error.
RequestOptions(RequestOptionsErr),
- /// Variant when [`PublicKeyCredentialRequestOptions::second_factor`] errors.
+ /// Variant when [`NonDiscoverableCredentialRequestOptions::second_factor`] errors.
SecondFactor(SecondFactorErr),
/// Variant when [`PublicKeyCredentialCreationOptions::start_ceremony`] errors.
CreationOptions(CreationOptionsErr),
/// Variant when [`Nickname::try_from`] errors.
Nickname(NicknameErr),
- /// Variant when [`UserHandle::rand`] or [`UserHandle::decode`] error.
- UserHandle(UserHandleErr),
/// Variant when [`Username::try_from`] errors.
Username(UsernameErr),
/// Variant when [`RegistrationServerState::verify`] errors.
RegCeremony(RegCeremonyErr),
- /// Variant when [`AuthenticationServerState::verify`] errors.
+ /// Variant when [`DiscoverableAuthenticationServerState::verify`] or.
+ /// [`NonDiscoverableAuthenticationServerState::verify`] error.
AuthCeremony(AuthCeremonyErr),
/// Variant when [`AttestationObject::try_from`] errors.
AttestationObject(AttestationObjectErr),
@@ -864,18 +1198,30 @@ pub enum AggErr {
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
DecodeRegistrationServerState(DecodeRegistrationServerStateErr),
- /// Variant when [`AuthenticationServerState::decode`] errors.
+ /// Variant when [`DiscoverableAuthenticationServerState::decode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+ #[cfg(feature = "serializable_server_state")]
+ DecodeDiscoverableAuthenticationServerState(DecodeDiscoverableAuthenticationServerStateErr),
+ /// Variant when [`NonDiscoverableAuthenticationServerState::decode`] errors.
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
- DecodeAuthenticationServerState(DecodeAuthenticationServerStateErr),
+ DecodeNonDiscoverableAuthenticationServerState(
+ DecodeNonDiscoverableAuthenticationServerStateErr,
+ ),
/// Variant when [`RegistrationServerState::encode`] errors.
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
EncodeRegistrationServerState(SystemTimeError),
- /// Variant when [`AuthenticationServerState::encode`] errors.
+ /// Variant when [`DiscoverableAuthenticationServerState::encode`] errors.
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
- EncodeAuthenticationServerState(EncodeAuthenticationServerStateErr),
+ EncodeDiscoverableAuthenticationServerState(SystemTimeError),
+ /// Variant when [`NonDiscoverableAuthenticationServerState::encode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+ #[cfg(feature = "serializable_server_state")]
+ EncodeNonDiscoverableAuthenticationServerState(
+ EncodeNonDiscoverableAuthenticationServerStateErr,
+ ),
/// Variant when [`AuthenticatedCredential::new`] errors.
#[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))]
#[cfg(any(feature = "bin", feature = "custom"))]
@@ -939,12 +1285,6 @@ impl From<NicknameErr> for AggErr {
Self::Nickname(value)
}
}
-impl From<UserHandleErr> for AggErr {
- #[inline]
- fn from(value: UserHandleErr) -> Self {
- Self::UserHandle(value)
- }
-}
impl From<UsernameErr> for AggErr {
#[inline]
fn from(value: UsernameErr) -> Self {
@@ -1051,26 +1391,26 @@ impl From<DecodeRegistrationServerStateErr> for AggErr {
}
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
-impl From<DecodeAuthenticationServerStateErr> for AggErr {
+impl From<DecodeDiscoverableAuthenticationServerStateErr> for AggErr {
#[inline]
- fn from(value: DecodeAuthenticationServerStateErr) -> Self {
- Self::DecodeAuthenticationServerState(value)
+ fn from(value: DecodeDiscoverableAuthenticationServerStateErr) -> Self {
+ Self::DecodeDiscoverableAuthenticationServerState(value)
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
-impl From<SystemTimeError> for AggErr {
+impl From<DecodeNonDiscoverableAuthenticationServerStateErr> for AggErr {
#[inline]
- fn from(value: SystemTimeError) -> Self {
- Self::EncodeRegistrationServerState(value)
+ fn from(value: DecodeNonDiscoverableAuthenticationServerStateErr) -> Self {
+ Self::DecodeNonDiscoverableAuthenticationServerState(value)
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
-impl From<EncodeAuthenticationServerStateErr> for AggErr {
+impl From<EncodeNonDiscoverableAuthenticationServerStateErr> for AggErr {
#[inline]
- fn from(value: EncodeAuthenticationServerStateErr) -> Self {
- Self::EncodeAuthenticationServerState(value)
+ fn from(value: EncodeNonDiscoverableAuthenticationServerStateErr) -> Self {
+ Self::EncodeNonDiscoverableAuthenticationServerState(value)
}
}
#[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))]
@@ -1102,7 +1442,6 @@ impl Display for AggErr {
Self::SecondFactor(err) => err.fmt(f),
Self::CreationOptions(err) => err.fmt(f),
Self::Nickname(err) => err.fmt(f),
- Self::UserHandle(err) => err.fmt(f),
Self::Username(err) => err.fmt(f),
Self::RegCeremony(ref err) => err.fmt(f),
Self::AuthCeremony(ref err) => err.fmt(f),
@@ -1126,11 +1465,15 @@ impl Display for AggErr {
#[cfg(feature = "serializable_server_state")]
Self::DecodeRegistrationServerState(err) => err.fmt(f),
#[cfg(feature = "serializable_server_state")]
- Self::DecodeAuthenticationServerState(err) => err.fmt(f),
+ Self::DecodeDiscoverableAuthenticationServerState(err) => err.fmt(f),
+ #[cfg(feature = "serializable_server_state")]
+ Self::DecodeNonDiscoverableAuthenticationServerState(err) => err.fmt(f),
#[cfg(feature = "serializable_server_state")]
Self::EncodeRegistrationServerState(ref err) => err.fmt(f),
#[cfg(feature = "serializable_server_state")]
- Self::EncodeAuthenticationServerState(ref err) => err.fmt(f),
+ Self::EncodeDiscoverableAuthenticationServerState(ref err) => err.fmt(f),
+ #[cfg(feature = "serializable_server_state")]
+ Self::EncodeNonDiscoverableAuthenticationServerState(ref err) => err.fmt(f),
#[cfg(any(feature = "bin", feature = "custom"))]
Self::Credential(err) => err.fmt(f),
#[cfg(any(feature = "bin", feature = "custom"))]
diff --git a/src/request.rs b/src/request.rs
@@ -3,17 +3,18 @@ use super::{
request::{
auth::{
AllowedCredential, AllowedCredentials, CredentialSpecificExtension,
+ DiscoverableAuthenticationServerState, DiscoverableCredentialRequestOptions,
+ NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
PublicKeyCredentialRequestOptions,
},
- register::PublicKeyCredentialCreationOptions,
+ register::{PublicKeyCredentialCreationOptions, RegistrationServerState},
},
response::register::ClientExtensionsOutputs,
};
use crate::{
request::{
- auth::AuthenticationServerState,
error::{AsciiDomainErr, DomainOriginParseErr, PortParseErr, SchemeParseErr, UrlErr},
- register::{RegistrationServerState, RegistrationVerificationOptions},
+ register::RegistrationVerificationOptions,
},
response::{
AuthData as _, AuthDataContainer, AuthResponse, AuthTransports, Backup, CeremonyErr,
@@ -44,8 +45,8 @@ use url::Url as Uri;
/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
/// # use webauthn_rp::{
/// # request::{
-/// # auth::{AllowedCredentials, PublicKeyCredentialRequestOptions},
-/// # register::{UserHandle, USER_HANDLE_MAX_LEN},
+/// # auth::{AllowedCredentials, DiscoverableCredentialRequestOptions, NonDiscoverableCredentialRequestOptions},
+/// # register::UserHandle64,
/// # AsciiDomain, Credentials, PublicKeyCredentialDescriptor, RpId,
/// # },
/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
@@ -54,35 +55,37 @@ use url::Url as Uri;
/// # #[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) = PublicKeyCredentialRequestOptions::passkey(&rp_id).start_ceremony()?;
+/// 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")))]
+/// let mut ceremonies_2 = FixedCapHashSet::new(128);
/// # #[cfg(feature = "serde")]
/// assert!(serde_json::to_string(&client).is_ok());
/// let user_handle = get_user_handle();
/// # #[cfg(feature = "custom")]
-/// let creds = get_registered_credentials((&user_handle).into())?;
+/// let creds = get_registered_credentials(&user_handle)?;
/// # #[cfg(feature = "custom")]
/// let (server_2, client_2) =
-/// PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?.start_ceremony()?;
+/// NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?.start_ceremony()?;
/// # #[cfg(all(feature = "custom", not(feature = "serializable_server_state")))]
/// assert!(matches!(
-/// ceremonies.insert_or_replace_all_expired(server_2),
+/// ceremonies_2.insert_or_replace_all_expired(server_2),
/// InsertResult::Success
/// ));
/// # #[cfg(all(feature = "custom", feature = "serde"))]
/// assert!(serde_json::to_string(&client_2).is_ok());
/// /// Extract `UserHandle` from session cookie.
-/// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MAX_LEN]> {
+/// fn get_user_handle() -> UserHandle64 {
/// // ⋮
-/// # UserHandle::new_rand()
+/// # UserHandle64::new()
/// }
/// # #[cfg(feature = "custom")]
/// /// Fetch the `AllowedCredentials` associated with `user`.
-/// fn get_registered_credentials(user: UserHandle<&[u8]>) -> Result<AllowedCredentials, AggErr> {
+/// fn get_registered_credentials(user: &UserHandle64) -> Result<AllowedCredentials, AggErr> {
/// // ⋮
/// # let mut creds = AllowedCredentials::new();
/// # creds.push(
@@ -110,43 +113,49 @@ pub mod error;
/// # use webauthn_rp::{
/// # request::{
/// # register::{
-/// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN,
+/// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64,
/// # },
/// # AsciiDomain, PublicKeyCredentialDescriptor, RpId
/// # },
/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
/// # AggErr,
/// # };
-/// # #[cfg(not(feature = "serializable_server_state"))]
+/// # #[cfg(all(not(feature = "serializable_server_state"), 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();
-/// let handle = (&user_handle).into();
-/// let user = get_user_entity(handle)?;
-/// let creds = get_registered_credentials(handle)?;
+/// # #[cfg(feature = "custom")]
+/// let user = get_user_entity(&user_handle)?;
+/// # #[cfg(feature = "custom")]
+/// let creds = get_registered_credentials(&user_handle)?;
+/// # #[cfg(feature = "custom")]
/// let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, user.clone(), creds)
/// .start_ceremony()?;
-/// # #[cfg(not(feature = "serializable_server_state"))]
+/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom"))]
/// assert!(matches!(
/// ceremonies.insert_or_replace_all_expired(server),
/// InsertResult::Success
/// ));
-/// # #[cfg(feature = "serde")]
+/// # #[cfg(all(feature = "serde", feature = "custom"))]
/// assert!(serde_json::to_string(&client).is_ok());
-/// let creds_2 = get_registered_credentials(handle)?;
+/// # #[cfg(feature = "custom")]
+/// let creds_2 = get_registered_credentials(&user_handle)?;
+/// # #[cfg(feature = "custom")]
/// let (server_2, client_2) =
/// PublicKeyCredentialCreationOptions::second_factor(&rp_id, user, creds_2).start_ceremony()?;
-/// # #[cfg(not(feature = "serializable_server_state"))]
+/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom"))]
/// assert!(matches!(
/// ceremonies.insert_or_replace_all_expired(server_2),
/// InsertResult::Success
/// ));
-/// # #[cfg(feature = "serde")]
+/// # #[cfg(all(feature = "serde", feature = "custom"))]
/// assert!(serde_json::to_string(&client_2).is_ok());
-/// /// Extract `UserHandle` from session cookie if this is not the first credential registered.
-/// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MAX_LEN]> {
+/// /// Extract `UserHandle` from session cookie or storage if this is not the first credential registered.
+/// # #[cfg(feature = "custom")]
+/// fn get_user_handle() -> UserHandle64 {
/// // ⋮
-/// # UserHandle::new_rand()
+/// # [0; USER_HANDLE_MAX_LEN].into()
/// }
/// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`.
/// ///
@@ -154,7 +163,8 @@ pub mod error;
/// /// will need to be constructed with `name` and `display_name` passed from the client and `UserHandle::new`
/// /// used for `id`. Once created, this info can be stored such that the entity information
/// /// does not need to be requested for subsequent registrations.
-/// fn get_user_entity(user: UserHandle<&[u8]>) -> Result<PublicKeyCredentialUserEntity<&[u8]>, AggErr> {
+/// # #[cfg(feature = "custom")]
+/// fn get_user_entity(user: &UserHandle64) -> Result<PublicKeyCredentialUserEntity<'_, '_, '_, USER_HANDLE_MAX_LEN>, AggErr> {
/// // ⋮
/// # Ok(PublicKeyCredentialUserEntity {
/// # name: "foo".try_into()?,
@@ -167,7 +177,7 @@ pub mod error;
/// /// This doesn't need to be called when this is the first credential registered for `user`; instead
/// /// an empty `Vec` should be passed.
/// fn get_registered_credentials(
-/// user: UserHandle<&[u8]>,
+/// user: &UserHandle64,
/// ) -> Result<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, AggErr> {
/// // ⋮
/// # Ok(Vec::new())
@@ -179,8 +189,8 @@ pub mod register;
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
#[cfg(feature = "serde")]
mod ser;
-/// Contains functionality to (de)serialize data needed for [`RegistrationServerState`] and
-/// [`AuthenticationServerState`] to a data store.
+/// Contains functionality to (de)serialize data needed for [`RegistrationServerState`],
+/// [`DiscoverableAuthenticationServerState`], and [`NonDiscoverableAuthenticationServerState`] to a data store.
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
pub(super) mod ser_server_state;
@@ -1147,13 +1157,13 @@ impl From<Backup> for BackupReq {
/// /// Fetches all credentials under `user_handle` to be allowed during authentication for non-discoverable
/// /// requests.
/// # #[cfg(feature = "custom")]
-/// fn get_allowed_credentials(user_handle: UserHandle<&[u8]>) -> AllowedCredentials {
+/// fn get_allowed_credentials<const LEN: usize>(user_handle: &UserHandle<LEN>) -> AllowedCredentials {
/// get_credentials(user_handle)
/// }
/// /// Fetches all credentials under `user_handle` to be excluded during registration.
/// # #[cfg(feature = "custom")]
-/// fn get_excluded_credentials(
-/// user_handle: UserHandle<&[u8]>,
+/// fn get_excluded_credentials<const LEN: usize>(
+/// user_handle: &UserHandle<LEN>,
/// ) -> Vec<PublicKeyCredentialDescriptor<Vec<u8>>> {
/// get_credentials(user_handle)
/// }
@@ -1161,7 +1171,7 @@ impl From<Backup> for BackupReq {
/// /// registration as well as the `AllowedCredentials` containing `AllowedCredential`s with no credential-specific
/// /// extensions which is used for non-discoverable requests.
/// # #[cfg(feature = "custom")]
-/// fn get_credentials<T>(user_handle: UserHandle<&[u8]>) -> T
+/// fn get_credentials<const LEN: usize, T>(user_handle: &UserHandle<LEN>) -> T
/// where
/// T: Credentials,
/// PublicKeyCredentialDescriptor<Vec<u8>>: Into<T::Credential>,
@@ -1182,8 +1192,8 @@ impl From<Backup> for BackupReq {
/// /// Fetches all `CredentialId`s and associated `AuthTransports` under `user_handle`
/// /// from the database.
/// # #[cfg(feature = "custom")]
-/// fn get_cred_parts(
-/// user_handle: UserHandle<&[u8]>,
+/// fn get_cred_parts<const LEN: usize>(
+/// user_handle: &UserHandle<LEN>,
/// ) -> impl Iterator<Item = (CredentialId<Vec<u8>>, AuthTransports)> {
/// // ⋮
/// # [(
@@ -1270,7 +1280,7 @@ impl<'o, 't, O, T> From<&RegistrationVerificationOptions<'o, 't, O, T>>
/// Functionality common to both registration and authentication ceremonies.
///
/// Designed to be implemented on the _request_ side.
-trait Ceremony<U> {
+trait Ceremony<const USER_LEN: usize, const DISCOVERABLE: bool> {
/// The type of response that is associated with the ceremony.
type R: Response;
/// Challenge.
@@ -1501,13 +1511,15 @@ pub(super) const THREE_HUNDRED_THOUSAND: NonZeroU32 = NonZeroU32::new(300_000).u
/// 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`] and [`AuthenticationServerState`] both implement [`Hash`] by simply writing the
+/// [`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` and
-/// `AuthenticationServerState` are also immutable and only constructable via
-/// [`PublicKeyCredentialCreationOptions::start_ceremony`] and
-/// [`PublicKeyCredentialRequestOptions::start_ceremony`] respectively. Since `Challenge` is already based 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"))))]
@@ -1634,19 +1646,11 @@ impl BuildHasher for BuildIdentityHasher {
IdentityHasher(0)
}
}
-/// Prevent users from implementing [`ServerState`] and [`super::register::User`].
-mod private {
- /// Marker trait used as a supertrait of `ServerState` and `User`.
- pub trait Sealed {}
- impl Sealed for super::AuthenticationServerState {}
- impl Sealed for super::RegistrationServerState {}
- impl<T> Sealed for super::register::UserHandle<T> {}
- impl<T> Sealed for Option<super::register::UserHandle<T>> {}
-}
-/// Subset of data shared by both [`RegistrationServerState`] and [`AuthenticationServerState`].
+/// "Ceremonies" stored on the server that expire after a certain duration.
///
-/// This `trait` is sealed and cannot be implemented for types outside of `webauthn_rp`.
-pub trait ServerState: private::Sealed {
+/// Types like [`RegistrationServerState`] and [`DiscoverableAuthenticationServerState`] are based on [`Challenge`]s
+/// that expire after a certain duration.
+pub trait TimedCeremony {
/// Returns the `Instant` the ceremony expires.
///
/// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead.
@@ -1656,7 +1660,7 @@ pub trait ServerState: private::Sealed {
/// Returns the `SystemTime` the ceremony expires.
#[cfg(all(not(doc), feature = "serializable_server_state"))]
fn expiration(&self) -> SystemTime;
- /// Returns the `SentChallenge` associated with the ceremony.
+ /// Returns the `SentChallenge`.
fn sent_challenge(&self) -> SentChallenge;
}
/// Fixed-capacity hash set that only inserts items when there is available capacity.
@@ -1671,13 +1675,13 @@ pub trait ServerState: private::Sealed {
/// 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 [`ServerState`], there are legitimate reasons why a ceremony will never finish (e.g.,
+/// 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 `ServerState` which in turn requires
+/// 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")))]
@@ -1748,7 +1752,7 @@ impl<T, S> FixedCapHashSet<T, S> {
}
}
#[cfg(any(doc, not(feature = "serializable_server_state")))]
-impl<T: ServerState, S> FixedCapHashSet<T, S> {
+impl<T: TimedCeremony, S> FixedCapHashSet<T, S> {
/// Removes all expired ceremonies.
#[inline]
pub fn remove_expired_ceremonies(&mut self) {
@@ -1840,7 +1844,7 @@ impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> {
}
}
#[cfg(any(doc, not(feature = "serializable_server_state")))]
-impl<T: Borrow<SentChallenge> + Eq + Hash + ServerState, S: BuildHasher> FixedCapHashSet<T, S> {
+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`] `==`
@@ -1908,7 +1912,10 @@ mod tests {
AggErr, AuthenticatedCredential,
response::{
AuthTransports, AuthenticatorAttachment, Backup, CredentialId,
- auth::{Authentication, AuthenticatorAssertion},
+ auth::{
+ DiscoverableAuthentication, DiscoverableAuthenticatorAssertion,
+ NonDiscoverableAuthentication, NonDiscoverableAuthenticatorAssertion,
+ },
register::{
AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation,
AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputs,
@@ -1922,8 +1929,8 @@ mod tests {
PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement,
auth::{
AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions,
- CredentialSpecificExtension, Extension as AuthExt, PrfInputOwned,
- PublicKeyCredentialRequestOptions,
+ CredentialSpecificExtension, DiscoverableCredentialRequestOptions,
+ Extension as AuthExt, NonDiscoverableCredentialRequestOptions, PrfInputOwned,
},
register::{
CredProtect, Extension as RegExt, PublicKeyCredentialCreationOptions,
@@ -1965,12 +1972,12 @@ mod tests {
#[cfg(feature = "custom")]
fn ed25519_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let id = UserHandle::try_from([0; 1].as_slice())?;
+ let id = UserHandle::from([0]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
- id,
+ id: &id,
display_name: None,
},
Vec::new(),
@@ -2288,7 +2295,6 @@ mod tests {
attestation_object.truncate(261);
assert!(matches!(opts.start_ceremony()?.0.verify(
&rp_id,
- id,
&Registration {
response: AuthenticatorAttestation::new(
client_data_json,
@@ -2323,10 +2329,10 @@ mod tests {
}),
},
});
- let mut opts = PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?;
- opts.user_verification = UserVerificationRequirement::Required;
- opts.challenge = Challenge(0);
- opts.extensions = AuthExt { prf: None };
+ let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?;
+ opts.options().user_verification = UserVerificationRequirement::Required;
+ opts.options().challenge = Challenge(0);
+ opts.options().extensions = AuthExt { prf: None };
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
let mut authenticator_data = Vec::with_capacity(164);
@@ -2484,9 +2490,9 @@ mod tests {
authenticator_data.truncate(132);
assert!(!opts.start_ceremony()?.0.verify(
&rp_id,
- &Authentication {
+ &NonDiscoverableAuthentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
- response: AuthenticatorAssertion::with_user(
+ response: NonDiscoverableAuthenticatorAssertion::with_user(
client_data_json,
authenticator_data,
sig,
@@ -2496,7 +2502,7 @@ mod tests {
},
&mut AuthenticatedCredential::new(
CredentialId::try_from([0; 16].as_slice())?,
- UserHandle::try_from([0].as_slice())?,
+ &UserHandle::from([0]),
StaticState {
credential_public_key: CompressedPubKey::<_, &[u8], &[u8], &[u8]>::Ed25519(
Ed25519PubKey::from(ed_priv.verifying_key().to_bytes()),
@@ -2521,12 +2527,12 @@ mod tests {
#[cfg(feature = "custom")]
fn p256_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let id = UserHandle::try_from([0; 1].as_slice())?;
+ let id = UserHandle::from([0]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
- id,
+ id: &id,
display_name: None,
},
Vec::new(),
@@ -2759,7 +2765,6 @@ mod tests {
attestation_object[146..].copy_from_slice(y);
assert!(matches!(opts.start_ceremony()?.0.verify(
&rp_id,
- id,
&Registration {
response: AuthenticatorAttestation::new(
client_data_json,
@@ -2780,8 +2785,8 @@ mod tests {
#[cfg(feature = "custom")]
fn p256_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let mut opts = PublicKeyCredentialRequestOptions::passkey(&rp_id);
- opts.challenge = Challenge(0);
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
+ opts.0.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
let mut authenticator_data = Vec::with_capacity(69);
@@ -2850,9 +2855,9 @@ mod tests {
authenticator_data.truncate(37);
assert!(!opts.start_ceremony()?.0.verify(
&rp_id,
- &Authentication {
+ &DiscoverableAuthentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
- response: AuthenticatorAssertion::with_user(
+ response: DiscoverableAuthenticatorAssertion::new(
client_data_json,
authenticator_data,
der_sig.as_bytes().into(),
@@ -2862,7 +2867,7 @@ mod tests {
},
&mut AuthenticatedCredential::new(
CredentialId::try_from([0; 16].as_slice())?,
- UserHandle::try_from([0].as_slice())?,
+ &UserHandle::from([0]),
StaticState {
credential_public_key: CompressedPubKey::<&[u8], _, &[u8], &[u8]>::P256(
CompressedP256PubKey::from((
@@ -2890,12 +2895,12 @@ mod tests {
#[cfg(feature = "custom")]
fn p384_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let id = UserHandle::try_from([0; 1].as_slice())?;
+ let id = UserHandle::from([0]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
- id,
+ id: &id,
display_name: None,
},
Vec::new(),
@@ -3163,7 +3168,6 @@ mod tests {
attestation_object[163..].copy_from_slice(y);
assert!(matches!(opts.start_ceremony()?.0.verify(
&rp_id,
- id,
&Registration {
response: AuthenticatorAttestation::new(
client_data_json,
@@ -3184,8 +3188,8 @@ mod tests {
#[cfg(feature = "custom")]
fn p384_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let mut opts = PublicKeyCredentialRequestOptions::passkey(&rp_id);
- opts.challenge = Challenge(0);
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
+ opts.0.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
let mut authenticator_data = Vec::with_capacity(69);
@@ -3255,9 +3259,9 @@ mod tests {
authenticator_data.truncate(37);
assert!(!opts.start_ceremony()?.0.verify(
&rp_id,
- &Authentication {
+ &DiscoverableAuthentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
- response: AuthenticatorAssertion::with_user(
+ response: DiscoverableAuthenticatorAssertion::new(
client_data_json,
authenticator_data,
der_sig.as_bytes().into(),
@@ -3267,7 +3271,7 @@ mod tests {
},
&mut AuthenticatedCredential::new(
CredentialId::try_from([0; 16].as_slice())?,
- UserHandle::try_from([0].as_slice())?,
+ &UserHandle::from([0]),
StaticState {
credential_public_key: CompressedPubKey::<&[u8], &[u8], _, &[u8]>::P384(
CompressedP384PubKey::from((
@@ -3295,12 +3299,12 @@ mod tests {
#[cfg(feature = "custom")]
fn rsa_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let id = UserHandle::try_from([0; 1].as_slice())?;
+ let id = UserHandle::from([0]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
- id,
+ id: &id,
display_name: None,
},
Vec::new(),
@@ -3777,7 +3781,6 @@ mod tests {
attestation_object[113..369].copy_from_slice(n.as_slice());
assert!(matches!(opts.start_ceremony()?.0.verify(
&rp_id,
- id,
&Registration {
response: AuthenticatorAttestation::new(
client_data_json,
@@ -3798,8 +3801,8 @@ mod tests {
#[cfg(feature = "custom")]
fn rsa_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let mut opts = PublicKeyCredentialRequestOptions::passkey(&rp_id);
- opts.challenge = Challenge(0);
+ let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
+ opts.0.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
let mut authenticator_data = Vec::with_capacity(69);
@@ -3922,9 +3925,9 @@ mod tests {
authenticator_data.truncate(37);
assert!(!opts.start_ceremony()?.0.verify(
&rp_id,
- &Authentication {
+ &DiscoverableAuthentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
- response: AuthenticatorAssertion::with_user(
+ response: DiscoverableAuthenticatorAssertion::new(
client_data_json,
authenticator_data,
sig,
@@ -3934,7 +3937,7 @@ mod tests {
},
&mut AuthenticatedCredential::new(
CredentialId::try_from([0; 16].as_slice())?,
- UserHandle::try_from([0].as_slice())?,
+ &UserHandle::from([0]),
StaticState {
credential_public_key: CompressedPubKey::<&[u8], &[u8], &[u8], _>::Rsa(
RsaPubKey::try_from((rsa_pub.as_ref().n().to_bytes_be(), e)).unwrap(),
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -11,19 +11,19 @@ use super::{
use super::{
super::{
AuthenticatedCredential,
- request::register::User,
response::{
AuthenticatorAttachment,
auth::{
- Authentication, AuthenticatorExtensionOutput, HmacSecret,
+ Authentication, AuthenticatorExtensionOutput, DiscoverableAuthentication,
+ HmacSecret, NonDiscoverableAuthentication,
error::{AuthCeremonyErr, ExtensionErr, OneOrTwo},
},
register::CompressedPubKey,
},
},
BackupReq, Ceremony, CeremonyOptions, Challenge, CredentialId, Credentials, ExtensionReq, Hint,
- Origin, PublicKeyCredentialDescriptor, RpId, SentChallenge, ServerState,
- THREE_HUNDRED_THOUSAND, UserVerificationRequirement,
+ Origin, PublicKeyCredentialDescriptor, RpId, SentChallenge, THREE_HUNDRED_THOUSAND,
+ TimedCeremony, UserVerificationRequirement,
auth::error::{RequestOptionsErr, SecondFactorErr},
};
use core::{
@@ -43,7 +43,8 @@ pub mod error;
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
#[cfg(feature = "serde")]
mod ser;
-/// Contains functionality to (de)serialize [`AuthenticationServerState`] to a data store.
+/// Contains functionality to (de)serialize [`DiscoverableAuthenticationServerState`] and
+/// [`NonDiscoverableAuthenticationServerState`] to a data store.
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
pub mod ser_server_state;
@@ -102,11 +103,9 @@ impl SignatureCounterEnforcement {
///
/// For the owned analog, see [`PrfInputOwned`].
///
-/// When relying on discoverable requests
-/// (i.e., [`PublicKeyCredentialRequestOptions::allow_credentials`] is empty),
-/// 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`.
+/// 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)]
pub struct PrfInput<'a> {
/// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first).
@@ -118,10 +117,8 @@ pub struct PrfInput<'a> {
}
/// Owned version of [`PrfInput`].
///
-/// When relying on non-discoverable requests
-/// (i.e., [`PublicKeyCredentialRequestOptions::allow_credentials`] is non-empty),
-/// it's recommended to use credential-specific PRF inputs that are continuously rolled over.
-/// One uses this type for such a thing.
+/// 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)]
pub struct PrfInputOwned {
/// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first).
@@ -284,78 +281,57 @@ impl From<&AllowedCredentials> for Vec<CredInfo> {
})
}
}
-/// Helper that verifies the overlap of [`PublicKeyCredentialRequestOptions::start_ceremony`] and
-/// [`AuthenticationServerState::decode`].
-fn validate_options_helper(
+/// Helper that verifies the overlap of [`DiscoverableCredentialRequestOptions::start_ceremony`] and
+/// [`DiscoverableAuthenticationServerState::decode`].
+fn validate_discoverable_options_helper(
ext: ServerExtensionInfo,
uv: UserVerificationRequirement,
- creds: &[CredInfo],
) -> Result<(), RequestOptionsErr> {
// If PRF is set, the user has to verify themselves.
- ext.prf
- .as_ref()
- .map_or(Ok(()), |_| {
+ ext.prf.as_ref().map_or(Ok(()), |_| {
+ if matches!(uv, UserVerificationRequirement::Required) {
+ Ok(())
+ } else {
+ Err(RequestOptionsErr::PrfWithoutUserVerification)
+ }
+ })
+}
+/// Helper that verifies the overlap of [`NonDiscoverableCredentialRequestOptions::start_ceremony`] and
+/// [`NonDiscoverableAuthenticationServerState::decode`].
+fn validate_non_discoverable_options_helper(
+ uv: UserVerificationRequirement,
+ creds: &[CredInfo],
+) -> Result<(), RequestOptionsErr> {
+ creds.iter().try_fold((), |(), cred| {
+ // If PRF is set, the user has to verify themselves.
+ cred.ext.prf.as_ref().map_or(Ok(()), |_| {
if matches!(uv, UserVerificationRequirement::Required) {
Ok(())
} else {
Err(RequestOptionsErr::PrfWithoutUserVerification)
}
})
- .and_then(|()| {
- creds.iter().try_fold((), |(), cred| {
- // If PRF is set, the user has to verify themselves.
- cred.ext.prf.as_ref().map_or(Ok(()), |_| {
- if matches!(uv, UserVerificationRequirement::Required) {
- Ok(())
- } else {
- Err(RequestOptionsErr::PrfWithoutUserVerification)
- }
- })
- })
- })
+ })
}
/// The [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions)
/// to send to the client when authenticating a credential.
///
-/// Upon saving the [`AuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send
-/// [`AuthenticationClientState`] to the client ASAP. After receiving the newly created [`Authentication`], it
-/// is validated using [`AuthenticationServerState::verify`].
+/// Upon saving the [`DiscoverableAuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send
+/// [`DiscoverableAuthenticationClientState`] to the client ASAP. After receiving the newly created
+/// [`DiscoverableAuthentication`], it is validated using [`DiscoverableAuthenticationServerState::verify`].
#[derive(Debug)]
-pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
- /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge).
- pub challenge: Challenge,
- /// [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout).
- ///
- /// Note we require a positive value despite the spec allowing an optional nonnegative value. This jives
- /// with the fact that in-memory storage is required when `serializable_server_state` is not enabled
- /// when authenticating credentials as no timeout would make out-of-memory (OOM) conditions more likely.
- pub timeout: NonZeroU32,
- /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-rpid).
- ///
- /// This MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] used when the credential was registered.
- pub rp_id: &'rp_id RpId,
- /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
- pub allow_credentials: AllowedCredentials,
- /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification).
- pub user_verification: UserVerificationRequirement,
- /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-hints).
- pub hints: Hint,
- /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions).
- pub extensions: Extension<'prf>,
-}
-impl<'rp_id, 'prf> PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
- /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to
- /// [`UserVerificationRequirement::Required`] and [`Self::timeout`] set to 5 minutes,
- ///
- /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the
- /// credential was registered.
+pub struct DiscoverableCredentialRequestOptions<'rp_id, 'prf>(
+ pub PublicKeyCredentialRequestOptions<'rp_id, 'prf>,
+);
+impl<'rp_id, 'prf> DiscoverableCredentialRequestOptions<'rp_id, 'prf> {
+ /// Creates a `DiscoverableCredentialRequestOptions` containing [`PublicKeyCredentialRequestOptions::passkey`].
///
/// # Examples
///
/// ```
- /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement};
+ /// # use webauthn_rp::request::{auth::DiscoverableCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement};
/// assert!(matches!(
- /// PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification,
+ /// DiscoverableCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).0.user_verification,
/// UserVerificationRequirement::Required
/// ));
/// # Ok::<_, webauthn_rp::AggErr>(())
@@ -363,25 +339,82 @@ impl<'rp_id, 'prf> PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
#[inline]
#[must_use]
pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self {
- Self {
- challenge: Challenge::new(),
- timeout: THREE_HUNDRED_THOUSAND,
- rp_id,
- allow_credentials: AllowedCredentials::new(),
- user_verification: UserVerificationRequirement::Required,
- hints: Hint::None,
- extensions: Extension::default(),
- }
+ Self(PublicKeyCredentialRequestOptions::passkey(rp_id))
}
- /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to
- /// [`UserVerificationRequirement::Discouraged`] and [`Self::timeout`] set to 5 minutes.
+ /// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming
+ /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so
+ /// `DiscoverableAuthenticationClientState` MUST be sent ASAP. In order to complete authentication, the returned
+ /// `DiscoverableAuthenticationServerState` MUST be saved so that it can later be used to verify the credential
+ /// assertion with [`DiscoverableAuthenticationServerState::verify`].
///
- /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the
- /// [`AllowedCredential`]s were registered.
+ /// # Errors
+ ///
+ /// Errors iff `self` contains incompatible configuration.
+ #[inline]
+ pub fn start_ceremony(
+ self,
+ ) -> Result<
+ (
+ DiscoverableAuthenticationServerState,
+ DiscoverableAuthenticationClientState<'rp_id, 'prf>,
+ ),
+ RequestOptionsErr,
+ > {
+ let extensions = self.0.extensions.into();
+ validate_discoverable_options_helper(extensions, self.0.user_verification).and_then(|()| {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let res = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let res = SystemTime::now();
+ res.checked_add(Duration::from_millis(
+ NonZeroU64::from(self.0.timeout).get(),
+ ))
+ .ok_or(RequestOptionsErr::InvalidTimeout)
+ .map(|expiration| {
+ (
+ DiscoverableAuthenticationServerState(AuthenticationServerState {
+ challenge: SentChallenge(self.0.challenge.0),
+ user_verification: self.0.user_verification,
+ extensions,
+ expiration,
+ }),
+ DiscoverableAuthenticationClientState(self),
+ )
+ })
+ })
+ }
+}
+/// The [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions)
+/// to send to the client when authenticating a credential.
+///
+/// Upon saving the [`NonDiscoverableAuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send
+/// [`NonDiscoverableAuthenticationClientState`] to the client ASAP. After receiving the newly created
+/// [`NonDiscoverableAuthentication`], it is validated using [`NonDiscoverableAuthenticationServerState::verify`].
+#[derive(Debug)]
+pub struct NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
+ /// [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions).
+ options: PublicKeyCredentialRequestOptions<'rp_id, 'prf>,
+ /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
+ allow_credentials: AllowedCredentials,
+}
+impl<'rp_id, 'prf> NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
+ /// Returns a mutable reference to the configurable options.
+ #[inline]
+ pub const fn options(&mut self) -> &mut PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
+ &mut self.options
+ }
+ /// Returns the `slice` of [`AllowedCredential`]s.
+ #[inline]
+ #[must_use]
+ pub fn allow_credentials(&self) -> &[AllowedCredential] {
+ self.allow_credentials.as_ref()
+ }
+ /// Creates a `NonDiscoverableCredentialRequestOptions` containing
+ /// [`PublicKeyCredentialRequestOptions::second_factor`] and the passed [`AllowedCredentials`].
///
/// # Errors
///
- /// Errors iff [`AllowedCredentials`] is empty.
+ /// Errors iff `allow_credentials` is empty.
///
/// # Examples
///
@@ -390,7 +423,7 @@ impl<'rp_id, 'prf> PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
/// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
/// # use webauthn_rp::{
/// # request::{
- /// # auth::{AllowedCredentials, PublicKeyCredentialRequestOptions},
+ /// # auth::{AllowedCredentials, NonDiscoverableCredentialRequestOptions},
/// # AsciiDomain, RpId, PublicKeyCredentialDescriptor, Credentials
/// # },
/// # response::{AuthTransports, CredentialId},
@@ -415,9 +448,8 @@ impl<'rp_id, 'prf> PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
/// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into()));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// assert_eq!(
- /// PublicKeyCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), creds)?
- /// .allow_credentials
- /// .as_ref()
+ /// NonDiscoverableCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), creds)?
+ /// .allow_credentials()
/// .len(),
/// 1
/// );
@@ -426,104 +458,175 @@ impl<'rp_id, 'prf> PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
#[inline]
pub fn second_factor<'a: 'rp_id>(
rp_id: &'a RpId,
- creds: AllowedCredentials,
+ allow_credentials: AllowedCredentials,
) -> Result<Self, SecondFactorErr> {
- if creds.as_ref().is_empty() {
+ if allow_credentials.is_empty() {
Err(SecondFactorErr)
} else {
- let mut opts = Self::passkey(rp_id);
- opts.allow_credentials = creds;
- opts.user_verification = UserVerificationRequirement::Discouraged;
- Ok(opts)
+ Ok(Self {
+ options: PublicKeyCredentialRequestOptions::second_factor(rp_id),
+ allow_credentials,
+ })
}
}
/// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming
- /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `AuthenticationClientState` MUST be
- /// sent ASAP. In order to complete authentication, the returned `AuthenticationServerState` MUST be saved so
- /// that it can later be used to verify the credential assertion with [`AuthenticationServerState::verify`].
+ /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `NonDiscoverableAuthenticationClientState`
+ /// MUST be sent ASAP. In order to complete authentication, the returned `NonDiscoverableAuthenticationServerState`
+ /// MUST be saved so that it can later be used to verify the credential assertion with
+ /// [`NonDiscoverableAuthenticationServerState::verify`].
///
/// # Errors
///
/// Errors iff `self` contains incompatible configuration.
- ///
- /// # Examples
- ///
- /// ```
- /// # #[cfg(not(feature = "serializable_server_state"))]
- /// # use std::time::Instant;
- /// # #[cfg(not(feature = "serializable_server_state"))]
- /// # use webauthn_rp::request::ServerState;
- /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId};
- /// # #[cfg(not(feature = "serializable_server_state"))]
- /// assert!(
- /// PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?))
- /// .start_ceremony()?
- /// .0
- /// .expiration() > Instant::now()
- /// );
- /// # Ok::<_, webauthn_rp::AggErr>(())
- /// ```
#[inline]
pub fn start_ceremony(
self,
) -> Result<
(
- AuthenticationServerState,
- AuthenticationClientState<'rp_id, 'prf>,
+ NonDiscoverableAuthenticationServerState,
+ NonDiscoverableAuthenticationClientState<'rp_id, 'prf>,
),
RequestOptionsErr,
> {
- let extensions = self.extensions.into();
- let allow_credentials = Vec::from(&self.allow_credentials);
- validate_options_helper(extensions, self.user_verification, &allow_credentials).and_then(
+ let extensions = self.options.extensions.into();
+ validate_discoverable_options_helper(extensions, self.options.user_verification).and_then(
|()| {
- #[cfg(not(feature = "serializable_server_state"))]
- let res = Instant::now();
- #[cfg(feature = "serializable_server_state")]
- let res = SystemTime::now();
- res.checked_add(Duration::from_millis(NonZeroU64::from(self.timeout).get()))
+ let allow_credentials = Vec::from(&self.allow_credentials);
+ validate_non_discoverable_options_helper(
+ self.options.user_verification,
+ allow_credentials.as_slice(),
+ )
+ .and_then(|()| {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let res = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let res = SystemTime::now();
+ res.checked_add(Duration::from_millis(
+ NonZeroU64::from(self.options.timeout).get(),
+ ))
.ok_or(RequestOptionsErr::InvalidTimeout)
.map(|expiration| {
(
- AuthenticationServerState {
- challenge: SentChallenge(self.challenge.0),
+ NonDiscoverableAuthenticationServerState {
+ state: AuthenticationServerState {
+ challenge: SentChallenge(self.options.challenge.0),
+ user_verification: self.options.user_verification,
+ extensions,
+ expiration,
+ },
allow_credentials,
- user_verification: self.user_verification,
- extensions,
- expiration,
},
- AuthenticationClientState(self),
+ NonDiscoverableAuthenticationClientState(self),
)
})
+ })
},
)
}
}
-/// Container of a [`PublicKeyCredentialRequestOptions`] that has been used to start the authentication ceremony.
-/// This gets sent to the client ASAP.
+/// The [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions)
+/// to send to the client when authenticating a credential.
+///
+/// This does _not_ contain [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
#[derive(Debug)]
-pub struct AuthenticationClientState<'rp_id, 'prf>(PublicKeyCredentialRequestOptions<'rp_id, 'prf>);
-impl<'rp_id, 'prf> AuthenticationClientState<'rp_id, 'prf> {
- /// Returns the `PublicKeyCredentialRequestOptions` that was used to start an authentication ceremony.
+pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge).
+ pub challenge: Challenge,
+ /// [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout).
+ ///
+ /// Note we require a positive value despite the spec allowing an optional nonnegative value. This jives
+ /// with the fact that in-memory storage is required when `serializable_server_state` is not enabled
+ /// when authenticating credentials as no timeout would make out-of-memory (OOM) conditions more likely.
+ pub timeout: NonZeroU32,
+ /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-rpid).
+ ///
+ /// This MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] used when the credential was registered.
+ pub rp_id: &'rp_id RpId,
+ /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification).
+ pub user_verification: UserVerificationRequirement,
+ /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-hints).
+ pub hints: Hint,
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions).
+ pub extensions: Extension<'prf>,
+}
+impl<'rp_id> PublicKeyCredentialRequestOptions<'rp_id, '_> {
+ /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to
+ /// [`UserVerificationRequirement::Required`] and [`Self::timeout`] set to 5 minutes,
+ ///
+ /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the
+ /// credential was registered.
///
/// # Examples
///
/// ```
- /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId};
- /// assert!(
- /// PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?))
- /// .start_ceremony()?
- /// .1
- /// .options()
- /// .allow_credentials
- /// .as_ref()
- /// .is_empty()
- /// );
+ /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement};
+ /// assert!(matches!(
+ /// PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification,
+ /// UserVerificationRequirement::Required
+ /// ));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self {
+ Self {
+ challenge: Challenge::new(),
+ timeout: THREE_HUNDRED_THOUSAND,
+ rp_id,
+ user_verification: UserVerificationRequirement::Required,
+ hints: Hint::None,
+ extensions: Extension::default(),
+ }
+ }
+ /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to
+ /// [`UserVerificationRequirement::Discouraged`] and [`Self::timeout`] set to 5 minutes.
+ ///
+ /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the
+ /// credentials were registered.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement};
+ /// assert!(matches!(
+ /// PublicKeyCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification,
+ /// UserVerificationRequirement::Discouraged
+ /// ));
/// # Ok::<_, webauthn_rp::AggErr>(())
/// ```
#[inline]
#[must_use]
- pub const fn options(&self) -> &PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
+ pub fn second_factor<'a: 'rp_id>(rp_id: &'a RpId) -> Self {
+ let mut opts = Self::passkey(rp_id);
+ opts.user_verification = UserVerificationRequirement::Discouraged;
+ opts
+ }
+}
+/// Container of a [`DiscoverableCredentialRequestOptions`] that has been used to start the authentication ceremony.
+/// This gets sent to the client ASAP.
+#[derive(Debug)]
+pub struct DiscoverableAuthenticationClientState<'rp_id, 'prf>(
+ DiscoverableCredentialRequestOptions<'rp_id, 'prf>,
+);
+impl<'rp_id, 'prf> DiscoverableAuthenticationClientState<'rp_id, 'prf> {
+ /// Returns the `DiscoverableCredentialRequestOptions` that was used to start an authentication ceremony.
+ #[inline]
+ #[must_use]
+ pub const fn options(&self) -> &DiscoverableCredentialRequestOptions<'rp_id, 'prf> {
+ &self.0
+ }
+}
+/// Container of a [`NonDiscoverableCredentialRequestOptions`] that has been used to start the authentication
+/// ceremony. This gets sent to the client ASAP.
+#[derive(Debug)]
+pub struct NonDiscoverableAuthenticationClientState<'rp_id, 'prf>(
+ NonDiscoverableCredentialRequestOptions<'rp_id, 'prf>,
+);
+impl<'rp_id, 'prf> NonDiscoverableAuthenticationClientState<'rp_id, 'prf> {
+ /// Returns the `NonDiscoverableCredentialRequestOptions` that was used to start an authentication ceremony.
+ #[inline]
+ #[must_use]
+ pub const fn options(&self) -> &NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
&self.0
}
}
@@ -832,15 +935,16 @@ impl Default for AuthenticatorAttachmentEnforcement {
Self::Ignore(false)
}
}
-/// Additional verification options to perform in [`AuthenticationServerState::verify`].
+/// Additional verification options to perform in [`DiscoverableAuthenticationServerState::verify`] and
+/// [`NonDiscoverableAuthenticationServerState::verify`].
#[derive(Clone, Copy, Debug)]
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 [`AuthenticationServerState::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.
+ /// 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.
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).
@@ -853,7 +957,8 @@ pub struct AuthenticationVerificationOptions<'origins, 'top_origins, O, T> {
///
/// Note that `None` is _not_ the same as `Some(BackupReq::None)` as the latter indicates that any [`Backup`]
/// is allowed. This is rarely what you want; instead, `None` indicates that [`BackupReq::from`] applied to
- /// [`DynamicState::backup`] will be used in [`AuthenticationServerState::verify`].
+ /// [`DynamicState::backup`] will be used in [`DiscoverableAuthenticationServerState::verify`] and
+ /// [`NonDiscoverableAuthenticationServerState::verify`] and
pub backup_requirement: Option<BackupReq>,
/// Error when unsolicited extensions are sent back iff `true`.
pub error_on_unsolicited_extensions: bool,
@@ -890,26 +995,176 @@ impl<O, T> Default for AuthenticationVerificationOptions<'_, '_, O, T> {
}
}
}
-// This is essentially the `PublicKeyCredentialRequestOptions` used to create it; however to reduce
-// memory usage, we remove all unnecessary data making an instance of this 64 bytes in size when
-// `Self::allow_credentials` is empty on `x86_64-unknown-linux-gnu` platforms.
+// 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.
//
-// The total memory used is dependent on the number of `AllowedCredential`s and the size of each `CredentialId`.
-// To be exact, it is the following:
-// 64 + i(32 + 56n + Σj_k from k=0 to k=m-1) where i is 0 iff `AllowedCredentials` has capacity 0; otherwise 1,
-// n is `AllowedCredentials` capacity, j_k is the kth `CredentialId` in `AllowedCredentials` and `m` is
-// `AllowedCredentials::len`.
/// State needed to be saved when beginning the authentication ceremony.
///
-/// Saves the necessary information associated with the [`PublicKeyCredentialRequestOptions`] used to create it
-/// via [`PublicKeyCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be
+/// Saves the necessary information associated with the [`DiscoverableCredentialRequestOptions`] used to create it
+/// via [`DiscoverableCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be
/// performed with [`Self::verify`].
///
-/// `AuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the correct
-/// `AuthenticationServerState` associated with an [`Authentication`], one should use its corresponding
-/// [`Authentication::challenge`].
+/// `DiscoverableAuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the correct
+/// `DiscoverableAuthenticationServerState` associated with a [`DiscoverableAuthentication`], one should use its
+/// corresponding [`DiscoverableAuthentication::challenge`].
#[derive(Debug)]
-pub struct AuthenticationServerState {
+pub struct DiscoverableAuthenticationServerState(AuthenticationServerState);
+impl DiscoverableAuthenticationServerState {
+ /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true`
+ /// iff `cred` was mutated.
+ ///
+ /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the
+ /// ceremony.
+ ///
+ /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)`
+ /// is returned.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `response` is not valid according to the
+ /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any
+ /// of the settings in `options`.
+ #[inline]
+ pub fn verify<
+ 'a,
+ 'cred_id,
+ 'user,
+ const USER_LEN: usize,
+ O: PartialEq<Origin<'a>>,
+ T: PartialEq<Origin<'a>>,
+ EdKey: AsRef<[u8]>,
+ P256Key: AsRef<[u8]>,
+ P384Key: AsRef<[u8]>,
+ RsaKey: AsRef<[u8]>,
+ >(
+ self,
+ rp_id: &RpId,
+ response: &'a DiscoverableAuthentication<USER_LEN>,
+ cred: &mut AuthenticatedCredential<
+ 'cred_id,
+ 'user,
+ USER_LEN,
+ CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>,
+ >,
+ options: &AuthenticationVerificationOptions<'_, '_, O, T>,
+ ) -> Result<bool, AuthCeremonyErr> {
+ // Step 6 item 2.
+ 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)
+ } else {
+ Err(AuthCeremonyErr::CredentialIdMismatch)
+ }
+ } else {
+ Err(AuthCeremonyErr::UserHandleMismatch)
+ }
+ }
+ #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
+ fn is_eq(&self, other: &Self) -> bool {
+ self.0.is_eq(&other.0)
+ }
+}
+// This is essentially the `NonDiscoverableCredentialRequestOptions` used to create it; however to reduce
+// memory usage, we remove all unnecessary data making an instance of this as small as 80 bytes in size on
+// `x86_64-unknown-linux-gnu` platforms. This does not include the size of each `CredInfo` which should exist
+// elsewhere on the heap but obviously contributes memory overall.
+/// State needed to be saved when beginning the authentication ceremony.
+///
+/// Saves the necessary information associated with the [`NonDiscoverableCredentialRequestOptions`] used to create
+/// it via [`NonDiscoverableCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be
+/// performed with [`Self::verify`].
+///
+/// `NonDiscoverableAuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the
+/// correct `NonDiscoverableAuthenticationServerState` associated with a [`NonDiscoverableAuthentication`], one
+/// should use its corresponding [`NonDiscoverableAuthentication::challenge`].
+#[derive(Debug)]
+pub struct NonDiscoverableAuthenticationServerState {
+ /// Most server state.
+ state: AuthenticationServerState,
+ /// The set of credentials that are allowed.
+ allow_credentials: Vec<CredInfo>,
+}
+impl NonDiscoverableAuthenticationServerState {
+ /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true`
+ /// iff `cred` was mutated.
+ ///
+ /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the
+ /// ceremony.
+ ///
+ /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)`
+ /// is returned.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `response` is not valid according to the
+ /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any
+ /// of the settings in `options`.
+ #[inline]
+ pub fn verify<
+ 'a,
+ 'cred_id,
+ 'user,
+ const USER_LEN: usize,
+ O: PartialEq<Origin<'a>>,
+ T: PartialEq<Origin<'a>>,
+ EdKey: AsRef<[u8]>,
+ P256Key: AsRef<[u8]>,
+ P384Key: AsRef<[u8]>,
+ RsaKey: AsRef<[u8]>,
+ >(
+ self,
+ rp_id: &RpId,
+ response: &'a NonDiscoverableAuthentication<USER_LEN>,
+ cred: &mut AuthenticatedCredential<
+ 'cred_id,
+ 'user,
+ USER_LEN,
+ CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>,
+ >,
+ options: &AuthenticationVerificationOptions<'_, '_, O, T>,
+ ) -> Result<bool, AuthCeremonyErr> {
+ response
+ .response
+ .user_handle()
+ .as_ref()
+ .map_or(Ok(()), |user| {
+ // Step 6 item 1.
+ if *user == cred.user_id() {
+ Ok(())
+ } else {
+ Err(AuthCeremonyErr::UserHandleMismatch)
+ }
+ })
+ .and_then(|()| {
+ self.allow_credentials
+ .iter()
+ // Step 6 item 1.
+ .find(|c| c.id == response.raw_id)
+ .ok_or(AuthCeremonyErr::NoMatchingAllowedCredential)
+ .and_then(|c| {
+ // Step 6 item 1.
+ if c.id == cred.id {
+ self.state
+ .verify(rp_id, response, cred, options, Some(c.ext))
+ } else {
+ Err(AuthCeremonyErr::CredentialIdMismatch)
+ }
+ })
+ })
+ }
+ #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
+ fn is_eq(&self, other: &Self) -> bool {
+ self.state.is_eq(&other.state) && self.allow_credentials == other.allow_credentials
+ }
+}
+// This is essentially the `PublicKeyCredentialRequestOptions` 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.
+/// Shared state used by [`DiscoverableAuthenticationServerState`] and [`NonDiscoverableAuthenticationServerState`].
+#[derive(Debug)]
+struct AuthenticationServerState {
// This is a `SentChallenge` since we need `AuthenticationServerState` to be fetchable after receiving the
// response from the client. This response must obviously be constructable; thus its challenge is a
// `SentChallenge`.
@@ -919,8 +1174,6 @@ pub struct AuthenticationServerState {
// `serializable_server_state` is not enabled.
/// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge).
challenge: SentChallenge,
- /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
- allow_credentials: Vec<CredInfo>,
/// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification).
user_verification: UserVerificationRequirement,
/// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions).
@@ -947,27 +1200,30 @@ impl AuthenticationServerState {
/// Errors iff `response` is not valid according to the
/// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any
/// of the settings in `options`.
- #[inline]
- pub fn verify<
+ fn verify<
'a,
+ 'cred_id,
'user,
+ const USER_LEN: usize,
+ const DISCOVERABLE: bool,
O: PartialEq<Origin<'a>>,
T: PartialEq<Origin<'a>>,
EdKey: AsRef<[u8]>,
P256Key: AsRef<[u8]>,
P384Key: AsRef<[u8]>,
RsaKey: AsRef<[u8]>,
- U: User,
>(
self,
rp_id: &RpId,
- response: &'a Authentication<U>,
+ response: &'a Authentication<USER_LEN, DISCOVERABLE>,
cred: &mut AuthenticatedCredential<
- 'a,
+ 'cred_id,
'user,
+ USER_LEN,
CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>,
>,
options: &AuthenticationVerificationOptions<'_, '_, O, T>,
+ cred_ext: Option<ServerCredSpecificExtensionInfo>,
) -> Result<bool, AuthCeremonyErr> {
// [Authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion)
// is handled by:
@@ -998,141 +1254,80 @@ impl AuthenticationServerState {
// 24. Below.
// 25. Below.
- if self.allow_credentials.is_empty() {
- // Step 6.
- response
- .response
- .user_handle()
- .is_same(cred.user_id)
- .ok_or(AuthCeremonyErr::MissingUserHandle)
- .and_then(|same| {
- if same {
- if cred.id == response.raw_id {
- Ok(None)
- } else {
- Err(AuthCeremonyErr::CredentialIdMismatch)
- }
- } else {
- Err(AuthCeremonyErr::UserHandleMismatch)
- }
- })
- } else {
- // Steps 5–6.
- self.verify_nondiscoverable(response, cred)
- }
- .and_then(|ext| {
- // Steps 8–21.
- self.partial_validate(
- rp_id,
- response,
- (&cred.static_state.credential_public_key).into(),
- &CeremonyOptions {
- allowed_origins: options.allowed_origins,
- allowed_top_origins: options.allowed_top_origins,
- backup_requirement: options
- .backup_requirement
- .unwrap_or_else(|| BackupReq::from(cred.dynamic_state.backup)),
- #[cfg(feature = "serde_relaxed")]
- client_data_json_relaxed: options.client_data_json_relaxed,
- },
- )
- .map_err(AuthCeremonyErr::from)
- .and_then(|auth_data| {
- options
- .auth_attachment_enforcement
- .validate(
- cred.dynamic_state.authenticator_attachment,
- response.authenticator_attachment,
+ // Steps 8–21.
+ self.partial_validate(
+ rp_id,
+ response,
+ (&cred.static_state.credential_public_key).into(),
+ &CeremonyOptions {
+ allowed_origins: options.allowed_origins,
+ allowed_top_origins: options.allowed_top_origins,
+ backup_requirement: options
+ .backup_requirement
+ .unwrap_or_else(|| BackupReq::from(cred.dynamic_state.backup)),
+ #[cfg(feature = "serde_relaxed")]
+ client_data_json_relaxed: options.client_data_json_relaxed,
+ },
+ )
+ .map_err(AuthCeremonyErr::from)
+ .and_then(|auth_data| {
+ options
+ .auth_attachment_enforcement
+ .validate(
+ cred.dynamic_state.authenticator_attachment,
+ response.authenticator_attachment,
+ )
+ .and_then(|auth_attachment| {
+ // Step 23.
+ validate_extensions(
+ self.extensions,
+ cred_ext,
+ auth_data.extensions(),
+ options.error_on_unsolicited_extensions,
+ cred.static_state.extensions.hmac_secret.unwrap_or_default(),
)
- .and_then(|auth_attachment| {
- // Step 23.
- validate_extensions(
- self.extensions,
- ext,
- auth_data.extensions(),
- options.error_on_unsolicited_extensions,
- cred.static_state.extensions.hmac_secret.unwrap_or_default(),
- )
- .map_err(AuthCeremonyErr::Extension)
- .and_then(|()| {
- // Step 22.
- options
- .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;
- if options.update_uv && flags.user_verified {
- // Step 24 item 3.
- cred.dynamic_state.user_verified = true;
- }
- // Step 24 item 1.
- 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| {
- cred.dynamic_state = prev_dyn_state;
- AuthCeremonyErr::Credential(e)
- })
- .map(|()| prev_dyn_state != cred.dynamic_state)
+ .map_err(AuthCeremonyErr::Extension)
+ .and_then(|()| {
+ // Step 22.
+ options
+ .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;
+ if options.update_uv && flags.user_verified {
+ // Step 24 item 3.
+ cred.dynamic_state.user_verified = true;
+ }
+ // Step 24 item 1.
+ 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| {
+ cred.dynamic_state = prev_dyn_state;
+ AuthCeremonyErr::Credential(e)
})
- })
+ .map(|()| prev_dyn_state != cred.dynamic_state)
+ })
})
- })
+ })
})
}
- /// Retrieves the corresponding [`CredInfo`] used for a non-discoverable request that corresponds to
- /// `response`. Since this is a non-discoverable request, one must have an external way of identifying the
- /// `UserHandle`.
- ///
- /// This MUST be called iff a non-discoverable request was sent to the client (e.g.,
- /// [`PublicKeyCredentialRequestOptions::second_factor`]).
- ///
- /// # Errors
- ///
- /// Errors iff [`AuthenticatedCredential::user_handle`] does not match [`Authentication::user_handle`] or
- /// [`PublicKeyCredentialRequestOptions::allow_credentials`] does not have a [`CredInfo`] such that
- /// [`CredInfo::id`] matches [`Authentication::raw_id`].
- fn verify_nondiscoverable<'a, PublicKey, U: User>(
- &self,
- response: &'a Authentication<U>,
- cred: &AuthenticatedCredential<'a, '_, PublicKey>,
- ) -> Result<Option<ServerCredSpecificExtensionInfo>, AuthCeremonyErr> {
- response
- .response
- .user_handle()
- .is_same(cred.user_id())
- .map_or(Ok(()), |same| {
- if same {
- Ok(())
- } else {
- Err(AuthCeremonyErr::UserHandleMismatch)
- }
- })
- .and_then(|()| {
- self.allow_credentials
- .iter()
- .find(|c| c.id == response.raw_id)
- .ok_or(AuthCeremonyErr::NoMatchingAllowedCredential)
- .map(|c| Some(c.ext))
- })
- }
#[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
fn is_eq(&self, other: &Self) -> bool {
self.challenge == other.challenge
- && self.allow_credentials == other.allow_credentials
&& self.user_verification == other.user_verification
&& self.extensions == other.extensions
&& self.expiration == other.expiration
}
}
-impl ServerState for AuthenticationServerState {
+impl TimedCeremony for AuthenticationServerState {
#[cfg(any(doc, not(feature = "serializable_server_state")))]
#[inline]
fn expiration(&self) -> Instant {
@@ -1148,8 +1343,42 @@ impl ServerState for AuthenticationServerState {
self.challenge
}
}
-impl<User> Ceremony<User> for AuthenticationServerState {
- type R = Authentication<User>;
+impl TimedCeremony for DiscoverableAuthenticationServerState {
+ #[cfg(any(doc, not(feature = "serializable_server_state")))]
+ #[inline]
+ fn expiration(&self) -> Instant {
+ self.0.expiration()
+ }
+ #[cfg(all(not(doc), feature = "serializable_server_state"))]
+ #[inline]
+ 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")))]
+ #[inline]
+ fn expiration(&self) -> Instant {
+ self.state.expiration()
+ }
+ #[cfg(all(not(doc), feature = "serializable_server_state"))]
+ #[inline]
+ 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
+{
+ type R = Authentication<USER_LEN, DISCOVERABLE>;
fn rand_challenge(&self) -> SentChallenge {
self.challenge
}
@@ -1208,6 +1437,94 @@ impl Ord for AuthenticationServerState {
self.challenge.cmp(&other.challenge)
}
}
+impl Borrow<SentChallenge> for DiscoverableAuthenticationServerState {
+ #[inline]
+ fn borrow(&self) -> &SentChallenge {
+ self.0.borrow()
+ }
+}
+impl PartialEq for DiscoverableAuthenticationServerState {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.0 == other.0
+ }
+}
+impl PartialEq<&Self> for DiscoverableAuthenticationServerState {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ self.0 == other.0
+ }
+}
+impl PartialEq<DiscoverableAuthenticationServerState> for &DiscoverableAuthenticationServerState {
+ #[inline]
+ fn eq(&self, other: &DiscoverableAuthenticationServerState) -> bool {
+ self.0 == other.0
+ }
+}
+impl Eq for DiscoverableAuthenticationServerState {}
+impl Hash for DiscoverableAuthenticationServerState {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.0.hash(state);
+ }
+}
+impl PartialOrd for DiscoverableAuthenticationServerState {
+ #[inline]
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+impl Ord for DiscoverableAuthenticationServerState {
+ #[inline]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.0.cmp(&other.0)
+ }
+}
+impl Borrow<SentChallenge> for NonDiscoverableAuthenticationServerState {
+ #[inline]
+ fn borrow(&self) -> &SentChallenge {
+ self.state.borrow()
+ }
+}
+impl PartialEq for NonDiscoverableAuthenticationServerState {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.state == other.state
+ }
+}
+impl PartialEq<&Self> for NonDiscoverableAuthenticationServerState {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ self.state == other.state
+ }
+}
+impl PartialEq<NonDiscoverableAuthenticationServerState>
+ for &NonDiscoverableAuthenticationServerState
+{
+ #[inline]
+ fn eq(&self, other: &NonDiscoverableAuthenticationServerState) -> bool {
+ self.state == other.state
+ }
+}
+impl Eq for NonDiscoverableAuthenticationServerState {}
+impl Hash for NonDiscoverableAuthenticationServerState {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.state.hash(state);
+ }
+}
+impl PartialOrd for NonDiscoverableAuthenticationServerState {
+ #[inline]
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+impl Ord for NonDiscoverableAuthenticationServerState {
+ #[inline]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.state.cmp(&other.state)
+ }
+}
#[cfg(test)]
mod tests {
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
@@ -1219,10 +1536,11 @@ mod tests {
},
AsciiDomain, AuthTransports,
},
- AllowedCredential, AllowedCredentials, AuthenticationServerState, Challenge, CredentialId,
- CredentialSpecificExtension, Credentials as _, Extension, ExtensionReq, PrfInputOwned,
- PublicKeyCredentialDescriptor, PublicKeyCredentialRequestOptions, RpId,
- UserVerificationRequirement,
+ AllowedCredential, AllowedCredentials, Challenge, CredentialId,
+ CredentialSpecificExtension, Credentials as _, DiscoverableAuthenticationServerState,
+ DiscoverableCredentialRequestOptions, Extension, ExtensionReq,
+ NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
+ PrfInputOwned, PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement,
};
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
use rsa::sha2::{Digest as _, Sha256};
@@ -1250,10 +1568,10 @@ mod tests {
}),
},
});
- let mut opts = PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?;
- opts.user_verification = UserVerificationRequirement::Required;
- opts.challenge = Challenge(0);
- opts.extensions = Extension { prf: None };
+ let mut opts = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?;
+ opts.options.user_verification = UserVerificationRequirement::Required;
+ opts.options.challenge = Challenge(0);
+ opts.options.extensions = Extension { prf: None };
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
let mut authenticator_data = Vec::with_capacity(164);
@@ -1408,9 +1726,23 @@ mod tests {
.extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
authenticator_data.truncate(132);
let server = opts.start_ceremony()?.0;
- assert!(server.is_eq(&AuthenticationServerState::decode(
- server.encode()?.as_slice()
- )?));
+ assert!(
+ server.is_eq(&NonDiscoverableAuthenticationServerState::decode(
+ server.encode()?.as_slice()
+ )?)
+ );
+ let mut opts_2 = DiscoverableCredentialRequestOptions::passkey(&rp_id);
+ opts_2.0.challenge = Challenge(0);
+ opts_2.0.extensions = Extension { prf: None };
+ let server_2 = opts_2.start_ceremony()?.0;
+ assert!(
+ server_2.is_eq(&DiscoverableAuthenticationServerState::decode(
+ server_2
+ .encode()
+ .map_err(AggErr::EncodeDiscoverableAuthenticationServerState)?
+ .as_slice()
+ )?)
+ );
Ok(())
}
}
diff --git a/src/request/auth/error.rs b/src/request/auth/error.rs
@@ -1,6 +1,7 @@
#[cfg(doc)]
use super::{
- AllowedCredentials, CredentialSpecificExtension, Extension, PublicKeyCredentialRequestOptions,
+ AllowedCredentials, CredentialSpecificExtension, DiscoverableCredentialRequestOptions,
+ Extension, NonDiscoverableCredentialRequestOptions, PublicKeyCredentialRequestOptions,
UserVerificationRequirement,
};
use core::{
@@ -9,7 +10,7 @@ use core::{
};
#[cfg(doc)]
use std::time::{Instant, SystemTime};
-/// Error returned from [`PublicKeyCredentialRequestOptions::second_factor`] when
+/// Error returned from [`NonDiscoverableCredentialRequestOptions::second_factor`] when
/// [`AllowedCredentials`] is empty.
#[derive(Clone, Copy, Debug)]
pub struct SecondFactorErr;
@@ -20,7 +21,8 @@ impl Display for SecondFactorErr {
}
}
impl Error for SecondFactorErr {}
-/// Error returned by [`PublicKeyCredentialRequestOptions::start_ceremony`].
+/// Error returned by [`DiscoverableCredentialRequestOptions::start_ceremony`]
+/// and [`NonDiscoverableCredentialRequestOptions::start_ceremony`]
#[derive(Clone, Copy, Debug)]
pub enum RequestOptionsErr {
/// Error when [`Extension::prf`] or [`CredentialSpecificExtension::prf`] is [`Some`] but
diff --git a/src/request/auth/ser.rs b/src/request/auth/ser.rs
@@ -1,6 +1,7 @@
use super::{
- AllowedCredential, AllowedCredentials, AuthenticationClientState, Extension, PrfInput,
- PrfInputOwned,
+ AllowedCredential, AllowedCredentials, Credentials as _, DiscoverableAuthenticationClientState,
+ Extension, NonDiscoverableAuthenticationClientState, PrfInput, PrfInputOwned,
+ PublicKeyCredentialRequestOptions,
};
use data_encoding::BASE64URL_NOPAD;
use serde::ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer};
@@ -249,7 +250,118 @@ impl Serialize for ExtensionHelper<'_, '_> {
})
}
}
-impl Serialize for AuthenticationClientState<'_, '_> {
+/// Helper type that peforms the serialization for both [`DiscoverableAuthenticationClientState`] and
+/// [`NonDiscoverableAuthenticationClientState`] and
+struct AuthenticationClientState<'rp_id, 'prf, 'opt, 'cred>(
+ &'opt PublicKeyCredentialRequestOptions<'rp_id, 'prf>,
+ &'cred AllowedCredentials,
+);
+impl Serialize for AuthenticationClientState<'_, '_, '_, '_> {
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("AuthenticationClientState", 9)
+ .and_then(|mut ser| {
+ ser.serialize_field("challenge", &self.0.challenge)
+ .and_then(|()| {
+ ser.serialize_field("timeout", &self.0.timeout)
+ .and_then(|()| {
+ ser.serialize_field("rpId", &self.0.rp_id).and_then(|()| {
+ ser.serialize_field("allowCredentials", &self.1).and_then(
+ |()| {
+ ser.serialize_field(
+ "userVerification",
+ &self.0.user_verification,
+ )
+ .and_then(
+ |()| {
+ ser.serialize_field("hints", &self.0.hints)
+ .and_then(|()| {
+ ser.serialize_field(
+ "extensions",
+ &ExtensionHelper {
+ extension: &self.0.extensions,
+ allow_credentials: self.1,
+ },
+ )
+ .and_then(|()| ser.end())
+ })
+ },
+ )
+ },
+ )
+ })
+ })
+ })
+ })
+ }
+}
+impl Serialize for DiscoverableAuthenticationClientState<'_, '_> {
+ /// Serializes `self` to conform with
+ /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::{
+ /// # request::{
+ /// # auth::{
+ /// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension,
+ /// # PrfInput, PrfInputOwned, DiscoverableCredentialRequestOptions
+ /// # },
+ /// # AsciiDomain, ExtensionReq, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
+ /// # },
+ /// # response::{AuthTransports, CredentialId},
+ /// # };
+ /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ /// let mut options = DiscoverableCredentialRequestOptions::passkey(&rp_id);
+ /// options.0.hints = Hint::SecurityKey;
+ /// options.0.extensions = Extension {
+ /// prf: Some(PrfInput {
+ /// first: [0; 4].as_slice(),
+ /// second: None,
+ /// ext_info: ExtensionReq::Require,
+ /// }),
+ /// };
+ /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap();
+ /// let json = serde_json::json!({
+ /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA",
+ /// "timeout":300000,
+ /// "rpId":"example.com",
+ /// "allowCredentials":[],
+ /// "userVerification":"required",
+ /// "hints":[
+ /// "security-key"
+ /// ],
+ /// "extensions":{
+ /// "prf":{
+ /// "eval":{
+ /// "first":"AAAAAA"
+ /// },
+ /// }
+ /// }
+ /// }).to_string();
+ /// // Since `Challenge`s are randomly generated, we don't know what it will be; thus
+ /// // we test the JSON string for everything except it.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(client_state.get(..14), json.get(..14));
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(client_state.get(36..), json.get(36..));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ AuthenticationClientState(&self.0.0, &AllowedCredentials::with_capacity(0))
+ .serialize(serializer)
+ }
+}
+impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_> {
/// Serializes `self` to conform with
/// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
///
@@ -262,7 +374,7 @@ impl Serialize for AuthenticationClientState<'_, '_> {
/// # request::{
/// # auth::{
/// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension,
- /// # PrfInput, PrfInputOwned, PublicKeyCredentialRequestOptions
+ /// # PrfInput, PrfInputOwned, NonDiscoverableCredentialRequestOptions
/// # },
/// # AsciiDomain, ExtensionReq, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
/// # },
@@ -296,15 +408,17 @@ impl Serialize for AuthenticationClientState<'_, '_> {
/// });
/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let mut options = PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?;
+ /// let mut options = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let opts = options.options();
/// # #[cfg(not(all(feature = "bin", feature = "custom")))]
- /// # let mut options = PublicKeyCredentialRequestOptions::passkey(&rp_id);
- /// options.hints = Hint::SecurityKey;
+ /// # let mut opts = webauthn_rp::DiscoverableCredentialRequestOptions::passkey(&rp_id).0;
+ /// opts.hints = Hint::SecurityKey;
/// // This is actually useless since `CredentialSpecificExtension` takes priority
/// // when the client receives the payload. We set it for illustration purposes only.
/// // If `creds` contained an `AllowedCredential` that didn't set
/// // `CredentialSpecificExtension::prf`, then this would be used for it.
- /// options.extensions = Extension {
+ /// opts.extensions = Extension {
/// prf: Some(PrfInput {
/// first: [0; 4].as_slice(),
/// second: None,
@@ -312,8 +426,8 @@ impl Serialize for AuthenticationClientState<'_, '_> {
/// }),
/// };
/// // Since we are requesting the PRF extension, we must require user verification; otherwise
- /// // `PublicKeyCredentialRequestOptions::start_ceremony` would error.
- /// options.user_verification = UserVerificationRequirement::Required;
+ /// // `NonDiscoverableCredentialRequestOptions::start_ceremony` would error.
+ /// opts.user_verification = UserVerificationRequirement::Required;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap();
/// let json = serde_json::json!({
@@ -358,43 +472,6 @@ impl Serialize for AuthenticationClientState<'_, '_> {
where
S: Serializer,
{
- serializer
- .serialize_struct("AuthenticationClientState", 9)
- .and_then(|mut ser| {
- ser.serialize_field("challenge", &self.0.challenge)
- .and_then(|()| {
- ser.serialize_field("timeout", &self.0.timeout)
- .and_then(|()| {
- ser.serialize_field("rpId", &self.0.rp_id).and_then(|()| {
- ser.serialize_field(
- "allowCredentials",
- &self.0.allow_credentials,
- )
- .and_then(|()| {
- ser.serialize_field(
- "userVerification",
- &self.0.user_verification,
- )
- .and_then(|()| {
- ser.serialize_field("hints", &self.0.hints).and_then(
- |()| {
- ser.serialize_field(
- "extensions",
- &ExtensionHelper {
- extension: &self.0.extensions,
- allow_credentials: &self
- .0
- .allow_credentials,
- },
- )
- .and_then(|()| ser.end())
- },
- )
- })
- })
- })
- })
- })
- })
+ AuthenticationClientState(&self.0.options, &self.0.allow_credentials).serialize(serializer)
}
}
diff --git a/src/request/auth/ser_server_state.rs b/src/request/auth/ser_server_state.rs
@@ -7,12 +7,13 @@ use super::{
super::super::bin::{
Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible,
},
- AuthenticationServerState, CredInfo, CredentialId, ExtensionReq, SentChallenge,
+ AuthenticationServerState, CredInfo, CredentialId, DiscoverableAuthenticationServerState,
+ ExtensionReq, NonDiscoverableAuthenticationServerState, SentChallenge,
ServerCredSpecificExtensionInfo, ServerExtensionInfo, ServerPrfInfo,
SignatureCounterEnforcement, UserVerificationRequirement,
};
#[cfg(doc)]
-use super::{AllowedCredential, PublicKeyCredentialRequestOptions};
+use super::{AllowedCredential, NonDiscoverableCredentialRequestOptions};
use core::{
error::Error,
fmt::{self, Display, Formatter},
@@ -131,18 +132,44 @@ impl<'a> DecodeBuffer<'a> for Vec<CredInfo> {
})
}
}
-/// Error returned from [`AuthenticationServerState::encode`].
+impl EncodeBufferFallible for AuthenticationServerState {
+ type Err = SystemTimeError;
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ self.challenge.encode_into_buffer(buffer);
+ self.user_verification.encode_into_buffer(buffer);
+ self.extensions.encode_into_buffer(buffer);
+ self.expiration.encode_into_buffer(buffer)
+ }
+}
+impl Encode for DiscoverableAuthenticationServerState {
+ type Output<'a>
+ = Vec<u8>
+ where
+ Self: 'a;
+ type Err = SystemTimeError;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ // Length of the anticipated most common output:
+ // * 16 for `SentChallenge`
+ // * 1 for `UserVerificationRequirement`
+ // * 1 or 3 for `ServerExtensionInfo` where we assume 1 is the most common
+ // * 12 for `SystemTime`
+ let mut buffer = Vec::with_capacity(16 + 1 + 1 + 12);
+ self.0.encode_into_buffer(&mut buffer).map(|()| buffer)
+ }
+}
+/// Error returned from [`NonDiscoverableAuthenticationServerState::encode`].
#[derive(Debug)]
-pub enum EncodeAuthenticationServerStateErr {
+pub enum EncodeNonDiscoverableAuthenticationServerStateErr {
/// Variant returned when
- /// [`AuthenticationServerState::expiration`](../struct.AuthenticationServerState.html#method.expiration-1)
+ /// [`NonDiscoverableAuthenticationServerState::expiration`](../struct.AuthenticationServerState.html#method.expiration-1)
/// is before [`UNIX_EPOCH`].
SystemTime(SystemTimeError),
- /// Variant returned when the corresponding [`PublicKeyCredentialRequestOptions::allow_credentials`] has more
+ /// Variant returned when the corresponding [`NonDiscoverableCredentialRequestOptions::allow_credentials`] has more
/// than [`u16::MAX`] [`AllowedCredential`]s.
AllowedCredentialsCount,
}
-impl Display for EncodeAuthenticationServerStateErr {
+impl Display for EncodeNonDiscoverableAuthenticationServerStateErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
@@ -153,10 +180,13 @@ impl Display for EncodeAuthenticationServerStateErr {
}
}
}
-impl Error for EncodeAuthenticationServerStateErr {}
-impl Encode for AuthenticationServerState {
- type Output<'a> = Vec<u8> where Self: 'a;
- type Err = EncodeAuthenticationServerStateErr;
+impl Error for EncodeNonDiscoverableAuthenticationServerStateErr {}
+impl Encode for NonDiscoverableAuthenticationServerState {
+ type Output<'a>
+ = Vec<u8>
+ where
+ Self: 'a;
+ type Err = EncodeNonDiscoverableAuthenticationServerStateErr;
#[inline]
fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
// Length of the anticipated most common output:
@@ -167,66 +197,124 @@ impl Encode for AuthenticationServerState {
// * 1 or 3 for `ServerExtensionInfo` where we assume 1 is the most common
// * 12 for `SystemTime`
let mut buffer = Vec::with_capacity(16 + 2 + 1 + 1 + 12);
- self.challenge.encode_into_buffer(&mut buffer);
- self.allow_credentials
- .as_slice()
+ self.state
.encode_into_buffer(&mut buffer)
- .map_err(|_e| EncodeAuthenticationServerStateErr::AllowedCredentialsCount)
+ .map_err(EncodeNonDiscoverableAuthenticationServerStateErr::SystemTime)
.and_then(|()| {
- self.user_verification.encode_into_buffer(&mut buffer);
- self.extensions.encode_into_buffer(&mut buffer);
- self.expiration
+ self.allow_credentials
+ .as_slice()
.encode_into_buffer(&mut buffer)
- .map_err(EncodeAuthenticationServerStateErr::SystemTime)
+ .map_err(|_e| {
+ EncodeNonDiscoverableAuthenticationServerStateErr::AllowedCredentialsCount
+ })
.map(|()| buffer)
})
}
}
-/// Error returned from [`AuthenticationServerState::decode`].
+impl<'a> DecodeBuffer<'a> for AuthenticationServerState {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ SentChallenge::decode_from_buffer(data).and_then(|challenge| {
+ UserVerificationRequirement::decode_from_buffer(data).and_then(|user_verification| {
+ ServerExtensionInfo::decode_from_buffer(data).and_then(|extensions| {
+ super::validate_discoverable_options_helper(extensions, user_verification)
+ .map_err(|_e| EncDecErr)
+ .and_then(|()| {
+ SystemTime::decode_from_buffer(data).map(|expiration| Self {
+ challenge,
+ user_verification,
+ extensions,
+ expiration,
+ })
+ })
+ })
+ })
+ })
+ }
+}
+/// Error returned from [`DiscoverableAuthenticationServerState::decode`].
#[derive(Clone, Copy, Debug)]
-pub enum DecodeAuthenticationServerStateErr {
- /// Variant returned when there was trailing data after decoding an [`AuthenticationServerState`].
+pub enum DecodeDiscoverableAuthenticationServerStateErr {
+ /// Variant returned when there was trailing data after decoding a [`DiscoverableAuthenticationServerState`].
TrailingData,
/// Variant returned for all other errors.
Other,
}
-impl Display for DecodeAuthenticationServerStateErr {
+impl Display for DecodeDiscoverableAuthenticationServerStateErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
- Self::TrailingData => "trailing data after decoding AuthenticationServerState",
- Self::Other => "AuthenticationServerState could not be decoded",
+ Self::TrailingData => {
+ "trailing data after decoding DiscoverableAuthenticationServerState"
+ }
+ Self::Other => "DiscoverableAuthenticationServerState could not be decoded",
})
}
}
-impl Error for DecodeAuthenticationServerStateErr {}
-impl Decode for AuthenticationServerState {
+impl Error for DecodeDiscoverableAuthenticationServerStateErr {}
+impl Decode for DiscoverableAuthenticationServerState {
type Input<'a> = &'a [u8];
- type Err = DecodeAuthenticationServerStateErr;
+ type Err = DecodeDiscoverableAuthenticationServerStateErr;
#[inline]
fn decode(mut input: Self::Input<'_>) -> Result<Self, Self::Err> {
- SentChallenge::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|challenge| {
- Vec::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|allow_credentials| {
- UserVerificationRequirement::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|user_verification| {
- ServerExtensionInfo::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|extensions| {
- super::validate_options_helper(extensions, user_verification, &allow_credentials).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|()| {
- SystemTime::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|expiration| {
- if input.is_empty() {
- Ok(Self {
- challenge,
- allow_credentials,
- user_verification,
- extensions,
- expiration,
- })
- } else {
- Err(DecodeAuthenticationServerStateErr::TrailingData)
- }
- })
+ AuthenticationServerState::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeDiscoverableAuthenticationServerStateErr::Other)
+ .and_then(|state| {
+ if input.is_empty() {
+ Ok(Self(state))
+ } else {
+ Err(DecodeDiscoverableAuthenticationServerStateErr::TrailingData)
+ }
+ })
+ }
+}
+/// Error returned from [`NonDiscoverableAuthenticationServerState::decode`].
+#[derive(Clone, Copy, Debug)]
+pub enum DecodeNonDiscoverableAuthenticationServerStateErr {
+ /// Variant returned when there was trailing data after decoding a [`NonDiscoverableAuthenticationServerState`].
+ TrailingData,
+ /// Variant returned for all other errors.
+ Other,
+}
+impl Display for DecodeNonDiscoverableAuthenticationServerStateErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::TrailingData => {
+ "trailing data after decoding NonDiscoverableAuthenticationServerState"
+ }
+ Self::Other => "NonDiscoverableAuthenticationServerState could not be decoded",
+ })
+ }
+}
+impl Error for DecodeNonDiscoverableAuthenticationServerStateErr {}
+impl Decode for NonDiscoverableAuthenticationServerState {
+ type Input<'a> = &'a [u8];
+ type Err = DecodeNonDiscoverableAuthenticationServerStateErr;
+ #[inline]
+ fn decode(mut input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ AuthenticationServerState::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeNonDiscoverableAuthenticationServerStateErr::Other)
+ .and_then(|state| {
+ Vec::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeNonDiscoverableAuthenticationServerStateErr::Other)
+ .and_then(|allow_credentials| {
+ super::validate_non_discoverable_options_helper(
+ state.user_verification,
+ allow_credentials.as_slice(),
+ )
+ .map_err(|_e| DecodeNonDiscoverableAuthenticationServerStateErr::Other)
+ .and({
+ if input.is_empty() {
+ Ok(Self {
+ state,
+ allow_credentials,
+ })
+ } else {
+ Err(DecodeNonDiscoverableAuthenticationServerStateErr::TrailingData)
+ }
})
})
- })
})
- })
}
}
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -13,16 +13,13 @@ use super::{
},
},
BackupReq, Ceremony, Challenge, CredentialMediationRequirement, ExtensionInfo, ExtensionReq,
- Hint, Origin, PublicKeyCredentialDescriptor, RpId, SentChallenge, ServerState,
- THREE_HUNDRED_THOUSAND, UserVerificationRequirement,
- register::error::{CreationOptionsErr, NicknameErr, UserHandleErr, UsernameErr},
+ Hint, Origin, PublicKeyCredentialDescriptor, RpId, SentChallenge, THREE_HUNDRED_THOUSAND,
+ TimedCeremony, UserVerificationRequirement,
+ register::error::{CreationOptionsErr, NicknameErr, UsernameErr},
};
#[cfg(doc)]
use crate::{
- request::{
- AsciiDomain, DomainOrigin, Url, auth::AuthenticationServerState,
- auth::PublicKeyCredentialRequestOptions,
- },
+ request::{AsciiDomain, DomainOrigin, Url, auth::PublicKeyCredentialRequestOptions},
response::{AuthTransports, AuthenticatorTransport, Backup, CollectedClientData},
};
use alloc::borrow::Cow;
@@ -733,104 +730,17 @@ pub const USER_HANDLE_MAX_LEN: usize = 64;
/// The minimum number of bytes a [`UserHandle`] can be made of per
/// [WebAuthn](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id).
pub const USER_HANDLE_MIN_LEN: usize = 1;
-#[derive(Clone, Copy, Debug)]
/// 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.
-pub struct UserHandle<T>(T);
-impl<T> UserHandle<T> {
- /// Returns the contained data consuming `self`.
- #[inline]
- pub fn into_inner(self) -> T {
- self.0
- }
- /// Returns the contained data.
- #[inline]
- pub const fn inner(&self) -> &T {
- &self.0
- }
-}
-impl<T: AsRef<[u8]>> UserHandle<T> {
- /// Returns a `UserHandle` containing a `slice`.
- pub(crate) fn as_slice(&self) -> UserHandle<&[u8]> {
- UserHandle(self.0.as_ref())
- }
-}
-#[cfg(any(feature = "bin", feature = "custom"))]
-impl<'a> UserHandle<&'a [u8]> {
- /// Creates a `UserHandle` from a `slice`.
- fn from_slice<'b: 'a>(value: &'b [u8]) -> Result<Self, UserHandleErr> {
- if (USER_HANDLE_MIN_LEN..=USER_HANDLE_MAX_LEN).contains(&value.len()) {
- Ok(Self(value))
- } else {
- Err(UserHandleErr)
- }
- }
-}
-impl UserHandle<Vec<u8>> {
- /// Returns a new `UserHandle` based on `len` randomly-generated [`u8`]s.
- ///
- /// # Errors
- ///
- /// Errors iff `len` is not inclusively between [`USER_HANDLE_MIN_LEN`] and [`USER_HANDLE_MAX_LEN`].
- ///
- /// # Examples
- ///
- /// ```
- /// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MIN_LEN, USER_HANDLE_MAX_LEN};
- /// assert_eq!(
- /// UserHandle::rand(USER_HANDLE_MIN_LEN)
- /// .unwrap_or_else(|_| unreachable!("there is a bug in UserHandle::rand"))
- /// .as_ref()
- /// .len(),
- /// 1
- /// );
- /// // The probability of an all-zero `UserHandle` being generated (assuming a good entropy
- /// // source) is 2^-512 ≈ 7.5 x 10^-155.
- /// assert_ne!(
- /// UserHandle::rand(USER_HANDLE_MAX_LEN)
- /// .unwrap_or_else(|_| unreachable!("there is a bug in UserHandle::rand"))
- /// .as_ref(),
- /// [0; USER_HANDLE_MAX_LEN]
- /// );
- /// assert!(UserHandle::rand(0).is_err());
- /// assert!(UserHandle::rand(65).is_err());
- /// ```
- #[inline]
- pub fn rand(len: usize) -> Result<Self, UserHandleErr> {
- if (USER_HANDLE_MIN_LEN..=USER_HANDLE_MAX_LEN).contains(&len) {
- let mut data = vec![0; len];
- rand::fill(data.as_mut_slice());
- Ok(Self(data))
- } else {
- Err(UserHandleErr)
- }
- }
- /// [Per WebAuthn](https://www.w3.org/TR/webauthn-3/#sctn-user-handle-privacy), user handles should be
- /// a random 64 bytes; thus this is the same as [`Self::rand`] with `len` set to [`USER_HANDLE_MAX_LEN`].
- ///
- /// # Examples
- ///
- /// ```
- /// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MAX_LEN};
- /// // The probability of an all-zero `UserHandle` being generated (assuming a good entropy
- /// // source) is 2^-512 ≈ 7.5 x 10^-155.
- /// assert_ne!(UserHandle::new().as_ref(), [0; USER_HANDLE_MAX_LEN]);
- /// ```
- #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
- #[inline]
- #[must_use]
- pub fn new() -> Self {
- Self::rand(USER_HANDLE_MAX_LEN)
- .unwrap_or_else(|_e| unreachable!("there is a bug in UserHandle::rand"))
- }
-}
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct UserHandle<const LEN: usize>([u8; LEN]);
/// 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.
macro_rules! user {
( $( $x:literal),* ) => {
$(
-impl Default for UserHandle<[u8; $x]> {
+impl Default for UserHandle<$x> {
#[inline]
fn default() -> Self {
let mut data = [0; $x];
@@ -847,7 +757,7 @@ user!(
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> UserHandle<[u8; LEN]>
+impl<const LEN: usize> UserHandle<LEN>
where
Self: Default,
{
@@ -856,9 +766,9 @@ where
/// # Examples
///
/// ```
- /// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MIN_LEN, USER_HANDLE_MAX_LEN};
+ /// # use webauthn_rp::request::register::{UserHandle, UserHandle64, USER_HANDLE_MIN_LEN, USER_HANDLE_MAX_LEN};
/// assert_eq!(
- /// UserHandle::<[u8; USER_HANDLE_MIN_LEN]>::new_rand()
+ /// UserHandle::<USER_HANDLE_MIN_LEN>::new()
/// .as_ref()
/// .len(),
/// 1
@@ -866,178 +776,78 @@ where
/// // The probability of an all-zero `UserHandle` being generated (assuming a good entropy
/// // source) is 2^-512 ≈ 7.5 x 10^-155.
/// assert_ne!(
- /// UserHandle::<[u8; USER_HANDLE_MAX_LEN]>::new_rand().as_ref(),
+ /// UserHandle64::new().as_ref(),
/// [0; USER_HANDLE_MAX_LEN]
/// );
/// ```
#[inline]
#[must_use]
- pub fn new_rand() -> Self {
+ pub fn new() -> Self {
Self::default()
}
}
-impl Default for UserHandle<Vec<u8>> {
- #[inline]
- fn default() -> Self {
- Self::new()
- }
-}
-impl<T: AsRef<[u8]>> AsRef<[u8]> for UserHandle<T> {
+impl<const LEN: usize> AsRef<[u8]> for UserHandle<LEN> {
#[inline]
fn as_ref(&self) -> &[u8] {
- self.0.as_ref()
+ self.0.as_slice()
}
}
-impl<T: Borrow<[u8]>> Borrow<[u8]> for UserHandle<T> {
+impl<const LEN: usize> Borrow<[u8]> for UserHandle<LEN> {
#[inline]
fn borrow(&self) -> &[u8] {
- self.0.borrow()
- }
-}
-impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<[u8; LEN]>> for UserHandle<&'b [u8; LEN]> {
- #[inline]
- fn from(value: &'a UserHandle<[u8; LEN]>) -> Self {
- Self(&value.0)
- }
-}
-impl<'a: 'b, 'b, const LEN: usize> From<UserHandle<&'a [u8; LEN]>> for UserHandle<&'b [u8]> {
- #[inline]
- fn from(value: UserHandle<&'a [u8; LEN]>) -> Self {
- Self(value.0.as_slice())
- }
-}
-impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<[u8; LEN]>> for UserHandle<&'b [u8]> {
- #[inline]
- fn from(value: &'a UserHandle<[u8; LEN]>) -> Self {
- Self(value.0.as_slice())
- }
-}
-impl<'a: 'b, 'b> From<&'a UserHandle<Vec<u8>>> for UserHandle<&'b Vec<u8>> {
- #[inline]
- fn from(value: &'a UserHandle<Vec<u8>>) -> Self {
- Self(&value.0)
- }
-}
-impl<'a: 'b, 'b> From<UserHandle<&'a Vec<u8>>> for UserHandle<&'b [u8]> {
- #[inline]
- fn from(value: UserHandle<&'a Vec<u8>>) -> Self {
- Self(value.0.as_slice())
- }
-}
-impl<'a: 'b, 'b> From<&'a UserHandle<Vec<u8>>> for UserHandle<&'b [u8]> {
- #[inline]
- fn from(value: &'a UserHandle<Vec<u8>>) -> Self {
- Self(value.0.as_slice())
- }
-}
-impl From<UserHandle<&[u8]>> for UserHandle<Vec<u8>> {
- #[inline]
- fn from(value: UserHandle<&[u8]>) -> Self {
- Self(value.0.to_owned())
- }
-}
-impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<UserHandle<T>> for UserHandle<T2> {
- #[inline]
- fn eq(&self, other: &UserHandle<T>) -> bool {
- self.0 == other.0
- }
-}
-impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<UserHandle<T>> for &UserHandle<T2> {
- #[inline]
- fn eq(&self, other: &UserHandle<T>) -> bool {
- **self == *other
- }
-}
-impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&UserHandle<T>> for UserHandle<T2> {
- #[inline]
- fn eq(&self, other: &&UserHandle<T>) -> bool {
- *self == **other
- }
-}
-impl<T: Eq> Eq for UserHandle<T> {}
-impl<T: Hash> Hash for UserHandle<T> {
- #[inline]
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.0.hash(state);
+ self.0.as_slice()
}
}
/// `UserHandle` that is based on the [spec recommendation](https://www.w3.org/TR/webauthn-3/#user-handle).
-pub type UserHandle64 = UserHandle<[u8; USER_HANDLE_MAX_LEN]>;
+pub type UserHandle64 = UserHandle<USER_HANDLE_MAX_LEN>;
/// `UserHandle` that is based on 16 bytes.
///
/// 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<[u8; 16]>;
-/// Unifies `Option<UserHandle<T>>` and [`UserHandle`] such that both can be used for
-/// [`AuthenticationServerState::verify`].
-///
-/// This `trait` is sealed and cannot be implemented for types outside of `webauthn_rp`.
-pub trait User: super::private::Sealed {
- /// Returns `true` iff [`UserHandle`] must exist.
- fn must_exist() -> bool;
- /// Returns `None` iff `self` cannot be compared to `other`.
- /// Returns `Some(true)` iff `self` is equivalent to `other`.
- /// Returns `Some(false)` iff `self` is not equivalent to `other`.
- fn is_same(&self, other: UserHandle<&[u8]>) -> Option<bool>;
-}
-impl<T: AsRef<[u8]>> User for UserHandle<T> {
- #[inline]
- fn must_exist() -> bool {
- true
- }
- #[inline]
- fn is_same(&self, other: UserHandle<&[u8]>) -> Option<bool> {
- Some(self.as_slice() == other)
- }
-}
-impl<T: AsRef<[u8]>> User for Option<UserHandle<T>> {
- #[inline]
- fn must_exist() -> bool {
- false
- }
- #[inline]
- fn is_same(&self, other: UserHandle<&[u8]>) -> Option<bool> {
- self.as_ref().map(|val| val.as_slice() == other)
- }
-}
+pub type UserHandle16 = UserHandle<16>;
/// [The `PublicKeyCredentialUserEntity`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity)
/// sent to the client.
#[derive(Clone, Debug)]
-pub struct PublicKeyCredentialUserEntity<'name, 'display_name, T> {
+pub struct PublicKeyCredentialUserEntity<'name, 'display_name, 'id, const LEN: usize> {
/// [`name`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name).
pub name: Username<'name>,
/// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id).
- pub id: UserHandle<T>,
+ pub id: &'id UserHandle<LEN>,
/// [`displayName`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname).
///
/// `None` iff the display name should be the empty string.
pub display_name: Option<Nickname<'display_name>>,
}
-impl PublicKeyCredentialUserEntity<'_, '_, Vec<u8>> {
- /// Returns a `PublicKeyCredentialUserEntity` 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, 'b>(self) -> PublicKeyCredentialUserEntity<'a, 'b, Vec<u8>> {
- PublicKeyCredentialUserEntity {
- name: self.name.into_owned(),
- id: self.id,
- display_name: self.display_name.map(Nickname::into_owned),
- }
- }
-}
-impl<'a: 'b, 'b, T: AsRef<[u8]>> From<&'a PublicKeyCredentialUserEntity<'_, '_, T>>
- for PublicKeyCredentialUserEntity<'b, 'b, &'b [u8]>
+impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<LEN>>
+ for PublicKeyCredentialUserEntity<'_, '_, 'b, LEN>
{
+ /// Returns a `PublicKeyCredentialUserEntity` with [`Self::name`] set to `"blank"`,
+ /// [`Self::id`] set to `value`, and [`Self::display_name`] set to `None`.
+ ///
+ /// One should let users set their own user name and user display name; however this can technically happen
+ /// _after_ successfully completing the registration ceremony since this information is not used during
+ /// [`RegistrationServerState::verify`]. For example the client can send such information along with
+ /// [`Registration`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{PublicKeyCredentialUserEntity, UserHandle64};
+ /// let user_handle = UserHandle64::new();
+ /// let entity = PublicKeyCredentialUserEntity::from(&user_handle);
+ /// assert_eq!("blank", entity.name.as_ref());
+ /// assert_eq!(user_handle, *entity.id);
+ /// assert!(entity.display_name.is_none());
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
#[inline]
- fn from(value: &'a PublicKeyCredentialUserEntity<'_, '_, T>) -> Self {
+ fn from(value: &'a UserHandle<LEN>) -> Self {
Self {
- name: Username(Cow::Borrowed(&value.name.0)),
- id: UserHandle(value.id.0.as_ref()),
- display_name: value
- .display_name
- .as_ref()
- .map(|v| Nickname(Cow::Borrowed(&v.0))),
+ name: Username::try_from("blank")
+ .unwrap_or_else(|_e| unreachable!("'blank' is no longer a valid Username")),
+ id: value,
+ display_name: None,
}
}
}
@@ -1344,14 +1154,19 @@ const fn validate_options_helper(
/// [`RegistrationClientState`] to the client ASAP. After receiving the newly created [`Registration`], it is
/// validated using [`RegistrationServerState::verify`].
#[derive(Debug)]
-pub struct PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_handle>
-{
+pub struct PublicKeyCredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ const USER_LEN: usize,
+> {
/// [`mediation`](https://www.w3.org/TR/credential-management-1/#dom-credentialcreationoptions-mediation).
pub mediation: CredentialMediationRequirement,
/// [`rp`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-rp).
pub rp_id: &'rp_id RpId,
/// [`user`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-user).
- pub user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, &'user_handle [u8]>,
+ pub user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>,
/// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge).
pub challenge: Challenge,
/// [`pubKeyCredParams`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-pubkeycredparams).
@@ -1369,8 +1184,8 @@ pub struct PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_
/// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions).
pub extensions: Extension,
}
-impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
- PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_handle>
+impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
+ PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_id, USER_LEN>
{
/// Most deployments of passkeys should use this function. Specifically deployments that are both userless and
/// passwordless and desire multi-factor authentication (MFA) to be done entirely on the authenticator. It
@@ -1389,7 +1204,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
/// ```
/// # use webauthn_rp::request::{
/// # register::{
- /// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
+ /// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle64
/// # },
/// # AsciiDomain, RpId, UserVerificationRequirement
/// # };
@@ -1398,7 +1213,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
/// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
/// PublicKeyCredentialUserEntity {
/// name: "archimedes.of.syracuse".try_into()?,
- /// id: (&UserHandle::new()).into(),
+ /// id: &UserHandle64::new(),
/// display_name: Some("Αρχιμήδης ο Συρακούσιος".try_into()?),
/// },
/// Vec::new()
@@ -1409,9 +1224,9 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
/// ```
#[inline]
#[must_use]
- pub fn passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_handle>(
+ pub fn passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>(
rp_id: &'a RpId,
- user: PublicKeyCredentialUserEntity<'b, 'c, &'d [u8]>,
+ user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>,
exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
) -> Self {
Self {
@@ -1433,6 +1248,33 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
},
}
}
+ /// Convenience function for [`Self::passkey`] passing an empty `Vec`.
+ ///
+ /// This MUST only be used when this is the first credential for a user.
+ #[inline]
+ #[must_use]
+ pub fn first_passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>(
+ rp_id: &'a RpId,
+ user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>,
+ ) -> Self {
+ Self::passkey(rp_id, user, Vec::new())
+ }
+ /// Convenience function for [`Self::first_passkey`] passing [`PublicKeyCredentialUserEntity::from`] applied
+ /// to `user_id` for `user`.
+ ///
+ /// This MUST only be used when user information is provided _after_ registration (e.g., when the client
+ /// sends user name and user display name along with [`Registration`]).
+ ///
+ /// Because user information is likely known for existing accounts, this will often only be called during
+ /// greenfield deployments.
+ #[inline]
+ #[must_use]
+ pub fn first_passkey_with_blank_user_info<'a: 'rp_id, 'b: 'user_id>(
+ rp_id: &'a RpId,
+ user_id: &'b UserHandle<USER_LEN>,
+ ) -> Self {
+ Self::first_passkey(rp_id, user_id.into())
+ }
/// Deployments that want to incorporate a "something a user has" factor into a larger multi-factor
/// authentication (MFA) setup. Specifically deployments that are _not_ userless or passwordless. It
/// is important `exclude_credentials` contains the information for _all_ [`RegisteredCredential`]s registered
@@ -1458,14 +1300,14 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
///
/// ```
/// # use webauthn_rp::request::{register::{
- /// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
+ /// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle64
/// # }, AsciiDomain, RpId};
/// assert_eq!(
/// PublicKeyCredentialCreationOptions::second_factor(
/// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
/// PublicKeyCredentialUserEntity {
/// name: "carl.gauss".try_into()?,
- /// id: (&UserHandle::new()).into(),
+ /// id: &UserHandle64::new(),
/// display_name: Some("Johann Carl Friedrich Gauß".try_into()?),
/// },
/// Vec::new()
@@ -1478,9 +1320,9 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
/// ```
#[inline]
#[must_use]
- pub fn second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_handle>(
+ pub fn second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>(
rp_id: &'a RpId,
- user: PublicKeyCredentialUserEntity<'b, 'c, &'d [u8]>,
+ user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>,
exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
) -> Self {
let mut opts = Self::passkey(rp_id, user, exclude_credentials);
@@ -1489,6 +1331,17 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
opts.extensions.cred_protect = CredProtect::None;
opts
}
+ /// Convenience function for [`Self::second_factor`] passing an empty `Vec`.
+ ///
+ /// This MUST only be used when this is the first credential for a user.
+ #[inline]
+ #[must_use]
+ pub fn first_second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_id>(
+ rp_id: &'a RpId,
+ user: PublicKeyCredentialUserEntity<'b, 'c, 'd, USER_LEN>,
+ ) -> Self {
+ Self::second_factor(rp_id, user, Vec::new())
+ }
/// Begins the [registration ceremony](https://www.w3.org/TR/webauthn-3/#registration-ceremony) consuming
/// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `RegistrationClientState` MUST be
/// sent ASAP. In order to complete registration, the returned `RegistrationServerState` MUST be saved so that
@@ -1504,9 +1357,9 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
/// # #[cfg(not(feature = "serializable_server_state"))]
/// # use std::time::Instant;
/// # #[cfg(not(feature = "serializable_server_state"))]
- /// # use webauthn_rp::request::ServerState;
+ /// # use webauthn_rp::request::TimedCeremony as _;
/// # use webauthn_rp::request::{
- /// # register::{PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle},
+ /// # register::{PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle64},
/// # AsciiDomain, RpId
/// # };
/// # #[cfg(not(feature = "serializable_server_state"))]
@@ -1515,7 +1368,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
/// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
/// PublicKeyCredentialUserEntity {
/// name: "bernard.riemann".try_into()?,
- /// id: (&UserHandle::new()).into(),
+ /// id: &UserHandle64::new(),
/// display_name: Some("Georg Friedrich Bernhard Riemann".try_into()?)
/// },
/// Vec::new()
@@ -1528,8 +1381,8 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
mut self,
) -> Result<
(
- RegistrationServerState,
- RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_handle>,
+ RegistrationServerState<USER_LEN>,
+ RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_id, USER_LEN>,
),
CreationOptionsErr,
> {
@@ -1554,6 +1407,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
authenticator_selection: self.authenticator_selection,
extensions: self.extensions,
expiration,
+ user_id: *self.user.id,
},
RegistrationClientState(self),
)
@@ -1564,11 +1418,15 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
/// Container of a [`PublicKeyCredentialCreationOptions`] that has been used to start the registration ceremony.
/// This gets sent to the client ASAP.
#[derive(Debug)]
-pub struct RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_handle>(
- PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_handle>,
-);
-impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
- RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_handle>
+pub struct RegistrationClientState<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ const USER_LEN: usize,
+>(PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_id, USER_LEN>);
+impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
+ RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_id, USER_LEN>
{
/// Returns the `PublicKeyCredentialCreationOptions` that was used to start a registration ceremony.
///
@@ -1577,14 +1435,14 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
/// ```
/// # use webauthn_rp::request::{register::{
/// # CoseAlgorithmIdentifiers, PublicKeyCredentialCreationOptions,
- /// # PublicKeyCredentialUserEntity, UserHandle
+ /// # PublicKeyCredentialUserEntity, UserHandle64,
/// # }, AsciiDomain, RpId};
/// assert_eq!(
/// PublicKeyCredentialCreationOptions::passkey(
/// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
/// PublicKeyCredentialUserEntity {
/// name: "david.hilbert".try_into()?,
- /// id: (&UserHandle::new()).into(),
+ /// id: &UserHandle64::new(),
/// display_name: Some("David Hilbert".try_into()?)
/// },
/// Vec::new()
@@ -1601,8 +1459,13 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
#[must_use]
pub const fn options(
&self,
- ) -> &PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_handle>
- {
+ ) -> &PublicKeyCredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ USER_LEN,
+ > {
&self.0
}
}
@@ -1665,7 +1528,7 @@ impl<O, T> Default for RegistrationVerificationOptions<'_, '_, O, T> {
/// `RegistrationServerState` associated with a [`Registration`], one should use its corresponding
/// [`Registration::challenge`].
#[derive(Debug)]
-pub struct RegistrationServerState {
+pub struct RegistrationServerState<const USER_LEN: usize> {
/// [`mediation`](https://www.w3.org/TR/credential-management-1/#dom-credentialcreationoptions-mediation).
mediation: CredentialMediationRequirement,
// This is a `SentChallenge` since we need `RegistrationServerState` to be fetchable after receiving the
@@ -1690,13 +1553,15 @@ pub struct RegistrationServerState {
/// `SystemTime` the ceremony expires.
#[cfg(feature = "serializable_server_state")]
expiration: SystemTime,
+ /// User handle.
+ user_id: UserHandle<USER_LEN>,
}
-impl RegistrationServerState {
+impl<const USER_LEN: usize> RegistrationServerState<USER_LEN> {
/// Verifies `response` is valid based on `self` consuming `self` and returning a `RegisteredCredential` that
- /// borrows the necessary data from `response` as well as borrowing `user_handle`.
+ /// borrows the necessary data from `response`.
///
- /// `rp_id` and `user_handle` MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] and
- /// [`PublicKeyCredentialUserEntity::id`] used when starting the ceremony.
+ /// `rp_id` MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] used when starting the
+ /// ceremony.
///
/// It is _essential_ to ensure [`RegisteredCredential::id`] has not been previously registered; if
/// so, the ceremony SHOULD be aborted and a failure reported. When saving `RegisteredCredential`, one may
@@ -1711,13 +1576,12 @@ impl RegistrationServerState {
/// [registration ceremony criteria](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential)
/// or violates any of the settings in `options`.
#[inline]
- pub fn verify<'a, 'b, O: PartialEq<Origin<'b>>, T: PartialEq<Origin<'b>>>(
+ pub fn verify<'a, O: PartialEq<Origin<'a>>, T: PartialEq<Origin<'a>>>(
self,
rp_id: &RpId,
- user_handle: UserHandle<&'a [u8]>,
- response: &'b Registration,
+ response: &'a Registration,
options: &RegistrationVerificationOptions<'_, '_, O, T>,
- ) -> Result<RegisteredCredential<'b, 'a>, RegCeremonyErr> {
+ ) -> Result<RegisteredCredential<'a, USER_LEN>, RegCeremonyErr> {
// [Registration ceremony](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential)
// is handled by:
//
@@ -1787,7 +1651,7 @@ impl RegistrationServerState {
RegisteredCredential::new(
attested_credential_data.credential_id,
response.response.transports(),
- user_handle,
+ self.user_id,
StaticState {
credential_public_key: attested_credential_data
.credential_public_key,
@@ -1833,7 +1697,9 @@ impl RegistrationServerState {
}
})
}
- #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
+}
+#[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
+impl<const USER_LEN: usize> RegistrationServerState<USER_LEN> {
fn is_eq(&self, other: &Self) -> bool {
self.mediation == other.mediation
&& self.challenge == other.challenge
@@ -1841,9 +1707,10 @@ impl RegistrationServerState {
&& self.authenticator_selection == other.authenticator_selection
&& self.extensions == other.extensions
&& self.expiration == other.expiration
+ && self.user_id == other.user_id
}
}
-impl ServerState for RegistrationServerState {
+impl<const USER_LEN: usize> TimedCeremony for RegistrationServerState<USER_LEN> {
#[cfg(any(doc, not(feature = "serializable_server_state")))]
#[inline]
fn expiration(&self) -> Instant {
@@ -1859,7 +1726,7 @@ impl ServerState for RegistrationServerState {
self.challenge
}
}
-impl Ceremony<()> for RegistrationServerState {
+impl<const USER_LEN: usize> Ceremony<USER_LEN, false> for RegistrationServerState<USER_LEN> {
type R = Registration;
fn rand_challenge(&self) -> SentChallenge {
self.challenge
@@ -1876,44 +1743,46 @@ impl Ceremony<()> for RegistrationServerState {
self.authenticator_selection.user_verification
}
}
-impl Borrow<SentChallenge> for RegistrationServerState {
+impl<const USER_LEN: usize> Borrow<SentChallenge> for RegistrationServerState<USER_LEN> {
#[inline]
fn borrow(&self) -> &SentChallenge {
&self.challenge
}
}
-impl PartialEq for RegistrationServerState {
+impl<const USER_LEN: usize> PartialEq for RegistrationServerState<USER_LEN> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.challenge == other.challenge
}
}
-impl PartialEq<&Self> for RegistrationServerState {
+impl<const USER_LEN: usize> PartialEq<&Self> for RegistrationServerState<USER_LEN> {
#[inline]
fn eq(&self, other: &&Self) -> bool {
*self == **other
}
}
-impl PartialEq<RegistrationServerState> for &RegistrationServerState {
+impl<const USER_LEN: usize> PartialEq<RegistrationServerState<USER_LEN>>
+ for &RegistrationServerState<USER_LEN>
+{
#[inline]
- fn eq(&self, other: &RegistrationServerState) -> bool {
+ fn eq(&self, other: &RegistrationServerState<USER_LEN>) -> bool {
**self == *other
}
}
-impl Eq for RegistrationServerState {}
-impl Hash for RegistrationServerState {
+impl<const USER_LEN: usize> Eq for RegistrationServerState<USER_LEN> {}
+impl<const USER_LEN: usize> Hash for RegistrationServerState<USER_LEN> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.challenge.hash(state);
}
}
-impl PartialOrd for RegistrationServerState {
+impl<const USER_LEN: usize> PartialOrd for RegistrationServerState<USER_LEN> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
-impl Ord for RegistrationServerState {
+impl<const USER_LEN: usize> Ord for RegistrationServerState<USER_LEN> {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
self.challenge.cmp(&other.challenge)
@@ -1955,12 +1824,12 @@ mod tests {
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
fn ed25519_reg_ser() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- let id = UserHandle::try_from([0; 1].as_slice())?;
+ let id = UserHandle::from([0; 1]);
let mut opts = PublicKeyCredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
- id,
+ id: &id,
display_name: None,
},
Vec::new(),
@@ -2277,9 +2146,14 @@ mod tests {
attestation_object[32..96].copy_from_slice(sig.to_bytes().as_slice());
attestation_object.truncate(261);
let server = opts.start_ceremony()?.0;
- assert!(server.is_eq(&RegistrationServerState::decode(
- server.encode()?.as_slice()
- )?));
+ assert!(
+ server.is_eq(&RegistrationServerState::decode(
+ server
+ .encode()
+ .map_err(AggErr::EncodeRegistrationServerState)?
+ .as_slice()
+ )?)
+ );
Ok(())
}
}
diff --git a/src/request/register/bin.rs b/src/request/register/bin.rs
@@ -1,7 +1,7 @@
extern crate alloc;
use super::{
super::super::bin::{Decode, Encode},
- Nickname, NicknameErr, UserHandle, UserHandleErr, Username, UsernameErr,
+ Nickname, NicknameErr, UserHandle, Username, UsernameErr,
};
use alloc::borrow::Cow;
use core::{
@@ -9,37 +9,18 @@ use core::{
error::Error,
fmt::{self, Display, Formatter},
};
-impl<T: AsRef<[u8]>> Encode for UserHandle<T> {
+impl<const LEN: usize> Encode for UserHandle<LEN> {
type Output<'a>
- = &'a [u8]
+ = [u8; LEN]
where
Self: 'a;
type Err = Infallible;
#[inline]
fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
- Ok(self.as_ref())
- }
-}
-impl Decode for UserHandle<Vec<u8>> {
- type Input<'a> = Vec<u8>;
- type Err = UserHandleErr;
- #[inline]
- fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
- match UserHandle::<&[u8]>::from_slice(input.as_slice()) {
- Ok(_) => Ok(Self(input)),
- Err(e) => Err(e),
- }
- }
-}
-impl<'b> Decode for UserHandle<&'b [u8]> {
- type Input<'a> = &'b [u8];
- type Err = UserHandleErr;
- #[inline]
- fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
- UserHandle::<&[u8]>::from_slice(input)
+ Ok(self.0)
}
}
-impl<const LEN: usize> Decode for UserHandle<[u8; LEN]>
+impl<const LEN: usize> Decode for UserHandle<LEN>
where
Self: Default,
{
diff --git a/src/request/register/custom.rs b/src/request/register/custom.rs
@@ -1,5 +1,5 @@
-use super::{UserHandle, UserHandleErr};
-impl<const LEN: usize> From<[u8; LEN]> for UserHandle<[u8; LEN]>
+use super::UserHandle;
+impl<const LEN: usize> From<[u8; LEN]> for UserHandle<LEN>
where
Self: Default,
{
@@ -8,20 +8,3 @@ where
Self(value)
}
}
-impl<'a: 'b, 'b> TryFrom<&'a [u8]> for UserHandle<&'b [u8]> {
- type Error = UserHandleErr;
- #[inline]
- fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
- Self::from_slice(value)
- }
-}
-impl TryFrom<Vec<u8>> for UserHandle<Vec<u8>> {
- type Error = UserHandleErr;
- #[inline]
- fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
- match UserHandle::<&[u8]>::try_from(value.as_slice()) {
- Ok(_) => Ok(Self(value)),
- Err(e) => Err(e),
- }
- }
-}
diff --git a/src/request/register/error.rs b/src/request/register/error.rs
@@ -1,8 +1,8 @@
#[cfg(doc)]
use super::{
AuthenticatorSelectionCriteria, CredProtect, Extension, Nickname,
- PublicKeyCredentialCreationOptions, UserHandle, UserVerificationRequirement, Username,
- USER_HANDLE_MAX_LEN, USER_HANDLE_MIN_LEN,
+ PublicKeyCredentialCreationOptions, USER_HANDLE_MAX_LEN, USER_HANDLE_MIN_LEN, UserHandle,
+ UserVerificationRequirement, Username,
};
use core::{
error::Error,
@@ -48,19 +48,6 @@ impl Display for UsernameErr {
}
}
impl Error for UsernameErr {}
-/// Error returned from [`UserHandle::rand`] when a `UserHandle` was attempted to be created
-/// with less than [`USER_HANDLE_MIN_LEN`] or more than [`USER_HANDLE_MAX_LEN`] bytes.
-#[derive(Clone, Copy, Debug)]
-pub struct UserHandleErr;
-impl Display for UserHandleErr {
- #[inline]
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- f.write_str(
- "user handle of less than 16 bytes or more than 64 bytes was attempted to be created",
- )
- }
-}
-impl Error for UserHandleErr {}
/// Error returned by [`PublicKeyCredentialCreationOptions::start_ceremony`].
#[derive(Clone, Copy, Debug)]
pub enum CreationOptionsErr {
diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs
@@ -1,15 +1,9 @@
-#![expect(
- clippy::question_mark_used,
- clippy::unseparated_literal_suffix,
- reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
-)]
extern crate alloc;
use super::{
AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria,
CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, CrossPlatformHint, Extension,
ExtensionInfo, Hint, Nickname, PlatformHint, PublicKeyCredentialUserEntity,
RegistrationClientState, ResidentKeyRequirement, RpId, UserHandle, Username,
- USER_HANDLE_MAX_LEN, USER_HANDLE_MIN_LEN,
};
use alloc::borrow::Cow;
#[cfg(doc)]
@@ -21,7 +15,7 @@ use core::{
};
use data_encoding::BASE64URL_NOPAD;
use serde::{
- de::{Deserialize, Deserializer, Error, MapAccess, Unexpected, Visitor},
+ de::{Deserialize, Deserializer, Error, Unexpected, Visitor},
ser::{Serialize, SerializeSeq as _, SerializeStruct, Serializer},
};
impl Serialize for Nickname<'_> {
@@ -182,7 +176,7 @@ impl Serialize for PublicKeyCredentialRpEntity<'_> {
})
}
}
-impl<T: AsRef<[u8]>> Serialize for UserHandle<T> {
+impl<const LEN: usize> Serialize for UserHandle<LEN> {
/// Serializes `self` to conform with
/// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentityjson-id).
///
@@ -193,10 +187,10 @@ impl<T: AsRef<[u8]>> Serialize for UserHandle<T> {
/// # #[cfg(feature = "custom")]
/// // We create this manually purely for example. One should almost always
/// // randomly generate this (e.g., `UserHandle::new`).
- /// let id = UserHandle::try_from(vec![0])?;
+ /// let id = UserHandle::from([0]);
/// # #[cfg(feature = "custom")]
- /// assert_eq!(serde_json::to_string(&id).unwrap(), r#""AA""#);
- /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// assert_eq!(serde_json::to_string(&id)?, r#""AA""#);
+ /// # Ok::<_, serde_json::Error>(())
/// ```
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -206,9 +200,7 @@ impl<T: AsRef<[u8]>> Serialize for UserHandle<T> {
serializer.serialize_str(BASE64URL_NOPAD.encode(self.0.as_ref()).as_str())
}
}
-/// `"displayName"`.
-const DISPLAY_NAME: &str = "displayName";
-impl<T: AsRef<[u8]>> Serialize for PublicKeyCredentialUserEntity<'_, '_, T> {
+impl<const LEN: usize> Serialize for PublicKeyCredentialUserEntity<'_, '_, '_, LEN> {
/// Serializes `self` to conform with
/// [`PublicKeyCredentialUserEntityJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentityjson).
///
@@ -219,15 +211,14 @@ impl<T: AsRef<[u8]>> Serialize for PublicKeyCredentialUserEntity<'_, '_, T> {
/// # #[cfg(feature = "custom")]
/// // We create this manually purely for example. One should almost always
/// // randomly generate this (e.g., `UserHandle::new`).
- /// let id = UserHandle::try_from(vec![0])?;
+ /// let id = UserHandle::from([0]);
/// # #[cfg(feature = "custom")]
/// assert_eq!(
/// serde_json::to_string(&PublicKeyCredentialUserEntity {
/// name: "georg.cantor".try_into()?,
- /// id: id.clone(),
+ /// id: &id,
/// display_name: Some("Гео́рг Ка́нтор".try_into()?),
- /// })
- /// .unwrap(),
+ /// }).unwrap(),
/// r#"{"name":"georg.cantor","id":"AA","displayName":"Гео́рг Ка́нтор"}"#
/// );
/// // The display name gets serialized as an empty string
@@ -236,10 +227,9 @@ impl<T: AsRef<[u8]>> Serialize for PublicKeyCredentialUserEntity<'_, '_, T> {
/// assert_eq!(
/// serde_json::to_string(&PublicKeyCredentialUserEntity {
/// name: "georg.cantor".try_into()?,
- /// id,
+ /// id: &id,
/// display_name: None,
- /// })
- /// .unwrap(),
+ /// }).unwrap(),
/// r#"{"name":"georg.cantor","id":"AA","displayName":""}"#
/// );
/// # Ok::<_, webauthn_rp::AggErr>(())
@@ -255,7 +245,7 @@ impl<T: AsRef<[u8]>> Serialize for PublicKeyCredentialUserEntity<'_, '_, T> {
ser.serialize_field(NAME, &self.name).and_then(|()| {
ser.serialize_field(ID, &self.id).and_then(|()| {
ser.serialize_field(
- DISPLAY_NAME,
+ "displayName",
self.display_name.as_ref().map_or("", |val| val.as_ref()),
)
.and_then(|()| ser.end())
@@ -556,7 +546,7 @@ impl Serialize for Extension {
})
}
}
-impl Serialize for RegistrationClientState<'_, '_, '_, '_> {
+impl<const USER_LEN: usize> Serialize for RegistrationClientState<'_, '_, '_, '_, USER_LEN> {
/// Serializes `self` to conform with
/// [`PublicKeyCredentialCreationOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptionsjson).
///
@@ -568,7 +558,7 @@ impl Serialize for RegistrationClientState<'_, '_, '_, '_> {
/// # use webauthn_rp::{
/// # request::{
/// # register::{
- /// # AuthenticatorAttachmentReq, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
+ /// # UserHandle64, AuthenticatorAttachmentReq, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
/// # },
/// # AsciiDomain, ExtensionInfo, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
/// # },
@@ -592,8 +582,8 @@ impl Serialize for RegistrationClientState<'_, '_, '_, '_> {
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// creds.push(PublicKeyCredentialDescriptor { id, transports });
/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
- /// let user_handle = UserHandle::new();
- /// let mut options = PublicKeyCredentialCreationOptions::passkey(&rp_id, PublicKeyCredentialUserEntity { name: "pierre.de.fermat".try_into()?, id: (&user_handle).into(), display_name: Some("Pierre de Fermat".try_into()?) }, creds);
+ /// 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));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
@@ -804,56 +794,7 @@ impl<'de: 'a, 'a> Deserialize<'de> for Username<'a> {
deserializer.deserialize_str(UsernameVisitor(PhantomData))
}
}
-impl<'de> Deserialize<'de> for UserHandle<Vec<u8>> {
- /// Deserializes [`prim@str`] based on
- /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-userhandle).
- ///
- /// # Examples
- ///
- /// ```
- /// # use webauthn_rp::request::register::UserHandle;
- /// # #[cfg(feature = "custom")]
- /// assert_eq!(
- /// serde_json::from_str::<UserHandle<Vec<u8>>>(r#""AA""#).unwrap(),
- /// UserHandle::try_from(vec![0; 1])?
- /// );
- /// # Ok::<_, webauthn_rp::AggErr>(())
- ///```
- #[inline]
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- /// `Visitor` for `UserHandle`.
- struct UserHandleVisitor;
- impl Visitor<'_> for UserHandleVisitor {
- type Value = UserHandle<Vec<u8>>;
- fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- formatter.write_str("UserHandle")
- }
- #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
- fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
- where
- E: Error,
- {
- // Any value between `USER_HANDLE_MIN_LEN` and `USER_HANDLE_MAX_LEN` can be base64url encoded
- // without fear since that range is just 1 to 64, and 4/3 of 64 is less than `usize::MAX`.
- if (crate::base64url_nopad_len(USER_HANDLE_MIN_LEN).unwrap_or_else(|| unreachable!("there is a bug in webauthn_rp::base64url_nopad_len"))..=crate::base64url_nopad_len(USER_HANDLE_MAX_LEN).unwrap_or_else(|| unreachable!("there is a bug in webauthn_rp::base64url_nopad_len"))).contains(&v.len()) {
- BASE64URL_NOPAD
- .decode(v.as_bytes())
- .map_err(E::custom)
- .map(UserHandle)
- } else {
- Err(E::invalid_value(
- Unexpected::Str(v), &"1 to 64 bytes encoded in base64url without padding"
- ))
- }
- }
- }
- deserializer.deserialize_str(UserHandleVisitor)
- }
-}
-impl<'de, const LEN: usize> Deserialize<'de> for UserHandle<[u8; LEN]>
+impl<'de, const LEN: usize> Deserialize<'de> for UserHandle<LEN>
where
Self: Default,
{
@@ -866,8 +807,8 @@ where
/// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MIN_LEN};
/// # #[cfg(feature = "custom")]
/// assert_eq!(
- /// serde_json::from_str::<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>(r#""AA""#)?,
- /// UserHandle::from([0; USER_HANDLE_MIN_LEN])
+ /// serde_json::from_str::<UserHandle<USER_HANDLE_MIN_LEN>>(r#""AA""#)?,
+ /// UserHandle::from([0])
/// );
/// # Ok::<_, serde_json::Error>(())
///```
@@ -878,11 +819,11 @@ where
{
/// `Visitor` for `UserHandle`.
struct UserHandleVisitor<const L: usize>;
- impl<const L: usize> Visitor<'_> for UserHandleVisitor::<L>
+ impl<const L: usize> Visitor<'_> for UserHandleVisitor<L>
where
- UserHandle<[u8; L]>: Default,
+ UserHandle<L>: Default,
{
- type Value = UserHandle<[u8; L]>;
+ type Value = UserHandle<L>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("UserHandle")
}
@@ -912,197 +853,6 @@ where
deserializer.deserialize_str(UserHandleVisitor)
}
}
-impl<'de: 'name + 'display_name, 'name, 'display_name, T> Deserialize<'de>
- for PublicKeyCredentialUserEntity<'name, 'display_name, T>
-where
- UserHandle<T>: Deserialize<'de>,
-{
- /// Deserializes a `struct` based on
- /// [`PublicKeyCredentialUserEntityJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentityjson).
- ///
- /// # Examples
- ///
- /// ```
- /// # use webauthn_rp::request::register::{Nickname, PublicKeyCredentialUserEntity, USER_HANDLE_MIN_LEN};
- /// assert_eq!(
- /// serde_json::from_str::<PublicKeyCredentialUserEntity<[u8; USER_HANDLE_MIN_LEN]>>(
- /// serde_json::json!({
- /// "name": "pythagoras.of.samos",
- /// "id": "AA",
- /// "displayName": "Πυθαγόρας ο Σάμιος"
- /// })
- /// .to_string()
- /// .as_str()
- /// )?
- /// .display_name
- /// .as_ref()
- /// .map(Nickname::as_ref),
- /// Some("Πυθαγόρας ο Σάμιος")
- /// );
- /// // Display name is `None` iff the empty string was sent.
- /// assert!(
- /// serde_json::from_str::<PublicKeyCredentialUserEntity<Vec<u8>>>(
- /// serde_json::json!({
- /// "name": "pythagoras.of.samos",
- /// "id": "AA",
- /// "displayName": ""
- /// })
- /// .to_string()
- /// .as_str()
- /// )?
- /// .display_name.is_none()
- /// );
- /// # Ok::<_, serde_json::Error>(())
- ///```
- #[expect(clippy::too_many_lines, reason = "116 lines is fine")]
- #[inline]
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- /// `Visitor` for `PublicKeyCredentialUserEntity`.
- #[expect(clippy::type_complexity, reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable")]
- struct PublicKeyCredentialUserEntityVisitor<'a, 'b, U>(PhantomData<fn() -> (&'a (), &'b (), U)>);
- impl<'d: 'a + 'b, 'a, 'b, U> Visitor<'d> for PublicKeyCredentialUserEntityVisitor<'a, 'b, U>
- where
- UserHandle<U>: Deserialize<'d>,
- {
- type Value = PublicKeyCredentialUserEntity<'a, 'b, U>;
- fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- formatter.write_str("PublicKeyCredentialUserEntity")
- }
- fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
- where
- A: MapAccess<'d>,
- {
- /// Fields for `PublicKeyCredentialUserEntityJSON`.
- enum Field {
- /// `"name"` field.
- Name,
- /// `"id"` field.
- Id,
- /// `"displayName"` field.
- DisplayName,
- }
- impl<'e> Deserialize<'e> for Field {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'e>,
- {
- /// `Visitor` for `Field`.
- struct FieldVisitor;
- impl Visitor<'_> for FieldVisitor {
- type Value = Field;
- fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- write!(formatter, "'{NAME}', '{ID}', or '{DISPLAY_NAME}'")
- }
- fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
- where
- E: Error,
- {
- match v {
- NAME => Ok(Field::Name),
- ID => Ok(Field::Id),
- DISPLAY_NAME => Ok(Field::DisplayName),
- _ => Err(E::unknown_field(v, FIELDS)),
- }
- }
- }
- deserializer.deserialize_identifier(FieldVisitor)
- }
- }
- /// `display_name`.
- struct DisplayName<'c>(Option<Nickname<'c>>);
- impl<'e: 'c, 'c> Deserialize<'e> for DisplayName<'c> {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'e>,
- {
- /// `Visitor` for `DisplayName`.
- struct DisplayNameVisitor<'a>(PhantomData<fn() -> &'a ()>);
- impl<'d: 'a, 'a> Visitor<'d> for DisplayNameVisitor<'a> {
- type Value = DisplayName<'a>;
- fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- formatter.write_str("Nickname or empty string")
- }
- fn visit_borrowed_str<E>(self, v: &'d str) -> Result<Self::Value, E>
- where
- E: Error,
- {
- if v.is_empty() {
- Ok(DisplayName(None))
- } else {
- Nickname::try_from(v)
- .map_err(E::custom)
- .map(|name| DisplayName(Some(name)))
- }
- }
- fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
- where
- E: Error,
- {
- if v.is_empty() {
- Ok(DisplayName(None))
- } else {
- Nickname::try_from(v).map_err(E::custom).map(|name| {
- DisplayName(Some(Nickname(Cow::Owned(name.0.into_owned()))))
- })
- }
- }
- }
- deserializer.deserialize_str(DisplayNameVisitor(PhantomData))
- }
- }
- let mut nam = None;
- let mut ident = None;
- let mut display = None;
- while let Some(key) = map.next_key()? {
- match key {
- Field::Name => {
- if nam.is_some() {
- return Err(Error::duplicate_field(NAME));
- }
- nam = Some(map.next_value()?);
- }
- Field::Id => {
- if ident.is_some() {
- return Err(Error::duplicate_field(ID));
- }
- ident = map.next_value().map(Some)?;
- }
- Field::DisplayName => {
- if display.is_some() {
- return Err(Error::duplicate_field(DISPLAY_NAME));
- }
- display = map.next_value::<DisplayName<'_>>().map(|val| Some(val.0))?;
- }
- }
- }
- nam.ok_or_else(|| Error::missing_field(NAME))
- .and_then(|name| {
- ident
- .ok_or_else(|| Error::missing_field(ID))
- .and_then(|id| {
- display
- .ok_or_else(|| Error::missing_field(DISPLAY_NAME))
- .map(|display_name| PublicKeyCredentialUserEntity {
- name,
- id,
- display_name,
- })
- })
- })
- }
- }
- /// Fields for `PublicKeyCredentialUserEntityJSON`.
- const FIELDS: &[&str; 3] = &[NAME, ID, DISPLAY_NAME];
- deserializer.deserialize_struct(
- "PublicKeyCredentialUserEntity",
- FIELDS,
- PublicKeyCredentialUserEntityVisitor(PhantomData),
- )
- }
-}
impl<'de> Deserialize<'de> for CoseAlgorithmIdentifier {
/// Deserializes [`i16`] based on
/// [COSE Algorithms](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
diff --git a/src/request/register/ser_server_state.rs b/src/request/register/ser_server_state.rs
@@ -8,7 +8,7 @@ use super::{
},
AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, CoseAlgorithmIdentifiers,
CredProtect, CredentialMediationRequirement, CrossPlatformHint, Extension, ExtensionInfo, Hint,
- PlatformHint, RegistrationServerState, ResidentKeyRequirement, SentChallenge,
+ PlatformHint, RegistrationServerState, ResidentKeyRequirement, SentChallenge, UserHandle,
UserVerificationRequirement,
};
use core::{
@@ -186,7 +186,28 @@ impl<'a> DecodeBuffer<'a> for Extension {
})
}
}
-impl Encode for RegistrationServerState {
+impl<const LEN: usize> EncodeBuffer for UserHandle<LEN> {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ buffer.extend_from_slice(self.0.as_slice());
+ }
+}
+impl<'a, const LEN: usize> DecodeBuffer<'a> for UserHandle<LEN>
+where
+ Self: Default,
+{
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ data.split_at_checked(LEN)
+ .ok_or(EncDecErr)
+ .map(|(val_slice, rem)| {
+ *data = rem;
+ let mut val = Self::default();
+ val.0.copy_from_slice(val_slice);
+ val
+ })
+ }
+}
+impl<const USER_LEN: usize> Encode for RegistrationServerState<USER_LEN> {
type Output<'a>
= Vec<u8>
where
@@ -201,15 +222,17 @@ impl Encode for RegistrationServerState {
// * 6 for `AuthenticatorSelectionCriteria`
// * 4–8 for `Extension` where we assume 4 is the most common
// * 12 for `SystemTime`
- let mut buffer = Vec::with_capacity(1 + 16 + 1 + 6 + 4 + 12);
+ // * Variable length for `U` where we assume a 16-byte `UserHandle` array is most common.
+ let mut buffer = Vec::with_capacity(1 + 16 + 1 + 6 + 4 + 12 + 16);
self.mediation.encode_into_buffer(&mut buffer);
self.challenge.encode_into_buffer(&mut buffer);
self.pub_key_cred_params.encode_into_buffer(&mut buffer);
self.authenticator_selection.encode_into_buffer(&mut buffer);
self.extensions.encode_into_buffer(&mut buffer);
- self.expiration
- .encode_into_buffer(&mut buffer)
- .map(|()| buffer)
+ self.expiration.encode_into_buffer(&mut buffer).map(|()| {
+ self.user_id.encode_into_buffer(&mut buffer);
+ buffer
+ })
}
}
/// Error returned from [`RegistrationServerState::decode`].
@@ -230,7 +253,10 @@ impl Display for DecodeRegistrationServerStateErr {
}
}
impl Error for DecodeRegistrationServerStateErr {}
-impl Decode for RegistrationServerState {
+impl<const USER_LEN: usize> Decode for RegistrationServerState<USER_LEN>
+where
+ UserHandle<USER_LEN>: Default,
+{
type Input<'a> = &'a [u8];
type Err = DecodeRegistrationServerStateErr;
#[inline]
@@ -245,11 +271,13 @@ impl Decode for RegistrationServerState {
.map_err(|_e| DecodeRegistrationServerStateErr::Other)
.and_then(|()| {
SystemTime::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|expiration| {
- if input.is_empty() {
- Ok(Self { mediation, challenge, pub_key_cred_params, authenticator_selection, extensions, expiration, })
- } else {
- Err(DecodeRegistrationServerStateErr::TrailingData)
- }
+ UserHandle::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|user_id| {
+ if input.is_empty() {
+ Ok(Self { mediation, challenge, pub_key_cred_params, authenticator_selection, extensions, expiration, user_id, })
+ } else {
+ Err(DecodeRegistrationServerStateErr::TrailingData)
+ }
+ })
})
})
})
diff --git a/src/response.rs b/src/response.rs
@@ -33,9 +33,9 @@ use ser_relaxed::SerdeJsonErr;
/// # #[cfg(not(feature = "serializable_server_state"))]
/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
/// # use webauthn_rp::{
-/// # request::{auth::{error::RequestOptionsErr, AuthenticationClientState, PublicKeyCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN}, AsciiDomain, BackupReq, RpId},
-/// # response::{auth::{error::AuthCeremonyErr, Authentication}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKey, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId},
-/// # AuthenticatedCredential, CredentialErr, PasskeyAuthentication64
+/// # 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},
+/// # AuthenticatedCredential, CredentialErr
/// # };
/// # #[derive(Debug)]
/// # enum E {
@@ -82,32 +82,32 @@ use ser_relaxed::SerdeJsonErr;
/// # #[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) = PublicKeyCredentialRequestOptions::passkey(&rp_id).start_ceremony()?;
+/// 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(feature = "serde")]
-/// let authentication = serde_json::from_str::<PasskeyAuthentication64>(get_authentication_json(client).as_str())?;
+/// let authentication = serde_json::from_str::<DiscoverableAuthentication64>(get_authentication_json(client).as_str())?;
/// # #[cfg(feature = "serde")]
/// let user_handle = authentication.response().user_handle();
/// # #[cfg(feature = "serde")]
-/// let (static_state, dynamic_state) = get_credential(authentication.raw_id(), user_handle.into()).ok_or(E::UnknownCredential)?;
+/// 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.into(), static_state, dynamic_state)?;
+/// 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"))]
/// 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 `AuthenticationClientState` and receive `Authentication` JSON from client.
+/// /// Send `DiscoverableAuthenticationClientState` and receive `DiscoverableAuthentication64` JSON from client.
/// # #[cfg(feature = "serde")]
-/// fn get_authentication_json(client: AuthenticationClientState<'_, '_>) -> String {
+/// fn get_authentication_json(client: DiscoverableAuthenticationClientState<'_, '_>) -> String {
/// // ⋮
/// # let client_data_json = BASE64URL_NOPAD.encode(serde_json::json!({
/// # "type": "webauthn.get",
-/// # "challenge": client.options().challenge,
-/// # "origin": format!("https://{}", client.options().rp_id.as_ref()),
+/// # "challenge": client.options().0.challenge,
+/// # "origin": format!("https://{}", client.options().0.rp_id.as_ref()),
/// # "crossOrigin": false
/// # }).to_string().as_bytes());
/// # serde_json::json!({
@@ -124,7 +124,7 @@ 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: UserHandle<&[u8]>) -> Option<(StaticState<CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>>, DynamicState)> {
+/// 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 }))
/// }
@@ -159,7 +159,7 @@ pub mod error;
/// # #[cfg(not(feature = "serializable_server_state"))]
/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
/// # use webauthn_rp::{
-/// # request::{register::{error::CreationOptionsErr, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomain, PublicKeyCredentialDescriptor, RpId},
+/// # 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,28 +197,32 @@ pub mod error;
/// # Self::RegCeremony(value)
/// # }
/// # }
-/// # #[cfg(not(feature = "serializable_server_state"))]
+/// # #[cfg(all(not(feature = "serializable_server_state"), 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();
-/// let handle = (&user_handle).into();
-/// let user = get_user_entity(handle);
-/// let creds = get_registered_credentials(handle);
+/// # #[cfg(feature = "custom")]
+/// let user = get_user_entity(&user_handle);
+/// # #[cfg(feature = "custom")]
+/// let creds = get_registered_credentials(user_handle);
+/// # #[cfg(feature = "custom")]
/// let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, user, creds).start_ceremony()?;
-/// # #[cfg(not(feature = "serializable_server_state"))]
+/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom"))]
/// assert!(matches!(
/// ceremonies.insert_or_replace_all_expired(server),
/// InsertResult::Success
/// ));
-/// # #[cfg(feature = "serde_relaxed")]
+/// # #[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"))]
-/// insert_cred(ceremonies.take(®istration.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, handle, ®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.
-/// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MAX_LEN]> {
+/// # #[cfg(feature = "custom")]
+/// fn get_user_handle() -> UserHandle64 {
/// // ⋮
-/// # UserHandle::new_rand()
+/// # [0; USER_HANDLE_MAX_LEN].into()
/// }
/// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`.
/// ///
@@ -226,7 +230,8 @@ pub mod error;
/// /// will need to be constructed with `name` and `display_name` passed from the client and `UserHandle::new`
/// /// used for `id`. Once created, this info can be stored such that the entity information
/// /// does not need to be requested for subsequent registrations.
-/// fn get_user_entity(user: UserHandle<&[u8]>) -> PublicKeyCredentialUserEntity<&[u8]> {
+/// # #[cfg(feature = "custom")]
+/// fn get_user_entity(user: &UserHandle<USER_HANDLE_MAX_LEN>) -> PublicKeyCredentialUserEntity<'_, '_, '_, USER_HANDLE_MAX_LEN> {
/// // ⋮
/// # PublicKeyCredentialUserEntity {
/// # name: "foo".try_into().unwrap(),
@@ -236,7 +241,7 @@ pub mod error;
/// }
/// /// Send `RegistrationClientState` and receive `Registration` JSON from client.
/// # #[cfg(feature = "serde")]
-/// fn get_registration_json(client: RegistrationClientState<'_, '_, '_, '_>) -> String {
+/// fn get_registration_json(client: RegistrationClientState<'_, '_, '_, '_, USER_HANDLE_MAX_LEN>) -> String {
/// // ⋮
/// # let client_data_json = BASE64URL_NOPAD.encode(serde_json::json!({
/// # "type": "webauthn.create",
@@ -256,13 +261,13 @@ pub mod error;
/// /// This doesn't need to be called when this is the first credential registered for `user`; instead
/// /// an empty `Vec` should be passed.
/// fn get_registered_credentials(
-/// user: UserHandle<&[u8]>,
+/// user: UserHandle<USER_HANDLE_MAX_LEN>,
/// ) -> Vec<PublicKeyCredentialDescriptor<Vec<u8>>> {
/// // ⋮
/// # Vec::new()
/// }
/// /// Inserts `RegisteredCredential::into_parts` into the database.
-/// fn insert_cred(cred: RegisteredCredential<'_, '_>) {
+/// fn insert_cred(cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>) {
/// // ⋮
/// }
/// # Ok::<_, E>(())
@@ -1591,11 +1596,11 @@ impl From<CeremonyErr<AuthAuthDataErr>> for AuthCeremonyErr {
/// When the client forwards this response to the authenticator, it can remove all credentials that don't have
/// a [`CredentialId`] in [`Self::all_accepted_credential_ids`].
#[derive(Debug)]
-pub struct AllAcceptedCredentialsOptions<'rp, 'user> {
+pub struct AllAcceptedCredentialsOptions<'rp, 'user, const USER_LEN: usize> {
/// [`rpId`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-rpid).
pub rp_id: &'rp RpId,
/// [`userId`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-userid).
- pub user_id: UserHandle<&'user [u8]>,
+ pub user_id: &'user UserHandle<USER_LEN>,
/// [`allAcceptedCredentialIds`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-allacceptedcredentialids).
pub all_accepted_credential_ids: Vec<CredentialId<Vec<u8>>>,
}
@@ -1605,13 +1610,13 @@ pub struct AllAcceptedCredentialsOptions<'rp, 'user> {
/// This can be useful when a user updates their user information on the RP's side but does not do so on the authenticator.
/// When the client forwards this response to the authenticator, it can update the user info for the associated credential.
#[derive(Debug)]
-pub struct CurrentUserDetailsOptions<'rp, 'user_name, 'user_display_name, 'user_handle> {
+pub struct CurrentUserDetailsOptions<'rp_id, 'name, 'display_name, 'id, const LEN: usize> {
/// [`rpId`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-rpid).
- pub rp_id: &'rp RpId,
+ pub rp_id: &'rp_id RpId,
/// [`userId`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-userid),
/// [`name`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-name), and
/// [`displayName`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-displayname).
- pub user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, &'user_handle [u8]>,
+ pub user: PublicKeyCredentialUserEntity<'name, 'display_name, 'id, LEN>,
}
#[cfg(test)]
mod tests {
diff --git a/src/response/auth.rs b/src/response/auth.rs
@@ -1,9 +1,6 @@
#[cfg(feature = "serde_relaxed")]
use self::{
- super::{
- super::request::register::User,
- ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr},
- },
+ super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr},
ser_relaxed::{AuthenticationRelaxed, CustomAuthentication},
};
#[cfg(all(doc, feature = "serde_relaxed"))]
@@ -13,14 +10,15 @@ use super::super::{
AuthenticatedCredential, RegisteredCredential, StaticState,
request::{
Challenge,
- auth::{AuthenticationServerState, PublicKeyCredentialRequestOptions},
+ auth::{
+ DiscoverableAuthenticationServerState, NonDiscoverableAuthenticationServerState,
+ PublicKeyCredentialRequestOptions,
+ },
+ register::{UserHandle16, UserHandle64},
},
};
use super::{
- super::{
- UserHandle,
- request::register::{UserHandle16, UserHandle64},
- },
+ super::{UserHandle, request::register::USER_HANDLE_MAX_LEN},
AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment,
CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor,
LimitedVerificationParser, ParsedAuthData, Response, SentChallenge,
@@ -284,7 +282,7 @@ impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> {
}
/// [`AuthenticatorAssertionResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse).
#[derive(Debug)]
-pub struct AuthenticatorAssertion<U> {
+pub struct AuthenticatorAssertion<const USER_LEN: usize, const DISCOVERABLE: bool> {
/// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
client_data_json: Vec<u8>,
/// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata)
@@ -293,9 +291,11 @@ pub struct AuthenticatorAssertion<U> {
/// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature).
signature: Vec<u8>,
/// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
- user_handle: U,
+ user_handle: Option<UserHandle<USER_LEN>>,
}
-impl<U> AuthenticatorAssertion<U> {
+impl<const USER_LEN: usize, const DISCOVERABLE: bool>
+ AuthenticatorAssertion<USER_LEN, DISCOVERABLE>
+{
/// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
#[inline]
#[must_use]
@@ -323,12 +323,6 @@ impl<U> AuthenticatorAssertion<U> {
pub fn signature(&self) -> &[u8] {
self.signature.as_slice()
}
- /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
- #[inline]
- #[must_use]
- pub const fn user_handle(&self) -> &U {
- &self.user_handle
- }
/// Constructs an instance of `Self` with the contained data.
///
/// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
@@ -337,7 +331,7 @@ impl<U> AuthenticatorAssertion<U> {
client_data_json: Vec<u8>,
mut authenticator_data: Vec<u8>,
signature: Vec<u8>,
- user_handle: U,
+ user_handle: Option<UserHandle<USER_LEN>>,
) -> Self {
authenticator_data
.extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
@@ -349,7 +343,13 @@ impl<U> AuthenticatorAssertion<U> {
}
}
}
-impl<T> AuthenticatorAssertion<Option<UserHandle<T>>> {
+impl<const USER_LEN: usize> AuthenticatorAssertion<USER_LEN, false> {
+ /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
+ #[inline]
+ #[must_use]
+ pub const fn user_handle(&self) -> Option<&UserHandle<USER_LEN>> {
+ self.user_handle.as_ref()
+ }
/// Constructs an instance of `Self` with the contained data.
///
/// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
@@ -360,7 +360,7 @@ impl<T> AuthenticatorAssertion<Option<UserHandle<T>>> {
client_data_json: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
- user_handle: Option<UserHandle<T>>,
+ user_handle: Option<UserHandle<USER_LEN>>,
) -> Self {
Self::new_inner(client_data_json, authenticator_data, signature, user_handle)
}
@@ -381,7 +381,7 @@ impl<T> AuthenticatorAssertion<Option<UserHandle<T>>> {
client_data_json: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
- user_handle: UserHandle<T>,
+ user_handle: UserHandle<USER_LEN>,
) -> Self {
Self::with_optional_user(
client_data_json,
@@ -391,7 +391,16 @@ impl<T> AuthenticatorAssertion<Option<UserHandle<T>>> {
)
}
}
-impl<T> AuthenticatorAssertion<UserHandle<T>> {
+impl<const USER_LEN: usize> AuthenticatorAssertion<USER_LEN, true> {
+ /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
+ #[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
+ #[inline]
+ #[must_use]
+ pub fn user_handle(&self) -> &UserHandle<USER_LEN> {
+ self.user_handle
+ .as_ref()
+ .unwrap_or_else(|| unreachable!("bug in AuthenticatorAssertion<USER_LEN, true>"))
+ }
/// Constructs an instance of `Self` with the contained data.
///
/// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
@@ -402,12 +411,19 @@ impl<T> AuthenticatorAssertion<UserHandle<T>> {
client_data_json: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
- user_handle: UserHandle<T>,
+ user_handle: UserHandle<USER_LEN>,
) -> Self {
- Self::new_inner(client_data_json, authenticator_data, signature, user_handle)
+ Self::new_inner(
+ client_data_json,
+ authenticator_data,
+ signature,
+ Some(user_handle),
+ )
}
}
-impl<U> AuthResponse for AuthenticatorAssertion<U> {
+impl<const USER_LEN: usize, const DISCOVERABLE: bool> AuthResponse
+ for AuthenticatorAssertion<USER_LEN, DISCOVERABLE>
+{
type Auth<'a>
= AuthenticatorData<'a>
where
@@ -423,31 +439,27 @@ impl<U> AuthResponse for AuthenticatorAssertion<U> {
> {
/// Always `panic`s.
#[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
- #[expect(
- clippy::extra_unused_type_parameters,
- reason = "same function signature as when serde_relaxed is enabled"
- )]
#[cfg(not(feature = "serde_relaxed"))]
- fn get_client_collected_data<'b, U: 'b>(_: &'b [u8]) -> ! {
+ fn get_client_collected_data<const LEN: usize, const DISC: bool>(_: &[u8]) -> ! {
unreachable!(
"AuthenticatorAssertion::parse_data_and_verify_sig must be passed false when serde_relaxed is not enabled"
);
}
/// Parses `data` using `CollectedClientData::from_client_data_json_relaxed::<false>`.
#[cfg(feature = "serde_relaxed")]
- fn get_client_collected_data<'b, U: 'b>(
- data: &'b [u8],
+ fn get_client_collected_data<const LEN: usize, const DISC: bool>(
+ data: &[u8],
) -> Result<
- CollectedClientData<'b>,
+ CollectedClientData<'_>,
AuthRespErr<
- <<AuthenticatorAssertion<U> as AuthResponse>::Auth<'b> as AuthDataContainer<'b>>::Err,
+ <<AuthenticatorAssertion<LEN, DISC> as AuthResponse>::Auth<'_> as AuthDataContainer<'_>>::Err,
>,
>{
CollectedClientData::from_client_data_json_relaxed::<false>(data)
.map_err(AuthRespErr::CollectedClientDataRelaxed)
}
if relaxed {
- get_client_collected_data::<'_, U>(self.client_data_json.as_slice())
+ get_client_collected_data::<USER_LEN, DISCOVERABLE>(self.client_data_json.as_slice())
} else {
CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice())
.map_err(AuthRespErr::CollectedClientData)
@@ -516,22 +528,26 @@ impl<U> AuthResponse for AuthenticatorAssertion<U> {
}
}
/// `AuthenticatorAssertion` with a required `UserHandle`.
-pub type PasskeyAuthenticatorAssertion<T> = AuthenticatorAssertion<UserHandle<T>>;
+pub type DiscoverableAuthenticatorAssertion<const USER_LEN: usize> =
+ AuthenticatorAssertion<USER_LEN, true>;
+/// `AuthenticatorAssertion` with an optional `UserHandle`.
+pub type NonDiscoverableAuthenticatorAssertion<const USER_LEN: usize> =
+ AuthenticatorAssertion<USER_LEN, false>;
/// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for authentication ceremonies.
#[expect(
clippy::field_scoped_visibility_modifiers,
reason = "no invariants to uphold"
)]
#[derive(Debug)]
-pub struct Authentication<User> {
+pub struct Authentication<const USER_LEN: usize, const DISCOVERABLE: bool> {
/// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
pub(crate) raw_id: CredentialId<Vec<u8>>,
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response)
- pub(crate) response: AuthenticatorAssertion<User>,
+ pub(crate) response: AuthenticatorAssertion<USER_LEN, DISCOVERABLE>,
/// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
pub(crate) authenticator_attachment: AuthenticatorAttachment,
}
-impl<U> Authentication<U> {
+impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, DISCOVERABLE> {
/// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
#[inline]
#[must_use]
@@ -541,7 +557,7 @@ impl<U> Authentication<U> {
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
#[inline]
#[must_use]
- pub const fn response(&self) -> &AuthenticatorAssertion<U> {
+ pub const fn response(&self) -> &AuthenticatorAssertion<USER_LEN, DISCOVERABLE> {
&self.response
}
/// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
@@ -557,7 +573,7 @@ impl<U> Authentication<U> {
#[must_use]
pub const fn new(
raw_id: CredentialId<Vec<u8>>,
- response: AuthenticatorAssertion<U>,
+ response: AuthenticatorAssertion<USER_LEN, DISCOVERABLE>,
authenticator_attachment: AuthenticatorAttachment,
) -> Self {
Self {
@@ -568,8 +584,9 @@ impl<U> Authentication<U> {
}
/// Returns the associated `SentChallenge`.
///
- /// This is useful when wanting to extract the corresponding [`AuthenticationServerState`] from
- /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage.
+ /// This is useful when wanting to extract the corresponding [`DiscoverableAuthenticationServerState`]
+ /// or [`NonDiscoverableAuthenticationServerState`] from an in-memory collection (e.g., [`FixedCapHashSet`]) or
+ /// storage.
///
/// Note if [`CollectedClientData::from_client_data_json`] returns `Ok`, then this will return `Ok`
/// containing the same value as [`CollectedClientData::challenge`]; however the converse is _not_ true.
@@ -587,8 +604,9 @@ impl<U> Authentication<U> {
}
/// Returns the associated `SentChallenge`.
///
- /// This is useful when wanting to extract the corresponding [`AuthenticationServerState`] from
- /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage.
+ /// This is useful when wanting to extract the corresponding [`DiscoverableAuthenticationServerState`]
+ /// or [`NonDiscoverableAuthenticationServerState`] from an in-memory collection (e.g.,
+ /// [`FixedCapHashSet`]) or storage.
///
/// Note if [`CollectedClientData::from_client_data_json_relaxed`] returns `Ok`, then this will return
/// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse
@@ -618,9 +636,10 @@ impl<U> Authentication<U> {
#[inline]
pub fn from_json_relaxed<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr>
where
- U: Deserialize<'a> + User + Default,
+ UserHandle<USER_LEN>: Deserialize<'a>,
{
- serde_json::from_slice::<AuthenticationRelaxed<U>>(json).map(|val| val.0)
+ serde_json::from_slice::<AuthenticationRelaxed<USER_LEN, DISCOVERABLE>>(json)
+ .map(|val| val.0)
}
/// Convenience function for [`CustomAuthentication::deserialize`].
///
@@ -632,20 +651,29 @@ impl<U> Authentication<U> {
#[inline]
pub fn from_json_custom<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr>
where
- U: Deserialize<'a> + User + Default,
+ UserHandle<USER_LEN>: Deserialize<'a>,
{
- serde_json::from_slice::<CustomAuthentication<U>>(json).map(|val| val.0)
+ serde_json::from_slice::<CustomAuthentication<USER_LEN, DISCOVERABLE>>(json)
+ .map(|val| val.0)
}
}
-impl<U> Response for Authentication<U> {
- type Auth = AuthenticatorAssertion<U>;
+impl<const USER_LEN: usize, const DISCOVERABLE: bool> Response
+ for Authentication<USER_LEN, DISCOVERABLE>
+{
+ type Auth = AuthenticatorAssertion<USER_LEN, DISCOVERABLE>;
fn auth(&self) -> &Self::Auth {
&self.response
}
}
-/// `Authentication` with a required `UserHandle`.
-pub type PasskeyAuthentication<T> = Authentication<UserHandle<T>>;
-/// `Authentication` with a required `UserHandle64`.
-pub type PasskeyAuthentication64 = Authentication<UserHandle64>;
-/// `Authentication` with a required `UserHandle16`.
-pub type PasskeyAuthentication16 = Authentication<UserHandle16>;
+/// `Authentication` with a required [`UserHandle`].
+pub type DiscoverableAuthentication<const USER_LEN: usize> = Authentication<USER_LEN, true>;
+/// `Authentication` with a required [`UserHandle64`].
+pub type DiscoverableAuthentication64 = Authentication<USER_HANDLE_MAX_LEN, true>;
+/// `Authentication` with a required [`UserHandle16`].
+pub type DiscoverableAuthentication16 = Authentication<16, true>;
+/// `Authentication` with an optional [`UserHandle`].
+pub type NonDiscoverableAuthentication<const USER_LEN: usize> = Authentication<USER_LEN, false>;
+/// `Authentication` with an optional [`UserHandle64`].
+pub type NonDiscoverableAuthentication64 = Authentication<USER_HANDLE_MAX_LEN, false>;
+/// `Authentication` with an optional [`UserHandle16`].
+pub type NonDiscoverableAuthentication16 = Authentication<16, false>;
diff --git a/src/response/auth/error.rs b/src/response/auth/error.rs
@@ -9,15 +9,15 @@ use super::super::{
use super::{
super::{
super::{
+ AuthenticatedCredential, DynamicState, StaticState,
request::{
+ BackupReq, UserVerificationRequirement,
auth::{
- AllowedCredential, AllowedCredentials, AuthenticationServerState,
- AuthenticationVerificationOptions, Extension,
- PublicKeyCredentialRequestOptions,
+ AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions,
+ DiscoverableAuthenticationServerState, Extension,
+ NonDiscoverableAuthenticationServerState, PublicKeyCredentialRequestOptions,
},
- BackupReq, UserVerificationRequirement,
},
- AuthenticatedCredential, DynamicState, StaticState,
},
Backup,
},
@@ -181,7 +181,8 @@ impl Display for ExtensionErr {
}
}
impl Error for ExtensionErr {}
-/// Error returned by [`AuthenticationServerState::verify`].
+/// Error returned by [`DiscoverableAuthenticationServerState::verify`] and
+/// [`NonDiscoverableAuthenticationServerState::verify`].
#[derive(Debug)]
pub enum AuthCeremonyErr {
/// [`PublicKeyCredentialRequestOptions::timeout`] was exceeded.
diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs
@@ -4,20 +4,17 @@
)]
use super::{
super::{
- super::{
- request::register::User,
- response::ser::{Base64DecodedVal, PublicKeyCredential},
- },
+ super::response::ser::{Base64DecodedVal, PublicKeyCredential},
ser::{
AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues,
ClientExtensions,
},
},
- Authentication, AuthenticatorAssertion,
+ Authentication, AuthenticatorAssertion, UserHandle,
error::UnknownCredentialOptions,
};
#[cfg(doc)]
-use super::{AuthenticatorAttachment, CredentialId, UserHandle};
+use super::{AuthenticatorAttachment, CredentialId};
use core::{
fmt::{self, Formatter},
marker::PhantomData,
@@ -85,18 +82,17 @@ impl<'e> Deserialize<'e> for AuthData {
///
/// Unknown fields are ignored and only `clientDataJSON`, `authenticatorData`, and `signature` are required iff
/// `RELAXED`.
-pub(super) struct AuthenticatorAssertionVisitor<const RELAXED: bool, U>(PhantomData<fn() -> U>);
-impl<const RELAXED: bool, USER> AuthenticatorAssertionVisitor<RELAXED, USER> {
- /// Returns `Self`.
- pub fn new() -> Self {
- Self(PhantomData)
- }
-}
-impl<'d, const R: bool, U> Visitor<'d> for AuthenticatorAssertionVisitor<R, U>
+pub(super) struct AuthenticatorAssertionVisitor<
+ const RELAXED: bool,
+ const USER_LEN: usize,
+ const DISCOVERABLE: bool,
+>;
+impl<'d, const R: bool, const LEN: usize, const DISC: bool> Visitor<'d>
+ for AuthenticatorAssertionVisitor<R, LEN, DISC>
where
- U: Deserialize<'d> + User + Default,
+ UserHandle<LEN>: Deserialize<'d>,
{
- type Value = AuthenticatorAssertion<U>;
+ type Value = AuthenticatorAssertion<LEN, DISC>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("AuthenticatorAssertion")
}
@@ -203,10 +199,10 @@ where
.and_then(|authenticator_data| {
sig.ok_or_else(|| Error::missing_field(SIGNATURE))
.and_then(|signature| {
- if U::must_exist() {
+ if DISC {
user_handle.ok_or_else(|| Error::missing_field(USER_HANDLE))
} else {
- user_handle.map_or_else(|| Ok(U::default()), Ok)
+ user_handle.map_or_else(|| Ok(None), Ok)
}
.map(|user| {
AuthenticatorAssertion::new_inner(
@@ -232,9 +228,10 @@ const USER_HANDLE: &str = "userHandle";
/// Fields in `AuthenticatorAssertionResponseJSON`.
pub(super) const AUTH_ASSERT_FIELDS: &[&str; 4] =
&[CLIENT_DATA_JSON, AUTHENTICATOR_DATA, SIGNATURE, USER_HANDLE];
-impl<'de, U> Deserialize<'de> for AuthenticatorAssertion<U>
+impl<'de, const USER_LEN: usize, const DISCOVERABLE: bool> Deserialize<'de>
+ for AuthenticatorAssertion<USER_LEN, DISCOVERABLE>
where
- U: Deserialize<'de> + User + Default,
+ UserHandle<USER_LEN>: Deserialize<'de>,
{
/// Deserializes a `struct` based on
/// [`AuthenticatorAssertionResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorassertionresponsejson).
@@ -246,9 +243,9 @@ where
/// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-signature) are
/// base64url-decoded;
/// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-userhandle) is
- /// based on `U`. The key is allowed to not exist or exist with the value `null` iff `U` is
- /// `Option<UserHandle<_>>`; otherwise it is deserialized via [`UserHandle::deserialize`]. All `required`
- /// fields in the `AuthenticatorAssertionResponseJSON` Web IDL `dictionary` exist (and are not `null`).
+ /// required and must not be `null` iff `DISCOVERABLE`. When it exists and is not `null`, it is deserialized
+ /// via [`UserHandle::deserialize`]. All `required` fields in the `AuthenticatorAssertionResponseJSON` Web IDL
+ /// `dictionary` exist (and are not `null`).
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -257,7 +254,7 @@ where
deserializer.deserialize_struct(
"AuthenticatorAssertion",
AUTH_ASSERT_FIELDS,
- AuthenticatorAssertionVisitor::<false, U>::new(),
+ AuthenticatorAssertionVisitor::<false, USER_LEN, DISCOVERABLE>,
)
}
}
@@ -371,9 +368,10 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputs {
)
}
}
-impl<'de, U> Deserialize<'de> for Authentication<U>
+impl<'de, const USER_LEN: usize, const DISCOVERABLE: bool> Deserialize<'de>
+ for Authentication<USER_LEN, DISCOVERABLE>
where
- U: Deserialize<'de> + User + Default,
+ UserHandle<USER_LEN>: Deserialize<'de>,
{
/// Deserializes a `struct` based on
/// [`AuthenticationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson).
@@ -410,9 +408,12 @@ where
where
D: Deserializer<'de>,
{
- PublicKeyCredential::<false, false, AuthenticatorAssertion<U>, ClientExtensionsOutputs>::deserialize(
- deserializer,
- )
+ PublicKeyCredential::<
+ false,
+ false,
+ AuthenticatorAssertion<USER_LEN, DISCOVERABLE>,
+ ClientExtensionsOutputs,
+ >::deserialize(deserializer)
.map(|cred| Self {
raw_id: cred.id.unwrap_or_else(|| {
unreachable!("there is a bug in PublicKeyCredential::deserialize")
@@ -462,8 +463,8 @@ impl Serialize for UnknownCredentialOptions<'_, '_> {
#[cfg(test)]
mod tests {
use super::super::{
- super::super::request::register::USER_HANDLE_MIN_LEN, Authentication,
- AuthenticatorAttachment, PasskeyAuthentication, UserHandle,
+ super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment,
+ DiscoverableAuthentication, NonDiscoverableAuthentication,
};
use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Digest as _, Sha256};
@@ -520,7 +521,7 @@ mod tests {
let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
// Base case is valid.
assert!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -560,7 +561,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "ABABABABABABABABABABAA",
@@ -585,7 +586,7 @@ mod tests {
// missing `id`.
err = Error::missing_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -611,7 +612,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": null,
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -635,7 +636,7 @@ mod tests {
// missing `rawId`.
err = Error::missing_field("rawId").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -660,7 +661,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": null,
@@ -686,7 +687,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -711,7 +712,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -735,7 +736,7 @@ mod tests {
// Missing `signature`.
err = Error::missing_field("signature").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -760,7 +761,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -783,7 +784,7 @@ mod tests {
);
// Missing `userHandle`.
assert!(
- serde_json::from_str::<Authentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>>(
+ serde_json::from_str::<NonDiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -802,7 +803,7 @@ mod tests {
);
// `null` `userHandle`.
assert!(
- serde_json::from_str::<Authentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>>(
+ serde_json::from_str::<NonDiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -822,7 +823,7 @@ mod tests {
);
// `null` `authenticatorAttachment`.
assert!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -852,7 +853,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -879,7 +880,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -904,7 +905,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -928,7 +929,7 @@ mod tests {
// Missing `response`.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -948,7 +949,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -969,7 +970,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -990,7 +991,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1018,7 +1019,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1042,7 +1043,7 @@ mod tests {
// Missing `type`.
err = Error::missing_field("type").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1067,7 +1068,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1093,7 +1094,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1119,7 +1120,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!(null).to_string().as_str()
)
.unwrap_err()
@@ -1130,7 +1131,7 @@ mod tests {
// Empty.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({}).to_string().as_str()
)
.unwrap_err()
@@ -1152,7 +1153,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1179,7 +1180,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1219,7 +1220,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1244,7 +1245,7 @@ mod tests {
// Duplicate field in `PublicKeyCredential`.
err = Error::duplicate_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1320,7 +1321,7 @@ mod tests {
let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
// Base case is valid.
assert!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1349,7 +1350,7 @@ mod tests {
);
// `null` `prf`.
assert!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1374,7 +1375,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1400,7 +1401,7 @@ mod tests {
// Duplicate field.
err = Error::duplicate_field("prf").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1427,7 +1428,7 @@ mod tests {
);
// `null` `results`.
assert!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1452,7 +1453,7 @@ mod tests {
// Duplicate field in `prf`.
err = Error::duplicate_field("results").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1482,7 +1483,7 @@ mod tests {
// Missing `first`.
err = Error::missing_field("first").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1509,7 +1510,7 @@ mod tests {
);
// `null` `first`.
assert!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1535,7 +1536,7 @@ mod tests {
);
// `null` `second`.
assert!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1565,7 +1566,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1597,7 +1598,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1630,7 +1631,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1661,7 +1662,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1692,7 +1693,7 @@ mod tests {
// Duplicate field in `results`.
err = Error::duplicate_field("first").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs
@@ -6,7 +6,7 @@
use super::super::{Challenge, CredentialId};
use super::{
super::{
- super::request::register::{User, UserHandle16, UserHandle64},
+ super::request::register::{USER_HANDLE_MAX_LEN, UserHandle},
auth::ser::{
AUTH_ASSERT_FIELDS, AuthData, AuthenticatorAssertionVisitor, ClientExtensionsOutputs,
ClientExtensionsOutputsVisitor, EXT_FIELDS,
@@ -17,7 +17,7 @@ use super::{
},
ser_relaxed::AuthenticationExtensionsPrfValuesRelaxed,
},
- Authentication, AuthenticatorAssertion, AuthenticatorAttachment, UserHandle,
+ Authentication, AuthenticatorAssertion, AuthenticatorAttachment,
};
use core::{
fmt::{self, Formatter},
@@ -60,10 +60,13 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputsRelaxed {
}
/// `newtype` around `AuthenticatorAssertion` with a "relaxed" [`Self::deserialize`] implementation.
#[derive(Debug)]
-pub struct AuthenticatorAssertionRelaxed<U>(pub AuthenticatorAssertion<U>);
-impl<'de, U> Deserialize<'de> for AuthenticatorAssertionRelaxed<U>
+pub struct AuthenticatorAssertionRelaxed<const USER_LEN: usize, const DISCOVERABLE: bool>(
+ pub AuthenticatorAssertion<USER_LEN, DISCOVERABLE>,
+);
+impl<'de, const USER_LEN: usize, const DISCOVERABLE: bool> Deserialize<'de>
+ for AuthenticatorAssertionRelaxed<USER_LEN, DISCOVERABLE>
where
- U: Deserialize<'de> + User + Default,
+ UserHandle<USER_LEN>: Deserialize<'de>,
{
/// Same as [`AuthenticatorAssertion::deserialize`] except unknown keys are ignored.
///
@@ -77,17 +80,20 @@ where
.deserialize_struct(
"AuthenticatorAssertionRelaxed",
AUTH_ASSERT_FIELDS,
- AuthenticatorAssertionVisitor::<true, U>::new(),
+ AuthenticatorAssertionVisitor::<true, USER_LEN, DISCOVERABLE>,
)
.map(Self)
}
}
/// `newtype` around `Authentication` with a "relaxed" [`Self::deserialize`] implementation.
#[derive(Debug)]
-pub struct AuthenticationRelaxed<U>(pub Authentication<U>);
-impl<'de, U> Deserialize<'de> for AuthenticationRelaxed<U>
+pub struct AuthenticationRelaxed<const USER_LEN: usize, const DISCOVERABLE: bool>(
+ pub Authentication<USER_LEN, DISCOVERABLE>,
+);
+impl<'de, const USER_LEN: usize, const DISCOVERABLE: bool> Deserialize<'de>
+ for AuthenticationRelaxed<USER_LEN, DISCOVERABLE>
where
- U: Deserialize<'de> + User + Default,
+ UserHandle<USER_LEN>: Deserialize<'de>,
{
/// Same as [`Authentication::deserialize`] except unknown keys are ignored;
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-response) is deserialized
@@ -120,7 +126,7 @@ where
PublicKeyCredential::<
true,
false,
- AuthenticatorAssertionRelaxed<U>,
+ AuthenticatorAssertionRelaxed<USER_LEN, DISCOVERABLE>,
ClientExtensionsOutputsRelaxed,
>::deserialize(deserializer)
.map(|cred| {
@@ -135,17 +141,28 @@ where
}
}
/// `AuthenticationRelaxed` with a required `UserHandle`.
-pub type PasskeyAuthenticationRelaxed<T> = AuthenticationRelaxed<UserHandle<T>>;
+pub type DiscoverableAuthenticationRelaxed<const USER_LEN: usize> =
+ AuthenticationRelaxed<USER_LEN, true>;
/// `AuthenticationRelaxed` with a required `UserHandle64`.
-pub type PasskeyAuthenticationRelaxed64 = AuthenticationRelaxed<UserHandle64>;
+pub type DiscoverableAuthenticationRelaxed64 = AuthenticationRelaxed<USER_HANDLE_MAX_LEN, true>;
/// `AuthenticationRelaxed` with a required `UserHandle16`.
-pub type PasskeyAuthenticationRelaxed16 = AuthenticationRelaxed<UserHandle16>;
+pub type DiscoverableAuthenticationRelaxed16 = AuthenticationRelaxed<16, true>;
+/// `AuthenticationRelaxed` with an optional `UserHandle`.
+pub type NonDiscoverableAuthenticationRelaxed<const USER_LEN: usize> =
+ AuthenticationRelaxed<USER_LEN, false>;
+/// `AuthenticationRelaxed` with an optional `UserHandle64`.
+pub type NonDiscoverableAuthenticationRelaxed64 = AuthenticationRelaxed<USER_HANDLE_MAX_LEN, false>;
+/// `AuthenticationRelaxed` with an optional `UserHandle16`.
+pub type NonDiscoverableAuthenticationRelaxed16 = AuthenticationRelaxed<16, false>;
/// `newtype` around `Authentication` with a custom [`Self::deserialize`] implementation.
#[derive(Debug)]
-pub struct CustomAuthentication<U>(pub Authentication<U>);
-impl<'de, U> Deserialize<'de> for CustomAuthentication<U>
+pub struct CustomAuthentication<const USER_LEN: usize, const DISCOVERABLE: bool>(
+ pub Authentication<USER_LEN, DISCOVERABLE>,
+);
+impl<'de, const USER_LEN: usize, const DISCOVERABLE: bool> Deserialize<'de>
+ for CustomAuthentication<USER_LEN, DISCOVERABLE>
where
- U: Deserialize<'de> + User + Default,
+ UserHandle<USER_LEN>: Deserialize<'de>,
{
/// Despite the spec having a
/// [pre-defined format](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson) that clients
@@ -183,7 +200,7 @@ where
/// }
/// ```
///
- /// `"userHandle"` is required to exist and not be `null` iff `U` is [`UserHandle`]. When it does exist and
+ /// `"userHandle"` is required to exist and not be `null` iff `DISCOVERABLE`. When it does exist and
/// is not `null`, then it is deserialized via [`UserHandle::deserialize`]. All of the remaining keys are
/// required with the exceptions of `"authenticatorAttachment"` and `"type"`. `"prf"` is not required in the
/// `clientExtensionResults` object, `"results"` is required in the `PRFJSON` object, and `"first"`
@@ -197,7 +214,7 @@ where
/// // The below payload is technically valid, but `AuthenticationServerState::verify` will fail
/// // since the authenticatorData is not valid. This is true for `Authentication::deserialize`
/// // as well since authenticatorData parsing is always deferred.
- /// serde_json::from_str::<CustomAuthentication<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>(
+ /// serde_json::from_str::<CustomAuthentication<USER_HANDLE_MIN_LEN, true>>(
/// r#"{
/// "authenticatorData": "AA",
/// "authenticatorAttachment": "cross-platform",
@@ -220,12 +237,12 @@ where
D: Deserializer<'de>,
{
/// `Visitor` for `CustomAuthentication`.
- struct CustomAuthenticationVisitor<UHand>(PhantomData<fn() -> UHand>);
- impl<'d, UHand> Visitor<'d> for CustomAuthenticationVisitor<UHand>
+ struct CustomAuthenticationVisitor<const LEN: usize, const DISC: bool>;
+ impl<'d, const LEN: usize, const DISC: bool> Visitor<'d> for CustomAuthenticationVisitor<LEN, DISC>
where
- UHand: Deserialize<'d> + User + Default,
+ UserHandle<LEN>: Deserialize<'d>,
{
- type Value = CustomAuthentication<UHand>;
+ type Value = CustomAuthentication<LEN, DISC>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("CustomAuthentication")
}
@@ -368,10 +385,10 @@ where
.ok_or_else(|| Error::missing_field(SIGNATURE))
.and_then(|sig| {
if ext {
- if UHand::must_exist() {
+ if DISC {
user_handle.ok_or_else(|| Error::missing_field(USER_HANDLE))
} else {
- user_handle.map_or_else(|| Ok(UHand::default()), Ok)
+ user_handle.map_or_else(|| Ok(None), Ok)
}.map(|user| {
CustomAuthentication(Authentication {
response: AuthenticatorAssertion::new_inner(
@@ -430,28 +447,29 @@ where
TYPE,
USER_HANDLE,
];
- deserializer.deserialize_struct(
- "CustomAuthentication",
- FIELDS,
- CustomAuthenticationVisitor(PhantomData),
- )
+ deserializer.deserialize_struct("CustomAuthentication", FIELDS, CustomAuthenticationVisitor)
}
}
/// `CustomAuthentication` with a required `UserHandle`.
-pub type PasskeyCustomAuthentication<T> = CustomAuthentication<UserHandle<T>>;
+pub type DiscoverableCustomAuthentication<const USER_LEN: usize> =
+ CustomAuthentication<USER_LEN, true>;
/// `CustomAuthentication` with a required `UserHandle64`.
-pub type PasskeyCustomAuthentication64 = CustomAuthentication<UserHandle64>;
+pub type DiscoverableCustomAuthentication64 = CustomAuthentication<USER_HANDLE_MAX_LEN, true>;
/// `CustomAuthentication` with a required `UserHandle16`.
-pub type PasskeyCustomAuthentication16 = CustomAuthentication<UserHandle16>;
+pub type DiscoverableCustomAuthentication16 = CustomAuthentication<16, true>;
+/// `CustomAuthentication` with an optional `UserHandle`.
+pub type NonDiscoverableCustomAuthentication<const USER_LEN: usize> =
+ CustomAuthentication<USER_LEN, false>;
+/// `CustomAuthentication` with an optional `UserHandle64`.
+pub type NonDiscoverableCustomAuthentication64 = CustomAuthentication<USER_HANDLE_MAX_LEN, false>;
+/// `CustomAuthentication` with an optional `UserHandle16`.
+pub type NonDiscoverableCustomAuthentication16 = CustomAuthentication<16, false>;
#[cfg(test)]
mod tests {
use super::{
- super::{
- super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment,
- UserHandle,
- },
- AuthenticationRelaxed, CustomAuthentication, PasskeyAuthenticationRelaxed,
- PasskeyCustomAuthentication,
+ super::{super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment},
+ DiscoverableAuthenticationRelaxed, DiscoverableCustomAuthentication,
+ NonDiscoverableAuthenticationRelaxed, NonDiscoverableCustomAuthentication,
};
use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Digest as _, Sha256};
@@ -508,7 +526,7 @@ mod tests {
let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
// Base case is valid.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -548,7 +566,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "ABABABABABABABABABABAA",
@@ -573,7 +591,7 @@ mod tests {
// missing `id`.
err = Error::missing_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -599,7 +617,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": null,
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -622,7 +640,7 @@ mod tests {
);
// missing `rawId`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -644,7 +662,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": null,
@@ -670,7 +688,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -695,7 +713,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -719,7 +737,7 @@ mod tests {
// Missing `signature`.
err = Error::missing_field("signature").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -744,7 +762,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -767,9 +785,7 @@ mod tests {
);
// Missing `userHandle`.
assert!(
- serde_json::from_str::<
- AuthenticationRelaxed<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>,
- >(
+ serde_json::from_str::<NonDiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -788,9 +804,7 @@ mod tests {
);
// `null` `userHandle`.
assert!(
- serde_json::from_str::<
- AuthenticationRelaxed<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>,
- >(
+ serde_json::from_str::<NonDiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -810,7 +824,7 @@ mod tests {
);
// `null` `authenticatorAttachment`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -840,7 +854,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -867,7 +881,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -892,7 +906,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -916,7 +930,7 @@ mod tests {
// Missing `response`.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -936,7 +950,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -957,7 +971,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -975,7 +989,7 @@ mod tests {
);
// Missing `clientExtensionResults`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -994,7 +1008,7 @@ mod tests {
);
// `null` `clientExtensionResults`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1014,7 +1028,7 @@ mod tests {
);
// Missing `type`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1036,7 +1050,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1062,7 +1076,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1088,7 +1102,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!(null).to_string().as_str()
)
.unwrap_err()
@@ -1099,7 +1113,7 @@ mod tests {
// Empty.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({}).to_string().as_str()
)
.unwrap_err()
@@ -1109,7 +1123,7 @@ mod tests {
);
// Unknown field in `response`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1133,7 +1147,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1159,7 +1173,7 @@ mod tests {
);
// Unknown field in `PublicKeyCredential`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1181,7 +1195,7 @@ mod tests {
// Duplicate field in `PublicKeyCredential`.
err = Error::duplicate_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1207,7 +1221,7 @@ mod tests {
);
// Base case is valid.
assert!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1234,7 +1248,7 @@ mod tests {
// missing `id`.
err = Error::missing_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
@@ -1257,7 +1271,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": null,
"clientDataJSON": b64_cdata,
@@ -1280,7 +1294,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1302,7 +1316,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1323,7 +1337,7 @@ mod tests {
// Missing `signature`.
err = Error::missing_field("signature").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1345,7 +1359,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1365,9 +1379,7 @@ mod tests {
);
// Missing `userHandle`.
assert!(
- serde_json::from_str::<
- CustomAuthentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>,
- >(
+ serde_json::from_str::<NonDiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1383,9 +1395,7 @@ mod tests {
);
// `null` `userHandle`.
assert!(
- serde_json::from_str::<
- CustomAuthentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>,
- >(
+ serde_json::from_str::<NonDiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1402,7 +1412,7 @@ mod tests {
);
// `null` `authenticatorAttachment`.
assert!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1429,7 +1439,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1453,7 +1463,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"authenticatorData": b64_adata,
@@ -1475,7 +1485,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": null,
@@ -1498,7 +1508,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({}).to_string().as_str()
)
.unwrap_err()
@@ -1511,7 +1521,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1533,7 +1543,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1552,7 +1562,7 @@ mod tests {
err
);
assert!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1571,7 +1581,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1594,7 +1604,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1617,7 +1627,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!(null).to_string().as_str()
)
.unwrap_err()
@@ -1643,7 +1653,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1667,7 +1677,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableCustomAuthentication<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1740,7 +1750,7 @@ mod tests {
let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
// Base case is valid.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1769,7 +1779,7 @@ mod tests {
);
// `null` `prf`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1791,7 +1801,7 @@ mod tests {
);
// Unknown `clientExtensionResults`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1814,7 +1824,7 @@ mod tests {
// Duplicate field.
let mut err = Error::duplicate_field("prf").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1841,7 +1851,7 @@ mod tests {
);
// `null` `results`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1866,7 +1876,7 @@ mod tests {
// Duplicate field in `prf`.
err = Error::duplicate_field("results").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1895,7 +1905,7 @@ mod tests {
);
// Missing `first`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1919,7 +1929,7 @@ mod tests {
);
// `null` `first`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1945,7 +1955,7 @@ mod tests {
);
// `null` `second`.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1975,7 +1985,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -2007,7 +2017,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -2040,7 +2050,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -2068,7 +2078,7 @@ mod tests {
);
// Unknown `prf` field.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -2093,7 +2103,7 @@ mod tests {
);
// Unknown `results` field.
assert!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -2121,7 +2131,7 @@ mod tests {
// Duplicate field in `results`.
err = Error::duplicate_field("first").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
diff --git a/src/response/bin.rs b/src/response/bin.rs
@@ -113,11 +113,12 @@ impl<'b> Decode for CredentialId<&'b [u8]> {
}
}
}
-impl EncodeBuffer for CredentialId<&[u8]> {
+impl<T: AsRef<[u8]>> EncodeBuffer for CredentialId<T> {
#[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
// Max length is 1023, so this won't error.
self.0
+ .as_ref()
.encode_into_buffer(buffer)
.unwrap_or_else(|_e| unreachable!("there is a bug in [u8]::encode_into_buffer"));
}
diff --git a/src/response/register.rs b/src/response/register.rs
@@ -3091,8 +3091,7 @@ mod tests {
AggErr,
request::{AsciiDomain, RpId},
},
- UserHandle,
- auth::{AuthenticatorAssertion, AuthenticatorData},
+ auth::{AuthenticatorData, NonDiscoverableAuthenticatorAssertion},
},
AttestationFormat, AttestationObject, AuthDataContainer as _, AuthTransports,
AuthenticatorAttestation, Backup, Sig, UncompressedPubKey,
@@ -3155,7 +3154,7 @@ mod tests {
.unwrap();
let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d".as_slice()).unwrap();
let signature = HEXLOWER.decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87".as_slice()).unwrap();
- let auth_assertion = AuthenticatorAssertion::<Option<UserHandle<Vec<u8>>>>::without_user(
+ let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user(
client_data_json_2,
authenticator_data,
signature,
@@ -3236,7 +3235,7 @@ mod tests {
.unwrap();
let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d".as_slice()).unwrap();
let signature = HEXLOWER.decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744".as_slice()).unwrap();
- let auth_assertion = AuthenticatorAssertion::<Option<UserHandle<Vec<u8>>>>::without_user(
+ let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user(
client_data_json_2,
authenticator_data,
signature,
diff --git a/src/response/ser.rs b/src/response/ser.rs
@@ -805,7 +805,7 @@ where
)
}
}
-impl Serialize for AllAcceptedCredentialsOptions<'_, '_> {
+impl<const USER_LEN: usize> Serialize for AllAcceptedCredentialsOptions<'_, '_, USER_LEN> {
/// Serializes `self` to conform with
/// [`AllAcceptedCredentialsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions).
///
@@ -821,13 +821,13 @@ impl Serialize for AllAcceptedCredentialsOptions<'_, '_> {
/// # };
/// /// Retrieves the `CredentialId`s associated with `user_id` from the database.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// fn get_credential_ids(user_id: UserHandle<&[u8]>) -> Result<Vec<CredentialId<Vec<u8>>>, CredentialIdErr> {
+ /// fn get_credential_ids(user_id: UserHandle<USER_HANDLE_MIN_LEN>) -> Result<Vec<CredentialId<Vec<u8>>>, CredentialIdErr> {
/// // ⋮
/// # CredentialId::decode(vec![0; 16]).map(|cred_id| vec![cred_id])
/// }
/// /// Retrieves the `UserHandle` from a session cookie.
/// # #[cfg(feature = "custom")]
- /// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MIN_LEN]> {
+ /// fn get_user_handle() -> UserHandle<USER_HANDLE_MIN_LEN> {
/// // ⋮
/// # [0].into()
/// }
@@ -837,8 +837,8 @@ impl Serialize for AllAcceptedCredentialsOptions<'_, '_> {
/// assert_eq!(
/// serde_json::to_string(&AllAcceptedCredentialsOptions {
/// rp_id: &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
- /// user_id: (&user_id).into(),
- /// all_accepted_credential_ids: get_credential_ids((&user_id).into())?,
+ /// user_id: &user_id,
+ /// all_accepted_credential_ids: get_credential_ids(user_id)?,
/// })
/// .unwrap(),
/// r#"{"rpId":"example.com","userId":"AA","allAcceptedCredentialIds":["AAAAAAAAAAAAAAAAAAAAAA"]}"#
@@ -865,7 +865,7 @@ impl Serialize for AllAcceptedCredentialsOptions<'_, '_> {
})
}
}
-impl Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_> {
+impl<const LEN: usize> Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_, LEN> {
/// Serializes `self` to conform with
/// [`CurrentUserDetailsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions).
///
@@ -882,27 +882,25 @@ impl Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_> {
/// # };
/// /// Retrieves the `PublicKeyCredentialUserEntity` info associated with `user_id` from the database.
/// # #[cfg(feature = "bin")]
- /// fn get_user_info(user_id: UserHandle<&[u8]>) -> Result<(Username, Option<Nickname>), AggErr> {
+ /// fn get_user_info(user_id: UserHandle<USER_HANDLE_MIN_LEN>) -> Result<(Username<'static>, Option<Nickname<'static>>), AggErr> {
/// // ⋮
/// # Ok((Username::decode("foo").unwrap(), Some(Nickname::decode("foo").unwrap())))
/// }
/// /// Retrieves the `UserHandle` from a session cookie.
/// # #[cfg(feature = "custom")]
- /// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MIN_LEN]> {
+ /// fn get_user_handle() -> UserHandle<USER_HANDLE_MIN_LEN> {
/// // ⋮
/// # [0].into()
/// }
/// # #[cfg(feature = "custom")]
/// let user_handle = get_user_handle();
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let id = (&user_handle).into();
- /// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let (name, display_name) = get_user_info(id)?;
+ /// let (name, display_name) = get_user_info(user_handle)?;
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// assert_eq!(
/// serde_json::to_string(&CurrentUserDetailsOptions {
/// rp_id: &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
- /// user: PublicKeyCredentialUserEntity { name, id, display_name, },
+ /// user: PublicKeyCredentialUserEntity { name, id: &user_handle, display_name, },
/// })
/// .unwrap(),
/// r#"{"rpId":"example.com","userId":"AA","name":"foo","displayName":"foo"}"#