commit fcd94da3dcb4e3353a974c9973cad59a8b083dab
parent 88c3f257fcf796e6642a254adbdb3c81cfc214f4
Author: Zack Newman <zack@philomathiclife.com>
Date: Tue, 3 Jun 2025 15:32:52 -0600
separate mediation into outer type. improve prf
Diffstat:
17 files changed, 2306 insertions(+), 1077 deletions(-)
diff --git a/src/hash.rs b/src/hash.rs
@@ -7,7 +7,7 @@ use super::{
DiscoverableAuthenticationServerState, DiscoverableCredentialRequestOptions,
NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
},
- register::{PublicKeyCredentialCreationOptions, RegistrationServerState},
+ register::{CredentialCreationOptions, RegistrationServerState},
},
};
use core::hash::{BuildHasher, Hasher};
@@ -29,7 +29,7 @@ pub mod hash_set;
/// optimize without fear by using this `Hasher` since `Challenge`s are immutable and can only ever be created on
/// the server via [`Challenge::new`] (and equivalently [`Challenge::default`]). `RegistrationServerState`,
/// `DiscoverableAuthenticationServerState`, and `NonDiscoverableAuthenticationServerState` are also immutable and
-/// only constructable via [`PublicKeyCredentialCreationOptions::start_ceremony`],
+/// only constructable via [`CredentialCreationOptions::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
diff --git a/src/lib.rs b/src/lib.rs
@@ -20,7 +20,7 @@
//! use core::convert;
//! use webauthn_rp::{
//! AuthenticatedCredential64, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
-//! DiscoverableCredentialRequestOptions, PublicKeyCredentialCreationOptions64, RegisteredCredential64,
+//! DiscoverableCredentialRequestOptions, CredentialCreationOptions64, RegisteredCredential64,
//! Registration, RegistrationServerState64,
//! hash::hash_set::FixedCapHashSet,
//! request::{
@@ -114,7 +114,7 @@
//! );
//! let user_id = UserHandle64::new();
//! let (server, client) =
-//! PublicKeyCredentialCreationOptions64::first_passkey_with_blank_user_info(
+//! CredentialCreationOptions64::first_passkey_with_blank_user_info(
//! &rp_id, &user_id,
//! )
//! .start_ceremony()
@@ -175,7 +175,7 @@
//! .unwrap_or_else(|_e| unreachable!("example.com is a valid domain")),
//! );
//! let (entity, creds) = select_user_info(user_id)?.ok_or_else(|| AppErr::NoAccount)?;
-//! let (server, client) = PublicKeyCredentialCreationOptions64::passkey(&rp_id, entity, creds)
+//! let (server, client) = CredentialCreationOptions64::passkey(&rp_id, entity, creds)
//! .start_ceremony()
//! .unwrap_or_else(|_e| {
//! unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
@@ -534,8 +534,8 @@ use crate::{
TimedCeremony, Url,
auth::{AllowedCredential, AllowedCredentials, PublicKeyCredentialRequestOptions},
register::{
- CoseAlgorithmIdentifier, Nickname, PublicKeyCredentialUserEntity, UserHandle16,
- UserHandle64, Username,
+ CoseAlgorithmIdentifier, Nickname, PublicKeyCredentialCreationOptions,
+ PublicKeyCredentialUserEntity, UserHandle16, UserHandle64, Username,
},
},
response::{
@@ -644,11 +644,11 @@ pub mod hash;
/// if encrypted is not desirable though.
/// * [`Label 4`](#label4) is ideal as a single-factor flow incorporated within a wider multi-factor authentication (MFA)
/// setup. The easiest way to register such a credential is with
-/// [`PublicKeyCredentialCreationOptions::second_factor`].
+/// [`CredentialCreationOptions::second_factor`].
/// * [`Label 13`](#label13) is ideal for passkey setups as it allows for pleasant UX where a user does not have to type a
/// username nor password while still being secured with MFA with one of the factors being based on public-key
/// cryptography which for many is the most secure form of single-factor authentication. The easiest way to register
-/// such a credential is with [`PublicKeyCredentialCreationOptions::passkey`].
+/// such a credential is with [`CredentialCreationOptions::passkey`].
///
/// 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)
@@ -658,8 +658,8 @@ pub mod hash;
/// seamlessly transition from a username and password scheme to a userless and passwordless one in the future.
///
/// Note the table is purely informative. While helper functions
-/// (e.g., [`PublicKeyCredentialCreationOptions::passkey`]) only exist for [`Label 4`](#label4) and
-/// [`Label 13`](#label13), one can create any credential since all fields in [`PublicKeyCredentialCreationOptions`]
+/// (e.g., [`CredentialCreationOptions::passkey`]) only exist for [`Label 4`](#label4) and
+/// [`Label 13`](#label13), one can create any credential since all fields in [`CredentialCreationOptions`]
/// and [`PublicKeyCredentialRequestOptions`] are accessible.
pub mod request;
/// Functionality for completing ceremonies.
@@ -675,10 +675,9 @@ pub use crate::{
NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
},
register::{
- PublicKeyCredentialCreationOptions, PublicKeyCredentialCreationOptions16,
- PublicKeyCredentialCreationOptions64, RegistrationClientState,
- RegistrationClientState16, RegistrationClientState64, RegistrationServerState,
- RegistrationServerState16, RegistrationServerState64,
+ CredentialCreationOptions, CredentialCreationOptions16, CredentialCreationOptions64,
+ RegistrationClientState, RegistrationClientState16, RegistrationClientState64,
+ RegistrationServerState, RegistrationServerState16, RegistrationServerState64,
},
},
response::{
@@ -834,7 +833,7 @@ 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. The [`PublicKeyCredentialUserEntity::id`]
-/// associated with the [`PublicKeyCredentialCreationOptions`] used to create the `RegisteredCredential` via
+/// associated with the [`CredentialCreationOptions`] 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`.
@@ -1131,7 +1130,7 @@ pub enum AggErr {
RequestOptions(RequestOptionsErr),
/// Variant when [`NonDiscoverableCredentialRequestOptions::second_factor`] errors.
SecondFactor(SecondFactorErr),
- /// Variant when [`PublicKeyCredentialCreationOptions::start_ceremony`] errors.
+ /// Variant when [`CredentialCreationOptions::start_ceremony`] errors.
CreationOptions(CreationOptionsErr),
/// Variant when [`Nickname::try_from`] errors.
Nickname(NicknameErr),
diff --git a/src/request.rs b/src/request.rs
@@ -8,7 +8,7 @@ use super::{
NonDiscoverableAuthenticationServerState, NonDiscoverableCredentialRequestOptions,
PublicKeyCredentialRequestOptions,
},
- register::{PublicKeyCredentialCreationOptions, RegistrationServerState},
+ register::{CredentialCreationOptions, RegistrationServerState},
},
response::register::ClientExtensionsOutputs,
};
@@ -108,7 +108,7 @@ pub mod error;
/// # hash::hash_set::FixedCapHashSet,
/// # request::{
/// # register::{
-/// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64,
+/// # CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64,
/// # },
/// # AsciiDomain, PublicKeyCredentialDescriptor, RpId
/// # },
@@ -125,7 +125,7 @@ pub mod error;
/// # #[cfg(feature = "custom")]
/// let creds = get_registered_credentials(&user_handle)?;
/// # #[cfg(feature = "custom")]
-/// let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, user.clone(), creds)
+/// let (server, client) = CredentialCreationOptions::passkey(&rp_id, user.clone(), creds)
/// .start_ceremony()?;
/// # #[cfg(feature = "custom")]
/// assert!(
@@ -137,7 +137,7 @@ pub mod error;
/// let creds_2 = get_registered_credentials(&user_handle)?;
/// # #[cfg(feature = "custom")]
/// let (server_2, client_2) =
-/// PublicKeyCredentialCreationOptions::second_factor(&rp_id, user, creds_2).start_ceremony()?;
+/// CredentialCreationOptions::second_factor(&rp_id, user, creds_2).start_ceremony()?;
/// # #[cfg(feature = "custom")]
/// assert!(
/// ceremonies.insert_remove_all_expired(server_2).map_or(false, convert::identity)
@@ -1071,19 +1071,20 @@ impl Display for ExtensionInfo {
}
}
/// [`CredentialMediationRequirement`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Default)]
pub enum CredentialMediationRequirement {
/// [`silent`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-silent).
Silent,
- /// [`optional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-optional)
+ /// [`optional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-optional).
+ #[default]
Optional,
- /// [`conditional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-conditional)
+ /// [`conditional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-conditional).
///
- /// Note that when registering a new credential with [`PublicKeyCredentialCreationOptions::mediation`] set to
+ /// Note that when registering a new credential with [`CredentialCreationOptions::mediation`] set to
/// `Self::Conditional`, [`UserVerificationRequirement::Discouraged`] MUST be used unless user verification
/// can be explicitly performed during the ceremony.
Conditional,
- /// [`required`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-required)
+ /// [`required`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-required).
Required,
}
#[cfg(test)]
@@ -1100,15 +1101,18 @@ impl PartialEq for CredentialMediationRequirement {
/// Backup requirements for the credential.
#[derive(Clone, Copy, Debug, Default)]
pub enum BackupReq {
- #[default]
/// No requirements (i.e., any [`Backup`] is allowed).
+ #[default]
None,
/// Credential must not be eligible for backup.
NotEligible,
/// Credential must be eligible for backup.
///
- /// Note the existence of a backup is ignored. If a backup must exist, then use [`Self::Exists`].
+ /// Note the existence of a backup is ignored. If a backup must exist, then use [`Self::Exists`]; if a
+ /// backup must not exist, then use [`Self::EligibleNotExists`].
Eligible,
+ /// Credential must be eligible for backup, but a backup must not exist.
+ EligibleNotExists,
/// Credential must be backed up.
Exists,
}
@@ -1465,6 +1469,13 @@ trait Ceremony<const USER_LEN: usize, const DISCOVERABLE: bool> {
Ok(())
}
}
+ BackupReq::EligibleNotExists => {
+ if matches!(flag.backup, Backup::Eligible) {
+ Ok(())
+ } else {
+ Err(CeremonyErr::BackupExists)
+ }
+ }
BackupReq::Exists => {
if matches!(flag.backup, Backup::Exists) {
Ok(())
@@ -1506,6 +1517,20 @@ pub trait TimedCeremony {
#[cfg(all(not(doc), feature = "serializable_server_state"))]
fn expiration(&self) -> SystemTime;
}
+/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
+#[derive(Clone, Copy, Debug)]
+pub struct PrfInput<'first, 'second> {
+ /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first).
+ pub first: &'first [u8],
+ /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second).
+ pub second: Option<&'second [u8]>,
+}
+#[cfg(test)]
+impl PartialEq for PrfInput<'_, '_> {
+ fn eq(&self, other: &Self) -> bool {
+ self.first == other.first && self.second == other.second
+ }
+}
#[cfg(test)]
mod tests {
#[cfg(feature = "custom")]
@@ -1527,7 +1552,7 @@ mod tests {
},
},
},
- AsciiDomain, Challenge, Credentials, ExtensionInfo, ExtensionReq,
+ AsciiDomain, Challenge, Credentials, ExtensionInfo, ExtensionReq, PrfInput,
PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement,
auth::{
AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions,
@@ -1535,7 +1560,7 @@ mod tests {
Extension as AuthExt, NonDiscoverableCredentialRequestOptions, PrfInputOwned,
},
register::{
- CredProtect, Extension as RegExt, FourToSixtyThree, PublicKeyCredentialCreationOptions,
+ CredProtect, CredentialCreationOptions, Extension as RegExt, FourToSixtyThree,
PublicKeyCredentialUserEntity, RegistrationVerificationOptions, UserHandle,
},
};
@@ -1575,7 +1600,7 @@ mod tests {
fn eddsa_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0]);
- let mut opts = PublicKeyCredentialCreationOptions::passkey(
+ let mut opts = CredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
@@ -1584,8 +1609,8 @@ mod tests {
},
Vec::new(),
);
- opts.challenge = Challenge(0);
- opts.extensions = RegExt {
+ opts.public_key.challenge = Challenge(0);
+ opts.public_key.extensions = RegExt {
cred_props: None,
cred_protect: CredProtect::UserVerificationRequired(
false,
@@ -1596,7 +1621,13 @@ mod tests {
.unwrap_or_else(|| unreachable!("bug in FourToSixyThree::new")),
ExtensionInfo::RequireEnforceValue,
)),
- prf: Some(ExtensionInfo::RequireEnforceValue),
+ prf: Some((
+ PrfInput {
+ first: [0].as_slice(),
+ second: None,
+ },
+ ExtensionInfo::RequireEnforceValue,
+ )),
};
let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
@@ -1934,7 +1965,7 @@ mod tests {
prf: Some(PrfInputOwned {
first: Vec::new(),
second: Some(Vec::new()),
- ext_info: ExtensionReq::Require,
+ ext_req: ExtensionReq::Require,
}),
},
});
@@ -2140,7 +2171,7 @@ mod tests {
fn es256_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0]);
- let mut opts = PublicKeyCredentialCreationOptions::passkey(
+ let mut opts = CredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
@@ -2149,7 +2180,7 @@ mod tests {
},
Vec::new(),
);
- opts.challenge = Challenge(0);
+ opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
let mut attestation_object = Vec::with_capacity(210);
@@ -2398,7 +2429,7 @@ mod tests {
fn es256_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
- opts.0.challenge = Challenge(0);
+ opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
let mut authenticator_data = Vec::with_capacity(69);
@@ -2509,7 +2540,7 @@ mod tests {
fn es384_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0]);
- let mut opts = PublicKeyCredentialCreationOptions::passkey(
+ let mut opts = CredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
@@ -2518,7 +2549,7 @@ mod tests {
},
Vec::new(),
);
- opts.challenge = Challenge(0);
+ opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
let mut attestation_object = Vec::with_capacity(243);
@@ -2802,7 +2833,7 @@ mod tests {
fn es384_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
- opts.0.challenge = Challenge(0);
+ opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
let mut authenticator_data = Vec::with_capacity(69);
@@ -2914,7 +2945,7 @@ mod tests {
fn rs256_reg() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0]);
- let mut opts = PublicKeyCredentialCreationOptions::passkey(
+ let mut opts = CredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
@@ -2923,7 +2954,7 @@ mod tests {
},
Vec::new(),
);
- opts.challenge = Challenge(0);
+ opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
let mut attestation_object = Vec::with_capacity(406);
@@ -3416,7 +3447,7 @@ mod tests {
fn rs256_auth() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
- opts.0.challenge = Challenge(0);
+ opts.public_key.challenge = Challenge(0);
let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
let mut authenticator_data = Vec::with_capacity(69);
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -24,9 +24,9 @@ use super::{
register::{CompressedPubKey, CredentialProtectionPolicy},
},
},
- BackupReq, Ceremony, CeremonyOptions, Challenge, CredentialId, Credentials, ExtensionReq, Hint,
- Origin, PublicKeyCredentialDescriptor, RpId, SentChallenge, THREE_HUNDRED_THOUSAND,
- TimedCeremony, UserVerificationRequirement,
+ BackupReq, Ceremony, CeremonyOptions, Challenge, CredentialId, CredentialMediationRequirement,
+ Credentials, ExtensionReq, Hint, Origin, PrfInput, PublicKeyCredentialDescriptor, RpId,
+ SentChallenge, THREE_HUNDRED_THOUSAND, TimedCeremony, UserVerificationRequirement,
auth::error::{RequestOptionsErr, SecondFactorErr},
};
use core::{
@@ -85,45 +85,6 @@ impl SignatureCounterEnforcement {
}
}
}
-/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
-///
-/// This is only applicable if
-/// [`PublicKeyCredentialRequestOptions::user_verification`] is [`UserVerificationRequirement::Required`].
-/// Additionally [`AuthenticatorExtensionOutputStaticState::hmac_secret`] must either be `None` or `Some(true)`
-/// and [`ClientExtensionsOutputsStaticState::prf`] must be `None` or
-/// `Some(AuthenticationExtensionsPrfOutputs { enabled: true })`.
-///
-/// Unlike the spec, it is forbidden for
-/// [the decrypted outputs](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs) to be
-/// passed back in an effort to ensure sensitive data remains client-side. This means
-/// [`prf`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs) must not exist,
-/// be `null`, or be an
-/// [`AuthenticationExtensionsPRFOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs)
-/// such that [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) does not exist,
-/// is `null`, or is an
-/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues) such
-/// that [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is `null` and
-/// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) does not exist or is `null`.
-///
-/// For the owned analog, see [`PrfInputOwned`].
-///
-/// 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).
- pub first: &'a [u8],
- /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second).
- pub second: Option<&'a [u8]>,
- /// Response requirements.
- ///
- /// Note this is only applicable for authenticators that implement the
- /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the
- /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension)
- /// extension since the data is encrypted and is part of the [`AuthenticatorData`].
- pub ext_info: ExtensionReq,
-}
/// Owned version of [`PrfInput`].
///
/// When relying on [`NonDiscoverableCredentialRequestOptions`], it's recommended to use credential-specific PRF
@@ -134,21 +95,25 @@ pub struct PrfInputOwned {
pub first: Vec<u8>,
/// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second).
pub second: Option<Vec<u8>>,
- ///
/// Note this is only applicable for authenticators that implement the
/// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the
/// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension)
/// extension since the data is encrypted and is part of the [`AuthenticatorData`].
- pub ext_info: ExtensionReq,
+ pub ext_req: ExtensionReq,
}
/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client.
#[derive(Clone, Copy, Debug, Default)]
-pub struct Extension<'prf> {
+pub struct Extension<'prf_first, 'prf_second> {
/// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension).
///
/// If both [`CredentialSpecificExtension::prf`] and this are [`Some`], then `CredentialSpecificExtension::prf`
/// takes priority.
- pub prf: Option<PrfInput<'prf>>,
+ ///
+ /// Note `ExtensionReq` is only applicable for authenticators that implement the
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) extension on top of the
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension)
+ /// extension since the data is encrypted and is part of the [`AuthenticatorData`].
+ pub prf: Option<(PrfInput<'prf_first, 'prf_second>, ExtensionReq)>,
}
/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client that
/// are credential-specific which among other things implies a non-discoverable request.
@@ -325,18 +290,18 @@ impl From<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>> for AllowedCredentials {
}
/// Helper that verifies the overlap of [`DiscoverableCredentialRequestOptions::start_ceremony`] and
/// [`DiscoverableAuthenticationServerState::decode`].
-fn validate_discoverable_options_helper(
+const fn validate_discoverable_options_helper(
ext: ServerExtensionInfo,
uv: UserVerificationRequirement,
) -> Result<(), RequestOptionsErr> {
// If PRF is set, the user has to verify themselves.
- ext.prf.as_ref().map_or(Ok(()), |_| {
- if matches!(uv, UserVerificationRequirement::Required) {
- Ok(())
- } else {
- Err(RequestOptionsErr::PrfWithoutUserVerification)
- }
- })
+ if matches!(ext.prf, ServerPrfInfo::One(_) | ServerPrfInfo::Two(_))
+ && !matches!(uv, UserVerificationRequirement::Required)
+ {
+ Err(RequestOptionsErr::PrfWithoutUserVerification)
+ } else {
+ Ok(())
+ }
}
/// Helper that verifies the overlap of [`NonDiscoverableCredentialRequestOptions::start_ceremony`] and
/// [`NonDiscoverableAuthenticationServerState::decode`].
@@ -346,34 +311,40 @@ fn validate_non_discoverable_options_helper(
) -> 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)
- }
- })
+ if matches!(cred.ext.prf, ServerPrfInfo::One(_) | ServerPrfInfo::Two(_))
+ && !matches!(uv, UserVerificationRequirement::Required)
+ {
+ Err(RequestOptionsErr::PrfWithoutUserVerification)
+ } else {
+ Ok(())
+ }
})
}
-/// The [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions)
-/// to send to the client when authenticating a credential.
+/// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions)
+/// to send to the client when authenticating a discoverable credentential.
///
/// 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 DiscoverableCredentialRequestOptions<'rp_id, 'prf>(
- pub PublicKeyCredentialRequestOptions<'rp_id, 'prf>,
-);
-impl<'rp_id, 'prf> DiscoverableCredentialRequestOptions<'rp_id, 'prf> {
- /// Creates a `DiscoverableCredentialRequestOptions` containing [`PublicKeyCredentialRequestOptions::passkey`].
+pub struct DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
+ /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
+ pub mediation: CredentialMediationRequirement,
+ /// `public-key` [credential type](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry).
+ pub public_key: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
+}
+impl<'rp_id, 'prf_first, 'prf_second>
+ DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>
+{
+ /// Creates a `DiscoverableCredentialRequestOptions` containing [`CredentialMediationRequirement::default`] and
+ /// [`PublicKeyCredentialRequestOptions::passkey`].
///
/// # Examples
///
/// ```
/// # use webauthn_rp::request::{auth::DiscoverableCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement};
/// assert!(matches!(
- /// DiscoverableCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).0.user_verification,
+ /// DiscoverableCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).public_key.user_verification,
/// UserVerificationRequirement::Required
/// ));
/// # Ok::<_, webauthn_rp::AggErr>(())
@@ -381,7 +352,10 @@ impl<'rp_id, 'prf> DiscoverableCredentialRequestOptions<'rp_id, 'prf> {
#[inline]
#[must_use]
pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self {
- Self(PublicKeyCredentialRequestOptions::passkey(rp_id))
+ Self {
+ mediation: CredentialMediationRequirement::default(),
+ public_key: PublicKeyCredentialRequestOptions::passkey(rp_id),
+ }
}
/// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming
/// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so
@@ -398,51 +372,63 @@ impl<'rp_id, 'prf> DiscoverableCredentialRequestOptions<'rp_id, 'prf> {
) -> Result<
(
DiscoverableAuthenticationServerState,
- DiscoverableAuthenticationClientState<'rp_id, 'prf>,
+ DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>,
),
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),
- )
+ let extensions = self.public_key.extensions.into();
+ validate_discoverable_options_helper(extensions, self.public_key.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.public_key.timeout).get(),
+ ))
+ .ok_or(RequestOptionsErr::InvalidTimeout)
+ .map(|expiration| {
+ (
+ DiscoverableAuthenticationServerState(AuthenticationServerState {
+ challenge: SentChallenge(self.public_key.challenge.0),
+ user_verification: self.public_key.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.
+/// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions)
+/// to send to the client when authenticating non-discoverable credententials.
///
/// 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> {
+pub struct NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
+ /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
+ mediation: CredentialMediationRequirement,
/// [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions).
- options: PublicKeyCredentialRequestOptions<'rp_id, 'prf>,
+ options: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
/// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
allow_credentials: AllowedCredentials,
}
-impl<'rp_id, 'prf> NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
+impl<'rp_id, 'prf_first, 'prf_second>
+ NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>
+{
+ /// Returns a mutable reference to the `CredentialMediationRequirement`.
+ #[inline]
+ pub const fn mediation(&mut self) -> &mut CredentialMediationRequirement {
+ &mut self.mediation
+ }
/// Returns a mutable reference to the configurable options.
#[inline]
- pub const fn options(&mut self) -> &mut PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
+ pub const fn options(
+ &mut self,
+ ) -> &mut PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
&mut self.options
}
/// Returns a reference to the [`AllowedCredential`]s.
@@ -452,7 +438,8 @@ impl<'rp_id, 'prf> NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
&self.allow_credentials
}
/// Creates a `NonDiscoverableCredentialRequestOptions` containing
- /// [`PublicKeyCredentialRequestOptions::second_factor`] and the passed [`AllowedCredentials`].
+ /// [`CredentialMediationRequirement::Optional`],
+ /// [`PublicKeyCredentialRequestOptions::second_factor`], and the passed [`AllowedCredentials`].
///
/// # Errors
///
@@ -506,6 +493,7 @@ impl<'rp_id, 'prf> NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
Err(SecondFactorErr)
} else {
Ok(Self {
+ mediation: CredentialMediationRequirement::default(),
options: PublicKeyCredentialRequestOptions::second_factor(rp_id),
allow_credentials,
})
@@ -526,7 +514,7 @@ impl<'rp_id, 'prf> NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
) -> Result<
(
NonDiscoverableAuthenticationServerState,
- NonDiscoverableAuthenticationClientState<'rp_id, 'prf>,
+ NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>,
),
RequestOptionsErr,
> {
@@ -571,7 +559,7 @@ impl<'rp_id, 'prf> NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
///
/// This does _not_ contain [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
#[derive(Debug)]
-pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
+pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
/// [`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).
@@ -589,9 +577,9 @@ pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
/// [`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>,
+ pub extensions: Extension<'prf_first, 'prf_second>,
}
-impl<'rp_id> PublicKeyCredentialRequestOptions<'rp_id, '_> {
+impl<'rp_id> PublicKeyCredentialRequestOptions<'rp_id, '_, '_> {
/// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to
/// [`UserVerificationRequirement::Required`] and [`Self::timeout`] set to 5 minutes,
///
@@ -647,28 +635,36 @@ impl<'rp_id> PublicKeyCredentialRequestOptions<'rp_id, '_> {
/// 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>,
+pub struct DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>(
+ DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
);
-impl<'rp_id, 'prf> DiscoverableAuthenticationClientState<'rp_id, 'prf> {
+impl<'rp_id, 'prf_first, 'prf_second>
+ DiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>
+{
/// Returns the `DiscoverableCredentialRequestOptions` that was used to start an authentication ceremony.
#[inline]
#[must_use]
- pub const fn options(&self) -> &DiscoverableCredentialRequestOptions<'rp_id, 'prf> {
+ pub const fn options(
+ &self,
+ ) -> &DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
&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>,
+pub struct NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>(
+ NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
);
-impl<'rp_id, 'prf> NonDiscoverableAuthenticationClientState<'rp_id, 'prf> {
+impl<'rp_id, 'prf_first, 'prf_second>
+ NonDiscoverableAuthenticationClientState<'rp_id, 'prf_first, 'prf_second>
+{
/// Returns the `NonDiscoverableCredentialRequestOptions` that was used to start an authentication ceremony.
#[inline]
#[must_use]
- pub const fn options(&self) -> &NonDiscoverableCredentialRequestOptions<'rp_id, 'prf> {
+ pub const fn options(
+ &self,
+ ) -> &NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
&self.0
}
}
@@ -705,18 +701,14 @@ impl CredPrf {
/// [`DiscoverableAuthenticationServerState`] in an in-memory collection.
#[derive(Clone, Copy, Debug)]
enum ServerPrfInfo {
+ /// No `PrfInput`.
+ None,
/// `PrfInput::second` was `None`.
One(ExtensionReq),
/// `PrfInput::second` was `Some`.
Two(ExtensionReq),
}
impl ServerPrfInfo {
- /// Returns the `ExtensionReq` sent to the client.
- const fn ext_info(self) -> ExtensionReq {
- match self {
- Self::One(info) | Self::Two(info) => info,
- }
- }
/// Validates `val` based on the passed arguments.
///
/// It's not possible to request the PRF extension without sending `UserVerificationRequirement::Required`;
@@ -724,33 +716,36 @@ impl ServerPrfInfo {
/// However when we _don't_ send the PRF extension _and_ we don't error on an unsolicited response, it's
/// possible to receive an `HmacSecret` without the user having been verified; thus we only ensure
/// `user_verified` is true when we don't error on unsolicted responses _and_ we didn't send the PRF extension.
- fn validate(
- val: Option<Self>,
+ const fn validate(
+ self,
user_verified: bool,
cred_prf: CredPrf,
hmac: HmacSecret,
err_unsolicited: bool,
) -> Result<(), ExtensionErr> {
match hmac {
- HmacSecret::None => val.map_or(Ok(()), |input| {
- if matches!(input.ext_info(), ExtensionReq::Allow) {
- if cred_prf.is_prf_capable() {
- Ok(())
- } else {
- Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
- }
- } else {
- match cred_prf {
- CredPrf::None | CredPrf::TrueNoHmac => Ok(()),
- CredPrf::FalseNoHmac | CredPrf::FalseFalseHmac => {
+ HmacSecret::None => match self {
+ Self::None => Ok(()),
+ Self::One(req) | Self::Two(req) => {
+ if matches!(req, ExtensionReq::Allow) {
+ if cred_prf.is_prf_capable() {
+ Ok(())
+ } else {
Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
}
- CredPrf::TrueTrueHmac => Err(ExtensionErr::MissingHmacSecret),
+ } else {
+ match cred_prf {
+ CredPrf::None | CredPrf::TrueNoHmac => Ok(()),
+ CredPrf::FalseNoHmac | CredPrf::FalseFalseHmac => {
+ Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
+ }
+ CredPrf::TrueTrueHmac => Err(ExtensionErr::MissingHmacSecret),
+ }
}
}
- }),
- HmacSecret::One => val.map_or_else(
- || {
+ },
+ HmacSecret::One => match self {
+ Self::None => {
if err_unsolicited {
Err(ExtensionErr::ForbiddenHmacSecret)
} else if cred_prf.is_prf_capable() {
@@ -762,24 +757,21 @@ impl ServerPrfInfo {
} else {
Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
}
- },
- |info| {
- if matches!(info, Self::One(_)) {
- if cred_prf.is_prf_capable() {
- Ok(())
- } else {
- Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
- }
+ }
+ Self::One(_) => {
+ if cred_prf.is_prf_capable() {
+ Ok(())
} else {
- Err(ExtensionErr::InvalidHmacSecretValue(
- OneOrTwo::Two,
- OneOrTwo::One,
- ))
+ Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
}
- },
- ),
- HmacSecret::Two => val.map_or_else(
- || {
+ }
+ Self::Two(_) => Err(ExtensionErr::InvalidHmacSecretValue(
+ OneOrTwo::Two,
+ OneOrTwo::One,
+ )),
+ },
+ HmacSecret::Two => match self {
+ Self::None => {
if err_unsolicited {
Err(ExtensionErr::ForbiddenHmacSecret)
} else if cred_prf.is_prf_capable() {
@@ -791,22 +783,19 @@ impl ServerPrfInfo {
} else {
Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
}
- },
- |info| {
- if matches!(info, Self::Two(_)) {
- if cred_prf.is_prf_capable() {
- Ok(())
- } else {
- Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
- }
+ }
+ Self::One(_) => Err(ExtensionErr::InvalidHmacSecretValue(
+ OneOrTwo::One,
+ OneOrTwo::Two,
+ )),
+ Self::Two(_) => {
+ if cred_prf.is_prf_capable() {
+ Ok(())
} else {
- Err(ExtensionErr::InvalidHmacSecretValue(
- OneOrTwo::One,
- OneOrTwo::Two,
- ))
+ Err(ExtensionErr::PrfRequestedForPrfIncapableCred)
}
- },
- ),
+ }
+ },
}
}
}
@@ -814,16 +803,19 @@ impl ServerPrfInfo {
impl PartialEq for ServerPrfInfo {
fn eq(&self, other: &Self) -> bool {
match *self {
+ Self::None => matches!(*other, Self::None),
Self::One(req) => matches!(*other, Self::One(req2) if req == req2),
Self::Two(req) => matches!(*other, Self::Two(req2) if req == req2),
}
}
}
-impl From<PrfInput<'_>> for ServerPrfInfo {
- fn from(value: PrfInput<'_>) -> Self {
- value
- .second
- .map_or_else(|| Self::One(value.ext_info), |_| Self::Two(value.ext_info))
+impl From<Option<(PrfInput<'_, '_>, ExtensionReq)>> for ServerPrfInfo {
+ fn from(value: Option<(PrfInput<'_, '_>, ExtensionReq)>) -> Self {
+ value.map_or(Self::None, |val| {
+ val.0
+ .second
+ .map_or_else(|| Self::One(val.1), |_| Self::Two(val.1))
+ })
}
}
impl From<&PrfInputOwned> for ServerPrfInfo {
@@ -831,7 +823,7 @@ impl From<&PrfInputOwned> for ServerPrfInfo {
value
.second
.as_ref()
- .map_or_else(|| Self::One(value.ext_info), |_| Self::Two(value.ext_info))
+ .map_or_else(|| Self::One(value.ext_req), |_| Self::Two(value.ext_req))
}
}
/// `Extension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`]
@@ -839,12 +831,12 @@ impl From<&PrfInputOwned> for ServerPrfInfo {
#[derive(Clone, Copy, Debug)]
struct ServerExtensionInfo {
/// `Extension::prf`.
- prf: Option<ServerPrfInfo>,
+ prf: ServerPrfInfo,
}
-impl From<Extension<'_>> for ServerExtensionInfo {
- fn from(value: Extension<'_>) -> Self {
+impl From<Extension<'_, '_>> for ServerExtensionInfo {
+ fn from(value: Extension<'_, '_>) -> Self {
Self {
- prf: value.prf.map(ServerPrfInfo::from),
+ prf: value.prf.into(),
}
}
}
@@ -859,7 +851,7 @@ impl PartialEq for ServerExtensionInfo {
#[derive(Clone, Copy, Debug)]
struct ServerCredSpecificExtensionInfo {
/// `CredentialSpecificExtension::prf`.
- prf: Option<ServerPrfInfo>,
+ prf: ServerPrfInfo,
}
#[cfg(test)]
impl PartialEq for ServerCredSpecificExtensionInfo {
@@ -870,7 +862,10 @@ impl PartialEq for ServerCredSpecificExtensionInfo {
impl From<&CredentialSpecificExtension> for ServerCredSpecificExtensionInfo {
fn from(value: &CredentialSpecificExtension) -> Self {
Self {
- prf: value.prf.as_ref().map(ServerPrfInfo::from),
+ prf: value
+ .prf
+ .as_ref()
+ .map_or(ServerPrfInfo::None, ServerPrfInfo::from),
}
}
}
@@ -878,7 +873,7 @@ impl ServerExtensionInfo {
/// Validates the extensions.
///
/// Note that this MUST only be called internally by `auth::validate_extensions`.
- fn validate_extensions(
+ const fn validate_extensions(
self,
user_verified: bool,
auth_ext: AuthenticatorExtensionOutput,
@@ -905,31 +900,26 @@ fn validate_extensions(
) -> Result<(), ExtensionErr> {
cred_ext.map_or_else(
|| {
- // No client-specific extensions, so we can simply focus on `ext`.
+ // No credental-specific extensions, so we can simply focus on `ext`.
ext.validate_extensions(user_verified, auth_ext, error_unsolicited, cred_prf)
},
|c_ext| {
// Must carefully process each extension based on overlap and which gets priority over the other.
- c_ext.prf.as_ref().map_or_else(
- || {
- ServerPrfInfo::validate(
- ext.prf,
- user_verified,
- cred_prf,
- auth_ext.hmac_secret,
- error_unsolicited,
- )
- },
- |_| {
- ServerPrfInfo::validate(
- c_ext.prf,
- user_verified,
- cred_prf,
- auth_ext.hmac_secret,
- error_unsolicited,
- )
- },
- )
+ if matches!(c_ext.prf, ServerPrfInfo::None) {
+ ext.prf.validate(
+ user_verified,
+ cred_prf,
+ auth_ext.hmac_secret,
+ error_unsolicited,
+ )
+ } else {
+ c_ext.prf.validate(
+ user_verified,
+ cred_prf,
+ auth_ext.hmac_secret,
+ error_unsolicited,
+ )
+ }
},
)
}
@@ -1717,7 +1707,7 @@ mod tests {
const CBOR_MAP: u8 = 0b101_00000;
#[test]
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
- fn ed25519_auth_ser() -> Result<(), AggErr> {
+ fn eddsa_auth_ser() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let mut creds = AllowedCredentials::with_capacity(1);
creds.push(AllowedCredential {
@@ -1729,7 +1719,7 @@ mod tests {
prf: Some(PrfInputOwned {
first: Vec::new(),
second: Some(Vec::new()),
- ext_info: ExtensionReq::Require,
+ ext_req: ExtensionReq::Require,
}),
},
});
@@ -1897,8 +1887,8 @@ mod tests {
)?)
);
let mut opts_2 = DiscoverableCredentialRequestOptions::passkey(&rp_id);
- opts_2.0.challenge = Challenge(0);
- opts_2.0.extensions = Extension { prf: None };
+ opts_2.public_key.challenge = Challenge(0);
+ opts_2.public_key.extensions = Extension { prf: None };
let server_2 = opts_2.start_ceremony()?.0;
assert!(
server_2.is_eq(&DiscoverableAuthenticationServerState::decode(
@@ -1937,7 +1927,7 @@ mod tests {
/// `true` iff `UserVerificationRequirement::Required` should be used; otherwise
/// `UserVerificationRequirement::Preferred` is used.
None(bool),
- Prf(PrfInput<'static>),
+ Prf((PrfInput<'static, 'static>, ExtensionReq)),
}
#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
#[derive(Clone, Copy)]
@@ -2119,17 +2109,17 @@ mod tests {
client_data_json_relaxed: false,
};
let mut opts = DiscoverableCredentialRequestOptions::passkey(&rp_id);
- opts.0.challenge = Challenge(0);
- opts.0.user_verification = UserVerificationRequirement::Preferred;
+ opts.public_key.challenge = Challenge(0);
+ opts.public_key.user_verification = UserVerificationRequirement::Preferred;
match options.request.prf_uv {
PrfUvOptions::None(required) => {
if required {
- opts.0.user_verification = UserVerificationRequirement::Required;
+ opts.public_key.user_verification = UserVerificationRequirement::Required;
};
}
PrfUvOptions::Prf(input) => {
- opts.0.user_verification = UserVerificationRequirement::Required;
- opts.0.extensions.prf = Some(input);
+ opts.public_key.user_verification = UserVerificationRequirement::Required;
+ opts.public_key.extensions.prf = Some(input);
}
}
let mut cred = AuthenticatedCredential::new(
@@ -2197,26 +2187,34 @@ mod tests {
const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true];
const ALL_NOT_FALSE_PRF_UV_OPTIONS: [PrfUvOptions; 5] = [
PrfUvOptions::None(true),
- PrfUvOptions::Prf(PrfInput {
- first: [].as_slice(),
- second: None,
- ext_info: ExtensionReq::Require,
- }),
- PrfUvOptions::Prf(PrfInput {
- first: [].as_slice(),
- second: None,
- ext_info: ExtensionReq::Allow,
- }),
- PrfUvOptions::Prf(PrfInput {
- first: [].as_slice(),
- second: Some([].as_slice()),
- ext_info: ExtensionReq::Require,
- }),
- PrfUvOptions::Prf(PrfInput {
- first: [].as_slice(),
- second: Some([].as_slice()),
- ext_info: ExtensionReq::Allow,
- }),
+ PrfUvOptions::Prf((
+ PrfInput {
+ first: [].as_slice(),
+ second: None,
+ },
+ ExtensionReq::Require,
+ )),
+ PrfUvOptions::Prf((
+ PrfInput {
+ first: [].as_slice(),
+ second: None,
+ },
+ ExtensionReq::Allow,
+ )),
+ PrfUvOptions::Prf((
+ PrfInput {
+ first: [].as_slice(),
+ second: Some([].as_slice()),
+ },
+ ExtensionReq::Require,
+ )),
+ PrfUvOptions::Prf((
+ PrfInput {
+ first: [].as_slice(),
+ second: Some([].as_slice()),
+ },
+ ExtensionReq::Allow,
+ )),
];
for cred_protect in ALL_CRED_PROTECT_OPTIONS {
for prf in ALL_PRF_CRED_OPTIONS {
@@ -2286,11 +2284,13 @@ mod tests {
let mut opts = TestOptions {
request: TestRequestOptions {
error_unsolicited: false,
- prf_uv: PrfUvOptions::Prf(PrfInput {
- first: [].as_slice(),
- second: None,
- ext_info: ExtensionReq::Allow,
- }),
+ prf_uv: PrfUvOptions::Prf((
+ PrfInput {
+ first: [].as_slice(),
+ second: None,
+ },
+ ExtensionReq::Allow,
+ )),
},
response: TestResponseOptions {
user_verified: true,
@@ -2302,19 +2302,23 @@ mod tests {
},
};
validate(opts)?;
- opts.request.prf_uv = PrfUvOptions::Prf(PrfInput {
- first: [].as_slice(),
- second: None,
- ext_info: ExtensionReq::Require,
- });
+ opts.request.prf_uv = PrfUvOptions::Prf((
+ PrfInput {
+ first: [].as_slice(),
+ second: None,
+ },
+ ExtensionReq::Require,
+ ));
opts.cred.prf = PrfCredOptions::TrueHmacTrue;
assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::AuthCeremony(auth_err) if matches!(auth_err, AuthCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::MissingHmacSecret))), |_| false));
opts.response.hmac = HmacSecret::One;
- opts.request.prf_uv = PrfUvOptions::Prf(PrfInput {
- first: [].as_slice(),
- second: None,
- ext_info: ExtensionReq::Allow,
- });
+ opts.request.prf_uv = PrfUvOptions::Prf((
+ PrfInput {
+ first: [].as_slice(),
+ second: None,
+ },
+ ExtensionReq::Allow,
+ ));
opts.cred.prf = PrfCredOptions::TrueNoHmac;
validate(opts)?;
opts.response.hmac = HmacSecret::Two;
diff --git a/src/request/auth/ser.rs b/src/request/auth/ser.rs
@@ -1,56 +1,10 @@
use super::{
AllowedCredential, AllowedCredentials, Credentials as _, DiscoverableAuthenticationClientState,
- Extension, NonDiscoverableAuthenticationClientState, PrfInput, PrfInputOwned,
+ DiscoverableCredentialRequestOptions, Extension, NonDiscoverableAuthenticationClientState,
+ NonDiscoverableCredentialRequestOptions, PrfInput, PrfInputOwned,
PublicKeyCredentialRequestOptions,
};
-use data_encoding::BASE64URL_NOPAD;
use serde::ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer};
-impl Serialize for PrfInput<'_> {
- /// Serializes `self` to conform with
- /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
- ///
- /// # Examples
- ///
- /// ```
- /// # use webauthn_rp::request::{auth::PrfInput, ExtensionReq};
- /// assert_eq!(
- /// serde_json::to_string(&PrfInput {
- /// first: [0; 4].as_slice(),
- /// second: Some([2; 1].as_slice()),
- /// ext_info: ExtensionReq::Require
- /// })?,
- /// r#"{"first":"AAAAAA","second":"Ag"}"#
- /// );
- /// # Ok::<_, serde_json::Error>(())
- /// ```
- #[expect(
- clippy::arithmetic_side_effects,
- reason = "comment justifies how overflow is not possible"
- )]
- #[inline]
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: Serializer,
- {
- serializer
- // The max value is 1 + 1 = 2, so overflow is not an issue.
- .serialize_struct("PrfInput", 1 + usize::from(self.second.is_some()))
- .and_then(|mut ser| {
- ser.serialize_field("first", BASE64URL_NOPAD.encode(self.first).as_str())
- .and_then(|()| {
- self.second
- .as_ref()
- .map_or(Ok(()), |second| {
- ser.serialize_field(
- "second",
- BASE64URL_NOPAD.encode(second).as_str(),
- )
- })
- .and_then(|()| ser.end())
- })
- })
- }
-}
impl Serialize for PrfInputOwned {
/// See [`PrfInput::serialize`]
#[inline]
@@ -61,7 +15,6 @@ impl Serialize for PrfInputOwned {
PrfInput {
first: self.first.as_slice(),
second: self.second.as_deref(),
- ext_info: self.ext_info,
}
.serialize(serializer)
}
@@ -179,13 +132,13 @@ impl Serialize for PrfCreds<'_> {
}
}
/// [`AuthenticationExtensionsPRFInputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfinputs).
-struct PrfInputs<'a, 'b> {
+struct PrfInputs<'a, 'b, 'c> {
/// [`eval`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-eval).
- eval: Option<PrfInput<'a>>,
+ eval: Option<PrfInput<'a, 'b>>,
/// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential).
- eval_by_credential: PrfCreds<'b>,
+ eval_by_credential: PrfCreds<'c>,
}
-impl Serialize for PrfInputs<'_, '_> {
+impl Serialize for PrfInputs<'_, '_, '_> {
#[expect(
clippy::arithmetic_side_effects,
reason = "comment explains how overflow is not possible"
@@ -217,15 +170,15 @@ impl Serialize for PrfInputs<'_, '_> {
}
/// Serializes `self` to conform with
/// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson).
-struct ExtensionHelper<'a, 'b> {
+struct ExtensionHelper<'a, 'b, 'c> {
/// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions).
- extension: &'a Extension<'b>,
+ extension: &'a Extension<'b, 'c>,
/// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions).
///
/// Some extensions contain records, so we need both this and above.
allow_credentials: &'a AllowedCredentials,
}
-impl Serialize for ExtensionHelper<'_, '_> {
+impl Serialize for ExtensionHelper<'_, '_, '_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -241,7 +194,7 @@ impl Serialize for ExtensionHelper<'_, '_> {
ser.serialize_field(
"prf",
&PrfInputs {
- eval: self.extension.prf,
+ eval: self.extension.prf.map(|prf| prf.0),
eval_by_credential: PrfCreds(self.allow_credentials),
},
)
@@ -252,18 +205,18 @@ impl Serialize for ExtensionHelper<'_, '_> {
}
/// Helper type that peforms the serialization for both [`DiscoverableAuthenticationClientState`] and
/// [`NonDiscoverableAuthenticationClientState`] and
-struct AuthenticationClientState<'rp_id, 'prf, 'opt, 'cred>(
- &'opt PublicKeyCredentialRequestOptions<'rp_id, 'prf>,
+struct AuthenticationClientState<'rp_id, 'prf_first, 'prf_second, 'opt, 'cred>(
+ &'opt PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
&'cred AllowedCredentials,
);
-impl Serialize for AuthenticationClientState<'_, '_, '_, '_> {
+impl Serialize for AuthenticationClientState<'_, '_, '_, '_, '_> {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer
- .serialize_struct("AuthenticationClientState", 9)
+ .serialize_struct("PublicKeyCredentialRequestOptions", 7)
.and_then(|mut ser| {
ser.serialize_field("challenge", &self.0.challenge)
.and_then(|()| {
@@ -299,9 +252,69 @@ impl Serialize for AuthenticationClientState<'_, '_, '_, '_> {
})
}
}
-impl Serialize for DiscoverableAuthenticationClientState<'_, '_> {
+/// `"mediation"`.
+const MEDIATION: &str = "mediation";
+/// `"publicKey"`.
+const PUBLIC_KEY: &str = "publicKey";
+impl Serialize for DiscoverableCredentialRequestOptions<'_, '_, '_> {
+ /// Serializes `self` to conform with
+ /// [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions).
+ ///
+ /// Note [`signal`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-signal)
+ /// is not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry)
+ /// is serialized to conform to
+ /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("DiscoverableCredentialRequestOptions", 2)
+ .and_then(|mut ser| {
+ ser.serialize_field(MEDIATION, &self.mediation)
+ .and_then(|()| {
+ ser.serialize_field(
+ PUBLIC_KEY,
+ &AuthenticationClientState(
+ &self.public_key,
+ &AllowedCredentials::with_capacity(0),
+ ),
+ )
+ .and_then(|()| ser.end())
+ })
+ })
+ }
+}
+impl Serialize for NonDiscoverableCredentialRequestOptions<'_, '_, '_> {
/// Serializes `self` to conform with
+ /// [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions).
+ ///
+ /// Note [`signal`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-signal)
+ /// is not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry)
+ /// is serialized to conform to
/// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("NonDiscoverableCredentialRequestOptions", 2)
+ .and_then(|mut ser| {
+ ser.serialize_field(MEDIATION, &self.mediation)
+ .and_then(|()| {
+ ser.serialize_field(
+ PUBLIC_KEY,
+ &AuthenticationClientState(&self.options, &self.allow_credentials),
+ )
+ .and_then(|()| ser.end())
+ })
+ })
+ }
+}
+impl Serialize for DiscoverableAuthenticationClientState<'_, '_, '_> {
+ /// Serializes `self` according to [`DiscoverableCredentialRequestOptions::serialize`].
///
/// # Examples
///
@@ -310,46 +323,48 @@ impl Serialize for DiscoverableAuthenticationClientState<'_, '_> {
/// # request::{
/// # auth::{
/// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension,
- /// # PrfInput, PrfInputOwned, DiscoverableCredentialRequestOptions
+ /// # PrfInputOwned, DiscoverableCredentialRequestOptions
/// # },
- /// # AsciiDomain, ExtensionReq, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
+ /// # AsciiDomain, ExtensionReq, Hint, PrfInput, 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 {
+ /// options.public_key.hints = Hint::SecurityKey;
+ /// options.public_key.extensions = Extension {
+ /// prf: Some((PrfInput {
/// first: [0; 4].as_slice(),
/// second: None,
- /// ext_info: ExtensionReq::Require,
- /// }),
+ /// }, 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"
- /// },
+ /// "mediation":"optional",
+ /// "publicKey":{
+ /// "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));
+ /// assert_eq!(client_state.get(..50), json.get(..50));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// assert_eq!(client_state.get(36..), json.get(36..));
+ /// assert_eq!(client_state.get(72..), json.get(72..));
/// # Ok::<_, webauthn_rp::AggErr>(())
/// ```
#[inline]
@@ -357,13 +372,11 @@ impl Serialize for DiscoverableAuthenticationClientState<'_, '_> {
where
S: Serializer,
{
- AuthenticationClientState(&self.0.0, &AllowedCredentials::with_capacity(0))
- .serialize(serializer)
+ self.0.serialize(serializer)
}
}
-impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_> {
- /// Serializes `self` to conform with
- /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
+impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_, '_> {
+ /// Serializes `self` according to [`NonDiscoverableCredentialRequestOptions::serialize`].
///
/// # Examples
///
@@ -374,9 +387,9 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_> {
/// # request::{
/// # auth::{
/// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension,
- /// # PrfInput, PrfInputOwned, NonDiscoverableCredentialRequestOptions
+ /// # PrfInputOwned, NonDiscoverableCredentialRequestOptions
/// # },
- /// # AsciiDomain, ExtensionReq, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
+ /// # AsciiDomain, ExtensionReq, Hint, PrfInput, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
/// # },
/// # response::{AuthTransports, CredentialId},
/// # };
@@ -402,7 +415,7 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_> {
/// prf: Some(PrfInputOwned {
/// first: vec![2; 6],
/// second: Some(vec![3; 2]),
- /// ext_info: ExtensionReq::Require,
+ /// ext_req: ExtensionReq::Require,
/// }),
/// },
/// });
@@ -412,18 +425,17 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_> {
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// let opts = options.options();
/// # #[cfg(not(all(feature = "bin", feature = "custom")))]
- /// # let mut opts = webauthn_rp::DiscoverableCredentialRequestOptions::passkey(&rp_id).0;
+ /// # let mut opts = webauthn_rp::DiscoverableCredentialRequestOptions::passkey(&rp_id).public_key;
/// 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.
/// opts.extensions = Extension {
- /// prf: Some(PrfInput {
+ /// prf: Some((PrfInput {
/// first: [0; 4].as_slice(),
/// second: None,
- /// ext_info: ExtensionReq::Require,
- /// }),
+ /// }, ExtensionReq::Require)),
/// };
/// // Since we are requesting the PRF extension, we must require user verification; otherwise
/// // `NonDiscoverableCredentialRequestOptions::start_ceremony` would error.
@@ -431,29 +443,32 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_> {
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// 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":[
- /// {
- /// "type":"public-key",
- /// "id":"AAAAAAAAAAAAAAAAAAAAAA",
- /// "transports":["usb"]
- /// }
- /// ],
- /// "userVerification":"required",
- /// "hints":[
- /// "security-key"
- /// ],
- /// "extensions":{
- /// "prf":{
- /// "eval":{
- /// "first":"AAAAAA"
- /// },
- /// "evalByCredential":{
- /// "AAAAAAAAAAAAAAAAAAAAAA":{
- /// "first":"AgICAgIC",
- /// "second":"AwM"
+ /// "mediation":"optional",
+ /// "publicKey":{
+ /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA",
+ /// "timeout":300000,
+ /// "rpId":"example.com",
+ /// "allowCredentials":[
+ /// {
+ /// "type":"public-key",
+ /// "id":"AAAAAAAAAAAAAAAAAAAAAA",
+ /// "transports":["usb"]
+ /// }
+ /// ],
+ /// "userVerification":"required",
+ /// "hints":[
+ /// "security-key"
+ /// ],
+ /// "extensions":{
+ /// "prf":{
+ /// "eval":{
+ /// "first":"AAAAAA"
+ /// },
+ /// "evalByCredential":{
+ /// "AAAAAAAAAAAAAAAAAAAAAA":{
+ /// "first":"AgICAgIC",
+ /// "second":"AwM"
+ /// }
/// }
/// }
/// }
@@ -462,9 +477,9 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_> {
/// // 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));
+ /// assert_eq!(client_state.get(..50), json.get(..50));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// assert_eq!(client_state.get(36..), json.get(36..));
+ /// assert_eq!(client_state.get(72..), json.get(72..));
/// # Ok::<_, webauthn_rp::AggErr>(())
/// ```
#[inline]
@@ -472,6 +487,6 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_> {
where
S: Serializer,
{
- AuthenticationClientState(&self.0.options, &self.0.allow_credentials).serialize(serializer)
+ self.0.serialize(serializer)
}
}
diff --git a/src/request/auth/ser_server_state.rs b/src/request/auth/ser_server_state.rs
@@ -19,24 +19,25 @@ use std::time::{SystemTime, SystemTimeError};
impl EncodeBuffer for ServerPrfInfo {
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
match *self {
+ Self::None => 0u8.encode_into_buffer(buffer),
Self::One(req) => {
- 0u8.encode_into_buffer(buffer);
- req
+ 1u8.encode_into_buffer(buffer);
+ req.encode_into_buffer(buffer);
}
Self::Two(req) => {
- 1u8.encode_into_buffer(buffer);
- req
+ 2u8.encode_into_buffer(buffer);
+ req.encode_into_buffer(buffer);
}
}
- .encode_into_buffer(buffer);
}
}
impl<'a> DecodeBuffer<'a> for ServerPrfInfo {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
u8::decode_from_buffer(data).and_then(|val| match val {
- 0 => ExtensionReq::decode_from_buffer(data).map(Self::One),
- 1 => ExtensionReq::decode_from_buffer(data).map(Self::Two),
+ 0 => Ok(Self::None),
+ 1 => ExtensionReq::decode_from_buffer(data).map(Self::One),
+ 2 => ExtensionReq::decode_from_buffer(data).map(Self::Two),
_ => Err(EncDecErr),
})
}
@@ -49,7 +50,7 @@ impl EncodeBuffer for ServerCredSpecificExtensionInfo {
impl<'a> DecodeBuffer<'a> for ServerCredSpecificExtensionInfo {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- Option::decode_from_buffer(data).map(|prf| Self { prf })
+ ServerPrfInfo::decode_from_buffer(data).map(|prf| Self { prf })
}
}
impl EncodeBuffer for CredInfo {
@@ -74,7 +75,7 @@ impl EncodeBuffer for ServerExtensionInfo {
impl<'a> DecodeBuffer<'a> for ServerExtensionInfo {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- Option::decode_from_buffer(data).map(|prf| Self { prf })
+ ServerPrfInfo::decode_from_buffer(data).map(|prf| Self { prf })
}
}
impl EncodeBuffer for SignatureCounterEnforcement {
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -6,15 +6,15 @@ use super::{
AuthenticatorAttachment,
register::{
Attestation, AttestationFormat, AuthenticatorExtensionOutput,
- ClientExtensionsOutputs, CredentialProtectionPolicy, Registration,
+ ClientExtensionsOutputs, CredentialProtectionPolicy, HmacSecret, Registration,
UncompressedPubKey,
error::{ExtensionErr, RegCeremonyErr},
},
},
},
BackupReq, Ceremony, Challenge, CredentialMediationRequirement, ExtensionInfo, ExtensionReq,
- Hint, Origin, PublicKeyCredentialDescriptor, RpId, SentChallenge, THREE_HUNDRED_THOUSAND,
- TimedCeremony, UserVerificationRequirement,
+ Hint, Origin, PrfInput, PublicKeyCredentialDescriptor, RpId, SentChallenge,
+ THREE_HUNDRED_THOUSAND, TimedCeremony, UserVerificationRequirement,
register::error::{CreationOptionsErr, NicknameErr, UsernameErr},
};
#[cfg(doc)]
@@ -694,7 +694,7 @@ impl Default for FourToSixtyThree {
}
/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client.
#[derive(Clone, Copy, Debug, Default)]
-pub struct Extension {
+pub struct Extension<'prf_first, 'prf_second> {
/// [`credProps`](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension).
///
/// The best one can do to ensure a server-side credential is created is by sending
@@ -729,217 +729,25 @@ pub struct Extension {
/// and [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) does not
/// exist or is `null`. This is to ensure the decrypted outputs stay on the client.
///
- /// Note for
- /// [CTAP 2.2](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
- /// this is only used to instruct the authenticator to create the necessary `hmac-secret` since it does not
- /// currently support PRF evaluation at creation time. This also requires
- /// [`UserVerificationRequirement::Required`].
- ///
- /// While many authenticators will be able to use the `prf` extension during authentication when this is not
- /// passed during registration, it is recommended to still pass this during registration in the event the
- /// authenticator does require it during registration.
- pub prf: Option<ExtensionInfo>,
-}
-impl Extension {
- /// Validates the extensions.
- fn validate(
- self,
- client_ext: ClientExtensionsOutputs,
- auth_ext: AuthenticatorExtensionOutput,
- error_unsolicited: bool,
- ) -> Result<(), ExtensionErr> {
- if error_unsolicited {
- self.validate_unsolicited(client_ext, auth_ext)
- } else {
- Ok(())
- }
- .and_then(|()| {
- self.validate_required(client_ext, auth_ext)
- .and_then(|()| self.validate_value(client_ext, auth_ext))
- })
- }
- /// Validates if there are any unsolicited extensions.
- ///
- /// Note no distinction is made between an extension that is empty and one that is not (i.e., we are checking
- /// purely for the existence of extension keys).
- fn validate_unsolicited(
- mut self,
- client_ext: ClientExtensionsOutputs,
- auth_ext: AuthenticatorExtensionOutput,
- ) -> Result<(), ExtensionErr> {
- // For simpler code, we artificially set non-requested extensions after verifying there was not an error
- // and recursively call this function. There are so few extensions and the checks are fast that there
- // should be no worry of stack overflow or performance overhead.
- if self.cred_props.is_some() {
- if !matches!(self.cred_protect, CredProtect::None) {
- if self.min_pin_length.is_some() {
- // This is the last extension, so recursion stops here.
- if self.prf.is_some() {
- Ok(())
- } else if client_ext.prf.is_some() {
- Err(ExtensionErr::ForbiddenPrf)
- } else if auth_ext.hmac_secret.is_some() {
- Err(ExtensionErr::ForbiddenHmacSecret)
- } else {
- Ok(())
- }
- } else if auth_ext.min_pin_length.is_some() {
- Err(ExtensionErr::ForbiddenMinPinLength)
- } else {
- // Pretend to set `minPinLength`, so we can check `prf`.
- self.min_pin_length =
- Some((FourToSixtyThree::MIN, ExtensionInfo::RequireEnforceValue));
- self.validate_unsolicited(client_ext, auth_ext)
- }
- } else if !matches!(auth_ext.cred_protect, CredentialProtectionPolicy::None) {
- Err(ExtensionErr::ForbiddenCredProtect)
- } else {
- // Pretend to set `credProtect`, so we can check `minPinLength` and `prf` extensions.
- self.cred_protect = CredProtect::UserVerificationOptional(
- false,
- ExtensionInfo::RequireEnforceValue,
- );
- self.validate_unsolicited(client_ext, auth_ext)
- }
- } else if client_ext.cred_props.is_some() {
- Err(ExtensionErr::ForbiddenCredProps)
- } else {
- // Pretend to set `credProps`; so we can check `credProtect`, `minPinLength`, and `prf` extensions.
- self.cred_props = Some(ExtensionReq::Require);
- self.validate_unsolicited(client_ext, auth_ext)
- }
- }
- /// Validates if any required extensions don't have a corresponding response.
- ///
- /// Note empty extensions are treated as missing. For example when requiring the `credProps` extension,
- /// all of the following responses would lead to a failure:
- /// `{"clientExtensionResults":{}}`: no extensions.
- /// `{"clientExtensionResults":{"prf":true}}`: only the `prf` extension.
- /// `{"clientExtensionResults":{"credProps":{}}}`: empty `credProps` extension.
- /// `{"clientExtensionResults":{"credProps":{"foo":false}}}`: `credProps` extension doesn't contain at least one
- /// expected field (i.e., still "empty").
- fn validate_required(
- self,
- client_ext: ClientExtensionsOutputs,
- auth_ext: AuthenticatorExtensionOutput,
- ) -> Result<(), ExtensionErr> {
- // We don't check `self.cred_protect` since `CredProtect::validate` checks for both a required response
- // and value enforcement; thus it only needs to be checked once (which it is in `Self::validate_value`).
- self.cred_props
- .map_or(Ok(()), |info| {
- if matches!(info, ExtensionReq::Require) {
- if client_ext
- .cred_props
- .is_some_and(|props| props.rk.is_some())
- {
- Ok(())
- } else {
- Err(ExtensionErr::MissingCredProps)
- }
- } else {
- Ok(())
- }
- })
- .and_then(|()| {
- self.min_pin_length
- .map_or(Ok(()), |info| {
- if matches!(
- info.1,
- ExtensionInfo::RequireEnforceValue
- | ExtensionInfo::RequireDontEnforceValue
- ) {
- auth_ext
- .min_pin_length
- .ok_or(ExtensionErr::MissingMinPinLength)
- .map(|_| ())
- } else {
- Ok(())
- }
- })
- .and_then(|()| {
- self.prf.map_or(Ok(()), |info| {
- if matches!(
- info,
- ExtensionInfo::RequireEnforceValue
- | ExtensionInfo::RequireDontEnforceValue
- ) {
- if client_ext.prf.is_some() {
- Ok(())
- } else {
- Err(ExtensionErr::MissingPrf)
- }
- } else {
- Ok(())
- }
- })
- })
- })
- }
- /// Validates the value of any extensions sent from the client.
- ///
- /// Note missing and empty extensions are always OK.
- fn validate_value(
- self,
- client_ext: ClientExtensionsOutputs,
- auth_ext: AuthenticatorExtensionOutput,
- ) -> Result<(), ExtensionErr> {
- // This also checks for a missing response. Instead of duplicating that check, we only call
- // `self.cred_protect.validate` once here and not also in `Self::validate_required`.
- self.cred_protect
- .validate(auth_ext.cred_protect)
- .and_then(|()| {
- self.min_pin_length
- .map_or(Ok(()), |info| {
- if matches!(
- info.1,
- ExtensionInfo::RequireEnforceValue | ExtensionInfo::AllowEnforceValue
- ) {
- auth_ext.min_pin_length.map_or(Ok(()), |pin| {
- if pin >= info.0 {
- Ok(())
- } else {
- Err(ExtensionErr::InvalidMinPinLength(info.0, pin))
- }
- })
- } else {
- Ok(())
- }
- })
- .and_then(|()| {
- self.prf.map_or(Ok(()), |info| {
- if matches!(
- info,
- ExtensionInfo::RequireEnforceValue
- | ExtensionInfo::AllowEnforceValue
- ) {
- client_ext
- .prf
- .map_or(Ok(()), |prf| {
- if prf.enabled {
- Ok(())
- } else {
- Err(ExtensionErr::InvalidPrfValue)
- }
- })
- .and_then(|()| {
- auth_ext.hmac_secret.map_or(Ok(()), |hmac| {
- if hmac {
- Ok(())
- } else {
- Err(ExtensionErr::InvalidHmacSecretValue)
- }
- })
- })
- } else {
- Ok(())
- }
- })
- })
- })
- }
+ /// Note some authenticators can only enable `prf` during registration (e.g., CTAP authenticators that only
+ /// support
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension)
+ /// and not
+ /// [`hmac-secret-mc`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-make-cred-extension));
+ /// thus the value of `PrfInput` is ignored and only used as a signal to enable `prf`. For many such
+ /// authenticators, not using this extension during registration will not preclude them from being used during
+ /// authentication; however it is still encouraged to use the extension during registration since some
+ /// authenticators actually require it.
+ ///
+ /// When the underlying credential is expected to be used during discoverable requests, it is likely that
+ /// `'prf_first` will be `'static` and [`PrfInput::second`] is `None` since one will not be able to
+ /// realistically rotate the underlying inputs and further the same input will likely be used for all credentials.
+ /// For credentials intended to be used during non-discoverable requests, however, one is encouraged to rotate
+ /// the inputs and have unique values for each credential.
+ pub prf: Option<(PrfInput<'prf_first, 'prf_second>, ExtensionInfo)>,
}
#[cfg(test)]
-impl PartialEq for Extension {
+impl PartialEq for Extension<'_, '_> {
fn eq(&self, other: &Self) -> bool {
self.cred_props == other.cred_props
&& self.cred_protect == other.cred_protect
@@ -1463,18 +1271,18 @@ impl PartialEq for AuthenticatorSelectionCriteria {
&& self.user_verification == other.user_verification
}
}
-/// Helper that verifies the overlap of [`PublicKeyCredentialCreationOptions::start_ceremony`] and
+/// Helper that verifies the overlap of [`CredentialCreationOptions::start_ceremony`] and
/// [`RegistrationServerState::decode`].
const fn validate_options_helper(
auth_crit: AuthenticatorSelectionCriteria,
- extensions: Extension,
+ extensions: ServerExtensionInfo,
) -> Result<(), CreationOptionsErr> {
if matches!(
auth_crit.user_verification,
UserVerificationRequirement::Required
) {
Ok(())
- } else if extensions.prf.is_some() {
+ } else if !matches!(extensions.prf, ServerPrfInfo::None) {
Err(CreationOptionsErr::PrfWithoutUserVerification)
} else if matches!(
extensions.cred_protect,
@@ -1485,22 +1293,263 @@ const fn validate_options_helper(
Ok(())
}
}
-/// The [`PublicKeyCredentialCreationOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions)
+/// The [`CredentialCreationOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialcreationoptions)
/// to send to the client when registering a new credential.
///
/// Upon saving the [`RegistrationServerState`] returned from [`Self::start_ceremony`], one MUST send
/// [`RegistrationClientState`] to the client ASAP. After receiving the newly created [`Registration`], it is
/// validated using [`RegistrationServerState::verify`].
#[derive(Debug)]
-pub struct PublicKeyCredentialCreationOptions<
+pub struct CredentialCreationOptions<
'rp_id,
'user_name,
'user_display_name,
'user_id,
+ 'prf_first,
+ 'prf_second,
const USER_LEN: usize,
> {
/// [`mediation`](https://www.w3.org/TR/credential-management-1/#dom-credentialcreationoptions-mediation).
pub mediation: CredentialMediationRequirement,
+ /// `public-key` [credential type](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry).
+ pub public_key: PublicKeyCredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_LEN,
+ >,
+}
+impl<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ const USER_LEN: usize,
+>
+ CredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_LEN,
+ >
+{
+ /// Sets [`Self::mediation`] to [`CredentialMediationRequirement::default`] and
+ /// [`Self::public_key`] to [`PublicKeyCredentialCreationOptions::passkey`].
+ #[inline]
+ #[must_use]
+ 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, USER_LEN>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ ) -> Self {
+ Self {
+ mediation: CredentialMediationRequirement::default(),
+ public_key: PublicKeyCredentialCreationOptions::passkey(
+ rp_id,
+ user,
+ exclude_credentials,
+ ),
+ }
+ }
+ /// 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())
+ }
+ /// Sets [`Self::mediation`] to [`CredentialMediationRequirement::default`] and
+ /// [`Self::public_key`] to [`PublicKeyCredentialCreationOptions::second_factor`].
+ #[inline]
+ #[must_use]
+ 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, USER_LEN>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ ) -> Self {
+ let mut opts = Self::passkey(rp_id, user, exclude_credentials);
+ opts.public_key.authenticator_selection = AuthenticatorSelectionCriteria::second_factor();
+ opts.public_key.extensions.cred_props = Some(ExtensionReq::Allow);
+ opts.public_key.extensions.cred_protect =
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ false,
+ ExtensionInfo::AllowEnforceValue,
+ );
+ 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
+ /// it can later be used to verify the new credential with [`RegistrationServerState::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::TimedCeremony as _;
+ /// # use webauthn_rp::request::{
+ /// # register::{CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle64},
+ /// # AsciiDomain, RpId
+ /// # };
+ /// # #[cfg(not(feature = "serializable_server_state"))]
+ /// assert!(
+ /// CredentialCreationOptions::passkey(
+ /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
+ /// PublicKeyCredentialUserEntity {
+ /// name: "bernard.riemann".try_into()?,
+ /// id: &UserHandle64::new(),
+ /// display_name: Some("Georg Friedrich Bernhard Riemann".try_into()?)
+ /// },
+ /// Vec::new()
+ /// ).start_ceremony()?.0.expiration() > Instant::now()
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ pub fn start_ceremony(
+ mut self,
+ ) -> Result<
+ (
+ RegistrationServerState<USER_LEN>,
+ RegistrationClientState<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_LEN,
+ >,
+ ),
+ CreationOptionsErr,
+ > {
+ let extensions = self.public_key.extensions.into();
+ validate_options_helper(self.public_key.authenticator_selection, extensions).and_then(
+ |()| {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let now = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let now = SystemTime::now();
+ now.checked_add(Duration::from_millis(
+ NonZeroU64::from(self.public_key.timeout).get(),
+ ))
+ .ok_or(CreationOptionsErr::InvalidTimeout)
+ .map(|expiration| {
+ // We remove duplicates. The order has no significance, so this is OK.
+ self.public_key
+ .exclude_credentials
+ .sort_unstable_by(|a, b| a.id.as_ref().cmp(b.id.as_ref()));
+ self.public_key
+ .exclude_credentials
+ .dedup_by(|a, b| a.id.as_ref() == b.id.as_ref());
+ (
+ RegistrationServerState {
+ mediation: self.mediation,
+ challenge: SentChallenge(self.public_key.challenge.0),
+ pub_key_cred_params: self.public_key.pub_key_cred_params,
+ authenticator_selection: self.public_key.authenticator_selection,
+ extensions,
+ expiration,
+ user_id: *self.public_key.user.id,
+ },
+ RegistrationClientState(self),
+ )
+ })
+ },
+ )
+ }
+}
+/// `CredentialCreationOptions` based on a [`UserHandle64`].
+pub type CredentialCreationOptions64<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+> = CredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_HANDLE_MAX_LEN,
+>;
+/// `CredentialCreationOptions` based on a [`UserHandle16`].
+pub type CredentialCreationOptions16<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+> = CredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ 16,
+>;
+/// The [`PublicKeyCredentialCreationOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions)
+/// to send to the client when registering a new credential.
+#[derive(Debug)]
+pub struct PublicKeyCredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ const USER_LEN: usize,
+> {
/// [`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).
@@ -1520,10 +1569,18 @@ pub struct PublicKeyCredentialCreationOptions<
/// [`authenticatorSelection`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection).
pub authenticator_selection: AuthenticatorSelectionCriteria,
/// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions).
- pub extensions: Extension,
+ pub extensions: Extension<'prf_first, 'prf_second>,
}
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>
+ 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
@@ -1534,8 +1591,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
/// Creates a `PublicKeyCredentialCreationOptions` that requires the authenticator to create a client-side
/// discoverable credential enforcing any form of user verification. A five-minute timeout is set.
/// [`Extension::cred_protect`] with [`CredProtect::UserVerificationRequired`] with `false` and
- /// [`ExtensionInfo::AllowEnforceValue`] is used. [`Self::mediation`] is
- /// [`CredentialMediationRequirement::Optional`].
+ /// [`ExtensionInfo::AllowEnforceValue`] is used.
///
/// # Examples
///
@@ -1568,7 +1624,6 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
) -> Self {
Self {
- mediation: CredentialMediationRequirement::Optional,
rp_id,
user,
challenge: Challenge::new(),
@@ -1624,7 +1679,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
/// credential without requiring user verification. A five-minute timeout is set. [`Extension::cred_props`]
/// is [`ExtensionReq::Allow`]. [`Extension::cred_protect`] is
/// [`CredProtect::UserVerificationOptionalWithCredentialIdList`] with `false` and
- /// [`ExtensionInfo::AllowEnforceValue`]. [`Self::mediation`] is [`CredentialMediationRequirement::Optional`].
+ /// [`ExtensionInfo::AllowEnforceValue`].
///
/// Note some authenticators require user verification during credential registration (e.g.,
/// [CTAP 2.0 authenticators](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#authenticatorMakeCredential)).
@@ -1686,92 +1741,42 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
) -> 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
- /// it can later be used to verify the new credential with [`RegistrationServerState::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::TimedCeremony as _;
- /// # use webauthn_rp::request::{
- /// # register::{PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle64},
- /// # AsciiDomain, RpId
- /// # };
- /// # #[cfg(not(feature = "serializable_server_state"))]
- /// assert!(
- /// PublicKeyCredentialCreationOptions::passkey(
- /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
- /// PublicKeyCredentialUserEntity {
- /// name: "bernard.riemann".try_into()?,
- /// id: &UserHandle64::new(),
- /// display_name: Some("Georg Friedrich Bernhard Riemann".try_into()?)
- /// },
- /// Vec::new()
- /// ).start_ceremony()?.0.expiration() > Instant::now()
- /// );
- /// # Ok::<_, webauthn_rp::AggErr>(())
- /// ```
- #[inline]
- pub fn start_ceremony(
- mut self,
- ) -> Result<
- (
- RegistrationServerState<USER_LEN>,
- RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_id, USER_LEN>,
- ),
- CreationOptionsErr,
- > {
- validate_options_helper(self.authenticator_selection, self.extensions).and_then(|()| {
- #[cfg(not(feature = "serializable_server_state"))]
- let now = Instant::now();
- #[cfg(feature = "serializable_server_state")]
- let now = SystemTime::now();
- now.checked_add(Duration::from_millis(NonZeroU64::from(self.timeout).get()))
- .ok_or(CreationOptionsErr::InvalidTimeout)
- .map(|expiration| {
- // We remove duplicates. The order has no significance, so this is OK.
- self.exclude_credentials
- .sort_unstable_by(|a, b| a.id.as_ref().cmp(b.id.as_ref()));
- self.exclude_credentials
- .dedup_by(|a, b| a.id.as_ref() == b.id.as_ref());
- (
- RegistrationServerState {
- mediation: self.mediation,
- challenge: SentChallenge(self.challenge.0),
- pub_key_cred_params: self.pub_key_cred_params,
- authenticator_selection: self.authenticator_selection,
- extensions: self.extensions,
- expiration,
- user_id: *self.user.id,
- },
- RegistrationClientState(self),
- )
- })
- })
- }
}
/// `PublicKeyCredentialCreationOptions` based on a [`UserHandle64`].
-pub type PublicKeyCredentialCreationOptions64<'rp_id, 'user_name, 'user_display_name, 'user_id> =
- PublicKeyCredentialCreationOptions<
- 'rp_id,
- 'user_name,
- 'user_display_name,
- 'user_id,
- USER_HANDLE_MAX_LEN,
- >;
+pub type PublicKeyCredentialCreationOptions64<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+> = PublicKeyCredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_HANDLE_MAX_LEN,
+>;
/// `PublicKeyCredentialCreationOptions` based on a [`UserHandle16`].
-pub type PublicKeyCredentialCreationOptions16<'rp_id, 'user_name, 'user_display_name, 'user_id> =
- PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_id, 16>;
-/// Container of a [`PublicKeyCredentialCreationOptions`] that has been used to start the registration ceremony.
+pub type PublicKeyCredentialCreationOptions16<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+> = PublicKeyCredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ 16,
+>;
+/// Container of a [`CredentialCreationOptions`] that has been used to start the registration ceremony.
/// This gets sent to the client ASAP.
#[derive(Debug)]
pub struct RegistrationClientState<
@@ -1779,22 +1784,50 @@ pub struct RegistrationClientState<
'user_name,
'user_display_name,
'user_id,
+ 'prf_first,
+ 'prf_second,
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>
+>(
+ CredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_LEN,
+ >,
+);
+impl<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ const USER_LEN: usize,
+>
+ RegistrationClientState<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_LEN,
+ >
{
- /// Returns the `PublicKeyCredentialCreationOptions` that was used to start a registration ceremony.
+ /// Returns the `CredentialCreationOptions` that was used to start a registration ceremony.
///
/// # Examples
///
/// ```
/// # use webauthn_rp::request::{register::{
- /// # CoseAlgorithmIdentifiers, PublicKeyCredentialCreationOptions,
+ /// # CoseAlgorithmIdentifiers, CredentialCreationOptions,
/// # PublicKeyCredentialUserEntity, UserHandle64,
/// # }, AsciiDomain, RpId};
/// assert_eq!(
- /// PublicKeyCredentialCreationOptions::passkey(
+ /// CredentialCreationOptions::passkey(
/// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
/// PublicKeyCredentialUserEntity {
/// name: "david.hilbert".try_into()?,
@@ -1806,6 +1839,7 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
/// .start_ceremony()?
/// .1
/// .options()
+ /// .public_key
/// .rp_id.as_ref(),
/// "example.com"
/// );
@@ -1815,22 +1849,52 @@ impl<'rp_id, 'user_name, 'user_display_name, 'user_id, const USER_LEN: usize>
#[must_use]
pub const fn options(
&self,
- ) -> &PublicKeyCredentialCreationOptions<
+ ) -> &CredentialCreationOptions<
'rp_id,
'user_name,
'user_display_name,
'user_id,
+ 'prf_first,
+ 'prf_second,
USER_LEN,
> {
&self.0
}
}
/// `RegistrationClientState` based on a [`UserHandle64`].
-pub type RegistrationClientState64<'rp_id, 'user_name, 'user_display_name, 'user_id> =
- RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_id, USER_HANDLE_MAX_LEN>;
+pub type RegistrationClientState64<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+> = RegistrationClientState<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_HANDLE_MAX_LEN,
+>;
/// `RegistrationClientState` based on a [`UserHandle16`].
-pub type RegistrationClientState16<'rp_id, 'user_name, 'user_display_name, 'user_id> =
- RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_id, 16>;
+pub type RegistrationClientState16<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+> = RegistrationClientState<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ 16,
+>;
/// Additional verification options to perform in [`RegistrationServerState::verify`].
#[derive(Clone, Copy, Debug)]
pub struct RegistrationVerificationOptions<'origins, 'top_origins, O, T> {
@@ -1877,13 +1941,270 @@ impl<O, T> Default for RegistrationVerificationOptions<'_, '_, O, T> {
}
}
}
+/// `PrfInput` without the actual data sent to reduce memory usage when storing [`RegistrationServerState`] in an
+/// in-memory collection.
+#[derive(Clone, Copy, Debug)]
+enum ServerPrfInfo {
+ /// No `PrfInput`.
+ None,
+ /// `PrfInput::second` was `None`.
+ One(ExtensionInfo),
+ /// `PrfInput::second` was `Some`.
+ Two(ExtensionInfo),
+}
+#[cfg(test)]
+impl PartialEq for ServerPrfInfo {
+ fn eq(&self, other: &Self) -> bool {
+ match *self {
+ Self::None => matches!(*other, Self::None),
+ Self::One(info) => matches!(*other, Self::One(info2) if info == info2),
+ Self::Two(info) => matches!(*other, Self::Two(info2) if info == info2),
+ }
+ }
+}
+impl From<Option<(PrfInput<'_, '_>, ExtensionInfo)>> for ServerPrfInfo {
+ fn from(value: Option<(PrfInput<'_, '_>, ExtensionInfo)>) -> Self {
+ value.map_or(Self::None, |val| {
+ val.0
+ .second
+ .map_or_else(|| Self::One(val.1), |_| Self::Two(val.1))
+ })
+ }
+}
+/// `Extension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`]
+/// in an in-memory collection.
+#[derive(Clone, Copy, Debug)]
+struct ServerExtensionInfo {
+ /// `Extension::cred_props`.
+ cred_props: Option<ExtensionReq>,
+ /// `Extension::cred_protect`.
+ cred_protect: CredProtect,
+ /// `Extension::min_pin_length`.
+ min_pin_length: Option<(FourToSixtyThree, ExtensionInfo)>,
+ /// `Extension::prf`.
+ prf: ServerPrfInfo,
+}
+impl ServerExtensionInfo {
+ /// Validates the extensions.
+ fn validate(
+ self,
+ client_ext: ClientExtensionsOutputs,
+ auth_ext: AuthenticatorExtensionOutput,
+ error_unsolicited: bool,
+ ) -> Result<(), ExtensionErr> {
+ if error_unsolicited {
+ self.validate_unsolicited(client_ext, auth_ext)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ self.validate_required(client_ext, auth_ext)
+ .and_then(|()| self.validate_value(client_ext, auth_ext))
+ })
+ }
+ /// Validates if there are any unsolicited extensions.
+ ///
+ /// Note no distinction is made between an extension that is empty and one that is not (i.e., we are checking
+ /// purely for the existence of extension keys).
+ fn validate_unsolicited(
+ mut self,
+ client_ext: ClientExtensionsOutputs,
+ auth_ext: AuthenticatorExtensionOutput,
+ ) -> Result<(), ExtensionErr> {
+ // For simpler code, we artificially set non-requested extensions after verifying there was not an error
+ // and recursively call this function. There are so few extensions and the checks are fast that there
+ // should be no worry of stack overflow or performance overhead.
+ if self.cred_props.is_some() {
+ if !matches!(self.cred_protect, CredProtect::None) {
+ if self.min_pin_length.is_some() {
+ // This is the last extension, so recursion stops here.
+ if !matches!(self.prf, ServerPrfInfo::None) {
+ Ok(())
+ } else if client_ext.prf.is_some() {
+ Err(ExtensionErr::ForbiddenPrf)
+ } else if !matches!(auth_ext.hmac_secret, HmacSecret::None) {
+ Err(ExtensionErr::ForbiddenHmacSecret)
+ } else {
+ Ok(())
+ }
+ } else if auth_ext.min_pin_length.is_some() {
+ Err(ExtensionErr::ForbiddenMinPinLength)
+ } else {
+ // Pretend to set `minPinLength`, so we can check `prf`.
+ self.min_pin_length =
+ Some((FourToSixtyThree::MIN, ExtensionInfo::RequireEnforceValue));
+ self.validate_unsolicited(client_ext, auth_ext)
+ }
+ } else if !matches!(auth_ext.cred_protect, CredentialProtectionPolicy::None) {
+ Err(ExtensionErr::ForbiddenCredProtect)
+ } else {
+ // Pretend to set `credProtect`, so we can check `minPinLength` and `prf` extensions.
+ self.cred_protect = CredProtect::UserVerificationOptional(
+ false,
+ ExtensionInfo::RequireEnforceValue,
+ );
+ self.validate_unsolicited(client_ext, auth_ext)
+ }
+ } else if client_ext.cred_props.is_some() {
+ Err(ExtensionErr::ForbiddenCredProps)
+ } else {
+ // Pretend to set `credProps`; so we can check `credProtect`, `minPinLength`, and `prf` extensions.
+ self.cred_props = Some(ExtensionReq::Require);
+ self.validate_unsolicited(client_ext, auth_ext)
+ }
+ }
+ /// Validates if any required extensions don't have a corresponding response.
+ ///
+ /// Note empty extensions are treated as missing. For example when requiring the `credProps` extension,
+ /// all of the following responses would lead to a failure:
+ /// `{"clientExtensionResults":{}}`: no extensions.
+ /// `{"clientExtensionResults":{"prf":true}}`: only the `prf` extension.
+ /// `{"clientExtensionResults":{"credProps":{}}}`: empty `credProps` extension.
+ /// `{"clientExtensionResults":{"credProps":{"foo":false}}}`: `credProps` extension doesn't contain at least one
+ /// expected field (i.e., still "empty").
+ fn validate_required(
+ self,
+ client_ext: ClientExtensionsOutputs,
+ auth_ext: AuthenticatorExtensionOutput,
+ ) -> Result<(), ExtensionErr> {
+ // We don't check `self.cred_protect` since `CredProtect::validate` checks for both a required response
+ // and value enforcement; thus it only needs to be checked once (which it is in `Self::validate_value`).
+ self.cred_props
+ .map_or(Ok(()), |info| {
+ if matches!(info, ExtensionReq::Require) {
+ if client_ext
+ .cred_props
+ .is_some_and(|props| props.rk.is_some())
+ {
+ Ok(())
+ } else {
+ Err(ExtensionErr::MissingCredProps)
+ }
+ } else {
+ Ok(())
+ }
+ })
+ .and_then(|()| {
+ self.min_pin_length
+ .map_or(Ok(()), |info| {
+ if matches!(
+ info.1,
+ ExtensionInfo::RequireEnforceValue
+ | ExtensionInfo::RequireDontEnforceValue
+ ) {
+ auth_ext
+ .min_pin_length
+ .ok_or(ExtensionErr::MissingMinPinLength)
+ .map(|_| ())
+ } else {
+ Ok(())
+ }
+ })
+ .and_then(|()| match self.prf {
+ ServerPrfInfo::None => Ok(()),
+ ServerPrfInfo::One(info) | ServerPrfInfo::Two(info) => {
+ if matches!(
+ info,
+ ExtensionInfo::RequireEnforceValue
+ | ExtensionInfo::RequireDontEnforceValue
+ ) {
+ if client_ext.prf.is_some() {
+ Ok(())
+ } else {
+ Err(ExtensionErr::MissingPrf)
+ }
+ } else {
+ Ok(())
+ }
+ }
+ })
+ })
+ }
+ /// Validates the value of any extensions sent from the client.
+ ///
+ /// Note missing and empty extensions are always OK.
+ fn validate_value(
+ self,
+ client_ext: ClientExtensionsOutputs,
+ auth_ext: AuthenticatorExtensionOutput,
+ ) -> Result<(), ExtensionErr> {
+ // This also checks for a missing response. Instead of duplicating that check, we only call
+ // `self.cred_protect.validate` once here and not also in `Self::validate_required`.
+ self.cred_protect
+ .validate(auth_ext.cred_protect)
+ .and_then(|()| {
+ self.min_pin_length
+ .map_or(Ok(()), |info| {
+ if matches!(
+ info.1,
+ ExtensionInfo::RequireEnforceValue | ExtensionInfo::AllowEnforceValue
+ ) {
+ auth_ext.min_pin_length.map_or(Ok(()), |pin| {
+ if pin >= info.0 {
+ Ok(())
+ } else {
+ Err(ExtensionErr::InvalidMinPinLength(info.0, pin))
+ }
+ })
+ } else {
+ Ok(())
+ }
+ })
+ .and_then(|()| match self.prf {
+ ServerPrfInfo::None => Ok(()),
+ ServerPrfInfo::One(info) | ServerPrfInfo::Two(info) => {
+ if matches!(
+ info,
+ ExtensionInfo::RequireEnforceValue
+ | ExtensionInfo::AllowEnforceValue
+ ) {
+ client_ext
+ .prf
+ .map_or(Ok(()), |prf| {
+ if prf.enabled {
+ Ok(())
+ } else {
+ Err(ExtensionErr::InvalidPrfValue)
+ }
+ })
+ .and({
+ if matches!(auth_ext.hmac_secret, HmacSecret::NotEnabled) {
+ Err(ExtensionErr::InvalidHmacSecretValue)
+ } else {
+ Ok(())
+ }
+ })
+ } else {
+ Ok(())
+ }
+ }
+ })
+ })
+ }
+}
+impl From<Extension<'_, '_>> for ServerExtensionInfo {
+ fn from(value: Extension<'_, '_>) -> Self {
+ Self {
+ cred_props: value.cred_props,
+ cred_protect: value.cred_protect,
+ min_pin_length: value.min_pin_length,
+ prf: value.prf.into(),
+ }
+ }
+}
+#[cfg(test)]
+impl PartialEq for ServerExtensionInfo {
+ fn eq(&self, other: &Self) -> bool {
+ self.prf == other.prf
+ }
+}
// This is essentially the `PublicKeyCredentialCreationOptions` used to create it; however to reduce
// memory usage, we remove all unnecessary data making an instance of this 48 bytes in size on
// `x86_64-unknown-linux-gnu` platforms.
/// State needed to be saved when beginning the registration ceremony.
///
-/// Saves the necessary information associated with the [`PublicKeyCredentialCreationOptions`] used to create it
-/// via [`PublicKeyCredentialCreationOptions::start_ceremony`] so that registration of a new credential can be
+/// Saves the necessary information associated with the [`CredentialCreationOptions`] used to create it
+/// via [`CredentialCreationOptions::start_ceremony`] so that registration of a new credential can be
/// performed with [`Self::verify`].
///
/// `RegistrationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the correct
@@ -1908,7 +2229,7 @@ pub struct RegistrationServerState<const USER_LEN: usize> {
/// [`authenticatorSelection`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection).
authenticator_selection: AuthenticatorSelectionCriteria,
/// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions).
- extensions: Extension,
+ extensions: ServerExtensionInfo,
/// `Instant` the ceremony expires.
#[cfg(not(feature = "serializable_server_state"))]
expiration: Instant,
@@ -2161,7 +2482,7 @@ mod tests {
))]
use super::{
super::{super::AggErr, ExtensionInfo},
- Challenge, CredProtect, FourToSixtyThree, PublicKeyCredentialCreationOptions, RpId,
+ Challenge, CredProtect, CredentialCreationOptions, FourToSixtyThree, PrfInput, RpId,
UserHandle,
};
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
@@ -2180,7 +2501,7 @@ mod tests {
response::register::{
AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation,
ClientExtensionsOutputs, CredentialPropertiesOutput,
- CredentialProtectionPolicy,
+ CredentialProtectionPolicy, HmacSecret,
},
},
AuthTransports,
@@ -2261,7 +2582,7 @@ mod tests {
fn eddsa_reg_ser() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
let id = UserHandle::from([0; 1]);
- let mut opts = PublicKeyCredentialCreationOptions::passkey(
+ let mut opts = CredentialCreationOptions::passkey(
&rp_id,
PublicKeyCredentialUserEntity {
name: "foo".try_into()?,
@@ -2270,8 +2591,8 @@ mod tests {
},
Vec::new(),
);
- opts.challenge = Challenge(0);
- opts.extensions = Extension {
+ opts.public_key.challenge = Challenge(0);
+ opts.public_key.extensions = Extension {
cred_props: None,
cred_protect: CredProtect::UserVerificationRequired(
false,
@@ -2282,7 +2603,13 @@ mod tests {
.unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new")),
ExtensionInfo::RequireEnforceValue,
)),
- prf: Some(ExtensionInfo::RequireEnforceValue),
+ prf: Some((
+ PrfInput {
+ first: [0].as_slice(),
+ second: None,
+ },
+ ExtensionInfo::RequireEnforceValue,
+ )),
};
let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
// We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
@@ -2605,7 +2932,7 @@ mod tests {
user_verified: bool,
cred_protect: CredentialProtectionPolicy,
prf: Option<bool>,
- hmac: Option<bool>,
+ hmac: HmacSecret,
min_pin: Option<FourToSixtyThree>,
cred_props: Option<Option<bool>>,
}
@@ -2674,13 +3001,28 @@ mod tests {
CBOR_BYTES | 24,
// Length.
113 + if matches!(options.cred_protect, CredentialProtectionPolicy::None) {
- if options.hmac.is_some() {
- 14 + options.min_pin.map_or(0, |_| 14)
- } else {
+ if matches!(options.hmac, HmacSecret::None) {
options.min_pin.map_or(0, |_| 15)
+ } else {
+ 14 + options.min_pin.map_or(0, |_| 14)
+ + match options.hmac {
+ HmacSecret::None => unreachable!("bug"),
+ HmacSecret::NotEnabled | HmacSecret::Enabled => 0,
+ HmacSecret::One => 65,
+ HmacSecret::Two => 97,
+ }
}
} else {
- 14 + options.hmac.map_or(0, |_| 13) + options.min_pin.map_or(0, |_| 14)
+ 14 + if matches!(options.hmac, HmacSecret::None) {
+ 0
+ } else {
+ 13
+ } + options.min_pin.map_or(0, |_| 14)
+ + match options.hmac {
+ HmacSecret::None | HmacSecret::NotEnabled | HmacSecret::Enabled => 0,
+ HmacSecret::One => 65,
+ HmacSecret::Two => 97,
+ }
},
// RP ID HASH.
// This will be overwritten later.
@@ -2725,7 +3067,7 @@ mod tests {
0b0000_0000
}
| if matches!(options.cred_protect, CredentialProtectionPolicy::None)
- && options.hmac.is_none()
+ && matches!(options.hmac, HmacSecret::None)
&& options.min_pin.is_none()
{
0
@@ -2833,19 +3175,25 @@ mod tests {
attestation_object[30..62]
.copy_from_slice(Sha256::digest("example.com".as_bytes()).as_slice());
if matches!(options.cred_protect, CredentialProtectionPolicy::None) {
- if options.hmac.is_some() {
+ if matches!(options.hmac, HmacSecret::None) {
if options.min_pin.is_some() {
- attestation_object.push(CBOR_MAP | 2);
- } else {
- attestation_object.push(CBOR_MAP | 1);
+ attestation_object.push(CBOR_MAP | 1)
}
} else if options.min_pin.is_some() {
- attestation_object.push(CBOR_MAP | 1);
+ attestation_object.push(
+ CBOR_MAP
+ | 2 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two)),
+ );
+ } else {
+ attestation_object.push(
+ CBOR_MAP
+ | 1 + u8::from(matches!(options.hmac, HmacSecret::One | HmacSecret::Two)),
+ );
}
} else {
attestation_object.extend_from_slice(
[
- CBOR_MAP | 1 + u8::from(options.hmac.is_some()) + u8::from(options.min_pin.is_some()),
+ CBOR_MAP | 1 + match options.hmac { HmacSecret::None => 0, HmacSecret::NotEnabled | HmacSecret::Enabled => 1, HmacSecret::One | HmacSecret::Two => 2, } + u8::from(options.min_pin.is_some()),
// CBOR text of length 11.
CBOR_TEXT | 11,
b'c',
@@ -2863,7 +3211,7 @@ mod tests {
].as_slice()
)
}
- options.hmac.map(|h| {
+ if !matches!(options.hmac, HmacSecret::None) {
attestation_object.extend_from_slice(
[
// CBOR text of length 11.
@@ -2879,11 +3227,15 @@ mod tests {
b'r',
b'e',
b't',
- if h { CBOR_TRUE } else { CBOR_FALSE },
+ if matches!(options.hmac, HmacSecret::NotEnabled) {
+ CBOR_FALSE
+ } else {
+ CBOR_TRUE
+ },
]
.as_slice(),
);
- });
+ }
options.min_pin.map(|p| {
assert!(p.value() <= 23, "bug");
attestation_object.extend_from_slice(
@@ -2907,6 +3259,37 @@ mod tests {
.as_slice(),
);
});
+ if matches!(options.hmac, HmacSecret::One | HmacSecret::Two) {
+ attestation_object.extend_from_slice(
+ [
+ // CBOR text of length 14.
+ CBOR_TEXT | 14,
+ b'h',
+ b'm',
+ b'a',
+ b'c',
+ b'-',
+ b's',
+ b'e',
+ b'c',
+ b'r',
+ b'e',
+ b't',
+ b'-',
+ b'm',
+ b'c',
+ CBOR_BYTES | 24,
+ ]
+ .as_slice(),
+ );
+ if matches!(options.hmac, HmacSecret::One) {
+ attestation_object.push(48);
+ attestation_object.extend_from_slice([1; 48].as_slice());
+ } else {
+ attestation_object.push(80);
+ attestation_object.extend_from_slice([1; 80].as_slice());
+ }
+ }
attestation_object
}
#[cfg(all(feature = "custom", not(any(feature = "bin", feature = "serde"))))]
@@ -2940,10 +3323,10 @@ mod tests {
client_data_json_relaxed: false,
};
let user = UserHandle::from([0; 1]);
- let mut opts =
- PublicKeyCredentialCreationOptions::first_passkey_with_blank_user_info(&rp_id, &user);
- opts.challenge = Challenge(0);
- opts.authenticator_selection.user_verification = UserVerificationRequirement::Preferred;
+ let mut opts = CredentialCreationOptions::first_passkey_with_blank_user_info(&rp_id, &user);
+ opts.public_key.challenge = Challenge(0);
+ opts.public_key.authenticator_selection.user_verification =
+ UserVerificationRequirement::Preferred;
match options.request.prf_uv {
PrfUvOptions::None(required) => {
if required
@@ -2952,19 +3335,25 @@ mod tests {
CredProtect::UserVerificationRequired(_, _)
)
{
- opts.authenticator_selection.user_verification =
+ opts.public_key.authenticator_selection.user_verification =
UserVerificationRequirement::Required;
}
}
PrfUvOptions::Prf(info) => {
- opts.authenticator_selection.user_verification =
+ opts.public_key.authenticator_selection.user_verification =
UserVerificationRequirement::Required;
- opts.extensions.prf = Some(info);
+ opts.public_key.extensions.prf = Some((
+ PrfInput {
+ first: [0].as_slice(),
+ second: None,
+ },
+ info,
+ ));
}
}
- opts.extensions.cred_protect = options.request.protect;
- opts.extensions.cred_props = options.request.props;
- opts.extensions.min_pin_length = options.request.pin;
+ opts.public_key.extensions.cred_protect = options.request.protect;
+ opts.public_key.extensions.cred_props = options.request.props;
+ opts.public_key.extensions.min_pin_length = options.request.pin;
opts.start_ceremony()?
.0
.verify(&rp_id, ®istration, ®_opts)
@@ -2972,7 +3361,7 @@ mod tests {
.map(|_| ())
}
/// Test all, and only, possible `UserNotVerified` errors.
- /// 4 * 3 * 3 * 2 * 13 * 5 * 3 * 5 * 4 * 4 = 1,123,200 tests.
+ /// 4 * 3 * 5 * 2 * 13 * 5 * 3 * 5 * 4 * 4 = 1,872,000 tests.
/// We ignore this due to how long it takes (around 30 seconds or so).
#[test]
#[ignore]
@@ -2985,7 +3374,13 @@ mod tests {
CredentialProtectionPolicy::UserVerificationRequired,
];
const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)];
- const ALL_HMAC_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)];
+ const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [
+ HmacSecret::None,
+ HmacSecret::NotEnabled,
+ HmacSecret::Enabled,
+ HmacSecret::One,
+ HmacSecret::Two,
+ ];
const ALL_UNSOLICIT_OPTIONS: [bool; 2] = [false, true];
const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [
CredProtect::None,
@@ -3089,15 +3484,15 @@ mod tests {
}
}
/// Test all, and only, possible `ForbiddenCredProps` errors.
- /// 4 * 3 * 3 * 2 * 13 * 6 * 5 * 3 * 4 = 336,960
+ /// 4 * 3 * 5 * 2 * 13 * 6 * 5 * 3 * 4 = 561,600
/// -
- /// 4 * 3 * 3 * 4 * 6 * 5 * 3 * 4 = 51,840
+ /// 4 * 3 * 5 * 4 * 6 * 5 * 3 * 4 = 86,400
/// -
- /// 4 * 3 * 3 * 13 * 5 * 5 * 3 * 4 = 140,400
+ /// 4 * 3 * 5 * 13 * 5 * 5 * 3 * 4 = 234,000
/// +
- /// 4 * 3 * 3 * 4 * 5 * 5 * 3 * 4 = 43,200
+ /// 4 * 3 * 5 * 4 * 5 * 5 * 3 * 4 = 72,000
/// =
- /// 187,920 total tests.
+ /// 313,200 total tests.
/// We ignore this due to how long it takes (around 6 seconds or so).
#[test]
#[ignore]
@@ -3110,7 +3505,13 @@ mod tests {
CredentialProtectionPolicy::UserVerificationRequired,
];
const ALL_PRF_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)];
- const ALL_HMAC_OPTIONS: [Option<bool>; 3] = [None, Some(false), Some(true)];
+ const ALL_HMAC_OPTIONS: [HmacSecret; 5] = [
+ HmacSecret::None,
+ HmacSecret::NotEnabled,
+ HmacSecret::Enabled,
+ HmacSecret::One,
+ HmacSecret::Two,
+ ];
const ALL_UV_OPTIONS: [bool; 2] = [false, true];
const ALL_CRED_PROTECT_OPTIONS: [CredProtect; 13] = [
CredProtect::None,
@@ -3230,7 +3631,7 @@ mod tests {
},
response: TestResponseOptions {
user_verified: true,
- hmac: None,
+ hmac: HmacSecret::None,
cred_protect: CredentialProtectionPolicy::None,
prf: Some(true),
min_pin: None,
@@ -3240,14 +3641,14 @@ mod tests {
validate(opts)?;
opts.response.prf = Some(false);
assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidPrfValue))), |_| false));
- opts.response.hmac = Some(false);
+ opts.response.hmac = HmacSecret::NotEnabled;
opts.response.prf = Some(true);
assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Extension(ext_err) if matches!(ext_err, ExtensionErr::InvalidHmacSecretValue))), |_| false));
opts.request.prf_uv = PrfUvOptions::Prf(ExtensionInfo::AllowDontEnforceValue);
- opts.response.hmac = Some(true);
+ opts.response.hmac = HmacSecret::Enabled;
opts.response.prf = None;
assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))), |_| false));
- opts.response.hmac = Some(false);
+ opts.response.hmac = HmacSecret::NotEnabled;
assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::HmacSecretWithoutPrf))), |_| false));
opts.response.prf = Some(true);
assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::PrfWithoutHmacSecret))), |_| false));
@@ -3255,7 +3656,7 @@ mod tests {
validate(opts)?;
opts.request.prf_uv = PrfUvOptions::None(false);
opts.response.user_verified = false;
- opts.response.hmac = None;
+ opts.response.hmac = HmacSecret::None;
opts.response.prf = Some(true);
assert!(validate(opts).map_or_else(|e| matches!(e, AggErr::RegCeremony(err) if matches!(err, RegCeremonyErr::Credential(cred_err) if matches!(cred_err, CredentialErr::PrfWithoutUserVerified))), |_| false));
opts.response.prf = None;
@@ -3278,7 +3679,7 @@ mod tests {
},
response: TestResponseOptions {
user_verified: true,
- hmac: None,
+ hmac: HmacSecret::None,
cred_protect: CredentialProtectionPolicy::UserVerificationRequired,
prf: None,
min_pin: None,
diff --git a/src/request/register/error.rs b/src/request/register/error.rs
@@ -1,6 +1,6 @@
#[cfg(doc)]
use super::{
- AuthenticatorSelectionCriteria, CredProtect, Extension, Nickname,
+ AuthenticatorSelectionCriteria, CredProtect, CredentialCreationOptions, Extension, Nickname,
PublicKeyCredentialCreationOptions, USER_HANDLE_MAX_LEN, USER_HANDLE_MIN_LEN, UserHandle,
UserVerificationRequirement, Username,
};
@@ -50,7 +50,7 @@ impl Display for UsernameErr {
}
}
impl Error for UsernameErr {}
-/// Error returned by [`PublicKeyCredentialCreationOptions::start_ceremony`].
+/// Error returned by [`CredentialCreationOptions::start_ceremony`].
#[derive(Clone, Copy, Debug)]
pub enum CreationOptionsErr {
/// Error when [`Extension::prf`] is [`Some`] but [`AuthenticatorSelectionCriteria::user_verification`] is not
diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs
@@ -1,9 +1,10 @@
extern crate alloc;
use super::{
- AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria,
- CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, CrossPlatformHint, Extension,
- Hint, Nickname, PlatformHint, PublicKeyCredentialUserEntity,
- RegistrationClientState, ResidentKeyRequirement, RpId, UserHandle, Username,
+ AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, CoseAlgorithmIdentifier,
+ CoseAlgorithmIdentifiers, CredProtect, CredentialCreationOptions, CrossPlatformHint, Extension,
+ Hint, Nickname, PlatformHint, PrfInput, PublicKeyCredentialCreationOptions,
+ PublicKeyCredentialUserEntity, RegistrationClientState, ResidentKeyRequirement, RpId,
+ UserHandle, Username,
};
use alloc::borrow::Cow;
#[cfg(doc)]
@@ -16,7 +17,7 @@ use core::{
use data_encoding::BASE64URL_NOPAD;
use serde::{
de::{Deserialize, Deserializer, Error, Unexpected, Visitor},
- ser::{Serialize, SerializeSeq as _, SerializeStruct, Serializer},
+ ser::{Serialize, SerializeSeq as _, SerializeStruct as _, Serializer},
};
impl Serialize for Nickname<'_> {
/// Serializes `self` as a [`prim@str`].
@@ -80,12 +81,15 @@ impl Serialize for CoseAlgorithmIdentifier {
.serialize_struct("PublicKeyCredentialParameters", 2)
.and_then(|mut ser| {
ser.serialize_field("type", "public-key").and_then(|()| {
- ser.serialize_field("alg", &match *self {
- Self::Eddsa => EDDSA,
- Self::Es256 => ES256,
- Self::Es384 => ES384,
- Self::Rs256 => RS256,
- })
+ ser.serialize_field(
+ "alg",
+ &match *self {
+ Self::Eddsa => EDDSA,
+ Self::Es256 => ES256,
+ Self::Es384 => ES384,
+ Self::Rs256 => RS256,
+ },
+ )
.and_then(|()| ser.end())
})
})
@@ -120,7 +124,12 @@ impl Serialize for CoseAlgorithmIdentifiers {
{
// At most we add `1` four times which clearly cannot overflow or `usize`.
serializer
- .serialize_seq(Some(usize::from(self.contains(CoseAlgorithmIdentifier::Eddsa)) + usize::from(self.contains(CoseAlgorithmIdentifier::Es256)) + usize::from(self.contains(CoseAlgorithmIdentifier::Es384)) + usize::from(self.contains(CoseAlgorithmIdentifier::Es384))))
+ .serialize_seq(Some(
+ usize::from(self.contains(CoseAlgorithmIdentifier::Eddsa))
+ + usize::from(self.contains(CoseAlgorithmIdentifier::Es256))
+ + usize::from(self.contains(CoseAlgorithmIdentifier::Es384))
+ + usize::from(self.contains(CoseAlgorithmIdentifier::Es384)),
+ ))
.and_then(|mut ser| {
if self.contains(CoseAlgorithmIdentifier::Eddsa) {
ser.serialize_element(&CoseAlgorithmIdentifier::Eddsa)
@@ -195,7 +204,10 @@ impl Serialize for UserHandle<1> {
where
S: Serializer,
{
- serializer.serialize_str(BASE64URL_NOPAD.encode_mut_str(self.0.as_slice(), [0; crate::base64url_nopad_len(1).unwrap()].as_mut_slice()))
+ serializer.serialize_str(BASE64URL_NOPAD.encode_mut_str(
+ self.0.as_slice(),
+ [0; crate::base64url_nopad_len(1).unwrap()].as_mut_slice(),
+ ))
}
}
/// Implements [`Serialize`] for [`UserHandle`] of array of length of the passed `usize` literal.
@@ -211,7 +223,7 @@ impl Serialize for UserHandle<$x> {
where
S: Serializer,
{
-
+
serializer.serialize_str(BASE64URL_NOPAD.encode_mut_str(self.0.as_slice(), [0; crate::base64url_nopad_len($x).unwrap()].as_mut_slice()))
}
}
@@ -220,9 +232,9 @@ impl Serialize for UserHandle<$x> {
}
// MUST only pass `2`–[`USER_HANDLE_MAX_LEN`] inclusively.
user_serialize!(
- 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
- 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
- 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64
+ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
+ 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64
);
impl<const LEN: usize> Serialize for PublicKeyCredentialUserEntity<'_, '_, '_, LEN>
where
@@ -454,20 +466,19 @@ impl Serialize for AuthenticatorSelectionCriteria {
}
/// Helper that serializes prf registration information to conform with
/// [`AuthenticationExtensionsPRFInputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfinputs).
-///
-/// Since CTAP 2.2 does not allow PRF evaluation at creation time, we send an empty map.
-struct Prf;
-impl Serialize for Prf {
+struct Prf<'a, 'b>(PrfInput<'a, 'b>);
+impl Serialize for Prf<'_, '_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
- serializer
- .serialize_struct("Prf", 0)
- .and_then(SerializeStruct::end)
+ serializer.serialize_struct("Prf", 1).and_then(|mut ser| {
+ ser.serialize_field("eval", &self.0)
+ .and_then(|()| ser.end())
+ })
}
}
-impl Serialize for Extension {
+impl Serialize for Extension<'_, '_> {
/// Serializes `self` to conform with
/// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson).
///
@@ -476,7 +487,7 @@ impl Serialize for Extension {
/// ```
/// # use webauthn_rp::request::{
/// # register::{CredProtect, Extension, FourToSixtyThree},
- /// # ExtensionInfo, ExtensionReq,
+ /// # PrfInput, ExtensionInfo, ExtensionReq,
/// # };
/// assert_eq!(serde_json::to_string(&Extension::default())?, r#"{}"#);
/// assert_eq!(
@@ -484,9 +495,9 @@ impl Serialize for Extension {
/// cred_props: Some(ExtensionReq::Allow),
/// cred_protect: CredProtect::UserVerificationRequired(false, ExtensionInfo::RequireEnforceValue),
/// min_pin_length: Some((FourToSixtyThree::new(16).unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new")), ExtensionInfo::AllowDontEnforceValue)),
- /// prf: Some(ExtensionInfo::AllowEnforceValue)
+ /// prf: Some((PrfInput { first: [0].as_slice(), second: None, }, ExtensionInfo::AllowEnforceValue))
/// })?,
- /// r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{}}"#
+ /// r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"AA"}}}"#
/// );
/// # Ok::<_, serde_json::Error>(())
/// ```
@@ -538,7 +549,8 @@ impl Serialize for Extension {
"userVerificationOptional"
}
CredProtect::UserVerificationOptionalWithCredentialIdList(
- enforce, _,
+ enforce,
+ _,
) => {
enforce_policy = enforce;
"userVerificationOptionalWithCredentialIDList"
@@ -561,7 +573,9 @@ impl Serialize for Extension {
.map_or(Ok(()), |_| ser.serialize_field(MIN_PIN_LENGTH, &true))
.and_then(|()| {
self.prf
- .map_or(Ok(()), |_| ser.serialize_field(PRF, &Prf))
+ .map_or(Ok(()), |(prf, _)| {
+ ser.serialize_field(PRF, &Prf(prf))
+ })
.and_then(|()| ser.end())
})
})
@@ -569,12 +583,156 @@ impl Serialize for Extension {
})
}
}
-impl<'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> Serialize for RegistrationClientState<'_, 'user_name, 'user_display_name, 'user_id, USER_LEN>
+impl<'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> Serialize
+ for PublicKeyCredentialCreationOptions<
+ '_,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ '_,
+ '_,
+ USER_LEN,
+ >
where
PublicKeyCredentialUserEntity<'user_name, 'user_display_name, 'user_id, USER_LEN>: Serialize,
{
/// Serializes `self` to conform with
/// [`PublicKeyCredentialCreationOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptionsjson).
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ /// "none".
+ const NONE: &str = "none";
+ serializer
+ .serialize_struct("PublicKeyCredentialCreationOptions", 11)
+ .and_then(|mut ser| {
+ ser.serialize_field("rp", &PublicKeyCredentialRpEntity(self.rp_id))
+ .and_then(|()| {
+ ser.serialize_field("user", &self.user).and_then(|()| {
+ ser.serialize_field("challenge", &self.challenge)
+ .and_then(|()| {
+ ser.serialize_field(
+ "pubKeyCredParams",
+ &self.pub_key_cred_params,
+ )
+ .and_then(|()| {
+ ser.serialize_field("timeout", &self.timeout).and_then(
+ |()| {
+ ser.serialize_field(
+ "excludeCredentials",
+ self.exclude_credentials.as_slice(),
+ )
+ .and_then(|()| {
+ ser.serialize_field(
+ "authenticatorSelection",
+ &self.authenticator_selection,
+ )
+ .and_then(|()| {
+ ser.serialize_field("hints", &match self.authenticator_selection.authenticator_attachment {
+ AuthenticatorAttachmentReq::None(hint) => hint,
+ AuthenticatorAttachmentReq::Platform(hint) => hint.into(),
+ AuthenticatorAttachmentReq::CrossPlatform(hint) => hint.into(),
+ }).and_then(|()| {
+ ser.serialize_field("attestation", NONE).and_then(|()| {
+ ser.serialize_field("attestationFormats", [NONE].as_slice()).and_then(|()| {
+ ser.serialize_field("extensions", &self.extensions).and_then(|()| ser.end())
+ })
+ })
+ })
+ })
+ })
+ },
+ )
+ })
+ })
+ })
+ })
+ })
+ }
+}
+impl<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ const USER_LEN: usize,
+> Serialize
+ for CredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_LEN,
+ >
+where
+ PublicKeyCredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_LEN,
+ >: Serialize,
+{
+ /// Serializes `self` to conform with
+ /// [`CredentialCreationOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialcreationoptions).
+ ///
+ /// Note [`signal`](https://www.w3.org/TR/credential-management-1/#dom-credentialcreationoptions-signal)
+ /// is not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry)
+ /// is serialized according to [`PublicKeyCredentialCreationOptions::serialize`].
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("CredentialCreationOptions", 2)
+ .and_then(|mut ser| {
+ ser.serialize_field("mediation", &self.mediation)
+ .and_then(|()| {
+ ser.serialize_field("publicKey", &self.public_key)
+ .and_then(|()| ser.end())
+ })
+ })
+ }
+}
+impl<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ const USER_LEN: usize,
+> Serialize
+ for RegistrationClientState<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_LEN,
+ >
+where
+ CredentialCreationOptions<
+ 'rp_id,
+ 'user_name,
+ 'user_display_name,
+ 'user_id,
+ 'prf_first,
+ 'prf_second,
+ USER_LEN,
+ >: Serialize,
+{
+ /// Serializes `self` according to [`CredentialCreationOptions::serialize`].
///
/// # Examples
///
@@ -584,7 +742,7 @@ where
/// # use webauthn_rp::{
/// # request::{
/// # register::{
- /// # FourToSixtyThree, UserHandle64, AuthenticatorAttachmentReq, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
+ /// # FourToSixtyThree, UserHandle64, AuthenticatorAttachmentReq, CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
/// # },
/// # AsciiDomain, ExtensionInfo, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
/// # },
@@ -609,75 +767,78 @@ where
/// creds.push(PublicKeyCredentialDescriptor { id, transports });
/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
/// let user_handle = UserHandle64::new();
- /// let mut options = 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((FourToSixtyThree::new(16).unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new")), ExtensionInfo::RequireEnforceValue));
+ /// let mut options = CredentialCreationOptions::passkey(&rp_id, PublicKeyCredentialUserEntity { name: "pierre.de.fermat".try_into()?, id: &user_handle, display_name: Some("Pierre de Fermat".try_into()?) }, creds);
+ /// options.public_key.authenticator_selection.authenticator_attachment = AuthenticatorAttachmentReq::None(Hint::SecurityKey);
+ /// options.public_key.extensions.min_pin_length = Some((FourToSixtyThree::new(16).unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new")), ExtensionInfo::RequireEnforceValue));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap();
+ /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState::serialize"));
/// let json = serde_json::json!({
- /// "rp":{
- /// "name":"example.com",
- /// "id":"example.com"
- /// },
- /// "user":{
- /// "name":"pierre.de.fermat",
- /// "id":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
- /// "displayName":"Pierre de Fermat"
- /// },
- /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA",
- /// "pubKeyCredParams":[
- /// {
- /// "type":"public-key",
- /// "alg":-8
+ /// "mediation":"optional",
+ /// "publicKey":{
+ /// "rp":{
+ /// "name":"example.com",
+ /// "id":"example.com"
/// },
- /// {
- /// "type":"public-key",
- /// "alg":-7
+ /// "user":{
+ /// "name":"pierre.de.fermat",
+ /// "id":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ /// "displayName":"Pierre de Fermat"
/// },
- /// {
- /// "type":"public-key",
- /// "alg":-35
+ /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA",
+ /// "pubKeyCredParams":[
+ /// {
+ /// "type":"public-key",
+ /// "alg":-8
+ /// },
+ /// {
+ /// "type":"public-key",
+ /// "alg":-7
+ /// },
+ /// {
+ /// "type":"public-key",
+ /// "alg":-35
+ /// },
+ /// {
+ /// "type":"public-key",
+ /// "alg":-257
+ /// },
+ /// ],
+ /// "timeout":300000,
+ /// "excludeCredentials":[
+ /// {
+ /// "type":"public-key",
+ /// "id":"AAAAAAAAAAAAAAAAAAAAAA",
+ /// "transports":["usb"]
+ /// }
+ /// ],
+ /// "authenticatorSelection":{
+ /// "residentKey":"required",
+ /// "requireResidentKey":true,
+ /// "userVerification":"required"
/// },
- /// {
- /// "type":"public-key",
- /// "alg":-257
- /// },
- /// ],
- /// "timeout":300000,
- /// "excludeCredentials":[
- /// {
- /// "type":"public-key",
- /// "id":"AAAAAAAAAAAAAAAAAAAAAA",
- /// "transports":["usb"]
+ /// "hints":[
+ /// "security-key"
+ /// ],
+ /// "attestation":"none",
+ /// "attestationFormats":[
+ /// "none"
+ /// ],
+ /// "extensions":{
+ /// "credentialProtectionPolicy":"userVerificationRequired",
+ /// "enforceCredentialProtectionPolicy":false,
+ /// "minPinLength":true
/// }
- /// ],
- /// "authenticatorSelection":{
- /// "residentKey":"required",
- /// "requireResidentKey":true,
- /// "userVerification":"required"
- /// },
- /// "hints":[
- /// "security-key"
- /// ],
- /// "attestation":"none",
- /// "attestationFormats":[
- /// "none"
- /// ],
- /// "extensions":{
- /// "credentialProtectionPolicy":"userVerificationRequired",
- /// "enforceCredentialProtectionPolicy":false,
- /// "minPinLength":true
/// }
/// }).to_string();
/// // Since `Challenge`s are randomly generated, we don't know what it will be.
/// // Similarly since we randomly generated a 64-byte `UserHandle`, we don't know what
/// // it will be; thus we test the JSON string for everything except those two.
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// assert_eq!(client_state.get(..88), json.get(..88));
+ /// assert_eq!(client_state.get(..124), json.get(..124));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// assert_eq!(client_state.get(174..212), json.get(174..212));
+ /// assert_eq!(client_state.get(210..259), json.get(210..259));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
- /// assert_eq!(client_state.get(245..), json.get(245..));
+ /// assert_eq!(client_state.get(281..), json.get(281..));
/// # Ok::<_, webauthn_rp::AggErr>(())
/// ```
#[inline]
@@ -685,53 +846,7 @@ where
where
S: Serializer,
{
- /// "none".
- const NONE: &str = "none";
- serializer
- .serialize_struct("RegistrationClientState", 11)
- .and_then(|mut ser| {
- ser.serialize_field("rp", &PublicKeyCredentialRpEntity(self.0.rp_id))
- .and_then(|()| {
- ser.serialize_field("user", &self.0.user).and_then(|()| {
- ser.serialize_field("challenge", &self.0.challenge)
- .and_then(|()| {
- ser.serialize_field(
- "pubKeyCredParams",
- &self.0.pub_key_cred_params,
- )
- .and_then(|()| {
- ser.serialize_field("timeout", &self.0.timeout).and_then(
- |()| {
- ser.serialize_field(
- "excludeCredentials",
- self.0.exclude_credentials.as_slice(),
- )
- .and_then(|()| {
- ser.serialize_field(
- "authenticatorSelection",
- &self.0.authenticator_selection,
- )
- .and_then(|()| {
- ser.serialize_field("hints", &match self.0.authenticator_selection.authenticator_attachment {
- AuthenticatorAttachmentReq::None(hint) => hint,
- AuthenticatorAttachmentReq::Platform(hint) => hint.into(),
- AuthenticatorAttachmentReq::CrossPlatform(hint) => hint.into(),
- }).and_then(|()| {
- ser.serialize_field("attestation", NONE).and_then(|()| {
- ser.serialize_field("attestationFormats", [NONE].as_slice()).and_then(|()| {
- ser.serialize_field("extensions", &self.0.extensions).and_then(|()| ser.end())
- })
- })
- })
- })
- })
- },
- )
- })
- })
- })
- })
- })
+ self.0.serialize(serializer)
}
}
impl<'de: 'a, 'a> Deserialize<'de> for Nickname<'a> {
@@ -853,25 +968,36 @@ where
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("UserHandle")
}
- #[expect(clippy::panic_in_result_fn, clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[expect(
+ clippy::panic_in_result_fn,
+ 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(L).unwrap_or_else(|| unreachable!("there is a bug in webauthn_rp::base64url_nopad_len")) == v.len() {
+ if crate::base64url_nopad_len(L).unwrap_or_else(|| {
+ unreachable!("there is a bug in webauthn_rp::base64url_nopad_len")
+ }) == v.len()
+ {
let mut data = [0; L];
BASE64URL_NOPAD
.decode_mut(v.as_bytes(), data.as_mut_slice())
.map_err(|e| E::custom(e.error))
.map(|len| {
- assert_eq!(len, L, "there is a bug in data_encoding::BASE64URL_NOPAD::decode_mut");
+ assert_eq!(
+ len, L,
+ "there is a bug in data_encoding::BASE64URL_NOPAD::decode_mut"
+ );
UserHandle(data)
})
} else {
Err(E::invalid_value(
- Unexpected::Str(v), &format!("{L} bytes encoded in base64url without padding").as_str()
+ Unexpected::Str(v),
+ &format!("{L} bytes encoded in base64url without padding").as_str(),
))
}
}
@@ -903,7 +1029,7 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifier {
match v {
EDDSA => Ok(CoseAlgorithmIdentifier::Eddsa),
ES256 => Ok(CoseAlgorithmIdentifier::Es256),
- ES384=> Ok(CoseAlgorithmIdentifier::Es384),
+ ES384 => Ok(CoseAlgorithmIdentifier::Es384),
RS256 => Ok(CoseAlgorithmIdentifier::Rs256),
_ => Err(E::invalid_value(
Unexpected::Signed(i64::from(v)),
diff --git a/src/request/register/ser_server_state.rs b/src/request/register/ser_server_state.rs
@@ -3,9 +3,9 @@ use super::{
Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible as _,
},
AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, CoseAlgorithmIdentifiers,
- CredProtect, CredentialMediationRequirement, CrossPlatformHint, Extension, ExtensionInfo, Hint,
- PlatformHint, RegistrationServerState, ResidentKeyRequirement, SentChallenge, UserHandle,
- UserVerificationRequirement,
+ CredProtect, CredentialMediationRequirement, CrossPlatformHint, ExtensionInfo, Hint,
+ PlatformHint, RegistrationServerState, ResidentKeyRequirement, SentChallenge,
+ ServerExtensionInfo, ServerPrfInfo, UserHandle, UserVerificationRequirement,
};
use core::{
error::Error,
@@ -168,7 +168,33 @@ impl<'a> DecodeBuffer<'a> for CredProtect {
})
}
}
-impl EncodeBuffer for Extension {
+impl EncodeBuffer for ServerPrfInfo {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::None => 0u8.encode_into_buffer(buffer),
+ Self::One(info) => {
+ 1u8.encode_into_buffer(buffer);
+ info.encode_into_buffer(buffer);
+ }
+ Self::Two(info) => {
+ 2u8.encode_into_buffer(buffer);
+ info.encode_into_buffer(buffer);
+ }
+ }
+ }
+}
+impl<'a> DecodeBuffer<'a> for ServerPrfInfo {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Ok(Self::None),
+ 1 => ExtensionInfo::decode_from_buffer(data).map(Self::One),
+ 2 => ExtensionInfo::decode_from_buffer(data).map(Self::Two),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for ServerExtensionInfo {
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
self.cred_props.encode_into_buffer(buffer);
self.cred_protect.encode_into_buffer(buffer);
@@ -176,13 +202,13 @@ impl EncodeBuffer for Extension {
self.prf.encode_into_buffer(buffer);
}
}
-impl<'a> DecodeBuffer<'a> for Extension {
+impl<'a> DecodeBuffer<'a> for ServerExtensionInfo {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
Option::decode_from_buffer(data).and_then(|cred_props| {
CredProtect::decode_from_buffer(data).and_then(|cred_protect| {
Option::decode_from_buffer(data).and_then(|min_pin_length| {
- Option::decode_from_buffer(data).map(|prf| Self {
+ ServerPrfInfo::decode_from_buffer(data).map(|prf| Self {
cred_props,
cred_protect,
min_pin_length,
@@ -273,7 +299,7 @@ where
CoseAlgorithmIdentifiers::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|pub_key_cred_params| {
AuthenticatorSelectionCriteria::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(
|authenticator_selection| {
- Extension::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|extensions| {
+ ServerExtensionInfo::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|extensions| {
super::validate_options_helper(authenticator_selection, extensions)
.map_err(|_e| DecodeRegistrationServerStateErr::Other)
.and_then(|()| {
diff --git a/src/request/ser.rs b/src/request/ser.rs
@@ -1,9 +1,49 @@
use super::{
- Challenge, CredentialId, Hint, PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement,
+ Challenge, CredentialId, CredentialMediationRequirement, Hint, PrfInput,
+ PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement,
};
use core::str;
use data_encoding::BASE64URL_NOPAD;
use serde::ser::{Serialize, SerializeSeq as _, SerializeStruct as _, Serializer};
+impl Serialize for CredentialMediationRequirement {
+ /// Serializes `self` to conform with
+ /// [`CredentialMediationRequirement`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::CredentialMediationRequirement;
+ /// assert_eq!(
+ /// serde_json::to_string(&CredentialMediationRequirement::Silent)?,
+ /// r#""silent""#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&CredentialMediationRequirement::Optional)?,
+ /// r#""optional""#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&CredentialMediationRequirement::Conditional)?,
+ /// r#""conditional""#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&CredentialMediationRequirement::Required)?,
+ /// r#""required""#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(match *self {
+ Self::Silent => "silent",
+ Self::Optional => "optional",
+ Self::Conditional => "conditional",
+ Self::Required => "required",
+ })
+ }
+}
impl Serialize for Challenge {
/// Serializes `self` to conform with
/// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-challenge).
@@ -313,3 +353,48 @@ impl Serialize for Hint {
})
}
}
+impl Serialize for PrfInput<'_, '_> {
+ /// Serializes `self` to conform with
+ /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{PrfInput, ExtensionReq};
+ /// assert_eq!(
+ /// serde_json::to_string(&PrfInput {
+ /// first: [0; 4].as_slice(),
+ /// second: Some([2; 1].as_slice()),
+ /// })?,
+ /// r#"{"first":"AAAAAA","second":"Ag"}"#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[expect(
+ clippy::arithmetic_side_effects,
+ reason = "comment justifies how overflow is not possible"
+ )]
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ // The max value is 1 + 1 = 2, so overflow is not an issue.
+ .serialize_struct("PrfInput", 1 + usize::from(self.second.is_some()))
+ .and_then(|mut ser| {
+ ser.serialize_field("first", BASE64URL_NOPAD.encode(self.first).as_str())
+ .and_then(|()| {
+ self.second
+ .as_ref()
+ .map_or(Ok(()), |second| {
+ ser.serialize_field(
+ "second",
+ BASE64URL_NOPAD.encode(second).as_str(),
+ )
+ })
+ .and_then(|()| ser.end())
+ })
+ })
+ }
+}
diff --git a/src/response.rs b/src/response.rs
@@ -99,12 +99,12 @@ use ser_relaxed::SerdeJsonErr;
/// }
/// /// Send `DiscoverableAuthenticationClientState` and receive `DiscoverableAuthentication64` JSON from client.
/// # #[cfg(feature = "serde")]
-/// fn get_authentication_json(client: DiscoverableAuthenticationClientState<'_, '_>) -> String {
+/// fn get_authentication_json(client: DiscoverableAuthenticationClientState<'_, '_, '_>) -> String {
/// // â‹®
/// # let client_data_json = BASE64URL_NOPAD.encode(serde_json::json!({
/// # "type": "webauthn.get",
-/// # "challenge": client.options().0.challenge,
-/// # "origin": format!("https://{}", client.options().0.rp_id.as_ref()),
+/// # "challenge": client.options().public_key.challenge,
+/// # "origin": format!("https://{}", client.options().public_key.rp_id.as_ref()),
/// # "crossOrigin": false
/// # }).to_string().as_bytes());
/// # serde_json::json!({
@@ -156,7 +156,7 @@ pub mod error;
/// # use data_encoding::BASE64URL_NOPAD;
/// # use webauthn_rp::{
/// # hash::hash_set::FixedCapHashSet,
-/// # request::{register::{error::CreationOptionsErr, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomain, PublicKeyCredentialDescriptor, RpId},
+/// # request::{register::{error::CreationOptionsErr, CredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomain, PublicKeyCredentialDescriptor, RpId},
/// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData},
/// # RegisteredCredential
/// # };
@@ -204,7 +204,7 @@ pub mod error;
/// # #[cfg(feature = "custom")]
/// let creds = get_registered_credentials(user_handle);
/// # #[cfg(feature = "custom")]
-/// let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, user, creds).start_ceremony()?;
+/// let (server, client) = CredentialCreationOptions::passkey(&rp_id, user, creds).start_ceremony()?;
/// # #[cfg(feature = "custom")]
/// assert!(
/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity)
@@ -237,12 +237,12 @@ pub mod error;
/// }
/// /// Send `RegistrationClientState` and receive `Registration` JSON from client.
/// # #[cfg(feature = "serde")]
-/// fn get_registration_json(client: RegistrationClientState<'_, '_, '_, '_, USER_HANDLE_MAX_LEN>) -> 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",
-/// # "challenge": client.options().challenge,
-/// # "origin": format!("https://{}", client.options().rp_id.as_ref()),
+/// # "challenge": client.options().public_key.challenge,
+/// # "origin": format!("https://{}", client.options().public_key.rp_id.as_ref()),
/// # "crossOrigin": false
/// # }).to_string().as_bytes());
/// # serde_json::json!({
@@ -1531,6 +1531,8 @@ pub(super) enum CeremonyErr<AuthDataErr> {
BackupEligible,
/// [`Backup::NotEligible`] was sent back despite [`BackupReq::Eligible`].
BackupNotEligible,
+ /// [`Backup::Eligible`] was not sent back despite [`BackupReq::EligibleNoteExists`].
+ BackupExists,
/// [`Backup::Exists`] was not sent back despite [`BackupReq::Exists`].
BackupDoesNotExist,
}
@@ -1557,6 +1559,7 @@ impl<A: Display> Display for CeremonyErr<A> {
Self::UserNotVerified => f.write_str("user was not verified despite being required to"),
Self::BackupEligible => f.write_str("credential is eligible to be backed up despite requiring that it not be"),
Self::BackupNotEligible => f.write_str("credential is not eligible to be backed up despite requiring that it be"),
+ Self::BackupExists => f.write_str("credential backup exists despite requiring that a backup not exist"),
Self::BackupDoesNotExist => f.write_str("credential backup does not exist despite requiring that a backup exist"),
}
}
@@ -1575,6 +1578,7 @@ impl From<CeremonyErr<AttestationObjectErr>> for RegCeremonyErr {
CeremonyErr::UserNotVerified => Self::UserNotVerified,
CeremonyErr::BackupEligible => Self::BackupEligible,
CeremonyErr::BackupNotEligible => Self::BackupNotEligible,
+ CeremonyErr::BackupExists => Self::BackupExists,
CeremonyErr::BackupDoesNotExist => Self::BackupDoesNotExist,
}
}
@@ -1593,6 +1597,7 @@ impl From<CeremonyErr<AuthAuthDataErr>> for AuthCeremonyErr {
CeremonyErr::UserNotVerified => Self::UserNotVerified,
CeremonyErr::BackupEligible => Self::BackupEligible,
CeremonyErr::BackupNotEligible => Self::BackupNotEligible,
+ CeremonyErr::BackupExists => Self::BackupExists,
CeremonyErr::BackupDoesNotExist => Self::BackupDoesNotExist,
}
}
@@ -1626,6 +1631,145 @@ pub struct CurrentUserDetailsOptions<'rp_id, 'name, 'display_name, 'id, const LE
/// [`displayName`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-displayname).
pub user: PublicKeyCredentialUserEntity<'name, 'display_name, 'id, LEN>,
}
+/// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
+/// during authentication and
+/// [`hmac-secret-mc`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-make-cred-extension)
+/// during registration.
+///
+/// `REG` iff `hmac-secret-mc`.
+enum HmacSecretGet<const REG: bool> {
+ /// No `hmac-secret` response.
+ None,
+ /// One encrypted `hmac-secret`.
+ One,
+ /// Two encrypted `hmac-secret`s.
+ Two,
+}
+/// Error returned by [`HmacSecretGet::from_cbor`]
+enum HmacSecretGetErr {
+ /// Error related to the length of the CBOR input.
+ Len,
+ /// Error related to the type of the CBOR key.
+ Type,
+ /// Error related to the value of the CBOR value.
+ Value,
+}
+impl<const REG: bool> FromCbor<'_> for HmacSecretGet<REG> {
+ type Err = HmacSecretGetErr;
+ fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
+ /// AES block size.
+ const AES_BLOCK_SIZE: usize = 16;
+ /// HMAC-SHA-256 output length.
+ const HMAC_SHA_256_LEN: usize = 32;
+ /// Length of two HMAC-SHA-256 outputs concatenated together.
+ const TWO_HMAC_SHA_256_LEN: usize = HMAC_SHA_256_LEN << 1;
+ // We need the smallest multiple of `AES_BLOCK_SIZE` that
+ // is strictly greater than `HMAC_SHA_256_LEN`.
+ /// AES-256 output length on a 32-byte input.
+ #[expect(
+ clippy::integer_division_remainder_used,
+ reason = "doesn't need to be constant time"
+ )]
+ const ONE_SECRET_LEN: usize =
+ HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (HMAC_SHA_256_LEN % AES_BLOCK_SIZE));
+ // We need the smallest multiple of `AES_BLOCK_SIZE` that
+ // is strictly greater than `TWO_HMAC_SHA_256_LEN`.
+ /// AES-256 output length on a 64-byte input.
+ #[expect(
+ clippy::integer_division_remainder_used,
+ reason = "doesn't need to be constant time"
+ )]
+ const TWO_SECRET_LEN: usize =
+ TWO_HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (TWO_HMAC_SHA_256_LEN % AES_BLOCK_SIZE));
+ /// `hmac-secret-mc`.
+ ///
+ /// This is the key iff `REG`.
+ const KEY: [u8; 15] = [
+ cbor::TEXT_14,
+ b'h',
+ b'm',
+ b'a',
+ b'c',
+ b'-',
+ b's',
+ b'e',
+ b'c',
+ b'r',
+ b'e',
+ b't',
+ b'-',
+ b'm',
+ b'c',
+ ];
+ /// Helper that unifies `HmacSecretGet`.
+ enum CborVal<'a> {
+ /// Extension does not exist with remaining payload
+ Success,
+ /// Extension exists with remaining payload.
+ Continue(&'a [u8]),
+ }
+ if REG {
+ cbor.split_at_checked(KEY.len()).map_or(
+ Ok(CborVal::Success),
+ |(key, key_rem)| {
+ if key == KEY {
+ Ok(CborVal::Continue(key_rem))
+ } else {
+ Ok(CborVal::Success)
+ }
+ }
+ )
+ } else {
+ cbor.split_at_checked(cbor::HMAC_SECRET.len()).map_or(
+ Ok(CborVal::Success),
+ |(key, key_rem)| {
+ if key == cbor::HMAC_SECRET {
+ Ok(CborVal::Continue(key_rem))
+ } else {
+ Ok(CborVal::Success)
+ }
+ }
+ )
+ }.and_then(|cbor_val| {
+ match cbor_val {
+ CborVal::Success => Ok(CborSuccess { value: Self::None, remaining: cbor, }),
+ CborVal::Continue(key_rem) => {
+ key_rem
+ .split_first()
+ .ok_or(HmacSecretGetErr::Len)
+ .and_then(|(bytes, bytes_rem)| {
+ if *bytes == cbor::BYTES_INFO_24 {
+ bytes_rem
+ .split_first()
+ .ok_or(HmacSecretGetErr::Len)
+ .and_then(|(&len, len_rem)| {
+ len_rem.split_at_checked(usize::from(len)).ok_or(HmacSecretGetErr::Len).and_then(|(_, remaining)| {
+ match usize::from(len) {
+ ONE_SECRET_LEN => {
+ Ok(CborSuccess {
+ value: Self::One,
+ remaining,
+ })
+ }
+ TWO_SECRET_LEN => {
+ Ok(CborSuccess {
+ value: Self::Two,
+ remaining,
+ })
+ }
+ _ => Err(HmacSecretGetErr::Value),
+ }
+ })
+ })
+ } else {
+ Err(HmacSecretGetErr::Type)
+ }
+ })
+ }
+ }
+ })
+ }
+}
#[cfg(test)]
mod tests {
use super::{CollectedClientDataErr, ClientDataJsonParser, LimitedVerificationParser};
diff --git a/src/response/auth.rs b/src/response/auth.rs
@@ -20,7 +20,8 @@ use super::{
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,
+ HmacSecretGet, HmacSecretGetErr, LimitedVerificationParser, ParsedAuthData, Response,
+ SentChallenge,
auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr, MissingUserHandleErr},
cbor,
error::CollectedClientDataErr,
@@ -78,74 +79,6 @@ pub enum HmacSecret {
/// extension.
Two,
}
-impl FromCbor<'_> for HmacSecret {
- type Err = AuthenticatorExtensionOutputErr;
- fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
- /// AES block size.
- const AES_BLOCK_SIZE: usize = 16;
- /// HMAC-SHA-256 output length.
- const HMAC_SHA_256_LEN: usize = 32;
- /// Length of two HMAC-SHA-256 outputs concatenated together.
- const TWO_HMAC_SHA_256_LEN: usize = HMAC_SHA_256_LEN << 1;
- // We need the smallest multiple of `AES_BLOCK_SIZE` that
- // is strictly greater than `HMAC_SHA_256_LEN`.
- /// AES-256 output length on a 32-byte input.
- #[expect(
- clippy::integer_division_remainder_used,
- reason = "doesn't need to be constant time"
- )]
- const ONE_SECRET_LEN: usize =
- HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (HMAC_SHA_256_LEN % AES_BLOCK_SIZE));
- // We need the smallest multiple of `AES_BLOCK_SIZE` that
- // is strictly greater than `TWO_HMAC_SHA_256_LEN`.
- /// AES-256 output length on a 64-byte input.
- #[expect(
- clippy::integer_division_remainder_used,
- reason = "doesn't need to be constant time"
- )]
- const TWO_SECRET_LEN: usize =
- TWO_HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (TWO_HMAC_SHA_256_LEN % AES_BLOCK_SIZE));
- cbor.split_at_checked(cbor::HMAC_SECRET.len())
- .ok_or(AuthenticatorExtensionOutputErr::Len)
- .and_then(|(key, key_rem)| {
- if key == cbor::HMAC_SECRET {
- key_rem
- .split_first()
- .ok_or(AuthenticatorExtensionOutputErr::Len)
- .and_then(|(bytes, bytes_rem)| {
- if *bytes == cbor::BYTES_INFO_24 {
- bytes_rem
- .split_first()
- .ok_or(AuthenticatorExtensionOutputErr::Len)
- .and_then(|(&len, len_rem)| {
- len_rem.split_at_checked(usize::from(len)).ok_or(AuthenticatorExtensionOutputErr::Len).and_then(|(_, remaining)| {
- match usize::from(len) {
- ONE_SECRET_LEN => {
- Ok(CborSuccess {
- value: Self::One,
- remaining,
- })
- }
- TWO_SECRET_LEN => {
- Ok(CborSuccess {
- value: Self::Two,
- remaining,
- })
- }
- _ => Err(AuthenticatorExtensionOutputErr::HmacSecretValue),
- }
- })
- })
- } else {
- Err(AuthenticatorExtensionOutputErr::HmacSecretType)
- }
- })
- } else {
- Err(AuthenticatorExtensionOutputErr::Unsupported)
- }
- })
- }
-}
/// [Authenticator extension output](https://www.w3.org/TR/webauthn-3/#authenticator-extension-output).
#[derive(Clone, Copy, Debug)]
pub struct AuthenticatorExtensionOutput {
@@ -157,6 +90,26 @@ impl AuthExtOutput for AuthenticatorExtensionOutput {
matches!(self.hmac_secret, HmacSecret::None)
}
}
+impl From<HmacSecretGetErr> for AuthenticatorExtensionOutputErr {
+ #[inline]
+ fn from(value: HmacSecretGetErr) -> Self {
+ match value {
+ HmacSecretGetErr::Len => Self::Len,
+ HmacSecretGetErr::Type => Self::HmacSecretType,
+ HmacSecretGetErr::Value => Self::HmacSecretValue,
+ }
+ }
+}
+impl From<HmacSecretGet<false>> for HmacSecret {
+ #[inline]
+ fn from(value: HmacSecretGet<false>) -> Self {
+ match value {
+ HmacSecretGet::None => Self::None,
+ HmacSecretGet::One => Self::One,
+ HmacSecretGet::Two => Self::Two,
+ }
+ }
+}
impl FromCbor<'_> for AuthenticatorExtensionOutput {
type Err = AuthenticatorExtensionOutputErr;
fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
@@ -173,12 +126,14 @@ impl FromCbor<'_> for AuthenticatorExtensionOutput {
},
|(map, map_rem)| {
if *map == cbor::MAP_1 {
- HmacSecret::from_cbor(map_rem).map(|success| CborSuccess {
- value: Self {
- hmac_secret: success.value,
- },
- remaining: success.remaining,
- })
+ HmacSecretGet::from_cbor(map_rem)
+ .map_err(AuthenticatorExtensionOutputErr::from)
+ .map(|success| CborSuccess {
+ value: Self {
+ hmac_secret: success.value.into(),
+ },
+ remaining: success.remaining,
+ })
} else {
Err(AuthenticatorExtensionOutputErr::CborHeader)
}
diff --git a/src/response/auth/error.rs b/src/response/auth/error.rs
@@ -235,6 +235,8 @@ pub enum AuthCeremonyErr {
BackupEligible,
/// [`Backup::NotEligible`] was sent back despite [`BackupReq::Eligible`].
BackupNotEligible,
+ /// [`Backup::Eligible`] was not sent back despite [`BackupReq::EligibleNotExists`].
+ BackupExists,
/// [`Backup::Exists`] was not sent back despite [`BackupReq::Exists`].
BackupDoesNotExist,
/// [`AuthenticatorAttachment`] was not sent back despite being required.
@@ -278,6 +280,7 @@ impl Display for AuthCeremonyErr {
Self::TopOriginMismatch => CeremonyErr::<AuthenticatorDataErr>::TopOriginMismatch.fmt(f),
Self::BackupEligible => CeremonyErr::<AuthenticatorDataErr>::BackupEligible.fmt(f),
Self::BackupNotEligible => CeremonyErr::<AuthenticatorDataErr>::BackupNotEligible.fmt(f),
+ Self::BackupExists => CeremonyErr::<AuthenticatorDataErr>::BackupExists.fmt(f),
Self::BackupDoesNotExist => CeremonyErr::<AuthenticatorDataErr>::BackupDoesNotExist.fmt(f),
Self::ChallengeMismatch => CeremonyErr::<AuthenticatorDataErr>::ChallengeMismatch.fmt(f),
Self::RpIdHashMismatch => CeremonyErr::<AuthenticatorDataErr>::RpIdHashMismatch.fmt(f),
diff --git a/src/response/cbor.rs b/src/response/cbor.rs
@@ -50,6 +50,8 @@ pub(super) const TEXT_8: u8 = TEXT | 8;
pub(super) const TEXT_11: u8 = TEXT | 11;
/// [`TEXT`] length `12`.
pub(super) const TEXT_12: u8 = TEXT | 12;
+/// [`TEXT`] length `14`.
+pub(super) const TEXT_14: u8 = TEXT | 14;
/// [`MAP`] length `0`.
pub(super) const MAP_0: u8 = MAP;
/// [`MAP`] length `1`.
diff --git a/src/response/register.rs b/src/response/register.rs
@@ -11,8 +11,8 @@ use super::{
super::request::register::{FourToSixtyThree, ResidentKeyRequirement},
AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthTransports,
AuthenticatorAttachment, Backup, CborSuccess, ClientDataJsonParser as _, CollectedClientData,
- CredentialId, Flag, FromCbor, LimitedVerificationParser, ParsedAuthData, Response,
- SentChallenge, cbor,
+ CredentialId, Flag, FromCbor, HmacSecretGet, HmacSecretGetErr, LimitedVerificationParser,
+ ParsedAuthData, Response, SentChallenge, cbor,
error::CollectedClientDataErr,
register::error::{
AaguidErr, AttestationErr, AttestationObjectErr, AttestedCredentialDataErr,
@@ -95,20 +95,41 @@ impl Display for CredentialProtectionPolicy {
})
}
}
+#[expect(clippy::too_long_first_doc_paragraph, reason = "false positive")]
+/// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
+/// and
+/// [`hmac-secret-mc`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-make-cred-extension).
+///
+/// Note `hmac-secret-mc` can only exist if `hmac-secret` exists with a value of `true`.
+#[derive(Clone, Copy, Debug)]
+pub enum HmacSecret {
+ /// No `hmac-secret` extension.
+ None,
+ /// `hmac-secret` extension with a value of `false`.
+ NotEnabled,
+ /// `hmac-secret` extension with a value of `true`.
+ Enabled,
+ /// `hmac-secret` extension with a value of `true` and `hmac-secret-mc` contained one encrypted PRF output.
+ One,
+ /// `hmac-secret` extension with a value of `true` and `hmac-secret-mc` contained two encrypted PRF outputs.
+ Two,
+}
/// [Authenticator extension output](https://www.w3.org/TR/webauthn-3/#authenticator-extension-output).
#[derive(Clone, Copy, Debug)]
pub struct AuthenticatorExtensionOutput {
/// [`credProtect`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension).
pub cred_protect: CredentialProtectionPolicy,
- /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension).
- pub hmac_secret: Option<bool>,
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
+ /// and
+ /// [`hmac-secret-mc`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-make-cred-extension).
+ pub hmac_secret: HmacSecret,
/// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension).
pub min_pin_length: Option<FourToSixtyThree>,
}
impl AuthExtOutput for AuthenticatorExtensionOutput {
fn missing(self) -> bool {
matches!(self.cred_protect, CredentialProtectionPolicy::None)
- && self.hmac_secret.is_none()
+ && matches!(self.hmac_secret, HmacSecret::None)
&& self.min_pin_length.is_none()
}
}
@@ -119,6 +140,9 @@ pub struct AuthenticatorExtensionOutputStaticState {
/// [`AuthenticatorExtensionOutput::cred_protect`].
pub cred_protect: CredentialProtectionPolicy,
/// [`AuthenticatorExtensionOutput::hmac_secret`].
+ ///
+ /// Note we only care about whether or not it has been enabled. Specifcally this is `None` iff
+ /// [`HmacSecret::None`], `Some(false)` iff [`HmacSecret::NotEnabled`]; otherwise `Some(true)`.
pub hmac_secret: Option<bool>,
}
/// [`AuthenticatorExtensionOutput`] extensions that are saved in [`Metadata`] because they are purely informative
@@ -141,7 +165,11 @@ impl From<AuthenticatorExtensionOutput> for AuthenticatorExtensionOutputStaticSt
fn from(value: AuthenticatorExtensionOutput) -> Self {
Self {
cred_protect: value.cred_protect,
- hmac_secret: value.hmac_secret,
+ hmac_secret: match value.hmac_secret {
+ HmacSecret::None => None,
+ HmacSecret::NotEnabled => Some(false),
+ HmacSecret::Enabled | HmacSecret::One | HmacSecret::Two => Some(true),
+ },
}
}
}
@@ -201,13 +229,13 @@ impl FromCbor<'_> for CredentialProtectionPolicy {
}
}
/// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension).
-enum HmacSecret {
+enum HmacSecretEnabled {
/// No `hmac-secret` extension.
None,
/// `hmac-secret` set to the contained `bool`.
Val(bool),
}
-impl FromCbor<'_> for HmacSecret {
+impl FromCbor<'_> for HmacSecretEnabled {
type Err = AuthenticatorExtensionOutputErr;
fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
cbor.split_at_checked(cbor::HMAC_SECRET.len()).map_or(
@@ -312,6 +340,16 @@ impl FromCbor<'_> for MinPinLength {
)
}
}
+impl From<HmacSecretGetErr> for AuthenticatorExtensionOutputErr {
+ #[inline]
+ fn from(value: HmacSecretGetErr) -> Self {
+ match value {
+ HmacSecretGetErr::Len => Self::Len,
+ HmacSecretGetErr::Type => Self::HmacSecretMcType,
+ HmacSecretGetErr::Value => Self::HmacSecretMcValue,
+ }
+ }
+}
impl FromCbor<'_> for AuthenticatorExtensionOutput {
type Err = AuthenticatorExtensionOutputErr;
#[expect(
@@ -320,16 +358,18 @@ impl FromCbor<'_> for AuthenticatorExtensionOutput {
)]
fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
// We don't allow unsupported extensions; thus the only possibilities is any ordered element of
- // the power set of {"credProtect":<1, 2, or 3>, "hmac-secret":<true or false>, "minPinLength":<0-255>}.
+ // the power set of {"credProtect":<1, 2, or 3>, "hmac-secret":<true or false>, "minPinLength":<0-255>, "hmac-secret-mc":<48|80 bytes>}.
// Since the keys are the same type (text), order is first done based on length; and then
// byte-wise lexical order is followed; thus `credProtect` must come before `hmac-secret` which
- // must come before `minPinLength`.
+ // must come before `minPinLength` which comes before `hmac-secret-mc`.
+ //
+ // Note `hmac-secret-mc` can only exist if `hmac-secret` exists with a value of `true`.
cbor.split_first().map_or_else(
|| {
Ok(CborSuccess {
value: Self {
cred_protect: CredentialProtectionPolicy::None,
- hmac_secret: None,
+ hmac_secret: HmacSecret::None,
min_pin_length: None,
},
remaining: cbor,
@@ -339,39 +379,46 @@ impl FromCbor<'_> for AuthenticatorExtensionOutput {
cbor::MAP_1 => {
CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| {
if matches!(cred_success.value, CredentialProtectionPolicy::None) {
- HmacSecret::from_cbor(cred_success.remaining).and_then(|hmac_success| {
- match hmac_success.value {
- HmacSecret::None => MinPinLength::from_cbor(
+ HmacSecretEnabled::from_cbor(cred_success.remaining).and_then(
+ |hmac_success| match hmac_success.value {
+ HmacSecretEnabled::None => MinPinLength::from_cbor(
hmac_success.remaining,
)
.and_then(|pin_success| match pin_success.value {
MinPinLength::None => {
+ // We don't even bother checking for `HmacSecretGet` since
+ // it's only valid when `HmacSecretEnabled` exists with a value
+ // of `true`.
Err(AuthenticatorExtensionOutputErr::Missing)
}
MinPinLength::Val(min_pin_len) => Ok(CborSuccess {
value: Self {
cred_protect: cred_success.value,
- hmac_secret: None,
+ hmac_secret: HmacSecret::None,
min_pin_length: Some(min_pin_len),
},
remaining: pin_success.remaining,
}),
}),
- HmacSecret::Val(hmac) => Ok(CborSuccess {
+ HmacSecretEnabled::Val(hmac) => Ok(CborSuccess {
value: Self {
cred_protect: cred_success.value,
- hmac_secret: Some(hmac),
+ hmac_secret: if hmac {
+ HmacSecret::Enabled
+ } else {
+ HmacSecret::NotEnabled
+ },
min_pin_length: None,
},
remaining: hmac_success.remaining,
}),
- }
- })
+ },
+ )
} else {
Ok(CborSuccess {
value: Self {
cred_protect: cred_success.value,
- hmac_secret: None,
+ hmac_secret: HmacSecret::None,
min_pin_length: None,
},
remaining: cred_success.remaining,
@@ -382,89 +429,275 @@ impl FromCbor<'_> for AuthenticatorExtensionOutput {
cbor::MAP_2 => {
CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| {
if matches!(cred_success.value, CredentialProtectionPolicy::None) {
- HmacSecret::from_cbor(cred_success.remaining).and_then(|hmac_success| {
- match hmac_success.value {
- HmacSecret::None => {
+ HmacSecretEnabled::from_cbor(cred_success.remaining).and_then(
+ |hmac_success| match hmac_success.value {
+ HmacSecretEnabled::None => {
+ // We don't even bother checking for `HmacSecretGet` since
+ // it's only valid when `HmacSecretEnabled` exists with a value
+ // of `true`.
Err(AuthenticatorExtensionOutputErr::Missing)
}
- HmacSecret::Val(hmac) => MinPinLength::from_cbor(
+ HmacSecretEnabled::Val(hmac) => MinPinLength::from_cbor(
hmac_success.remaining,
)
.and_then(|pin_success| match pin_success.value {
MinPinLength::None => {
- Err(AuthenticatorExtensionOutputErr::Missing)
+ if hmac {
+ HmacSecretGet::<true>::from_cbor(
+ pin_success.remaining,
+ )
+ .map_err(AuthenticatorExtensionOutputErr::from)
+ .and_then(|hmac_get| match hmac_get.value {
+ HmacSecretGet::None => Err(
+ AuthenticatorExtensionOutputErr::Missing,
+ ),
+ HmacSecretGet::One => Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: HmacSecret::One,
+ min_pin_length: None,
+ },
+ remaining: hmac_get.remaining,
+ }),
+ HmacSecretGet::Two => Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: HmacSecret::Two,
+ min_pin_length: None,
+ },
+ remaining: hmac_get.remaining,
+ }),
+ })
+ } else {
+ // We don't even bother checking for `HmacSecretGet` since
+ // it's only valid when `HmacSecretEnabled` exists with a value
+ // of `true`.
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
}
MinPinLength::Val(min_pin_len) => Ok(CborSuccess {
value: Self {
cred_protect: cred_success.value,
- hmac_secret: Some(hmac),
+ hmac_secret: if hmac {
+ HmacSecret::Enabled
+ } else {
+ HmacSecret::NotEnabled
+ },
min_pin_length: Some(min_pin_len),
},
remaining: pin_success.remaining,
}),
}),
- }
- })
+ },
+ )
} else {
- HmacSecret::from_cbor(cred_success.remaining).and_then(|hmac_success| {
- match hmac_success.value {
- HmacSecret::None => MinPinLength::from_cbor(
+ HmacSecretEnabled::from_cbor(cred_success.remaining).and_then(
+ |hmac_success| match hmac_success.value {
+ HmacSecretEnabled::None => MinPinLength::from_cbor(
hmac_success.remaining,
)
.and_then(|pin_success| match pin_success.value {
MinPinLength::None => {
+ // We don't even bother checking for `HmacSecretGet` since
+ // it's only valid when `HmacSecretEnabled` exists with a value
+ // of `true`.
Err(AuthenticatorExtensionOutputErr::Missing)
}
MinPinLength::Val(min_pin_len) => Ok(CborSuccess {
value: Self {
cred_protect: cred_success.value,
- hmac_secret: None,
+ hmac_secret: HmacSecret::None,
min_pin_length: Some(min_pin_len),
},
remaining: pin_success.remaining,
}),
}),
- HmacSecret::Val(hmac) => Ok(CborSuccess {
+ // We don't even bother checking for `HmacSecretGet` since
+ // it's only valid when `HmacSecretEnabled` exists with a value
+ // of `true`.
+ HmacSecretEnabled::Val(hmac) => Ok(CborSuccess {
value: Self {
cred_protect: cred_success.value,
- hmac_secret: Some(hmac),
+ hmac_secret: if hmac {
+ HmacSecret::Enabled
+ } else {
+ HmacSecret::NotEnabled
+ },
min_pin_length: None,
},
remaining: hmac_success.remaining,
}),
- }
- })
+ },
+ )
}
})
}
cbor::MAP_3 => {
CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| {
if matches!(cred_success.value, CredentialProtectionPolicy::None) {
+ HmacSecretEnabled::from_cbor(cred_success.remaining).and_then(
+ |hmac_success| match hmac_success.value {
+ HmacSecretEnabled::None => {
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ HmacSecretEnabled::Val(hmac) => {
+ if hmac {
+ MinPinLength::from_cbor(
+ hmac_success.remaining,
+ )
+ .and_then(|pin_success| match pin_success.value {
+ MinPinLength::None => Err(AuthenticatorExtensionOutputErr::Missing),
+ MinPinLength::Val(min_pin_len) => HmacSecretGet::<true>::from_cbor(pin_success.remaining).map_err(AuthenticatorExtensionOutputErr::from).and_then(|hmac_get| {
+ match hmac_get.value {
+ HmacSecretGet::None => Err(AuthenticatorExtensionOutputErr::Missing),
+ HmacSecretGet::One => {
+ Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: HmacSecret::One,
+ min_pin_length: Some(min_pin_len),
+ },
+ remaining: hmac_get.remaining,
+ })
+ }
+ HmacSecretGet::Two => {
+ Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: HmacSecret::Two,
+ min_pin_length: Some(min_pin_len),
+ },
+ remaining: hmac_get.remaining,
+ })
+ }
+ }
+ })
+ })
+ } else {
+ // We don't even bother checking for `HmacSecretGet` since
+ // it's only valid when `HmacSecretEnabled` exists with a value
+ // of `true`.
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ }
+ },
+ )
+ } else {
+ HmacSecretEnabled::from_cbor(cred_success.remaining).and_then(
+ |hmac_success| match hmac_success.value {
+ // We don't even bother checking for `HmacSecretGet` since
+ // it's only valid when `HmacSecretEnabled` exists with a value
+ // of `true`.
+ HmacSecretEnabled::None => Err(AuthenticatorExtensionOutputErr::Missing),
+ HmacSecretEnabled::Val(hmac) => {
+ MinPinLength::from_cbor(hmac_success.remaining).and_then(|pin_success| {
+ match pin_success.value {
+ MinPinLength::None => {
+ if hmac {
+ HmacSecretGet::<true>::from_cbor(pin_success.remaining).map_err(AuthenticatorExtensionOutputErr::from).and_then(|hmac_get| {
+ match hmac_get.value {
+ HmacSecretGet::None => Err(AuthenticatorExtensionOutputErr::Missing),
+ HmacSecretGet::One => {
+ Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: HmacSecret::One,
+ min_pin_length: None,
+ },
+ remaining: hmac_get.remaining,
+ })
+ }
+ HmacSecretGet::Two => {
+ Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: HmacSecret::Two,
+ min_pin_length: None,
+ },
+ remaining: hmac_get.remaining,
+ })
+ }
+ }
+ })
+ } else {
+ // We don't even bother checking for `HmacSecretGet` since
+ // it's only valid when `HmacSecretEnabled` exists with a value
+ // of `true`.
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ }
+ MinPinLength::Val(min_pin_len) => {
+ Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: if hmac { HmacSecret::Enabled } else { HmacSecret::NotEnabled },
+ min_pin_length: Some(min_pin_len),
+ },
+ remaining: pin_success.remaining,
+ })
+ }
+ }
+ })
+ }
+ }
+ )
+ }
+ })
+ }
+ cbor::MAP_4 => {
+ CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| {
+ if matches!(cred_success.value, CredentialProtectionPolicy::None) {
Err(AuthenticatorExtensionOutputErr::Missing)
} else {
- HmacSecret::from_cbor(cred_success.remaining).and_then(|hmac_success| {
- match hmac_success.value {
- HmacSecret::None => {
+ HmacSecretEnabled::from_cbor(cred_success.remaining).and_then(
+ |hmac_success| match hmac_success.value {
+ HmacSecretEnabled::None => {
Err(AuthenticatorExtensionOutputErr::Missing)
}
- HmacSecret::Val(hmac) => MinPinLength::from_cbor(
- hmac_success.remaining,
- )
- .and_then(|pin_success| match pin_success.value {
- MinPinLength::None => {
+ HmacSecretEnabled::Val(hmac) => {
+ if hmac {
+ MinPinLength::from_cbor(
+ hmac_success.remaining,
+ )
+ .and_then(|pin_success| match pin_success.value {
+ MinPinLength::None => {
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ MinPinLength::Val(min_pin_len) => HmacSecretGet::<true>::from_cbor(pin_success.remaining).map_err(AuthenticatorExtensionOutputErr::from).and_then(|hmac_get| {
+ match hmac_get.value {
+ HmacSecretGet::None => Err(AuthenticatorExtensionOutputErr::Missing),
+ HmacSecretGet::One => {
+ Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: HmacSecret::One,
+ min_pin_length: Some(min_pin_len),
+ },
+ remaining: hmac_get.remaining,
+ })
+ }
+ HmacSecretGet::Two => {
+ Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: HmacSecret::Two,
+ min_pin_length: Some(min_pin_len),
+ },
+ remaining: hmac_get.remaining,
+ })
+ }
+ }
+ })
+ })
+ } else {
+ // We don't even bother checking for `HmacSecretGet` since
+ // it's only valid when `HmacSecretEnabled` exists with a value
+ // of `true`.
Err(AuthenticatorExtensionOutputErr::Missing)
}
- MinPinLength::Val(min_pin_len) => Ok(CborSuccess {
- value: Self {
- cred_protect: cred_success.value,
- hmac_secret: Some(hmac),
- min_pin_length: Some(min_pin_len),
- },
- remaining: pin_success.remaining,
- }),
- }),
- }
- })
+ }
+ },
+ )
}
})
}
@@ -3214,8 +3447,14 @@ mod tests {
},
auth::{AuthenticatorData, NonDiscoverableAuthenticatorAssertion},
},
- AttestationFormat, AttestationObject, AuthDataContainer as _, AuthTransports,
- AuthenticatorAttestation, Backup, Sig, UncompressedPubKey,
+ AttestationFormat, AttestationObject, AuthDataContainer as _, AuthExtOutput as _,
+ AuthTransports, AuthenticatorAttestation, AuthenticatorExtensionOutput,
+ AuthenticatorExtensionOutputErr, Backup, CborSuccess, CredentialProtectionPolicy,
+ FromCbor as _, HmacSecret, Sig, UncompressedPubKey,
+ cbor::{
+ BYTES, BYTES_INFO_24, MAP_1, MAP_2, MAP_3, MAP_4, SIMPLE_FALSE, SIMPLE_TRUE, TEXT_11,
+ TEXT_12, TEXT_14,
+ },
};
use data_encoding::HEXLOWER;
use ed25519_dalek::Verifier as _;
@@ -3378,4 +3617,193 @@ mod tests {
assert!(key.verify(msg.as_slice(), &sig).is_ok());
Ok(())
}
+ struct AuthExtOptions<'a> {
+ cred_protect: Option<u8>,
+ hmac_secret: Option<bool>,
+ min_pin_length: Option<u8>,
+ hmac_secret_mc: Option<&'a [u8]>,
+ }
+ fn generate_auth_extensions(opts: AuthExtOptions<'_>) -> Vec<u8> {
+ let map_len = u8::from(opts.cred_protect.is_some())
+ + u8::from(opts.hmac_secret.is_some())
+ + u8::from(opts.min_pin_length.is_some())
+ + u8::from(opts.hmac_secret_mc.is_some());
+ let header = match map_len {
+ 0 => return Vec::new(),
+ 1 => MAP_1,
+ 2 => MAP_2,
+ 3 => MAP_3,
+ 4 => MAP_4,
+ _ => unreachable!("bug"),
+ };
+ let mut cbor = Vec::with_capacity(128);
+ cbor.push(header);
+ if let Some(protect) = opts.cred_protect {
+ cbor.push(TEXT_11);
+ cbor.extend_from_slice(b"credProtect".as_slice());
+ if protect >= 24 {
+ cbor.push(24);
+ }
+ cbor.push(protect);
+ }
+ if let Some(hmac) = opts.hmac_secret {
+ cbor.push(TEXT_11);
+ cbor.extend_from_slice(b"hmac-secret".as_slice());
+ cbor.push(if hmac { SIMPLE_TRUE } else { SIMPLE_FALSE });
+ }
+ if let Some(pin) = opts.min_pin_length {
+ cbor.push(TEXT_12);
+ cbor.extend_from_slice(b"minPinLength".as_slice());
+ if pin >= 24 {
+ cbor.push(24);
+ }
+ cbor.push(pin);
+ }
+ if let Some(mc) = opts.hmac_secret_mc {
+ cbor.push(TEXT_14);
+ cbor.extend_from_slice(b"hmac-secret-mc".as_slice());
+ match mc.len() {
+ len @ ..=23 => {
+ cbor.push(BYTES | len as u8);
+ }
+ len @ 24..=255 => {
+ cbor.push(BYTES_INFO_24);
+ cbor.push(len as u8);
+ }
+ _ => panic!(
+ "AuthExtOptions does not allow hmac_secret_mc to have length greater than 255"
+ ),
+ }
+ cbor.extend_from_slice(mc);
+ }
+ cbor
+ }
+ #[test]
+ fn test_auth_ext() -> Result<(), AuthenticatorExtensionOutputErr> {
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: None,
+ hmac_secret: None,
+ min_pin_length: None,
+ hmac_secret_mc: None,
+ });
+ let CborSuccess { value, remaining } =
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?;
+ assert!(remaining.is_empty());
+ assert!(value.missing());
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: None,
+ hmac_secret: None,
+ min_pin_length: None,
+ hmac_secret_mc: Some([0; 48].as_slice()),
+ });
+ assert!(
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else(
+ |e| matches!(e, AuthenticatorExtensionOutputErr::Missing),
+ |_| false,
+ )
+ );
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: None,
+ hmac_secret: Some(true),
+ min_pin_length: None,
+ hmac_secret_mc: Some([0; 48].as_slice()),
+ });
+ let CborSuccess { value, remaining } =
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?;
+ assert!(remaining.is_empty());
+ assert!(
+ matches!(value.cred_protect, CredentialProtectionPolicy::None)
+ && matches!(value.hmac_secret, HmacSecret::One)
+ && value.min_pin_length.is_none()
+ );
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: None,
+ hmac_secret: Some(false),
+ min_pin_length: None,
+ hmac_secret_mc: Some([0; 48].as_slice()),
+ });
+ assert!(
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else(
+ |e| matches!(e, AuthenticatorExtensionOutputErr::Missing),
+ |_| false,
+ )
+ );
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: None,
+ hmac_secret: Some(true),
+ min_pin_length: None,
+ hmac_secret_mc: Some([0; 49].as_slice()),
+ });
+ assert!(
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else(
+ |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcValue),
+ |_| false,
+ )
+ );
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: None,
+ hmac_secret: Some(true),
+ min_pin_length: None,
+ hmac_secret_mc: Some([0; 23].as_slice()),
+ });
+ assert!(
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else(
+ |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcType),
+ |_| false,
+ )
+ );
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: Some(1),
+ hmac_secret: Some(true),
+ min_pin_length: Some(5),
+ hmac_secret_mc: Some([0; 48].as_slice()),
+ });
+ let CborSuccess { value, remaining } =
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?;
+ assert!(remaining.is_empty());
+ assert!(
+ matches!(
+ value.cred_protect,
+ CredentialProtectionPolicy::UserVerificationOptional
+ ) && matches!(value.hmac_secret, HmacSecret::One)
+ && value.min_pin_length.is_some_and(|pin| pin.value() == 5)
+ );
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: Some(0),
+ hmac_secret: None,
+ min_pin_length: None,
+ hmac_secret_mc: None,
+ });
+ assert!(
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else(
+ |e| matches!(e, AuthenticatorExtensionOutputErr::CredProtectValue),
+ |_| false,
+ )
+ );
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: None,
+ hmac_secret: None,
+ min_pin_length: Some(3),
+ hmac_secret_mc: None,
+ });
+ assert!(
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else(
+ |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue),
+ |_| false,
+ )
+ );
+ let opts = generate_auth_extensions(AuthExtOptions {
+ cred_protect: None,
+ hmac_secret: None,
+ min_pin_length: Some(64),
+ hmac_secret_mc: None,
+ });
+ assert!(
+ AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else(
+ |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue),
+ |_| false,
+ )
+ );
+ Ok(())
+ }
}
diff --git a/src/response/register/error.rs b/src/response/register/error.rs
@@ -7,9 +7,9 @@ use super::{
request::{
BackupReq, CredentialMediationRequirement, UserVerificationRequirement,
register::{
- AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, Extension,
- PublicKeyCredentialCreationOptions, RegistrationServerState,
- RegistrationVerificationOptions,
+ AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria,
+ CredentialCreationOptions, Extension, PublicKeyCredentialCreationOptions,
+ RegistrationServerState, RegistrationVerificationOptions,
},
},
},
@@ -178,6 +178,10 @@ pub enum AuthenticatorExtensionOutputErr {
HmacSecretValue,
/// `minPinLength` had an invalid value.
MinPinLengthValue,
+ /// `hmac-secret-mc` was not a byte string with additional info 24.
+ HmacSecretMcType,
+ /// `hmac-secret-mc` was not a byte string of length 48 or 80.
+ HmacSecretMcValue,
/// Fewer extensions existed than expected.
Missing,
}
@@ -190,6 +194,8 @@ impl Display for AuthenticatorExtensionOutputErr {
Self::CredProtectValue => "CBOR authenticator extension 'credProtect' had an invalid value",
Self::HmacSecretValue => "CBOR authenticator extension 'hmac-secret' had an invalid value",
Self::MinPinLengthValue => "CBOR authenticator extension 'minPinLength' had an invalid value",
+ Self::HmacSecretMcType => "CBOR authenticator extension 'hmac-secret-mc' was not a byte string with additional info 24",
+ Self::HmacSecretMcValue => "CBOR authenticator extension 'hmac-secret-mc' was not a byte string of length 48 or 80",
Self::Missing => "CBOR authenticator extensions had fewer extensions than expected",
})
}
@@ -562,7 +568,7 @@ pub enum RegCeremonyErr {
ChallengeMismatch,
/// The SHA-256 hash of [`PublicKeyCredentialCreationOptions::rp_id`] does not match [`AuthenticatorData::rp_id_hash`].
RpIdHashMismatch,
- /// [`Flag::user_present`] was `false` despite [`PublicKeyCredentialCreationOptions::mediation`]
+ /// [`Flag::user_present`] was `false` despite [`CredentialCreationOptions::mediation`]
/// being something other than [`CredentialMediationRequirement::Conditional`].
UserNotPresent,
/// [`AuthenticatorSelectionCriteria::user_verification`] was set to [`UserVerificationRequirement::Required`],
@@ -572,6 +578,8 @@ pub enum RegCeremonyErr {
BackupEligible,
/// [`Backup::NotEligible`] was sent back despite [`BackupReq::Eligible`].
BackupNotEligible,
+ /// [`Backup::Eligible`] was not sent back despite [`BackupReq::EligibleNotExists`].
+ BackupExists,
/// [`Backup::Exists`] was not sent back despite [`BackupReq::Exists`].
BackupDoesNotExist,
/// [`AuthenticatorAttachment`] was not sent back despite being required.
@@ -603,6 +611,7 @@ impl Display for RegCeremonyErr {
Self::TopOriginMismatch => CeremonyErr::<AttestationObjectErr>::TopOriginMismatch.fmt(f),
Self::BackupEligible => CeremonyErr::<AttestationObjectErr>::BackupEligible.fmt(f),
Self::BackupNotEligible => CeremonyErr::<AttestationObjectErr>::BackupNotEligible.fmt(f),
+ Self::BackupExists => CeremonyErr::<AttestationObjectErr>::BackupExists.fmt(f),
Self::BackupDoesNotExist => CeremonyErr::<AttestationObjectErr>::BackupDoesNotExist.fmt(f),
Self::ChallengeMismatch => CeremonyErr::<AttestationObjectErr>::ChallengeMismatch.fmt(f),
Self::RpIdHashMismatch => CeremonyErr::<AttestationObjectErr>::RpIdHashMismatch.fmt(f),