commit 18f2066416892be8936c6677fb0f8dcd35213b1f
parent ac55944f66d2533d5ab9542f2b54c1dc9d72ea9d
Author: Zack Newman <zack@philomathiclife.com>
Date: Mon, 24 Mar 2025 18:25:09 -0600
more userhandle impls, more decode impls, better relaxed deserialization integration
Diffstat:
20 files changed, 5190 insertions(+), 2989 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -10,7 +10,7 @@ name = "webauthn_rp"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/webauthn_rp/"
rust-version = "1.85.0"
-version = "0.2.7"
+version = "0.3.0"
[package.metadata.docs.rs]
all-features = true
diff --git a/README.md b/README.md
@@ -40,6 +40,10 @@ cannot be constructed when [`bin`](#bin) or [`serde`](#serde) is not enabled.
### `serde`
+For many [`serde_relaxed`](#serde_relaxed) should be used instead. This feature _strictly_ adheres to the
+JSON-motivated definitions. You _will_ encounter clients that send data that cannot be deserialized using
+this feature.
+
Enables (de)serialization of data sent to/from the client via [`serde`](https://docs.rs/serde/latest/serde/)
based on the JSON-motivated definitions (e.g.,
[`RegistrationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson)). Since
diff --git a/src/bin.rs b/src/bin.rs
@@ -207,11 +207,13 @@ impl<'a> DecodeBuffer<'a> for u16 {
reason = "we must standardize the endianness to remove ambiguity"
)]
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- data.split_at_checked(2)
+ /// Number of bytes `u16` is made of.
+ const SIZE: usize = 2;
+ data.split_at_checked(SIZE)
.ok_or(EncDecErr)
.map(|(le_bytes, rem)| {
*data = rem;
- let mut val = [0; 2];
+ let mut val = [0; SIZE];
val.copy_from_slice(le_bytes);
Self::from_le_bytes(val)
})
@@ -224,11 +226,13 @@ impl<'a> DecodeBuffer<'a> for u32 {
reason = "we must standardize the endianness to remove ambiguity"
)]
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- data.split_at_checked(4)
+ /// Number of bytes `u32` is made of.
+ const SIZE: usize = 4;
+ data.split_at_checked(SIZE)
.ok_or(EncDecErr)
.map(|(le_bytes, rem)| {
*data = rem;
- let mut val = [0; 4];
+ let mut val = [0; SIZE];
val.copy_from_slice(le_bytes);
Self::from_le_bytes(val)
})
@@ -241,11 +245,13 @@ impl<'a> DecodeBuffer<'a> for u64 {
reason = "we must standardize the endianness to remove ambiguity"
)]
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- data.split_at_checked(8)
+ /// Number of bytes `u64` is made of.
+ const SIZE: usize = 8;
+ data.split_at_checked(SIZE)
.ok_or(EncDecErr)
.map(|(le_bytes, rem)| {
*data = rem;
- let mut val = [0; 8];
+ let mut val = [0; SIZE];
val.copy_from_slice(le_bytes);
Self::from_le_bytes(val)
})
@@ -258,11 +264,13 @@ impl<'a> DecodeBuffer<'a> for u128 {
reason = "we must standardize the endianness to remove ambiguity"
)]
fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
- data.split_at_checked(16)
+ /// Number of bytes `u128` is made of.
+ const SIZE: usize = 16;
+ data.split_at_checked(SIZE)
.ok_or(EncDecErr)
.map(|(le_bytes, rem)| {
*data = rem;
- let mut val = [0; 16];
+ let mut val = [0; SIZE];
val.copy_from_slice(le_bytes);
Self::from_le_bytes(val)
})
diff --git a/src/lib.rs b/src/lib.rs
@@ -40,6 +40,10 @@
//!
//! ### `serde`
//!
+//! For many [`serde_relaxed`](#serde_relaxed) should be used instead. This feature _strictly_ adheres to the
+//! JSON-motivated definitions. You _will_ encounter clients that send data that cannot be deserialized using
+//! this feature.
+//!
//! Enables (de)serialization of data sent to/from the client via [`serde`](https://docs.rs/serde/latest/serde/)
//! based on the JSON-motivated definitions (e.g.,
//! [`RegistrationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson)). Since
diff --git a/src/request.rs b/src/request.rs
@@ -45,7 +45,7 @@ use url::Url as Uri;
/// # use webauthn_rp::{
/// # request::{
/// # auth::{AllowedCredentials, PublicKeyCredentialRequestOptions},
-/// # register::UserHandle,
+/// # register::{UserHandle, USER_HANDLE_MAX_LEN},
/// # AsciiDomain, Credentials, PublicKeyCredentialDescriptor, RpId,
/// # },
/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
@@ -76,9 +76,9 @@ use url::Url as Uri;
/// # #[cfg(all(feature = "custom", feature = "serde"))]
/// assert!(serde_json::to_string(&client_2).is_ok());
/// /// Extract `UserHandle` from session cookie.
-/// fn get_user_handle() -> UserHandle<Vec<u8>> {
+/// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MAX_LEN]> {
/// // ⋮
-/// # UserHandle::new()
+/// # UserHandle::new_rand()
/// }
/// # #[cfg(feature = "custom")]
/// /// Fetch the `AllowedCredentials` associated with `user`.
@@ -110,7 +110,7 @@ pub mod error;
/// # use webauthn_rp::{
/// # request::{
/// # register::{
-/// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle,
+/// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN,
/// # },
/// # AsciiDomain, PublicKeyCredentialDescriptor, RpId
/// # },
@@ -144,9 +144,9 @@ pub mod error;
/// # #[cfg(feature = "serde")]
/// assert!(serde_json::to_string(&client_2).is_ok());
/// /// Extract `UserHandle` from session cookie if this is not the first credential registered.
-/// fn get_user_handle() -> UserHandle<Vec<u8>> {
+/// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MAX_LEN]> {
/// // ⋮
-/// # UserHandle::new()
+/// # UserHandle::new_rand()
/// }
/// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`.
/// ///
@@ -926,17 +926,27 @@ pub enum UserVerificationRequirement {
/// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred).
Preferred,
}
-/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints).
+#[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)]
pub enum Hint {
/// No hints.
#[default]
None,
- /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-security-key).
+ /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-security-key).
SecurityKey,
- /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-client-device).
+ /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-client-device).
ClientDevice,
- /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid).
+ /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-hybrid).
Hybrid,
/// [`Self::SecurityKey`] and [`Self::ClientDevice`].
SecurityKeyClientDevice,
@@ -963,6 +973,41 @@ 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
@@ -978,6 +1023,15 @@ pub enum ExtensionReq {
/// 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`].
@@ -992,6 +1046,17 @@ 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 {
@@ -1019,6 +1084,17 @@ pub enum CredentialMediationRequirement {
/// [`required`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-required)
Required,
}
+#[cfg(test)]
+impl PartialEq for CredentialMediationRequirement {
+ fn eq(&self, other: &Self) -> bool {
+ match *self {
+ Self::Silent => matches!(other, Self::Silent),
+ Self::Optional => matches!(other, Self::Optional),
+ Self::Conditional => matches!(other, Self::Conditional),
+ Self::Required => matches!(other, Self::Required),
+ }
+ }
+}
/// Backup requirements for the credential.
#[derive(Clone, Copy, Debug, Default)]
pub enum BackupReq {
@@ -1194,7 +1270,7 @@ impl<'o, 't, O, T> From<&RegistrationVerificationOptions<'o, 't, O, T>>
/// Functionality common to both registration and authentication ceremonies.
///
/// Designed to be implemented on the _request_ side.
-trait Ceremony {
+trait Ceremony<U> {
/// The type of response that is associated with the ceremony.
type R: Response;
/// Challenge.
@@ -2408,11 +2484,11 @@ mod tests {
&rp_id,
&Authentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
- response: AuthenticatorAssertion::new(
+ response: AuthenticatorAssertion::with_user(
client_data_json,
authenticator_data,
sig,
- Some(UserHandle::try_from(vec![0])?),
+ UserHandle::from([0]),
),
authenticator_attachment: AuthenticatorAttachment::None,
},
@@ -2774,11 +2850,11 @@ mod tests {
&rp_id,
&Authentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
- response: AuthenticatorAssertion::new(
+ response: AuthenticatorAssertion::with_user(
client_data_json,
authenticator_data,
der_sig.as_bytes().into(),
- Some(UserHandle::try_from(vec![0])?),
+ UserHandle::from([0]),
),
authenticator_attachment: AuthenticatorAttachment::None,
},
@@ -3179,11 +3255,11 @@ mod tests {
&rp_id,
&Authentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
- response: AuthenticatorAssertion::new(
+ response: AuthenticatorAssertion::with_user(
client_data_json,
authenticator_data,
der_sig.as_bytes().into(),
- Some(UserHandle::try_from(vec![0])?),
+ UserHandle::from([0]),
),
authenticator_attachment: AuthenticatorAttachment::None,
},
@@ -3846,11 +3922,11 @@ mod tests {
&rp_id,
&Authentication {
raw_id: CredentialId::try_from(vec![0; 16])?,
- response: AuthenticatorAssertion::new(
+ response: AuthenticatorAssertion::with_user(
client_data_json,
authenticator_data,
sig,
- Some(UserHandle::try_from(vec![0])?),
+ UserHandle::from([0]),
),
authenticator_attachment: AuthenticatorAttachment::None,
},
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -612,6 +612,15 @@ impl ServerPrfInfo {
}
}
}
+#[cfg(test)]
+impl PartialEq for ServerPrfInfo {
+ fn eq(&self, other: &Self) -> bool {
+ match *self {
+ Self::One(req) => matches!(*other, Self::One(req2) if req == req2),
+ Self::Two(req) => matches!(*other, Self::Two(req2) if req == req2),
+ }
+ }
+}
impl From<PrfInput<'_>> for ServerPrfInfo {
fn from(value: PrfInput<'_>) -> Self {
value
@@ -641,6 +650,12 @@ impl From<Extension<'_>> for ServerExtensionInfo {
}
}
}
+#[cfg(test)]
+impl PartialEq for ServerExtensionInfo {
+ fn eq(&self, other: &Self) -> bool {
+ self.prf == other.prf
+ }
+}
/// `CredentialSpecificExtension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`]
/// in an in-memory collection.
#[derive(Clone, Copy, Debug)]
@@ -648,6 +663,12 @@ struct ServerCredSpecificExtensionInfo {
/// `CredentialSpecificExtension::prf`.
prf: Option<ServerPrfInfo>,
}
+#[cfg(test)]
+impl PartialEq for ServerCredSpecificExtensionInfo {
+ fn eq(&self, other: &Self) -> bool {
+ self.prf == other.prf
+ }
+}
impl From<&CredentialSpecificExtension> for ServerCredSpecificExtensionInfo {
fn from(value: &CredentialSpecificExtension) -> Self {
Self {
@@ -718,6 +739,12 @@ struct CredInfo {
/// Any credential-specific extensions.
ext: ServerCredSpecificExtensionInfo,
}
+#[cfg(test)]
+impl PartialEq for CredInfo {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id && self.ext == other.ext
+ }
+}
/// Controls how to handle a change in [`DynamicState::authenticator_attachment`].
///
/// Note when `DynamicState::authenticator_attachment` is [`AuthenticatorAttachment::None`], then it will
@@ -929,10 +956,11 @@ impl AuthenticationServerState {
P256Key: AsRef<[u8]>,
P384Key: AsRef<[u8]>,
RsaKey: AsRef<[u8]>,
+ User: AsRef<[u8]>,
>(
self,
rp_id: &RpId,
- response: &'a Authentication,
+ response: &'a Authentication<User>,
cred: &mut AuthenticatedCredential<
'a,
'user,
@@ -1070,9 +1098,9 @@ impl AuthenticationServerState {
/// Errors iff [`AuthenticatedCredential::user_handle`] does not match [`Authentication::user_handle`] or
/// [`PublicKeyCredentialRequestOptions::allow_credentials`] does not have a [`CredInfo`] such that
/// [`CredInfo::id`] matches [`Authentication::raw_id`].
- fn verify_nondiscoverable<'a, PublicKey>(
+ fn verify_nondiscoverable<'a, PublicKey, User: AsRef<[u8]>>(
&self,
- response: &'a Authentication,
+ response: &'a Authentication<User>,
cred: &AuthenticatedCredential<'a, '_, PublicKey>,
) -> Result<Option<ServerCredSpecificExtensionInfo>, AuthCeremonyErr> {
response
@@ -1094,6 +1122,14 @@ impl AuthenticationServerState {
.map(|c| Some(c.ext))
})
}
+ #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
+ fn is_eq(&self, other: &Self) -> bool {
+ self.challenge == other.challenge
+ && self.allow_credentials == other.allow_credentials
+ && self.user_verification == other.user_verification
+ && self.extensions == other.extensions
+ && self.expiration == other.expiration
+ }
}
impl ServerState for AuthenticationServerState {
#[cfg(any(doc, not(feature = "serializable_server_state")))]
@@ -1111,8 +1147,8 @@ impl ServerState for AuthenticationServerState {
self.challenge
}
}
-impl Ceremony for AuthenticationServerState {
- type R = Authentication;
+impl<User> Ceremony<User> for AuthenticationServerState {
+ type R = Authentication<User>;
fn rand_challenge(&self) -> SentChallenge {
self.challenge
}
@@ -1184,7 +1220,7 @@ mod tests {
},
AllowedCredential, AllowedCredentials, AuthenticationServerState, Challenge, CredentialId,
CredentialSpecificExtension, Credentials as _, Extension, ExtensionReq, PrfInputOwned,
- PublicKeyCredentialDescriptor, PublicKeyCredentialRequestOptions, RpId, ServerPrfInfo,
+ PublicKeyCredentialDescriptor, PublicKeyCredentialRequestOptions, RpId,
UserVerificationRequirement,
};
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
@@ -1371,35 +1407,9 @@ mod tests {
.extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
authenticator_data.truncate(132);
let server = opts.start_ceremony()?.0;
- let server_2 = AuthenticationServerState::decode(server.encode()?.as_slice())?;
- assert_eq!(server.challenge.0, server_2.challenge.0);
- assert_eq!(server.allow_credentials.len(), 1);
- assert_eq!(
- server.allow_credentials.len(),
- server_2.allow_credentials.len()
- );
- assert_eq!(
- server.allow_credentials[0].id,
- server_2.allow_credentials[0].id
- );
- assert!(matches!(
- server.allow_credentials[0].ext.prf.unwrap(),
- ServerPrfInfo::Two(req) if matches!(req, ExtensionReq::Require)
- ));
- assert!(server_2.allow_credentials[0].ext.prf.map_or(false, |prf| {
- matches!(prf, ServerPrfInfo::Two(req) if matches!(req, ExtensionReq::Require))
- }));
- assert!(
- matches!(
- server.user_verification,
- UserVerificationRequirement::Required
- ) && matches!(
- server_2.user_verification,
- UserVerificationRequirement::Required
- )
- );
- assert!(server.extensions.prf.is_none() && server_2.extensions.prf.is_none());
- assert_eq!(server.expiration, server_2.expiration);
+ assert!(server.is_eq(&AuthenticationServerState::decode(
+ server.encode()?.as_slice()
+ )?));
Ok(())
}
}
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -157,6 +157,23 @@ impl CredProtect {
}
}
}
+#[cfg(test)]
+impl PartialEq for CredProtect {
+ fn eq(&self, other: &Self) -> bool {
+ match *self {
+ Self::None => matches!(other, Self::None),
+ Self::UserVerificationOptional(info) => {
+ matches!(*other, Self::UserVerificationOptional(info2) if info == info2)
+ }
+ Self::UserVerificationOptionalWithCredentialIdList(info) => {
+ matches!(*other, Self::UserVerificationOptionalWithCredentialIdList(info2) if info == info2)
+ }
+ Self::UserVerificationRequired(info) => {
+ matches!(*other, Self::UserVerificationRequired(info2) if info == info2)
+ }
+ }
+ }
+}
impl Display for CredProtect {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
@@ -415,6 +432,12 @@ impl Default for CoseAlgorithmIdentifiers {
Self::ALL
}
}
+#[cfg(test)]
+impl PartialEq for CoseAlgorithmIdentifiers {
+ fn eq(&self, other: &Self) -> bool {
+ self.0 == other.0
+ }
+}
/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client.
#[derive(Clone, Copy, Debug, Default)]
pub struct Extension {
@@ -658,6 +681,15 @@ impl Extension {
})
}
}
+#[cfg(test)]
+impl PartialEq for Extension {
+ fn eq(&self, other: &Self) -> bool {
+ self.cred_props == other.cred_props
+ && self.cred_protect == other.cred_protect
+ && self.min_pin_length == other.min_pin_length
+ && self.prf == other.prf
+ }
+}
/// The maximum number of bytes a [`UserHandle`] can be made of per
/// [WebAuthn](https://www.w3.org/TR/webauthn-3/#user-handle).
pub const USER_HANDLE_MAX_LEN: usize = 64;
@@ -680,6 +712,12 @@ impl<T> UserHandle<T> {
&self.0
}
}
+impl<T: AsRef<[u8]>> UserHandle<T> {
+ /// Returns a `UserHandle` containing a `slice`.
+ pub(crate) fn as_slice(&self) -> UserHandle<&[u8]> {
+ UserHandle(self.0.as_ref())
+ }
+}
#[cfg(any(feature = "bin", feature = "custom"))]
impl<'a> UserHandle<&'a [u8]> {
/// Creates a `UserHandle` from a `slice`.
@@ -936,16 +974,26 @@ pub enum ResidentKeyRequirement {
/// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred).
Preferred,
}
-/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints)
+#[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)]
pub enum CrossPlatformHint {
/// No hints.
#[default]
None,
- /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-security-key).
+ /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-security-key).
SecurityKey,
- /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid).
+ /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-hybrid).
Hybrid,
/// [`Self::SecurityKey`] and [`Self::Hybrid`].
SecurityKeyHybrid,
@@ -964,14 +1012,26 @@ impl From<CrossPlatformHint> for Hint {
}
}
}
-/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints)
+#[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)]
pub enum PlatformHint {
/// No hints.
#[default]
None,
- /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-client-device).
+ /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhint-client-device).
ClientDevice,
}
impl From<PlatformHint> for Hint {
@@ -983,6 +1043,15 @@ 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)]
@@ -1046,6 +1115,18 @@ 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 {
@@ -1149,6 +1230,14 @@ impl AuthenticatorSelectionCriteria {
.validate(require_auth_attachment, auth_attachment)
}
}
+#[cfg(test)]
+impl PartialEq for AuthenticatorSelectionCriteria {
+ fn eq(&self, other: &Self) -> bool {
+ self.authenticator_attachment == other.authenticator_attachment
+ && self.resident_key == other.resident_key
+ && self.user_verification == other.user_verification
+ }
+}
/// Helper that verifies the overlap of [`PublicKeyCredentialCreationOptions::start_ceremony`] and
/// [`RegistrationServerState::decode`].
const fn validate_options_helper(
@@ -1667,6 +1756,15 @@ impl RegistrationServerState {
}
})
}
+ #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))]
+ fn is_eq(&self, other: &Self) -> bool {
+ self.mediation == other.mediation
+ && self.challenge == other.challenge
+ && self.pub_key_cred_params == other.pub_key_cred_params
+ && self.authenticator_selection == other.authenticator_selection
+ && self.extensions == other.extensions
+ && self.expiration == other.expiration
+ }
}
impl ServerState for RegistrationServerState {
#[cfg(any(doc, not(feature = "serializable_server_state")))]
@@ -1684,7 +1782,7 @@ impl ServerState for RegistrationServerState {
self.challenge
}
}
-impl Ceremony for RegistrationServerState {
+impl Ceremony<()> for RegistrationServerState {
type R = Registration;
fn rand_challenge(&self) -> SentChallenge {
self.challenge
@@ -1755,10 +1853,8 @@ mod tests {
},
AsciiDomain,
},
- AuthenticatorAttachmentReq, Challenge, CredProtect, CredentialMediationRequirement,
- Extension, ExtensionInfo, Hint, PublicKeyCredentialCreationOptions,
- PublicKeyCredentialUserEntity, RegistrationServerState, ResidentKeyRequirement, RpId,
- UserHandle, UserVerificationRequirement,
+ Challenge, CredProtect, Extension, ExtensionInfo, PublicKeyCredentialCreationOptions,
+ PublicKeyCredentialUserEntity, RegistrationServerState, RpId, UserHandle,
};
#[cfg(all(feature = "custom", feature = "serializable_server_state"))]
use ed25519_dalek::{Signer as _, SigningKey};
@@ -2104,64 +2200,9 @@ mod tests {
attestation_object[32..96].copy_from_slice(sig.to_bytes().as_slice());
attestation_object.truncate(261);
let server = opts.start_ceremony()?.0;
- let server_2 = RegistrationServerState::decode(server.encode()?.as_slice())?;
- assert!(
- matches!(server.mediation, CredentialMediationRequirement::Optional)
- && matches!(server_2.mediation, CredentialMediationRequirement::Optional)
- );
- assert_eq!(server.challenge.0, server_2.challenge.0);
- assert_eq!(server.pub_key_cred_params.0, server_2.pub_key_cred_params.0);
- assert!(
- matches!(server.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
- && matches!(server_2.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
- );
- assert!(
- matches!(
- server.authenticator_selection.resident_key,
- ResidentKeyRequirement::Required
- ) && matches!(
- server_2.authenticator_selection.resident_key,
- ResidentKeyRequirement::Required
- )
- );
- assert!(
- matches!(
- server.authenticator_selection.user_verification,
- UserVerificationRequirement::Required
- ) && matches!(
- server_2.authenticator_selection.user_verification,
- UserVerificationRequirement::Required
- )
- );
- assert!(server.extensions.cred_props.is_none() && server_2.extensions.cred_props.is_none());
- assert!(
- matches!(
- server.extensions.cred_protect,
- CredProtect::UserVerificationRequired(info) if matches!(info, ExtensionInfo::RequireEnforceValue)
- ) && matches!(
- server_2.extensions.cred_protect,
- CredProtect::UserVerificationRequired(info) if matches!(info, ExtensionInfo::RequireEnforceValue)
- )
- );
- let (pin, info) = server.extensions.min_pin_length.unwrap();
- assert!(
- server_2
- .extensions
- .min_pin_length
- .map_or(false, |(pin2, info2)| pin == pin2
- && matches!(info, ExtensionInfo::RequireEnforceValue)
- && matches!(info2, ExtensionInfo::RequireEnforceValue))
- );
- assert!(
- matches!(
- server.extensions.prf.unwrap(),
- ExtensionInfo::RequireEnforceValue
- ) && server_2.extensions.prf.map_or(false, |info| matches!(
- info,
- ExtensionInfo::RequireEnforceValue
- ))
- );
- assert_eq!(server.expiration, server_2.expiration);
+ assert!(server.is_eq(&RegistrationServerState::decode(
+ server.encode()?.as_slice()
+ )?));
Ok(())
}
}
diff --git a/src/request/register/bin.rs b/src/request/register/bin.rs
@@ -31,6 +31,14 @@ impl Decode for UserHandle<Vec<u8>> {
}
}
}
+impl<'b> Decode for UserHandle<&'b [u8]> {
+ type Input<'a> = &'b [u8];
+ type Err = UserHandleErr;
+ #[inline]
+ fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ UserHandle::<&[u8]>::from_slice(input)
+ }
+}
impl<const LEN: usize> Decode for UserHandle<[u8; LEN]>
where
Self: Default,
@@ -73,16 +81,16 @@ impl Display for DecodeNicknameErr {
}
}
impl Error for DecodeNicknameErr {}
-impl Decode for Nickname<'_> {
- type Input<'a> = String;
+impl<'b> Decode for Nickname<'b> {
+ type Input<'a> = &'b str;
type Err = DecodeNicknameErr;
#[inline]
fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
- match Nickname::try_from(input.as_str()).map_err(DecodeNicknameErr::Nickname) {
+ match Nickname::try_from(input).map_err(DecodeNicknameErr::Nickname) {
Ok(v) => match v.0 {
Cow::Borrowed(name) => {
- if name == input.as_str() {
- Ok(Self(Cow::Owned(input)))
+ if name == input {
+ Ok(Self(Cow::Borrowed(input)))
} else {
Err(DecodeNicknameErr::NotCanonical)
}
@@ -124,16 +132,16 @@ impl Display for DecodeUsernameErr {
}
}
impl Error for DecodeUsernameErr {}
-impl Decode for Username<'_> {
- type Input<'a> = String;
+impl<'b> Decode for Username<'b> {
+ type Input<'a> = &'b str;
type Err = DecodeUsernameErr;
#[inline]
fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
- match Username::try_from(input.as_str()).map_err(DecodeUsernameErr::Username) {
+ match Username::try_from(input).map_err(DecodeUsernameErr::Username) {
Ok(v) => match v.0 {
Cow::Borrowed(name) => {
- if name == input.as_str() {
- Ok(Self(Cow::Owned(input)))
+ if name == input {
+ Ok(Self(Cow::Borrowed(input)))
} else {
Err(DecodeUsernameErr::NotCanonical)
}
diff --git a/src/request/register/custom.rs b/src/request/register/custom.rs
@@ -1,16 +1,11 @@
-use super::{USER_HANDLE_MAX_LEN, USER_HANDLE_MIN_LEN, UserHandle, UserHandleErr};
-#[expect(clippy::fallible_impl_from, reason = "backward compatible fix")]
-impl<const LEN: usize> From<[u8; LEN]> for UserHandle<[u8; LEN]> {
- #[expect(clippy::panic, reason = "backward compatible fix")]
+use super::{UserHandle, UserHandleErr};
+impl<const LEN: usize> From<[u8; LEN]> for UserHandle<[u8; LEN]>
+where
+ Self: Default,
+{
#[inline]
fn from(value: [u8; LEN]) -> Self {
- if (USER_HANDLE_MIN_LEN..=USER_HANDLE_MAX_LEN).contains(&value.len()) {
- Self(value)
- } else {
- panic!(
- "UserHandle::from must only be passed an array of length 1 to 64 inclusively. Update webauthn_rp to 0.3.0 or greater to avoid this `panic` possibility"
- );
- }
+ Self(value)
}
}
impl<'a: 'b, 'b> TryFrom<&'a [u8]> for UserHandle<&'b [u8]> {
diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs
@@ -912,8 +912,10 @@ where
deserializer.deserialize_str(UserHandleVisitor)
}
}
-impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de>
- for PublicKeyCredentialUserEntity<'name, 'display_name, Vec<u8>>
+impl<'de: 'name + 'display_name, 'name, 'display_name, T> Deserialize<'de>
+ for PublicKeyCredentialUserEntity<'name, 'display_name, T>
+where
+ UserHandle<T>: Deserialize<'de>,
{
/// Deserializes a `struct` based on
/// [`PublicKeyCredentialUserEntityJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentityjson).
@@ -921,9 +923,9 @@ impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de>
/// # Examples
///
/// ```
- /// # use webauthn_rp::request::register::{Nickname, PublicKeyCredentialUserEntity};
+ /// # use webauthn_rp::request::register::{Nickname, PublicKeyCredentialUserEntity, USER_HANDLE_MIN_LEN};
/// assert_eq!(
- /// serde_json::from_str::<PublicKeyCredentialUserEntity<Vec<u8>>>(
+ /// serde_json::from_str::<PublicKeyCredentialUserEntity<[u8; USER_HANDLE_MIN_LEN]>>(
/// serde_json::json!({
/// "name": "pythagoras.of.samos",
/// "id": "AA",
@@ -959,9 +961,13 @@ impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de>
D: Deserializer<'de>,
{
/// `Visitor` for `PublicKeyCredentialUserEntity`.
- struct PublicKeyCredentialUserEntityVisitor<'a, 'b>(PhantomData<fn() -> (&'a (), &'b ())>);
- impl<'d: 'a + 'b, 'a, 'b> Visitor<'d> for PublicKeyCredentialUserEntityVisitor<'a, 'b> {
- type Value = PublicKeyCredentialUserEntity<'a, 'b, Vec<u8>>;
+ #[expect(clippy::type_complexity, reason = "type alias doesn't fit well with lifetimes")]
+ struct PublicKeyCredentialUserEntityVisitor<'a, 'b, U>(PhantomData<fn() -> (&'a (), &'b (), U)>);
+ impl<'d: 'a + 'b, 'a, 'b, U> Visitor<'d> for PublicKeyCredentialUserEntityVisitor<'a, 'b, U>
+ where
+ UserHandle<U>: Deserialize<'d>,
+ {
+ type Value = PublicKeyCredentialUserEntity<'a, 'b, U>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("PublicKeyCredentialUserEntity")
}
@@ -988,7 +994,7 @@ impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de>
impl Visitor<'_> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- formatter.write_str("'name', 'id', or 'displayName'")
+ write!(formatter, "'{NAME}', '{ID}', '{DISPLAY_NAME}'")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
diff --git a/src/response.rs b/src/response.rs
@@ -33,7 +33,7 @@ use ser_relaxed::SerdeJsonErr;
/// # #[cfg(not(feature = "serializable_server_state"))]
/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
/// # use webauthn_rp::{
-/// # request::{auth::{error::RequestOptionsErr, AuthenticationClientState, PublicKeyCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::UserHandle, AsciiDomain, BackupReq, RpId},
+/// # request::{auth::{error::RequestOptionsErr, AuthenticationClientState, PublicKeyCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN}, AsciiDomain, BackupReq, RpId},
/// # response::{auth::{error::AuthCeremonyErr, Authentication}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKey, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId},
/// # AuthenticatedCredential, CredentialErr
/// # };
@@ -89,7 +89,7 @@ use ser_relaxed::SerdeJsonErr;
/// InsertResult::Success
/// ));
/// # #[cfg(feature = "serde")]
-/// let authentication = serde_json::from_str::<Authentication>(get_authentication_json(client).as_str())?;
+/// let authentication = serde_json::from_str::<Authentication<[u8; USER_HANDLE_MAX_LEN]>>(get_authentication_json(client).as_str())?;
/// // `UserHandle` must exist since we sent an empty `AllowedCredentials`.
/// # #[cfg(feature = "serde")]
/// let user_handle = authentication.response().user_handle().ok_or(E::MissingUserHandle)?;
@@ -147,7 +147,7 @@ mod cbor;
/// Contains functionality that needs to be accessible when `bin` or `serde` are not enabled.
#[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
#[cfg(feature = "custom")]
-mod custom;
+pub mod custom;
/// Contains error types.
pub mod error;
/// Contains functionality for completing the
@@ -160,7 +160,7 @@ pub mod error;
/// # #[cfg(not(feature = "serializable_server_state"))]
/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
/// # use webauthn_rp::{
-/// # request::{register::{error::CreationOptionsErr, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomain, PublicKeyCredentialDescriptor, RpId},
+/// # request::{register::{error::CreationOptionsErr, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomain, PublicKeyCredentialDescriptor, RpId},
/// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData},
/// # RegisteredCredential
/// # };
@@ -217,9 +217,9 @@ pub mod error;
/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom", feature = "serde_relaxed"))]
/// insert_cred(ceremonies.take(®istration.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, handle, ®istration, &ver_opts)?);
/// /// Extract `UserHandle` from session cookie if this is not the first credential registered.
-/// fn get_user_handle() -> UserHandle<Vec<u8>> {
+/// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MAX_LEN]> {
/// // ⋮
-/// # UserHandle::new()
+/// # UserHandle::new_rand()
/// }
/// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`.
/// ///
@@ -483,7 +483,7 @@ impl PartialEq<AuthenticatorAttachment> for &AuthenticatorAttachment {
}
}
/// The maximum number of bytes that can make up a Credential ID
-/// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#credential-id)
+/// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#credential-id).
pub const CRED_ID_MAX_LEN: usize = 1023;
/// The minimum number of bytes that can make up a Credential ID
/// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#credential-id).
diff --git a/src/response/auth.rs b/src/response/auth.rs
@@ -1,24 +1,27 @@
+#[cfg(feature = "serde_relaxed")]
+use self::{
+ super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr},
+ ser_relaxed::{AuthenticationRelaxed, CustomAuthentication},
+};
#[cfg(all(doc, feature = "serde_relaxed"))]
use super::super::request::FixedCapHashSet;
#[cfg(doc)]
use super::super::{
+ AuthenticatedCredential, RegisteredCredential, StaticState,
request::{
- auth::{AuthenticationServerState, PublicKeyCredentialRequestOptions},
Challenge,
+ auth::{AuthenticationServerState, PublicKeyCredentialRequestOptions},
},
- AuthenticatedCredential, RegisteredCredential, StaticState,
};
-#[cfg(feature = "serde_relaxed")]
-use super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr};
use super::{
super::UserHandle,
+ AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment,
+ CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor,
+ LimitedVerificationParser, ParsedAuthData, Response, SentChallenge,
auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr},
cbor,
error::CollectedClientDataErr,
register::CompressedPubKey,
- AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment,
- CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor,
- LimitedVerificationParser, ParsedAuthData, Response, SentChallenge,
};
use core::convert::Infallible;
use ed25519_dalek::{Signature, Verifier as _};
@@ -26,8 +29,10 @@ use p256::ecdsa::DerSignature as P256DerSig;
use p384::ecdsa::DerSignature as P384DerSig;
use rsa::{
pkcs1v15,
- sha2::{digest::Digest as _, Sha256},
+ sha2::{Sha256, digest::Digest as _},
};
+#[cfg(feature = "serde_relaxed")]
+use serde::Deserialize;
/// Contains error types.
pub mod error;
/// Contains functionality to deserialize data from a client.
@@ -273,7 +278,7 @@ impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> {
}
/// [`AuthenticatorAssertionResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse).
#[derive(Debug)]
-pub struct AuthenticatorAssertion {
+pub struct AuthenticatorAssertion<User> {
/// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
client_data_json: Vec<u8>,
/// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata)
@@ -282,9 +287,9 @@ pub struct AuthenticatorAssertion {
/// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature).
signature: Vec<u8>,
/// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
- user_handle: Option<UserHandle<Vec<u8>>>,
+ user_handle: Option<UserHandle<User>>,
}
-impl AuthenticatorAssertion {
+impl<User> AuthenticatorAssertion<User> {
/// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
#[inline]
#[must_use]
@@ -312,35 +317,99 @@ impl AuthenticatorAssertion {
pub fn signature(&self) -> &[u8] {
self.signature.as_slice()
}
+ /// Constructs an instance of `Self` with the contained data.
+ ///
+ /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
+ /// of available capacity; if not, a reallocation will occur.
+ fn new_inner(
+ client_data_json: Vec<u8>,
+ mut authenticator_data: Vec<u8>,
+ signature: Vec<u8>,
+ user_handle: Option<UserHandle<User>>,
+ ) -> Self {
+ authenticator_data
+ .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
+ Self {
+ client_data_json,
+ authenticator_data_and_c_data_hash: authenticator_data,
+ signature,
+ user_handle,
+ }
+ }
+}
+impl<User: AsRef<[u8]>> AuthenticatorAssertion<User> {
/// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
#[inline]
#[must_use]
pub fn user_handle(&self) -> Option<UserHandle<&[u8]>> {
- self.user_handle.as_ref().map(UserHandle::from)
+ self.user_handle.as_ref().map(UserHandle::as_slice)
}
+}
+impl AuthenticatorAssertion<Vec<u8>> {
/// Constructs an instance of `Self` with the contained data.
///
/// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
/// of available capacity; if not, a reallocation will occur.
#[inline]
#[must_use]
- pub fn new(
+ pub fn new_user_vec(
client_data_json: Vec<u8>,
- mut authenticator_data: Vec<u8>,
+ authenticator_data: Vec<u8>,
signature: Vec<u8>,
user_handle: Option<UserHandle<Vec<u8>>>,
) -> Self {
- authenticator_data
- .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
- Self {
+ Self::new_inner(client_data_json, authenticator_data, signature, user_handle)
+ }
+ /// Same as [`Self::new_user_vec`] except `user_handle` is required.
+ #[inline]
+ #[must_use]
+ pub fn with_user_vec(
+ client_data_json: Vec<u8>,
+ authenticator_data: Vec<u8>,
+ signature: Vec<u8>,
+ user_handle: UserHandle<Vec<u8>>,
+ ) -> Self {
+ Self::new_inner(
client_data_json,
- authenticator_data_and_c_data_hash: authenticator_data,
+ authenticator_data,
signature,
- user_handle,
- }
+ Some(user_handle),
+ )
}
}
-impl AuthResponse for AuthenticatorAssertion {
+impl<const USER_LEN: usize> AuthenticatorAssertion<[u8; USER_LEN]> {
+ /// Constructs an instance of `Self` with the contained data.
+ ///
+ /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
+ /// of available capacity; if not, a reallocation will occur.
+ #[inline]
+ #[must_use]
+ pub fn new(
+ client_data_json: Vec<u8>,
+ authenticator_data: Vec<u8>,
+ signature: Vec<u8>,
+ user_handle: Option<UserHandle<[u8; USER_LEN]>>,
+ ) -> Self {
+ Self::new_inner(client_data_json, authenticator_data, signature, user_handle)
+ }
+ /// Same as [`Self::new`] except `user_handle` is required.
+ #[inline]
+ #[must_use]
+ pub fn with_user(
+ client_data_json: Vec<u8>,
+ authenticator_data: Vec<u8>,
+ signature: Vec<u8>,
+ user_handle: UserHandle<[u8; USER_LEN]>,
+ ) -> Self {
+ Self::new_inner(
+ client_data_json,
+ authenticator_data,
+ signature,
+ Some(user_handle),
+ )
+ }
+}
+impl<User> AuthResponse for AuthenticatorAssertion<User> {
type Auth<'a>
= AuthenticatorData<'a>
where
@@ -356,25 +425,31 @@ impl AuthResponse for AuthenticatorAssertion {
> {
/// Always `panic`s.
#[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[expect(
+ clippy::extra_unused_type_parameters,
+ reason = "same function signature as when serde_relaxed is enabled"
+ )]
#[cfg(not(feature = "serde_relaxed"))]
- fn get_client_collected_data(_: &[u8]) -> ! {
- unreachable!("AuthenticatorAssertion::parse_data_and_verify_sig must be passed false when serde_relaxed is not enabled");
+ fn get_client_collected_data<'b, U: 'b>(_: &'b [u8]) -> ! {
+ unreachable!(
+ "AuthenticatorAssertion::parse_data_and_verify_sig must be passed false when serde_relaxed is not enabled"
+ );
}
/// Parses `data` using `CollectedClientData::from_client_data_json_relaxed::<false>`.
#[cfg(feature = "serde_relaxed")]
- fn get_client_collected_data(
- data: &[u8],
+ fn get_client_collected_data<'b, U: 'b>(
+ data: &'b [u8],
) -> Result<
- CollectedClientData<'_>,
+ CollectedClientData<'b>,
AuthRespErr<
- <<AuthenticatorAssertion as AuthResponse>::Auth<'_> as AuthDataContainer<'_>>::Err,
+ <<AuthenticatorAssertion<U> as AuthResponse>::Auth<'b> as AuthDataContainer<'b>>::Err,
>,
- > {
+ >{
CollectedClientData::from_client_data_json_relaxed::<false>(data)
.map_err(AuthRespErr::CollectedClientDataRelaxed)
}
if relaxed {
- get_client_collected_data(self.client_data_json.as_slice())
+ get_client_collected_data::<'_, User>(self.client_data_json.as_slice())
} else {
CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice())
.map_err(AuthRespErr::CollectedClientData)
@@ -448,15 +523,15 @@ impl AuthResponse for AuthenticatorAssertion {
reason = "no invariants to uphold"
)]
#[derive(Debug)]
-pub struct Authentication {
+pub struct Authentication<User> {
/// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
pub(crate) raw_id: CredentialId<Vec<u8>>,
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response)
- pub(crate) response: AuthenticatorAssertion,
+ pub(crate) response: AuthenticatorAssertion<User>,
/// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
pub(crate) authenticator_attachment: AuthenticatorAttachment,
}
-impl Authentication {
+impl<User> Authentication<User> {
/// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
#[inline]
#[must_use]
@@ -466,7 +541,7 @@ impl Authentication {
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
#[inline]
#[must_use]
- pub const fn response(&self) -> &AuthenticatorAssertion {
+ pub const fn response(&self) -> &AuthenticatorAssertion<User> {
&self.response
}
/// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
@@ -482,7 +557,7 @@ impl Authentication {
#[must_use]
pub const fn new(
raw_id: CredentialId<Vec<u8>>,
- response: AuthenticatorAssertion,
+ response: AuthenticatorAssertion<User>,
authenticator_attachment: AuthenticatorAttachment,
) -> Self {
Self {
@@ -533,9 +608,37 @@ impl Authentication {
self.response.client_data_json.as_slice(),
)
}
+ /// Convenience function for [`AuthenticationRelaxed::deserialize`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`AuthenticationRelaxed::deserialize`] does.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ #[inline]
+ pub fn from_json_relaxed<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr>
+ where
+ UserHandle<User>: Deserialize<'a>,
+ {
+ serde_json::from_slice::<AuthenticationRelaxed<User>>(json).map(|val| val.0)
+ }
+ /// Convenience function for [`CustomAuthentication::deserialize`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`CustomAuthentication::deserialize`] does.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ #[inline]
+ pub fn from_json_custom<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr>
+ where
+ UserHandle<User>: Deserialize<'a>,
+ {
+ serde_json::from_slice::<CustomAuthentication<User>>(json).map(|val| val.0)
+ }
}
-impl Response for Authentication {
- type Auth = AuthenticatorAssertion;
+impl<User> Response for Authentication<User> {
+ type Auth = AuthenticatorAssertion<User>;
fn auth(&self) -> &Self::Auth {
&self.response
}
diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs
@@ -10,36 +10,95 @@ use super::{
ClientExtensions,
},
},
+ Authentication, AuthenticatorAssertion, UserHandle,
error::UnknownCredentialOptions,
- Authentication, AuthenticatorAssertion,
};
#[cfg(doc)]
-use super::{AuthenticatorAttachment, CredentialId, UserHandle};
+use super::{AuthenticatorAttachment, CredentialId};
use core::{
fmt::{self, Formatter},
marker::PhantomData,
str,
};
use data_encoding::BASE64URL_NOPAD;
-use rsa::sha2::{digest::OutputSizeUser as _, Sha256};
+use rsa::sha2::{Sha256, digest::OutputSizeUser as _};
use serde::{
de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor},
ser::{Serialize, SerializeStruct as _, Serializer},
};
+/// Authenticator data.
+pub(super) struct AuthData(pub Vec<u8>);
+impl<'e> Deserialize<'e> for AuthData {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `AuthData`.
+ struct AuthDataVisitor;
+ impl Visitor<'_> for AuthDataVisitor {
+ type Value = AuthData;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ 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"
+ )]
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ crate::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
+ // if usize::MAX / 4 < 32 => usize::MAX < 128 < u8::MAX; thus overflow is not
+ // possible.
+ // We add 32 since the SHA-256 hash of `clientDataJSON` will be added to the
+ // 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)
+ })
+ })
+ }
+ }
+ deserializer.deserialize_str(AuthDataVisitor)
+ }
+}
/// `Visitor` for `AuthenticatorAssertion`.
///
/// Unknown fields are ignored and only `clientDataJSON`, `authenticatorData`, and `signature` are required iff
/// `RELAXED`.
-pub(super) struct AuthenticatorAssertionVisitor<const RELAXED: bool>;
-impl<'d, const R: bool> Visitor<'d> for AuthenticatorAssertionVisitor<R> {
- type Value = AuthenticatorAssertion;
+pub(super) struct AuthenticatorAssertionVisitor<const RELAXED: bool, User>(
+ PhantomData<fn() -> User>,
+);
+impl<const RELAXED: bool, USER> AuthenticatorAssertionVisitor<RELAXED, USER> {
+ /// Returns `Self`.
+ pub fn new() -> Self {
+ Self(PhantomData)
+ }
+}
+impl<'d, const R: bool, User> Visitor<'d> for AuthenticatorAssertionVisitor<R, User>
+where
+ UserHandle<User>: Deserialize<'d>,
+{
+ type Value = AuthenticatorAssertion<User>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("AuthenticatorAssertion")
}
- #[expect(
- clippy::too_many_lines,
- reason = "don't want to move code to an outer scope"
- )]
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'d>,
@@ -67,7 +126,10 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAssertionVisitor<R> {
impl<const IG: bool> Visitor<'_> for FieldVisitor<IG> {
type Value = Field<IG>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- write!(formatter, "'{CLIENT_DATA_JSON}', '{AUTHENTICATOR_DATA}', '{SIGNATURE}', or '{USER_HANDLE}'")
+ write!(
+ formatter,
+ "'{CLIENT_DATA_JSON}', '{AUTHENTICATOR_DATA}', '{SIGNATURE}', or '{USER_HANDLE}'"
+ )
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
@@ -91,60 +153,6 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAssertionVisitor<R> {
deserializer.deserialize_identifier(FieldVisitor::<I>)
}
}
- /// Authenticator data.
- struct AuthData(Vec<u8>);
- impl<'e> Deserialize<'e> for AuthData {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'e>,
- {
- /// `Visitor` for `AuthData`.
- struct AuthDataVisitor;
- impl Visitor<'_> for AuthDataVisitor {
- type Value = AuthData;
- fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- 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"
- )]
- fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
- where
- E: Error,
- {
- crate::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
- // if usize::MAX / 4 < 32 => usize::MAX < 128 < u8::MAX; thus overflow is not
- // possible.
- // We add 32 since the SHA-256 hash of `clientDataJSON` will be added to the
- // 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)
- })
- })
- }
- }
- deserializer.deserialize_str(AuthDataVisitor)
- }
- }
let mut client_data = None;
let mut auth = None;
let mut sig = None;
@@ -193,7 +201,7 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAssertionVisitor<R> {
.and_then(|authenticator_data| {
sig.ok_or_else(|| Error::missing_field(SIGNATURE))
.map(|signature| {
- AuthenticatorAssertion::new(
+ AuthenticatorAssertion::new_inner(
client_data_json,
authenticator_data,
signature,
@@ -215,7 +223,10 @@ const USER_HANDLE: &str = "userHandle";
/// Fields in `AuthenticatorAssertionResponseJSON`.
pub(super) const AUTH_ASSERT_FIELDS: &[&str; 4] =
&[CLIENT_DATA_JSON, AUTHENTICATOR_DATA, SIGNATURE, USER_HANDLE];
-impl<'de> Deserialize<'de> for AuthenticatorAssertion {
+impl<'de, User> Deserialize<'de> for AuthenticatorAssertion<User>
+where
+ UserHandle<User>: Deserialize<'de>,
+{
/// Deserializes a `struct` based on
/// [`AuthenticatorAssertionResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorassertionresponsejson).
///
@@ -236,7 +247,7 @@ impl<'de> Deserialize<'de> for AuthenticatorAssertion {
deserializer.deserialize_struct(
"AuthenticatorAssertion",
AUTH_ASSERT_FIELDS,
- AuthenticatorAssertionVisitor::<false>,
+ AuthenticatorAssertionVisitor::<false, User>::new(),
)
}
}
@@ -350,7 +361,10 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputs {
)
}
}
-impl<'de> Deserialize<'de> for Authentication {
+impl<'de, User> Deserialize<'de> for Authentication<User>
+where
+ UserHandle<User>: Deserialize<'de>,
+{
/// Deserializes a `struct` based on
/// [`AuthenticationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson).
///
@@ -386,7 +400,7 @@ impl<'de> Deserialize<'de> for Authentication {
where
D: Deserializer<'de>,
{
- PublicKeyCredential::<false, false, AuthenticatorAssertion, ClientExtensionsOutputs>::deserialize(
+ PublicKeyCredential::<false, false, AuthenticatorAssertion<User>, ClientExtensionsOutputs>::deserialize(
deserializer,
)
.map(|cred| Self {
@@ -437,7 +451,10 @@ impl Serialize for UnknownCredentialOptions<'_, '_> {
}
#[cfg(test)]
mod tests {
- use super::super::{Authentication, AuthenticatorAttachment};
+ use super::super::{
+ super::super::request::register::USER_HANDLE_MIN_LEN, Authentication,
+ AuthenticatorAttachment,
+ };
use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Digest as _, Sha256};
use serde::de::{Error as _, Unexpected};
@@ -492,32 +509,34 @@ mod tests {
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::<Authentication>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "authenticatorAttachment": "cross-platform",
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |auth| auth.response.client_data_json
- == c_data_json.as_bytes()
- && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data
- && auth.response.authenticator_data_and_c_data_hash[37..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && matches!(
- auth.authenticator_attachment,
- AuthenticatorAttachment::CrossPlatform
- )));
+ assert!(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |auth| auth.response.client_data_json
+ == c_data_json.as_bytes()
+ && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data
+ && auth.response.authenticator_data_and_c_data_hash[37..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && matches!(
+ auth.authenticator_attachment,
+ AuthenticatorAttachment::CrossPlatform
+ ))
+ );
// `id` and `rawId` mismatch.
let mut err = Error::invalid_value(
Unexpected::Bytes(
@@ -531,7 +550,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "ABABABABABABABABABABAA",
@@ -556,7 +575,7 @@ mod tests {
// missing `id`.
err = Error::missing_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -582,7 +601,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": null,
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -606,7 +625,7 @@ mod tests {
// missing `rawId`.
err = Error::missing_field("rawId").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -631,7 +650,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": null,
@@ -657,7 +676,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -682,7 +701,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -706,7 +725,7 @@ mod tests {
// Missing `signature`.
err = Error::missing_field("signature").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -731,7 +750,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -753,62 +772,68 @@ mod tests {
err
);
// Missing `userHandle`.
- assert!(serde_json::from_str::<Authentication>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `userHandle`.
- assert!(serde_json::from_str::<Authentication>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": null,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": null,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `authenticatorAttachment`.
- assert!(serde_json::from_str::<Authentication>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "authenticatorAttachment": null,
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |auth| matches!(
- auth.authenticator_attachment,
- AuthenticatorAttachment::None
- )));
+ assert!(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "authenticatorAttachment": null,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |auth| matches!(
+ auth.authenticator_attachment,
+ AuthenticatorAttachment::None
+ ))
+ );
// Unknown `authenticatorAttachment`.
err = Error::invalid_value(
Unexpected::Str("Platform"),
@@ -817,7 +842,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -844,7 +869,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -869,7 +894,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -893,7 +918,7 @@ mod tests {
// Missing `response`.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -913,7 +938,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -934,7 +959,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -955,7 +980,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -983,7 +1008,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1007,7 +1032,7 @@ mod tests {
// Missing `type`.
err = Error::missing_field("type").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1032,7 +1057,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1058,7 +1083,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1084,19 +1109,23 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(serde_json::json!(null).to_string().as_str())
- .unwrap_err()
- .to_string()
- .into_bytes()[..err.len()],
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!(null).to_string().as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
err
);
// Empty.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(serde_json::json!({}).to_string().as_str())
- .unwrap_err()
- .to_string()
- .into_bytes()[..err.len()],
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({}).to_string().as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
err
);
// Unknown field in `response`.
@@ -1113,7 +1142,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1140,7 +1169,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1180,7 +1209,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1205,7 +1234,7 @@ mod tests {
// Duplicate field in `PublicKeyCredential`.
err = Error::duplicate_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1280,58 +1309,62 @@ mod tests {
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::<Authentication>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "authenticatorAttachment": "cross-platform",
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |auth| auth.response.client_data_json
- == c_data_json.as_bytes()
- && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data
- && auth.response.authenticator_data_and_c_data_hash[37..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && matches!(
- auth.authenticator_attachment,
- AuthenticatorAttachment::CrossPlatform
- )));
+ assert!(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |auth| auth.response.client_data_json
+ == c_data_json.as_bytes()
+ && auth.response.authenticator_data_and_c_data_hash[..37] == auth_data
+ && auth.response.authenticator_data_and_c_data_hash[37..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && matches!(
+ auth.authenticator_attachment,
+ AuthenticatorAttachment::CrossPlatform
+ ))
+ );
// `null` `prf`.
- assert!(serde_json::from_str::<Authentication>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": null
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": null
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Unknown `clientExtensionResults`.
let mut err = Error::unknown_field("Prf", ["prf"].as_slice())
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1357,7 +1390,7 @@ mod tests {
// Duplicate field.
err = Error::duplicate_field("prf").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1383,31 +1416,33 @@ mod tests {
err
);
// `null` `results`.
- assert!(serde_json::from_str::<Authentication>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": {
- "results": null,
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "results": null,
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Duplicate field in `prf`.
err = Error::duplicate_field("results").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1437,7 +1472,7 @@ mod tests {
// Missing `first`.
err = Error::missing_field("first").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1463,60 +1498,64 @@ mod tests {
err
);
// `null` `first`.
- assert!(serde_json::from_str::<Authentication>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": {
- "results": {
- "first": null
- },
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "results": {
+ "first": null
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `second`.
- assert!(serde_json::from_str::<Authentication>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": {
- "results": {
- "first": null,
- "second": null
- },
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "results": {
+ "first": null,
+ "second": null
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Non-`null` `first`.
err = Error::invalid_type(Unexpected::Option, &"null")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1548,7 +1587,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1581,7 +1620,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1612,7 +1651,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1643,7 +1682,7 @@ mod tests {
// Duplicate field in `results`.
err = Error::duplicate_field("first").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication>(
+ serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs
@@ -1,20 +1,30 @@
+#![expect(
+ clippy::question_mark_used,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
#[cfg(doc)]
-use super::super::Challenge;
+use super::super::{Challenge, CredentialId};
use super::{
super::{
auth::ser::{
- AuthenticatorAssertionVisitor, ClientExtensionsOutputs, ClientExtensionsOutputsVisitor,
- AUTH_ASSERT_FIELDS, EXT_FIELDS,
+ AUTH_ASSERT_FIELDS, AuthData, AuthenticatorAssertionVisitor, ClientExtensionsOutputs,
+ ClientExtensionsOutputsVisitor, EXT_FIELDS,
+ },
+ ser::{
+ AuthenticationExtensionsPrfOutputsHelper, Base64DecodedVal, ClientExtensions,
+ PublicKeyCredential, Type,
},
- ser::{AuthenticationExtensionsPrfOutputsHelper, ClientExtensions, PublicKeyCredential},
ser_relaxed::AuthenticationExtensionsPrfValuesRelaxed,
},
- Authentication, AuthenticatorAssertion,
+ Authentication, AuthenticatorAssertion, AuthenticatorAttachment, UserHandle,
+};
+use core::{
+ fmt::{self, Formatter},
+ marker::PhantomData,
};
-use core::marker::PhantomData;
#[cfg(doc)]
use data_encoding::BASE64URL_NOPAD;
-use serde::de::{Deserialize, Deserializer};
+use serde::de::{Deserialize, Deserializer, Error, MapAccess, Visitor};
/// `newtype` around `ClientExtensionsOutputs` with a "relaxed" [`Self::deserialize`] implementation.
struct ClientExtensionsOutputsRelaxed(pub ClientExtensionsOutputs);
impl ClientExtensions for ClientExtensionsOutputsRelaxed {
@@ -49,8 +59,11 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputsRelaxed {
}
/// `newtype` around `AuthenticatorAssertion` with a "relaxed" [`Self::deserialize`] implementation.
#[derive(Debug)]
-pub struct AuthenticatorAssertionRelaxed(pub AuthenticatorAssertion);
-impl<'de> Deserialize<'de> for AuthenticatorAssertionRelaxed {
+pub struct AuthenticatorAssertionRelaxed<User>(pub AuthenticatorAssertion<User>);
+impl<'de, User> Deserialize<'de> for AuthenticatorAssertionRelaxed<User>
+where
+ UserHandle<User>: Deserialize<'de>,
+{
/// Same as [`AuthenticatorAssertion::deserialize`] except unknown keys are ignored.
///
/// Note that duplicate keys are still forbidden.
@@ -63,15 +76,18 @@ impl<'de> Deserialize<'de> for AuthenticatorAssertionRelaxed {
.deserialize_struct(
"AuthenticatorAssertionRelaxed",
AUTH_ASSERT_FIELDS,
- AuthenticatorAssertionVisitor::<true>,
+ AuthenticatorAssertionVisitor::<true, User>::new(),
)
.map(Self)
}
}
/// `newtype` around `Authentication` with a "relaxed" [`Self::deserialize`] implementation.
#[derive(Debug)]
-pub struct AuthenticationRelaxed(pub Authentication);
-impl<'de> Deserialize<'de> for AuthenticationRelaxed {
+pub struct AuthenticationRelaxed<User>(pub Authentication<User>);
+impl<'de, User> Deserialize<'de> for AuthenticationRelaxed<User>
+where
+ UserHandle<User>: Deserialize<'de>,
+{
/// Same as [`Authentication::deserialize`] except unknown keys are ignored;
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-response) is deserialized
/// via [`AuthenticatorAssertionRelaxed::deserialize`];
@@ -103,7 +119,7 @@ impl<'de> Deserialize<'de> for AuthenticationRelaxed {
PublicKeyCredential::<
true,
false,
- AuthenticatorAssertionRelaxed,
+ AuthenticatorAssertionRelaxed<User>,
ClientExtensionsOutputsRelaxed,
>::deserialize(deserializer)
.map(|cred| {
@@ -117,9 +133,289 @@ impl<'de> Deserialize<'de> for AuthenticationRelaxed {
})
}
}
+/// `newtype` around `Authentication` with a custom [`Self::deserialize`] implementation.
+#[derive(Debug)]
+pub struct CustomAuthentication<User>(pub Authentication<User>);
+impl<'de, User> Deserialize<'de> for CustomAuthentication<User>
+where
+ UserHandle<User>: Deserialize<'de>,
+{
+ /// Despite the spec having a
+ /// [pre-defined format](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson) that clients
+ /// can follow, the downside is the superfluous data it contains.
+ ///
+ /// There simply is no reason to send the [`CredentialId`] _two_ times. This redundant data puts RPs in
+ /// a position where they either ignore the data or parse the data to ensure no contradictions exist
+ /// (e.g., [FIDO conformance requires one to verify `id` and `rawId` exist and match](https://github.com/w3c/webauthn/issues/2119#issuecomment-2287875401)).
+ ///
+ /// While [`Authentication::deserialize`] _strictly_ adheres to the JSON definition, this implementation
+ /// strictly disallows superfluous data. Specifically the following JSON is required to be sent where duplicate
+ /// and unknown keys are disallowed:
+ ///
+ /// ```json
+ /// {
+ /// "authenticatorAttachment":null|"platform"|"cross-platform",
+ /// "authenticatorData":<base64url string>,
+ /// "clientDataJSON":<base64url string>,
+ /// "clientExtensionResults":{}|{"prf":null}|{"prf":{}}|{"prf":{"results":null}}|{"prf":{"results":{"first":null}}}|{"prf":{"results":{"first":null,"second":null}}},
+ /// "id":<see CredentialId::deserialize>,
+ /// "signature":<base64url string>,
+ /// "type":null|"public-key",
+ /// "userHandle":null|<see UserHandle::deserialize>
+ /// }
+ /// ```
+ ///
+ /// All of the above keys are required with the exceptions of `"authenticatorAttachment"`, `"type"`, and
+ /// `"userHandle"`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::{request::register::USER_HANDLE_MIN_LEN, response::auth::ser_relaxed::CustomAuthentication};
+ /// assert!(
+ /// // The below payload is technically valid, but `AuthenticationServerState::verify` will fail
+ /// // since the authenticatorData is not valid. This is true for `Authentication::deserialize`
+ /// // as well since authenticatorData parsing is always deferred.
+ /// serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ /// r#"{
+ /// "authenticatorData": "AA",
+ /// "authenticatorAttachment": "cross-platform",
+ /// "clientExtensionResults": {},
+ /// "clientDataJSON": "AA",
+ /// "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ /// "signature": "AA",
+ /// "type": "public-key",
+ /// "userHandle": "AA"
+ /// }"#
+ /// ).is_ok());
+ /// ```
+ #[expect(
+ clippy::too_many_lines,
+ reason = "want to hide; thus don't put in outer scope"
+ )]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `CustomAuthentication`.
+ struct CustomAuthenticationVisitor<U>(PhantomData<fn() -> U>);
+ impl<'d, U> Visitor<'d> for CustomAuthenticationVisitor<U>
+ where
+ UserHandle<U>: Deserialize<'d>,
+ {
+ type Value = CustomAuthentication<U>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("CustomAuthentication")
+ }
+ #[expect(
+ clippy::too_many_lines,
+ reason = "want to hide; thus don't put in outer scope"
+ )]
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Fields in the JSON.
+ enum Field {
+ /// `authenticatorAttachment` key.
+ AuthenticatorAttachment,
+ /// `authenticatorData` key.
+ AuthenticatorData,
+ /// `clientDataJSON` key.
+ ClientDataJson,
+ /// `clientExtensionResults` key.
+ ClientExtensionResults,
+ /// `id` key.
+ Id,
+ /// `signature` key.
+ Signature,
+ /// `type` key.
+ Type,
+ /// `userHandle` key.
+ UserHandle,
+ }
+ 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}', '{AUTHENTICATOR_DATA}', '{CLIENT_DATA_JSON}', '{CLIENT_EXTENSION_RESULTS}', '{ID}', '{SIGNATURE}', '{TYPE}', or '{USER_HANDLE}'"
+ )
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ AUTHENTICATOR_ATTACHMENT => Ok(Field::AuthenticatorAttachment),
+ AUTHENTICATOR_DATA => Ok(Field::AuthenticatorData),
+ CLIENT_DATA_JSON => Ok(Field::ClientDataJson),
+ CLIENT_EXTENSION_RESULTS => Ok(Field::ClientExtensionResults),
+ ID => Ok(Field::Id),
+ SIGNATURE => Ok(Field::Signature),
+ TYPE => Ok(Field::Type),
+ USER_HANDLE => Ok(Field::UserHandle),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut authenticator_attachment = None;
+ let mut authenticator_data = None;
+ let mut client_data_json = None;
+ let mut ext = false;
+ let mut id = None;
+ let mut signature = None;
+ let mut typ = false;
+ let mut user_handle = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::AuthenticatorAttachment => {
+ if authenticator_attachment.is_some() {
+ return Err(Error::duplicate_field(AUTHENTICATOR_ATTACHMENT));
+ }
+ authenticator_attachment = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::AuthenticatorData => {
+ if authenticator_data.is_some() {
+ return Err(Error::duplicate_field(AUTHENTICATOR_DATA));
+ }
+ authenticator_data =
+ map.next_value::<AuthData>().map(|val| Some(val.0))?;
+ }
+ Field::ClientDataJson => {
+ if client_data_json.is_some() {
+ return Err(Error::duplicate_field(CLIENT_DATA_JSON));
+ }
+ client_data_json = map
+ .next_value::<Base64DecodedVal>()
+ .map(|val| Some(val.0))?;
+ }
+ Field::ClientExtensionResults => {
+ if ext {
+ return Err(Error::duplicate_field(CLIENT_EXTENSION_RESULTS));
+ }
+ ext = map.next_value::<ClientExtensionsOutputs>().map(|_| true)?;
+ }
+ Field::Id => {
+ if id.is_some() {
+ return Err(Error::duplicate_field(ID));
+ }
+ id = map.next_value().map(Some)?;
+ }
+ Field::Signature => {
+ if signature.is_some() {
+ return Err(Error::duplicate_field(SIGNATURE));
+ }
+ signature = map
+ .next_value::<Base64DecodedVal>()
+ .map(|val| Some(val.0))?;
+ }
+ Field::Type => {
+ if typ {
+ return Err(Error::duplicate_field(TYPE));
+ }
+ typ = map.next_value::<Option<Type>>().map(|_| true)?;
+ }
+ Field::UserHandle => {
+ if user_handle.is_some() {
+ return Err(Error::duplicate_field(USER_HANDLE));
+ }
+ user_handle = map.next_value::<Option<_>>().map(Some)?;
+ }
+ }
+ }
+ authenticator_data
+ .ok_or_else(|| Error::missing_field(AUTHENTICATOR_DATA))
+ .and_then(|auth_data| {
+ client_data_json
+ .ok_or_else(|| Error::missing_field(CLIENT_DATA_JSON))
+ .and_then(|c_data| {
+ id.ok_or_else(|| Error::missing_field(ID))
+ .and_then(|raw_id| {
+ signature
+ .ok_or_else(|| Error::missing_field(SIGNATURE))
+ .and_then(|sig| {
+ if ext {
+ Ok(CustomAuthentication(Authentication {
+ response: AuthenticatorAssertion::new_inner(
+ c_data,
+ auth_data,
+ sig,
+ user_handle.unwrap_or_default(),
+ ),
+ authenticator_attachment:
+ authenticator_attachment.map_or(
+ AuthenticatorAttachment::None,
+ |auth_attach| {
+ auth_attach.unwrap_or(
+ AuthenticatorAttachment::None,
+ )
+ },
+ ),
+ raw_id,
+ }))
+ } else {
+ Err(Error::missing_field(
+ CLIENT_EXTENSION_RESULTS,
+ ))
+ }
+ })
+ })
+ })
+ })
+ }
+ }
+ /// `authenticatorAttachment` key.
+ const AUTHENTICATOR_ATTACHMENT: &str = "authenticatorAttachment";
+ /// `authenticatorData` key.
+ const AUTHENTICATOR_DATA: &str = "authenticatorData";
+ /// `clientDataJSON` key.
+ const CLIENT_DATA_JSON: &str = "clientDataJSON";
+ /// `clientExtensionResults` key.
+ const CLIENT_EXTENSION_RESULTS: &str = "clientExtensionResults";
+ /// `id` key.
+ const ID: &str = "id";
+ /// `signature` key.
+ const SIGNATURE: &str = "signature";
+ /// `type` key.
+ const TYPE: &str = "type";
+ /// `userHandle` key.
+ const USER_HANDLE: &str = "userHandle";
+ /// Fields.
+ const FIELDS: &[&str; 8] = &[
+ AUTHENTICATOR_ATTACHMENT,
+ AUTHENTICATOR_DATA,
+ CLIENT_DATA_JSON,
+ CLIENT_EXTENSION_RESULTS,
+ ID,
+ SIGNATURE,
+ TYPE,
+ USER_HANDLE,
+ ];
+ deserializer.deserialize_struct(
+ "CustomAuthentication",
+ FIELDS,
+ CustomAuthenticationVisitor(PhantomData),
+ )
+ }
+}
#[cfg(test)]
mod tests {
- use super::{super::AuthenticatorAttachment, AuthenticationRelaxed};
+ use super::{
+ super::{super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment},
+ AuthenticationRelaxed, CustomAuthentication,
+ };
use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Digest as _, Sha256};
use serde::de::{Error as _, Unexpected};
@@ -174,32 +470,34 @@ mod tests {
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::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "authenticatorAttachment": "cross-platform",
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |auth| auth.0.response.client_data_json
- == c_data_json.as_bytes()
- && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data
- && auth.0.response.authenticator_data_and_c_data_hash[37..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && matches!(
- auth.0.authenticator_attachment,
- AuthenticatorAttachment::CrossPlatform
- )));
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |auth| auth.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data
+ && auth.0.response.authenticator_data_and_c_data_hash[37..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && matches!(
+ auth.0.authenticator_attachment,
+ AuthenticatorAttachment::CrossPlatform
+ ))
+ );
// `id` and `rawId` mismatch.
let mut err = Error::invalid_value(
Unexpected::Bytes(
@@ -213,7 +511,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "ABABABABABABABABABABAA",
@@ -238,7 +536,7 @@ mod tests {
// missing `id`.
err = Error::missing_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -264,7 +562,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": null,
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -286,46 +584,50 @@ mod tests {
err
);
// missing `rawId`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `rawId`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": null,
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": null,
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Missing `authenticatorData`.
err = Error::missing_field("authenticatorData")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -350,7 +652,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -374,7 +676,7 @@ mod tests {
// Missing `signature`.
err = Error::missing_field("signature").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -399,7 +701,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -421,62 +723,68 @@ mod tests {
err
);
// Missing `userHandle`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `userHandle`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": null,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": null,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `authenticatorAttachment`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "authenticatorAttachment": null,
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |auth| matches!(
- auth.0.authenticator_attachment,
- AuthenticatorAttachment::None
- )));
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "authenticatorAttachment": null,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |auth| matches!(
+ auth.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ ))
+ );
// Unknown `authenticatorAttachment`.
err = Error::invalid_value(
Unexpected::Str("Platform"),
@@ -485,7 +793,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -512,7 +820,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -537,7 +845,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -561,7 +869,7 @@ mod tests {
// Missing `response`.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -581,7 +889,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -602,7 +910,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -619,81 +927,89 @@ mod tests {
err
);
// Missing `clientExtensionResults`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `clientExtensionResults`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": null,
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": null,
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Missing `type`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {},
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {},
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `type`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {},
- "type": null
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {},
+ "type": null
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Not exactly `public-type` `type`.
err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -719,7 +1035,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!(null).to_string().as_str()
)
.unwrap_err()
@@ -730,7 +1046,7 @@ mod tests {
// Empty.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({}).to_string().as_str()
)
.unwrap_err()
@@ -739,30 +1055,32 @@ mod tests {
err
);
// Unknown field in `response`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- "foo": true,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "foo": true,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Duplicate field in `response`.
err = Error::duplicate_field("userHandle")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -787,39 +1105,513 @@ mod tests {
err
);
// Unknown field in `PublicKeyCredential`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key",
+ "foo": true,
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // Duplicate field in `PublicKeyCredential`.
+ err = Error::duplicate_field("id").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\"
+ }},
+ \"clientExtensionResults\": {{}},
+ \"type\": \"public-key\"
+
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
"signature": b64_sig,
"userHandle": b64_user,
- },
- "clientExtensionResults": {},
- "type": "public-key",
- "foo": true,
- })
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |auth| auth.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data
+ && auth.0.response.authenticator_data_and_c_data_hash[37..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && matches!(
+ auth.0.authenticator_attachment,
+ AuthenticatorAttachment::CrossPlatform
+ ))
+ );
+ // missing `id`.
+ err = Error::missing_field("id").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `id`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": null,
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `authenticatorData`.
+ err = Error::missing_field("authenticatorData")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `authenticatorData`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorData")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": null,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `signature`.
+ err = Error::missing_field("signature").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `signature`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": null,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `userHandle`.
+ assert!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `null` `userHandle`.
+ assert!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": null,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `null` `authenticatorAttachment`.
+ assert!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "authenticatorAttachment": null,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |auth| matches!(
+ auth.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ ))
+ );
+ // Unknown `authenticatorAttachment`.
+ err = Error::invalid_value(
+ Unexpected::Str("Platform"),
+ &"'platform' or 'cross-platform'",
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "authenticatorAttachment": "Platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `clientDataJSON`.
+ err = Error::missing_field("clientDataJSON")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `clientDataJSON`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": null,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Empty.
+ err = Error::missing_field("authenticatorData")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({}).to_string().as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `clientExtensionResults`.
+ err = Error::missing_field("clientExtensionResults")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `clientExtensionResults`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "clientExtensionResults": null,
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ assert!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ assert!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ "type": null
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // Not exactly `public-type` `type`.
+ err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "clientExtensionResults": {},
+ "type": "Public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"CustomAuthentication")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!(null).to_string().as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown field.
+ err = Error::unknown_field(
+ "foo",
+ [
+ "authenticatorAttachment",
+ "authenticatorData",
+ "clientDataJSON",
+ "clientExtensionResults",
+ "id",
+ "signature",
+ "type",
+ "userHandle",
+ ]
+ .as_slice(),
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ "foo": true,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field.
+ err = Error::duplicate_field("userHandle")
.to_string()
- .as_str()
- )
- .is_ok());
- // Duplicate field in `PublicKeyCredential`.
- err = Error::duplicate_field("id").to_string().into_bytes();
+ .into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
- \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
- \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
- \"response\": {{
- \"clientDataJSON\": \"{b64_cdata}\",
- \"authenticatorData\": \"{b64_adata}\",
- \"signature\": \"{b64_sig}\",
- \"userHandle\": \"{b64_user}\"
- }},
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\",
+ \"userHandle\": \"{b64_user}\"
\"clientExtensionResults\": {{}},
\"type\": \"public-key\"
@@ -883,76 +1675,82 @@ mod tests {
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::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "authenticatorAttachment": "cross-platform",
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |auth| auth.0.response.client_data_json
- == c_data_json.as_bytes()
- && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data
- && auth.0.response.authenticator_data_and_c_data_hash[37..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && matches!(
- auth.0.authenticator_attachment,
- AuthenticatorAttachment::CrossPlatform
- )));
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |auth| auth.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && auth.0.response.authenticator_data_and_c_data_hash[..37] == auth_data
+ && auth.0.response.authenticator_data_and_c_data_hash[37..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && matches!(
+ auth.0.authenticator_attachment,
+ AuthenticatorAttachment::CrossPlatform
+ ))
+ );
// `null` `prf`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": null
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": null
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Unknown `clientExtensionResults`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "Prf": null
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "Prf": null
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Duplicate field.
let mut err = Error::duplicate_field("prf").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -978,31 +1776,33 @@ mod tests {
err
);
// `null` `results`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": {
- "results": null,
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "results": null,
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Duplicate field in `prf`.
err = Error::duplicate_field("results").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1030,82 +1830,88 @@ mod tests {
err
);
// Missing `first`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": {
- "results": {},
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "results": {},
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `first`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": {
- "results": {
- "first": null
- },
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "results": {
+ "first": null
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `second`.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": {
- "results": {
- "first": null,
- "second": null
- },
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "results": {
+ "first": null,
+ "second": null
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Non-`null` `first`.
err = Error::invalid_type(Unexpected::Option, &"null")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1137,7 +1943,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1170,7 +1976,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1197,57 +2003,61 @@ mod tests {
err
);
// Unknown `prf` field.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": {
- "foo": true,
- "results": null
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "foo": true,
+ "results": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Unknown `results` field.
- assert!(serde_json::from_str::<AuthenticationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "signature": b64_sig,
- "userHandle": b64_user,
- },
- "clientExtensionResults": {
- "prf": {
- "results": {
- "first": null,
- "Second": null
+ assert!(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "signature": b64_sig,
+ "userHandle": b64_user,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "results": {
+ "first": null,
+ "Second": null
+ }
}
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Duplicate field in `results`.
err = Error::duplicate_field("first").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
diff --git a/src/response/bin.rs b/src/response/bin.rs
@@ -102,6 +102,17 @@ impl Decode for CredentialId<Vec<u8>> {
}
}
}
+impl<'b> Decode for CredentialId<&'b [u8]> {
+ type Input<'a> = &'b [u8];
+ type Err = CredentialIdErr;
+ #[inline]
+ fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ match CredentialId::from_slice(input) {
+ Ok(_) => Ok(Self(input)),
+ Err(e) => Err(e),
+ }
+ }
+}
impl EncodeBuffer for CredentialId<&[u8]> {
#[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
diff --git a/src/response/custom.rs b/src/response/custom.rs
@@ -1,4 +1,5 @@
-use super::{CredentialId, CredentialIdErr};
+use super::{AuthTransports, AuthenticatorTransport, CredentialId, CredentialIdErr};
+use core::iter::FusedIterator;
impl<'a: 'b, 'b> TryFrom<&'a [u8]> for CredentialId<&'b [u8]> {
type Error = CredentialIdErr;
#[inline]
@@ -16,3 +17,158 @@ impl TryFrom<Vec<u8>> for CredentialId<Vec<u8>> {
}
}
}
+/// [`Iterator`] of [`AuthenticatorTransport`]s returned from
+/// [`AuthTransports::into_iter`].
+#[derive(Debug)]
+pub struct AuthTransportIter(AuthTransports);
+impl Iterator for AuthTransportIter {
+ type Item = AuthenticatorTransport;
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ let mut nxt = self.0.remove(AuthenticatorTransport::Ble);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::Ble);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::Hybrid);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::Hybrid);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::Internal);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::Internal);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::Nfc);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::Nfc);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::SmartCard);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::SmartCard);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::Usb);
+ if self.0.0 == nxt.0 {
+ None
+ } else {
+ self.0 = nxt;
+ Some(AuthenticatorTransport::Usb)
+ }
+ }
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ let count = self.len();
+ (count, Some(count))
+ }
+ #[inline]
+ fn count(self) -> usize
+ where
+ Self: Sized,
+ {
+ self.len()
+ }
+ #[inline]
+ fn last(mut self) -> Option<Self::Item>
+ where
+ Self: Sized,
+ {
+ self.next_back()
+ }
+}
+impl ExactSizeIterator for AuthTransportIter {
+ #[expect(clippy::as_conversions, reason = "comment justifies correctness")]
+ #[inline]
+ fn len(&self) -> usize {
+ // Maximum count is 6, so this is fine.
+ self.0.count() as usize
+ }
+}
+impl DoubleEndedIterator for AuthTransportIter {
+ #[inline]
+ fn next_back(&mut self) -> Option<Self::Item> {
+ let mut nxt = self.0.remove(AuthenticatorTransport::Usb);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::Usb);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::SmartCard);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::SmartCard);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::Nfc);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::Nfc);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::Internal);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::Internal);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::Hybrid);
+ if self.0.0 != nxt.0 {
+ self.0 = nxt;
+ return Some(AuthenticatorTransport::Hybrid);
+ }
+ nxt = self.0.remove(AuthenticatorTransport::Ble);
+ if self.0.0 == nxt.0 {
+ None
+ } else {
+ self.0 = nxt;
+ Some(AuthenticatorTransport::Ble)
+ }
+ }
+}
+impl FusedIterator for AuthTransportIter {}
+impl IntoIterator for AuthTransports {
+ type Item = AuthenticatorTransport;
+ type IntoIter = AuthTransportIter;
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ AuthTransportIter(self)
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::{AuthTransports, AuthenticatorTransport};
+ #[test]
+ fn iter_all() {
+ let mut iter = AuthTransports::ALL.into_iter();
+ assert_eq!(iter.len(), 6);
+ assert!(
+ iter.next()
+ .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Ble))
+ );
+ assert_eq!(iter.len(), 5);
+ assert!(
+ iter.next()
+ .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Hybrid))
+ );
+ assert_eq!(iter.len(), 4);
+ assert!(
+ iter.next_back()
+ .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Usb))
+ );
+ assert_eq!(iter.len(), 3);
+ assert!(iter.next().map_or(false, |tran| matches!(
+ tran,
+ AuthenticatorTransport::Internal
+ )));
+ assert_eq!(iter.len(), 2);
+ assert!(iter.next_back().map_or(false, |tran| matches!(
+ tran,
+ AuthenticatorTransport::SmartCard
+ )));
+ assert_eq!(iter.len(), 1);
+ assert!(
+ iter.next()
+ .map_or(false, |tran| matches!(tran, AuthenticatorTransport::Nfc))
+ );
+ assert_eq!(iter.len(), 0);
+ assert!(iter.next().is_none());
+ }
+}
diff --git a/src/response/register.rs b/src/response/register.rs
@@ -1,7 +1,10 @@
+#[cfg(feature = "serde_relaxed")]
+use self::{
+ super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr},
+ ser_relaxed::{CustomRegistration, RegistrationRelaxed},
+};
#[cfg(all(doc, feature = "serde_relaxed"))]
use super::super::request::FixedCapHashSet;
-#[cfg(feature = "serde_relaxed")]
-use super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr};
#[cfg(all(doc, feature = "bin"))]
use super::{
super::bin::{Decode, Encode},
@@ -9,7 +12,10 @@ use super::{
};
use super::{
super::request::register::ResidentKeyRequirement,
- cbor,
+ AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthTransports,
+ AuthenticatorAttachment, Backup, CborSuccess, ClientDataJsonParser as _, CollectedClientData,
+ CredentialId, Flag, FromCbor, LimitedVerificationParser, ParsedAuthData, Response,
+ SentChallenge, cbor,
error::CollectedClientDataErr,
register::error::{
AaguidErr, AttestationErr, AttestationObjectErr, AttestedCredentialDataErr,
@@ -17,20 +23,16 @@ use super::{
CompressedP384PubKeyErr, CoseKeyErr, Ed25519PubKeyErr, Ed25519SignatureErr, PubKeyErr,
RsaPubKeyErr, UncompressedP256PubKeyErr, UncompressedP384PubKeyErr,
},
- AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthTransports,
- AuthenticatorAttachment, Backup, CborSuccess, ClientDataJsonParser as _, CollectedClientData,
- CredentialId, Flag, FromCbor, LimitedVerificationParser, ParsedAuthData, Response,
- SentChallenge,
};
#[cfg(doc)]
use super::{
super::{
+ AuthenticatedCredential, RegisteredCredential,
request::{
+ BackupReq, Challenge,
auth::{AuthenticationVerificationOptions, PublicKeyCredentialRequestOptions},
register::{Extension, RegistrationServerState},
- BackupReq, Challenge,
},
- AuthenticatedCredential, RegisteredCredential,
},
AuthenticatorTransport,
};
@@ -41,19 +43,21 @@ use core::{
};
use ed25519_dalek::{Signature, Verifier as _, VerifyingKey};
use p256::{
- ecdsa::{DerSignature as P256Sig, VerifyingKey as P256VerKey},
- elliptic_curve::{generic_array::typenum::ToInt as _, point::DecompressPoint as _, Curve},
AffinePoint as P256Affine, EncodedPoint as P256Pt, NistP256,
+ ecdsa::{DerSignature as P256Sig, VerifyingKey as P256VerKey},
+ elliptic_curve::{Curve, generic_array::typenum::ToInt as _, point::DecompressPoint as _},
};
use p384::{
- ecdsa::{DerSignature as P384Sig, VerifyingKey as P384VerKey},
AffinePoint as P384Affine, EncodedPoint as P384Pt, NistP384,
+ ecdsa::{DerSignature as P384Sig, VerifyingKey as P384VerKey},
};
use rsa::{
- pkcs1v15::{self, VerifyingKey as RsaVerKey},
- sha2::{digest::Digest as _, Sha256},
BigUint, RsaPublicKey,
+ pkcs1v15::{self, VerifyingKey as RsaVerKey},
+ sha2::{Sha256, digest::Digest as _},
};
+#[cfg(all(doc, feature = "serde_relaxed"))]
+use serde::Deserialize;
/// Contains functionality to (de)serialize data to a data store.
#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
#[cfg(feature = "bin")]
@@ -635,6 +639,14 @@ impl<'a> UncompressedP256PubKey<'a> {
))
.map_err(|_e| PubKeyErr::P256)
}
+ /// Returns `true` iff [`Self::y`] is odd.
+ #[expect(clippy::indexing_slicing, reason = "comment justifies correctness")]
+ #[inline]
+ #[must_use]
+ pub const fn y_is_odd(self) -> bool {
+ // `self.1.len() == 32`, so this won't `panic`.
+ self.1[31] & 1 == 1
+ }
}
impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP256PubKey<'b> {
type Error = UncompressedP256PubKeyErr;
@@ -874,6 +886,14 @@ impl<'a> UncompressedP384PubKey<'a> {
))
.map_err(|_e| PubKeyErr::P384)
}
+ /// Returns `true` iff [`Self::y`] is odd.
+ #[expect(clippy::indexing_slicing, reason = "comment justifies correctness")]
+ #[inline]
+ #[must_use]
+ pub const fn y_is_odd(self) -> bool {
+ // `self.1.len() == 48`, so this won't `panic`.
+ self.1[47] & 1 == 1
+ }
}
impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP384PubKey<'b> {
type Error = UncompressedP384PubKeyErr;
@@ -939,6 +959,7 @@ impl<T> CompressedP384PubKey<T> {
}
/// `true` iff the y-coordinate is odd.
#[inline]
+ #[must_use]
pub const fn y_is_odd(&self) -> bool {
self.y_is_odd
}
@@ -1691,15 +1712,15 @@ impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8
}
}
impl<
- T: PartialEq<T5>,
- T5: PartialEq<T>,
- T2: PartialEq<T6>,
- T6: PartialEq<T2>,
- T3: PartialEq<T7>,
- T7: PartialEq<T3>,
- T4: PartialEq<T8>,
- T8: PartialEq<T4>,
- > PartialEq<CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8>
+ T: PartialEq<T5>,
+ T5: PartialEq<T>,
+ T2: PartialEq<T6>,
+ T6: PartialEq<T2>,
+ T3: PartialEq<T7>,
+ T7: PartialEq<T3>,
+ T4: PartialEq<T8>,
+ T8: PartialEq<T4>,
+> PartialEq<CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8>
{
#[inline]
fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool {
@@ -1718,15 +1739,15 @@ impl<
}
}
impl<
- T: PartialEq<T5>,
- T5: PartialEq<T>,
- T2: PartialEq<T6>,
- T6: PartialEq<T2>,
- T3: PartialEq<T7>,
- T7: PartialEq<T3>,
- T4: PartialEq<T8>,
- T8: PartialEq<T4>,
- > PartialEq<CompressedPubKey<T, T2, T3, T4>> for &CompressedPubKey<T5, T6, T7, T8>
+ T: PartialEq<T5>,
+ T5: PartialEq<T>,
+ T2: PartialEq<T6>,
+ T6: PartialEq<T2>,
+ T3: PartialEq<T7>,
+ T7: PartialEq<T3>,
+ T4: PartialEq<T8>,
+ T8: PartialEq<T4>,
+> PartialEq<CompressedPubKey<T, T2, T3, T4>> for &CompressedPubKey<T5, T6, T7, T8>
{
#[inline]
fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool {
@@ -1734,15 +1755,15 @@ impl<
}
}
impl<
- T: PartialEq<T5>,
- T5: PartialEq<T>,
- T2: PartialEq<T6>,
- T6: PartialEq<T2>,
- T3: PartialEq<T7>,
- T7: PartialEq<T3>,
- T4: PartialEq<T8>,
- T8: PartialEq<T4>,
- > PartialEq<&CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8>
+ T: PartialEq<T5>,
+ T5: PartialEq<T>,
+ T2: PartialEq<T6>,
+ T6: PartialEq<T2>,
+ T3: PartialEq<T7>,
+ T7: PartialEq<T3>,
+ T4: PartialEq<T8>,
+ T8: PartialEq<T4>,
+> PartialEq<&CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8>
{
#[inline]
fn eq(&self, other: &&CompressedPubKey<T, T2, T3, T4>) -> bool {
@@ -2539,7 +2560,7 @@ impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AttestationObject<'b> {
/// [`AuthenticatorAttestationResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse).
#[derive(Debug)]
pub struct AuthenticatorAttestation {
- /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-clientdatajson).
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
client_data_json: Vec<u8>,
/// [attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object) followed by the SHA-256 hash
/// of [`Self::client_data_json`].
@@ -2548,7 +2569,7 @@ pub struct AuthenticatorAttestation {
transports: AuthTransports,
}
impl AuthenticatorAttestation {
- /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-clientdatajson).
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
#[inline]
#[must_use]
pub fn client_data_json(&self) -> &[u8] {
@@ -2786,6 +2807,28 @@ impl Registration {
self.response.client_data_json.as_slice(),
)
}
+ /// Convenience function for [`RegistrationRelaxed::deserialize`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`RegistrationRelaxed::deserialize`] does.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ #[inline]
+ pub fn from_json_relaxed(json: &[u8]) -> Result<Self, SerdeJsonErr> {
+ serde_json::from_slice::<RegistrationRelaxed>(json).map(|val| val.0)
+ }
+ /// Convenience function for [`CustomRegistration::deserialize`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`CustomRegistration::deserialize`] does.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ #[inline]
+ pub fn from_json_custom(json: &[u8]) -> Result<Self, SerdeJsonErr> {
+ serde_json::from_slice::<CustomRegistration>(json).map(|val| val.0)
+ }
}
impl Response for Registration {
type Auth = AuthenticatorAttestation;
@@ -3045,8 +3088,8 @@ mod tests {
use super::{
super::{
super::{
- request::{AsciiDomain, RpId},
AggErr,
+ request::{AsciiDomain, RpId},
},
auth::{AuthenticatorAssertion, AuthenticatorData},
},
@@ -3111,8 +3154,12 @@ mod tests {
.unwrap();
let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d".as_slice()).unwrap();
let signature = HEXLOWER.decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87".as_slice()).unwrap();
- let auth_assertion =
- AuthenticatorAssertion::new(client_data_json_2, authenticator_data, signature, None);
+ let auth_assertion = AuthenticatorAssertion::new_user_vec(
+ client_data_json_2,
+ authenticator_data,
+ signature,
+ None,
+ );
let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?;
assert_eq!(
auth_data.rp_id_hash(),
@@ -3189,8 +3236,12 @@ mod tests {
.unwrap();
let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d".as_slice()).unwrap();
let signature = HEXLOWER.decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744".as_slice()).unwrap();
- let auth_assertion =
- AuthenticatorAssertion::new(client_data_json_2, authenticator_data, signature, None);
+ let auth_assertion = AuthenticatorAssertion::new_user_vec(
+ client_data_json_2,
+ authenticator_data,
+ signature,
+ None,
+ );
let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?;
assert_eq!(
auth_data.rp_id_hash(),
diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs
@@ -22,7 +22,7 @@ use core::{
str,
};
use data_encoding::BASE64URL_NOPAD;
-use rsa::sha2::{digest::OutputSizeUser as _, Sha256};
+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
/// verifying the key is valid. This exists purely to ensure that the public key we receive in JSON is the same as
@@ -34,8 +34,8 @@ mod spki {
};
use core::fmt::{self, Display, Formatter};
use p256::{
- elliptic_curve::{generic_array::typenum::type_operators::ToInt as _, Curve},
NistP256,
+ elliptic_curve::{Curve, generic_array::typenum::type_operators::ToInt as _},
};
use p384::NistP384;
/// Value assigned to the integer type under the universal tag class per
@@ -703,7 +703,10 @@ impl<'e, const I: bool> Deserialize<'e> for AttestField<I> {
impl<const IG: bool> Visitor<'_> for AttestFieldVisitor<IG> {
type Value = AttestField<IG>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- write!(formatter, "'{CLIENT_DATA_JSON}', '{ATTESTATION_OBJECT}', '{AUTHENTICATOR_DATA}', '{TRANSPORTS}', '{PUBLIC_KEY}', or '{PUBLIC_KEY_ALGORITHM}'")
+ write!(
+ formatter,
+ "'{CLIENT_DATA_JSON}', '{ATTESTATION_OBJECT}', '{AUTHENTICATOR_DATA}', '{TRANSPORTS}', '{PUBLIC_KEY}', or '{PUBLIC_KEY_ALGORITHM}'"
+ )
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
@@ -735,7 +738,7 @@ impl<'e, const I: bool> Deserialize<'e> for AttestField<I> {
/// a `Vec` that contains the attestation object and hash for signature verification. Calling code
/// can avoid any reallocation that would occur when the capacity is not large enough by ensuring the
/// passed `Vec` has at least 32 bytes of available capacity.
-struct AttObj(Vec<u8>);
+pub(super) struct AttObj(pub Vec<u8>);
impl<'e> Deserialize<'e> for AttObj {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -1382,37 +1385,38 @@ impl<'de> Deserialize<'de> for Registration {
mod tests {
use super::{
super::{
- cbor, AuthenticatorAttachment, Ed25519PubKey, Registration, RsaPubKey,
- UncompressedP256PubKey, UncompressedP384PubKey, ALG, EC2, EDDSA, ES256, ES384, KTY,
- OKP, RSA,
+ ALG, AuthenticatorAttachment, EC2, EDDSA, ES256, ES384, Ed25519PubKey, KTY, OKP, RSA,
+ Registration, RsaPubKey, UncompressedP256PubKey, UncompressedP384PubKey, cbor,
},
- spki::SubjectPublicKeyInfo,
CoseAlgorithmIdentifier,
+ spki::SubjectPublicKeyInfo,
};
use data_encoding::BASE64URL_NOPAD;
- use ed25519_dalek::{pkcs8::EncodePublicKey, VerifyingKey};
+ use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey};
use p256::{
- elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _},
EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key,
+ elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _},
};
use p384::{EncodedPoint as P384Pt, PublicKey as P384PubKey, SecretKey as P384Key};
use rsa::{
+ BigUint, RsaPrivateKey,
sha2::{Digest as _, Sha256},
traits::PublicKeyParts,
- BigUint, RsaPrivateKey,
};
use serde::de::{Error as _, Unexpected};
use serde_json::Error;
#[test]
fn ed25519_spki() {
- assert!(Ed25519PubKey::from_der(
- VerifyingKey::from_bytes(&[1; 32])
- .unwrap()
- .to_public_key_der()
- .unwrap()
- .as_bytes()
- )
- .map_or(false, |k| k.0 == [1; 32]));
+ assert!(
+ Ed25519PubKey::from_der(
+ VerifyingKey::from_bytes(&[1; 32])
+ .unwrap()
+ .to_public_key_der()
+ .unwrap()
+ .as_bytes()
+ )
+ .map_or(false, |k| k.0 == [1; 32])
+ );
}
#[test]
fn p256_spki() {
@@ -1692,38 +1696,40 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "authenticatorAttachment": "cross-platform",
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.response.client_data_json
- == c_data_json.as_bytes()
- && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.response.transports.count() == 6
- && matches!(
- reg.authenticator_attachment,
- AuthenticatorAttachment::CrossPlatform
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
)
- && reg.client_extension_results.cred_props.is_none()
- && reg.client_extension_results.prf.is_none()));
+ .map_or(false, |reg| reg.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.response.transports.count() == 6
+ && matches!(
+ reg.authenticator_attachment,
+ AuthenticatorAttachment::CrossPlatform
+ )
+ && reg.client_extension_results.cred_props.is_none()
+ && reg.client_extension_results.prf.is_none())
+ );
// `id` and `rawId` mismatch.
let mut err = Error::invalid_value(
Unexpected::Bytes(
@@ -2184,25 +2190,27 @@ mod tests {
err
);
// Duplicate `transports` are allowed.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": ["usb", "usb"],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.response.transports.count() == 1));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": ["usb", "usb"],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.response.transports.count() == 1)
+ );
// `null` `transports`.
err = Error::invalid_type(Unexpected::Other("null"), &"transports")
.to_string()
@@ -2263,29 +2271,31 @@ mod tests {
err
);
// `null` `authenticatorAttachment`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "authenticatorAttachment": null,
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| matches!(
- reg.authenticator_attachment,
- AuthenticatorAttachment::None
- )));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "authenticatorAttachment": null,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| matches!(
+ reg.authenticator_attachment,
+ AuthenticatorAttachment::None
+ ))
+ );
// Unknown `authenticatorAttachment`.
err = Error::invalid_value(
Unexpected::Str("Platform"),
@@ -2959,86 +2969,92 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.response.client_data_json
- == c_data_json.as_bytes()
- && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.response.transports.is_empty()
- && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
- && reg.client_extension_results.cred_props.is_none()
- && reg.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.response.transports.is_empty()
+ && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
+ && reg.client_extension_results.cred_props.is_none()
+ && reg.client_extension_results.prf.is_none())
+ );
// `null` `credProps`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": null
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .is_none()
- && reg.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": null
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .client_extension_results
+ .cred_props
+ .is_none()
+ && reg.client_extension_results.prf.is_none())
+ );
// `null` `prf`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": null
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .is_none()
- && reg.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": null
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .client_extension_results
+ .cred_props
+ .is_none()
+ && reg.client_extension_results.prf.is_none())
+ );
// Unknown `clientExtensionResults`.
let mut err = Error::unknown_field("CredProps", ["credProps", "prf"].as_slice())
.to_string()
@@ -3102,115 +3118,123 @@ mod tests {
err
);
// `null` `rk`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": {
- "rk": null
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .map_or(false, |props| props.rk.is_none())
- && reg.client_extension_results.prf.is_none()));
- // Missing `rk`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": {}
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .map_or(false, |props| props.rk.is_none())
- && reg.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": {
+ "rk": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .client_extension_results
+ .cred_props
+ .map_or(false, |props| props.rk.is_none())
+ && reg.client_extension_results.prf.is_none())
+ );
+ // Missing `rk`.
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": {}
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .client_extension_results
+ .cred_props
+ .map_or(false, |props| props.rk.is_none())
+ && reg.client_extension_results.prf.is_none())
+ );
// `true` rk`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": {
- "rk": true
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .map_or(false, |props| props.rk.unwrap_or_default())
- && reg.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": {
+ "rk": true
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .client_extension_results
+ .cred_props
+ .map_or(false, |props| props.rk.unwrap_or_default())
+ && reg.client_extension_results.prf.is_none())
+ );
// `false` rk`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": {
- "rk": false
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .map_or(false, |props| props.rk.map_or(false, |rk| !rk))
- && reg.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": {
+ "rk": false
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .client_extension_results
+ .cred_props
+ .map_or(false, |props| props.rk.map_or(false, |rk| !rk))
+ && reg.client_extension_results.prf.is_none())
+ );
// Invalid `rk`.
err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean")
.to_string()
@@ -3368,67 +3392,71 @@ mod tests {
err
);
// `true` `enabled`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
.client_extension_results
- .prf
- .map_or(false, |prf| prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled))
+ );
// `false` `enabled`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": false,
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": false,
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
.client_extension_results
- .prf
- .map_or(false, |prf| !prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| !prf.enabled))
+ );
// Invalid `enabled`.
err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean")
.to_string()
@@ -3462,37 +3490,39 @@ mod tests {
err
);
// `null` `results` with `enabled` `true`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true,
- "results": null,
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true,
+ "results": null,
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
.client_extension_results
- .prf
- .map_or(false, |prf| prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled))
+ );
// `null` `results` with `enabled` `false`.
err = Error::custom(
"prf must not have 'results', including a null 'results', if 'enabled' is false",
@@ -3560,9 +3590,39 @@ mod tests {
.into_bytes()[..err.len()],
err
);
- // Missing `first`.
- err = Error::missing_field("first").to_string().into_bytes();
- assert_eq!(
+ // Missing `first`.
+ err = Error::missing_field("first").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true,
+ "results": {},
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `first`.
+ assert!(
serde_json::from_str::<Registration>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -3578,7 +3638,9 @@ mod tests {
"clientExtensionResults": {
"prf": {
"enabled": true,
- "results": {},
+ "results": {
+ "first": null
+ },
}
},
"type": "public-key"
@@ -3586,80 +3648,52 @@ mod tests {
.to_string()
.as_str()
)
- .unwrap_err()
- .to_string()
- .into_bytes()[..err.len()],
- err
- );
- // `null` `first`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true,
- "results": {
- "first": null
- },
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ .map_or(false, |reg| reg
.client_extension_results
- .prf
- .map_or(false, |prf| prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled))
+ );
// `null` `second`.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true,
- "results": {
- "first": null,
- "second": null
- },
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true,
+ "results": {
+ "first": null,
+ "second": null
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
.client_extension_results
- .prf
- .map_or(false, |prf| prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled))
+ );
// Non-`null` `first`.
err = Error::invalid_type(Unexpected::Option, &"null")
.to_string()
@@ -4052,34 +4086,36 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -7,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.response.client_data_json
- == c_data_json.as_bytes()
- && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.response.transports.is_empty()
- && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
- && reg.client_extension_results.cred_props.is_none()
- && reg.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.response.transports.is_empty()
+ && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
+ && reg.client_extension_results.cred_props.is_none()
+ && reg.client_extension_results.prf.is_none())
+ );
// `publicKeyAlgorithm` mismatch.
let mut err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
@@ -4515,34 +4551,36 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -35,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.response.client_data_json
- == c_data_json.as_bytes()
- && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.response.transports.is_empty()
- && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
- && reg.client_extension_results.cred_props.is_none()
- && reg.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -35,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.response.transports.is_empty()
+ && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
+ && reg.client_extension_results.cred_props.is_none()
+ && reg.client_extension_results.prf.is_none())
+ );
// `publicKeyAlgorithm` mismatch.
let mut err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
@@ -4677,24 +4715,26 @@ mod tests {
err
);
// Missing `publicKey` is allowed when not using EdDSA, ES256, or RS256.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKeyAlgorithm": -35,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -35,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
@@ -4725,25 +4765,27 @@ mod tests {
err
);
// `null` `publicKey` is allowed when not using EdDSA, ES256, or RS256.
- assert!(serde_json::from_str::<Registration>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": null,
- "publicKeyAlgorithm": -35,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -35,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `publicKeyAlgorithm` mismatch when `publicKey` is null.
err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
@@ -5236,34 +5278,36 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -257,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.response.client_data_json
- == c_data_json.as_bytes()
- && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.response.transports.is_empty()
- && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
- && reg.client_extension_results.cred_props.is_none()
- && reg.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -257,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.response.transports.is_empty()
+ && matches!(reg.authenticator_attachment, AuthenticatorAttachment::None)
+ && reg.client_extension_results.cred_props.is_none()
+ && reg.client_extension_results.prf.is_none())
+ );
// `publicKeyAlgorithm` mismatch.
let mut err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs
@@ -1,22 +1,30 @@
+#![expect(
+ clippy::question_mark_used,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
#[cfg(doc)]
-use super::super::Challenge;
+use super::super::{super::request::register::CoseAlgorithmIdentifier, Challenge, CredentialId};
use super::{
super::{
register::ser::{
- AuthenticatorAttestationVisitor, ClientExtensionsOutputsVisitor, AUTH_ATTEST_FIELDS,
- EXT_FIELDS,
+ AUTH_ATTEST_FIELDS, AttObj, AuthenticatorAttestationVisitor,
+ ClientExtensionsOutputsVisitor, EXT_FIELDS,
+ },
+ ser::{
+ AuthenticationExtensionsPrfOutputsHelper, Base64DecodedVal, ClientExtensions,
+ PublicKeyCredential, Type,
},
- ser::{AuthenticationExtensionsPrfOutputsHelper, ClientExtensions, PublicKeyCredential},
ser_relaxed::AuthenticationExtensionsPrfValuesRelaxed,
},
+ AttestationObject, AuthenticationExtensionsPrfOutputs, AuthenticatorAttachment,
+ AuthenticatorAttestation, ClientExtensionsOutputs, CredentialPropertiesOutput, Registration,
ser::{AuthAttest, CredentialPropertiesOutputVisitor, PROPS_FIELDS},
- AttestationObject, AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation,
- ClientExtensionsOutputs, CredentialPropertiesOutput, Registration,
};
-use core::marker::PhantomData;
-#[cfg(doc)]
-use data_encoding::BASE64URL_NOPAD;
-use serde::de::{Deserialize, Deserializer, Error, Unexpected};
+use core::{
+ fmt::{self, Formatter},
+ marker::PhantomData,
+};
+use serde::de::{Deserialize, Deserializer, Error, MapAccess, Unexpected, Visitor};
/// `newtype` around `CredentialPropertiesOutput` with a "relaxed" [`Self::deserialize`] implementation.
#[derive(Debug)]
pub struct CredentialPropertiesOutputRelaxed(pub CredentialPropertiesOutput);
@@ -201,26 +209,253 @@ impl<'de> Deserialize<'de> for RegistrationRelaxed {
})
}
}
+/// `newtype` around `Registration` with a custom [`Self::deserialize`] implementation.
+#[derive(Debug)]
+pub struct CustomRegistration(pub Registration);
+impl<'de> Deserialize<'de> for CustomRegistration {
+ /// Despite the spec having a
+ /// [pre-defined format](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson) that clients
+ /// can follow, the downside is the superfluous data it contains.
+ ///
+ /// There simply is no reason to send the [`CredentialId`] _four_ times. This redundant data puts RPs in
+ /// a position where they either ignore the data or parse the data to ensure no contradictions exist
+ /// (e.g., [FIDO conformance requires one to verify `id` and `rawId` exist and match](https://github.com/w3c/webauthn/issues/2119#issuecomment-2287875401)).
+ ///
+ /// While [`Registration::deserialize`] _strictly_ adheres to the JSON definition (e.g., it requires `publicKey`
+ /// to exist and match with what is in both `authenticatorData` and `attestationObject` when the underlying
+ /// algorithm is not [`CoseAlgorithmIdentifier::Es384`]), this implementation
+ /// strictly disallows superfluous data. Specifically the following JSON is required to be sent where duplicate
+ /// and unknown keys are disallowed:
+ ///
+ /// ```json
+ /// {
+ /// "attestationObject":<base64url string>,
+ /// "authenticatorAttachment":null|"platform"|"cross-platform",
+ /// "clientDataJSON":<base64url string>,
+ /// "clientExtensionResults":<see ClientExtensionsOutputs::deserialize>,
+ /// "transports":<see AuthTransports::deserialize>,
+ /// "type":null|"public-key"
+ /// }
+ /// ```
+ ///
+ /// All of the above keys are required with the exceptions of `"authenticatorAttachment"` and `"type"`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::register::ser_relaxed::CustomRegistration;
+ /// assert!(
+ /// // The below payload is technically valid, but `RegistrationServerState::verify` will fail
+ /// // since the attestationObject is not valid. This is true for `Registration::deserialize`
+ /// // as well since attestationObject parsing is always deferred.
+ /// serde_json::from_str::<CustomRegistration>(
+ /// r#"{
+ /// "transports": ["usb"],
+ /// "attestationObject": "AA",
+ /// "authenticatorAttachment": "cross-platform",
+ /// "clientExtensionResults": {},
+ /// "clientDataJSON": "AA",
+ /// "type": "public-key"
+ /// }"#
+ /// ).is_ok());
+ /// ```
+ #[expect(
+ clippy::too_many_lines,
+ reason = "want to hide; thus don't want to put in an outer scope"
+ )]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `CustomRegistration`.
+ struct CustomRegistrationVisitor;
+ impl<'d> Visitor<'d> for CustomRegistrationVisitor {
+ type Value = CustomRegistration;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("CustomRegistration")
+ }
+ #[expect(
+ clippy::too_many_lines,
+ reason = "want to hide; thus don't want to put in an outer scope"
+ )]
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Fields in the JSON.
+ enum Field {
+ /// `attestationObject` key.
+ AttestationObject,
+ /// `authenticatorAttachment` key.
+ AuthenticatorAttachment,
+ /// `clientDataJSON` key.
+ ClientDataJson,
+ /// `clientExtensionResults` key.
+ ClientExtensionResults,
+ /// `transports` key.
+ Transports,
+ /// `type` key.
+ Type,
+ }
+ 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,
+ "'{ATTESTATION_OBJECT}', '{AUTHENTICATOR_ATTACHMENT}', '{CLIENT_DATA_JSON}', '{CLIENT_EXTENSION_RESULTS}', '{TRANSPORTS}', or '{TYPE}'"
+ )
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ ATTESTATION_OBJECT => Ok(Field::AttestationObject),
+ AUTHENTICATOR_ATTACHMENT => Ok(Field::AuthenticatorAttachment),
+ CLIENT_DATA_JSON => Ok(Field::ClientDataJson),
+ CLIENT_EXTENSION_RESULTS => Ok(Field::ClientExtensionResults),
+ TRANSPORTS => Ok(Field::Transports),
+ TYPE => Ok(Field::Type),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut attestation_object = None;
+ let mut authenticator_attachment = None;
+ let mut client_data_json = None;
+ let mut ext = None;
+ let mut transports = None;
+ let mut typ = false;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::AttestationObject => {
+ if attestation_object.is_some() {
+ return Err(Error::duplicate_field(ATTESTATION_OBJECT));
+ }
+ attestation_object =
+ map.next_value::<AttObj>().map(|val| Some(val.0))?;
+ }
+ Field::AuthenticatorAttachment => {
+ if authenticator_attachment.is_some() {
+ return Err(Error::duplicate_field(AUTHENTICATOR_ATTACHMENT));
+ }
+ authenticator_attachment = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::ClientDataJson => {
+ if client_data_json.is_some() {
+ return Err(Error::duplicate_field(CLIENT_DATA_JSON));
+ }
+ client_data_json = map
+ .next_value::<Base64DecodedVal>()
+ .map(|val| Some(val.0))?;
+ }
+ Field::ClientExtensionResults => {
+ if ext.is_some() {
+ return Err(Error::duplicate_field(CLIENT_EXTENSION_RESULTS));
+ }
+ ext = map.next_value().map(Some)?;
+ }
+ Field::Transports => {
+ if transports.is_some() {
+ return Err(Error::duplicate_field(TRANSPORTS));
+ }
+ transports = map.next_value().map(Some)?;
+ }
+ Field::Type => {
+ if typ {
+ return Err(Error::duplicate_field(TYPE));
+ }
+ typ = map.next_value::<Option<Type>>().map(|_| true)?;
+ }
+ }
+ }
+ attestation_object
+ .ok_or_else(|| Error::missing_field(ATTESTATION_OBJECT))
+ .and_then(|att_obj| {
+ client_data_json
+ .ok_or_else(|| Error::missing_field(CLIENT_DATA_JSON))
+ .and_then(|c_data| {
+ ext.ok_or_else(|| Error::missing_field(CLIENT_EXTENSION_RESULTS))
+ .and_then(|client_extension_results| {
+ transports
+ .ok_or_else(|| Error::missing_field(TRANSPORTS))
+ .map(|trans| {
+ CustomRegistration(Registration {
+ response: AuthenticatorAttestation::new(
+ c_data, att_obj, trans,
+ ),
+ authenticator_attachment:
+ authenticator_attachment.map_or(
+ AuthenticatorAttachment::None,
+ |auth_attach| {
+ auth_attach.unwrap_or(
+ AuthenticatorAttachment::None,
+ )
+ },
+ ),
+ client_extension_results,
+ })
+ })
+ })
+ })
+ })
+ }
+ }
+ /// `attestationObject` key.
+ const ATTESTATION_OBJECT: &str = "attestationObject";
+ /// `authenticatorAttachment` key.
+ const AUTHENTICATOR_ATTACHMENT: &str = "authenticatorAttachment";
+ /// `clientDataJSON` key.
+ const CLIENT_DATA_JSON: &str = "clientDataJSON";
+ /// `clientExtensionResults` key.
+ const CLIENT_EXTENSION_RESULTS: &str = "clientExtensionResults";
+ /// `transports` key.
+ const TRANSPORTS: &str = "transports";
+ /// `type` key.
+ const TYPE: &str = "type";
+ /// Fields.
+ const FIELDS: &[&str; 6] = &[
+ ATTESTATION_OBJECT,
+ AUTHENTICATOR_ATTACHMENT,
+ CLIENT_DATA_JSON,
+ CLIENT_EXTENSION_RESULTS,
+ TRANSPORTS,
+ TYPE,
+ ];
+ deserializer.deserialize_struct("CustomRegistration", FIELDS, CustomRegistrationVisitor)
+ }
+}
#[cfg(test)]
mod tests {
use super::{
super::{
- super::super::request::register::CoseAlgorithmIdentifier, cbor,
- AuthenticatorAttachment, ALG, EC2, EDDSA, ES256, ES384, KTY, OKP, RSA,
+ super::super::request::register::CoseAlgorithmIdentifier, ALG, AuthenticatorAttachment,
+ EC2, EDDSA, ES256, ES384, KTY, OKP, RSA, cbor,
},
- RegistrationRelaxed,
+ CustomRegistration, RegistrationRelaxed,
};
use data_encoding::BASE64URL_NOPAD;
- use ed25519_dalek::{pkcs8::EncodePublicKey, VerifyingKey};
+ use ed25519_dalek::{VerifyingKey, pkcs8::EncodePublicKey};
use p256::{
- elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _},
EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key,
+ elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _},
};
use p384::{EncodedPoint as P384Pt, PublicKey as P384PubKey, SecretKey as P384Key};
use rsa::{
+ BigUint, RsaPrivateKey,
sha2::{Digest as _, Sha256},
traits::PublicKeyParts,
- BigUint, RsaPrivateKey,
};
use serde::de::{Error as _, Unexpected};
use serde_json::Error;
@@ -392,38 +627,40 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "authenticatorAttachment": "cross-platform",
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.0.response.client_data_json
- == c_data_json.as_bytes()
- && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.0.response.transports.count() == 6
- && matches!(
- reg.0.authenticator_attachment,
- AuthenticatorAttachment::CrossPlatform
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
)
- && reg.0.client_extension_results.cred_props.is_none()
- && reg.0.client_extension_results.prf.is_none()));
+ .map_or(false, |reg| reg.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.0.response.transports.count() == 6
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::CrossPlatform
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
// `id` and `rawId` mismatch.
let mut err = Error::invalid_value(
Unexpected::Bytes(
@@ -461,83 +698,91 @@ mod tests {
err
);
// missing `id`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `id`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": null,
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": null,
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Missing `rawId`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `rawId`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": null,
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": null,
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `id` and the credential id in authenticator data mismatch.
err = Error::invalid_value(
Unexpected::Bytes(
@@ -607,61 +852,16 @@ mod tests {
err
);
// Missing `authenticatorData`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
- // `null `authenticatorData`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "transports": [],
- "authenticatorData": null,
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
- // `publicKeyAlgorithm` mismatch.
- err = Error::invalid_value(
- Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
- &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Eddsa).as_str()
- )
- .to_string().into_bytes();
- assert_eq!(
+ assert!(
serde_json::from_str::<RegistrationRelaxed>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
"clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
"transports": [],
"publicKey": b64_key,
- "publicKeyAlgorithm": -7,
+ "publicKeyAlgorithm": -8,
"attestationObject": b64_aobj,
},
"clientExtensionResults": {},
@@ -670,50 +870,103 @@ mod tests {
.to_string()
.as_str()
)
- .unwrap_err()
- .to_string()
- .into_bytes()[..err.len()],
- err
+ .is_ok()
);
- // Missing `publicKeyAlgorithm`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
+ // `null `authenticatorData`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "transports": [],
+ "authenticatorData": null,
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `publicKeyAlgorithm` mismatch.
+ err = Error::invalid_value(
+ Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Es256).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Eddsa).as_str()
)
- .is_ok());
- // `null` `publicKeyAlgorithm`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": null,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
.to_string()
- .as_str()
- )
- .is_ok());
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `publicKeyAlgorithm`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `null` `publicKeyAlgorithm`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `publicKey` mismatch.
err = Error::invalid_value(
Unexpected::Bytes([0; 32].as_slice()),
@@ -746,111 +999,71 @@ mod tests {
err
);
// Missing `publicKey`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `publicKey`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": null,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Missing `transports`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Duplicate `transports` are allowed.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": ["usb", "usb"],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.0.response.transports.count() == 1));
- // `null` `transports`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": null,
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
- // Unknown `transports`.
- err = Error::invalid_value(
- Unexpected::Str("Usb"),
- &"'ble', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'",
- )
- .to_string()
- .into_bytes();
- assert_eq!(
+ assert!(
serde_json::from_str::<RegistrationRelaxed>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -858,7 +1071,7 @@ mod tests {
"response": {
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
- "transports": ["Usb"],
+ "transports": ["usb", "usb"],
"publicKey": b64_key,
"publicKeyAlgorithm": -8,
"attestationObject": b64_aobj,
@@ -869,35 +1082,662 @@ mod tests {
.to_string()
.as_str()
)
- .unwrap_err()
- .to_string()
- .into_bytes()[..err.len()],
- err
+ .map_or(false, |reg| reg.0.response.transports.count() == 1)
);
- // `null` `authenticatorAttachment`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
+ // `null` `transports`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": null,
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // Unknown `transports`.
+ err = Error::invalid_value(
+ Unexpected::Str("Usb"),
+ &"'ble', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'",
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": ["Usb"],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `authenticatorAttachment`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "authenticatorAttachment": null,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ ))
+ );
+ // Unknown `authenticatorAttachment`.
+ err = Error::invalid_value(
+ Unexpected::Str("Platform"),
+ &"'platform' or 'cross-platform'",
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "authenticatorAttachment": "Platform",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `clientDataJSON`.
+ err = Error::missing_field("clientDataJSON")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `clientDataJSON`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"base64url-encoded data")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": null,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `attestationObject`.
+ err = Error::missing_field("attestationObject")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `attestationObject`.
+ err = Error::invalid_type(
+ Unexpected::Other("null"),
+ &"base64url-encoded attestation object",
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": null,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `response`.
+ err = Error::missing_field("response").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `response`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": null,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Empty `response`.
+ err = Error::missing_field("clientDataJSON")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {},
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `clientExtensionResults`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `null` `clientExtensionResults`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": null,
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // Missing `type`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // `null` `type`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": null
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // Not exactly `public-type` `type`.
+ err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "Public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!(null).to_string().as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Empty.
+ err = Error::missing_field("response").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(serde_json::json!({}).to_string().as_str())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown field in `response`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ "foo": true,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // Duplicate field in `response`.
+ err = Error::duplicate_field("transports")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"transports\": [],
+ \"publicKey\": \"{b64_key}\",
+ \"publicKeyAlgorithm\": -8,
+ \"attestationObject\": \"{b64_aobj}\",
+ \"transports\": []
+ }},
+ \"clientExtensionResults\": {{}},
+ \"type\": \"public-key\"
+
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown field in `PublicKeyCredential`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj
+ },
+ "clientExtensionResults": {},
+ "type": "public-key",
+ "foo": true,
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // Duplicate field in `PublicKeyCredential`.
+ err = Error::duplicate_field("id").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"transports\": [],
+ \"publicKey\": \"{b64_key}\",
+ \"publicKeyAlgorithm\": -8,
+ \"attestationObject\": \"{b64_aobj}\"
+ }},
+ \"clientExtensionResults\": {{}},
+ \"type\": \"public-key\"
+
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Base case is correct.
+ assert!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
"attestationObject": b64_aobj,
- },
- "authenticatorAttachment": null,
- "clientExtensionResults": {},
- "type": "public-key"
- })
+ "authenticatorAttachment": "cross-platform",
+ "clientDataJSON": b64_cdata,
+ "clientExtensionResults": {},
+ "transports": ["ble", "usb", "hybrid", "internal", "nfc", "smart-card"],
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.0.response.transports.count() == 6
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::CrossPlatform
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
+ // Missing `transports`.
+ err = Error::missing_field("transports").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
+ "attestationObject": b64_aobj,
+ "authenticatorAttachment": "cross-platform",
+ "clientDataJSON": b64_cdata,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
.to_string()
- .as_str()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate `transports` are allowed.
+ assert!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
+ "attestationObject": b64_aobj,
+ "authenticatorAttachment": "cross-platform",
+ "clientDataJSON": b64_cdata,
+ "clientExtensionResults": {},
+ "transports": ["usb", "usb"],
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.0.response.transports.count() == 1)
+ );
+ // `null` `transports`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"AuthTransports")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
+ "clientDataJSON": b64_cdata,
+ "transports": null,
+ "attestationObject": b64_aobj,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown `transports`.
+ err = Error::invalid_value(
+ Unexpected::Str("Usb"),
+ &"'ble', 'hybrid', 'internal', 'nfc', 'smart-card', or 'usb'",
)
- .map_or(false, |reg| matches!(
- reg.0.authenticator_attachment,
- AuthenticatorAttachment::None
- )));
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
+ "attestationObject": b64_aobj,
+ "authenticatorAttachment": "cross-platform",
+ "clientDataJSON": b64_cdata,
+ "clientExtensionResults": {},
+ "transports": ["Usb"],
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `authenticatorAttachment`.
+ assert!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
+ "attestationObject": b64_aobj,
+ "authenticatorAttachment": null,
+ "clientDataJSON": b64_cdata,
+ "clientExtensionResults": {},
+ "transports": [],
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ ))
+ );
// Unknown `authenticatorAttachment`.
err = Error::invalid_value(
Unexpected::Str("Platform"),
@@ -906,20 +1746,13 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
+ "attestationObject": b64_aobj,
"authenticatorAttachment": "Platform",
+ "clientDataJSON": b64_cdata,
"clientExtensionResults": {},
+ "transports": [],
"type": "public-key"
})
.to_string()
@@ -935,17 +1768,10 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
+ "transports": [],
+ "attestationObject": b64_aobj,
"clientExtensionResults": {},
"type": "public-key"
})
@@ -962,20 +1788,11 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": null,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
+ "clientDataJSON": null,
+ "transports": [],
+ "attestationObject": b64_aobj,
})
.to_string()
.as_str()
@@ -990,17 +1807,10 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- },
+ "clientDataJSON": b64_cdata,
+ "transports": [],
"clientExtensionResults": {},
"type": "public-key"
})
@@ -1020,18 +1830,11 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": null,
- },
+ "clientDataJSON": b64_cdata,
+ "transports": [],
+ "attestationObject": null,
"clientExtensionResults": {},
"type": "public-key"
})
@@ -1043,14 +1846,16 @@ mod tests {
.into_bytes()[..err.len()],
err
);
- // Missing `response`.
- err = Error::missing_field("response").to_string().into_bytes();
+ // Missing `clientExtensionResults`.
+ err = Error::missing_field("clientExtensionResults")
+ .to_string()
+ .into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "clientExtensionResults": {},
+ "clientDataJSON": b64_cdata,
+ "transports": [],
+ "attestationObject": b64_aobj,
"type": "public-key"
})
.to_string()
@@ -1061,17 +1866,17 @@ mod tests {
.into_bytes()[..err.len()],
err
);
- // `null` `response`.
- err = Error::invalid_type(Unexpected::Other("null"), &"AuthenticatorAttestation")
+ // `null` `clientExtensionResults`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"ClientExtensionsOutputs")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": null,
- "clientExtensionResults": {},
+ "clientDataJSON": b64_cdata,
+ "transports": [],
+ "attestationObject": b64_aobj,
+ "clientExtensionResults": null,
"type": "public-key"
})
.to_string()
@@ -1082,122 +1887,45 @@ mod tests {
.into_bytes()[..err.len()],
err
);
- // Empty `response`.
- err = Error::missing_field("clientDataJSON")
- .to_string()
- .into_bytes();
- assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ // Missing `type`.
+ assert!(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {},
+ "attestationObject": b64_aobj,
+ "clientDataJSON": b64_cdata,
"clientExtensionResults": {},
- "type": "public-key"
+ "transports": []
})
.to_string()
.as_str()
)
- .unwrap_err()
- .to_string()
- .into_bytes()[..err.len()],
- err
+ .map_or(false, |_| true)
);
- // Missing `clientExtensionResults`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
- // `null` `clientExtensionResults`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": null,
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
- // Missing `type`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- })
- .to_string()
- .as_str()
- )
- .is_ok());
// `null` `type`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
+ assert!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
+ "attestationObject": b64_aobj,
"clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
+ "clientExtensionResults": {},
"transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": null
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ "type": null
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |_| true)
+ );
// Not exactly `public-type` `type`.
err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
+ "clientDataJSON": b64_cdata,
+ "transports": [],
+ "attestationObject": b64_aobj,
"clientExtensionResults": {},
"type": "Public-key"
})
@@ -1210,11 +1938,11 @@ mod tests {
err
);
// `null`.
- err = Error::invalid_type(Unexpected::Other("null"), &"PublicKeyCredential")
+ err = Error::invalid_type(Unexpected::Other("null"), &"CustomRegistration")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
serde_json::json!(null).to_string().as_str()
)
.unwrap_err()
@@ -1223,59 +1951,42 @@ mod tests {
err
);
// Empty.
- err = Error::missing_field("response").to_string().into_bytes();
+ err = Error::missing_field("attestationObject")
+ .to_string()
+ .into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(serde_json::json!({}).to_string().as_str())
+ serde_json::from_str::<CustomRegistration>(serde_json::json!({}).to_string().as_str())
.unwrap_err()
.to_string()
.into_bytes()[..err.len()],
err
);
- // Unknown field in `response`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- "foo": true,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
+ // Unknown field.
+ err = Error::unknown_field(
+ "foo",
+ [
+ "attestationObject",
+ "authenticatorAttachment",
+ "clientDataJSON",
+ "clientExtensionResults",
+ "transports",
+ "type",
+ ]
+ .as_slice(),
)
- .is_ok());
- // Duplicate field in `response`.
- err = Error::duplicate_field("transports")
- .to_string()
- .into_bytes();
+ .to_string()
+ .into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
- format!(
- "{{
- \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
- \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
- \"response\": {{
- \"clientDataJSON\": \"{b64_cdata}\",
- \"authenticatorData\": \"{b64_adata}\",
- \"transports\": [],
- \"publicKey\": \"{b64_key}\",
- \"publicKeyAlgorithm\": -8,
- \"attestationObject\": \"{b64_aobj}\",
- \"transports\": []
- }},
- \"clientExtensionResults\": {{}},
- \"type\": \"public-key\"
-
- }}"
- )
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
+ "clientDataJSON": b64_cdata,
+ "transports": [],
+ "attestationObject": b64_aobj,
+ "foo": true,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
.as_str()
)
.unwrap_err()
@@ -1283,47 +1994,20 @@ mod tests {
.into_bytes()[..err.len()],
err
);
- // Unknown field in `PublicKeyCredential`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj
- },
- "clientExtensionResults": {},
- "type": "public-key",
- "foo": true,
- })
+ // Duplicate field.
+ err = Error::duplicate_field("transports")
.to_string()
- .as_str()
- )
- .is_ok());
- // Duplicate field in `PublicKeyCredential`.
- err = Error::duplicate_field("id").to_string().into_bytes();
+ .into_bytes();
assert_eq!(
- serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::from_str::<CustomRegistration>(
format!(
"{{
- \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
- \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
- \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
- \"response\": {{
- \"clientDataJSON\": \"{b64_cdata}\",
- \"authenticatorData\": \"{b64_adata}\",
- \"transports\": [],
- \"publicKey\": \"{b64_key}\",
- \"publicKeyAlgorithm\": -8,
- \"attestationObject\": \"{b64_aobj}\"
- }},
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"transports\": [],
+ \"attestationObject\": \"{b64_aobj}\",
+ \"transports\": []
\"clientExtensionResults\": {{}},
\"type\": \"public-key\"
-
}}"
)
.as_str()
@@ -1502,115 +2186,123 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.0.response.client_data_json
- == c_data_json.as_bytes()
- && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.0.response.transports.is_empty()
- && matches!(
- reg.0.authenticator_attachment,
- AuthenticatorAttachment::None
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
)
- && reg.0.client_extension_results.cred_props.is_none()
- && reg.0.client_extension_results.prf.is_none()));
+ .map_or(false, |reg| reg.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
// `null` `credProps`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": null
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .is_none()
- && reg.0.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": null
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .0
+ .client_extension_results
+ .cred_props
+ .is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
// `null` `prf`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": null
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .is_none()
- && reg.0.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": null
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .0
+ .client_extension_results
+ .cred_props
+ .is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
// Unknown `clientExtensionResults`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "CredProps": {
- "rk": true
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "CredProps": {
+ "rk": true
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Duplicate field.
let mut err = Error::duplicate_field("credProps").to_string().into_bytes();
assert_eq!(
@@ -1636,125 +2328,133 @@ mod tests {
)
.as_str()
)
- .unwrap_err()
- .to_string()
- .into_bytes()[..err.len()],
- err
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `rk`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": {
+ "rk": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .0
+ .client_extension_results
+ .cred_props
+ .map_or(false, |props| props.rk.is_none())
+ && reg.0.client_extension_results.prf.is_none())
+ );
+ // Missing `rk`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": {}
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .0
+ .client_extension_results
+ .cred_props
+ .map_or(false, |props| props.rk.is_none())
+ && reg.0.client_extension_results.prf.is_none())
+ );
+ // `true` rk`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": {
+ "rk": true
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .0
+ .client_extension_results
+ .cred_props
+ .map_or(false, |props| props.rk.unwrap_or_default())
+ && reg.0.client_extension_results.prf.is_none())
);
- // `null` `rk`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": {
- "rk": null
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .map_or(false, |props| props.rk.is_none())
- && reg.0.client_extension_results.prf.is_none()));
- // Missing `rk`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": {}
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .map_or(false, |props| props.rk.is_none())
- && reg.0.client_extension_results.prf.is_none()));
- // `true` rk`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": {
- "rk": true
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .map_or(false, |props| props.rk.unwrap_or_default())
- && reg.0.client_extension_results.prf.is_none()));
// `false` rk`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": {
- "rk": false
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .map_or(false, |props| props.rk.map_or(false, |rk| !rk))
- && reg.0.client_extension_results.prf.is_none()));
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": {
+ "rk": false
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
+ .0
+ .client_extension_results
+ .cred_props
+ .map_or(false, |props| props.rk.map_or(false, |rk| !rk))
+ && reg.0.client_extension_results.prf.is_none())
+ );
// Invalid `rk`.
err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean")
.to_string()
@@ -1788,29 +2488,31 @@ mod tests {
err
);
// Unknown `credProps` field.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "credProps": {
- "Rk": true,
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "credProps": {
+ "Rk": true,
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Duplicate field in `credProps`.
err = Error::duplicate_field("rk").to_string().into_bytes();
assert_eq!(
@@ -1904,71 +2606,75 @@ mod tests {
err
);
// `true` `enabled`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
.0
.client_extension_results
- .prf
- .map_or(false, |prf| prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .0
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled))
+ );
// `false` `enabled`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": false,
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": false,
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
.0
.client_extension_results
- .prf
- .map_or(false, |prf| !prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .0
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| !prf.enabled))
+ );
// Invalid `enabled`.
err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean")
.to_string()
@@ -1996,45 +2702,47 @@ mod tests {
.to_string()
.as_str()
)
- .unwrap_err()
- .to_string()
- .into_bytes()[..err.len()],
- err
- );
- // `null` `results` with `enabled` `true`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true,
- "results": null,
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `results` with `enabled` `true`.
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true,
+ "results": null,
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
.0
.client_extension_results
- .prf
- .map_or(false, |prf| prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .0
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled))
+ );
// `null` `results` with `enabled` `false`.
err = Error::custom(
"prf must not have 'results', including a null 'results', if 'enabled' is false",
@@ -2103,103 +2811,109 @@ mod tests {
err
);
// Missing `first`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true,
- "results": {},
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true,
+ "results": {},
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `first`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true,
- "results": {
- "first": null
- },
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true,
+ "results": {
+ "first": null
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
.0
.client_extension_results
- .prf
- .map_or(false, |prf| prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .0
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled))
+ );
// `null` `second`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true,
- "results": {
- "first": null,
- "second": null
- },
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg
- .0
- .client_extension_results
- .cred_props
- .is_none()
- && reg
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true,
+ "results": {
+ "first": null,
+ "second": null
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg
.0
.client_extension_results
- .prf
- .map_or(false, |prf| prf.enabled)));
+ .cred_props
+ .is_none()
+ && reg
+ .0
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled))
+ );
// Non-`null` `first`.
err = Error::invalid_type(Unexpected::Option, &"null")
.to_string()
@@ -2272,58 +2986,62 @@ mod tests {
err
);
// Unknown `prf` field.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true,
- "Results": null
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true,
+ "Results": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Unknown `results` field.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -8,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {
- "prf": {
- "enabled": true,
- "results": {
- "first": null,
- "Second": null
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {
+ "prf": {
+ "enabled": true,
+ "results": {
+ "first": null,
+ "Second": null
+ }
}
- }
- },
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// Duplicate field in `results`.
err = Error::duplicate_field("first").to_string().into_bytes();
assert_eq!(
@@ -2576,37 +3294,39 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -7,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.0.response.client_data_json
- == c_data_json.as_bytes()
- && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.0.response.transports.is_empty()
- && matches!(
- reg.0.authenticator_attachment,
- AuthenticatorAttachment::None
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
)
- && reg.0.client_extension_results.cred_props.is_none()
- && reg.0.client_extension_results.prf.is_none()));
+ .map_or(false, |reg| reg.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
// `publicKeyAlgorithm` mismatch.
let mut err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
@@ -2638,44 +3358,48 @@ mod tests {
err
);
// Missing `publicKeyAlgorithm`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `publicKeyAlgorithm`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": null,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `publicKey` mismatch.
let bad_pub_key = P256PubKey::from_encoded_point(&P256Pt::from_affine_coordinates(
&[
@@ -2723,44 +3447,75 @@ mod tests {
err
);
// Missing `publicKey`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKeyAlgorithm": -7,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `publicKey`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
"clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
"transports": [],
- "publicKey": null,
- "publicKeyAlgorithm": -7,
"attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
}
#[test]
fn es384_registration_deserialize_data_mismatch() {
@@ -3012,37 +3767,39 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -35,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.0.response.client_data_json
- == c_data_json.as_bytes()
- && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.0.response.transports.is_empty()
- && matches!(
- reg.0.authenticator_attachment,
- AuthenticatorAttachment::None
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -35,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
)
- && reg.0.client_extension_results.cred_props.is_none()
- && reg.0.client_extension_results.prf.is_none()));
+ .map_or(false, |reg| reg.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
// `publicKeyAlgorithm` mismatch.
let mut err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
@@ -3074,44 +3831,48 @@ mod tests {
err
);
// Missing `publicKeyAlgorithm`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `publicKeyAlgorithm`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": null,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `publicKey` mismatch.
let bad_pub_key = P384PubKey::from_encoded_point(&P384Pt::from_affine_coordinates(
&[
@@ -3161,24 +3922,26 @@ mod tests {
err
);
// Missing `publicKey`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKeyAlgorithm": -35,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -35,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
@@ -3209,25 +3972,27 @@ mod tests {
err
);
// `null` `publicKey`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": null,
- "publicKeyAlgorithm": -35,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -35,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `publicKeyAlgorithm` mismatch when `publicKey` is null.
err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
@@ -3258,6 +4023,33 @@ mod tests {
.into_bytes()[..err.len()],
err
);
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
+ "clientDataJSON": b64_cdata,
+ "transports": [],
+ "attestationObject": b64_aobj,
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
}
#[test]
fn rs256_registration_deserialize_data_mismatch() {
@@ -3720,37 +4512,39 @@ mod tests {
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>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": -257,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .map_or(false, |reg| reg.0.response.client_data_json
- == c_data_json.as_bytes()
- && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
- == att_obj
- && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
- == *Sha256::digest(c_data_json.as_bytes()).as_slice()
- && reg.0.response.transports.is_empty()
- && matches!(
- reg.0.authenticator_attachment,
- AuthenticatorAttachment::None
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": -257,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
)
- && reg.0.client_extension_results.cred_props.is_none()
- && reg.0.client_extension_results.prf.is_none()));
+ .map_or(false, |reg| reg.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
// `publicKeyAlgorithm` mismatch.
let mut err = Error::invalid_value(
Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
@@ -3782,44 +4576,48 @@ mod tests {
err
);
// Missing `publicKeyAlgorithm`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `publicKeyAlgorithm`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKey": b64_key,
- "publicKeyAlgorithm": null,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": b64_key,
+ "publicKeyAlgorithm": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `publicKey` mismatch.
let bad_pub_key = RsaPrivateKey::from_components(
BigUint::from_slice(
@@ -3913,43 +4711,74 @@ mod tests {
err
);
// Missing `publicKey`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
- "clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
- "transports": [],
- "publicKeyAlgorithm": -257,
- "attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKeyAlgorithm": -257,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
// `null` `publicKey`.
- assert!(serde_json::from_str::<RegistrationRelaxed>(
- serde_json::json!({
- "id": "AAAAAAAAAAAAAAAAAAAAAA",
- "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
- "response": {
+ assert!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": b64_adata,
+ "transports": [],
+ "publicKey": null,
+ "publicKeyAlgorithm": -257,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok()
+ );
+ // Base case is valid.
+ assert!(
+ serde_json::from_str::<CustomRegistration>(
+ serde_json::json!({
"clientDataJSON": b64_cdata,
- "authenticatorData": b64_adata,
"transports": [],
- "publicKey": null,
- "publicKeyAlgorithm": -257,
"attestationObject": b64_aobj,
- },
- "clientExtensionResults": {},
- "type": "public-key"
- })
- .to_string()
- .as_str()
- )
- .is_ok());
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .map_or(false, |reg| reg.0.response.client_data_json
+ == c_data_json.as_bytes()
+ && reg.0.response.attestation_object_and_c_data_hash[..att_obj.len()]
+ == att_obj
+ && reg.0.response.attestation_object_and_c_data_hash[att_obj.len()..]
+ == *Sha256::digest(c_data_json.as_bytes()).as_slice()
+ && reg.0.response.transports.is_empty()
+ && matches!(
+ reg.0.authenticator_attachment,
+ AuthenticatorAttachment::None
+ )
+ && reg.0.client_extension_results.cred_props.is_none()
+ && reg.0.client_extension_results.prf.is_none())
+ );
}
}
diff --git a/src/response/ser.rs b/src/response/ser.rs
@@ -99,7 +99,10 @@ impl<'de> Deserialize<'de> for AuthenticatorTransport {
USB => Ok(AuthenticatorTransport::Usb),
_ => Err(E::invalid_value(
Unexpected::Str(v),
- &format!("'{BLE}', '{HYBRID}', '{INTERNAL}', '{NFC}', '{SMART_CARD}', or '{USB}'").as_str(),
+ &format!(
+ "'{BLE}', '{HYBRID}', '{INTERNAL}', '{NFC}', '{SMART_CARD}', or '{USB}'"
+ )
+ .as_str(),
)),
}
}
@@ -532,6 +535,34 @@ pub(super) struct PublicKeyCredential<const RELAXED: bool, const REG: bool, Auth
/// [`getClientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults).
pub client_extension_results: Ext,
}
+/// Deserializes the value for type.
+pub(super) struct Type;
+impl<'e> Deserialize<'e> for Type {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Type`.
+ struct TypeVisitor;
+ impl Visitor<'_> for TypeVisitor {
+ type Value = Type;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str(PUBLIC_KEY)
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v == PUBLIC_KEY {
+ Ok(Type)
+ } else {
+ Err(E::invalid_value(Unexpected::Str(v), &PUBLIC_KEY))
+ }
+ }
+ }
+ deserializer.deserialize_str(TypeVisitor)
+ }
+}
/// `Visitor` for `PublicKeyCredential`.
///
/// When `!RELAXED`, `REG` is ignored and all fields must exist and unknown fields are not allowed.
@@ -544,7 +575,7 @@ struct PublicKeyCredentialVisitor<const RELAXED: bool, const REG: bool, R, E>(
impl<'d, const REL: bool, const REGI: bool, R, E> Visitor<'d>
for PublicKeyCredentialVisitor<REL, REGI, R, E>
where
- R: for<'a> Deserialize<'a>,
+ R: Deserialize<'d>,
E: for<'a> Deserialize<'a> + ClientExtensions,
{
type Value = PublicKeyCredential<REL, REGI, R, E>;
@@ -586,7 +617,10 @@ where
impl<const IGN: bool> Visitor<'_> for FieldVisitor<IGN> {
type Value = Field<IGN>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- write!(formatter, "'{ID}', '{TYPE}', '{RAW_ID}', '{RESPONSE}', '{AUTHENTICATOR_ATTACHMENT}', or '{CLIENT_EXTENSION_RESULTS}'")
+ write!(
+ formatter,
+ "'{ID}', '{TYPE}', '{RAW_ID}', '{RESPONSE}', '{AUTHENTICATOR_ATTACHMENT}', or '{CLIENT_EXTENSION_RESULTS}'"
+ )
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
@@ -612,34 +646,6 @@ where
deserializer.deserialize_identifier(FieldVisitor::<IGNORE>)
}
}
- /// Deserializes the value for type.
- struct Type;
- impl<'e> Deserialize<'e> for Type {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'e>,
- {
- /// `Visitor` for `Type`.
- struct TypeVisitor;
- impl Visitor<'_> for TypeVisitor {
- type Value = Type;
- fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- formatter.write_str(PUBLIC_KEY)
- }
- fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
- where
- E: Error,
- {
- if v == PUBLIC_KEY {
- Ok(Type)
- } else {
- Err(E::invalid_value(Unexpected::Str(v), &PUBLIC_KEY))
- }
- }
- }
- deserializer.deserialize_str(TypeVisitor)
- }
- }
let mut opt_id = None;
let mut typ = None;
let mut raw = None;
@@ -791,7 +797,7 @@ const REG_FIELDS: &[&str; 6] = &[
impl<'de, const REL: bool, const REGI: bool, R, E> Deserialize<'de>
for PublicKeyCredential<REL, REGI, R, E>
where
- R: for<'a> Deserialize<'a>,
+ R: Deserialize<'de>,
E: for<'a> Deserialize<'a> + ClientExtensions,
{
/// Deserializes a `struct` based on
@@ -821,7 +827,7 @@ impl Serialize for AllAcceptedCredentialsOptions<'_, '_> {
/// # #[cfg(feature = "bin")]
/// # use webauthn_rp::bin::Decode;
/// # use webauthn_rp::{
- /// # request::{register::UserHandle, AsciiDomain, RpId},
+ /// # request::{register::{UserHandle, USER_HANDLE_MIN_LEN}, AsciiDomain, RpId},
/// # response::{error::CredentialIdErr, AllAcceptedCredentialsOptions, CredentialId},
/// # };
/// /// Retrieves the `CredentialId`s associated with `user_id` from the database.
@@ -832,9 +838,9 @@ impl Serialize for AllAcceptedCredentialsOptions<'_, '_> {
/// }
/// /// Retrieves the `UserHandle` from a session cookie.
/// # #[cfg(feature = "custom")]
- /// fn get_user_handle() -> UserHandle<Vec<u8>> {
+ /// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MIN_LEN]> {
/// // ⋮
- /// # UserHandle::try_from(vec![0]).unwrap()
+ /// # [0].into()
/// }
/// # #[cfg(feature = "custom")]
/// let user_id = get_user_handle();
@@ -881,7 +887,7 @@ impl Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_> {
/// # #[cfg(feature = "bin")]
/// # use webauthn_rp::bin::Decode;
/// # use webauthn_rp::{
- /// # request::{register::{Nickname, PublicKeyCredentialUserEntity, UserHandle, Username}, AsciiDomain, RpId},
+ /// # request::{register::{Nickname, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MIN_LEN, Username}, AsciiDomain, RpId},
/// # response::CurrentUserDetailsOptions,
/// # AggErr,
/// # };
@@ -889,13 +895,13 @@ impl Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_> {
/// # #[cfg(feature = "bin")]
/// fn get_user_info(user_id: UserHandle<&[u8]>) -> Result<(Username, Option<Nickname>), AggErr> {
/// // ⋮
- /// # Ok((Username::decode("foo".to_owned()).unwrap(), Some(Nickname::decode("foo".to_owned()).unwrap())))
+ /// # Ok((Username::decode("foo").unwrap(), Some(Nickname::decode("foo").unwrap())))
/// }
/// /// Retrieves the `UserHandle` from a session cookie.
/// # #[cfg(feature = "custom")]
- /// fn get_user_handle() -> UserHandle<Vec<u8>> {
+ /// fn get_user_handle() -> UserHandle<[u8; USER_HANDLE_MIN_LEN]> {
/// // ⋮
- /// # UserHandle::try_from(vec![0]).unwrap()
+ /// # [0].into()
/// }
/// # #[cfg(feature = "custom")]
/// let user_handle = get_user_handle();