commit d35fd32bad2cec478105a246c0ce4dc443d3523e
parent 554b521f867d6cc1b00456b90dd59b7d92607fed
Author: Zack Newman <zack@philomathiclife.com>
Date: Mon, 11 Aug 2025 09:15:03 -0600
more trait impls. use base64url_nopad crate
Diffstat:
28 files changed, 4564 insertions(+), 758 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0"
name = "webauthn_rp"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/webauthn_rp/"
-rust-version = "1.88.0"
+rust-version = "1.89.0"
version = "0.4.0"
[lints.rust]
@@ -81,9 +81,11 @@ implicit_return = "allow"
min_ident_chars = "allow"
missing_trait_methods = "allow"
module_name_repetitions = "allow"
+option_option = "allow"
pub_use = "allow"
pub_with_shorthand = "allow"
question_mark_used = "allow"
+redundant_pub_crate = "allow"
ref_patterns = "allow"
return_and_then = "allow"
self_named_module_files = "allow"
@@ -96,20 +98,20 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
-data-encoding = { version = "2.9.0", default-features = false }
+base64url_nopad = { version = "0.1.0", default-features = false }
ed25519-dalek = { version = "2.2.0", default-features = false }
hashbrown = { version = "0.15.4", default-features = false }
p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] }
p384 = { version = "0.13.1", default-features = false, features = ["ecdsa"] }
precis-profiles = { version = "0.1.12", default-features = false }
-rand = { version = "0.9.1", default-features = false, features = ["thread_rng"] }
+rand = { version = "0.9.2", default-features = false, features = ["thread_rng"] }
rsa = { version = "0.9.8", default-features = false, features = ["sha2"] }
serde = { version = "1.0.219", default-features = false, features = ["alloc"], optional = true }
serde_json = { version = "1.0.141", default-features = false, features = ["alloc"], optional = true }
url = { version = "2.5.4", default-features = false }
[dev-dependencies]
-data-encoding = { version = "2.9.0", default-features = false, features = ["alloc"] }
+base64url_nopad = { version = "0.1.0", default-features = false, features = ["alloc"] }
ed25519-dalek = { version = "2.2.0", default-features = false, features = ["alloc", "pkcs8"] }
p256 = { version = "0.13.2", default-features = false, features = ["pem"] }
p384 = { version = "0.13.1", default-features = false, features = ["pkcs8"] }
@@ -130,7 +132,7 @@ custom = []
# Provide client (de)serialization based on JSON-motivated
# data structures.
-serde = ["data-encoding/alloc", "dep:serde"]
+serde = ["base64url_nopad/alloc", "dep:serde"]
# Provide "relaxed" JSON deserialization implementations.
serde_relaxed = ["serde", "dep:serde_json"]
diff --git a/src/lib.rs b/src/lib.rs
@@ -24,7 +24,7 @@
//! Registration, RegistrationServerState64,
//! hash::hash_set::FixedCapHashSet,
//! request::{
-//! AsciiDomainStatic, PublicKeyCredentialDescriptor, RpId,
+//! PublicKeyCredentialDescriptor, RpId,
//! auth::AuthenticationVerificationOptions,
//! register::{
//! Nickname, PublicKeyCredentialUserEntity64, RegistrationVerificationOptions,
@@ -42,7 +42,7 @@
//! # #[cfg(feature = "serde_relaxed")]
//! use serde_json::Error as JsonErr;
//! /// The RP ID our application uses.
-//! const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+//! const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap();
//! /// The registration verification options.
//! const REG_OPTS: &RegistrationVerificationOptions::<'static, 'static, &'static str, &'static str> = &RegistrationVerificationOptions::new();
//! /// The authentication verification options.
@@ -677,7 +677,7 @@ pub use crate::{
},
};
/// Error returned in [`RegCeremonyErr::Credential`] and [`AuthenticatedCredential::new`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CredentialErr {
/// Variant when [`CredentialProtectionPolicy::UserVerificationRequired`], but
/// [`DynamicState::user_verified`] is `false`.
@@ -1460,75 +1460,3 @@ impl Display for AggErr {
}
}
impl Error for AggErr {}
-/// Calculates the number of bytes needed to encode an input of length `n` bytes into base64url.
-///
-/// # Panics
-///
-/// `panics` iff `n > isize::MAX`.
-#[expect(
- clippy::arithmetic_side_effects,
- clippy::as_conversions,
- clippy::integer_division,
- clippy::integer_division_remainder_used,
- reason = "proof and comment justifies their correctness"
-)]
-const fn base64url_nopad_len(n: usize) -> usize {
- // 256^n is the number of distinct values of the input. Let the base64 encoding in a URL safe
- // way without padding of the input be O. There are 64 possible values each byte in O can be; thus we must find
- // the minimum nonnegative integer m such that:
- // 64^m = (2^6)^m = 2^(6m) >= 256^n = (2^8)^n = 2^(8n)
- // <==>
- // lg(2^(6m)) = 6m >= lg(2^(8n)) = 8n lg is defined on all positive reals which 2^(6m) and 2^(8n) are
- // <==>
- // m >= 8n/6 = 4n/3
- // Clearly that corresponds to m = ⌈4n/3⌉.
- // We claim ⌈4n/3⌉ = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉.
- // Proof:
- // There are three partitions for n:
- // (1) 3i = n ≡ 0 (mod 3) for some integer i
- // <==>
- // ⌈4n/3⌉ = ⌈4(3i)/3⌉ = ⌈4i⌉ = 4i = 4⌊i⌋ = 4⌊3i/3⌋ = 4⌊n/3⌋ + 0 = 4⌊n/3⌋ + ⌈4(0)/3⌉ = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉
- // (2) 3i + 1 = n ≡ 1 (mod 3) for some integer i
- // <==>
- // ⌈4n/3⌉ = ⌈4(3i + 1)/3⌉ = ⌈4i + 4/3⌉ = 4i + ⌈4/3⌉ = 4i + 2 = 4⌊i + 1/3⌋ + ⌈4(1)/3⌉
- // = 4⌊(3i + 1)/3⌋ + ⌈4((3i + 1) mod 3)/3⌉
- // = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉
- // (3) 3i + 2 = n ≡ 2 (mod 3) for some integer i
- // <==>
- // ⌈4n/3⌉ = ⌈4(3i + 2)/3⌉ = ⌈4i + 8/3⌉ = 4i + ⌈8/3⌉ = 4i + 3 = 4⌊i + 2/3⌋ + ⌈4(2)/3⌉
- // = 4⌊(3i + 2)/3⌋ + ⌈4((3i + 2) mod 3)/3⌉
- // = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉
- // QED
- // Proof of no overflow:
- // usize >= u16::MAX
- // usize = 2^i - 1 where i is any integer >= 16 (due to above)
- // isize = 2^(i-1) - 1
- // Suppose n <= isize::MAX, then:
- // ⌈4n/3⌉ <= ⌈4*isize::MAX/3⌉ = ⌈4*(2^(i-1)-1)/3⌉
- // = ⌈(2^(i+1)-4)/3⌉
- // < ⌈(2^(i+1)-4)/2⌉
- // = ⌈2^i-2⌉ = 2^i-2 < 2^i - 1 = usize::MAX
- // thus ignoring intermediate calcuations, overflow is not possible.
- // QED
- // Naively implementing ⌈4n/3⌉ as (4 * n).div_ceil(3) can cause overflow due to `4 * n`; thus
- // we implement the equivalent equation 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉ instead:
- // `(4 * (n / 3)) + (4 * (n % 3)).div_ceil(3)` since none of the intermediate calculations suffer
- // from overflow.
-
- // `isize::MAX > 0 = usize::MIN`; thus this conversion is lossless.
- assert!(
- n <= isize::MAX as usize,
- "base64url_nopad_len must be passed a `const` that is no larger than isize::MAX"
- );
- // n = 3quot + rem.
- let (quot, rem) = (n / 3, n % 3);
- // quot << 2u8 <= m < usize::MAX; thus the left operand of + is fine.
- // rem <= 2
- // <==>
- // 4rem <= 8 < usize::MAX; thus rem << 2u8 is fine.
- // <==>
- // ⌈4rem/3⌉ <= 4rem, so the right operand of + is fine.
- // The sum is fine since
- // m = ⌈4n/3⌉ = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉ = (quot << 2u8) + (rem << 2u8).div_ceil(3), and m < usize::MAX.
- (quot << 2u8) + (rem << 2u8).div_ceil(3)
-}
diff --git a/src/request.rs b/src/request.rs
@@ -14,7 +14,9 @@ use super::{
};
use crate::{
request::{
- error::{AsciiDomainErr, DomainOriginParseErr, PortParseErr, SchemeParseErr, UrlErr},
+ error::{
+ AsciiDomainErr, DomainOriginParseErr, PortParseErr, RpIdErr, SchemeParseErr, UrlErr,
+ },
register::RegistrationVerificationOptions,
},
response::{
@@ -46,12 +48,12 @@ use url::Url as Uri;
/// # request::{
/// # auth::{AllowedCredentials, DiscoverableCredentialRequestOptions, NonDiscoverableCredentialRequestOptions},
/// # register::UserHandle64,
-/// # AsciiDomainStatic, Credentials, PublicKeyCredentialDescriptor, RpId,
+/// # Credentials, PublicKeyCredentialDescriptor, RpId,
/// # },
/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
/// # AggErr,
/// # };
-/// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+/// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap();
/// let mut ceremonies = FixedCapHashSet::new(128);
/// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?;
/// assert!(
@@ -110,12 +112,12 @@ pub mod error;
/// # register::{
/// # CredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64,
/// # },
-/// # AsciiDomainStatic, PublicKeyCredentialDescriptor, RpId
+/// # PublicKeyCredentialDescriptor, RpId
/// # },
/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
/// # AggErr,
/// # };
-/// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+/// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap();
/// # #[cfg(feature = "custom")]
/// let mut ceremonies = FixedCapHashSet::new(128);
/// # #[cfg(feature = "custom")]
@@ -201,7 +203,7 @@ pub(super) mod ser_server_state;
pub struct Challenge(u128);
impl Challenge {
/// The number of bytes a `Challenge` takes to encode in base64url.
- pub(super) const BASE64_LEN: usize = super::base64url_nopad_len(16);
+ pub(super) const BASE64_LEN: usize = base64url_nopad::encode_len(16);
/// Generates a random `Challenge`.
///
/// # Examples
@@ -674,6 +676,16 @@ pub enum RpId {
Url(Url),
}
impl RpId {
+ /// Returns `Some` containing an [`AsciiDomainStatic`] iff [`AsciiDomainStatic::new`] does.
+ #[inline]
+ #[must_use]
+ pub const fn from_static_domain(domain: &'static str) -> Option<Self> {
+ if let Some(dom) = AsciiDomainStatic::new(domain) {
+ Some(Self::StaticDomain(dom))
+ } else {
+ None
+ }
+ }
/// Validates `hash` is the same as the SHA-256 hash of `self`.
fn validate_rp_id_hash<E>(&self, hash: &[u8]) -> Result<(), CeremonyErr<E>> {
if hash == Sha256::digest(self.as_ref()).as_slice() {
@@ -743,6 +755,22 @@ impl From<Url> for RpId {
Self::Url(value)
}
}
+impl TryFrom<String> for RpId {
+ type Error = RpIdErr;
+ /// Returns `Ok` iff `value` is a valid [`Url`] or [`AsciiDomain`].
+ ///
+ /// Note when `value` is a valid `Url` and `AsciiDomain`, it will be treated as a `Url`.
+ #[inline]
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ Url::from_str(value.as_str())
+ .map(Self::Url)
+ .or_else(|_err| {
+ AsciiDomain::try_from(value)
+ .map(Self::Domain)
+ .map_err(|_e| RpIdErr)
+ })
+ }
+}
/// A URI scheme. This can be used to make
/// [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) more convenient.
#[derive(Clone, Copy, Debug, Default)]
@@ -1117,7 +1145,7 @@ pub struct PublicKeyCredentialDescriptor<T> {
pub transports: AuthTransports,
}
/// [`UserVerificationRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UserVerificationRequirement {
/// [`required`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-required).
Required,
@@ -1130,18 +1158,8 @@ pub enum UserVerificationRequirement {
/// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred).
Preferred,
}
-#[cfg(test)]
-impl PartialEq for UserVerificationRequirement {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::Required => matches!(other, Self::Required),
- Self::Discouraged => matches!(other, Self::Discouraged),
- Self::Preferred => matches!(other, Self::Preferred),
- }
- }
-}
/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint).
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Hint {
/// No hints.
#[default]
@@ -1177,69 +1195,24 @@ pub enum Hint {
/// [`Self::HybridClientDevice`] and [`Self::SecurityKey`].
HybridClientDeviceSecurityKey,
}
-#[cfg(test)]
-impl PartialEq for Hint {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::None => matches!(other, Self::None),
- Self::SecurityKey => matches!(other, Self::SecurityKey),
- Self::ClientDevice => matches!(other, Self::ClientDevice),
- Self::Hybrid => matches!(other, Self::Hybrid),
- Self::SecurityKeyClientDevice => matches!(other, Self::SecurityKeyClientDevice),
- Self::ClientDeviceSecurityKey => matches!(other, Self::ClientDeviceSecurityKey),
- Self::SecurityKeyHybrid => matches!(other, Self::SecurityKeyHybrid),
- Self::HybridSecurityKey => matches!(other, Self::HybridSecurityKey),
- Self::ClientDeviceHybrid => matches!(other, Self::ClientDeviceHybrid),
- Self::HybridClientDevice => matches!(other, Self::HybridClientDevice),
- Self::SecurityKeyClientDeviceHybrid => {
- matches!(other, Self::SecurityKeyClientDeviceHybrid)
- }
- Self::SecurityKeyHybridClientDevice => {
- matches!(other, Self::SecurityKeyHybridClientDevice)
- }
- Self::ClientDeviceSecurityKeyHybrid => {
- matches!(other, Self::ClientDeviceSecurityKeyHybrid)
- }
- Self::ClientDeviceHybridSecurityKey => {
- matches!(other, Self::ClientDeviceHybridSecurityKey)
- }
- Self::HybridSecurityKeyClientDevice => {
- matches!(other, Self::HybridSecurityKeyClientDevice)
- }
- Self::HybridClientDeviceSecurityKey => {
- matches!(other, Self::HybridClientDeviceSecurityKey)
- }
- }
- }
-}
/// Controls if the response to a requested extension is required to be sent back.
///
/// Note when requiring an extension, the extension must not only be sent back but also
-/// contain at least one expected field
-/// (e.g., [`ClientExtensionsOutputs::cred_props`] must be
+/// contain at least one expected field (e.g., [`ClientExtensionsOutputs::cred_props`] must be
/// `Some(CredentialPropertiesOutput { rk: Some(_) })`.
///
/// If one wants to additionally control the values of an extension, use [`ExtensionInfo`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExtensionReq {
/// The response to a requested extension is required to be sent back.
Require,
/// The response to a requested extension is allowed, but not required, to be sent back.
Allow,
}
-#[cfg(test)]
-impl PartialEq for ExtensionReq {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::Require => matches!(other, Self::Require),
- Self::Allow => matches!(other, Self::Allow),
- }
- }
-}
/// Dictates how an extension should be processed.
///
/// If one wants to only control if the extension should be returned, use [`ExtensionReq`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExtensionInfo {
/// Require the associated extension and enforce its value.
RequireEnforceValue,
@@ -1250,17 +1223,6 @@ pub enum ExtensionInfo {
/// Allow the associated extension to exist but don't enforce its value.
AllowDontEnforceValue,
}
-#[cfg(test)]
-impl PartialEq for ExtensionInfo {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::RequireEnforceValue => matches!(other, Self::RequireEnforceValue),
- Self::RequireDontEnforceValue => matches!(other, Self::RequireDontEnforceValue),
- Self::AllowEnforceValue => matches!(other, Self::AllowEnforceValue),
- Self::AllowDontEnforceValue => matches!(other, Self::AllowDontEnforceValue),
- }
- }
-}
impl Display for ExtensionInfo {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
@@ -1279,7 +1241,7 @@ impl Display for ExtensionInfo {
/// [`optional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-optional)
/// is just an alias for [`Self::Required`].
#[expect(clippy::doc_markdown, reason = "false positive")]
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CredentialMediationRequirement {
/// [`required`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-required).
///
@@ -1289,21 +1251,12 @@ pub enum CredentialMediationRequirement {
/// [`conditional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-conditional).
///
/// Note that when registering a new credential with [`CredentialCreationOptions::mediation`] set to
- /// `Self::Conditional`, [`UserVerificationRequirement::Discouraged`] MUST be used unless user verification
+ /// `Self::Conditional`, [`UserVerificationRequirement::Required`] MUST NOT be used unless user verification
/// can be explicitly performed during the ceremony.
Conditional,
}
-#[cfg(test)]
-impl PartialEq for CredentialMediationRequirement {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::Required => matches!(other, Self::Required),
- Self::Conditional => matches!(other, Self::Conditional),
- }
- }
-}
/// Backup requirements for the credential.
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BackupReq {
/// No requirements (i.e., any [`Backup`] is allowed).
#[default]
@@ -1723,20 +1676,35 @@ pub trait TimedCeremony {
fn expiration(&self) -> SystemTime;
}
/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
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
+impl<'first, 'second> PrfInput<'first, 'second> {
+ /// Returns a `PrfInput` with [`Self::first`] set to `first` and [`Self::second`] set to `None`.
+ #[expect(single_use_lifetimes, reason = "false positive")]
+ #[inline]
+ #[must_use]
+ pub const fn with_first<'a: 'first>(first: &'a [u8]) -> Self {
+ Self {
+ first,
+ second: None,
+ }
+ }
+ /// Same as [`Self::with_first`] except [`Self::second`] is set to `Some` containing `second`.
+ #[expect(single_use_lifetimes, reason = "false positive")]
+ #[inline]
+ #[must_use]
+ pub const fn with_two<'a: 'first, 'b: 'second>(first: &'a [u8], second: &'b [u8]) -> Self {
+ Self {
+ first,
+ second: Some(second),
+ }
}
}
-
/// The number of milliseconds in 5 minutes.
///
/// This is the recommended default timeout duration for ceremonies
@@ -1844,7 +1812,7 @@ mod tests {
assert!(AsciiDomainStatic::new("λ.com").is_none());
}
#[cfg(feature = "custom")]
- const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+ const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap();
#[test]
#[cfg(feature = "custom")]
fn eddsa_reg() -> Result<(), AggErr> {
@@ -1865,11 +1833,7 @@ mod tests {
false,
ExtensionInfo::RequireEnforceValue,
),
- min_pin_length: Some((
- FourToSixtyThree::new(10)
- .unwrap_or_else(|| unreachable!("bug in FourToSixyThree::new")),
- ExtensionInfo::RequireEnforceValue,
- )),
+ min_pin_length: Some((FourToSixtyThree::Ten, ExtensionInfo::RequireEnforceValue)),
prf: Some((
PrfInput {
first: [0].as_slice(),
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -45,7 +45,7 @@ pub mod error;
/// Contains functionality to serialize data to a client.
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
#[cfg(feature = "serde")]
-mod ser;
+pub mod ser;
/// Contains functionality to (de)serialize [`DiscoverableAuthenticationServerState`] and
/// [`NonDiscoverableAuthenticationServerState`] to a data store.
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
@@ -55,7 +55,7 @@ pub mod ser_server_state;
///
/// Note that if the previous signature counter is positive and the new counter is not strictly greater, then the
/// authenticator is likely a clone (i.e., there are at least two copies of the private key).
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SignatureCounterEnforcement {
/// Fail the authentication ceremony if the counter is less than or equal to the previous value when the
/// previous value is positive.
@@ -102,7 +102,7 @@ pub struct PrfInputOwned {
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)]
+#[derive(Clone, Copy, Debug)]
pub struct Extension<'prf_first, 'prf_second> {
/// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension).
///
@@ -115,6 +115,33 @@ pub struct Extension<'prf_first, 'prf_second> {
/// extension since the data is encrypted and is part of the [`AuthenticatorData`].
pub prf: Option<(PrfInput<'prf_first, 'prf_second>, ExtensionReq)>,
}
+impl<'prf_first, 'prf_second> Extension<'prf_first, 'prf_second> {
+ /// Returns an `Extension` with [`Self::prf`] set to `None`.
+ #[inline]
+ #[must_use]
+ pub const fn none() -> Self {
+ Self { prf: None }
+ }
+ /// Returns an `Extension` with [`Self::prf`] set to `None`.
+ #[expect(single_use_lifetimes, reason = "false positive")]
+ #[inline]
+ #[must_use]
+ pub const fn with_prf<'a: 'prf_first, 'b: 'prf_second>(
+ input: PrfInput<'a, 'b>,
+ req: ExtensionReq,
+ ) -> Self {
+ Self {
+ prf: Some((input, req)),
+ }
+ }
+}
+impl Default for Extension<'_, '_> {
+ /// Same as [`Self::none`].
+ #[inline]
+ fn default() -> Self {
+ Self::none()
+ }
+}
/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client that
/// are credential-specific which among other things implies a non-discoverable request.
#[derive(Clone, Debug, Default)]
@@ -885,7 +912,7 @@ impl PartialEq for CredInfo {
/// Note when `DynamicState::authenticator_attachment` is [`AuthenticatorAttachment::None`], then it will
/// be updated regardless. Similarly when [`Authentication::authenticator_attachment`] is
/// `AuthenticatorAttachment::None`, it will never update `DynamicState::authenticator_attachment`.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthenticatorAttachmentEnforcement {
/// Fail the authentication ceremony if [`AuthenticatorAttachment`] is not the same.
///
diff --git a/src/request/auth/error.rs b/src/request/auth/error.rs
@@ -14,7 +14,7 @@ use std::time::{Instant, SystemTime};
///
/// This happens when [`PublicKeyCredentialRequestOptions::timeout`] could not be added to [`Instant::now`] or
/// [`SystemTime::now`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InvalidTimeout;
impl Display for InvalidTimeout {
#[inline]
@@ -24,7 +24,7 @@ impl Display for InvalidTimeout {
}
impl Error for InvalidTimeout {}
/// Error returned by [`NonDiscoverableCredentialRequestOptions::start_ceremony`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NonDiscoverableCredentialRequestOptionsErr {
/// Variant when [`NonDiscoverableCredentialRequestOptions::allow_credentials`] is
/// empty.
diff --git a/src/request/auth/ser.rs b/src/request/auth/ser.rs
@@ -1,10 +1,24 @@
+#[cfg(doc)]
+use super::ExtensionReq;
use super::{
- AllowedCredential, AllowedCredentials, Credentials as _, DiscoverableAuthenticationClientState,
- DiscoverableCredentialRequestOptions, Extension, NonDiscoverableAuthenticationClientState,
+ super::{
+ super::response::ser::Null,
+ ser::{DEFAULT_RP_ID, PrfHelper},
+ },
+ AllowedCredential, AllowedCredentials, Challenge, CredentialMediationRequirement,
+ Credentials as _, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions,
+ Extension, FIVE_MINUTES, Hint, NonDiscoverableAuthenticationClientState,
NonDiscoverableCredentialRequestOptions, PrfInput, PrfInputOwned,
- PublicKeyCredentialRequestOptions,
+ PublicKeyCredentialRequestOptions, RpId, UserVerificationRequirement,
+};
+use core::{
+ fmt::{self, Formatter},
+ num::NonZeroU32,
+};
+use serde::{
+ de::{Deserialize, Deserializer, Error, MapAccess, Visitor},
+ ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer},
};
-use serde::ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer};
impl Serialize for PrfInputOwned {
/// See [`PrfInput::serialize`]
#[inline]
@@ -203,6 +217,20 @@ impl Serialize for ExtensionHelper<'_, '_, '_> {
})
}
}
+/// `"challenge"`
+const CHALLENGE: &str = "challenge";
+/// `"timeout"`
+const TIMEOUT: &str = "timeout";
+/// `"rpId"`
+const RP_ID: &str = "rpId";
+/// `"allowCredentials"`
+const ALLOW_CREDENTIALS: &str = "allowCredentials";
+/// `"extensions"`
+const EXTENSIONS: &str = "extensions";
+/// `"hints"`
+const HINTS: &str = "hints";
+/// `"userVerification"`
+const USER_VERIFICATION: &str = "userVerification";
/// Helper type that peforms the serialization for both [`DiscoverableAuthenticationClientState`] and
/// [`NonDiscoverableAuthenticationClientState`] and
struct AuthenticationClientState<'rp_id, 'prf_first, 'prf_second, 'opt, 'cred>(
@@ -218,23 +246,23 @@ impl Serialize for AuthenticationClientState<'_, '_, '_, '_, '_> {
serializer
.serialize_struct("PublicKeyCredentialRequestOptions", 7)
.and_then(|mut ser| {
- ser.serialize_field("challenge", &self.0.challenge)
+ ser.serialize_field(CHALLENGE, &self.0.challenge)
.and_then(|()| {
- ser.serialize_field("timeout", &self.0.timeout)
+ ser.serialize_field(TIMEOUT, &self.0.timeout)
.and_then(|()| {
- ser.serialize_field("rpId", &self.0.rp_id).and_then(|()| {
- ser.serialize_field("allowCredentials", &self.1).and_then(
- |()| {
+ ser.serialize_field(RP_ID, &self.0.rp_id).and_then(|()| {
+ ser.serialize_field(ALLOW_CREDENTIALS, &self.1)
+ .and_then(|()| {
ser.serialize_field(
- "userVerification",
+ USER_VERIFICATION,
&self.0.user_verification,
)
.and_then(
|()| {
- ser.serialize_field("hints", &self.0.hints)
+ ser.serialize_field(HINTS, &self.0.hints)
.and_then(|()| {
ser.serialize_field(
- "extensions",
+ EXTENSIONS,
&ExtensionHelper {
extension: &self.0.extensions,
allow_credentials: self.1,
@@ -244,8 +272,7 @@ impl Serialize for AuthenticationClientState<'_, '_, '_, '_, '_> {
})
},
)
- },
- )
+ })
})
})
})
@@ -490,3 +517,714 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_, '_> {
self.0.serialize(serializer)
}
}
+/// Similar to [`Extension`] except [`PrfInputOwned`] is used.
+///
+/// This is primarily useful to assist [`ClientCredentialRequestOptions::deserialize`].
+#[derive(Debug, Default)]
+pub struct ExtensionOwned {
+ /// See [`Extension::prf`].
+ pub prf: Option<PrfInputOwned>,
+}
+impl<'a: 'prf_first + 'prf_second, 'prf_first, 'prf_second> From<&'a ExtensionOwned>
+ for Extension<'prf_first, 'prf_second>
+{
+ #[inline]
+ fn from(value: &'a ExtensionOwned) -> Self {
+ Self {
+ prf: value.prf.as_ref().map(|input| {
+ (
+ PrfInput {
+ first: input.first.as_slice(),
+ second: input.second.as_deref(),
+ },
+ input.ext_req,
+ )
+ }),
+ }
+ }
+}
+impl<'de> Deserialize<'de> for ExtensionOwned {
+ /// Deserializes a `struct` according to the following pseudo-schema:
+ ///
+ /// ```json
+ /// {
+ /// "prf": null | PRFJSON
+ /// }
+ /// // PRFJSON:
+ /// {
+ /// "eval": PRFInputs
+ /// }
+ /// // PRFInputs:
+ /// {
+ /// "first": <base64url-encoded string>,
+ /// "second": null | <base64url-encoded string>
+ /// }
+ /// ```
+ ///
+ /// where the only required fields are `"eval"` and `"first"`.
+ ///
+ /// All extensions are not required to have a response sent back; but _if_ a response is sent back, its value
+ /// will be enforced.
+ ///
+ /// Unknown or duplicate fields lead to an error.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{ExtensionReq, auth::ser::ExtensionOwned};
+ /// let ext = serde_json::from_str::<ExtensionOwned>(
+ /// r#"{"prf":{"eval":{"first":"","second":null}}}"#,
+ /// )?;
+ /// assert!(ext.prf.map_or(false, |prf| prf.first.is_empty()
+ /// && prf.second.is_none()
+ /// && matches!(prf.ext_req, ExtensionReq::Allow)));
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `ExtensionOwned`.
+ struct ExtensionOwnedVisitor;
+ impl<'d> Visitor<'d> for ExtensionOwnedVisitor {
+ type Value = ExtensionOwned;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("ExtensionOwned")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field for `ExtensionOwned`.
+ struct Field;
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{PRF}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v == PRF {
+ Ok(Field)
+ } else {
+ Err(E::unknown_field(v, FIELDS))
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ map.next_key::<Field>().and_then(|opt_key| {
+ opt_key
+ .map_or_else(
+ || Ok(None),
+ |_k| {
+ map.next_value::<Option<PrfHelper>>().and_then(|prf| {
+ map.next_key::<Field>().and_then(|opt_key2| {
+ opt_key2.map_or_else(
+ || Ok(prf.map(|val| val.0)),
+ |_k2| Err(Error::duplicate_field(PRF)),
+ )
+ })
+ })
+ },
+ )
+ .map(|prf| ExtensionOwned { prf })
+ })
+ }
+ }
+ /// `"prf"`.
+ const PRF: &str = "prf";
+ /// Fields for `ExtensionOwned`.
+ const FIELDS: &[&str; 1] = &[PRF];
+ deserializer.deserialize_struct("ExtensionOwned", FIELDS, ExtensionOwnedVisitor)
+ }
+}
+/// Similar to [`PublicKeyCredentialRequestOptions`] except the fields are based on owned data.
+///
+/// This is primarily useful to assist [`ClientCredentialRequestOptions::deserialize`],
+#[derive(Debug)]
+pub struct PublicKeyCredentialRequestOptionsOwned {
+ /// See [`PublicKeyCredentialRequestOptions::rp_id`].
+ pub rp_id: RpId,
+ /// See [`PublicKeyCredentialRequestOptions::timeout`].
+ pub timeout: NonZeroU32,
+ /// See [`PublicKeyCredentialRequestOptions::user_verification`].
+ pub user_verification: UserVerificationRequirement,
+ /// See [`PublicKeyCredentialRequestOptions::hints`].
+ pub hints: Hint,
+ /// See [`PublicKeyCredentialRequestOptions::extensions`].
+ pub extensions: ExtensionOwned,
+}
+impl PublicKeyCredentialRequestOptionsOwned {
+ /// Creates a `PublicKeyCredentialRequestOptions` based on the contained data and randomly-generated
+ /// [`Challenge`].
+ #[inline]
+ #[must_use]
+ pub fn into_options(&self) -> PublicKeyCredentialRequestOptions<'_, '_, '_> {
+ PublicKeyCredentialRequestOptions {
+ rp_id: &self.rp_id,
+ challenge: Challenge::new(),
+ timeout: self.timeout,
+ user_verification: self.user_verification,
+ hints: self.hints,
+ extensions: (&self.extensions).into(),
+ }
+ }
+}
+impl Default for PublicKeyCredentialRequestOptionsOwned {
+ #[inline]
+ fn default() -> Self {
+ Self {
+ rp_id: DEFAULT_RP_ID,
+ timeout: FIVE_MINUTES,
+ user_verification: UserVerificationRequirement::Preferred,
+ hints: Hint::default(),
+ extensions: ExtensionOwned::default(),
+ }
+ }
+}
+impl<'de> Deserialize<'de> for PublicKeyCredentialRequestOptionsOwned {
+ /// Deserializes a `struct` based on
+ /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
+ ///
+ /// Note that none of the fields are required, and all are allowed to be `null`.
+ ///
+ /// If [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-challenge)
+ /// exists, it must be `null`. If
+ /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-allowcredentials)
+ /// exists, it must be `null` or empty.
+ ///
+ /// If [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-timeout) exists,
+ /// it must be `null` or positive.
+ ///
+ /// In the event there is no RP ID defined, the value `"example.invalid"` will be used.
+ ///
+ /// For any field that does not exist or is `null`, the corresponding [`Default`] `impl` will be used. For
+ /// `user_verification`, [`UserVerificationRequirement::Preferred`] will be used. For `timeout`,
+ /// [`FIVE_MINUTES`] will be used.
+ ///
+ /// Unknown or duplicate fields lead to an error.
+ #[expect(clippy::too_many_lines, reason = "131 lines is fine")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `PublicKeyCredentialRequestOptionsOwned`.
+ struct PublicKeyCredentialRequestOptionsOwnedVisitor;
+ impl<'d> Visitor<'d> for PublicKeyCredentialRequestOptionsOwnedVisitor {
+ type Value = PublicKeyCredentialRequestOptionsOwned;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("PublicKeyCredentialRequestOptionsOwned")
+ }
+ #[expect(clippy::too_many_lines, reason = "104 lines is fine")]
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field for `PublicKeyCredentialRequestOptionsOwned`.
+ enum Field {
+ /// `rpId`.
+ RpId,
+ /// `userVerification`.
+ UserVerification,
+ /// `challenge`.
+ Challenge,
+ /// `timeout`.
+ Timeout,
+ /// `allowCredentials`.
+ AllowCredentials,
+ /// `hints`.
+ Hints,
+ /// `extensions`.
+ Extensions,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(
+ formatter,
+ "'{RP_ID}', '{USER_VERIFICATION}', '{CHALLENGE}', '{TIMEOUT}', '{ALLOW_CREDENTIALS}', '{HINTS}', or '{EXTENSIONS}'"
+ )
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ RP_ID => Ok(Field::RpId),
+ USER_VERIFICATION => Ok(Field::UserVerification),
+ CHALLENGE => Ok(Field::Challenge),
+ TIMEOUT => Ok(Field::Timeout),
+ ALLOW_CREDENTIALS => Ok(Field::AllowCredentials),
+ HINTS => Ok(Field::Hints),
+ EXTENSIONS => Ok(Field::Extensions),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut rp = None;
+ let mut user_veri = None;
+ let mut chall = None;
+ let mut time = None;
+ let mut allow = None;
+ let mut hint = None;
+ let mut ext = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::RpId => {
+ if rp.is_some() {
+ return Err(Error::duplicate_field(RP_ID));
+ }
+ rp = map.next_value::<Option<RpId>>().map(Some)?;
+ }
+ Field::UserVerification => {
+ if user_veri.is_some() {
+ return Err(Error::duplicate_field(USER_VERIFICATION));
+ }
+ user_veri = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::Challenge => {
+ if chall.is_some() {
+ return Err(Error::duplicate_field(CHALLENGE));
+ }
+ chall = map.next_value::<Null>().map(Some)?;
+ }
+ Field::Timeout => {
+ if time.is_some() {
+ return Err(Error::duplicate_field(TIMEOUT));
+ }
+ time = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::AllowCredentials => {
+ if allow.is_some() {
+ return Err(Error::duplicate_field(ALLOW_CREDENTIALS));
+ }
+ allow = map.next_value::<Option<[(); 0]>>().map(Some)?;
+ }
+ Field::Hints => {
+ if hint.is_some() {
+ return Err(Error::duplicate_field(HINTS));
+ }
+ hint = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::Extensions => {
+ if ext.is_some() {
+ return Err(Error::duplicate_field(EXTENSIONS));
+ }
+ ext = map.next_value::<Option<_>>().map(Some)?;
+ }
+ }
+ }
+ Ok(PublicKeyCredentialRequestOptionsOwned {
+ rp_id: rp.flatten().unwrap_or(DEFAULT_RP_ID),
+ user_verification: user_veri
+ .flatten()
+ .unwrap_or(UserVerificationRequirement::Preferred),
+ timeout: time.flatten().unwrap_or(FIVE_MINUTES),
+ extensions: ext.flatten().unwrap_or_default(),
+ hints: hint.flatten().unwrap_or_default(),
+ })
+ }
+ }
+ /// Fields for `PublicKeyCredentialRequestOptionsOwned`.
+ const FIELDS: &[&str; 7] = &[
+ RP_ID,
+ USER_VERIFICATION,
+ CHALLENGE,
+ TIMEOUT,
+ ALLOW_CREDENTIALS,
+ HINTS,
+ EXTENSIONS,
+ ];
+ deserializer.deserialize_struct(
+ "PublicKeyCredentialRequestOptionsOwned",
+ FIELDS,
+ PublicKeyCredentialRequestOptionsOwnedVisitor,
+ )
+ }
+}
+/// Deserializes client-supplied data to assist in the creation of [`DiscoverableCredentialRequestOptions`]
+/// and [`NonDiscoverableCredentialRequestOptions`].
+///
+/// It's common to tailor an authentication ceremony based on a user's environment. The options that should be
+/// used are then sent to the server. To facilitate this, [`Self::deserialize`] can be used to deserialize the data
+/// sent from the client. Upon successful deserialization, [`Self::into_discoverable_options`] and
+/// [`Self::into_non_discoverable_options`] can then be used to construct the
+/// appropriate [`DiscoverableCredentialRequestOptions`] and [`NonDiscoverableCredentialRequestOptions`]
+/// respectively.
+///
+/// Note one may want to change some of the [`Extension`] data since [`ExtensionReq::Allow`] is unconditionally
+/// used. Read [`ExtensionOwned::deserialize`] for more information.
+///
+/// Additionally, one may want to change the value of [`PublicKeyCredentialRequestOptions::rp_id`] since
+/// `"example.invalid"` is used in the event the RP ID was not supplied.
+#[derive(Debug)]
+pub struct ClientCredentialRequestOptions {
+ /// See [`DiscoverableCredentialRequestOptions::mediation`] and
+ /// [`NonDiscoverableCredentialRequestOptions::mediation`].
+ pub mediation: CredentialMediationRequirement,
+ /// See [`DiscoverableCredentialRequestOptions::public_key`] and
+ /// See [`NonDiscoverableCredentialRequestOptions::options`].
+ pub public_key: PublicKeyCredentialRequestOptionsOwned,
+}
+impl ClientCredentialRequestOptions {
+ /// Creates a `DiscoverableCredentialRequestOptions` based on the contained data where
+ /// [`DiscoverableCredentialRequestOptions::public_key`] is constructed via
+ /// [`PublicKeyCredentialRequestOptionsOwned::into_options`].
+ #[inline]
+ #[must_use]
+ pub fn into_discoverable_options(&self) -> DiscoverableCredentialRequestOptions<'_, '_, '_> {
+ DiscoverableCredentialRequestOptions {
+ mediation: self.mediation,
+ public_key: self.public_key.into_options(),
+ }
+ }
+ /// Creates a `NonDiscoverableCredentialRequestOptions` based on the contained data where
+ /// [`NonDiscoverableCredentialRequestOptions::options`] is constructed via
+ /// [`PublicKeyCredentialRequestOptionsOwned::into_options`].
+ #[inline]
+ #[must_use]
+ pub fn into_non_discoverable_options(
+ &self,
+ allow_credentials: AllowedCredentials,
+ ) -> NonDiscoverableCredentialRequestOptions<'_, '_, '_> {
+ NonDiscoverableCredentialRequestOptions {
+ mediation: self.mediation,
+ options: self.public_key.into_options(),
+ allow_credentials,
+ }
+ }
+}
+impl Default for ClientCredentialRequestOptions {
+ #[inline]
+ fn default() -> Self {
+ Self {
+ mediation: CredentialMediationRequirement::default(),
+ public_key: PublicKeyCredentialRequestOptionsOwned::default(),
+ }
+ }
+}
+impl<'de> Deserialize<'de> for ClientCredentialRequestOptions {
+ /// Deserializes a `struct` according to the following pseudo-schema:
+ ///
+ /// ```json
+ /// {
+ /// "mediation": null | "required" | "conditional",
+ /// "publicKey": null | <PublicKeyCredentialRequestOptionsOwned>
+ /// }
+ /// ```
+ ///
+ /// where none of the fields are required and `"publicKey"` is deserialized according to
+ /// [`PublicKeyCredentialRequestOptionsOwned::deserialize`]. If any field is missing or is `null`, then
+ /// the corresponding [`Default`] `impl` will be used.
+ ///
+ /// Unknown or duplicate fields lead to an error.
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `ClientCredentialRequestOptions`.
+ struct ClientCredentialRequestOptionsVisitor;
+ impl<'d> Visitor<'d> for ClientCredentialRequestOptionsVisitor {
+ type Value = ClientCredentialRequestOptions;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("ClientCredentialRequestOptions")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field in `ClientCredentialRequestOptions`.
+ enum Field {
+ /// `mediation`.
+ Mediation,
+ /// `publicKey`
+ PublicKey,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{MEDIATION}' or '{PUBLIC_KEY}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ MEDIATION => Ok(Field::Mediation),
+ PUBLIC_KEY => Ok(Field::PublicKey),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut med = None;
+ let mut key = None;
+ while let Some(k) = map.next_key()? {
+ match k {
+ Field::Mediation => {
+ if med.is_some() {
+ return Err(Error::duplicate_field(MEDIATION));
+ }
+ med = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::PublicKey => {
+ if key.is_some() {
+ return Err(Error::duplicate_field(PUBLIC_KEY));
+ }
+ key = map.next_value::<Option<_>>().map(Some)?;
+ }
+ }
+ }
+ Ok(ClientCredentialRequestOptions {
+ mediation: med.flatten().unwrap_or_default(),
+ public_key: key.flatten().unwrap_or_default(),
+ })
+ }
+ }
+ /// Fields for `ClientCredentialRequestOptions`.
+ const FIELDS: &[&str; 2] = &[MEDIATION, PUBLIC_KEY];
+ deserializer.deserialize_struct(
+ "ClientCredentialRequestOptions",
+ FIELDS,
+ ClientCredentialRequestOptionsVisitor,
+ )
+ }
+}
+#[cfg(test)]
+mod test {
+ use super::{
+ super::ExtensionReq, ClientCredentialRequestOptions, CredentialMediationRequirement,
+ DEFAULT_RP_ID, ExtensionOwned, FIVE_MINUTES, Hint, NonZeroU32,
+ PublicKeyCredentialRequestOptionsOwned, UserVerificationRequirement,
+ };
+ use serde_json::Error;
+ #[test]
+ fn client_options() -> Result<(), Error> {
+ let mut err =
+ serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"bob":true}"#).unwrap_err();
+ assert_eq!(
+ err.to_string()[..56],
+ *"unknown field `bob`, expected `mediation` or `publicKey`"
+ );
+ err = serde_json::from_str::<ClientCredentialRequestOptions>(
+ r#"{"mediation":"required","mediation":"required"}"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..27], *"duplicate field `mediation`");
+ let mut options = serde_json::from_str::<ClientCredentialRequestOptions>(r#"{}"#)?;
+ assert!(matches!(
+ options.mediation,
+ CredentialMediationRequirement::Required
+ ));
+ assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(options.public_key.timeout, FIVE_MINUTES);
+ assert!(matches!(
+ options.public_key.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(matches!(options.public_key.hints, Hint::None));
+ assert!(options.public_key.extensions.prf.is_none());
+ options = serde_json::from_str::<ClientCredentialRequestOptions>(
+ r#"{"mediation":null,"publicKey":null}"#,
+ )?;
+ assert!(matches!(
+ options.mediation,
+ CredentialMediationRequirement::Required
+ ));
+ assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(options.public_key.timeout, FIVE_MINUTES);
+ assert!(matches!(
+ options.public_key.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(matches!(options.public_key.hints, Hint::None));
+ assert!(options.public_key.extensions.prf.is_none());
+ options = serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"publicKey":{}}"#)?;
+ assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(options.public_key.timeout, FIVE_MINUTES);
+ assert!(matches!(
+ options.public_key.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(matches!(options.public_key.hints, Hint::None));
+ assert!(options.public_key.extensions.prf.is_none());
+ options = serde_json::from_str::<ClientCredentialRequestOptions>(
+ r#"{"mediation":"conditional","publicKey":{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}}"#,
+ )?;
+ assert!(matches!(
+ options.mediation,
+ CredentialMediationRequirement::Conditional
+ ));
+ assert_eq!(options.public_key.rp_id.as_ref(), "example.com");
+ assert_eq!(options.public_key.timeout, FIVE_MINUTES);
+ assert!(matches!(
+ options.public_key.user_verification,
+ UserVerificationRequirement::Required
+ ));
+ assert!(
+ options
+ .public_key
+ .extensions
+ .prf
+ .map_or(false, |prf| prf.first.is_empty()
+ && prf.second.is_some_and(|p| p.is_empty())
+ && matches!(prf.ext_req, ExtensionReq::Allow))
+ );
+ Ok(())
+ }
+ #[test]
+ fn key_options() -> Result<(), Error> {
+ let mut err =
+ serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{"bob":true}"#)
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..130],
+ *"unknown field `bob`, expected one of `rpId`, `userVerification`, `challenge`, `timeout`, `allowCredentials`, `hints`, `extensions`"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(
+ r#"{"rpId":"example.com","rpId":"example.com"}"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..22], *"duplicate field `rpId`");
+ err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(
+ r#"{"challenge":"AAAAAAAAAAAAAAAAAAAAAA"}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..41],
+ *"invalid type: Option value, expected null"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(
+ r#"{"allowCredentials":[{"type":"public-key","transports":["usb"],"id":"AAAAAAAAAAAAAAAAAAAAAA"}]}"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..19], *"trailing characters");
+ err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{"timeout":0}"#)
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..50],
+ *"invalid value: integer `0`, expected a nonzero u32"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(
+ r#"{"timeout":4294967296}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..59],
+ *"invalid value: integer `4294967296`, expected a nonzero u32"
+ );
+ let mut key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{}"#)?;
+ assert_eq!(key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(key.timeout, FIVE_MINUTES);
+ assert!(matches!(
+ key.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(key.extensions.prf.is_none());
+ assert!(matches!(key.hints, Hint::None));
+ key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(
+ r#"{"rpId":null,"timeout":null,"allowCredentials":null,"userVerification":null,"extensions":null,"hints":null,"challenge":null}"#,
+ )?;
+ assert_eq!(key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(key.timeout, FIVE_MINUTES);
+ assert!(matches!(
+ key.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(key.extensions.prf.is_none());
+ assert!(matches!(key.hints, Hint::None));
+ key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(
+ r#"{"allowCredentials":[],"extensions":{},"hints":[]}"#,
+ )?;
+ assert!(matches!(
+ key.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(matches!(key.hints, Hint::None));
+ assert!(key.extensions.prf.is_none());
+ key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(
+ r#"{"extensions":{"prf":null}}"#,
+ )?;
+ assert!(key.extensions.prf.is_none());
+ key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(
+ r#"{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}"#,
+ )?;
+ assert_eq!(key.rp_id.as_ref(), "example.com");
+ assert_eq!(key.timeout, FIVE_MINUTES);
+ assert!(matches!(
+ key.user_verification,
+ UserVerificationRequirement::Required
+ ));
+ assert!(matches!(key.hints, Hint::SecurityKey));
+ assert!(key.extensions.prf.map_or(false, |prf| prf.first.is_empty()
+ && prf.second.is_some_and(|p| p.is_empty())
+ && matches!(prf.ext_req, ExtensionReq::Allow)));
+ key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(
+ r#"{"timeout":4294967295}"#,
+ )?;
+ assert_eq!(key.timeout, NonZeroU32::MAX);
+ Ok(())
+ }
+ #[test]
+ fn extension() -> Result<(), Error> {
+ let mut err = serde_json::from_str::<ExtensionOwned>(r#"{"bob":true}"#).unwrap_err();
+ assert_eq!(
+ err.to_string()[..35],
+ *"unknown field `bob`, expected `prf`"
+ );
+ err = serde_json::from_str::<ExtensionOwned>(
+ r#"{"prf":{"eval":{"first":"","second":""}},"prf":{"eval":{"first":"","second":""}}}"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..21], *"duplicate field `prf`");
+ err = serde_json::from_str::<ExtensionOwned>(r#"{"prf":{"eval":{"first":null}}}"#)
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..51],
+ *"invalid type: null, expected base64url-encoded data"
+ );
+ let mut ext =
+ serde_json::from_str::<ExtensionOwned>(r#"{"prf":{"eval":{"first":"","second":""}}}"#)?;
+ assert!(ext.prf.map_or(false, |prf| prf.first.is_empty()
+ && prf.second.is_some_and(|v| v.is_empty())
+ && matches!(prf.ext_req, ExtensionReq::Allow)));
+ ext = serde_json::from_str::<ExtensionOwned>(r#"{"prf":null}"#)?;
+ assert!(ext.prf.is_none());
+ ext = serde_json::from_str::<ExtensionOwned>(r#"{}"#)?;
+ assert!(ext.prf.is_none());
+ Ok(())
+ }
+}
diff --git a/src/request/auth/ser_server_state.rs b/src/request/auth/ser_server_state.rs
@@ -241,7 +241,7 @@ impl<'a> DecodeBuffer<'a> for AuthenticationServerState {
}
}
/// Error returned from [`DiscoverableAuthenticationServerState::decode`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeDiscoverableAuthenticationServerStateErr {
/// Variant returned when there was trailing data after decoding a [`DiscoverableAuthenticationServerState`].
TrailingData,
@@ -277,7 +277,7 @@ impl Decode for DiscoverableAuthenticationServerState {
}
}
/// Error returned from [`NonDiscoverableAuthenticationServerState::decode`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeNonDiscoverableAuthenticationServerStateErr {
/// Variant returned when there was trailing data after decoding a [`NonDiscoverableAuthenticationServerState`].
TrailingData,
diff --git a/src/request/error.rs b/src/request/error.rs
@@ -8,7 +8,7 @@ use core::{
num::ParseIntError,
};
/// Error returned by [`AsciiDomain::try_from`] when the `Vec` is not a valid ASCII domain.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AsciiDomainErr {
/// Variant returned when the domain is empty.
Empty,
@@ -39,7 +39,7 @@ impl Display for AsciiDomainErr {
impl Error for AsciiDomainErr {}
/// Error returned by [`Url::from_str`] when the `str` passed to the
/// [URL serializer](https://url.spec.whatwg.org/#concept-url-serializer) leads to a failure.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UrlErr;
impl Display for UrlErr {
#[inline]
@@ -48,8 +48,18 @@ impl Display for UrlErr {
}
}
impl Error for UrlErr {}
+/// Error returned by [`RpId::try_from`] when the `String` is not a valid [`AsciiDomain`] nor [`Url`].
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct RpIdErr;
+impl Display for RpIdErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("RpId is invalid")
+ }
+}
+impl Error for RpIdErr {}
/// Error returned by [`Scheme::try_from`] when the passed [`str`] is empty.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SchemeParseErr;
impl Display for SchemeParseErr {
#[inline]
@@ -60,7 +70,7 @@ impl Display for SchemeParseErr {
impl Error for SchemeParseErr {}
/// Error returned by [`Port::from_str`] when the passed [`str`] is not a valid unsigned 16-bit integer in
/// decimal form without leading 0s.
-#[derive(Debug)]
+#[derive(Debug, Eq, PartialEq)]
pub enum PortParseErr {
/// Variant returned iff [`u16::from_str`] does.
ParseInt(ParseIntError),
@@ -80,7 +90,7 @@ impl Display for PortParseErr {
}
impl Error for PortParseErr {}
/// Error returned by [`DomainOrigin::try_from`].
-#[derive(Debug)]
+#[derive(Debug, Eq, PartialEq)]
pub enum DomainOriginParseErr {
/// Variant returned when there is an error parsing the scheme.
Scheme(SchemeParseErr),
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -31,6 +31,7 @@ use core::{
cmp::Ordering,
fmt::{self, Display, Formatter},
hash::{Hash, Hasher},
+ mem,
num::{NonZeroU32, NonZeroU64},
time::Duration,
};
@@ -49,17 +50,17 @@ pub mod bin;
mod custom;
/// Contains error types.
pub mod error;
-/// Contains functionality to serialize data to a client.
+/// Contains functionality to (de)serialize data to a client.
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
#[cfg(feature = "serde")]
-pub(crate) mod ser;
+pub mod ser;
/// Contains functionality to (de)serialize [`RegistrationServerState`] to a data store.
#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
#[cfg(feature = "serializable_server_state")]
pub mod ser_server_state;
/// Used by [`Extension::cred_protect`] to enforce the [`CredentialProtectionPolicy`] sent by the client via
/// [`Registration`].
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CredProtect {
/// No `credProtect` request.
#[default]
@@ -169,23 +170,6 @@ impl CredProtect {
}
}
}
-#[cfg(test)]
-impl PartialEq for CredProtect {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::None => matches!(other, Self::None),
- Self::UserVerificationOptional(enforce, info) => {
- matches!(*other, Self::UserVerificationOptional(enforce2, info2) if enforce == enforce2 && info == info2)
- }
- Self::UserVerificationOptionalWithCredentialIdList(enforce, info) => {
- matches!(*other, Self::UserVerificationOptionalWithCredentialIdList(enforce2, info2) if enforce == enforce2 && info == info2)
- }
- Self::UserVerificationRequired(enforce, info) => {
- matches!(*other, Self::UserVerificationRequired(enforce2, info2) if enforce == enforce2 && info == info2)
- }
- }
- }
-}
impl Display for CredProtect {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
@@ -428,6 +412,12 @@ impl<'a> Username<'a> {
pub fn with_max_len<'b: 'a>(value: Cow<'b, str>) -> Result<Self, UsernameErr> {
Self::try_from(value)
}
+ /// Returns `Self` containing `"blank"`.
+ #[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
+ fn blank() -> Self {
+ Self::try_from("blank")
+ .unwrap_or_else(|_e| unreachable!("'blank' is no longer a valid Username"))
+ }
}
impl AsRef<str> for Username<'_> {
#[inline]
@@ -593,7 +583,7 @@ impl PartialEq<CoseAlgorithmIdentifier> for &CoseAlgorithmIdentifier {
}
}
/// Non-empty ordered set of [`CoseAlgorithmIdentifier`]s.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CoseAlgorithmIdentifiers(u8);
impl CoseAlgorithmIdentifiers {
/// Contains all [`CoseAlgorithmIdentifier`]s.
@@ -646,61 +636,177 @@ impl Default for CoseAlgorithmIdentifiers {
Self::ALL
}
}
-#[cfg(test)]
-impl PartialEq for CoseAlgorithmIdentifiers {
- fn eq(&self, other: &Self) -> bool {
- self.0 == other.0
- }
-}
-/// `newtype` of `u8` bound inclusively between [`Self::MIN`] and [`Self::MAX`].
+/// Four to sixty-three inclusively.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
-pub struct FourToSixtyThree(u8);
+#[repr(u8)]
+pub enum FourToSixtyThree {
+ /// 4.
+ Four = 4,
+ /// 5.
+ Five,
+ /// 6.
+ Six,
+ /// 7.
+ Seven,
+ /// 8.
+ Eight,
+ /// 9.
+ Nine,
+ /// 10.
+ Ten,
+ /// 11.
+ Eleven,
+ /// 12.
+ Twelve,
+ /// 13.
+ Thirteen,
+ /// 14.
+ Fourteen,
+ /// 15.
+ Fifteen,
+ /// 16.
+ Sixteen,
+ /// 17.
+ Seventeen,
+ /// 18.
+ Eighteen,
+ /// 19.
+ Nineteen,
+ /// 20.
+ Twenty,
+ /// 21.
+ TwentyOne,
+ /// 22.
+ TwentyTwo,
+ /// 23.
+ TwentyThree,
+ /// 24.
+ TwentyFour,
+ /// 25.
+ TwentyFive,
+ /// 26.
+ TwentySix,
+ /// 27.
+ TwentySeven,
+ /// 28.
+ TwentyEight,
+ /// 29.
+ TwentyNine,
+ /// 30.
+ Thirty,
+ /// 31.
+ ThirtyOne,
+ /// 32.
+ ThirtyTwo,
+ /// 33.
+ ThirtyThree,
+ /// 34.
+ ThirtyFour,
+ /// 35.
+ ThirtyFive,
+ /// 36.
+ ThirtySix,
+ /// 37.
+ ThirtySeven,
+ /// 38.
+ ThirtyEight,
+ /// 39.
+ ThirtyNine,
+ /// 40.
+ Fourty,
+ /// 41.
+ FourtyOne,
+ /// 42.
+ FourtyTwo,
+ /// 43.
+ FourtyThree,
+ /// 44.
+ FourtyFour,
+ /// 45.
+ FourtyFive,
+ /// 46.
+ FourtySix,
+ /// 47.
+ FourtySeven,
+ /// 48.
+ FourtyEight,
+ /// 49.
+ FourtyNine,
+ /// 50.
+ Fifty,
+ /// 51.
+ FiftyOne,
+ /// 52.
+ FiftyTwo,
+ /// 53.
+ FiftyThree,
+ /// 54.
+ FiftyFour,
+ /// 55.
+ FiftyFive,
+ /// 56.
+ FiftySix,
+ /// 57.
+ FiftySeven,
+ /// 58.
+ FiftyEight,
+ /// 59.
+ FiftyNine,
+ /// 60.
+ Sixty,
+ /// 61.
+ SixtyOne,
+ /// 62.
+ SixtyTwo,
+ /// 63.
+ SixtyThree,
+}
impl FourToSixtyThree {
- /// Minimum inner value.
- const MIN_INNER: u8 = 4;
- /// Maximum inner value.
- const MAX_INNER: u8 = 63;
- /// Minimum value.
- pub const MIN: Self = Self(Self::MIN_INNER);
- /// Maximum value.
- pub const MAX: Self = Self(Self::MAX_INNER);
- /// Returns `Self` iff `val` is inclusively between [`Self::MIN`] and [`Self::MAX`].
+ /// Returns the equivalent `u8`.
+ #[expect(clippy::as_conversions, reason = "comment justifies correctness")]
#[inline]
#[must_use]
- pub const fn new(val: u8) -> Option<Self> {
- match val {
- Self::MIN_INNER..=Self::MAX_INNER => Some(Self(val)),
- _ => None,
- }
+ pub const fn into_u8(self) -> u8 {
+ // This is correct since `Self` is `repr(u8)`, and the initial discriminant has the value `4`
+ // and subsequent discriminants are implicitly incremented by 1.
+ self as u8
}
- /// Returns the contained value.
+ /// Returns `Some` representing `val` iff `val` is inclusively between 4 and 63.
+ #[expect(unsafe_code, reason = "comment justifies correctness")]
#[inline]
#[must_use]
- pub const fn value(self) -> u8 {
- self.0
+ pub const fn from_u8(val: u8) -> Option<Self> {
+ match val {
+ 0..=3 | 64.. => None,
+ _ => {
+ // SAFETY:
+ // `val` is inclusively between 4 and 63, and `Self` is `repr(u8)`; thus this
+ // is safe and correct.
+ Some(unsafe { mem::transmute::<u8, Self>(val) })
+ }
+ }
}
}
impl Display for FourToSixtyThree {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
+ self.into_u8().fmt(f)
}
}
impl From<FourToSixtyThree> for u8 {
#[inline]
fn from(value: FourToSixtyThree) -> Self {
- value.0
+ value.into_u8()
}
}
impl Default for FourToSixtyThree {
- /// Returns [`Self::MIN`].
#[inline]
fn default() -> Self {
- Self::MIN
+ Self::Four
}
}
/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client.
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug)]
pub struct Extension<'prf_first, 'prf_second> {
/// [`credProps`](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension).
///
@@ -753,6 +859,66 @@ pub struct Extension<'prf_first, 'prf_second> {
/// the inputs and have unique values for each credential.
pub prf: Option<(PrfInput<'prf_first, 'prf_second>, ExtensionInfo)>,
}
+impl<'prf_first, 'prf_second> Extension<'prf_first, 'prf_second> {
+ /// Returns an empty `Extension`.
+ #[inline]
+ #[must_use]
+ pub const fn none() -> Self {
+ Self {
+ cred_props: None,
+ cred_protect: CredProtect::None,
+ min_pin_length: None,
+ prf: None,
+ }
+ }
+ /// Same as [`Self::none`] except [`Self::cred_props`] is `Some` containing `req`.
+ #[inline]
+ #[must_use]
+ pub const fn with_cred_props(req: ExtensionReq) -> Self {
+ Self {
+ cred_props: Some(req),
+ ..Self::none()
+ }
+ }
+ /// Same as [`Self::none`] except [`Self::cred_protect`] is `cred_protect`.
+ #[inline]
+ #[must_use]
+ pub const fn with_cred_protect(cred_protect: CredProtect) -> Self {
+ Self {
+ cred_protect,
+ ..Self::none()
+ }
+ }
+ /// Same as [`Self::none`] except [`Self::min_pin_length`] is `Some` containing `min_len` and `info`.
+ #[inline]
+ #[must_use]
+ pub const fn with_min_pin_length(min_len: FourToSixtyThree, info: ExtensionInfo) -> Self {
+ Self {
+ min_pin_length: Some((min_len, info)),
+ ..Self::none()
+ }
+ }
+ /// Same as [`Self::none`] except [`Self::prf`] is `Some` containing `input` and `info`.
+ #[expect(single_use_lifetimes, reason = "false positive")]
+ #[inline]
+ #[must_use]
+ pub const fn with_prf<'a: 'prf_first, 'b: 'prf_second>(
+ input: PrfInput<'a, 'b>,
+ info: ExtensionInfo,
+ ) -> Self {
+ Self {
+ prf: Some((input, info)),
+ ..Self::none()
+ }
+ }
+}
+impl Default for Extension<'_, '_> {
+ /// Same as [`Self::none`].
+ #[inline]
+ fn default() -> Self {
+ Self::none()
+ }
+}
#[cfg(test)]
impl PartialEq for Extension<'_, '_> {
fn eq(&self, other: &Self) -> bool {
@@ -987,12 +1153,10 @@ impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<LEN>>
/// assert!(entity.display_name.is_none());
/// # Ok::<_, webauthn_rp::AggErr>(())
/// ```
- #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
#[inline]
fn from(value: &'a UserHandle<LEN>) -> Self {
Self {
- name: Username::try_from("blank")
- .unwrap_or_else(|_e| unreachable!("'blank' is no longer a valid Username")),
+ name: Username::blank(),
id: value,
display_name: None,
}
@@ -1005,7 +1169,7 @@ pub type PublicKeyCredentialUserEntity64<'name, 'display_name, 'id> =
pub type PublicKeyCredentialUserEntity16<'name, 'display_name, 'id> =
PublicKeyCredentialUserEntity<'name, 'display_name, 'id, 16>;
/// [`ResidentKeyRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement) sent to the client.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResidentKeyRequirement {
/// [`required`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-required).
Required,
@@ -1014,19 +1178,9 @@ pub enum ResidentKeyRequirement {
/// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred).
Preferred,
}
-#[cfg(test)]
-impl PartialEq for ResidentKeyRequirement {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::Required => matches!(other, Self::Required),
- Self::Discouraged => matches!(other, Self::Discouraged),
- Self::Preferred => matches!(other, Self::Preferred),
- }
- }
-}
/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint)
/// for [`AuthenticatorAttachment::CrossPlatform`] authenticators.
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CrossPlatformHint {
/// No hints.
#[default]
@@ -1052,21 +1206,9 @@ impl From<CrossPlatformHint> for Hint {
}
}
}
-#[cfg(test)]
-impl PartialEq for CrossPlatformHint {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::None => matches!(other, Self::None),
- Self::SecurityKey => matches!(other, Self::SecurityKey),
- Self::Hybrid => matches!(other, Self::Hybrid),
- Self::SecurityKeyHybrid => matches!(other, Self::SecurityKeyHybrid),
- Self::HybridSecurityKey => matches!(other, Self::HybridSecurityKey),
- }
- }
-}
/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhint)
/// for [`AuthenticatorAttachment::Platform`] authenticators.
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum PlatformHint {
/// No hints.
#[default]
@@ -1083,18 +1225,9 @@ impl From<PlatformHint> for Hint {
}
}
}
-#[cfg(test)]
-impl PartialEq for PlatformHint {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::None => matches!(other, Self::None),
- Self::ClientDevice => matches!(other, Self::ClientDevice),
- }
- }
-}
/// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment)
/// requirement with associated hints for further refinement.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthenticatorAttachmentReq {
/// No attachment information (i.e., any [`AuthenticatorAttachment`]).
None(Hint),
@@ -1155,18 +1288,6 @@ impl AuthenticatorAttachmentReq {
}
}
}
-#[cfg(test)]
-impl PartialEq for AuthenticatorAttachmentReq {
- fn eq(&self, other: &Self) -> bool {
- match *self {
- Self::None(info) => matches!(*other, Self::None(info2) if info == info2),
- Self::Platform(info) => matches!(*other, Self::Platform(info2) if info == info2),
- Self::CrossPlatform(info) => {
- matches!(*other, Self::CrossPlatform(info2) if info == info2)
- }
- }
- }
-}
/// [`AuthenticatorSelectionCriteria`](https://www.w3.org/TR/webauthn-3/#dictionary-authenticatorSelection).
#[derive(Clone, Copy, Debug)]
pub struct AuthenticatorSelectionCriteria {
@@ -2072,7 +2193,7 @@ impl ServerExtensionInfo {
} else {
// Pretend to set `minPinLength`, so we can check `prf`.
self.min_pin_length =
- Some((FourToSixtyThree::MIN, ExtensionInfo::RequireEnforceValue));
+ Some((FourToSixtyThree::Four, ExtensionInfo::RequireEnforceValue));
self.validate_unsolicited(client_ext, auth_ext)
}
} else if !matches!(auth_ext.cred_protect, CredentialProtectionPolicy::None) {
@@ -2588,11 +2709,7 @@ mod tests {
false,
ExtensionInfo::RequireEnforceValue,
),
- min_pin_length: Some((
- FourToSixtyThree::new(10)
- .unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new")),
- ExtensionInfo::RequireEnforceValue,
- )),
+ min_pin_length: Some((FourToSixtyThree::Ten, ExtensionInfo::RequireEnforceValue)),
prf: Some((
PrfInput {
first: [0].as_slice(),
@@ -2921,7 +3038,7 @@ mod tests {
);
}
_ = options.min_pin.map(|p| {
- assert!(p.value() <= 23, "bug");
+ assert!(p <= FourToSixtyThree::TwentyThree, "bug");
attestation_object.extend_from_slice(
[
// CBOR text of length 12.
@@ -2938,7 +3055,7 @@ mod tests {
b'g',
b't',
b'h',
- CBOR_UINT | p.value(),
+ CBOR_UINT | p.into_u8(),
]
.as_slice(),
);
@@ -3104,30 +3221,21 @@ mod tests {
[None, Some(ExtensionReq::Require), Some(ExtensionReq::Allow)];
const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [
None,
+ Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)),
Some((
- FourToSixtyThree::new(5).unwrap(),
- ExtensionInfo::RequireEnforceValue,
- )),
- Some((
- FourToSixtyThree::new(5).unwrap(),
+ FourToSixtyThree::Five,
ExtensionInfo::RequireDontEnforceValue,
)),
- Some((
- FourToSixtyThree::new(5).unwrap(),
- ExtensionInfo::AllowEnforceValue,
- )),
- Some((
- FourToSixtyThree::new(5).unwrap(),
- ExtensionInfo::AllowDontEnforceValue,
- )),
+ Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)),
+ Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)),
];
const ALL_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 4] =
[None, Some(None), Some(Some(false)), Some(Some(true))];
const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [
None,
- Some(FourToSixtyThree::MIN),
- Some(FourToSixtyThree::new(5).unwrap()),
- Some(FourToSixtyThree::new(6).unwrap()),
+ Some(FourToSixtyThree::Four),
+ Some(FourToSixtyThree::Five),
+ Some(FourToSixtyThree::Six),
];
for cred_protect in ALL_CRED_PROTECTION_OPTIONS {
for prf in ALL_PRF_OPTIONS {
@@ -3234,30 +3342,21 @@ mod tests {
];
const ALL_PIN_OPTIONS: [Option<(FourToSixtyThree, ExtensionInfo)>; 5] = [
None,
+ Some((FourToSixtyThree::Five, ExtensionInfo::RequireEnforceValue)),
Some((
- FourToSixtyThree::new(5).unwrap(),
- ExtensionInfo::RequireEnforceValue,
- )),
- Some((
- FourToSixtyThree::new(5).unwrap(),
+ FourToSixtyThree::Five,
ExtensionInfo::RequireDontEnforceValue,
)),
- Some((
- FourToSixtyThree::new(5).unwrap(),
- ExtensionInfo::AllowEnforceValue,
- )),
- Some((
- FourToSixtyThree::new(5).unwrap(),
- ExtensionInfo::AllowDontEnforceValue,
- )),
+ Some((FourToSixtyThree::Five, ExtensionInfo::AllowEnforceValue)),
+ Some((FourToSixtyThree::Five, ExtensionInfo::AllowDontEnforceValue)),
];
const ALL_NON_EMPTY_CRED_PROPS_OPTIONS: [Option<Option<bool>>; 3] =
[Some(None), Some(Some(false)), Some(Some(true))];
const ALL_MIN_PIN_OPTIONS: [Option<FourToSixtyThree>; 4] = [
None,
- Some(FourToSixtyThree::MIN),
- Some(FourToSixtyThree::new(5).unwrap()),
- Some(FourToSixtyThree::new(6).unwrap()),
+ Some(FourToSixtyThree::Four),
+ Some(FourToSixtyThree::Five),
+ Some(FourToSixtyThree::Six),
];
for cred_protect in ALL_CRED_PROTECTION_OPTIONS {
for prf in ALL_PRF_OPTIONS {
diff --git a/src/request/register/bin.rs b/src/request/register/bin.rs
@@ -43,7 +43,7 @@ impl Encode for Nickname<'_> {
}
}
/// Error returned from [`Nickname::decode`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeNicknameErr {
/// Variant returned when the encoded data could not be decoded
/// into a [`Nickname`].
@@ -94,7 +94,7 @@ impl Encode for Username<'_> {
}
}
/// Error returned from [`Username::decode`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeUsernameErr {
/// Variant returned when the encoded data could not be decoded
/// into a [`Username`].
diff --git a/src/request/register/error.rs b/src/request/register/error.rs
@@ -11,7 +11,7 @@ use core::{
#[cfg(doc)]
use std::time::{Instant, SystemTime};
/// Error returned by [`Nickname::try_from`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NicknameErr {
/// Error returned when the [Nickname Enforcement rule](https://www.rfc-editor.org/rfc/rfc8266#section-2.3)
/// fails.
@@ -31,7 +31,7 @@ impl Display for NicknameErr {
}
impl Error for NicknameErr {}
/// Error returned by [`Username::try_from`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UsernameErr {
/// Error returned when the
/// [UsernameCasePreserved Enforcement rule](https://www.rfc-editor.org/rfc/rfc8265#section-3.4.3) fails.
@@ -51,7 +51,7 @@ impl Display for UsernameErr {
}
impl Error for UsernameErr {}
/// Error returned by [`CredentialCreationOptions::start_ceremony`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CreationOptionsErr {
/// Error when [`Extension::cred_protect`] is [`CredProtect::UserVerificationRequired`] but [`AuthenticatorSelectionCriteria::user_verification`] is not
/// [`UserVerificationRequirement::Required`].
diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs
@@ -1,22 +1,32 @@
extern crate alloc;
use super::{
- AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, CoseAlgorithmIdentifier,
- CoseAlgorithmIdentifiers, CredProtect, CredentialCreationOptions, CrossPlatformHint, Extension,
- Hint, Nickname, PlatformHint, PrfInput, PublicKeyCredentialCreationOptions,
+ super::{
+ super::response::ser::{Null, Type},
+ auth::PrfInputOwned,
+ ser::{DEFAULT_RP_ID, PrfHelper},
+ },
+ AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, Challenge, CoseAlgorithmIdentifier,
+ CoseAlgorithmIdentifiers, CredProtect, CredentialCreationOptions,
+ CredentialMediationRequirement, CrossPlatformHint, Extension, ExtensionInfo, ExtensionReq,
+ FIVE_MINUTES, FourToSixtyThree, Hint, Nickname, PlatformHint, PrfInput,
+ PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor,
PublicKeyCredentialUserEntity, RegistrationClientState, ResidentKeyRequirement, RpId,
- UserHandle, Username,
+ UserHandle, UserVerificationRequirement, Username,
};
+#[cfg(doc)]
+use crate::response::AuthenticatorAttachment;
use alloc::borrow::Cow;
#[cfg(doc)]
use core::str::FromStr;
use core::{
+ convert,
fmt::{self, Formatter},
marker::PhantomData,
+ num::NonZeroU32,
str,
};
-use data_encoding::BASE64URL_NOPAD;
use serde::{
- de::{Deserialize, Deserializer, Error, Unexpected, Visitor},
+ de::{Deserialize, Deserializer, Error, MapAccess, SeqAccess, Unexpected, Visitor},
ser::{Serialize, SerializeSeq as _, SerializeStruct as _, Serializer},
};
impl Serialize for Nickname<'_> {
@@ -61,6 +71,12 @@ impl Serialize for Username<'_> {
serializer.serialize_str(self.0.as_ref())
}
}
+/// `"type"`
+const TYPE: &str = "type";
+/// `"public-key"`
+const PUBLIC_KEY: &str = "public-key";
+/// `"alg"`
+const ALG: &str = "alg";
/// [EdDSA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
const EDDSA: i16 = -8i16;
/// [ES256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
@@ -80,9 +96,9 @@ impl Serialize for CoseAlgorithmIdentifier {
serializer
.serialize_struct("PublicKeyCredentialParameters", 2)
.and_then(|mut ser| {
- ser.serialize_field("type", "public-key").and_then(|()| {
+ ser.serialize_field(TYPE, PUBLIC_KEY).and_then(|()| {
ser.serialize_field(
- "alg",
+ ALG,
&match *self {
Self::Eddsa => EDDSA,
Self::Es256 => ES256,
@@ -204,9 +220,9 @@ impl Serialize for UserHandle<1> {
where
S: Serializer,
{
- serializer.serialize_str(BASE64URL_NOPAD.encode_mut_str(
+ serializer.serialize_str(base64url_nopad::encode_buffer(
self.0.as_slice(),
- [0; crate::base64url_nopad_len(1)].as_mut_slice(),
+ [0; base64url_nopad::encode_len(1)].as_mut_slice(),
))
}
}
@@ -224,7 +240,7 @@ impl Serialize for UserHandle<$x> {
S: Serializer,
{
- serializer.serialize_str(BASE64URL_NOPAD.encode_mut_str(self.0.as_slice(), [0; crate::base64url_nopad_len($x)].as_mut_slice()))
+ serializer.serialize_str(base64url_nopad::encode_buffer(self.0.as_slice(), [0; base64url_nopad::encode_len($x)].as_mut_slice()))
}
}
)*
@@ -236,6 +252,8 @@ user_serialize!(
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
);
+/// `"displayName"`.
+const DISPLAY_NAME: &str = "displayName";
impl<const LEN: usize> Serialize for PublicKeyCredentialUserEntity<'_, '_, '_, LEN>
where
UserHandle<LEN>: Serialize,
@@ -284,7 +302,7 @@ where
ser.serialize_field(NAME, &self.name).and_then(|()| {
ser.serialize_field(ID, &self.id).and_then(|()| {
ser.serialize_field(
- "displayName",
+ DISPLAY_NAME,
self.display_name.as_ref().map_or("", |val| val.as_ref()),
)
.and_then(|()| ser.end())
@@ -293,6 +311,12 @@ where
})
}
}
+/// `"required"`
+const REQUIRED: &str = "required";
+/// `"discouraged"`
+const DISCOURAGED: &str = "discouraged";
+/// `"preferred"`
+const PREFERRED: &str = "preferred";
impl Serialize for ResidentKeyRequirement {
/// Serializes `self` to conform with
/// [`ResidentKeyRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement).
@@ -321,9 +345,9 @@ impl Serialize for ResidentKeyRequirement {
S: Serializer,
{
serializer.serialize_str(match *self {
- Self::Required => "required",
- Self::Discouraged => "discouraged",
- Self::Preferred => "preferred",
+ Self::Required => REQUIRED,
+ Self::Discouraged => DISCOURAGED,
+ Self::Preferred => PREFERRED,
})
}
}
@@ -391,6 +415,18 @@ impl Serialize for PlatformHint {
Hint::from(*self).serialize(serializer)
}
}
+/// `"platform"`.
+const PLATFORM: &str = "platform";
+/// `"cross-platform"`.
+const CROSS_PLATFORM: &str = "cross-platform";
+/// `"authenticatorAttachment"`.
+const AUTHENTICATOR_ATTACHMENT: &str = "authenticatorAttachment";
+/// `"residentKey"`.
+const RESIDENT_KEY: &str = "residentKey";
+/// `"requireResidentKey"`.
+const REQUIRE_RESIDENT_KEY: &str = "requireResidentKey";
+/// `"userVerification"`.
+const USER_VERIFICATION: &str = "userVerification";
impl Serialize for AuthenticatorSelectionCriteria {
/// Serializes `self` to conform with
/// [`AuthenticatorSelectionCriteria`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria).
@@ -437,26 +473,26 @@ impl Serialize for AuthenticatorSelectionCriteria {
Ok(())
} else {
ser.serialize_field(
- "authenticatorAttachment",
+ AUTHENTICATOR_ATTACHMENT,
if matches!(
self.authenticator_attachment,
AuthenticatorAttachmentReq::Platform(_)
) {
- "platform"
+ PLATFORM
} else {
- "cross-platform"
+ CROSS_PLATFORM
},
)
}
.and_then(|()| {
- ser.serialize_field("residentKey", &self.resident_key)
+ ser.serialize_field(RESIDENT_KEY, &self.resident_key)
.and_then(|()| {
ser.serialize_field(
- "requireResidentKey",
+ REQUIRE_RESIDENT_KEY,
&matches!(self.resident_key, ResidentKeyRequirement::Required),
)
.and_then(|()| {
- ser.serialize_field("userVerification", &self.user_verification)
+ ser.serialize_field(USER_VERIFICATION, &self.user_verification)
.and_then(|()| ser.end())
})
})
@@ -478,6 +514,23 @@ impl Serialize for Prf<'_, '_> {
})
}
}
+/// `credProps` key name.
+const CRED_PROPS: &str = "credProps";
+/// `minPinLength` key name.
+const MIN_PIN_LENGTH: &str = "minPinLength";
+/// `prf` key name.
+const PRF: &str = "prf";
+/// `credentialProtectionPolicy` key name.
+const CREDENTIAL_PROTECTION_POLICY: &str = "credentialProtectionPolicy";
+/// `enforceCredentialProtectionPolicy` key name.
+const ENFORCE_CREDENTIAL_PROTECTION_POLICY: &str = "enforceCredentialProtectionPolicy";
+/// `"userVerificationOptional"`.
+const USER_VERIFICATION_OPTIONAL: &str = "userVerificationOptional";
+/// `"userVerificationOptionalWithCredentialIDList"`.
+const USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST: &str =
+ "userVerificationOptionalWithCredentialIDList";
+/// `"userVerificationRequired"`.
+const USER_VERIFICATION_REQUIRED: &str = "userVerificationRequired";
impl Serialize for Extension<'_, '_> {
/// Serializes `self` to conform with
/// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson).
@@ -494,7 +547,7 @@ impl Serialize for Extension<'_, '_> {
/// serde_json::to_string(&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)),
+ /// min_pin_length: Some((FourToSixtyThree::Sixteen, ExtensionInfo::AllowDontEnforceValue)),
/// prf: Some((PrfInput { first: [0].as_slice(), second: None, }, ExtensionInfo::AllowEnforceValue))
/// })?,
/// r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"AA"}}}"#
@@ -511,12 +564,6 @@ impl Serialize for Extension<'_, '_> {
where
S: Serializer,
{
- /// `credProps` key name.
- const CRED_PROPS: &str = "credProps";
- /// `minPinLength` key name.
- const MIN_PIN_LENGTH: &str = "minPinLength";
- /// `prf` key name.
- const PRF: &str = "prf";
// The max is 1 + 2 + 1 + 1 = 5, so overflow is no concern.
let count = usize::from(self.cred_props.is_some())
+ if matches!(self.cred_protect, CredProtect::None) {
@@ -539,31 +586,31 @@ impl Serialize for Extension<'_, '_> {
// [`credProtect`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension)
// is serialized by serializing its fields directly and not as a map of fields.
ser.serialize_field(
- "credentialProtectionPolicy",
+ CREDENTIAL_PROTECTION_POLICY,
match self.cred_protect {
CredProtect::None => unreachable!(
"Extensions is incorrectly serializing credProtect"
),
CredProtect::UserVerificationOptional(enforce, _) => {
enforce_policy = enforce;
- "userVerificationOptional"
+ USER_VERIFICATION_OPTIONAL
}
CredProtect::UserVerificationOptionalWithCredentialIdList(
enforce,
_,
) => {
enforce_policy = enforce;
- "userVerificationOptionalWithCredentialIDList"
+ USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST
}
CredProtect::UserVerificationRequired(enforce, _) => {
enforce_policy = enforce;
- "userVerificationRequired"
+ USER_VERIFICATION_REQUIRED
}
},
)
.and_then(|()| {
ser.serialize_field(
- "enforceCredentialProtectionPolicy",
+ ENFORCE_CREDENTIAL_PROTECTION_POLICY,
&enforce_policy,
)
})
@@ -583,6 +630,30 @@ impl Serialize for Extension<'_, '_> {
})
}
}
+/// `"rp"`
+const RP: &str = "rp";
+/// `"user"`
+const USER: &str = "user";
+/// `"challenge"`
+const CHALLENGE: &str = "challenge";
+/// `"pubKeyCredParams"`
+const PUB_KEY_CRED_PARAMS: &str = "pubKeyCredParams";
+/// `"timeout"`
+const TIMEOUT: &str = "timeout";
+/// `"excludeCredentials"`
+const EXCLUDE_CREDENTIALS: &str = "excludeCredentials";
+/// `"authenticatorSelection"`
+const AUTHENTICATOR_SELECTION: &str = "authenticatorSelection";
+/// `"hints"`
+const HINTS: &str = "hints";
+/// `"attestation"`
+const ATTESTATION: &str = "attestation";
+/// `"attestationFormats"`
+const ATTESTATION_FORMATS: &str = "attestationFormats";
+/// `"extensions"`
+const EXTENSIONS: &str = "extensions";
+/// "none".
+const NONE: &str = "none";
impl<'user_name, 'user_display_name, 'user_id, const USER_LEN: usize> Serialize
for PublicKeyCredentialCreationOptions<
'_,
@@ -603,41 +674,39 @@ where
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))
+ ser.serialize_field(RP, &PublicKeyCredentialRpEntity(self.rp_id))
.and_then(|()| {
- ser.serialize_field("user", &self.user).and_then(|()| {
- ser.serialize_field("challenge", &self.challenge)
+ ser.serialize_field(USER, &self.user).and_then(|()| {
+ ser.serialize_field(CHALLENGE, &self.challenge)
.and_then(|()| {
ser.serialize_field(
- "pubKeyCredParams",
+ PUB_KEY_CRED_PARAMS,
&self.pub_key_cred_params,
)
.and_then(|()| {
- ser.serialize_field("timeout", &self.timeout).and_then(
+ ser.serialize_field(TIMEOUT, &self.timeout).and_then(
|()| {
ser.serialize_field(
- "excludeCredentials",
+ EXCLUDE_CREDENTIALS,
self.exclude_credentials.as_slice(),
)
.and_then(|()| {
ser.serialize_field(
- "authenticatorSelection",
+ AUTHENTICATOR_SELECTION,
&self.authenticator_selection,
)
.and_then(|()| {
- ser.serialize_field("hints", &match self.authenticator_selection.authenticator_attachment {
+ 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())
+ ser.serialize_field(ATTESTATION, NONE).and_then(|()| {
+ ser.serialize_field(ATTESTATION_FORMATS, [NONE].as_slice()).and_then(|()| {
+ ser.serialize_field(EXTENSIONS, &self.extensions).and_then(|()| ser.end())
})
})
})
@@ -652,6 +721,10 @@ where
})
}
}
+/// `"mediation"`.
+const MEDIATION: &str = "mediation";
+/// `"publicKey"`.
+const PUBLIC_KEY_NO_HYPEN: &str = "publicKey";
impl<
'rp_id,
'user_name,
@@ -695,9 +768,9 @@ where
serializer
.serialize_struct("CredentialCreationOptions", 2)
.and_then(|mut ser| {
- ser.serialize_field("mediation", &self.mediation)
+ ser.serialize_field(MEDIATION, &self.mediation)
.and_then(|()| {
- ser.serialize_field("publicKey", &self.public_key)
+ ser.serialize_field(PUBLIC_KEY_NO_HYPEN, &self.public_key)
.and_then(|()| ser.end())
})
})
@@ -769,7 +842,7 @@ where
/// let user_handle = UserHandle64::new();
/// 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));
+ /// options.public_key.extensions.min_pin_length = Some((FourToSixtyThree::Sixteen, ExtensionInfo::RequireEnforceValue));
/// # #[cfg(all(feature = "bin", feature = "custom"))]
/// 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!({
@@ -968,26 +1041,15 @@ where
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("UserHandle")
}
- #[expect(
- clippy::panic_in_result_fn,
- reason = "we want to crash when there is a bug"
- )]
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
- if crate::base64url_nopad_len(L) == v.len() {
+ if base64url_nopad::encode_len(L) == 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"
- );
- UserHandle(data)
- })
+ base64url_nopad::decode_buffer_exact(v.as_bytes(), data.as_mut_slice())
+ .map_err(E::custom)
+ .map(|()| UserHandle(data))
} else {
Err(E::invalid_value(
Unexpected::Str(v),
@@ -1051,3 +1113,2530 @@ impl<'de> Deserialize<'de> for CoseAlgorithmIdentifier {
deserializer.deserialize_i16(CoseAlgorithmIdentifierVisitor)
}
}
+/// Helper to deserialize `PublicKeyCredentialRpEntity` with an optional `RpId`.
+///
+/// Used in [`ClientCredentialCreationOptions::deserialize`].
+struct PublicKeyCredentialRpEntityHelper(RpId);
+impl<'de> Deserialize<'de> for PublicKeyCredentialRpEntityHelper {
+ /// Conforms to the following schema:
+ ///
+ /// ```json
+ /// {
+ /// "id": null | <RpId>,
+ /// "name": null | "" | <RpId> | <Nickname>
+ /// }
+ /// ```
+ ///
+ /// None of the fields are required.
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `PublicKeyCredentialRpEntityHelper`.
+ struct PublicKeyCredentialRpEntityHelperVisitor;
+ impl<'d> Visitor<'d> for PublicKeyCredentialRpEntityHelperVisitor {
+ type Value = PublicKeyCredentialRpEntityHelper;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("PublicKeyCredentialRpEntityHelper")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field for `PublicKeyCredentialRpEntityHelper`.
+ enum Field {
+ /// `id`.
+ Id,
+ /// `name`.
+ Name,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{ID}' or '{NAME}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ ID => Ok(Field::Id),
+ NAME => Ok(Field::Name),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ /// Helper to deserialize `name`.
+ struct Name;
+ impl<'e> Deserialize<'e> for Name {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Name`.
+ struct NameVisitor;
+ impl Visitor<'_> for NameVisitor {
+ type Value = Name;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("RpId name")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v.is_empty() {
+ Ok(Name)
+ } else {
+ Nickname::try_from(v).map(|_n| Name).or_else(|_e| {
+ RpId::try_from(v.to_owned())
+ .map_err(E::custom)
+ .map(|_r| Name)
+ })
+ }
+ }
+ }
+ deserializer.deserialize_str(NameVisitor)
+ }
+ }
+ let mut id = None;
+ let mut name = false;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::Id => {
+ if id.is_some() {
+ return Err(Error::duplicate_field(ID));
+ }
+ id = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::Name => {
+ if name {
+ return Err(Error::duplicate_field(NAME));
+ }
+ name = map.next_value::<Option<Name>>().map(|_n| true)?;
+ }
+ }
+ }
+ Ok(PublicKeyCredentialRpEntityHelper(
+ id.flatten().unwrap_or(DEFAULT_RP_ID),
+ ))
+ }
+ }
+ /// Fields for `PublicKeyCredentialRpEntityHelper`.
+ const FIELDS: &[&str; 2] = &[ID, NAME];
+ deserializer.deserialize_struct(
+ "PublicKeyCredentialRpEntityHelper",
+ FIELDS,
+ PublicKeyCredentialRpEntityHelperVisitor,
+ )
+ }
+}
+/// Similar to [`PublicKeyCredentialUserEntity`] except the [`UserHandle`] is owned.
+///
+/// This is primarily useful to assist [`ClientCredentialCreationOptions::deserialize`].
+#[derive(Debug)]
+pub struct PublicKeyCredentialUserEntityOwned<'name, 'display_name, const LEN: usize> {
+ /// See [`PublicKeyCredentialUserEntity::name`].
+ pub name: Username<'name>,
+ /// See [`PublicKeyCredentialUserEntity::id`].
+ pub id: UserHandle<LEN>,
+ /// See [`PublicKeyCredentialUserEntity::display_name`].
+ pub display_name: Option<Nickname<'display_name>>,
+}
+impl<'a: 'name + 'display_name + 'id, 'name, 'display_name, 'id, const LEN: usize>
+ From<&'a PublicKeyCredentialUserEntityOwned<'_, '_, LEN>>
+ for PublicKeyCredentialUserEntity<'name, 'display_name, 'id, LEN>
+{
+ #[inline]
+ fn from(value: &'a PublicKeyCredentialUserEntityOwned<'_, '_, LEN>) -> Self {
+ Self {
+ name: (&value.name).into(),
+ id: &value.id,
+ display_name: value.display_name.as_ref().map(Into::into),
+ }
+ }
+}
+impl<const LEN: usize> Default for PublicKeyCredentialUserEntityOwned<'_, '_, LEN>
+where
+ UserHandle<LEN>: Default,
+{
+ #[inline]
+ fn default() -> Self {
+ Self {
+ name: Username::blank(),
+ id: UserHandle::default(),
+ display_name: None,
+ }
+ }
+}
+impl<'de: 'name + 'display_name, 'name, 'display_name, const LEN: usize> Deserialize<'de>
+ for PublicKeyCredentialUserEntityOwned<'name, 'display_name, LEN>
+where
+ UserHandle<LEN>: Default,
+{
+ /// Deserializes a `struct` according to
+ /// [`PublicKeyCredentialUserEntityJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentityjson).
+ ///
+ /// Note none of the fields are required and all of them are allowed to be `null`.
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentityjson-id) is deserialized
+ /// according to [`UserHandle::deserialize`],
+ /// [`name`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentityjson-name) is deserialized
+ /// according to [`Username::deserialize`], and
+ /// [`displayName`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentityjson-displayname) is
+ /// deserialized according to [`Nickname::deserialize`] where `""` is deserialized to `None` (since
+ /// blank strings are not valid `Nickname`s).
+ ///
+ /// In the event `id` does not exist, a randomly generated `UserHandle` will be used. In the event `name`
+ /// does not exist, `"blank"` will be used. In the event `displayName` does not exist, `None` will
+ /// be used.
+ ///
+ /// Unknown or duplicate fields lead to an error.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::ser::PublicKeyCredentialUserEntityOwned;
+ /// let val = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 16>>(r#"{"name":"paul.erdos","displayName":"Erdős Pál"}"#)?;
+ /// assert_eq!(val.name.as_ref(), "paul.erdos");
+ /// assert_eq!(val.display_name.as_ref().map(|v| v.as_ref()), Some("Erdős Pál"));
+ /// assert_ne!(val.id.as_slice(), [0; 16]);
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[expect(clippy::too_many_lines, reason = "122 is fine")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `PublicKeyCredentialUserEntityOwned`.
+ struct PublicKeyCredentialUserEntityOwnedVisitor<'a, 'b, const L: usize>(
+ PhantomData<fn() -> (&'a (), &'b ())>,
+ );
+ impl<'d: 'a + 'b, 'a, 'b, const L: usize> Visitor<'d>
+ for PublicKeyCredentialUserEntityOwnedVisitor<'a, 'b, L>
+ where
+ UserHandle<L>: Default,
+ {
+ type Value = PublicKeyCredentialUserEntityOwned<'a, 'b, L>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("PublicKeyCredentialUserEntityOwned")
+ }
+ #[expect(clippy::too_many_lines, reason = "102 is fine")]
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field for `PublicKeyCredentialUserEntityOwned`.
+ enum Field {
+ /// `id`.
+ Id,
+ /// `name`.
+ Name,
+ /// `displayName`
+ DisplayName,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{ID}', '{NAME}', or '{DISPLAY_NAME}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ ID => Ok(Field::Id),
+ NAME => Ok(Field::Name),
+ DISPLAY_NAME => Ok(Field::DisplayName),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ /// Helper to deserialize `displayName`.
+ struct DisplayName<'e>(Option<Nickname<'e>>);
+ impl<'e: 'f, 'f> Deserialize<'e> for DisplayName<'f> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `DisplayName`.
+ struct DisplayNameVisitor<'g>(PhantomData<fn() -> &'g ()>);
+ impl<'g: 'h, 'h> Visitor<'g> for DisplayNameVisitor<'h> {
+ type Value = DisplayName<'h>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("User display name")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v.is_empty() {
+ Ok(DisplayName(None))
+ } else {
+ Nickname::try_from(v).map_err(E::custom).map(|name| {
+ DisplayName(Some(Nickname(Cow::Owned(name.0.into_owned()))))
+ })
+ }
+ }
+ fn visit_borrowed_str<E>(self, v: &'g str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v.is_empty() {
+ Ok(DisplayName(None))
+ } else {
+ Nickname::try_from(v)
+ .map_err(E::custom)
+ .map(|n| DisplayName(Some(n)))
+ }
+ }
+ }
+ deserializer.deserialize_str(DisplayNameVisitor(PhantomData))
+ }
+ }
+ let mut user_handle = None;
+ let mut username = None;
+ let mut display = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::Id => {
+ if user_handle.is_some() {
+ return Err(Error::duplicate_field(ID));
+ }
+ user_handle = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::Name => {
+ if username.is_some() {
+ return Err(Error::duplicate_field(NAME));
+ }
+ username = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::DisplayName => {
+ if display.is_some() {
+ return Err(Error::duplicate_field(DISPLAY_NAME));
+ }
+ display = map
+ .next_value::<Option<DisplayName<'_>>>()
+ .map(|n| n.map_or_else(|| Some(None), |disp| Some(disp.0)))?;
+ }
+ }
+ }
+ Ok(PublicKeyCredentialUserEntityOwned {
+ id: user_handle.flatten().unwrap_or_default(),
+ name: username.flatten().unwrap_or_else(Username::blank),
+ display_name: display.flatten(),
+ })
+ }
+ }
+ /// Fields for `PublicKeyCredentialUserEntityOwned`.
+ const FIELDS: &[&str; 3] = &[ID, NAME, DISPLAY_NAME];
+ deserializer.deserialize_struct(
+ "PublicKeyCredentialUserEntityOwned",
+ FIELDS,
+ PublicKeyCredentialUserEntityOwnedVisitor(PhantomData),
+ )
+ }
+}
+/// `newtype` around `CoseAlgorithmIdentifier`.
+struct PubParam(CoseAlgorithmIdentifier);
+impl<'de> Deserialize<'de> for PubParam {
+ /// Conforms to the following schema:
+ ///
+ /// ```json
+ /// {
+ /// "alg": <CoseAlgorithmIdentifier>,
+ /// "type": "public-key",
+ /// }
+ /// ```
+ ///
+ /// `"alg"` is required.
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `PubParam`.
+ struct PubParamVisitor;
+ impl<'d> Visitor<'d> for PubParamVisitor {
+ type Value = PubParam;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("PubParam")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field for `PubParam`.
+ enum Field {
+ /// `"type"`.
+ Type,
+ /// `"alg"`.
+ Alg,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{TYPE}' or '{ALG}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ TYPE => Ok(Field::Type),
+ ALG => Ok(Field::Alg),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut typ = false;
+ let mut alg = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::Type => {
+ if typ {
+ return Err(Error::duplicate_field(TYPE));
+ }
+ typ = map.next_value::<Type>().map(|_t| true)?;
+ }
+ Field::Alg => {
+ if alg.is_some() {
+ return Err(Error::duplicate_field(ALG));
+ }
+ alg = map.next_value().map(Some)?;
+ }
+ }
+ }
+ alg.ok_or_else(|| Error::missing_field(ALG)).map(PubParam)
+ }
+ }
+ /// Fields for `PubParam`.
+ const FIELDS: &[&str; 2] = &[TYPE, ALG];
+ deserializer.deserialize_struct("PubParam", FIELDS, PubParamVisitor)
+ }
+}
+impl<'de> Deserialize<'de> for CoseAlgorithmIdentifiers {
+ /// Deserializes a sequence based on
+ /// [`pubKeyCredParams`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-pubkeycredparams)
+ /// except [`type`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialparameters-type) is not required.
+ ///
+ /// Note the sequence of [`CoseAlgorithmIdentifier`]s MUST match [`CoseAlgorithmIdentifier::cmp`] or an
+ /// error will occur (e.g., if [`CoseAlgorithmIdentifier::Eddsa`] exists, then it must appear first).
+ ///
+ /// An empty sequence will be treated as [`Self::ALL`].
+ ///
+ /// Unknown or duplicate fields lead to an error.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::CoseAlgorithmIdentifiers;
+ /// assert!(serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},{"type":"public-key","alg":-257}]"#).is_ok());
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `CoseAlgorithmIdentifiers`.
+ struct CoseAlgorithmIdentifiersVisitor;
+ impl<'d> Visitor<'d> for CoseAlgorithmIdentifiersVisitor {
+ type Value = CoseAlgorithmIdentifiers;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("CoseAlgorithmIdentifiers")
+ }
+ #[expect(clippy::else_if_without_else, reason = "prefer it this way")]
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'d>,
+ {
+ let mut eddsa = false;
+ let mut es256 = false;
+ let mut es384 = false;
+ let mut rs256 = false;
+ while let Some(elem) = seq.next_element::<PubParam>()? {
+ match elem.0 {
+ CoseAlgorithmIdentifier::Eddsa => {
+ if eddsa {
+ return Err(Error::custom(
+ "pubKeyCredParams contained duplicate EdDSA values",
+ ));
+ } else if es256 || es384 || rs256 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained EdDSA, but it wasn't the first value",
+ ));
+ }
+ eddsa = true;
+ }
+ CoseAlgorithmIdentifier::Es256 => {
+ if es256 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained duplicate Es256 values",
+ ));
+ } else if es384 || rs256 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained Es256, but it was preceded by Es384 or Rs256",
+ ));
+ }
+ es256 = true;
+ }
+ CoseAlgorithmIdentifier::Es384 => {
+ if es384 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained duplicate Es384 values",
+ ));
+ } else if rs256 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained Es384, but it was preceded by Rs256",
+ ));
+ }
+ es384 = true;
+ }
+ CoseAlgorithmIdentifier::Rs256 => {
+ if rs256 {
+ return Err(Error::custom(
+ "pubKeyCredParams contained duplicate Rs256 values",
+ ));
+ }
+ rs256 = true;
+ }
+ }
+ }
+ let mut algs = CoseAlgorithmIdentifiers(0);
+ if eddsa {
+ algs = algs.add(CoseAlgorithmIdentifier::Eddsa);
+ }
+ if es256 {
+ algs = algs.add(CoseAlgorithmIdentifier::Es256);
+ }
+ if es384 {
+ algs = algs.add(CoseAlgorithmIdentifier::Es384);
+ }
+ if rs256 {
+ algs = algs.add(CoseAlgorithmIdentifier::Rs256);
+ }
+ Ok(if algs.0 == 0 {
+ CoseAlgorithmIdentifiers::ALL
+ } else {
+ algs
+ })
+ }
+ }
+ deserializer.deserialize_seq(CoseAlgorithmIdentifiersVisitor)
+ }
+}
+/// Helper for `UserVerificatonRequirement::deserialize` and [`ResidentKeyRequirement::deserialize`].
+enum Requirement {
+ /// Required.
+ Required,
+ /// Discouraged.
+ Discouraged,
+ /// Preferred.
+ Preferred,
+}
+impl<'de> Deserialize<'de> for Requirement {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `Requirement`.
+ struct RequirementVisitor;
+ impl Visitor<'_> for RequirementVisitor {
+ type Value = Requirement;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{REQUIRED}', '{DISCOURAGED}', or '{PREFERRED}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ REQUIRED => Ok(Requirement::Required),
+ DISCOURAGED => Ok(Requirement::Discouraged),
+ PREFERRED => Ok(Requirement::Preferred),
+ _ => Err(E::invalid_value(
+ Unexpected::Str(v),
+ &format!("'{REQUIRED}', '{DISCOURAGED}', or '{PREFERRED}'").as_str(),
+ )),
+ }
+ }
+ }
+ deserializer.deserialize_str(RequirementVisitor)
+ }
+}
+impl From<Requirement> for ResidentKeyRequirement {
+ #[inline]
+ fn from(value: Requirement) -> Self {
+ match value {
+ Requirement::Required => Self::Required,
+ Requirement::Discouraged => Self::Discouraged,
+ Requirement::Preferred => Self::Preferred,
+ }
+ }
+}
+impl From<Requirement> for UserVerificationRequirement {
+ #[inline]
+ fn from(value: Requirement) -> Self {
+ match value {
+ Requirement::Required => Self::Required,
+ Requirement::Discouraged => Self::Discouraged,
+ Requirement::Preferred => Self::Preferred,
+ }
+ }
+}
+impl<'de> Deserialize<'de> for ResidentKeyRequirement {
+ /// Deserializes [`prim@str`] based on
+ /// [`ResidentKeyRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::ResidentKeyRequirement;
+ /// assert!(
+ /// matches!(
+ /// serde_json::from_str(r#""required""#)?,
+ /// ResidentKeyRequirement::Required
+ /// )
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ Requirement::deserialize(deserializer).map(Self::from)
+ }
+}
+impl<'de> Deserialize<'de> for UserVerificationRequirement {
+ /// Deserializes [`prim@str`] based on
+ /// [`UserVerificationRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::UserVerificationRequirement;
+ /// assert!(
+ /// matches!(
+ /// serde_json::from_str(r#""required""#)?,
+ /// UserVerificationRequirement::Required
+ /// )
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ Requirement::deserialize(deserializer).map(Self::from)
+ }
+}
+impl<'de> Deserialize<'de> for AuthenticatorAttachmentReq {
+ /// Deserializes a [`prim@str`] according to
+ /// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment).
+ ///
+ /// Note the contained hint will be none (e.g., [`PlatformHint::None`]).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{AuthenticatorAttachmentReq, PlatformHint};
+ /// assert!(matches!(
+ /// serde_json::from_str(r#""platform""#)?,
+ /// AuthenticatorAttachmentReq::Platform(hint) if matches!(hint, PlatformHint::None)
+ /// ));
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `AuthenticatorAttachmentReq`.
+ struct AuthenticatorAttachmentReqVisitor;
+ impl Visitor<'_> for AuthenticatorAttachmentReqVisitor {
+ type Value = AuthenticatorAttachmentReq;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{PLATFORM}' or '{CROSS_PLATFORM}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ PLATFORM => Ok(AuthenticatorAttachmentReq::Platform(PlatformHint::None)),
+ CROSS_PLATFORM => Ok(AuthenticatorAttachmentReq::CrossPlatform(
+ CrossPlatformHint::None,
+ )),
+ _ => Err(E::invalid_value(
+ Unexpected::Str(v),
+ &format!("'{PLATFORM}' or '{CROSS_PLATFORM}'").as_str(),
+ )),
+ }
+ }
+ }
+ deserializer.deserialize_str(AuthenticatorAttachmentReqVisitor)
+ }
+}
+impl<'de> Deserialize<'de> for AuthenticatorSelectionCriteria {
+ /// Deserializes a `struct` based on
+ /// [`AuthenticatorSelectionCriteria`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria).
+ ///
+ /// Note that none of the fields are required, and all are allowed to be `null`. Additionally
+ /// [`residentKey`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey) and
+ /// [`requireResidentKey`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-requireresidentkey)
+ /// must be consistent (i.e., `requireResidentKey` iff `residentKey` is [`ResidentKeyRequirement::Required`]).
+ ///
+ /// `residentKey` defaults to [`ResidentKeyRequirement::Discouraged`] when it is `null` or does not exist
+ /// unless `requireResidentKey` is `true` in which case it is `ResidentKeyRequirement::Required`.
+ ///
+ /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification)
+ /// is [`UserVerificationRequirement::Preferred`] if it does not exist or is `null`.
+ ///
+ /// If
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment)
+ /// does not exist or is `null`, then [`AuthenticatorAttachmentReq::None`] will be used containing
+ /// [`Hint::None`].
+ ///
+ /// Unknown or duplicate fields lead to an error.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{Hint, register::{AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria}};
+ /// assert!(
+ /// matches!(
+ /// serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"authenticatorAttachment":null,"residentKey":"required","requireResidentKey":true,"userVerification":"required"}"#)?.authenticator_attachment,
+ /// AuthenticatorAttachmentReq::None(hints) if matches!(hints, Hint::None)
+ /// )
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[expect(clippy::too_many_lines, reason = "144 isn't too bad")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `AuthenticatorSelectionCriteria`.
+ struct AuthenticatorSelectionCriteriaVisitor;
+ impl<'de> Visitor<'de> for AuthenticatorSelectionCriteriaVisitor {
+ type Value = AuthenticatorSelectionCriteria;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("AuthenticatorSelectionCriteria")
+ }
+ #[expect(clippy::too_many_lines, reason = "121 isn't too bad")]
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'de>,
+ {
+ /// Field for `AuthenticatorSelectionCriteria`.
+ enum Field {
+ /// `"authenticatorAttachment"`.
+ AuthenticatorAttachment,
+ /// `"residentKey"`.
+ ResidentKey,
+ /// `"requireResidentKey"`.
+ RequireResidentKey,
+ /// `"userVerification"`.
+ UserVerification,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(
+ formatter,
+ "'{AUTHENTICATOR_ATTACHMENT}', '{RESIDENT_KEY}', '{REQUIRE_RESIDENT_KEY}', or '{USER_VERIFICATION}'"
+ )
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ AUTHENTICATOR_ATTACHMENT => Ok(Field::AuthenticatorAttachment),
+ RESIDENT_KEY => Ok(Field::ResidentKey),
+ REQUIRE_RESIDENT_KEY => Ok(Field::RequireResidentKey),
+ USER_VERIFICATION => Ok(Field::UserVerification),
+ _ => Err(Error::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut attach = None;
+ let mut res_key = None;
+ let mut res_req: Option<Option<bool>> = None;
+ let mut uv = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::AuthenticatorAttachment => {
+ if attach.is_some() {
+ return Err(Error::duplicate_field(AUTHENTICATOR_ATTACHMENT));
+ }
+ attach = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::ResidentKey => {
+ if res_key.is_some() {
+ return Err(Error::duplicate_field(RESIDENT_KEY));
+ }
+ res_key = map.next_value::<Option<_>>().and_then(|opt| {
+ opt.map_or(Ok(Some(None)), |res| res_req.map_or(Ok(Some(opt)), |req_opt| req_opt.map_or(Ok(Some(opt)), |req| {
+ match res {
+ ResidentKeyRequirement::Required => {
+ if req {
+ Ok(Some(opt))
+ } else {
+ Err(Error::custom(format!("'{RESIDENT_KEY}' is '{REQUIRED}', but '{REQUIRE_RESIDENT_KEY}' is false")))
+ }
+ }
+ ResidentKeyRequirement::Discouraged | ResidentKeyRequirement::Preferred => {
+ if req {
+ Err(Error::custom(format!("'{RESIDENT_KEY}' is not '{REQUIRED}', but '{REQUIRE_RESIDENT_KEY}' is true")))
+ } else {
+ Ok(Some(opt))
+ }
+ }
+ }
+ })))
+ })?;
+ }
+ Field::RequireResidentKey => {
+ if res_req.is_some() {
+ return Err(Error::duplicate_field(REQUIRE_RESIDENT_KEY));
+ }
+ res_req = map.next_value::<Option<_>>().and_then(|opt| {
+ opt.map_or(Ok(Some(None)), |req| res_key.map_or(Ok(Some(opt)), |req_opt| req_opt.map_or(Ok(Some(opt)), |res| {
+ match res {
+ ResidentKeyRequirement::Required => {
+ if req {
+ Ok(Some(opt))
+ } else {
+ Err(Error::custom(format!("'{RESIDENT_KEY}' is '{REQUIRED}', but '{REQUIRE_RESIDENT_KEY}' is false")))
+ }
+ }
+ ResidentKeyRequirement::Discouraged | ResidentKeyRequirement::Preferred => {
+ if req {
+ Err(Error::custom(format!("'{RESIDENT_KEY}' is not '{REQUIRED}', but '{REQUIRE_RESIDENT_KEY}' is true")))
+ } else {
+ Ok(Some(opt))
+ }
+ }
+ }
+ })))
+ })?;
+ }
+ Field::UserVerification => {
+ if uv.is_some() {
+ return Err(Error::duplicate_field(USER_VERIFICATION));
+ }
+ uv = map.next_value::<Option<_>>().map(Some)?;
+ }
+ }
+ }
+ Ok(AuthenticatorSelectionCriteria {
+ authenticator_attachment: attach.flatten().unwrap_or_default(),
+ resident_key: res_key.flatten().unwrap_or_else(|| {
+ if res_req.flatten().is_some_and(convert::identity) {
+ ResidentKeyRequirement::Required
+ } else {
+ ResidentKeyRequirement::Discouraged
+ }
+ }),
+ user_verification: uv
+ .flatten()
+ .unwrap_or(UserVerificationRequirement::Preferred),
+ })
+ }
+ }
+ /// Fields for `AuthenticatorSelectionCriteria`.
+ const FIELDS: &[&str; 4] = &[
+ AUTHENTICATOR_ATTACHMENT,
+ RESIDENT_KEY,
+ REQUIRE_RESIDENT_KEY,
+ USER_VERIFICATION,
+ ];
+ deserializer.deserialize_struct(
+ "AuthenticatorSelectionCriteria",
+ FIELDS,
+ AuthenticatorSelectionCriteriaVisitor,
+ )
+ }
+}
+/// Helper for [`ClientCredentialCreationOptions::deserialize`].
+struct Attestation;
+impl<'de> Deserialize<'de> for Attestation {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `Attestation`.
+ struct AttestationVisitor;
+ impl Visitor<'_> for AttestationVisitor {
+ type Value = Attestation;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str(NONE)
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v == NONE {
+ Ok(Attestation)
+ } else {
+ Err(E::invalid_value(Unexpected::Str(v), &NONE))
+ }
+ }
+ }
+ deserializer.deserialize_str(AttestationVisitor)
+ }
+}
+/// Helper for [`ClientCredentialCreationOptions::deserialize`].
+struct AttestationFormats;
+impl<'de> Deserialize<'de> for AttestationFormats {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `AttestationFormats`.
+ struct AttestationFormatsVisitor;
+ impl<'d> Visitor<'d> for AttestationFormatsVisitor {
+ type Value = AttestationFormats;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("AttestationFormats")
+ }
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'d>,
+ {
+ seq.next_element::<Attestation>().and_then(|opt| {
+ opt.map_or(Ok(AttestationFormats), |_f| {
+ seq.next_element::<Attestation>().and_then(|opt2| {
+ opt2.map_or(Ok(AttestationFormats), |_val| Err(Error::custom("attestationFormats must be an empty sequence or contain exactly one string whose value is 'none'")))
+ })
+ })
+ })
+ }
+ }
+ deserializer.deserialize_seq(AttestationFormatsVisitor)
+ }
+}
+/// Similar to [`Extension`] except [`PrfInputOwned`] is used.
+///
+/// This is primarily useful to assist [`ClientCredentialCreationOptions::deserialize`].
+#[derive(Debug, Default)]
+pub struct ExtensionOwned {
+ /// See [`Extension::cred_props`].
+ pub cred_props: Option<ExtensionReq>,
+ /// See [`Extension::cred_protect`].
+ pub cred_protect: CredProtect,
+ /// See [`Extension::min_pin_length`].
+ pub min_pin_length: Option<(FourToSixtyThree, ExtensionInfo)>,
+ /// See [`Extension::prf`].
+ pub prf: Option<PrfInputOwned>,
+}
+impl<'a: 'prf_first + 'prf_second, 'prf_first, 'prf_second> From<&'a ExtensionOwned>
+ for Extension<'prf_first, 'prf_second>
+{
+ #[inline]
+ fn from(value: &'a ExtensionOwned) -> Self {
+ Self {
+ cred_props: value.cred_props,
+ cred_protect: value.cred_protect,
+ min_pin_length: value.min_pin_length,
+ prf: value.prf.as_ref().map(|input| {
+ (
+ PrfInput {
+ first: input.first.as_slice(),
+ second: input.second.as_deref(),
+ },
+ ExtensionInfo::AllowEnforceValue,
+ )
+ }),
+ }
+ }
+}
+impl<'de> Deserialize<'de> for ExtensionOwned {
+ /// Deserializes a `struct` according to the following pseudo-schema:
+ ///
+ /// ```json
+ /// {
+ /// "credProps": null | false | true,
+ /// "credentialProtectionPolicy": null | "userVerificationOptional" | "userVerificationOptionalWithCredentialIDList" | "userVerificationRequired",
+ /// "enforceCredentialProtectionPolicy": null | false | true,
+ /// "minPinLength": null | false | true,
+ /// "prf": null | PRFJSON
+ /// }
+ /// // PRFJSON:
+ /// {
+ /// "eval": PRFInputs
+ /// }
+ /// // PRFInputs:
+ /// {
+ /// "first": <base64url-encoded string>,
+ /// "second": null | <base64url-encoded string>
+ /// }
+ /// ```
+ ///
+ /// where the only required fields are `"eval"` and `"first"`. Additionally `"credentialProtectionPolicy"`
+ /// must exist if `"enforceCredentialProtectionPolicy"` exists, and it must not be `null` if the latter
+ /// is not `null`. If the former is defined and not `null` but the latter is not defined or is `null`, then
+ /// `false` will be used for the latter. Unknown or duplicate fields lead to an error.
+ ///
+ /// All extensions are not required to have a response sent back; but _if_ a response is sent back, its value
+ /// will be enforced. In the case of `"minPinLength"`, [`FourToSixtyThree::Four`] will be the minimum
+ /// length enforced (i.e., any valid response is guaranteed to satisfy since it will have length at least
+ /// as large).
+ ///
+ /// Unknown or duplicate fields lead to an error.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{ExtensionInfo, ExtensionReq, register::{CredProtect, FourToSixtyThree, ser::ExtensionOwned}};
+ /// let ext = serde_json::from_str::<ExtensionOwned>(
+ /// r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":null}}}"#,
+ /// )?;
+ /// assert!(
+ /// ext.cred_props
+ /// .map_or(false, |req| matches!(req, ExtensionReq::Allow))
+ /// );
+ /// assert!(
+ /// matches!(ext.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue))
+ /// );
+ /// assert!(ext.min_pin_length.map_or(false, |pin| pin.0 == FourToSixtyThree::Four
+ /// && matches!(pin.1, ExtensionInfo::AllowEnforceValue)));
+ /// assert!(ext.prf.map_or(false, |prf| prf.first.is_empty()
+ /// && prf.second.is_none()
+ /// && matches!(prf.ext_req, ExtensionReq::Allow)));
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[expect(clippy::too_many_lines, reason = "want to keep logic internal")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `ExtensionOwned`.
+ struct ExtensionOwnedVisitor;
+ impl<'d> Visitor<'d> for ExtensionOwnedVisitor {
+ type Value = ExtensionOwned;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("ExtensionOwned")
+ }
+ #[expect(clippy::too_many_lines, reason = "want to keep logic internal")]
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field for `ExtensionOwned`.
+ enum Field {
+ /// `credProps`.
+ CredProps,
+ /// `credentialProtectionPolicy`.
+ CredentialProtectionPolicy,
+ /// `enforceCredentialProtectionPolicy`.
+ EnforceCredentialProtectionPolicy,
+ /// `minPinLength`.
+ MinPinLength,
+ /// `prf`
+ Prf,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(
+ formatter,
+ "'{CRED_PROPS}', '{CREDENTIAL_PROTECTION_POLICY}', '{ENFORCE_CREDENTIAL_PROTECTION_POLICY}', '{MIN_PIN_LENGTH}', or '{PRF}'"
+ )
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ CRED_PROPS => Ok(Field::CredProps),
+ CREDENTIAL_PROTECTION_POLICY => {
+ Ok(Field::CredentialProtectionPolicy)
+ }
+ ENFORCE_CREDENTIAL_PROTECTION_POLICY => {
+ Ok(Field::EnforceCredentialProtectionPolicy)
+ }
+ MIN_PIN_LENGTH => Ok(Field::MinPinLength),
+ PRF => Ok(Field::Prf),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ /// Credential protection policy values.
+ #[expect(clippy::enum_variant_names, reason = "consistent with ctap names")]
+ enum Policy {
+ /// `userVerificationOptional`.
+ UserVerificationOptional,
+ /// `userVerificationOptionalWithCredentialIdList`.
+ UserVerificationOptionalWithCredentialIdLisit,
+ /// `userVerificationRequired`.
+ UserVerificationRequired,
+ }
+ impl<'e> Deserialize<'e> for Policy {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Policy`.
+ struct PolicyVisitor;
+ impl Visitor<'_> for PolicyVisitor {
+ type Value = Policy;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(
+ formatter,
+ "'{USER_VERIFICATION_OPTIONAL}', '{USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST}', or '{USER_VERIFICATION_REQUIRED}'"
+ )
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ USER_VERIFICATION_OPTIONAL => Ok(Policy::UserVerificationOptional),
+ USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST => Ok(Policy::UserVerificationOptionalWithCredentialIdLisit),
+ USER_VERIFICATION_REQUIRED => Ok(Policy::UserVerificationRequired),
+ _ => Err(E::invalid_value(Unexpected::Str(v), &format!("'{USER_VERIFICATION_OPTIONAL}', '{USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST}', or '{USER_VERIFICATION_REQUIRED}'").as_str())),
+ }
+ }
+ }
+ deserializer.deserialize_str(PolicyVisitor)
+ }
+ }
+ let mut props: Option<Option<bool>> = None;
+ let mut policy = None;
+ let mut enforce = None;
+ let mut pin: Option<Option<bool>> = None;
+ let mut prf_inputs = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::CredProps => {
+ if props.is_some() {
+ return Err(Error::duplicate_field(CRED_PROPS));
+ }
+ props = map.next_value().map(Some)?;
+ }
+ Field::CredentialProtectionPolicy => {
+ if policy.is_some() {
+ return Err(Error::duplicate_field(CREDENTIAL_PROTECTION_POLICY));
+ }
+ policy = map.next_value::<Option<Policy>>().map(Some)?;
+ }
+ Field::EnforceCredentialProtectionPolicy => {
+ if enforce.is_some() {
+ return Err(Error::duplicate_field(
+ ENFORCE_CREDENTIAL_PROTECTION_POLICY,
+ ));
+ }
+ enforce = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::MinPinLength => {
+ if pin.is_some() {
+ return Err(Error::duplicate_field(MIN_PIN_LENGTH));
+ }
+ pin = map.next_value().map(Some)?;
+ }
+ Field::Prf => {
+ if prf_inputs.is_some() {
+ return Err(Error::duplicate_field(PRF));
+ }
+ prf_inputs = map
+ .next_value::<Option<PrfHelper>>()
+ .map(|opt| Some(opt.map(|p| p.0)))?;
+ }
+ }
+ }
+ policy.map_or_else(
+ || {
+ if enforce.is_some() {
+ Err(Error::custom(format!("'{ENFORCE_CREDENTIAL_PROTECTION_POLICY}' must not exist when '{CREDENTIAL_PROTECTION_POLICY}' does not exist")))
+ } else {
+ Ok(CredProtect::None)
+ }
+ },
+ |opt_policy| opt_policy.map_or_else(
+ || {
+ if enforce.is_some_and(|opt| opt.is_some()) {
+ Err(Error::custom(format!("'{ENFORCE_CREDENTIAL_PROTECTION_POLICY}' must be null or not exist when '{CREDENTIAL_PROTECTION_POLICY}' is null")))
+ } else {
+ Ok(CredProtect::None)
+ }
+ },
+ |cred_policy| {
+ match cred_policy {
+ Policy::UserVerificationOptional => Ok(CredProtect::UserVerificationOptional(enforce.flatten().unwrap_or_default(), ExtensionInfo::AllowEnforceValue)),
+ Policy::UserVerificationOptionalWithCredentialIdLisit => Ok(CredProtect::UserVerificationOptionalWithCredentialIdList(enforce.flatten().unwrap_or_default(), ExtensionInfo::AllowEnforceValue)),
+ Policy::UserVerificationRequired => Ok(CredProtect::UserVerificationRequired(enforce.flatten().unwrap_or_default(), ExtensionInfo::AllowEnforceValue)),
+ }
+ }
+ ),
+ ).map(|cred_protect| {
+ ExtensionOwned { cred_props: props.flatten().and_then(|p| p.then_some(ExtensionReq::Allow)), cred_protect, min_pin_length: pin.flatten().and_then(|m| m.then_some((FourToSixtyThree::Four, ExtensionInfo::AllowEnforceValue))), prf: prf_inputs.flatten(), }
+ })
+ }
+ }
+ /// Fields for `ExtensionOwned`.
+ const FIELDS: &[&str; 5] = &[
+ CRED_PROPS,
+ CREDENTIAL_PROTECTION_POLICY,
+ ENFORCE_CREDENTIAL_PROTECTION_POLICY,
+ MIN_PIN_LENGTH,
+ PRF,
+ ];
+ deserializer.deserialize_struct("ExtensionOwned", FIELDS, ExtensionOwnedVisitor)
+ }
+}
+/// Similar to [`PublicKeyCredentialCreationOptions`] except the fields are based on owned data.
+///
+/// This is primarily useful to assist [`ClientCredentialCreationOptions::deserialize`].
+#[derive(Debug)]
+pub struct PublicKeyCredentialCreationOptionsOwned<
+ 'user_name,
+ 'user_display_name,
+ const USER_LEN: usize,
+> {
+ /// See [`PublicKeyCredentialCreationOptions::rp_id`].
+ pub rp_id: RpId,
+ /// See [`PublicKeyCredentialCreationOptions::user`].
+ pub user: PublicKeyCredentialUserEntityOwned<'user_name, 'user_display_name, USER_LEN>,
+ /// See [`PublicKeyCredentialCreationOptions::pub_key_cred_params`].
+ pub pub_key_cred_params: CoseAlgorithmIdentifiers,
+ /// See [`PublicKeyCredentialCreationOptions::timeout`].
+ pub timeout: NonZeroU32,
+ /// See [`PublicKeyCredentialCreationOptions::authenticator_selection`].
+ pub authenticator_selection: AuthenticatorSelectionCriteria,
+ /// See [`PublicKeyCredentialCreationOptions::extensions`].
+ pub extensions: ExtensionOwned,
+}
+impl<const USER_LEN: usize> PublicKeyCredentialCreationOptionsOwned<'_, '_, USER_LEN> {
+ /// Creates a `PublicKeyCredentialCreationOptions` based on the contained data and randomly-generated
+ /// [`Challenge`].
+ #[inline]
+ #[must_use]
+ pub fn into_options(
+ &self,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ ) -> PublicKeyCredentialCreationOptions<'_, '_, '_, '_, '_, '_, USER_LEN> {
+ PublicKeyCredentialCreationOptions {
+ rp_id: &self.rp_id,
+ user: (&self.user).into(),
+ challenge: Challenge::new(),
+ pub_key_cred_params: self.pub_key_cred_params,
+ timeout: self.timeout,
+ exclude_credentials,
+ authenticator_selection: self.authenticator_selection,
+ extensions: (&self.extensions).into(),
+ }
+ }
+}
+impl<'user_name, 'user_display_name, const USER_LEN: usize> Default
+ for PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN>
+where
+ PublicKeyCredentialUserEntityOwned<'user_name, 'user_display_name, USER_LEN>: Default,
+{
+ #[inline]
+ fn default() -> Self {
+ Self {
+ rp_id: DEFAULT_RP_ID,
+ user: PublicKeyCredentialUserEntityOwned::default(),
+ pub_key_cred_params: CoseAlgorithmIdentifiers::default(),
+ timeout: FIVE_MINUTES,
+ authenticator_selection: AuthenticatorSelectionCriteria {
+ authenticator_attachment: AuthenticatorAttachmentReq::default(),
+ resident_key: ResidentKeyRequirement::Discouraged,
+ user_verification: UserVerificationRequirement::Preferred,
+ },
+ extensions: ExtensionOwned::default(),
+ }
+ }
+}
+impl<'de: 'user_name + 'user_display_name, 'user_name, 'user_display_name, const USER_LEN: usize>
+ Deserialize<'de>
+ for PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN>
+where
+ UserHandle<USER_LEN>: Default,
+ PublicKeyCredentialUserEntityOwned<'user_name, 'user_display_name, USER_LEN>: Default,
+{
+ /// Deserializes a `struct` based on
+ /// [`PublicKeyCredentialCreationOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptionsjson).
+ ///
+ /// Note that none of the fields are required, and all are allowed to be `null`.
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment)
+ /// must be consistent with
+ /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints)
+ /// (e.g., if [`"platform"`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-platform) is
+ /// requested, then `hints` must either not exist, be `null`, be empty, or be `["client-device"]`).
+ ///
+ /// If [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-challenge)
+ /// exists, it must be `null`. If
+ /// [`excludeCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-excludecredentials)
+ /// exists, it must be `null` or empty. If
+ /// [`attestation`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-attestation)
+ /// exists, it must be `null`or `"none"`. If
+ /// [`attestationFormats`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-attestationformats)
+ /// exists, it must be `null`, empty, or `["none"]`.
+ ///
+ /// If [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-timeout) exists,
+ /// it must be `null` or positive.
+ ///
+ /// In the event there is no RP ID defined, the value `"example.invalid"` will be used.
+ ///
+ /// For any field that does not exist or is `null`, the corresponding [`Default`] `impl` will be used. For
+ /// [`AuthenticatorSelectionCriteria`], `AuthenticatorAttachmentReq::None(Hint::None)`,
+ /// [`ResidentKeyRequirement::Discouraged`], and [`UserVerificationRequirement::Preferred`] will be used.
+ /// For `timeout`, [`FIVE_MINUTES`] will be used.
+ ///
+ /// Unknown or duplicate fields lead to an error.
+ #[expect(clippy::too_many_lines, reason = "want to keep logic internal")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `PublicKeyCredentialCreationOptionsOwned`.
+ struct PublicKeyCredentialCreationOptionsOwnedVisitor<'a, 'b, const LEN: usize>(
+ PhantomData<fn() -> (&'a (), &'b ())>,
+ );
+ impl<'d: 'a + 'b, 'a, 'b, const LEN: usize> Visitor<'d>
+ for PublicKeyCredentialCreationOptionsOwnedVisitor<'a, 'b, LEN>
+ where
+ UserHandle<LEN>: Default,
+ PublicKeyCredentialUserEntityOwned<'a, 'b, LEN>: Default,
+ {
+ type Value = PublicKeyCredentialCreationOptionsOwned<'a, 'b, LEN>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("PublicKeyCredentialCreationOptionsOwned")
+ }
+ #[expect(clippy::too_many_lines, reason = "want to keep logic internal")]
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field for `PublicKeyCredentialCreationOptionsOwned`.
+ enum Field {
+ /// `rp`.
+ Rp,
+ /// `user`.
+ User,
+ /// `challenge`.
+ Challenge,
+ /// `pubKeyCredParams`.
+ PubKeyCredParams,
+ /// `timeout`.
+ Timeout,
+ /// `excludeCredentials`.
+ ExcludeCredentials,
+ /// `authenticatorSelection`.
+ AuthenticatorSelection,
+ /// `hints`.
+ Hints,
+ /// `extensions`.
+ Extensions,
+ /// `attestation`.
+ Attestation,
+ /// `attestationFormats`.
+ AttestationFormats,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(
+ formatter,
+ "'{RP}', '{USER}', '{CHALLENGE}', '{PUB_KEY_CRED_PARAMS}', '{TIMEOUT}', '{EXCLUDE_CREDENTIALS}', '{AUTHENTICATOR_SELECTION}', '{HINTS}', '{EXTENSIONS}', '{ATTESTATION}', or '{ATTESTATION_FORMATS}'"
+ )
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ RP => Ok(Field::Rp),
+ USER => Ok(Field::User),
+ CHALLENGE => Ok(Field::Challenge),
+ PUB_KEY_CRED_PARAMS => Ok(Field::PubKeyCredParams),
+ TIMEOUT => Ok(Field::Timeout),
+ EXCLUDE_CREDENTIALS => Ok(Field::ExcludeCredentials),
+ AUTHENTICATOR_SELECTION => Ok(Field::AuthenticatorSelection),
+ HINTS => Ok(Field::Hints),
+ EXTENSIONS => Ok(Field::Extensions),
+ ATTESTATION => Ok(Field::Attestation),
+ ATTESTATION_FORMATS => Ok(Field::AttestationFormats),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut rp = None;
+ let mut user_info = None;
+ let mut chall = None;
+ let mut params = None;
+ let mut time = None;
+ let mut exclude = None;
+ let mut auth = None;
+ let mut hint: Option<Hint> = None;
+ let mut ext = None;
+ let mut attest = None;
+ let mut formats = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::Rp => {
+ if rp.is_some() {
+ return Err(Error::duplicate_field(RP));
+ }
+ rp = map
+ .next_value::<Option<PublicKeyCredentialRpEntityHelper>>()
+ .map(|opt| Some(opt.map(|val| val.0)))?;
+ }
+ Field::User => {
+ if user_info.is_some() {
+ return Err(Error::duplicate_field(USER));
+ }
+ user_info = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::Challenge => {
+ if chall.is_some() {
+ return Err(Error::duplicate_field(CHALLENGE));
+ }
+ chall = map.next_value::<Null>().map(Some)?;
+ }
+ Field::PubKeyCredParams => {
+ if params.is_some() {
+ return Err(Error::duplicate_field(PUB_KEY_CRED_PARAMS));
+ }
+ params = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::Timeout => {
+ if time.is_some() {
+ return Err(Error::duplicate_field(TIMEOUT));
+ }
+ time = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::ExcludeCredentials => {
+ if exclude.is_some() {
+ return Err(Error::duplicate_field(EXCLUDE_CREDENTIALS));
+ }
+ exclude = map.next_value::<Option<[(); 0]>>().map(Some)?;
+ }
+ Field::AuthenticatorSelection => {
+ if auth.is_some() {
+ return Err(Error::duplicate_field(AUTHENTICATOR_SELECTION));
+ }
+ auth = map.next_value::<Option<AuthenticatorSelectionCriteria>>().and_then(|opt| {
+ opt.map_or(Ok(Some(AuthenticatorSelectionCriteria { authenticator_attachment: AuthenticatorAttachmentReq::default(), resident_key: ResidentKeyRequirement::Discouraged, user_verification: UserVerificationRequirement::Preferred, })), |mut crit| {
+ let h = hint.unwrap_or_default();
+ match crit.authenticator_attachment {
+ AuthenticatorAttachmentReq::None(ref mut hi) => {
+ *hi = h;
+ Ok(Some(crit))
+ }
+ AuthenticatorAttachmentReq::Platform(ref mut hi) => {
+ match h {
+ Hint::None => Ok(Some(crit)),
+ Hint::ClientDevice => {
+ *hi = PlatformHint::ClientDevice;
+ Ok(Some(crit))
+ }
+ Hint::SecurityKey | Hint::Hybrid | Hint::SecurityKeyClientDevice | Hint::ClientDeviceSecurityKey | Hint::SecurityKeyHybrid | Hint::HybridSecurityKey | Hint::ClientDeviceHybrid | Hint::HybridClientDevice | Hint::SecurityKeyClientDeviceHybrid | Hint::SecurityKeyHybridClientDevice | Hint::ClientDeviceSecurityKeyHybrid | Hint::ClientDeviceHybridSecurityKey | Hint::HybridSecurityKeyClientDevice | Hint::HybridClientDeviceSecurityKey => Err(Error::custom("'platform' authenticator attachment modality must coincide with no hints or 'client-device' hint")),
+ }
+ }
+ AuthenticatorAttachmentReq::CrossPlatform(ref mut hi) => {
+ match h {
+ Hint::None => Ok(Some(crit)),
+ Hint::SecurityKey => {
+ *hi = CrossPlatformHint::SecurityKey;
+ Ok(Some(crit))
+ }
+ Hint::Hybrid => {
+ *hi = CrossPlatformHint::Hybrid;
+ Ok(Some(crit))
+ }
+ Hint::SecurityKeyHybrid => {
+ *hi = CrossPlatformHint::SecurityKeyHybrid;
+ Ok(Some(crit))
+ }
+ Hint::HybridSecurityKey => {
+ *hi = CrossPlatformHint::HybridSecurityKey;
+ Ok(Some(crit))
+ }
+ Hint::ClientDevice | Hint::SecurityKeyClientDevice | Hint::ClientDeviceSecurityKey | Hint::ClientDeviceHybrid | Hint::HybridClientDevice | Hint::SecurityKeyClientDeviceHybrid | Hint::SecurityKeyHybridClientDevice | Hint::ClientDeviceSecurityKeyHybrid | Hint::ClientDeviceHybridSecurityKey | Hint::HybridSecurityKeyClientDevice | Hint::HybridClientDeviceSecurityKey => Err(Error::custom("'cross-platform' authenticator attachment modality must coincide with no hints or hints that lack 'client-device'")),
+ }
+ }
+ }
+ })
+ })?;
+ }
+ Field::Hints => {
+ if hint.is_some() {
+ return Err(Error::duplicate_field(HINTS));
+ }
+ hint = map.next_value::<Option<Hint>>().and_then(|opt| {
+ opt.map_or(Ok(Some(Hint::None)), |h| {
+ auth.as_mut().map_or(Ok(Some(h)), |crit| {
+ match crit.authenticator_attachment {
+ AuthenticatorAttachmentReq::None(ref mut hi) => {
+ *hi = h;
+ Ok(Some(h))
+ }
+ AuthenticatorAttachmentReq::Platform(ref mut hi) => {
+ match h{
+ Hint::None => Ok(Some(h)),
+ Hint::ClientDevice => {
+ *hi = PlatformHint::ClientDevice;
+ Ok(Some(h))
+ }
+ Hint::SecurityKey | Hint::Hybrid | Hint::SecurityKeyClientDevice | Hint::ClientDeviceSecurityKey | Hint::SecurityKeyHybrid | Hint::HybridSecurityKey | Hint::ClientDeviceHybrid | Hint::HybridClientDevice | Hint::SecurityKeyClientDeviceHybrid | Hint::SecurityKeyHybridClientDevice | Hint::ClientDeviceSecurityKeyHybrid | Hint::ClientDeviceHybridSecurityKey | Hint::HybridSecurityKeyClientDevice | Hint::HybridClientDeviceSecurityKey => Err(Error::custom("'platform' authenticator attachment modality must coincide with no hints or 'client-device' hint")),
+ }
+ }
+ AuthenticatorAttachmentReq::CrossPlatform(ref mut hi) => {
+ match h {
+ Hint::None => Ok(Some(h)),
+ Hint::SecurityKey => {
+ *hi = CrossPlatformHint::SecurityKey;
+ Ok(Some(h))
+ }
+ Hint::Hybrid => {
+ *hi = CrossPlatformHint::Hybrid;
+ Ok(Some(h))
+ }
+ Hint::SecurityKeyHybrid => {
+ *hi = CrossPlatformHint::SecurityKeyHybrid;
+ Ok(Some(h))
+ }
+ Hint::HybridSecurityKey => {
+ *hi = CrossPlatformHint::HybridSecurityKey;
+ Ok(Some(h))
+ }
+ Hint::ClientDevice | Hint::SecurityKeyClientDevice | Hint::ClientDeviceSecurityKey | Hint::ClientDeviceHybrid | Hint::HybridClientDevice | Hint::SecurityKeyClientDeviceHybrid | Hint::SecurityKeyHybridClientDevice | Hint::ClientDeviceSecurityKeyHybrid | Hint::ClientDeviceHybridSecurityKey | Hint::HybridSecurityKeyClientDevice | Hint::HybridClientDeviceSecurityKey => Err(Error::custom("'cross-platform' authenticator attachment modality must coincide with no hints or hints that lack 'client-device'")),
+ }
+ }
+ }
+ })
+ })
+ })?;
+ }
+ Field::Extensions => {
+ if ext.is_some() {
+ return Err(Error::duplicate_field(EXTENSIONS));
+ }
+ ext = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::Attestation => {
+ if attest.is_some() {
+ return Err(Error::duplicate_field(ATTESTATION));
+ }
+ attest = map.next_value::<Option<Attestation>>().map(Some)?;
+ }
+ Field::AttestationFormats => {
+ if formats.is_some() {
+ return Err(Error::duplicate_field(ATTESTATION_FORMATS));
+ }
+ formats = map.next_value::<Option<AttestationFormats>>().map(Some)?;
+ }
+ }
+ }
+ Ok(PublicKeyCredentialCreationOptionsOwned {
+ rp_id: rp.flatten().unwrap_or(DEFAULT_RP_ID),
+ user: user_info.flatten().unwrap_or_default(),
+ pub_key_cred_params: params.flatten().unwrap_or_default(),
+ timeout: time.flatten().unwrap_or(FIVE_MINUTES),
+ authenticator_selection: auth.unwrap_or_else(|| {
+ AuthenticatorSelectionCriteria {
+ authenticator_attachment: AuthenticatorAttachmentReq::default(),
+ resident_key: ResidentKeyRequirement::Discouraged,
+ user_verification: UserVerificationRequirement::Preferred,
+ }
+ }),
+ extensions: ext.flatten().unwrap_or_default(),
+ })
+ }
+ }
+ /// Fields for `PublicKeyCredentialCreationOptionsOwned`.
+ const FIELDS: &[&str; 11] = &[
+ RP,
+ USER,
+ CHALLENGE,
+ PUB_KEY_CRED_PARAMS,
+ TIMEOUT,
+ EXCLUDE_CREDENTIALS,
+ AUTHENTICATOR_SELECTION,
+ HINTS,
+ EXTENSIONS,
+ ATTESTATION,
+ ATTESTATION_FORMATS,
+ ];
+ deserializer.deserialize_struct(
+ "PublicKeyCredentialCreationOptionsOwned",
+ FIELDS,
+ PublicKeyCredentialCreationOptionsOwnedVisitor(PhantomData),
+ )
+ }
+}
+/// Deserializes client-supplied data to assist in the creation of [`CredentialCreationOptions`].
+///
+/// It's common to tailor a registration ceremony based on a user's environment. The options that should be
+/// used are then sent to the server. For example, [`CredentialMediationRequirement::Conditional`] ceremonies
+/// typically work best for [`AuthenticatorAttachment::Platform`] authenticators; a subset of which cannot
+/// rely on [`UserVerificationRequirement::Required`]. Unfortunately one may not want to use
+/// [`UserVerificationRequirement::Preferred`] unconditionally either since security keys may benefit from
+/// [`CredProtect::UserVerificationRequired`] which can typically only be used when
+/// [`UserVerificationRequirement::Required`] is requested since many user agents error otherwise.
+///
+/// To facilitate this, [`Self::deserialize`] can be used to deserialize the data sent from the client. Upon
+/// successful deserialization, [`Self::into_options`] can then be used to construct the appropriate
+/// [`CredentialCreationOptions`].
+///
+/// Note one may want to change some of the [`Extension`] data since [`ExtensionInfo::AllowEnforceValue`] and
+/// [`ExtensionReq::Allow`] are unconditionally used. Read [`ExtensionOwned::deserialize`] for more information.
+///
+/// Additionally, one may want to change the value of [`PublicKeyCredentialCreationOptions::rp_id`] since
+/// `"example.invalid"` is used in the event the RP ID was not supplied.
+#[derive(Debug)]
+pub struct ClientCredentialCreationOptions<'user_name, 'user_display_name, const USER_LEN: usize> {
+ /// See [`CredentialCreationOptions::mediation`].
+ pub mediation: CredentialMediationRequirement,
+ /// See [`CredentialCreationOptions::public_key`].
+ pub public_key:
+ PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN>,
+}
+impl<const USER_LEN: usize> ClientCredentialCreationOptions<'_, '_, USER_LEN> {
+ /// Creates a `CredentialCreationOptions` based on the contained data where
+ /// [`CredentialCreationOptions::public_key`] is constructed via
+ /// [`PublicKeyCredentialCreationOptionsOwned::into_options`].
+ #[inline]
+ #[must_use]
+ pub fn into_options(
+ &self,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ ) -> CredentialCreationOptions<'_, '_, '_, '_, '_, '_, USER_LEN> {
+ CredentialCreationOptions {
+ mediation: self.mediation,
+ public_key: self.public_key.into_options(exclude_credentials),
+ }
+ }
+}
+impl<'user_name, 'user_display_name, const USER_LEN: usize> Default
+ for ClientCredentialCreationOptions<'user_name, 'user_display_name, USER_LEN>
+where
+ PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN>: Default,
+{
+ #[inline]
+ fn default() -> Self {
+ Self {
+ mediation: CredentialMediationRequirement::default(),
+ public_key: PublicKeyCredentialCreationOptionsOwned::default(),
+ }
+ }
+}
+impl<'de: 'user_name + 'user_display_name, 'user_name, 'user_display_name, const USER_LEN: usize>
+ Deserialize<'de> for ClientCredentialCreationOptions<'user_name, 'user_display_name, USER_LEN>
+where
+ UserHandle<USER_LEN>: Default,
+ PublicKeyCredentialCreationOptionsOwned<'user_name, 'user_display_name, USER_LEN>: Default,
+{
+ /// Deserializes a `struct` according to the following pseudo-schema:
+ ///
+ /// ```json
+ /// {
+ /// "mediation": null | "required" | "conditional",
+ /// "publicKey": null | <PublicKeyCredentialCreationOptionsOwned>
+ /// }
+ /// ```
+ ///
+ /// where none of the fields are required and `"publicKey"` is deserialized according to
+ /// [`PublicKeyCredentialCreationOptionsOwned::deserialize`]. If any field is missing or is `null`, then
+ /// the corresponding [`Default`] `impl` will be used.
+ ///
+ /// Unknown or duplicate fields lead to an error.
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `ClientCredentialCreationOptions`.
+ struct ClientCredentialCreationOptionsVisitor<'a, 'b, const LEN: usize>(
+ PhantomData<fn() -> (&'a (), &'b ())>,
+ );
+ impl<'d: 'a + 'b, 'a, 'b, const LEN: usize> Visitor<'d>
+ for ClientCredentialCreationOptionsVisitor<'a, 'b, LEN>
+ where
+ UserHandle<LEN>: Default,
+ PublicKeyCredentialCreationOptionsOwned<'a, 'b, LEN>: Default,
+ {
+ type Value = ClientCredentialCreationOptions<'a, 'b, LEN>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("ClientCredentialCreationOptions")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field in `ClientCredentialCreationOptions`.
+ enum Field {
+ /// `mediation`.
+ Mediation,
+ /// `publicKey`
+ PublicKey,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{MEDIATION}' or '{PUBLIC_KEY_NO_HYPEN}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ MEDIATION => Ok(Field::Mediation),
+ PUBLIC_KEY_NO_HYPEN => Ok(Field::PublicKey),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut med = None;
+ let mut key = None;
+ while let Some(k) = map.next_key()? {
+ match k {
+ Field::Mediation => {
+ if med.is_some() {
+ return Err(Error::duplicate_field(MEDIATION));
+ }
+ med = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::PublicKey => {
+ if key.is_some() {
+ return Err(Error::duplicate_field(PUBLIC_KEY_NO_HYPEN));
+ }
+ key = map.next_value::<Option<_>>().map(Some)?;
+ }
+ }
+ }
+ Ok(ClientCredentialCreationOptions {
+ mediation: med.flatten().unwrap_or_default(),
+ public_key: key.flatten().unwrap_or_default(),
+ })
+ }
+ }
+ /// Fields for `ClientCredentialCreationOptions`.
+ const FIELDS: &[&str; 2] = &[MEDIATION, PUBLIC_KEY_NO_HYPEN];
+ deserializer.deserialize_struct(
+ "ClientCredentialCreationOptions",
+ FIELDS,
+ ClientCredentialCreationOptionsVisitor(PhantomData),
+ )
+ }
+}
+#[cfg(test)]
+mod test {
+ use super::{
+ AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria,
+ ClientCredentialCreationOptions, CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers,
+ CredProtect, CredentialMediationRequirement, CrossPlatformHint, DEFAULT_RP_ID,
+ ExtensionInfo, ExtensionOwned, ExtensionReq, FIVE_MINUTES, FourToSixtyThree, Hint,
+ NonZeroU32, PlatformHint, PublicKeyCredentialCreationOptionsOwned,
+ PublicKeyCredentialUserEntityOwned, ResidentKeyRequirement, UserVerificationRequirement,
+ };
+ use serde_json::Error;
+ #[test]
+ fn client_options() -> Result<(), Error> {
+ let mut err =
+ serde_json::from_str::<ClientCredentialCreationOptions<'_, '_, 16>>(r#"{"bob":true}"#)
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..56],
+ *"unknown field `bob`, expected `mediation` or `publicKey`"
+ );
+ err = serde_json::from_str::<ClientCredentialCreationOptions<'_, '_, 1>>(
+ r#"{"mediation":"required","mediation":"required"}"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..27], *"duplicate field `mediation`");
+ let mut options =
+ serde_json::from_str::<ClientCredentialCreationOptions<'_, '_, 1>>(r#"{}"#)?;
+ assert!(matches!(
+ options.mediation,
+ CredentialMediationRequirement::Required
+ ));
+ assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(options.public_key.user.name.as_ref(), "blank");
+ assert!(options.public_key.user.display_name.is_none());
+ assert_eq!(
+ options.public_key.pub_key_cred_params.0,
+ CoseAlgorithmIdentifiers::ALL.0
+ );
+ assert_eq!(options.public_key.timeout, FIVE_MINUTES);
+ assert!(
+ matches!(options.public_key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ options.public_key.authenticator_selection.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ assert!(matches!(
+ options.public_key.authenticator_selection.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(options.public_key.extensions.cred_props.is_none());
+ assert!(matches!(
+ options.public_key.extensions.cred_protect,
+ CredProtect::None
+ ));
+ assert!(options.public_key.extensions.min_pin_length.is_none());
+ assert!(options.public_key.extensions.prf.is_none());
+ options = serde_json::from_str::<ClientCredentialCreationOptions<'_, '_, 1>>(
+ r#"{"mediation":null,"publicKey":null}"#,
+ )?;
+ assert!(matches!(
+ options.mediation,
+ CredentialMediationRequirement::Required
+ ));
+ assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(options.public_key.user.name.as_ref(), "blank");
+ assert!(options.public_key.user.display_name.is_none());
+ assert_eq!(
+ options.public_key.pub_key_cred_params.0,
+ CoseAlgorithmIdentifiers::ALL.0
+ );
+ assert_eq!(options.public_key.timeout, FIVE_MINUTES);
+ assert!(
+ matches!(options.public_key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ options.public_key.authenticator_selection.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ assert!(matches!(
+ options.public_key.authenticator_selection.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(options.public_key.extensions.cred_props.is_none());
+ assert!(matches!(
+ options.public_key.extensions.cred_protect,
+ CredProtect::None
+ ));
+ assert!(options.public_key.extensions.min_pin_length.is_none());
+ assert!(options.public_key.extensions.prf.is_none());
+ options = serde_json::from_str::<ClientCredentialCreationOptions<'_, '_, 1>>(
+ r#"{"publicKey":{}}"#,
+ )?;
+ assert_eq!(options.public_key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(options.public_key.user.name.as_ref(), "blank");
+ assert!(options.public_key.user.display_name.is_none());
+ assert_eq!(
+ options.public_key.pub_key_cred_params.0,
+ CoseAlgorithmIdentifiers::ALL.0
+ );
+ assert_eq!(options.public_key.timeout, FIVE_MINUTES);
+ assert!(
+ matches!(options.public_key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ options.public_key.authenticator_selection.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ assert!(matches!(
+ options.public_key.authenticator_selection.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(options.public_key.extensions.cred_props.is_none());
+ assert!(matches!(
+ options.public_key.extensions.cred_protect,
+ CredProtect::None
+ ));
+ assert!(options.public_key.extensions.min_pin_length.is_none());
+ assert!(options.public_key.extensions.prf.is_none());
+ options = serde_json::from_str::<ClientCredentialCreationOptions<'_, '_, 1>>(
+ r#"{"mediation":"conditional","publicKey":{"rp":{"name":"Example.com","id":"example.com"},"user":{"name":"bob","displayName":"Bob","id":"AA"},"timeout":300000,"excludeCredentials":[],"attestation":"none","attestationFormats":["none"],"authenticatorSelection":{"authenticatorAttachment":"cross-platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"},"extensions":{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":""}}},"pubKeyCredParams":[{"type":"public-key","alg":-8}],"hints":["security-key"],"challenge":null}}"#,
+ )?;
+ assert!(matches!(
+ options.mediation,
+ CredentialMediationRequirement::Conditional
+ ));
+ assert_eq!(options.public_key.rp_id.as_ref(), "example.com");
+ assert_eq!(options.public_key.user.name.as_ref(), "bob");
+ assert!(
+ options
+ .public_key
+ .user
+ .display_name
+ .map_or(false, |name| name.as_ref() == "Bob")
+ );
+ assert_eq!(
+ options.public_key.pub_key_cred_params.0,
+ CoseAlgorithmIdentifiers::ALL
+ .remove(CoseAlgorithmIdentifier::Es256)
+ .remove(CoseAlgorithmIdentifier::Es384)
+ .remove(CoseAlgorithmIdentifier::Rs256)
+ .0
+ );
+ assert_eq!(options.public_key.timeout, FIVE_MINUTES);
+ assert!(
+ matches!(options.public_key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::CrossPlatform(hint) if matches!(hint, CrossPlatformHint::SecurityKey))
+ );
+ assert!(matches!(
+ options.public_key.authenticator_selection.resident_key,
+ ResidentKeyRequirement::Required
+ ));
+ assert!(matches!(
+ options.public_key.authenticator_selection.user_verification,
+ UserVerificationRequirement::Required
+ ));
+ assert!(
+ options
+ .public_key
+ .extensions
+ .cred_props
+ .map_or(false, |req| matches!(req, ExtensionReq::Allow))
+ );
+ assert!(
+ matches!(options.public_key.extensions.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue))
+ );
+ assert!(
+ options
+ .public_key
+ .extensions
+ .min_pin_length
+ .map_or(false, |min| min.0 == FourToSixtyThree::Four
+ && matches!(min.1, ExtensionInfo::AllowEnforceValue))
+ );
+ assert!(
+ options
+ .public_key
+ .extensions
+ .prf
+ .map_or(false, |prf| prf.first.is_empty()
+ && prf.second.is_some_and(|p| p.is_empty())
+ && matches!(prf.ext_req, ExtensionReq::Allow))
+ );
+ Ok(())
+ }
+ #[test]
+ fn key_options() -> Result<(), Error> {
+ let mut err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 16>>(
+ r#"{"bob":true}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..201],
+ *"unknown field `bob`, expected one of `rp`, `user`, `challenge`, `pubKeyCredParams`, `timeout`, `excludeCredentials`, `authenticatorSelection`, `hints`, `extensions`, `attestation`, `attestationFormats`"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"attestation":"none","attestation":"none"}"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..29], *"duplicate field `attestation`");
+ err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"authenticatorSelection":{"authenticatorAttachment":"platform"},"hints":["client-device", "security-key"]}"#,
+ ).unwrap_err();
+ assert_eq!(
+ err.to_string()[..96],
+ *"'platform' authenticator attachment modality must coincide with no hints or 'client-device' hint"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"challenge":"AAAAAAAAAAAAAAAAAAAAAA"}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..41],
+ *"invalid type: Option value, expected null"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"excludeCredentials":[{"type":"public-key","transports":["usb"],"id":"AAAAAAAAAAAAAAAAAAAAAA"}]}"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..19], *"trailing characters");
+ err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"attestation":"foo"}"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..27], *"invalid value: string \"foo\"");
+ err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"attestationFormats":["none","none"]}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..96],
+ *"attestationFormats must be an empty sequence or contain exactly one string whose value is 'none'"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"attestationFormats":["foo"]}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..42],
+ *"invalid value: string \"foo\", expected none"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"timeout":0}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..50],
+ *"invalid value: integer `0`, expected a nonzero u32"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"timeout":4294967296}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..59],
+ *"invalid value: integer `4294967296`, expected a nonzero u32"
+ );
+ let mut key =
+ serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(r#"{}"#)?;
+ assert_eq!(key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(key.user.name.as_ref(), "blank");
+ assert!(key.user.display_name.is_none());
+ assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0);
+ assert_eq!(key.timeout, FIVE_MINUTES);
+ assert!(
+ matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ key.authenticator_selection.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ assert!(matches!(
+ key.authenticator_selection.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(key.extensions.cred_props.is_none());
+ assert!(matches!(key.extensions.cred_protect, CredProtect::None));
+ assert!(key.extensions.min_pin_length.is_none());
+ assert!(key.extensions.prf.is_none());
+ key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"rp":null,"user":null,"timeout":null,"excludeCredentials":null,"attestation":null,"attestationFormats":null,"authenticatorSelection":null,"extensions":null,"pubKeyCredParams":null,"hints":null,"challenge":null}"#,
+ )?;
+ assert_eq!(key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(key.user.name.as_ref(), "blank");
+ assert!(key.user.display_name.is_none());
+ assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0);
+ assert_eq!(key.timeout, FIVE_MINUTES);
+ assert!(
+ matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ key.authenticator_selection.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ assert!(matches!(
+ key.authenticator_selection.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(key.extensions.cred_props.is_none());
+ assert!(matches!(key.extensions.cred_protect, CredProtect::None));
+ assert!(key.extensions.min_pin_length.is_none());
+ assert!(key.extensions.prf.is_none());
+ key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"rp":{},"user":{},"excludeCredentials":[],"attestationFormats":[],"authenticatorSelection":{},"extensions":{},"pubKeyCredParams":[],"hints":[]}"#,
+ )?;
+ assert_eq!(key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(key.user.name.as_ref(), "blank");
+ assert!(key.user.display_name.is_none());
+ assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0);
+ assert!(
+ matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ key.authenticator_selection.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ assert!(matches!(
+ key.authenticator_selection.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(key.extensions.cred_props.is_none());
+ assert!(matches!(key.extensions.cred_protect, CredProtect::None));
+ assert!(key.extensions.min_pin_length.is_none());
+ assert!(key.extensions.prf.is_none());
+ key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"rp":{"name":null,"id":null},"user":{"name":null,"id":null,"displayName":null},"authenticatorSelection":{"residentKey":null,"requireResidentKey":null,"userVerification":null,"authenticatorAttachment":null},"extensions":{"credProps":null,"credentialProtectionPolicy":null,"enforceCredentialProtectionPolicy":null,"minPinLength":null,"prf":null}}"#,
+ )?;
+ assert_eq!(key.rp_id, DEFAULT_RP_ID);
+ assert_eq!(key.user.name.as_ref(), "blank");
+ assert!(key.user.display_name.is_none());
+ assert_eq!(key.pub_key_cred_params.0, CoseAlgorithmIdentifiers::ALL.0);
+ assert!(
+ matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ key.authenticator_selection.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ assert!(matches!(
+ key.authenticator_selection.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ assert!(key.extensions.cred_props.is_none());
+ assert!(matches!(key.extensions.cred_protect, CredProtect::None));
+ assert!(key.extensions.min_pin_length.is_none());
+ assert!(key.extensions.prf.is_none());
+ key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"rp":{"name":"Example.com","id":"example.com"},"user":{"name":"bob","displayName":"Bob","id":"AA"},"timeout":300000,"excludeCredentials":[],"attestation":"none","attestationFormats":["none"],"authenticatorSelection":{"authenticatorAttachment":"cross-platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"},"extensions":{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":""}}},"pubKeyCredParams":[{"type":"public-key","alg":-8}],"hints":["security-key"],"challenge":null}"#,
+ )?;
+ assert_eq!(key.rp_id.as_ref(), "example.com");
+ assert_eq!(key.user.name.as_ref(), "bob");
+ assert!(
+ key.user
+ .display_name
+ .map_or(false, |name| name.as_ref() == "Bob")
+ );
+ assert_eq!(
+ key.pub_key_cred_params.0,
+ CoseAlgorithmIdentifiers::ALL
+ .remove(CoseAlgorithmIdentifier::Es256)
+ .remove(CoseAlgorithmIdentifier::Es384)
+ .remove(CoseAlgorithmIdentifier::Rs256)
+ .0
+ );
+ assert_eq!(key.timeout, FIVE_MINUTES);
+ assert!(
+ matches!(key.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::CrossPlatform(hint) if matches!(hint, CrossPlatformHint::SecurityKey))
+ );
+ assert!(matches!(
+ key.authenticator_selection.resident_key,
+ ResidentKeyRequirement::Required
+ ));
+ assert!(matches!(
+ key.authenticator_selection.user_verification,
+ UserVerificationRequirement::Required
+ ));
+ assert!(
+ key.extensions
+ .cred_props
+ .map_or(false, |req| matches!(req, ExtensionReq::Allow))
+ );
+ assert!(
+ matches!(key.extensions.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue))
+ );
+ assert!(
+ key.extensions
+ .min_pin_length
+ .map_or(false, |min| min.0 == FourToSixtyThree::Four
+ && matches!(min.1, ExtensionInfo::AllowEnforceValue))
+ );
+ assert!(key.extensions.prf.map_or(false, |prf| prf.first.is_empty()
+ && prf.second.is_some_and(|p| p.is_empty())
+ && matches!(prf.ext_req, ExtensionReq::Allow)));
+ key = serde_json::from_str::<PublicKeyCredentialCreationOptionsOwned<'_, '_, 1>>(
+ r#"{"timeout":4294967295}"#,
+ )?;
+ assert_eq!(key.timeout, NonZeroU32::MAX);
+ Ok(())
+ }
+ #[test]
+ fn extension() -> Result<(), Error> {
+ let mut err = serde_json::from_str::<ExtensionOwned>(r#"{"bob":true}"#).unwrap_err();
+ assert_eq!(
+ err.to_string()[..138],
+ *"unknown field `bob`, expected one of `credProps`, `credentialProtectionPolicy`, `enforceCredentialProtectionPolicy`, `minPinLength`, `prf`"
+ );
+ err = serde_json::from_str::<ExtensionOwned>(r#"{"credProps":true,"credProps":true}"#)
+ .unwrap_err();
+ assert_eq!(err.to_string()[..27], *"duplicate field `credProps`");
+ err =
+ serde_json::from_str::<ExtensionOwned>(r#"{"enforceCredentialProtectionPolicy":null}"#)
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..84],
+ *"'enforceCredentialProtectionPolicy' must not exist when 'credentialProtectionPolicy'"
+ );
+ err = serde_json::from_str::<ExtensionOwned>(
+ r#"{"enforceCredentialProtectionPolicy":false,"credentialProtectionPolicy":null}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..103],
+ *"'enforceCredentialProtectionPolicy' must be null or not exist when 'credentialProtectionPolicy' is null"
+ );
+ let mut ext = serde_json::from_str::<ExtensionOwned>(
+ r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":false,"minPinLength":true,"prf":{"eval":{"first":"","second":""}}}"#,
+ )?;
+ assert!(
+ ext.cred_props
+ .map_or(false, |props| matches!(props, ExtensionReq::Allow))
+ );
+ assert!(
+ matches!(ext.cred_protect, CredProtect::UserVerificationRequired(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue))
+ );
+ assert!(
+ ext.min_pin_length
+ .map_or(false, |min| min.0 == FourToSixtyThree::Four
+ && matches!(min.1, ExtensionInfo::AllowEnforceValue))
+ );
+ assert!(ext.prf.map_or(false, |prf| prf.first.is_empty()
+ && prf.second.is_some_and(|v| v.is_empty())
+ && matches!(prf.ext_req, ExtensionReq::Allow)));
+ ext = serde_json::from_str::<ExtensionOwned>(
+ r#"{"credProps":null,"credentialProtectionPolicy":null,"enforceCredentialProtectionPolicy":null,"minPinLength":null,"prf":null}"#,
+ )?;
+ assert!(ext.cred_props.is_none());
+ assert!(matches!(ext.cred_protect, CredProtect::None));
+ assert!(ext.min_pin_length.is_none());
+ assert!(ext.prf.is_none());
+ ext = serde_json::from_str::<ExtensionOwned>(r#"{}"#)?;
+ assert!(ext.cred_props.is_none());
+ assert!(matches!(ext.cred_protect, CredProtect::None));
+ assert!(ext.min_pin_length.is_none());
+ assert!(ext.prf.is_none());
+ ext = serde_json::from_str::<ExtensionOwned>(r#"{"credentialProtectionPolicy":null}"#)?;
+ assert!(matches!(ext.cred_protect, CredProtect::None));
+ ext = serde_json::from_str::<ExtensionOwned>(
+ r#"{"credentialProtectionPolicy":"userVerificationOptional"}"#,
+ )?;
+ assert!(
+ matches!(ext.cred_protect, CredProtect::UserVerificationOptional(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue))
+ );
+ ext = serde_json::from_str::<ExtensionOwned>(
+ r#"{"credentialProtectionPolicy":"userVerificationOptionalWithCredentialIDList","enforceCredentialProtectionPolicy":null}"#,
+ )?;
+ assert!(
+ matches!(ext.cred_protect, CredProtect::UserVerificationOptionalWithCredentialIdList(enforce, info) if !enforce && matches!(info, ExtensionInfo::AllowEnforceValue))
+ );
+ Ok(())
+ }
+ #[test]
+ fn user_entity() -> Result<(), Error> {
+ let mut err = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 16>>(
+ r#"{"bob":true}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..64],
+ *"unknown field `bob`, expected one of `id`, `name`, `displayName`"
+ );
+ err = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 1>>(
+ r#"{"name":"bob","name":"bob"}"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..22], *"duplicate field `name`");
+ let mut user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 1>>(
+ r#"{"id":"AA","name":"bob","displayName":"Bob"}"#,
+ )?;
+ assert_eq!(user.id.as_slice(), [0; 1].as_slice());
+ assert_eq!(user.name.as_ref(), "bob");
+ assert_eq!(user.display_name.as_ref().map(|v| v.as_ref()), Some("Bob"));
+ user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 1>>(
+ r#"{"id":null,"name":null,"displayName":null}"#,
+ )?;
+ assert_eq!(user.name.as_ref(), "blank");
+ assert!(user.display_name.is_none());
+ user = serde_json::from_str::<PublicKeyCredentialUserEntityOwned<'_, '_, 1>>(r#"{}"#)?;
+ assert_eq!(user.name.as_ref(), "blank");
+ assert!(user.display_name.is_none());
+ Ok(())
+ }
+ #[test]
+ fn auth_crit() -> Result<(), Error> {
+ let mut err =
+ serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"null"#).unwrap_err();
+ assert_eq!(
+ err.to_string()[..59],
+ *"invalid type: null, expected AuthenticatorSelectionCriteria"
+ );
+ err = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":"required","requireResidentKey":false}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..62],
+ *"'residentKey' is 'required', but 'requireResidentKey' is false"
+ );
+ err = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":"preferred","requireResidentKey":true}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..65],
+ *"'residentKey' is not 'required', but 'requireResidentKey' is true"
+ );
+ err =
+ serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"residentKey":"prefered"}"#)
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..84],
+ *"invalid value: string \"prefered\", expected 'required', 'discouraged', or 'preferred'"
+ );
+ err =
+ serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{"bob":true}"#).unwrap_err();
+ assert_eq!(
+ err.to_string()[..119],
+ *"unknown field `bob`, expected one of `authenticatorAttachment`, `residentKey`, `requireResidentKey`, `userVerification`"
+ );
+ err = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"requireResidentKey":true,"requireResidentKey":true}"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..36],
+ *"duplicate field `requireResidentKey`"
+ );
+ let mut crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"authenticatorAttachment":"platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"}"#,
+ )?;
+ assert!(
+ matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::Platform(hint) if matches!(hint, PlatformHint::None))
+ );
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Required
+ ));
+ assert!(matches!(
+ crit.user_verification,
+ UserVerificationRequirement::Required
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"authenticatorAttachment":null,"residentKey":null,"requireResidentKey":null,"userVerification":null}"#,
+ )?;
+ assert!(
+ matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ assert!(matches!(
+ crit.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(r#"{}"#)?;
+ assert!(
+ matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ assert!(matches!(
+ crit.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":"preferred","requireResidentKey":false}"#,
+ )?;
+ assert!(
+ matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ );
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Preferred
+ ));
+ assert!(matches!(
+ crit.user_verification,
+ UserVerificationRequirement::Preferred
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":"preferred"}"#,
+ )?;
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Preferred
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"requireResidentKey":true}"#,
+ )?;
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Required
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"requireResidentKey":false}"#,
+ )?;
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":"required"}"#,
+ )?;
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Required
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":"discouraged"}"#,
+ )?;
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":"discouraged","requireResidentKey":null}"#,
+ )?;
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":"required","requireResidentKey":null}"#,
+ )?;
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Required
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":null,"requireResidentKey":true}"#,
+ )?;
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Required
+ ));
+ crit = serde_json::from_str::<AuthenticatorSelectionCriteria>(
+ r#"{"residentKey":null,"requireResidentKey":false}"#,
+ )?;
+ assert!(matches!(
+ crit.resident_key,
+ ResidentKeyRequirement::Discouraged
+ ));
+ Ok(())
+ }
+ #[test]
+ fn cose_algs() -> Result<(), Error> {
+ let mut err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"null"#).unwrap_err();
+ assert_eq!(
+ err.to_string()[..53],
+ *"invalid type: null, expected CoseAlgorithmIdentifiers"
+ );
+ err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[null]"#).unwrap_err();
+ assert_eq!(
+ err.to_string()[..37],
+ *"invalid type: null, expected PubParam"
+ );
+ err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{}]"#).unwrap_err();
+ assert_eq!(err.to_string()[..19], *"missing field `alg`");
+ err = serde_json::from_str::<CoseAlgorithmIdentifiers>(
+ r#"[{"type":"public-key","alg":-7,"foo":true}]"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..45],
+ *"unknown field `foo`, expected `type` or `alg`"
+ );
+ err = serde_json::from_str::<CoseAlgorithmIdentifiers>(
+ r#"[{"type":"public-key","alg":-7,"alg":-7}]"#,
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string()[..21], *"duplicate field `alg`");
+ err = serde_json::from_str::<CoseAlgorithmIdentifiers>(
+ r#"[{"type":"public-key","alg":null}]"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..52],
+ *"invalid type: null, expected CoseAlgorithmIdentifier"
+ );
+ err = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":null,"alg":-8}]"#)
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..39],
+ *"invalid type: null, expected public-key"
+ );
+ err =
+ serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[{"type":"public-key","alg":-6}]"#)
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..58],
+ *"invalid value: integer `-6`, expected -8, -7, -35, or -257"
+ );
+ err = serde_json::from_str::<CoseAlgorithmIdentifiers>(
+ r#"[{"type":"public-key","alg":-7},{"type":"public-key","alg":-7}]"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..49],
+ *"pubKeyCredParams contained duplicate Es256 values"
+ );
+ err = serde_json::from_str::<CoseAlgorithmIdentifiers>(
+ r#"[{"type":"public-key","alg":-7},{"type":"public-key","alg":-8}]"#,
+ )
+ .unwrap_err();
+ assert_eq!(
+ err.to_string()[..63],
+ *"pubKeyCredParams contained EdDSA, but it wasn't the first value"
+ );
+ let mut alg = serde_json::from_str::<CoseAlgorithmIdentifiers>(
+ r#"[{"type":"public-key","alg":-8},{"alg":-7}]"#,
+ )?;
+ assert!(alg.contains(CoseAlgorithmIdentifier::Eddsa));
+ assert!(alg.contains(CoseAlgorithmIdentifier::Es256));
+ assert!(!alg.contains(CoseAlgorithmIdentifier::Es384));
+ assert!(!alg.contains(CoseAlgorithmIdentifier::Rs256));
+ alg = serde_json::from_str::<CoseAlgorithmIdentifiers>(r#"[]"#)?;
+ assert!(alg.contains(CoseAlgorithmIdentifier::Eddsa));
+ assert!(alg.contains(CoseAlgorithmIdentifier::Es256));
+ assert!(alg.contains(CoseAlgorithmIdentifier::Es384));
+ assert!(alg.contains(CoseAlgorithmIdentifier::Rs256));
+ Ok(())
+ }
+}
diff --git a/src/request/register/ser_server_state.rs b/src/request/register/ser_server_state.rs
@@ -293,7 +293,7 @@ impl<const USER_LEN: usize> Encode for RegistrationServerState<USER_LEN> {
}
}
/// Error returned from [`RegistrationServerState::decode`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeRegistrationServerStateErr {
/// Variant returned when there was trailing data after decoding a [`RegistrationServerState`].
TrailingData,
diff --git a/src/request/ser.rs b/src/request/ser.rs
@@ -1,10 +1,20 @@
use super::{
- Challenge, CredentialId, CredentialMediationRequirement, Hint, PrfInput,
- PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement,
+ super::response::ser::Base64DecodedVal, AsciiDomain, AsciiDomainStatic, Challenge,
+ CredentialId, CredentialMediationRequirement, ExtensionReq, Hint, PrfInput,
+ PublicKeyCredentialDescriptor, RpId, Url, UserVerificationRequirement, auth::PrfInputOwned,
};
-use core::str;
-use data_encoding::BASE64URL_NOPAD;
-use serde::ser::{Serialize, SerializeSeq as _, SerializeStruct as _, Serializer};
+use core::{
+ fmt::{self, Formatter},
+ str::FromStr as _,
+};
+use serde::{
+ de::{Deserialize, Deserializer, Error, MapAccess, SeqAccess, Unexpected, Visitor},
+ ser::{Serialize, SerializeSeq as _, SerializeStruct as _, Serializer},
+};
+/// `"required"`.
+const REQUIRED: &str = "required";
+/// `"conditional"`.
+const CONDITIONAL: &str = "conditional";
impl Serialize for CredentialMediationRequirement {
/// Serializes `self` to conform with
/// [`CredentialMediationRequirement`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
@@ -29,8 +39,8 @@ impl Serialize for CredentialMediationRequirement {
S: Serializer,
{
serializer.serialize_str(match *self {
- Self::Conditional => "conditional",
- Self::Required => "required",
+ Self::Required => REQUIRED,
+ Self::Conditional => CONDITIONAL,
})
}
}
@@ -53,12 +63,73 @@ impl Serialize for Challenge {
where
S: Serializer,
{
- serializer.serialize_str(BASE64URL_NOPAD.encode_mut_str(
+ serializer.serialize_str(base64url_nopad::encode_buffer(
self.as_array().as_slice(),
[0; Self::BASE64_LEN].as_mut_slice(),
))
}
}
+impl Serialize for AsciiDomain {
+ /// Serializes `self` as a [`prim@str`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::AsciiDomain;
+ /// assert_eq!(
+ /// serde_json::to_string(&AsciiDomain::try_from("www.example.com".to_owned()).unwrap()).unwrap(),
+ /// r#""www.example.com""#
+ /// );
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.as_ref())
+ }
+}
+impl Serialize for AsciiDomainStatic {
+ /// Serializes `self` as a [`prim@str`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::AsciiDomainStatic;
+ /// assert_eq!(
+ /// serde_json::to_string(&AsciiDomainStatic::new("www.example.com").unwrap()).unwrap(),
+ /// r#""www.example.com""#
+ /// );
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.as_str())
+ }
+}
+impl Serialize for Url {
+ /// Serializes `self` as a [`prim@str`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use core::str::FromStr as _;
+ /// # use webauthn_rp::request::Url;
+ /// assert_eq!(
+ /// serde_json::to_string(&Url::from_str("ssh:foo").unwrap()).unwrap(),
+ /// r#""ssh:foo""#
+ /// );
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.as_ref())
+ }
+}
impl Serialize for RpId {
/// Serializes `self` as a [`prim@str`].
///
@@ -172,6 +243,12 @@ impl Serialize for UserVerificationRequirement {
})
}
}
+/// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-security-key).
+const SECURITY_KEY: &str = "security-key";
+/// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-client-device).
+const CLIENT_DEVICE: &str = "client-device";
+/// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid).
+const HYBRID: &str = "hybrid";
impl Serialize for Hint {
/// Serializes `self` to conform with
/// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints).
@@ -251,12 +328,6 @@ impl Serialize for Hint {
where
S: Serializer,
{
- /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-security-key).
- const SECURITY_KEY: &str = "security-key";
- /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-client-device).
- const CLIENT_DEVICE: &str = "client-device";
- /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid).
- const HYBRID: &str = "hybrid";
let count = match *self {
Self::None => 0,
Self::SecurityKey | Self::ClientDevice | Self::Hybrid => 1,
@@ -338,6 +409,10 @@ impl Serialize for Hint {
})
}
}
+/// `"first"`.
+const FIRST: &str = "first";
+/// `"second"`.
+const SECOND: &str = "second";
impl Serialize for PrfInput<'_, '_> {
/// Serializes `self` to conform with
/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
@@ -368,14 +443,14 @@ impl Serialize for PrfInput<'_, '_> {
// 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())
+ 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(),
+ SECOND,
+ base64url_nopad::encode(second).as_str(),
)
})
.and_then(|()| ser.end())
@@ -383,3 +458,492 @@ impl Serialize for PrfInput<'_, '_> {
})
}
}
+impl<'de> Deserialize<'de> for PrfInputOwned {
+ /// Deserializes a `struct` based on
+ /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
+ ///
+ /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is required and
+ /// must not be `null`.
+ /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) is not required
+ /// and can be `null`.
+ ///
+ /// Note [`PrfInputOwned::ext_req`] is set to [`ExtensionReq::Allow`].
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `PrfInputOwned`.
+ struct PrfInputOwnedVisitor;
+ impl<'d> Visitor<'d> for PrfInputOwnedVisitor {
+ type Value = PrfInputOwned;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("PrfInputOwned")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Field for `PrfInputOwned`.
+ enum Field {
+ /// `first`.
+ First,
+ /// `second`.
+ Second,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{FIRST}' or '{SECOND}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ FIRST => Ok(Field::First),
+ SECOND => Ok(Field::Second),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut fst = None;
+ let mut snd = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::First => {
+ if fst.is_some() {
+ return Err(Error::duplicate_field(FIRST));
+ }
+ fst = map
+ .next_value::<Base64DecodedVal>()
+ .map(|val| Some(val.0))?;
+ }
+ Field::Second => {
+ if snd.is_some() {
+ return Err(Error::duplicate_field(SECOND));
+ }
+ snd = map
+ .next_value::<Option<Base64DecodedVal>>()
+ .map(|opt| Some(opt.map(|val| val.0)))?;
+ }
+ }
+ }
+ fst.ok_or_else(|| Error::missing_field(FIRST))
+ .map(|first| PrfInputOwned {
+ first,
+ second: snd.flatten(),
+ ext_req: ExtensionReq::Allow,
+ })
+ }
+ }
+ const FIELDS: &[&str; 2] = &[FIRST, SECOND];
+ deserializer.deserialize_struct("PrfInputOwned", FIELDS, PrfInputOwnedVisitor)
+ }
+}
+impl<'de> Deserialize<'de> for AsciiDomain {
+ /// Deserializes [`String`] based on [`Self::try_from`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::AsciiDomain;
+ /// assert!(matches!(
+ /// serde_json::from_str::<AsciiDomain>(r#""example.com""#)?.as_ref(),
+ /// "example.com"
+ /// ));
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ String::deserialize(deserializer).and_then(|dom| Self::try_from(dom).map_err(Error::custom))
+ }
+}
+impl Deserialize<'static> for AsciiDomainStatic {
+ /// Deserializes [`prim@str`] based on [`Self::new`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::AsciiDomainStatic;
+ /// assert!(matches!(
+ /// serde_json::from_str::<AsciiDomainStatic>(r#""example.com""#)?.as_str(),
+ /// "example.com"
+ /// ));
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'static>,
+ {
+ <&'static str>::deserialize(deserializer).and_then(|dom| {
+ Self::new(dom)
+ .ok_or_else(|| Error::custom("AsciiDomainStatic requires a valid ASCII domain"))
+ })
+ }
+}
+impl<'de> Deserialize<'de> for Url {
+ /// Deserializes [`prim@str`] based on [`Self::from_str`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::Url;
+ /// assert!(matches!(
+ /// serde_json::from_str::<Url>(r#""ssh:foo""#)?.as_ref(),
+ /// "ssh:foo"
+ /// ));
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `Url`
+ struct UrlVisitor;
+ impl Visitor<'_> for UrlVisitor {
+ type Value = Url;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("Url")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Url::from_str(v).map_err(E::custom)
+ }
+ }
+ deserializer.deserialize_str(UrlVisitor)
+ }
+}
+impl<'de> Deserialize<'de> for RpId {
+ /// Deserializes a [`String`] based on [`Self::try_from`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::RpId;
+ /// assert!(matches!(
+ /// serde_json::from_str::<RpId>(r#""example.com""#)?.as_ref(),
+ /// "example.com"
+ /// ));
+ /// assert!(matches!(
+ /// serde_json::from_str::<RpId>(r#""ssh:foo""#)?.as_ref(),
+ /// "ssh:foo"
+ /// ));
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ String::deserialize(deserializer).and_then(|dom| Self::try_from(dom).map_err(Error::custom))
+ }
+}
+/// Helper for `Hint::deserialize`.
+enum HintHelper {
+ /// `"security-key"`
+ SecurityKey,
+ /// `"client-device"`
+ ClientDevice,
+ /// `"hybrid"`
+ Hybrid,
+}
+impl<'de> Deserialize<'de> for HintHelper {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `HintHelper`.
+ struct HintHelperVisitor;
+ impl Visitor<'_> for HintHelperVisitor {
+ type Value = HintHelper;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(
+ formatter,
+ "'{SECURITY_KEY}', '{CLIENT_DEVICE}', or '{HYBRID}'"
+ )
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ SECURITY_KEY => Ok(HintHelper::SecurityKey),
+ CLIENT_DEVICE => Ok(HintHelper::ClientDevice),
+ HYBRID => Ok(HintHelper::Hybrid),
+ _ => Err(E::invalid_value(
+ Unexpected::Str(v),
+ &format!("'{SECURITY_KEY}', '{CLIENT_DEVICE}', or '{HYBRID}'").as_str(),
+ )),
+ }
+ }
+ }
+ deserializer.deserialize_str(HintHelperVisitor)
+ }
+}
+impl<'de> Deserialize<'de> for Hint {
+ /// Deserializes a sequence based on
+ /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints).
+ ///
+ /// Note duplicates and unknown values will lead to an error.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::Hint;
+ /// assert!(
+ /// matches!(
+ /// serde_json::from_str(r#"["security-key", "hybrid", "client-device"]"#)?,
+ /// Hint::SecurityKeyHybridClientDevice
+ /// )
+ /// );
+ /// assert!(
+ /// matches!(
+ /// serde_json::from_str(r#"["hybrid", "security-key", "client-device"]"#)?,
+ /// Hint::HybridSecurityKeyClientDevice
+ /// )
+ /// );
+ /// assert!(
+ /// matches!(
+ /// serde_json::from_str(r#"[]"#)?,
+ /// Hint::None
+ /// )
+ /// );
+ /// assert!(
+ /// serde_json::from_str::<Hint>(r#"["security-key", "hybrid", "hybrid"]"#).is_err()
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `Hint`.
+ struct HintVisitor;
+ impl<'d> Visitor<'d> for HintVisitor {
+ type Value = Hint;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("unique sequence of hints")
+ }
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'d>,
+ {
+ let mut hint = Hint::None;
+ while let Some(elem) = seq.next_element::<HintHelper>()? {
+ hint = match elem {
+ HintHelper::SecurityKey => match hint {
+ Hint::None => Hint::SecurityKey,
+ Hint::SecurityKey
+ | Hint::SecurityKeyClientDevice
+ | Hint::ClientDeviceSecurityKey
+ | Hint::SecurityKeyHybrid
+ | Hint::HybridSecurityKey
+ | Hint::SecurityKeyClientDeviceHybrid
+ | Hint::SecurityKeyHybridClientDevice
+ | Hint::ClientDeviceSecurityKeyHybrid
+ | Hint::ClientDeviceHybridSecurityKey
+ | Hint::HybridSecurityKeyClientDevice
+ | Hint::HybridClientDeviceSecurityKey => {
+ return Err(Error::custom(format!(
+ "'{SECURITY_KEY}' hint appeared more than once"
+ )));
+ }
+ Hint::ClientDevice => Hint::ClientDeviceSecurityKey,
+ Hint::Hybrid => Hint::HybridSecurityKey,
+ Hint::ClientDeviceHybrid => Hint::ClientDeviceHybridSecurityKey,
+ Hint::HybridClientDevice => Hint::HybridClientDeviceSecurityKey,
+ },
+ HintHelper::ClientDevice => match hint {
+ Hint::None => Hint::ClientDevice,
+ Hint::SecurityKey => Hint::SecurityKeyClientDevice,
+ Hint::ClientDevice
+ | Hint::ClientDeviceSecurityKey
+ | Hint::SecurityKeyClientDevice
+ | Hint::ClientDeviceHybrid
+ | Hint::HybridClientDevice
+ | Hint::ClientDeviceSecurityKeyHybrid
+ | Hint::ClientDeviceHybridSecurityKey
+ | Hint::SecurityKeyClientDeviceHybrid
+ | Hint::SecurityKeyHybridClientDevice
+ | Hint::HybridClientDeviceSecurityKey
+ | Hint::HybridSecurityKeyClientDevice => {
+ return Err(Error::custom(format!(
+ "'{CLIENT_DEVICE}' hint appeared more than once"
+ )));
+ }
+ Hint::Hybrid => Hint::HybridClientDevice,
+ Hint::SecurityKeyHybrid => Hint::SecurityKeyHybridClientDevice,
+ Hint::HybridSecurityKey => Hint::HybridSecurityKeyClientDevice,
+ },
+ HintHelper::Hybrid => match hint {
+ Hint::None => Hint::Hybrid,
+ Hint::Hybrid
+ | Hint::HybridClientDevice
+ | Hint::ClientDeviceHybrid
+ | Hint::HybridSecurityKey
+ | Hint::SecurityKeyHybrid
+ | Hint::HybridClientDeviceSecurityKey
+ | Hint::HybridSecurityKeyClientDevice
+ | Hint::ClientDeviceHybridSecurityKey
+ | Hint::ClientDeviceSecurityKeyHybrid
+ | Hint::SecurityKeyHybridClientDevice
+ | Hint::SecurityKeyClientDeviceHybrid => {
+ return Err(Error::custom(format!(
+ "'{HYBRID}' hint appeared more than once"
+ )));
+ }
+ Hint::ClientDevice => Hint::ClientDeviceHybrid,
+ Hint::SecurityKey => Hint::SecurityKeyHybrid,
+ Hint::ClientDeviceSecurityKey => Hint::ClientDeviceSecurityKeyHybrid,
+ Hint::SecurityKeyClientDevice => Hint::SecurityKeyClientDeviceHybrid,
+ },
+ };
+ }
+ Ok(hint)
+ }
+ }
+ deserializer.deserialize_seq(HintVisitor)
+ }
+}
+impl<'de> Deserialize<'de> for CredentialMediationRequirement {
+ /// Deserializes a [`prim@str`] based on
+ /// [`CredentialMediationRequirement`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::CredentialMediationRequirement;
+ /// assert!(
+ /// matches!(
+ /// serde_json::from_str(r#""required""#)?,
+ /// CredentialMediationRequirement::Required,
+ /// )
+ /// );
+ /// assert!(
+ /// matches!(
+ /// serde_json::from_str(r#""conditional""#)?,
+ /// CredentialMediationRequirement::Conditional,
+ /// )
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `CredentialMediationRequirement`.
+ struct CredentialMediationRequirementVisitor;
+ impl Visitor<'_> for CredentialMediationRequirementVisitor {
+ type Value = CredentialMediationRequirement;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{REQUIRED}' or '{CONDITIONAL}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ REQUIRED => Ok(CredentialMediationRequirement::Required),
+ CONDITIONAL => Ok(CredentialMediationRequirement::Conditional),
+ _ => Err(E::invalid_value(
+ Unexpected::Str(v),
+ &format!("'{REQUIRED}' or '{CONDITIONAL}'").as_str(),
+ )),
+ }
+ }
+ }
+ deserializer.deserialize_str(CredentialMediationRequirementVisitor)
+ }
+}
+/// Helper to deserialize the prf extension.
+pub(super) struct PrfHelper(pub PrfInputOwned);
+impl<'e> Deserialize<'e> for PrfHelper {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `PrfHelper`.
+ struct PrfHelperVisitor;
+ impl<'f> Visitor<'f> for PrfHelperVisitor {
+ type Value = PrfHelper;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("Prf")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'f>,
+ {
+ /// Field for `PrfHelper`.
+ struct Field;
+ impl<'g> Deserialize<'g> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'g>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{EVAL}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v == EVAL {
+ Ok(Field)
+ } else {
+ Err(E::unknown_field(v, FIELDS))
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ map.next_key::<Field>().and_then(|opt_key| {
+ opt_key
+ .ok_or_else(|| Error::missing_field(EVAL))
+ .and_then(|_k| {
+ map.next_value().and_then(|prf_input| {
+ map.next_key::<Field>().and_then(|opt_key2| {
+ opt_key2.map_or_else(
+ || Ok(PrfHelper(prf_input)),
+ |_k2| Err(Error::duplicate_field(EVAL)),
+ )
+ })
+ })
+ })
+ })
+ }
+ }
+ /// `"eval"`.
+ const EVAL: &str = "eval";
+ /// Fields for `PrfHelper`
+ const FIELDS: &[&str; 1] = &[EVAL];
+ deserializer.deserialize_struct("Prf", FIELDS, PrfHelperVisitor)
+ }
+}
+/// Default RP ID to use containing the value `"example.invalid"` when an RP ID is not sent.
+pub(super) const DEFAULT_RP_ID: RpId =
+ RpId::StaticDomain(AsciiDomainStatic::new("example.invalid").unwrap());
diff --git a/src/response.rs b/src/response.rs
@@ -19,7 +19,6 @@ use core::{
hash::{Hash, Hasher},
str,
};
-use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{digest::OutputSizeUser as _, Sha256};
#[cfg(feature = "serde_relaxed")]
use ser_relaxed::SerdeJsonErr;
@@ -30,17 +29,15 @@ use ser_relaxed::SerdeJsonErr;
///
/// ```no_run
/// # use core::convert;
-/// # use data_encoding::BASE64URL_NOPAD;
/// # use webauthn_rp::{
/// # hash::hash_set::FixedCapHashSet,
-/// # request::{auth::{error::InvalidTimeout, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, AsciiDomainStatic, BackupReq, RpId},
+/// # request::{auth::{error::InvalidTimeout, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, BackupReq, RpId},
/// # response::{auth::{error::AuthCeremonyErr, DiscoverableAuthentication64}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKeyOwned, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId},
/// # AuthenticatedCredential, CredentialErr
/// # };
/// # #[derive(Debug)]
/// # enum E {
/// # CollectedClientData(CollectedClientDataErr),
-/// # RpId(AsciiDomainErr),
/// # InvalidTimeout(InvalidTimeout),
/// # SerdeJson(serde_json::Error),
/// # MissingUserHandle,
@@ -49,11 +46,6 @@ use ser_relaxed::SerdeJsonErr;
/// # Credential(CredentialErr),
/// # AuthCeremony(AuthCeremonyErr),
/// # }
-/// # impl From<AsciiDomainErr> for E {
-/// # fn from(value: AsciiDomainErr) -> Self {
-/// # Self::RpId(value)
-/// # }
-/// # }
/// # impl From<CollectedClientDataErr> for E {
/// # fn from(value: CollectedClientDataErr) -> Self {
/// # Self::CollectedClientData(value)
@@ -79,7 +71,7 @@ use ser_relaxed::SerdeJsonErr;
/// # Self::AuthCeremony(value)
/// # }
/// # }
-/// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+/// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap();
/// let mut ceremonies = FixedCapHashSet::new(128);
/// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?;
/// assert!(
@@ -101,7 +93,7 @@ use ser_relaxed::SerdeJsonErr;
/// # #[cfg(feature = "serde")]
/// fn get_authentication_json(client: DiscoverableAuthenticationClientState<'_, '_, '_>) -> String {
/// // ⋮
-/// # let client_data_json = BASE64URL_NOPAD.encode(serde_json::json!({
+/// # let client_data_json = base64url_nopad::encode(serde_json::json!({
/// # "type": "webauthn.get",
/// # "challenge": client.options().public_key.challenge,
/// # "origin": format!("https://{}", client.options().public_key.rp_id.as_ref()),
@@ -153,27 +145,20 @@ pub mod error;
///
/// ```no_run
/// # use core::convert;
-/// # use data_encoding::BASE64URL_NOPAD;
/// # use webauthn_rp::{
/// # hash::hash_set::FixedCapHashSet,
-/// # request::{register::{error::CreationOptionsErr, CredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomainStatic, PublicKeyCredentialDescriptor, RpId},
+/// # request::{register::{error::CreationOptionsErr, CredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, PublicKeyCredentialDescriptor, RpId},
/// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData},
/// # RegisteredCredential
/// # };
/// # #[derive(Debug)]
/// # enum E {
/// # CollectedClientData(CollectedClientDataErr),
-/// # RpId(AsciiDomainErr),
/// # CreationOptions(CreationOptionsErr),
/// # SerdeJson(serde_json::Error),
/// # MissingCeremony,
/// # RegCeremony(RegCeremonyErr),
/// # }
-/// # impl From<AsciiDomainErr> for E {
-/// # fn from(value: AsciiDomainErr) -> Self {
-/// # Self::RpId(value)
-/// # }
-/// # }
/// # impl From<CollectedClientDataErr> for E {
/// # fn from(value: CollectedClientDataErr) -> Self {
/// # Self::CollectedClientData(value)
@@ -194,7 +179,7 @@ pub mod error;
/// # Self::RegCeremony(value)
/// # }
/// # }
-/// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
+/// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap();
/// # #[cfg(feature = "custom")]
/// let mut ceremonies = FixedCapHashSet::new(128);
/// # #[cfg(feature = "custom")]
@@ -239,7 +224,7 @@ pub mod error;
/// # #[cfg(feature = "serde")]
/// fn get_registration_json(client: RegistrationClientState<'_, '_, '_, '_, '_, '_, USER_HANDLE_MAX_LEN>) -> String {
/// // ⋮
-/// # let client_data_json = BASE64URL_NOPAD.encode(serde_json::json!({
+/// # let client_data_json = base64url_nopad::encode(serde_json::json!({
/// # "type": "webauthn.create",
/// # "challenge": client.options().public_key.challenge,
/// # "origin": format!("https://{}", client.options().public_key.rp_id.as_ref()),
@@ -272,7 +257,7 @@ pub mod register;
/// Contains functionality to (de)serialize data to/from a client.
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
#[cfg(feature = "serde")]
-mod ser;
+pub(crate) mod ser;
/// Contains functionality to deserialize data from a client in a "relaxed" way.
#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
#[cfg(feature = "serde_relaxed")]
@@ -301,7 +286,7 @@ impl PartialEq<Backup> for &Backup {
}
}
/// [`AuthenticatorTransport`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthenticatorTransport {
/// [`ble`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-ble).
Ble,
@@ -1054,7 +1039,6 @@ impl<const R: bool> LimitedVerificationParser<R> {
}
impl<const R: bool> ClientDataJsonParser for LimitedVerificationParser<R> {
type Err = CollectedClientDataErr;
- #[expect(clippy::panic_in_result_fn, reason = "want to crash when there is a bug")]
#[expect(clippy::little_endian_bytes, reason = "Challenge::serialize and this need to be consistent across architectures")]
#[expect(clippy::too_many_lines, reason = "110 lines is fine")]
fn parse(json: &[u8]) -> Result<CollectedClientData<'_>, Self::Err> {
@@ -1102,8 +1086,7 @@ impl<const R: bool> ClientDataJsonParser for LimitedVerificationParser<R> {
if chall_key == AFTER_TYPE {
chall_key_rem.split_at_checked(Challenge::BASE64_LEN).ok_or(CollectedClientDataErr::Len).and_then(|(base64_chall, base64_chall_rem)| {
let mut chall = [0; 16];
- BASE64URL_NOPAD.decode_mut(base64_chall, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).and_then(|chall_len| {
- assert_eq!(chall_len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut");
+ base64url_nopad::decode_buffer_exact(base64_chall, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).and_then(|()| {
base64_chall_rem.split_at_checked(AFTER_CHALLENGE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(origin_key, origin_key_rem)| {
if origin_key == AFTER_CHALLENGE {
Self::parse_string(origin_key_rem).and_then(|(origin, origin_rem)| {
@@ -1180,7 +1163,6 @@ impl<const R: bool> ClientDataJsonParser for LimitedVerificationParser<R> {
}
})
}
- #[expect(clippy::panic_in_result_fn, reason = "want to crash when there is a bug")]
#[expect(clippy::arithmetic_side_effects, reason = "comment justifies correctness")]
#[expect(clippy::little_endian_bytes, reason = "Challenge::serialize and this need to be consistent across architectures")]
fn get_sent_challenge(json: &[u8]) -> Result<SentChallenge, Self::Err> {
@@ -1192,8 +1174,7 @@ impl<const R: bool> ClientDataJsonParser for LimitedVerificationParser<R> {
// This maxes at 39 + 22 = 61; thus overflow is not an issue.
json.get(idx..idx + Challenge::BASE64_LEN).ok_or(CollectedClientDataErr::Len).and_then(|chall_slice| {
let mut chall = [0; 16];
- BASE64URL_NOPAD.decode_mut(chall_slice, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).map(|len| {
- assert_eq!(len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut");
+ base64url_nopad::decode_buffer_exact(chall_slice, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).map(|()| {
SentChallenge(u128::from_le_bytes(chall))
})
})
diff --git a/src/response/auth.rs b/src/response/auth.rs
@@ -55,7 +55,7 @@ pub mod ser_relaxed;
///
/// Note while many authenticators that implement `prf` don't require `prf` to have been sent during registration
/// (i.e., [`register::Extension::prf`]), it is recommended to do so for those authenticators that do require it.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HmacSecret {
/// No `hmac-secret` response.
///
diff --git a/src/response/auth/error.rs b/src/response/auth/error.rs
@@ -32,7 +32,7 @@ use core::{
fmt::{self, Display, Formatter},
};
/// Error returned in [`AuthenticatorDataErr::AuthenticatorExtension`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthenticatorExtensionOutputErr {
/// The `slice` had an invalid length.
Len,
@@ -64,7 +64,7 @@ impl Display for AuthenticatorExtensionOutputErr {
}
impl Error for AuthenticatorExtensionOutputErr {}
/// Error returned from [`AuthenticatorData::try_from`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthenticatorDataErr {
/// The `slice` had an invalid length.
Len,
@@ -132,7 +132,7 @@ impl Display for AuthenticatorDataErr {
}
impl Error for AuthenticatorDataErr {}
/// One or two.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OneOrTwo {
/// One.
One,
@@ -149,7 +149,7 @@ impl Display for OneOrTwo {
}
}
/// Error in [`AuthCeremonyErr::Extension`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExtensionErr {
/// [`AuthenticatorExtensionOutput::hmac_secret`] was sent from the client, but [`Flag::user_verified`]
/// was `false`.
@@ -311,7 +311,7 @@ impl Error for AuthCeremonyErr {}
/// This can be sent to the client when an authentication ceremony fails due to an unknown [`CredentialId`]. This
/// can be due to the user deleting a credential on the RP's side but not deleting it on the authenticator. This
/// response can be forwarded to the authenticator which can subsequently delete the credential.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UnknownCredentialOptions<'rp, 'cred> {
/// [`rpId`](https://www.w3.org/TR/webauthn-3/#dictdef-unknowncredentialoptions-rpid).
pub rp_id: &'rp RpId,
@@ -319,7 +319,7 @@ pub struct UnknownCredentialOptions<'rp, 'cred> {
pub credential_id: CredentialId<&'cred [u8]>,
}
/// Error when a [`UserHandle`] does not exist that is required to.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MissingUserHandleErr;
impl Display for MissingUserHandleErr {
#[inline]
diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs
@@ -2,7 +2,7 @@ use super::{
super::{
super::response::ser::{Base64DecodedVal, PublicKeyCredential},
ser::{
- self, AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues,
+ AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues,
ClientExtensions,
},
},
@@ -16,7 +16,6 @@ use core::{
marker::PhantomData,
str,
};
-use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Sha256, digest::OutputSizeUser as _};
use serde::{
de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor},
@@ -37,10 +36,6 @@ impl<'e> Deserialize<'e> for AuthData {
formatter.write_str("AuthenticatorData")
}
#[expect(
- clippy::panic_in_result_fn,
- reason = "we want to crash when there is a bug"
- )]
- #[expect(
clippy::arithmetic_side_effects,
reason = "comment justifies its correctness"
)]
@@ -48,7 +43,7 @@ impl<'e> Deserialize<'e> for AuthData {
where
E: Error,
{
- ser::base64url_nopad_decode_len(v.len())
+ base64url_nopad::decode_len(v.len())
.ok_or_else(|| E::invalid_value(Unexpected::Str(v), &"base64url-encoded value"))
.and_then(|len| {
// The decoded length is 3/4 of the encoded length, so overflow could only occur
@@ -58,16 +53,9 @@ impl<'e> Deserialize<'e> for AuthData {
// raw authenticator data by `AuthenticatorDataAssertion::new`.
let mut auth_data = vec![0; len + Sha256::output_size()];
auth_data.truncate(len);
- BASE64URL_NOPAD
- .decode_mut(v.as_bytes(), auth_data.as_mut_slice())
- .map_err(|e| E::custom(e.error))
- .map(|dec_len| {
- assert_eq!(
- len, dec_len,
- "there is a bug in BASE64URL_NOPAD::decode_mut"
- );
- AuthData(auth_data)
- })
+ base64url_nopad::decode_buffer_exact(v.as_bytes(), auth_data.as_mut_slice())
+ .map_err(E::custom)
+ .map(|()| AuthData(auth_data))
})
}
}
@@ -462,7 +450,6 @@ mod tests {
super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment,
DiscoverableAuthentication, NonDiscoverableAuthentication,
};
- use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Digest as _, Sha256};
use serde::de::{Error as _, Unexpected};
use serde_json::Error;
@@ -511,10 +498,10 @@ mod tests {
0,
0,
];
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(auth_data.as_slice());
- let b64_sig = BASE64URL_NOPAD.encode([].as_slice());
- let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(auth_data.as_slice());
+ let b64_sig = base64url_nopad::encode([].as_slice());
+ let b64_user = base64url_nopad::encode(b"\x00".as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
@@ -547,8 +534,7 @@ mod tests {
// `id` and `rawId` mismatch.
let mut err = Error::invalid_value(
Unexpected::Bytes(
- BASE64URL_NOPAD
- .decode("ABABABABABABABABABABAA".as_bytes())
+ base64url_nopad::decode("ABABABABABABABABABABAA".as_bytes())
.unwrap()
.as_slice(),
),
@@ -1311,10 +1297,10 @@ mod tests {
0,
0,
];
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(auth_data.as_slice());
- let b64_sig = BASE64URL_NOPAD.encode([].as_slice());
- let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(auth_data.as_slice());
+ let b64_sig = base64url_nopad::encode([].as_slice());
+ let b64_user = base64url_nopad::encode(b"\x00".as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<DiscoverableAuthentication<USER_HANDLE_MIN_LEN>>(
diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs
@@ -19,8 +19,6 @@ use core::{
fmt::{self, Formatter},
marker::PhantomData,
};
-#[cfg(doc)]
-use data_encoding::BASE64URL_NOPAD;
use serde::de::{Deserialize, Deserializer, Error, MapAccess, Visitor};
/// `newtype` around `ClientExtensionsOutputs` with a "relaxed" [`Self::deserialize`] implementation.
struct ClientExtensionsOutputsRelaxed(pub ClientExtensionsOutputs);
@@ -467,7 +465,6 @@ mod tests {
DiscoverableAuthenticationRelaxed, DiscoverableCustomAuthentication,
NonDiscoverableAuthenticationRelaxed, NonDiscoverableCustomAuthentication,
};
- use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Digest as _, Sha256};
use serde::de::{Error as _, Unexpected};
use serde_json::Error;
@@ -516,10 +513,10 @@ mod tests {
0,
0,
];
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(auth_data.as_slice());
- let b64_sig = BASE64URL_NOPAD.encode([].as_slice());
- let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(auth_data.as_slice());
+ let b64_sig = base64url_nopad::encode([].as_slice());
+ let b64_user = base64url_nopad::encode(b"\x00".as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
@@ -552,8 +549,7 @@ mod tests {
// `id` and `rawId` mismatch.
let mut err = Error::invalid_value(
Unexpected::Bytes(
- BASE64URL_NOPAD
- .decode("ABABABABABABABABABABAA".as_bytes())
+ base64url_nopad::decode("ABABABABABABABABABABAA".as_bytes())
.unwrap()
.as_slice(),
),
@@ -1740,10 +1736,10 @@ mod tests {
0,
0,
];
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(auth_data.as_slice());
- let b64_sig = BASE64URL_NOPAD.encode([].as_slice());
- let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(auth_data.as_slice());
+ let b64_sig = base64url_nopad::encode([].as_slice());
+ let b64_user = base64url_nopad::encode(b"\x00".as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<DiscoverableAuthenticationRelaxed<USER_HANDLE_MIN_LEN>>(
diff --git a/src/response/bin.rs b/src/response/bin.rs
@@ -37,7 +37,7 @@ impl<'a> DecodeBuffer<'a> for Backup {
}
}
/// Error returned from [`AuthTransports::decode`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DecodeAuthTransportsErr;
impl Display for DecodeAuthTransportsErr {
#[inline]
diff --git a/src/response/error.rs b/src/response/error.rs
@@ -1,7 +1,7 @@
extern crate alloc;
+use super::{CRED_ID_MAX_LEN, CRED_ID_MIN_LEN};
#[cfg(doc)]
use super::{Challenge, CollectedClientData, CredentialId};
-use super::{CRED_ID_MAX_LEN, CRED_ID_MIN_LEN};
use alloc::string::FromUtf8Error;
use core::{
error::Error,
@@ -10,7 +10,7 @@ use core::{
};
/// Error returned when a [`CredentialId`] does not have length inclusively between [`CRED_ID_MIN_LEN`] and
/// [`CRED_ID_MAX_LEN`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CredentialIdErr;
impl Display for CredentialIdErr {
#[inline]
@@ -23,7 +23,7 @@ impl Display for CredentialIdErr {
}
impl Error for CredentialIdErr {}
/// Error returned from [`CollectedClientData::from_client_data_json`].
-#[derive(Debug)]
+#[derive(Debug, Eq, PartialEq)]
pub enum CollectedClientDataErr {
/// The `slice` had invalid length.
Len,
diff --git a/src/response/register.rs b/src/response/register.rs
@@ -29,7 +29,7 @@ use super::{
request::{
BackupReq, Challenge, UserVerificationRequirement,
auth::{AuthenticationVerificationOptions, PublicKeyCredentialRequestOptions},
- register::{Extension, RegistrationServerState},
+ register::{CoseAlgorithmIdentifier, Extension, RegistrationServerState},
},
},
AuthenticatorTransport,
@@ -71,7 +71,7 @@ mod ser;
#[cfg(feature = "serde_relaxed")]
pub mod ser_relaxed;
/// [`credentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#dom-authenticationextensionsclientinputs-credentialprotectionpolicy).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CredentialProtectionPolicy {
/// `credProtect` was not sent.
None,
@@ -101,7 +101,7 @@ impl Display for CredentialProtectionPolicy {
/// [`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)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HmacSecret {
/// No `hmac-secret` extension.
None,
@@ -303,7 +303,7 @@ impl FromCbor<'_> for MinPinLength {
.split_first()
.ok_or(AuthenticatorExtensionOutputErr::Len)
.and_then(|(&key_len, remaining)| match key_len.cmp(&24) {
- Ordering::Less => FourToSixtyThree::new(key_len)
+ Ordering::Less => FourToSixtyThree::from_u8(key_len)
.ok_or(AuthenticatorExtensionOutputErr::MinPinLengthValue)
.map(|val| CborSuccess {
value: Self::Val(val),
@@ -314,7 +314,7 @@ impl FromCbor<'_> for MinPinLength {
.ok_or(AuthenticatorExtensionOutputErr::Len)
.and_then(|(&key_24, rem)| {
if key_24 > 23 {
- FourToSixtyThree::new(key_24)
+ FourToSixtyThree::from_u8(key_24)
.ok_or(
AuthenticatorExtensionOutputErr::MinPinLengthValue,
)
@@ -2132,7 +2132,7 @@ impl<'a> FromCbor<'a> for UncompressedPubKey<'a> {
const AAGUID_LEN: usize = 16;
/// 16 bytes representing an
/// [Authenticator Attestation Globally Unique Identifier (AAGUID)](https://www.w3.org/TR/webauthn-3/#aaguid).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Aaguid<'a>(&'a [u8]);
impl<'a> Aaguid<'a> {
/// Returns the contained data.
@@ -2321,7 +2321,7 @@ impl FromCbor<'_> for NoneAttestation {
}
}
/// A 64-byte slice that allegedly represents an Ed25519 signature.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Ed25519Signature<'a>(&'a [u8]);
impl<'a> Ed25519Signature<'a> {
/// Returns signature.
@@ -2523,7 +2523,7 @@ impl<'a> FromCbor<'a> for RsaPkcs1v15Sig<'a> {
}
}
/// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) signature.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Sig<'a> {
/// Alleged Ed25519 signature.
Ed25519(Ed25519Signature<'a>),
@@ -2535,7 +2535,7 @@ pub enum Sig<'a> {
Rs256(&'a [u8]),
}
/// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PackedAttestation<'a> {
/// [Attestation signature](https://www.w3.org/TR/webauthn-3/#attestation-signature).
pub signature: Sig<'a>,
@@ -2660,7 +2660,7 @@ impl<'a> FromCbor<'a> for PackedAttestation<'a> {
}
}
/// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AttestationFormat<'a> {
/// [None](https://www.w3.org/TR/webauthn-3/#sctn-none-attestation).
None,
@@ -3192,7 +3192,7 @@ impl Response for Registration {
}
}
/// [Attestation statement](https://www.w3.org/TR/webauthn-3/#attestation-statement).
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Attestation {
/// [None](https://www.w3.org/TR/webauthn-3/#none).
None,
@@ -3266,7 +3266,7 @@ impl Metadata<'_> {
/// attestation: Attestation::None,
/// aaguid: Aaguid::try_from([15; 16].as_slice())?,
/// extensions: AuthenticatorExtensionOutputMetadata {
- /// min_pin_length: Some(FourToSixtyThree::new(16).unwrap_or_else(|| unreachable!("bug in FourToSixtyThree::new"))),
+ /// min_pin_length: Some(FourToSixtyThree::Sixteen),
/// },
/// client_extension_results: ClientExtensionsOutputsMetadata {
/// cred_props: Some(CredentialPropertiesOutput {
@@ -3331,9 +3331,9 @@ impl Metadata<'_> {
None => buffer.extend_from_slice(b"null"),
Some(pin) => {
// Clearly correct.
- let dig_1 = pin.value() / 10;
+ let dig_1 = pin.into_u8() / 10;
// Clearly correct.
- let dig_2 = pin.value() % 10;
+ let dig_2 = pin.into_u8() % 10;
if dig_1 > 0 {
// We simply add the appropriate offset which is `b'0` for decimal digits.
// Overflow cannot occur since this maxes at `b'9'`.
@@ -3479,31 +3479,53 @@ mod tests {
AttestationFormat, AttestationObject, AuthDataContainer as _, AuthExtOutput as _,
AuthTransports, AuthenticatorAttestation, AuthenticatorExtensionOutput,
AuthenticatorExtensionOutputErr, Backup, CborSuccess, CredentialProtectionPolicy,
- FromCbor as _, HmacSecret, Sig, UncompressedPubKey,
+ FourToSixtyThree, 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 _;
use p256::ecdsa::{DerSignature as P256Sig, SigningKey as P256Key};
use rsa::sha2::{Digest as _, Sha256};
+ fn hex_decode<const N: usize>(input: &[u8; N]) -> Vec<u8> {
+ /// Value to subtract from a lowercase hex digit.
+ const LOWER_OFFSET: u8 = b'a' - 10;
+ assert_eq!(
+ N & 1,
+ 0,
+ "hex_decode must be passed a reference to an array of even length"
+ );
+ let mut data = Vec::with_capacity(N >> 1);
+ input.chunks_exact(2).fold((), |(), byte| {
+ let mut hex = byte[0];
+ let val = match hex {
+ b'0'..=b'9' => hex - b'0',
+ b'a'..=b'f' => hex - LOWER_OFFSET,
+ _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"),
+ } << 4;
+ hex = byte[1];
+ data.push(
+ val | match hex {
+ b'0'..=b'9' => hex - b'0',
+ b'a'..=b'f' => hex - LOWER_OFFSET,
+ _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"),
+ },
+ );
+ });
+ data
+ }
/// https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-none-es256
#[test]
fn es256_test_vector() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?);
- let credential_private_key = HEXLOWER
- .decode(b"6e68e7a58484a3264f66b77f5d6dc5bc36a47085b615c9727ab334e8c369c2ee".as_slice())
- .unwrap();
- let aaguid = HEXLOWER
- .decode(b"8446ccb9ab1db374750b2367ff6f3a1f".as_slice())
- .unwrap();
- let credential_id = HEXLOWER
- .decode(b"f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4".as_slice())
- .unwrap();
- let client_data_json = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c4535513d3d227d".as_slice()).unwrap();
- let attestation_object = HEXLOWER.decode(b"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220".as_slice()).unwrap();
+ let credential_private_key =
+ hex_decode(b"6e68e7a58484a3264f66b77f5d6dc5bc36a47085b615c9727ab334e8c369c2ee");
+ let aaguid = hex_decode(b"8446ccb9ab1db374750b2367ff6f3a1f");
+ let credential_id =
+ hex_decode(b"f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4");
+ let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c4535513d3d227d");
+ let attestation_object = hex_decode(b"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220");
let key = *P256Key::from_slice(credential_private_key.as_slice())
.unwrap()
.verifying_key();
@@ -3535,14 +3557,11 @@ mod tests {
);
assert!(att_obj.data.auth_data.flags.user_present);
assert!(matches!(att_obj.data.attestation, AttestationFormat::None));
- let authenticator_data = HEXLOWER
- .decode(
- b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000"
- .as_slice(),
- )
- .unwrap();
- let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d".as_slice()).unwrap();
- let signature = HEXLOWER.decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87".as_slice()).unwrap();
+ let authenticator_data = hex_decode(
+ b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000",
+ );
+ let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d");
+ let signature = hex_decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87");
let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user(
client_data_json_2,
authenticator_data,
@@ -3569,17 +3588,13 @@ mod tests {
#[test]
fn es256_self_attest_test_vector() -> Result<(), AggErr> {
let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?);
- let credential_private_key = HEXLOWER
- .decode(b"b4bbfa5d68e1693b6ef5a19a0e60ef7ee2cbcac81f7fec7006ac3a21e0c5116a".as_slice())
- .unwrap();
- let aaguid = HEXLOWER
- .decode(b"df850e09db6afbdfab51697791506cfc".as_slice())
- .unwrap();
- let credential_id = HEXLOWER
- .decode(b"455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c".as_slice())
- .unwrap();
- let client_data_json = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f3078594856673d3d227d".as_slice()).unwrap();
- let attestation_object = HEXLOWER.decode(b"a363666d74667061636b65646761747453746d74a263616c67266373696758483046022100ae045923ded832b844cae4d5fc864277c0dc114ad713e271af0f0d371bd3ac540221009077a088ed51a673951ad3ba2673d5029bab65b64f4ea67b234321f86fcfac5d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2".as_slice()).unwrap();
+ let credential_private_key =
+ hex_decode(b"b4bbfa5d68e1693b6ef5a19a0e60ef7ee2cbcac81f7fec7006ac3a21e0c5116a");
+ let aaguid = hex_decode(b"df850e09db6afbdfab51697791506cfc");
+ let credential_id =
+ hex_decode(b"455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c");
+ let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f3078594856673d3d227d");
+ let attestation_object = hex_decode(b"a363666d74667061636b65646761747453746d74a263616c67266373696758483046022100ae045923ded832b844cae4d5fc864277c0dc114ad713e271af0f0d371bd3ac540221009077a088ed51a673951ad3ba2673d5029bab65b64f4ea67b234321f86fcfac5d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2");
let key = *P256Key::from_slice(credential_private_key.as_slice())
.unwrap()
.verifying_key();
@@ -3616,14 +3631,11 @@ mod tests {
}
}
});
- let authenticator_data = HEXLOWER
- .decode(
- b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000"
- .as_slice(),
- )
- .unwrap();
- let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d".as_slice()).unwrap();
- let signature = HEXLOWER.decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744".as_slice()).unwrap();
+ let authenticator_data = hex_decode(
+ b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000",
+ );
+ let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d");
+ let signature = hex_decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744");
let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user(
client_data_json_2,
authenticator_data,
@@ -3795,7 +3807,9 @@ mod tests {
value.cred_protect,
CredentialProtectionPolicy::UserVerificationOptional
) && matches!(value.hmac_secret, HmacSecret::One)
- && value.min_pin_length.is_some_and(|pin| pin.value() == 5)
+ && value
+ .min_pin_length
+ .is_some_and(|pin| pin == FourToSixtyThree::Five)
);
let opts = generate_auth_extensions(AuthExtOptions {
cred_protect: Some(0),
diff --git a/src/response/register/bin.rs b/src/response/register/bin.rs
@@ -263,7 +263,7 @@ impl EncodeBuffer for Aaguid<'_> {
}
}
/// Owned version of [`Aaguid`] that exists for [`MetadataOwned::aaguid`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AaguidOwned(pub [u8; super::AAGUID_LEN]);
impl<'a: 'b, 'b> From<&'a AaguidOwned> for Aaguid<'b> {
#[inline]
@@ -279,7 +279,7 @@ impl<'a> DecodeBuffer<'a> for AaguidOwned {
}
impl EncodeBuffer for FourToSixtyThree {
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
- self.value().encode_into_buffer(buffer);
+ self.into_u8().encode_into_buffer(buffer);
}
}
impl EncodeBuffer for AuthenticatorExtensionOutputMetadata {
@@ -290,7 +290,7 @@ impl EncodeBuffer for AuthenticatorExtensionOutputMetadata {
impl<'a> DecodeBuffer<'a> for FourToSixtyThree {
type Err = EncDecErr;
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- u8::decode_from_buffer(data).and_then(|val| Self::new(val).ok_or(EncDecErr))
+ u8::decode_from_buffer(data).and_then(|val| Self::from_u8(val).ok_or(EncDecErr))
}
}
impl<'a> DecodeBuffer<'a> for AuthenticatorExtensionOutputMetadata {
@@ -394,7 +394,7 @@ impl<'a: 'b, 'b> From<&'a MetadataOwned> for Metadata<'b> {
}
}
/// Error returned from [`MetadataOwned::decode`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeMetadataOwnedErr {
/// Variant returned when [`MetadataOwned::attestation`] could not be decoded.
Attestation,
@@ -497,7 +497,7 @@ impl Encode for StaticState<UncompressedPubKey<'_>> {
}
}
/// Error returned from [`StaticState::decode`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeStaticStateErr {
/// Variant returned when [`StaticState::credential_public_key`] could not be decoded.
CredentialPublicKey,
@@ -592,7 +592,7 @@ impl Encode for DynamicState {
}
}
/// Error returned from [`DynamicState::decode`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeDynamicStateErr {
/// Variant returned when [`DynamicState::user_verified`] could not be decoded.
UserVerified,
diff --git a/src/response/register/error.rs b/src/response/register/error.rs
@@ -34,7 +34,7 @@ use core::{
fmt::{self, Display, Formatter},
};
/// Error returned from [`Ed25519PubKey::try_from`] when the `slice` is not 32-bytes in length.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Ed25519PubKeyErr;
impl Display for Ed25519PubKeyErr {
#[inline]
@@ -44,7 +44,7 @@ impl Display for Ed25519PubKeyErr {
}
impl Error for Ed25519PubKeyErr {}
/// Error returned from [`UncompressedP256PubKey::try_from`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UncompressedP256PubKeyErr {
/// Variant returned when the x-coordinate is not 32-bytes in length.
X,
@@ -63,7 +63,7 @@ impl Display for UncompressedP256PubKeyErr {
impl Error for UncompressedP256PubKeyErr {}
/// Error returned from [`CompressedP256PubKey::try_from`] when the x-coordinate
/// is not exactly 32 bytes in length.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CompressedP256PubKeyErr;
impl Display for CompressedP256PubKeyErr {
#[inline]
@@ -73,7 +73,7 @@ impl Display for CompressedP256PubKeyErr {
}
impl Error for CompressedP256PubKeyErr {}
/// Error returned from [`UncompressedP384PubKey::try_from`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UncompressedP384PubKeyErr {
/// Variant returned when the x-coordinate is not 48-bytes in length.
X,
@@ -92,7 +92,7 @@ impl Display for UncompressedP384PubKeyErr {
impl Error for UncompressedP384PubKeyErr {}
/// Error returned from [`CompressedP384PubKey::try_from`] when the x-coordinate
/// is not exactly 48 bytes in length.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CompressedP384PubKeyErr;
impl Display for CompressedP384PubKeyErr {
#[inline]
@@ -102,7 +102,7 @@ impl Display for CompressedP384PubKeyErr {
}
impl Error for CompressedP384PubKeyErr {}
/// Error returned from [`RsaPubKey::try_from`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RsaPubKeyErr {
/// Variant returned when the modulus has a leading 0.
NLeading0,
@@ -132,7 +132,7 @@ impl Display for RsaPubKeyErr {
}
impl Error for RsaPubKeyErr {}
/// Error returned when an alleged public key is not valid.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PubKeyErr {
/// Error when [`Ed25519PubKey`] is not valid.
///
@@ -156,7 +156,7 @@ impl Display for PubKeyErr {
}
impl Error for PubKeyErr {}
/// Error returned from [`Ed25519Signature::try_from`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Ed25519SignatureErr;
impl Display for Ed25519SignatureErr {
#[inline]
@@ -167,7 +167,7 @@ impl Display for Ed25519SignatureErr {
impl Error for Ed25519SignatureErr {}
/// Error returned from [`Aaguid::try_from`] when the slice is not exactly
/// 16-bytes in length.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AaguidErr;
impl Display for AaguidErr {
#[inline]
@@ -177,7 +177,7 @@ impl Display for AaguidErr {
}
impl Error for AaguidErr {}
/// Error returned in [`AuthenticatorDataErr::AuthenticatorExtension`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthenticatorExtensionOutputErr {
/// The `slice` had an invalid length.
Len,
@@ -213,7 +213,7 @@ impl Display for AuthenticatorExtensionOutputErr {
}
impl Error for AuthenticatorExtensionOutputErr {}
/// Error returned in [`AttestedCredentialDataErr::CoseKey`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CoseKeyErr {
/// The `slice` had an invalid length.
Len,
@@ -261,7 +261,7 @@ impl Display for CoseKeyErr {
}
impl Error for CoseKeyErr {}
/// Error returned in [`AuthenticatorDataErr::AttestedCredential`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AttestedCredentialDataErr {
/// The `slice` had an invalid length.
Len,
@@ -282,7 +282,7 @@ impl Display for AttestedCredentialDataErr {
}
impl Error for AttestedCredentialDataErr {}
/// Error returned from [`AuthenticatorData::try_from`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthenticatorDataErr {
/// The `slice` had an invalid length.
Len,
@@ -364,7 +364,7 @@ impl Display for AuthenticatorDataErr {
}
impl Error for AuthenticatorDataErr {}
/// Error returned in [`AttestationObjectErr::Attestation`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AttestationErr {
/// The `slice` had an invalid length.
Len,
@@ -424,7 +424,7 @@ impl Display for AttestationErr {
}
impl Error for AttestationErr {}
/// Error returned by [`AttestationObject::try_from`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AttestationObjectErr {
/// The `slice` had an invalid length.
Len,
@@ -475,7 +475,7 @@ impl Display for AttestationObjectErr {
}
impl Error for AttestationObjectErr {}
/// Error in [`RegCeremonyErr::Extension`].
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExtensionErr {
/// [`ClientExtensionsOutputs::cred_props`] was sent from the client but was not supposed to be.
ForbiddenCredProps,
diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs
@@ -2,7 +2,7 @@ use super::{
super::{
super::request::register::CoseAlgorithmIdentifier,
ser::{
- self, AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues,
+ AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues,
Base64DecodedVal, ClientExtensions, PublicKeyCredential,
},
},
@@ -17,7 +17,6 @@ use core::{
marker::PhantomData,
str,
};
-use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Sha256, digest::OutputSizeUser as _};
use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor};
/// Functionality for deserializing DER-encoded `SubjectPublicKeyInfo` _without_ making copies of data or
@@ -754,10 +753,6 @@ impl<'e> Deserialize<'e> for AttObj {
formatter.write_str("base64url-encoded attestation object")
}
#[expect(
- clippy::panic_in_result_fn,
- reason = "we want to crash when there is a bug"
- )]
- #[expect(
clippy::arithmetic_side_effects,
reason = "comment justifies their correctness"
)]
@@ -765,7 +760,7 @@ impl<'e> Deserialize<'e> for AttObj {
where
E: Error,
{
- ser::base64url_nopad_decode_len(v.len())
+ base64url_nopad::decode_len(v.len())
.ok_or_else(|| E::invalid_value(Unexpected::Str(v), &"base64url-encoded value"))
.and_then(|len| {
// The decoded length is 3/4 of the encoded length, so overflow could only occur
@@ -774,16 +769,9 @@ impl<'e> Deserialize<'e> for AttObj {
// the raw attestation object by `AuthenticatorAttestation::new`.
let mut att_obj = vec![0; len + Sha256::output_size()];
att_obj.truncate(len);
- BASE64URL_NOPAD
- .decode_mut(v.as_bytes(), &mut att_obj)
- .map_err(|e| E::custom(e.error))
- .map(|dec_len| {
- assert_eq!(
- len, dec_len,
- "there is a bug in BASE64URL_NOPAD::decode_mut"
- );
- AttObj(att_obj)
- })
+ base64url_nopad::decode_buffer_exact(v.as_bytes(), &mut att_obj)
+ .map_err(E::custom)
+ .map(|()| AttObj(att_obj))
})
}
}
@@ -1403,7 +1391,6 @@ mod tests {
CoseAlgorithmIdentifier,
spki::SubjectPublicKeyInfo,
};
- use data_encoding::BASE64URL_NOPAD;
use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey};
use p256::{
EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key,
@@ -1709,10 +1696,10 @@ mod tests {
.unwrap()
.to_public_key_der()
.unwrap();
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 113..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 113..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<Registration>(
@@ -1751,8 +1738,7 @@ mod tests {
// `id` and `rawId` mismatch.
let mut err = Error::invalid_value(
Unexpected::Bytes(
- BASE64URL_NOPAD
- .decode("ABABABABABABABABABABAA".as_bytes())
+ base64url_nopad::decode("ABABABABABABABABABABAA".as_bytes())
.unwrap()
.as_slice(),
),
@@ -1893,8 +1879,8 @@ mod tests {
// `id` and the credential id in authenticator data mismatch.
err = Error::invalid_value(
Unexpected::Bytes(
- BASE64URL_NOPAD
- .decode("ABABABABABABABABABABAA".as_bytes())
+ base64url_nopad
+ ::decode("ABABABABABABABABABABAA".as_bytes())
.unwrap()
.as_slice(),
),
@@ -1941,7 +1927,7 @@ mod tests {
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
"clientDataJSON": b64_cdata,
- "authenticatorData": BASE64URL_NOPAD.encode(bad_auth.as_slice()),
+ "authenticatorData": base64url_nopad::encode(bad_auth.as_slice()),
"transports": [],
"publicKey": b64_key,
"publicKeyAlgorithm": -8,
@@ -2116,7 +2102,7 @@ mod tests {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"transports": [],
- "publicKey": BASE64URL_NOPAD.encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()),
+ "publicKey": base64url_nopad::encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()),
"publicKeyAlgorithm": -8,
"attestationObject": b64_aobj,
},
@@ -2982,10 +2968,10 @@ mod tests {
.unwrap()
.to_public_key_der()
.unwrap();
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 113..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 113..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<Registration>(
@@ -4099,10 +4085,10 @@ mod tests {
att_obj[att_obj_len - 67..att_obj_len - 35]
.copy_from_slice(enc_key.x().unwrap().as_slice());
att_obj[att_obj_len - 32..].copy_from_slice(enc_key.y().unwrap().as_slice());
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 148..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 148..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<Registration>(
@@ -4252,7 +4238,7 @@ mod tests {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"transports": [],
- "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
"publicKeyAlgorithm": -7,
"attestationObject": b64_aobj,
},
@@ -4564,10 +4550,10 @@ mod tests {
att_obj[att_obj_len - 99..att_obj_len - 51]
.copy_from_slice(enc_key.x().unwrap().as_slice());
att_obj[att_obj_len - 48..].copy_from_slice(enc_key.y().unwrap().as_slice());
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 181..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 181..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<Registration>(
@@ -4719,7 +4705,7 @@ mod tests {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"transports": [],
- "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
"publicKeyAlgorithm": -35,
"attestationObject": b64_aobj,
},
@@ -5297,10 +5283,10 @@ mod tests {
let att_obj_len = att_obj.len();
att_obj[att_obj_len - 261..att_obj_len - 5]
.copy_from_slice(key.n().to_bytes_be().as_slice());
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 343..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 343..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<Registration>(
@@ -5508,7 +5494,7 @@ mod tests {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"transports": [],
- "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
"publicKeyAlgorithm": -257,
"attestationObject": b64_aobj,
},
diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs
@@ -442,7 +442,6 @@ mod tests {
},
CustomRegistration, RegistrationRelaxed,
};
- use data_encoding::BASE64URL_NOPAD;
use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey};
use p256::{
EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key,
@@ -619,10 +618,10 @@ mod tests {
.unwrap()
.to_public_key_der()
.unwrap();
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 113..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 113..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<RegistrationRelaxed>(
@@ -661,8 +660,7 @@ mod tests {
// `id` and `rawId` mismatch.
let mut err = Error::invalid_value(
Unexpected::Bytes(
- BASE64URL_NOPAD
- .decode("ABABABABABABABABABABAA".as_bytes())
+ base64url_nopad::decode("ABABABABABABABABABABAA".as_bytes())
.unwrap()
.as_slice(),
),
@@ -795,8 +793,8 @@ mod tests {
// `id` and the credential id in authenticator data mismatch.
err = Error::invalid_value(
Unexpected::Bytes(
- BASE64URL_NOPAD
- .decode("ABABABABABABABABABABAA".as_bytes())
+ base64url_nopad
+ ::decode("ABABABABABABABABABABAA".as_bytes())
.unwrap()
.as_slice(),
),
@@ -843,7 +841,7 @@ mod tests {
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
"clientDataJSON": b64_cdata,
- "authenticatorData": BASE64URL_NOPAD.encode(bad_auth.as_slice()),
+ "authenticatorData": base64url_nopad::encode(bad_auth.as_slice()),
"transports": [],
"publicKey": b64_key,
"publicKeyAlgorithm": -8,
@@ -994,7 +992,7 @@ mod tests {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"transports": [],
- "publicKey": BASE64URL_NOPAD.encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()),
+ "publicKey": base64url_nopad::encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()),
"publicKeyAlgorithm": -8,
"attestationObject": b64_aobj,
},
@@ -2202,10 +2200,10 @@ mod tests {
.unwrap()
.to_public_key_der()
.unwrap();
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 113..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 113..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<RegistrationRelaxed>(
@@ -3310,10 +3308,10 @@ mod tests {
att_obj[att_obj_len - 67..att_obj_len - 35]
.copy_from_slice(enc_key.x().unwrap().as_slice());
att_obj[att_obj_len - 32..].copy_from_slice(enc_key.y().unwrap().as_slice());
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 148..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 148..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<RegistrationRelaxed>(
@@ -3454,7 +3452,7 @@ mod tests {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"transports": [],
- "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
"publicKeyAlgorithm": -7,
"attestationObject": b64_aobj,
},
@@ -3783,10 +3781,10 @@ mod tests {
att_obj[att_obj_len - 99..att_obj_len - 51]
.copy_from_slice(enc_key.x().unwrap().as_slice());
att_obj[att_obj_len - 48..].copy_from_slice(enc_key.y().unwrap().as_slice());
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 181..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 181..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<RegistrationRelaxed>(
@@ -3929,7 +3927,7 @@ mod tests {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"transports": [],
- "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
"publicKeyAlgorithm": -35,
"attestationObject": b64_aobj,
},
@@ -4529,10 +4527,10 @@ mod tests {
let att_obj_len = att_obj.len();
att_obj[att_obj_len - 261..att_obj_len - 5]
.copy_from_slice(key.n().to_bytes_be().as_slice());
- let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes());
- let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 343..]);
- let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes());
- let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice());
+ let b64_cdata = base64url_nopad::encode(c_data_json.as_bytes());
+ let b64_adata = base64url_nopad::encode(&att_obj[att_obj.len() - 343..]);
+ let b64_key = base64url_nopad::encode(pub_key.as_bytes());
+ let b64_aobj = base64url_nopad::encode(att_obj.as_slice());
// Base case is valid.
assert!(
serde_json::from_str::<RegistrationRelaxed>(
@@ -4731,7 +4729,7 @@ mod tests {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"transports": [],
- "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKey": base64url_nopad::encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
"publicKeyAlgorithm": -257,
"attestationObject": b64_aobj,
},
diff --git a/src/response/ser.rs b/src/response/ser.rs
@@ -8,7 +8,6 @@ use core::{
fmt::{self, Formatter},
marker::PhantomData,
};
-use data_encoding::BASE64URL_NOPAD;
use serde::{
de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, SeqAccess, Unexpected, Visitor},
ser::{Serialize, SerializeSeq as _, SerializeStruct as _, Serializer},
@@ -247,7 +246,7 @@ impl<T: AsRef<[u8]>> Serialize for CredentialId<T> {
where
S: Serializer,
{
- serializer.serialize_str(BASE64URL_NOPAD.encode(self.0.as_ref()).as_str())
+ serializer.serialize_str(base64url_nopad::encode(self.0.as_ref()).as_str())
}
}
impl<'de> Deserialize<'de> for CredentialId<Vec<u8>> {
@@ -282,12 +281,11 @@ impl<'de> Deserialize<'de> for CredentialId<Vec<u8>> {
E: Error,
{
/// Minimum possible encoded length of a `CredentialId`.
- const MIN_LEN: usize = crate::base64url_nopad_len(super::CRED_ID_MIN_LEN);
+ const MIN_LEN: usize = base64url_nopad::encode_len(super::CRED_ID_MIN_LEN);
/// Maximum possible encoded length of a `CredentialId`.
- const MAX_LEN: usize = crate::base64url_nopad_len(super::CRED_ID_MAX_LEN);
+ const MAX_LEN: usize = base64url_nopad::encode_len(super::CRED_ID_MAX_LEN);
if (MIN_LEN..=MAX_LEN).contains(&v.len()) {
- BASE64URL_NOPAD
- .decode(v.as_bytes())
+ base64url_nopad::decode(v.as_bytes())
.map_err(E::custom)
.map(CredentialId)
} else {
@@ -360,7 +358,7 @@ impl<'de> Deserialize<'de> for AuthenticatorAttachment {
}
}
/// Container of data that was encoded in base64url.
-pub(super) struct Base64DecodedVal(pub Vec<u8>);
+pub(crate) struct Base64DecodedVal(pub Vec<u8>);
impl<'de> Deserialize<'de> for Base64DecodedVal {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -377,8 +375,7 @@ impl<'de> Deserialize<'de> for Base64DecodedVal {
where
E: Error,
{
- BASE64URL_NOPAD
- .decode(v.as_bytes())
+ base64url_nopad::decode(v.as_bytes())
.map_err(E::custom)
.map(Base64DecodedVal)
}
@@ -417,23 +414,15 @@ impl<'de> Deserialize<'de> for SentChallenge {
"base64 encoding of the 16-byte challenge in a URL safe way without padding",
)
}
- #[expect(
- clippy::panic_in_result_fn,
- reason = "we want to crash when there is a bug"
- )]
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: Error,
{
if v.len() == Challenge::BASE64_LEN {
let mut data = [0; 16];
- BASE64URL_NOPAD
- .decode_mut(v, data.as_mut_slice())
- .map_err(|err| E::custom(err.error))
- .map(|len| {
- assert_eq!(len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut");
- SentChallenge::from_array(data)
- })
+ base64url_nopad::decode_buffer_exact(v, data.as_mut_slice())
+ .map_err(E::custom)
+ .map(|()| SentChallenge::from_array(data))
} else {
Err(E::invalid_value(
Unexpected::Bytes(v),
@@ -525,7 +514,7 @@ pub(super) struct PublicKeyCredential<const RELAXED: bool, const REG: bool, Auth
pub client_extension_results: Ext,
}
/// Deserializes the value for type.
-pub(super) struct Type;
+pub(crate) struct Type;
impl<'e> Deserialize<'e> for Type {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -923,7 +912,7 @@ where
}
}
/// JSON `null`.
-struct Null;
+pub(crate) struct Null;
impl<'de> Deserialize<'de> for Null {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -1198,66 +1187,3 @@ where
)
}
}
-/// Calculates the number of bytes a base64url-encoded input of length `n` bytes will be decoded into.
-///
-/// `Some` is returned iff `n` is a valid input length.
-///
-/// Note `n` must not only be a valid length mathematically but also represent a possible allocation of that
-/// many bytes. Since only allocations <= [`isize::MAX`] are possible, this will always return `None` when
-/// `n > isize::MAX`.
-#[expect(
- clippy::arithmetic_side_effects,
- clippy::as_conversions,
- reason = "proof and comment justifies their correctness"
-)]
-pub(super) const fn base64url_nopad_decode_len(n: usize) -> Option<usize> {
- // 64^n is the number of distinct values of the input. Let the decoded output be O.
- // There are 256 possible values each byte in O can be; thus we must find
- // the maximum nonnegative integer m such that:
- // 256^m = (2^8)^m = 2^(8m) <= 64^n = (2^6)^n = 2^(6n)
- // <==>
- // lg(2^(8m)) = 8m <= lg(2^(6n)) = 6n lg is defined on all positive reals which 2^(8m) and 2^(6n) are
- // <==>
- // m <= 6n/8 = 3n/4
- // Clearly that corresponds to m = ⌊3n/4⌋.
- // From the proof in `crate::base64url_nopad_len`, we know that n is a valid length
- // iff n ≢ 1 (mod 4) and n <= isize::MAX.
- // We claim ⌊3n/4⌋ = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋.
- // Proof:
- // There are three partitions for n:
- // (1) 4i = n ≡ 0 (mod 4) for some integer i
- // <==>
- // ⌊3n/4⌋ = ⌊3(4i)/4⌋ = ⌊3i⌋ = 3i = 3⌊i⌋ = 3⌊4i/4⌋ = 3⌊n/4⌋ + 0 = 3⌊n/4⌋ + ⌊3(0)/4⌋ = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋
- // (2) 4i + 2 = n ≡ 2 (mod 4) for some integer i
- // <==>
- // ⌊3n/4⌋ = ⌊3(4i + 2)/4⌋ = ⌊3i + 6/4⌋ = 3i + ⌊6/4⌋ = 3i + 1 = 3⌊i⌋ + ⌊3(2)/4⌋
- // = 3⌊(4i + 2)/4⌋ + ⌊3((4i + 2) mod 4)/4⌋
- // = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋
- // (3) 4i + 3 = n ≡ 3 (mod 4) for some integer i
- // <==>
- // ⌊3n/4⌋ = ⌊3(4i + 3)/4⌋ = ⌊3i + 9/4⌋ = 3i + ⌊9/4⌋ = 3i + 2 = 3⌊i⌋ + ⌊3(3)/4⌋
- // = 3⌊(4i + 3)/4⌋ + ⌊3((4i + 3) mod 4)/4⌋
- // = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋
- // QED
- // Naively implementing ⌊3n/4⌋ as (3 * n) / 3 can cause overflow due to `3 * n`; thus
- // we implement the equivalent equation 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋ instead:
- // `(3 * (n / 4)) + ((3 * (n % 4)) / 4)` since none of the intermediate calculations suffer
- // from overflow.
-
- // `isize::MAX > 0 = usize::MIN`; thus this conversion is lossless.
- if n & 3 == 1 || n > isize::MAX as usize {
- None
- } else {
- // n = 4quot + rem
- let (quot, rem) = (n >> 2u8, n & 3);
- // 3 * quot <= m < usize::MAX; thus the left operand of + is fine.
- // rem <= 3
- // <==>
- // 3rem <= 9 < usize::MAX; thus 3 * rem is fine.
- // <==>
- // ⌊3rem/4⌋ <= 3rem, so the right operand of + is fine.
- // The sum is fine since
- // m = ⌊3n/4⌋ = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋ = (3 * quot) + ((3 * rem) >> 2u8), and m < usize::MAX.
- Some((3 * quot) + ((3 * rem) >> 2u8))
- }
-}
diff --git a/src/response/ser_relaxed.rs b/src/response/ser_relaxed.rs
@@ -13,8 +13,6 @@ use core::{
fmt::{self, Formatter},
marker::PhantomData,
};
-#[cfg(doc)]
-use data_encoding::BASE64URL_NOPAD;
use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor};
#[cfg(doc)]
use serde_json::de;