webauthn_rp

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

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:
MCargo.toml | 2+-
MREADME.md | 322+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 539++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/request.rs | 193++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/request/auth.rs | 942+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/request/auth/error.rs | 8+++++---
Msrc/request/auth/ser.rs | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/request/auth/ser_server_state.rs | 184++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/request/register.rs | 456+++++++++++++++++++++++++++++--------------------------------------------------
Msrc/request/register/bin.rs | 29+++++------------------------
Msrc/request/register/custom.rs | 21++-------------------
Msrc/request/register/error.rs | 17++---------------
Msrc/request/register/ser.rs | 294++++++-------------------------------------------------------------------------
Msrc/request/register/ser_server_state.rs | 52++++++++++++++++++++++++++++++++++++++++------------
Msrc/response.rs | 67++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/response/auth.rs | 144+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/response/auth/error.rs | 13+++++++------
Msrc/response/auth/ser.rs | 151++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/response/auth/ser_relaxed.rs | 236+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/response/bin.rs | 3++-
Msrc/response/register.rs | 7+++----
Msrc/response/ser.rs | 22++++++++++------------
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(&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")), + ), + &registration, + &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(&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")), +//! ), +//! &registration, +//! &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(&registration.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, handle, &registration, &ver_opts)?); +/// insert_cred(ceremonies.take(&registration.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, &registration, &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"}"#