commit 09f505df6d54ab95baeb64256e3203d412598266
parent 8d4bd507df89f0c896ff8b4cd795aa936a8738ca
Author: Zack Newman <zack@philomathiclife.com>
Date: Sat, 7 Dec 2024 10:14:04 -0700
level 3 api
Diffstat:
35 files changed, 33015 insertions(+), 37 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,24 +1,61 @@
[package]
authors = ["Zack Newman <zack@philomathiclife.com>"]
-categories = ["api-bindings", "authentication", "web-programming::http-server"]
-description = "Web Authentication (WebAuthn) Level 3 Relying Party (RP) API."
+categories = ["api-bindings", "authentication", "web-programming"]
+description = "Server-side Web Authentication (WebAuthn) Relying Party (RP) API."
documentation = "https://docs.rs/webauthn_rp/latest/webauthn_rp/"
edition = "2021"
-keywords = ["authentication", "fido2", "rp", "webauthn"]
+keywords = ["authentication", "fido2", "passkey", "rp", "webauthn"]
license = "MIT OR Apache-2.0"
name = "webauthn_rp"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/webauthn_rp/"
-version = "0.1.0"
+rust-version = "1.82.0"
+version = "0.2.0"
-[lib]
-name = "webauthn_rp"
-path = "src/lib.rs"
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
+[dependencies]
+data-encoding = { version = "2.6.0", default-features = false }
+ed25519-dalek = { version = "2.1.1", default-features = false, features = ["fast"] }
+p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] }
+p384 = { version = "0.13.0", default-features = false, features = ["ecdsa"] }
+precis-profiles = { version = "0.1.11", default-features = false }
+rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
+rsa = { version = "0.9.7", default-features = false, features = ["sha2"] }
+serde = { version = "1.0.215", default-features = false, features = ["alloc"], optional = true }
+serde_json = { version = "1.0.133", default-features = false, features = ["alloc"], optional = true }
+url = { version = "2.5.4", default-features = false }
+
+[dev-dependencies]
+data-encoding = { version = "2.6.0", default-features = false, features = ["alloc"] }
+ed25519-dalek = { version = "2.1.1", default-features = false, features = ["alloc", "pkcs8"] }
+p256 = { version = "0.13.2", default-features = false, features = ["pem"] }
+p384 = { version = "0.13.0", default-features = false, features = ["pkcs8"] }
+serde_json = { version = "1.0.133", default-features = false, features = ["preserve_order"] }
+
+
+### FEATURES #################################################################
+
+[features]
+default = ["bin", "serde"]
+
+# Provide binary (de)serialization for persistent data.
+bin = []
+
+# Provide constructors for types that ideally are constructed
+# indirectly via the bin and serde features.
+custom = []
+
+# Provide client (de)serialization based on JSON-motivated
+# data structures.
+serde = ["data-encoding/alloc", "dep:serde"]
-[badges]
-maintenance = { status = "actively-developed" }
+# Provide "relaxed" JSON deserialization implementations.
+serde_relaxed = ["serde", "dep:serde_json"]
-[profile.release]
-lto = true
-panic = 'abort'
-strip = true
+# Provide binary (de)serialization for RegistrationServerState
+# and AuthenticationServerState to avoid the use of in-memory
+# collections for ceremony completion.
+serializable_server_state = ["bin"]
diff --git a/LICENSE-MIT b/LICENSE-MIT
@@ -1,4 +1,4 @@
-Copyright © 2023 Zack Newman
+Copyright © 2024 Zack Newman
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
diff --git a/README.md b/README.md
@@ -1,16 +1,210 @@
-# webauthn_rp
-
-`webauthn_rp` will be a library for [Web Authentication (WebAuthn) Level 3](https://www.w3.org/TR/webauthn-3/#sctn-rp-operations)
-Relying Party (RP) operations. For better cross-platform compatibility and Rust integration, it will rely on
-[`ring`](https://docs.rs/ring/latest/ring/) for crypto operations; but as development progresses, it may allow the use of
-[`openssl`](https://docs.rs/openssl/latest/openssl/) in a way that is compatible with [LibreSSL](https://www.libressl.org/).
-
-There will be no attempt to adhere to standards that are not accepted as "legitimate" by _actual_ cryptographers (e.g., Federal Information Processing Standards (FIPS)).
-
-### Status
-
-This package is still in development.
-
-The crate will only be tested on the `x86_64-unknown-linux-gnu` and `x86_64-unknown-openbsd` targets, but
-it should work on any [Tier 1 with Host Tools](https://doc.rust-lang.org/beta/rustc/platform-support.html)
-target.
+# `webauthn_rp`
+
+[<img alt="git" src="https://git.philomathiclife.com/badges/webauthn_rp.svg" height="20">](https://git.philomathiclife.com/webauthn_rp/log.html)
+[<img alt="crates.io" src="https://img.shields.io/crates/v/webauthn_rp.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/webauthn_rp)
+[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-webauthn_rp-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/webauthn_rp/latest/webauthn_rp/)
+
+`webauthn_rp` is a library for _server-side_
+[Web Authentication (WebAuthn)](https://www.w3.org/TR/webauthn-3/#sctn-rp-operations) Relying Party
+(RP) operations.
+
+The purpose of a server-side RP library is to be modular so that any client can be used with it as a backend
+_including_ native applications—WebAuthn technically only covers web applications; however it's relatively easy
+to adapt to native applications as well. It achieves this by not assuming how data is sent to/from the client;
+having said that, there are pre-defined serialization formats for "common" deployments which can be used when
+[`serde`](#serde) is enabled.
+
+## Cargo "features"
+
+[`custom`](#custom) or both [`bin`](#bin) and [`serde`](#serde) must be enabled; otherwise a `compile_error`
+ will occur.
+
+### `bin`
+
+Enables binary (de)serialization via `Encode` and `Decode`. Since registered credentials will almost always
+have to be saved to persistent storage, _some_ form of (de)serialization is necessary. In the event `bin` is
+unsuitable or only partially suitable (e.g., human-readable output is desired), one will need to enable
+[`custom`](#custom) to allow construction of certain types (e.g., `AuthenticatedCredential`).
+
+If possible and desired, one may wish to save the data "directly" to avoid any potential temporary allocations.
+For example `StaticState::encode` will return a `Vec` containing hundreds (and possibly thousands in the
+extreme case) of bytes if the underlying public key is an RSA key. This additional allocation and copy of data
+is obviously avoided if `StaticState` is stored as a
+[composite type](https://www.postgresql.org/docs/current/rowtypes.html) or its fields are stored in separate
+columns when written to a relational database (RDB).
+
+### `custom`
+
+Exposes functions (e.g., `AuthenticatedCredential::new`) that allows one to construct instances of types that
+cannot be constructed when [`bin`](#bin) or [`serde`](#serde) is not enabled.
+
+### `serde`
+
+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
+data has to be sent to/from the client, _some_ form of (de)serialization is necessary. In the event `serde`
+is unsuitable or only partially suitable, one will need to enable [`custom`](#custom) to allow construction
+of certain types (e.g., `Registration`).
+
+Code is _strongly_ encouraged to rely on the `Deserialize` implementations as much as possible to reduce the
+chances of improperly deserializing the client data.
+
+Note that clients are free to send data in whatever form works best, so there is no requirement the
+JSON-motivated definitions are used even when JSON is sent. This is especially relevant since the JSON-motivated
+definitions were only added in [WebAuthn Level 3](https://www.w3.org/TR/webauthn-3/); thus many deployments only
+partially conform. Some specific deviations that may require partial customization of deserialization are the
+following:
+
+* [`ArrayBuffer`](https://webidl.spec.whatwg.org/#idl-ArrayBuffer)s encoded using something other than
+ base64url.
+* `ArrayBuffer`s that are encoded multiple times (including the use of different encodings each time).
+* Missing fields (e.g.,
+ [`transports`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-transports)).
+* Different field names (e.g., `extensions` instead of
+ [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-clientextensionresults)).
+
+### `serde_relaxed`
+
+Automatically enables [`serde`](#serde) in addition to "relaxed" `Deserialize` implementations
+(e.g., `RegistrationRelaxed`). Roughly "relaxed" translates to unknown fields being ignored and only
+the fields necessary for construction of the type are required. Case still matters, duplicate fields are still
+forbidden, and interrelated data validation is still performed when applicable. This can be useful when one
+wants to accommodate non-conforming clients or clients that implement older versions of the spec.
+
+### `serializable_server_state`
+
+Automatically enables [`bin`](#bin) in addition to `Encode` and `Decode` implementations for
+`RegistrationServerState` and `AuthenticationServerState`. Less accurate `SystemTime` is used instead of
+`Instant` for timeout enforcement. This should be enabled if you don't desire to use in-memory collections to
+store the instances of those types.
+
+Note even when written to persistent storage, an application should still periodically remove expired ceremonies.
+If one is using a relational database (RDB); then one can achieve this by storing `ServerState::sent_challenge`,
+the `Vec` returned from `Encode::encode`, and `ServerState::expiration` and periodically remove all rows
+whose expiration exceeds the current date and time.
+
+## Registration and authentication
+
+Both [registration](https://www.w3.org/TR/webauthn-3/#registration-ceremony) and
+[authentication](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) ceremonies rely on "challenges", and
+these challenges are inherently temporary. For this reason the data associated with challenge completion can
+often be stored in memory without concern for out-of-memory (OOM) conditions. There are several benefits to
+storing such data in memory:
+
+* No data manipulation
+ * By leveraging move semantics, the data sent to the client cannot be mutated once the ceremony begins.
+* Improved timeout enforcement
+ * By ensuring the same machine that started the ceremony is also used to finish the ceremony, deviation of
+ system clocks is not a concern. Additionally, allowing serialization requires the use of some form of
+ cross-platform "timestamp" (e.g., [Unix time](https://en.wikipedia.org/wiki/Unix_time)) which differ in
+ implementation (e.g., platforms implement leap seconds in different ways) and are often not monotonically
+ increasing. If data resides in memory, a monotonic `Instant` can be used instead.
+
+It is for those reasons data like `RegistrationServerState` are not serializable by default and require the
+use of in-memory collections (e.g., `FixedCapHashSet`). To better ensure OOM is not a concern, RPs should set
+reasonable timeouts. Since ceremonies can only be completed by moving data (e.g.,
+`RegistrationServerState::verify`), ceremony completion is guaranteed to free up the memory used—
+`RegistrationServerState` instances are only 48 bytes on `x86_64-unknown-linux-gnu` platforms. To avoid issues
+related to incomplete ceremonies, RPs can periodically iterate the collection for expired ceremonies and remove
+such data. Other techniques can be employed as well to mitigate OOM, but they are application specific and
+out-of-scope. If this is undesirable, one can enable [`serializable_server_state`](#serializable_server_state)
+so that `RegistrationServerState` and `AuthenticationServerState` implement `Encode` and `Decode`. Another
+reason one may need to store this information persistently is for load-balancing purposes where the server that
+started the ceremony is not guaranteed to be the server that finishes the ceremony.
+
+## Supported signature algorithms
+
+The only supported signature algorithms are the following:
+
+* Ed25519 as defined in [RFC 8032 § 5.1](https://www.rfc-editor.org/rfc/rfc8032#section-5.1). This corresponds
+ to `CoseAlgorithmIdentifier::Eddsa`.
+* ECDSA as defined in [SEC 1 Version 2.0 § 4.1](https://www.secg.org/sec1-v2.pdf#subsection.4.1) using SHA-256
+ as the hash function and NIST P-256 as defined in
+ [NIST SP 800-186 § 3.2.1.3](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf#%5B%7B%22num%22%3A229%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C70%2C275%2C0%5D)
+ for the underlying elliptic curve. This corresponds to `CoseAlgorithmIdentifier::Es256`.
+* ECDSA as defined in SEC 1 Version 2.0 § 4.1 using SHA-384 as the hash function and NIST P-384 as defined in
+ [NIST SP 800-186 § 3.2.1.4](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf#%5B%7B%22num%22%3A232%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C70%2C264%2C0%5D)
+ for the underlying elliptic curve. This corresponds to `CoseAlgorithmIdentifier::Es384`.
+* RSASSA-PKCS1-v1_5 as defined in [RFC 8017 § 8.2](https://www.rfc-editor.org/rfc/rfc8017#section-8.2) using
+ SHA-256 as the hash function. This corresponds to `CoseAlgorithmIdentifier::Rs256`.
+
+## Correctness of code
+
+This library more strictly adheres to the spec than many other similar libraries including but not limited to
+the following ways:
+
+* [CTAP2 canonical CBOR encoding form](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#ctap2-canonical-cbor-encoding-form).
+* `Deserialize` implementations requiring _exact_ conformance (e.g., not allowing unknown data).
+* More thorough interrelated data validation (e.g., all places a Credential ID exists must match).
+* Implement a lot of recommended (i.e., SHOULD) criteria (e.g.,
+ [User display names conforming to the Nickname Profile as defined in RFC 8266](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name)).
+
+Unfortunately like almost all software, this library has not been formally verified; however great care is
+employed in the following ways:
+
+* Leverage move semantics to prevent mutation of data once in a static state.
+* Ensure a great many invariants via types.
+* Reduce code duplication.
+* Reduce variable mutation allowing for simpler algebraic reasoning.
+* `panic`-free code[^note] (i.e., define true/total functions).
+* Ensure arithmetic "side effects" don't occur (e.g., overflow).
+* Aggressive use of compiler and [Clippy](https://doc.rust-lang.org/stable/clippy/lints.html) lints.
+* Unit tests for common cases, edge cases, and error cases.
+
+## Cryptographic libraries
+
+This library does not rely on _any_ sensitive data (e.g., private keys) as only signature verification is
+ever performed. This means that the only thing that matters with the libraries used is their algorithmic
+correctness and not other normally essential aspects like susceptibility to side-channel attacks. While I
+personally believe the libraries that are used are at least as "secure" as alternatives even when dealing with
+sensitive data, one only needs to audit the correctness of the libraries to be confident in their use. In fact
+[`curve25519_dalek`](https://docs.rs/curve25519-dalek/latest/curve25519_dalek/#backends) has been formally
+verified when the [`fiat`](https://github.com/mit-plv/fiat-crypto) backend is used making it _objectively_
+better than many other libraries whose correctness has not been proven. Two additional benefits of the library
+choices are simpler APIs making it more likely their use is correct and better cross-platform compatibility.
+
+## Minimum Supported Rust Version (MSRV)
+
+This will frequently be updated to be the same as stable. Specifically, any time stable is updated and that
+update has "useful" features or compilation no longer succeeds (e.g., due to new compiler lints), then MSRV
+will be updated.
+
+MSRV changes will correspond to a SemVer patch version bump pre-`1.0.0`; otherwise a minor version bump.
+
+## SemVer Policy
+
+* All on-by-default features of this library are covered by SemVer
+* MSRV is considered exempt from SemVer as noted above
+
+## License
+
+Licensed under either of
+
+* Apache License, Version 2.0 ([LICENSE-APACHE](https://www.apache.org/licenses/LICENSE-2.0))
+* MIT license ([LICENSE-MIT](https://opensource.org/licenses/MIT))
+
+at your option.
+
+## Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you,
+as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
+
+Before any PR is sent, `cargo clippy` and `cargo t` should be run _for each possible combination of "features"_
+using stable Rust. One easy way to achieve this is by building `ci` and invoking it with no commands in the
+`webauthn_rp` directory or sub-directories. You can fetch `ci` via `git clone https://git.philomathiclife.com/repos/ci`,
+and it can be built with `cargo build --release`. Additionally,
+`RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be built.
+
+### Status
+
+This package is actively maintained and will conform to the
+[latest WebAuthn API version](https://www.w3.org/TR/webauthn-3/). Previous versions will not be supported—excluding
+bug fixes of course—however functionality will exist to facilitate the migration process from the previous version.
+
+The crate is only tested on `x86_64-unknown-linux-gnu` and `x86_64-unknown-openbsd` targets, but it should work
+on most platforms.
+
+[^note]: `panic`s related to memory allocations or stack overflow are possible since such issues are not
+ formally guarded against.
diff --git a/src/bin.rs b/src/bin.rs
@@ -0,0 +1,412 @@
+extern crate alloc;
+use alloc::borrow::Cow;
+use core::str;
+/// Unit error returned from [`Encode::encode_into_buffer`] and
+/// [`Decode::decode_from_buffer`] to simply signal an error occurred.
+pub(super) struct EncDecErr;
+/// Encodes data as a binary format into a passed `Vec` buffer with the possibility
+/// of erring.
+pub(super) trait EncodeBufferFallible {
+ /// Error when [`Self::encode_into_buffer`] fails.
+ type Err;
+ /// Writes `self` into `buffer`.
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err>;
+}
+/// Encodes data as a binary format into a passed `Vec` buffer.
+pub(super) trait EncodeBuffer {
+ /// Writes `self` into `buffer`.
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>);
+}
+impl EncodeBuffer for u8 {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ buffer.push(*self);
+ }
+}
+impl EncodeBuffer for u16 {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ buffer.extend_from_slice(self.to_le_bytes().as_slice());
+ }
+}
+impl EncodeBuffer for u32 {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ buffer.extend_from_slice(self.to_le_bytes().as_slice());
+ }
+}
+impl EncodeBuffer for u64 {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ buffer.extend_from_slice(self.to_le_bytes().as_slice());
+ }
+}
+impl EncodeBuffer for u128 {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ buffer.extend_from_slice(self.to_le_bytes().as_slice());
+ }
+}
+// `false as u8` is defined as 0.
+/// `false` tag.
+#[expect(clippy::as_conversions, reason = "this is safe, and we want a const")]
+const FALSE: u8 = false as u8;
+// `true as u8` is defined as 1.
+/// `true` tag.
+#[expect(clippy::as_conversions, reason = "this is safe, and we want a const")]
+const TRUE: u8 = true as u8;
+impl EncodeBuffer for bool {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ buffer.push(if *self { TRUE } else { FALSE });
+ }
+}
+// We don't implement `EncodeBuffer` for `[T]` since we only ever need `[u8]`; and one can specialize
+// the implementation such that it's _a lot_ faster than a generic `T`.
+impl EncodeBufferFallible for [u8] {
+ type Err = EncDecErr;
+ /// # Errors
+ ///
+ /// Errors iff `self.len() > usize::from(u16::MAX)`.
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ u16::try_from(self.len())
+ .map_err(|_e| EncDecErr)
+ .map(|len| {
+ len.encode_into_buffer(buffer);
+ buffer.extend_from_slice(self);
+ })
+ }
+}
+// We don't implement `EncodeBuffer` for `Vec<T>` since we only ever need `Vec<u8>`; and one can specialize
+// the implementation such that it's _a lot_ faster than a generic `T`.
+impl EncodeBufferFallible for Vec<u8> {
+ type Err = EncDecErr;
+ /// # Errors
+ ///
+ /// See [`[u8]::encode_into_buffer`].
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ self.as_slice().encode_into_buffer(buffer)
+ }
+}
+// We don't implement `EncodeBuffer` for `[T; LEN]` since we only ever need `[u8; LEN]`; and one can specialize
+// the implementation such that it's _a lot_ faster than a generic `T`.
+impl<const LEN: usize> EncodeBuffer for [u8; LEN]
+where
+ [u8; LEN]: Default,
+{
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ buffer.extend_from_slice(self.as_slice());
+ }
+}
+/// [`Option::None`] tag.
+const NONE: u8 = 0;
+/// [`Option::Some`] tag.
+const SOME: u8 = 1;
+impl<T: EncodeBuffer> EncodeBuffer for Option<T> {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ None => {
+ buffer.push(NONE);
+ }
+ Some(ref val) => {
+ buffer.push(SOME);
+ val.encode_into_buffer(buffer);
+ }
+ }
+ }
+}
+impl<T: EncodeBuffer, T2: EncodeBuffer> EncodeBuffer for (T, T2) {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.0.encode_into_buffer(buffer);
+ self.1.encode_into_buffer(buffer);
+ }
+}
+impl EncodeBufferFallible for str {
+ type Err = EncDecErr;
+ /// # Errors
+ ///
+ /// See [`[u8]::encode_into_buffer`].
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ self.as_bytes().encode_into_buffer(buffer)
+ }
+}
+impl EncodeBufferFallible for String {
+ type Err = EncDecErr;
+ /// # Errors
+ ///
+ /// See [`[u8]::encode_into_buffer`].
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ self.as_str().encode_into_buffer(buffer)
+ }
+}
+impl EncodeBufferFallible for Cow<'_, str> {
+ type Err = EncDecErr;
+ /// # Errors
+ ///
+ /// See [`[u8]::encode_into_buffer`].
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ self.as_ref().encode_into_buffer(buffer)
+ }
+}
+impl<T: EncodeBuffer + ?Sized> EncodeBuffer for &T {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ (**self).encode_into_buffer(buffer);
+ }
+}
+impl<T: EncodeBuffer + ?Sized> EncodeBuffer for &mut T {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ (**self).encode_into_buffer(buffer);
+ }
+}
+impl<T: EncodeBufferFallible + ?Sized> EncodeBufferFallible for &T {
+ type Err = T::Err;
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ (**self).encode_into_buffer(buffer)
+ }
+}
+impl<T: EncodeBufferFallible + ?Sized> EncodeBufferFallible for &mut T {
+ type Err = T::Err;
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ (**self).encode_into_buffer(buffer)
+ }
+}
+impl<T: EncodeBufferFallible> EncodeBufferFallible for Option<T> {
+ type Err = T::Err;
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ match *self {
+ None => {
+ buffer.push(NONE);
+ Ok(())
+ }
+ Some(ref val) => {
+ buffer.push(SOME);
+ val.encode_into_buffer(buffer)
+ }
+ }
+ }
+}
+/// Decodes binary data generated by [`EncodeBuffer::encode_into_buffer`].
+pub(super) trait DecodeBuffer<'a>: Sized {
+ /// Error returned from [`Self::decode_from_buffer`].
+ type Err;
+ /// Transforms a sub-`slice` of `data` into `Self`; and upon success,
+ /// mutates `data` to be the remaining portion.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `data` cannot be decoded into `Self`.
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err>;
+}
+impl<'a> DecodeBuffer<'a> for u8 {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ data.split_first().ok_or(EncDecErr).map(|(&val, rem)| {
+ *data = rem;
+ val
+ })
+ }
+}
+impl<'a> DecodeBuffer<'a> for u16 {
+ type Err = EncDecErr;
+ #[expect(
+ clippy::little_endian_bytes,
+ 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)
+ .ok_or(EncDecErr)
+ .map(|(le_bytes, rem)| {
+ *data = rem;
+ let mut val = [0; 2];
+ val.copy_from_slice(le_bytes);
+ Self::from_le_bytes(val)
+ })
+ }
+}
+impl<'a> DecodeBuffer<'a> for u32 {
+ type Err = EncDecErr;
+ #[expect(
+ clippy::little_endian_bytes,
+ 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)
+ .ok_or(EncDecErr)
+ .map(|(le_bytes, rem)| {
+ *data = rem;
+ let mut val = [0; 4];
+ val.copy_from_slice(le_bytes);
+ Self::from_le_bytes(val)
+ })
+ }
+}
+impl<'a> DecodeBuffer<'a> for u64 {
+ type Err = EncDecErr;
+ #[expect(
+ clippy::little_endian_bytes,
+ 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)
+ .ok_or(EncDecErr)
+ .map(|(le_bytes, rem)| {
+ *data = rem;
+ let mut val = [0; 8];
+ val.copy_from_slice(le_bytes);
+ Self::from_le_bytes(val)
+ })
+ }
+}
+impl<'a> DecodeBuffer<'a> for u128 {
+ type Err = EncDecErr;
+ #[expect(
+ clippy::little_endian_bytes,
+ 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)
+ .ok_or(EncDecErr)
+ .map(|(le_bytes, rem)| {
+ *data = rem;
+ let mut val = [0; 16];
+ val.copy_from_slice(le_bytes);
+ Self::from_le_bytes(val)
+ })
+ }
+}
+impl<'a> DecodeBuffer<'a> for bool {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ FALSE => Ok(false),
+ TRUE => Ok(true),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+// We don't implement `DecodeBuffer` for `&'a [T]` since we only ever need `&[u8]`; and one can specialize
+// the implementation such that it's _a lot_ faster than a generic `T`.
+impl<'a> DecodeBuffer<'a> for &'a [u8] {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u16::decode_from_buffer(data).and_then(|len| {
+ data.split_at_checked(usize::from(len))
+ .ok_or(EncDecErr)
+ .map(|(val, rem)| {
+ *data = rem;
+ val
+ })
+ })
+ }
+}
+// We don't implement `DecodeBuffer` for `Vec<T>` since we only ever need `Vec<u8>`; and one can specialize
+// the implementation such that it's _a lot_ faster than a generic `T`.
+impl<'a> DecodeBuffer<'a> for Vec<u8> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ <&[u8]>::decode_from_buffer(data).map(ToOwned::to_owned)
+ }
+}
+// We don't implement `DecodeBuffer` for `[T; LEN]` since we only ever need `[u8; LEN]`; and one can specialize
+// the implementation such that it's _a lot_ faster than a generic `T`.
+impl<'a, const LEN: usize> DecodeBuffer<'a> for [u8; LEN]
+where
+ [u8; LEN]: Default,
+{
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ data.split_at_checked(LEN)
+ .ok_or(EncDecErr)
+ .map(|(val_slice, rem)| {
+ *data = rem;
+ let mut val = Self::default();
+ val.copy_from_slice(val_slice);
+ val
+ })
+ }
+}
+impl<'a, T> DecodeBuffer<'a> for Option<T>
+where
+ T: DecodeBuffer<'a, Err = EncDecErr>,
+{
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|tag| match tag {
+ NONE => Ok(None),
+ SOME => T::decode_from_buffer(data).map(Some),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl<'a, T, T2> DecodeBuffer<'a> for (T, T2)
+where
+ T: DecodeBuffer<'a, Err = EncDecErr>,
+ T2: DecodeBuffer<'a, Err = EncDecErr>,
+{
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ T::decode_from_buffer(data)
+ .and_then(|val| T2::decode_from_buffer(data).map(|val2| (val, val2)))
+ }
+}
+impl<'a> DecodeBuffer<'a> for &'a str {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ <&[u8]>::decode_from_buffer(data)
+ .and_then(|utf8| str::from_utf8(utf8).map_err(|_e| EncDecErr))
+ }
+}
+impl<'a> DecodeBuffer<'a> for String {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ <&str>::decode_from_buffer(data).map(ToOwned::to_owned)
+ }
+}
+/// Encodes `self` into a "primitive"-like type that can be saved to persistent storage
+/// and later decoded via [`Decode::decode`].
+///
+/// The purpose of this trait is to transform `Self` into something "easily" consumable by persistent
+/// storage which already has some form of inherent metadata (e.g., a column in a relational database (RDB)).
+pub trait Encode {
+ /// "Primitive"-like type that `self` will be converted into. This should be one of the following:
+ /// * [`u8`]
+ /// * [`i8`]
+ /// * [`u16`]
+ /// * [`i16`]
+ /// * [`u32`]
+ /// * [`i32`]
+ /// * [`u64`]
+ /// * [`i64`]
+ /// * [`u128`]
+ /// * [`i128`]
+ /// * [`f32`]
+ /// * [`f64`]
+ /// * [`bool`]
+ /// * `&[u8]`
+ /// * [`&str`](prim@str)
+ /// * `[u8; N]`
+ /// * `Vec<u8>`
+ /// * [`String`]
+ type Output<'a>
+ where
+ Self: 'a;
+ /// Error returned when encoding fails.
+ type Err;
+ /// Transforms `self` into a "primitive"-like type.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` cannot be encoded into [`Self::Output`].
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err>;
+}
+/// Decodes a "primitive"-like instance that was created via [`Encode::encode`]
+/// into `Self`.
+pub trait Decode: Sized {
+ /// "Primitive"-like input to be decoded.
+ ///
+ /// This should be the same as or the "owned" version of the corresponding [`Encode::Output`]. For example if
+ /// `Encode::Output` is `&[u8]`, then this should be `&[u8]` or `Vec<u8>`.
+ type Input<'a>;
+ /// Error returned when decoding fails.
+ type Err;
+ /// Decodes `input` into `Self`. Note `input` must not have any trailing data.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `input` cannot be decoded into `Self`.
+ fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err>;
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,12 +1,183 @@
-//! # `webauthn_rp`
+//! [![git]](https://git.philomathiclife.com/webauthn_rp/log.html) [![crates-io]](https://crates.io/crates/webauthn_rp) [![docs-rs]](crate)
//!
-//! `webauthn_rp` will be a library for [Web Authentication (WebAuthn) Level 3](https://www.w3.org/TR/webauthn-3/#sctn-rp-operations)
-//! Relying Party (RP) operations. For better cross-platform compatibility and Rust integration, it will rely on
-//! [`ring`](https://docs.rs/ring/latest/ring/) for crypto operations; but as development progresses, it may allow the use of
-//! [`openssl`](https://docs.rs/openssl/latest/openssl/) in a way that is compatible with [LibreSSL](https://www.libressl.org/).
+//! [git]: https://git.philomathiclife.com/git_badge.svg
+//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
+//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
//!
-//! There will be no attempt to adhere to standards that are not accepted as "legitimate" by _actual_ cryptographers (e.g., Federal Information Processing Standards (FIPS)).
+//! `webauthn_rp` is a library for _server-side_
+//! [Web Authentication (WebAuthn)](https://www.w3.org/TR/webauthn-3/#sctn-rp-operations) Relying Party
+//! (RP) operations.
+//!
+//! The purpose of a server-side RP library is to be modular so that any client can be used with it as a backend
+//! _including_ native applications—WebAuthn technically only covers web applications; however it's relatively easy
+//! to adapt to native applications as well. It achieves this by not assuming how data is sent to/from the client;
+//! having said that, there are pre-defined serialization formats for "common" deployments which can be used when
+//! [`serde`](#serde) is enabled.
+//!
+//! ## Cargo "features"
+//!
+//! [`custom`](#custom) or both [`bin`](#bin) and [`serde`](#serde) must be enabled; otherwise a [`compile_error`]
+//! will occur.
+//!
+//! ### `bin`
+//!
+//! Enables binary (de)serialization via [`Encode`] and [`Decode`]. Since registered credentials will almost always
+//! have to be saved to persistent storage, _some_ form of (de)serialization is necessary. In the event `bin` is
+//! unsuitable or only partially suitable (e.g., human-readable output is desired), one will need to enable
+//! [`custom`](#custom) to allow construction of certain types (e.g., [`AuthenticatedCredential`]).
+//!
+//! If possible and desired, one may wish to save the data "directly" to avoid any potential temporary allocations.
+//! For example [`StaticState::encode`] will return a [`Vec`] containing hundreds (and possibly thousands in the
+//! extreme case) of bytes if the underlying public key is an RSA key. This additional allocation and copy of data
+//! is obviously avoided if [`StaticState`] is stored as a
+//! [composite type](https://www.postgresql.org/docs/current/rowtypes.html) or its fields are stored in separate
+//! columns when written to a relational database (RDB).
+//!
+//! ### `custom`
+//!
+//! Exposes functions (e.g., [`AuthenticatedCredential::new`]) that allows one to construct instances of types that
+//! cannot be constructed when [`bin`](#bin) or [`serde`](#serde) is not enabled.
+//!
+//! ### `serde`
+//!
+//! 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
+//! data has to be sent to/from the client, _some_ form of (de)serialization is necessary. In the event `serde`
+//! is unsuitable or only partially suitable, one will need to enable [`custom`](#custom) to allow construction
+//! of certain types (e.g., [`Registration`]).
+//!
+//! Code is _strongly_ encouraged to rely on the [`Deserialize`] implementations as much as possible to reduce the
+//! chances of improperly deserializing the client data.
+//!
+//! Note that clients are free to send data in whatever form works best, so there is no requirement the
+//! JSON-motivated definitions are used even when JSON is sent. This is especially relevant since the JSON-motivated
+//! definitions were only added in [WebAuthn Level 3](https://www.w3.org/TR/webauthn-3/); thus many deployments only
+//! partially conform. Some specific deviations that may require partial customization of deserialization are the
+//! following:
+//!
+//! * [`ArrayBuffer`](https://webidl.spec.whatwg.org/#idl-ArrayBuffer)s encoded using something other than
+//! base64url.
+//! * `ArrayBuffer`s that are encoded multiple times (including the use of different encodings each time).
+//! * Missing fields (e.g.,
+//! [`transports`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-transports)).
+//! * Different field names (e.g., `extensions` instead of
+//! [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-clientextensionresults)).
+//!
+//! ### `serde_relaxed`
+//!
+//! Automatically enables [`serde`](#serde) in addition to "relaxed" [`Deserialize`] implementations
+//! (e.g., [`RegistrationRelaxed`]). Roughly "relaxed" translates to unknown fields being ignored and only
+//! the fields necessary for construction of the type are required. Case still matters, duplicate fields are still
+//! forbidden, and interrelated data validation is still performed when applicable. This can be useful when one
+//! wants to accommodate non-conforming clients or clients that implement older versions of the spec.
+//!
+//! ### `serializable_server_state`
+//!
+//! Automatically enables [`bin`](#bin) in addition to [`Encode`] and [`Decode`] implementations for
+//! [`RegistrationServerState`] and [`AuthenticationServerState`]. Less accurate [`SystemTime`] is used instead of
+//! [`Instant`] for timeout enforcement. This should be enabled if you don't desire to use in-memory collections to
+//! store the instances of those types.
+//!
+//! Note even when written to persistent storage, an application should still periodically remove expired ceremonies.
+//! If one is using a relational database (RDB); then one can achieve this by storing [`ServerState::sent_challenge`],
+//! the `Vec` returned from [`Encode::encode`], and [`ServerState::expiration`] and periodically remove all rows
+//! whose expiration exceeds the current date and time.
+//!
+//! ## Registration and authentication
+//!
+//! Both [registration](https://www.w3.org/TR/webauthn-3/#registration-ceremony) and
+//! [authentication](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) ceremonies rely on "challenges", and
+//! these challenges are inherently temporary. For this reason the data associated with challenge completion can
+//! often be stored in memory without concern for out-of-memory (OOM) conditions. There are several benefits to
+//! storing such data in memory:
+//!
+//! * No data manipulation
+//! * By leveraging move semantics, the data sent to the client cannot be mutated once the ceremony begins.
+//! * Improved timeout enforcement
+//! * By ensuring the same machine that started the ceremony is also used to finish the ceremony, deviation of
+//! system clocks is not a concern. Additionally, allowing serialization requires the use of some form of
+//! cross-platform "timestamp" (e.g., [Unix time](https://en.wikipedia.org/wiki/Unix_time)) which differ in
+//! implementation (e.g., platforms implement leap seconds in different ways) and are often not monotonically
+//! increasing. If data resides in memory, a monotonic [`Instant`] can be used instead.
+//!
+//! It is for those reasons data like [`RegistrationServerState`] are not serializable by default and require the
+//! use of in-memory collections (e.g., [`FixedCapHashSet`]). To better ensure OOM is not a concern, RPs should set
+//! reasonable timeouts. Since ceremonies can only be completed by moving data (e.g.,
+//! [`RegistrationServerState::verify`]), ceremony completion is guaranteed to free up the memory used—
+//! `RegistrationServerState` instances are only 48 bytes on `x86_64-unknown-linux-gnu` platforms. To avoid issues
+//! related to incomplete ceremonies, RPs can periodically iterate the collection for expired ceremonies and remove
+//! such data. Other techniques can be employed as well to mitigate OOM, but they are application specific and
+//! out-of-scope. If this is undesirable, one can enable [`serializable_server_state`](#serializable_server_state)
+//! so that `RegistrationServerState` and [`AuthenticationServerState`] implement [`Encode`] and [`Decode`]. Another
+//! reason one may need to store this information persistently is for load-balancing purposes where the server that
+//! started the ceremony is not guaranteed to be the server that finishes the ceremony.
+//!
+//! ## Supported signature algorithms
+//!
+//! The only supported signature algorithms are the following:
+//!
+//! * Ed25519 as defined in [RFC 8032 § 5.1](https://www.rfc-editor.org/rfc/rfc8032#section-5.1). This corresponds
+//! to [`CoseAlgorithmIdentifier::Eddsa`].
+//! * ECDSA as defined in [SEC 1 Version 2.0 § 4.1](https://www.secg.org/sec1-v2.pdf#subsection.4.1) using SHA-256
+//! as the hash function and NIST P-256 as defined in
+//! [NIST SP 800-186 § 3.2.1.3](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf#%5B%7B%22num%22%3A229%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C70%2C275%2C0%5D)
+//! for the underlying elliptic curve. This corresponds to [`CoseAlgorithmIdentifier::Es256`].
+//! * ECDSA as defined in SEC 1 Version 2.0 § 4.1 using SHA-384 as the hash function and NIST P-384 as defined in
+//! [NIST SP 800-186 § 3.2.1.4](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf#%5B%7B%22num%22%3A232%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C70%2C264%2C0%5D)
+//! for the underlying elliptic curve. This corresponds to [`CoseAlgorithmIdentifier::Es384`].
+//! * RSASSA-PKCS1-v1_5 as defined in [RFC 8017 § 8.2](https://www.rfc-editor.org/rfc/rfc8017#section-8.2) using
+//! SHA-256 as the hash function. This corresponds to [`CoseAlgorithmIdentifier::Rs256`].
+//!
+//! ## Correctness of code
+//!
+//! This library more strictly adheres to the spec than many other similar libraries including but not limited to
+//! the following ways:
+//!
+//! * [CTAP2 canonical CBOR encoding form](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#ctap2-canonical-cbor-encoding-form).
+//! * `Deserialize` implementations requiring _exact_ conformance (e.g., not allowing unknown data).
+//! * More thorough interrelated data validation (e.g., all places a Credential ID exists must match).
+//! * Implement a lot of recommended (i.e., SHOULD) criteria (e.g.,
+//! [User display names conforming to the Nickname Profile as defined in RFC 8266](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name)).
+//!
+//! Unfortunately like almost all software, this library has not been formally verified; however great care is
+//! employed in the following ways:
+//!
+//! * Leverage move semantics to prevent mutation of data once in a static state.
+//! * Ensure a great many invariants via types.
+//! * Reduce code duplication.
+//! * Reduce variable mutation allowing for simpler algebraic reasoning.
+//! * `panic`-free code[^note] (i.e., define true/total functions).
+//! * Ensure arithmetic "side effects" don't occur (e.g., overflow).
+//! * Aggressive use of compiler and [Clippy](https://doc.rust-lang.org/stable/clippy/lints.html) lints.
+//! * Unit tests for common cases, edge cases, and error cases.
+//!
+//! ## Cryptographic libraries
+//!
+//! This library does not rely on _any_ sensitive data (e.g., private keys) as only signature verification is
+//! ever performed. This means that the only thing that matters with the libraries used is their algorithmic
+//! correctness and not other normally essential aspects like susceptibility to side-channel attacks. While I
+//! personally believe the libraries that are used are at least as "secure" as alternatives even when dealing with
+//! sensitive data, one only needs to audit the correctness of the libraries to be confident in their use. In fact
+//! [`curve25519_dalek`](https://docs.rs/curve25519-dalek/latest/curve25519_dalek/#backends) has been formally
+//! verified when the [`fiat`](https://github.com/mit-plv/fiat-crypto) backend is used making it _objectively_
+//! better than many other libraries whose correctness has not been proven. Two additional benefits of the library
+//! choices are simpler APIs making it more likely their use is correct and better cross-platform compatibility.
+//!
+//! [^note]: `panic`s related to memory allocations or stack overflow are possible since such issues are not
+//! formally guarded against.
+#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(
+ unknown_lints,
+ future_incompatible,
+ let_underscore,
+ missing_docs,
+ nonstandard_style,
+ refining_impl_trait,
+ rust_2018_compatibility,
+ rust_2018_idioms,
+ rust_2021_compatibility,
+ rust_2024_compatibility,
unsafe_code,
unused,
warnings,
@@ -21,4 +192,988 @@
clippy::style,
clippy::suspicious
)]
-#![allow(clippy::blanket_clippy_restriction_lints)]
+#![expect(
+ clippy::blanket_clippy_restriction_lints,
+ clippy::exhaustive_enums,
+ clippy::exhaustive_structs,
+ clippy::implicit_return,
+ clippy::min_ident_chars,
+ clippy::missing_trait_methods,
+ clippy::pub_with_shorthand,
+ clippy::pub_use,
+ clippy::ref_patterns,
+ clippy::self_named_module_files,
+ clippy::single_call_fn,
+ clippy::single_char_lifetime_names,
+ clippy::unseparated_literal_suffix,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+#[cfg(not(any(feature = "custom", all(feature = "bin", feature = "serde"))))]
+compile_error!("'custom' must be enabled or both 'bin' and 'serde' must be enabled");
+#[cfg(feature = "serializable_server_state")]
+use crate::request::{
+ auth::ser_server_state::{
+ DecodeAuthenticationServerStateErr, EncodeAuthenticationServerStateErr,
+ },
+ register::ser_server_state::DecodeRegistrationServerStateErr,
+};
+#[cfg(any(feature = "bin", feature = "custom"))]
+use crate::response::error::CredentialIdErr;
+#[cfg(feature = "serde_relaxed")]
+use crate::response::ser_relaxed::SerdeJsonErr;
+#[cfg(feature = "bin")]
+use crate::{
+ request::register::bin::{DecodeNicknameErr, DecodeUsernameErr},
+ response::{
+ bin::DecodeAuthTransportsErr,
+ register::bin::{DecodeDynamicStateErr, DecodeStaticStateErr},
+ },
+};
+use crate::{
+ request::{
+ auth::error::{RequestOptionsErr, SecondFactorErr},
+ error::{AsciiDomainErr, DomainOriginParseErr, PortParseErr, SchemeParseErr, UrlErr},
+ register::{
+ error::{CreationOptionsErr, NicknameErr, UserHandleErr, UsernameErr},
+ ResidentKeyRequirement, UserHandle,
+ },
+ },
+ response::{
+ auth::error::{AuthCeremonyErr, AuthenticatorDataErr as AuthAuthDataErr},
+ error::CollectedClientDataErr,
+ register::{
+ error::{
+ AaguidErr, AttestationObjectErr, AuthenticatorDataErr as RegAuthDataErr,
+ RegCeremonyErr,
+ },
+ CredentialProtectionPolicy, DynamicState, Metadata, StaticState, UncompressedPubKey,
+ },
+ AuthTransports, CredentialId,
+ },
+};
+#[cfg(doc)]
+use crate::{
+ request::{
+ auth::{AllowedCredential, AllowedCredentials},
+ register::{CoseAlgorithmIdentifier, Nickname, Username},
+ AsciiDomain, DomainOrigin, FixedCapHashSet, Port, PublicKeyCredentialDescriptor, RpId,
+ Scheme, ServerState, Url,
+ },
+ response::{
+ auth::{self, AuthenticatorAssertion},
+ register::{
+ self, Aaguid, Attestation, AttestationObject, AttestedCredentialData,
+ AuthenticatorExtensionOutput, ClientExtensionsOutputs, CompressedPubKey,
+ CredentialPropertiesOutput,
+ },
+ CollectedClientData, Flag,
+ },
+};
+#[cfg(all(doc, feature = "bin"))]
+use bin::{Decode, Encode};
+#[cfg(doc)]
+use core::str::FromStr;
+use core::{
+ convert,
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+use data_encoding::{Encoding, BASE64URL_NOPAD};
+#[cfg(all(doc, feature = "serde_relaxed"))]
+use response::register::ser_relaxed::RegistrationRelaxed;
+#[cfg(all(doc, feature = "serde"))]
+use serde::Deserialize;
+#[cfg(all(doc, feature = "serde_relaxed"))]
+use serde_json::de::{Deserializer, StreamDeserializer};
+#[cfg(feature = "serializable_server_state")]
+use std::time::SystemTimeError;
+#[cfg(doc)]
+use std::time::{Instant, SystemTime};
+/// Contains functionality to (de)serialize data to a data store.
+#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+#[cfg(feature = "bin")]
+pub mod bin;
+/// Functionality for starting ceremonies.
+///
+/// # What kind of credential should I create?
+///
+/// Without partitioning the possibilities _too_ much, the following are possible authentication flows:
+///
+/// | Label | Username | Password | Client-side credential | Authenticator-side user verification | Recommended |
+/// |-------|----------|----------|------------------------|--------------------------------------|:-----------:|
+/// | 1 | Yes | Yes | Required | Yes | ❌ |
+/// | 2 | Yes | Yes | Required | No | ❌ |
+/// | 3 | Yes | Yes | Optional | Yes | ❌ |
+/// | <a name="label4">4</a> | Yes | Yes | Optional | No | ✅ |
+/// | 5 | Yes | No | Required | Yes | ❌ |
+/// | 6 | Yes | No | Required | No | ❌ |
+/// | <a name="label7">7</a> | Yes | No | Optional | Yes | ❔ |
+/// | 8 | Yes | No | Optional | No | ❌ |
+/// | 9 | No | Yes | Required | Yes | ❌ |
+/// | 10 | No | Yes | Required | No | ❌ |
+/// | 11 | No | Yes | Optional | Yes | ❌ |
+/// | 12 | No | Yes | Optional | No | ❌ |
+/// | <a name="label13">13</a> | No | No | Required | Yes | ✅ |
+/// | 14 | No | No | Required | No | ❌ |
+/// | 15 | No | No | Optional | Yes | ❌ |
+/// | 16 | No | No | Optional | No | ❌ |
+///
+/// * All `Label`s with both `Password` and `Authenticator-side user verification` set to `Yes` are not recommended
+/// since the verification done on the authenticator is likely the same "factor" as a password; thus it does not
+/// add benefit but only serves as an annoyance to users.
+/// * All `Label`s with `Username` or `Password` set to `Yes` and `Client-side credential` set to `Required` are not
+/// recommended since you may preclude authenticators that are storage constrained (e.g., security keys).
+/// * All `Label`s with `Username` set to `No` and `Client-side credential` set to `Optional` are not possible since
+/// RPs would not have a way to identify the set of encrypted credentials to pass to the unknown user.
+/// * All `Label`s with `Password` and `Authenticator-side user verification` set to `No` are not recommended since
+/// those are single-factor authentication schemes; thus anyone possessing the credential without also passing
+/// some form of user verification (e.g., password) would authenticate.
+/// * [`Label 7`](#label7) is possible for RPs that are comfortable passing an encrypted credential to a potential user
+/// without having that user first pass another form of authentication. For many RPs passing such information even
+/// if encrypted is not desirable though.
+/// * [`Label 4`](#label4) is ideal as a single-factor flow incorporated within a wider multi-factor authentication (MFA)
+/// setup. The easiest way to register such a credential is with
+/// [`PublicKeyCredentialCreationOptions::second_factor`].
+/// * [`Label 13`](#label13) is ideal for passkey setups as it allows for pleasant UX where a user does not have to type a
+/// username nor password while still being secured with MFA with one of the factors being based on public-key
+/// cryptography which for many is the most secure form of single-factor authentication. The easiest way to register
+/// such a credential is with [`PublicKeyCredentialCreationOptions::passkey`].
+///
+/// Two other reasons one may prefer to construct client-side credentials is richer support for extensions (e.g.,
+/// [`largeBlobKey`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-largeBlobKey-extension)
+/// for CTAP 2.2 authenticators) and the ability to use both discoverable and nondiscoverable requests (i.e.,
+/// [`PublicKeyCredentialRequestOptions::allow_credentials`] is empty and not empty respectively). The former is not
+/// relevant for this library—at least currently—since the only extensions supported are applicable for both
+/// client-side and server-side credentials. The latter can be important especially if an RP wants the ability to
+/// seamlessly transition from a username and password scheme to a userless and passwordless one in the future.
+///
+/// Note the table is purely informative. While helper functions
+/// (e.g., [`PublicKeyCredentialCreationOptions::passkey`]) only exist for [`Label 4`](#label4) and
+/// [`Label 13`](#label13), one can create any credential since all fields in [`PublicKeyCredentialCreationOptions`]
+/// and [`PublicKeyCredentialRequestOptions`] are accessible.
+pub mod request;
+/// Functionality for completing ceremonies.
+///
+/// Read [`request`] for more information about what credentials one should create.
+pub mod response;
+#[doc(inline)]
+pub use crate::{
+ request::{
+ auth::{
+ AuthenticationClientState, AuthenticationServerState, PublicKeyCredentialRequestOptions,
+ },
+ register::{
+ PublicKeyCredentialCreationOptions, RegistrationClientState, RegistrationServerState,
+ },
+ },
+ response::{auth::Authentication, register::Registration},
+};
+/// Error returned in [`RegCeremonyErr::Credential`] and [`AuthCeremonyErr::Credential`] as well as
+/// from [`AuthenticatedCredential::new`].
+#[derive(Clone, Copy, Debug)]
+pub enum CredentialErr {
+ /// Variant when [`CredentialProtectionPolicy::UserVerificationRequired`], but
+ /// [`DynamicState::user_verified`] is `false`.
+ CredProtectUserVerificationRequiredWithoutUserVerified,
+ /// Variant when [`AuthenticatorExtensionOutput::hmac_secret`] is `Some(true)`, but
+ /// [`DynamicState::user_verified`] is `false`.
+ HmacSecretWithoutUserVerified,
+ /// Variant when [`AuthenticatorExtensionOutput::hmac_secret`] is `Some(true)`, but
+ /// [`ClientExtensionsOutputs::prf`] is not `Some(AuthenticationExtensionsPRFOutputs { enabled: true })`.
+ HmacSecretWithoutPrf,
+ /// Variant when [`ClientExtensionsOutputs::prf`] is
+ /// `Some(AuthenticationExtensionsPRFOutputs { enabled: true })`, but
+ /// [`AuthenticatorExtensionOutput::hmac_secret`] is not `Some(true)`.
+ PrfWithoutHmacSecret,
+ /// Variant when [`ResidentKeyRequirement::Required`] was sent, but
+ /// [`CredentialPropertiesOutput::rk`] is `Some(false)`.
+ ResidentKeyRequiredServerCredentialCreated,
+}
+impl Display for CredentialErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::CredProtectUserVerificationRequiredWithoutUserVerified => {
+ "credProtect requires user verification, but the user is not verified"
+ }
+ Self::HmacSecretWithoutUserVerified => {
+ "hmac-secret is enabled, but the user is not verified"
+ }
+ Self::HmacSecretWithoutPrf => "hmac-secret was enabled but prf was not",
+ Self::PrfWithoutHmacSecret => "prf was enabled, but hmac-secret was not",
+ Self::ResidentKeyRequiredServerCredentialCreated => {
+ "server-side credential was created, but a client-side credential is required"
+ }
+ })
+ }
+}
+impl Error for CredentialErr {}
+/// Checks if the `static_state` and `dynamic_state` are valid for a credential.
+///
+/// # Errors
+///
+/// Errors iff `static_state` or `dynamic_state` are invalid.
+fn verify_static_and_dynamic_state<T>(
+ static_state: &StaticState<T>,
+ dynamic_state: DynamicState,
+) -> Result<(), CredentialErr> {
+ if dynamic_state.user_verified {
+ Ok(())
+ } else if matches!(
+ static_state.extensions.cred_protect,
+ CredentialProtectionPolicy::UserVerificationRequired
+ ) {
+ Err(CredentialErr::CredProtectUserVerificationRequiredWithoutUserVerified)
+ } else if static_state.extensions.hmac_secret.unwrap_or_default() {
+ Err(CredentialErr::HmacSecretWithoutUserVerified)
+ } else {
+ Ok(())
+ }
+}
+/// Registered credential that needs to be saved server-side to perform future
+/// [authentication ceremonies](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) with
+/// [`AuthenticatedCredential`].
+///
+/// When saving `RegisteredCredential` to persistent storage, one will almost always want to save the contained data
+/// separately. The reasons for this are the following:
+///
+/// * [`CredentialId`]
+/// * MUST be globally unique, and it will likely be easier to enforce such uniqueness when it's separate.
+/// * Fetching the [`AuthenticatedCredential`] by [`Authentication::raw_id`] when completing the
+/// authentication ceremony via [`AuthenticationServerState::verify`] will likely be easier than alternatives.
+/// * [`AuthTransports`]
+/// * Fetching [`CredentialId`]s and associated `AuthTransports` by [`UserHandle`] will likely make credential
+/// registration easier since one should set [`PublicKeyCredentialCreationOptions::exclude_credentials`] to
+/// the [`PublicKeyCredentialDescriptor`]s belonging to a `UserHandle` in order to avoid accidentally
+/// overwriting an existing credential on the authenticator.
+/// * Fetching `CredentialId`s and associated `AuthTransports` by `UserHandle` will likely make starting
+/// authentication ceremonies easier for non-discoverable requests (i.e., setting
+/// [`PublicKeyCredentialRequestOptions::allow_credentials`] to a non-empty [`AllowedCredentials`]).
+/// * [`UserHandle`]
+/// * Fetching the [`AuthenticatedCredential`] by [`Authentication::raw_id`] must also coincide with
+/// verifying the associated `UserHandle` matches [`AuthenticatorAssertion::user_handle`] when `Some`.
+/// * Fetching [`CredentialId`]s and associated [`AuthTransports`] by `UserHandle` will likely make credential
+/// registration easier since one should set [`PublicKeyCredentialCreationOptions::exclude_credentials`] to
+/// the [`PublicKeyCredentialDescriptor`]s belonging to a `UserHandle` in order to avoid accidentally
+/// overwriting an existing credential on the authenticator.
+/// * Fetching `CredentialId`s and associated `AuthTransports` by `UserHandle` will likely make starting
+/// authentication ceremonies easier for non-discoverable requests (i.e., setting
+/// [`PublicKeyCredentialRequestOptions::allow_credentials`] to a non-empty [`AllowedCredentials`]).
+/// * [`DynamicState`]
+/// * `DynamicState` is the only part that is ever updated after a successful authentication ceremony
+/// via [`AuthenticationServerState::verify`]. It being separate allows for smaller and quicker updates.
+/// * [`Metadata`]
+/// * Informative data that is never used during authentication ceremonies; consequently, one may wish to
+/// not even save this information.
+/// * [`StaticState`]
+/// * All other data exists as part of `StaticState`.
+///
+/// It is for those reasons that `RegisteredCredential` does not implement [`Encode`] or [`Decode`]; instead its parts
+/// do.
+///
+/// Note that [`RpId`] and user information other than the `UserHandle` are not stored in `RegisteredCredential`.
+/// RPs that wish to store such information must do so on their own. Since user information is likely the same
+/// for a given `UserHandle` and `RpId` is likely static, it makes little sense to store such information
+/// automatically. Types like [`Username`] implement `Encode` and `Decode` to assist such a thing.
+///
+/// When registering a credential, [`AttestedCredentialData::aaguid`], [`AttestedCredentialData::credential_id`],
+/// and [`AttestedCredentialData::credential_public_key`] will be the sources for [`Metadata::aaguid`],
+/// [`Self::id`], and [`StaticState::credential_public_key`] respectively. Additionally, there must be some way for
+/// the RP to know what `UserHandle` the [`Registration`] is associated with (e.g., a session cookie); thus the
+/// source of [`Self::user_id`] is the `UserHandle` passed to [`RegistrationServerState::verify`].
+///
+/// The only way to create this is via `RegistrationServerState::verify`.
+#[derive(Debug)]
+pub struct RegisteredCredential<'reg, 'user> {
+ /// The credential ID.
+ ///
+ /// For client-side credentials, this is a unique identifier; but for server-side
+ /// credentials, this _is_ the credential (i.e., the encrypted private key and necessary information).
+ id: CredentialId<&'reg [u8]>,
+ /// Hints for how the client might communicate with the authenticator containing the credential.
+ transports: AuthTransports,
+ /// The identifier for the user.
+ ///
+ /// Unlike [`Self::id`] which is globally unique for an RP, this is unique up to "user" (i.e.,
+ /// multiple [`CredentialId`]s will often exist for the same `UserHandle`).
+ user_id: UserHandle<&'user [u8]>,
+ /// Immutable state returned during registration.
+ static_state: StaticState<UncompressedPubKey<'reg>>,
+ /// State that can change during authentication ceremonies.
+ dynamic_state: DynamicState,
+ /// Metadata.
+ metadata: Metadata<'reg>,
+}
+impl<'reg, 'user> RegisteredCredential<'reg, 'user> {
+ /// The credential ID.
+ ///
+ /// For client-side credentials, this is a unique identifier; but for server-side
+ /// credentials, this _is_ the credential (i.e., the encrypted private key and necessary information).
+ #[inline]
+ #[must_use]
+ pub const fn id(&self) -> CredentialId<&'reg [u8]> {
+ self.id
+ }
+ /// Hints for how the client might communicate with the authenticator containing the credential.
+ #[inline]
+ #[must_use]
+ pub const fn transports(&self) -> AuthTransports {
+ self.transports
+ }
+ /// The identifier for the user.
+ ///
+ /// Unlike [`Self::id`] which is globally unique for an RP, this is unique up to "user" (i.e.,
+ /// multiple [`CredentialId`]s will often exist for the same `UserHandle`).
+ #[inline]
+ #[must_use]
+ pub const fn user_id(&self) -> UserHandle<&'user [u8]> {
+ self.user_id
+ }
+ /// Immutable state returned during registration.
+ #[inline]
+ #[must_use]
+ pub const fn static_state(&self) -> StaticState<UncompressedPubKey<'reg>> {
+ self.static_state
+ }
+ /// State that can change during authentication ceremonies.
+ #[inline]
+ #[must_use]
+ pub const fn dynamic_state(&self) -> DynamicState {
+ self.dynamic_state
+ }
+ /// Metadata.
+ #[inline]
+ #[must_use]
+ pub const fn metadata(&self) -> Metadata<'reg> {
+ self.metadata
+ }
+ /// Constructs a `RegisteredCredential` based on the passed arguments.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff the passed arguments are invalid. Read [`CredentialErr`]
+ /// for more information.
+ #[inline]
+ fn new<'a: 'reg, 'b: 'user>(
+ id: CredentialId<&'a [u8]>,
+ transports: AuthTransports,
+ user_id: UserHandle<&'b [u8]>,
+ static_state: StaticState<UncompressedPubKey<'a>>,
+ dynamic_state: DynamicState,
+ metadata: Metadata<'a>,
+ ) -> Result<Self, CredentialErr> {
+ verify_static_and_dynamic_state(&static_state, dynamic_state).and_then(|()| {
+ // `verify_static_and_dynamic_state` already ensures that
+ // `hmac-secret` is not `Some(true)` when `!dynamic_state.user_verified`;
+ // thus we only need to check that one is not enabled without the other.
+ if static_state.extensions.hmac_secret.unwrap_or_default() {
+ if metadata
+ .client_extension_results
+ .prf
+ .is_some_and(|prf| prf.enabled)
+ {
+ Ok(())
+ } else {
+ Err(CredentialErr::HmacSecretWithoutPrf)
+ }
+ } else if metadata
+ .client_extension_results
+ .prf
+ .is_some_and(|prf| prf.enabled)
+ {
+ Err(CredentialErr::PrfWithoutHmacSecret)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if !matches!(metadata.resident_key, ResidentKeyRequirement::Required)
+ || metadata
+ .client_extension_results
+ .cred_props
+ .as_ref()
+ .is_none_or(|props| props.rk.is_none_or(convert::identity))
+ {
+ Ok(Self {
+ id,
+ transports,
+ user_id,
+ static_state,
+ dynamic_state,
+ metadata,
+ })
+ } else {
+ Err(CredentialErr::ResidentKeyRequiredServerCredentialCreated)
+ }
+ })
+ })
+ }
+ /// Returns the contained data consuming `self`.
+ #[expect(
+ clippy::type_complexity,
+ reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
+ )]
+ #[inline]
+ #[must_use]
+ pub const fn into_parts(
+ self,
+ ) -> (
+ CredentialId<&'reg [u8]>,
+ AuthTransports,
+ UserHandle<&'user [u8]>,
+ StaticState<UncompressedPubKey<'reg>>,
+ DynamicState,
+ Metadata<'reg>,
+ ) {
+ (
+ self.id,
+ self.transports,
+ self.user_id,
+ self.static_state,
+ self.dynamic_state,
+ self.metadata,
+ )
+ }
+ /// Returns the contained data.
+ #[expect(
+ clippy::type_complexity,
+ reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
+ )]
+ #[inline]
+ #[must_use]
+ pub const fn as_parts(
+ &self,
+ ) -> (
+ CredentialId<&'reg [u8]>,
+ AuthTransports,
+ UserHandle<&'user [u8]>,
+ StaticState<UncompressedPubKey<'reg>>,
+ DynamicState,
+ Metadata<'reg>,
+ ) {
+ (
+ self.id,
+ self.transports,
+ self.user_id,
+ self.static_state,
+ self.dynamic_state,
+ self.metadata,
+ )
+ }
+}
+/// Credential used in authentication ceremonies.
+///
+/// Similar to [`RegisteredCredential`] except designed to only contain the necessary data to complete
+/// authentication ceremonies. In particular there is no [`AuthTransports`] or [`Metadata`],
+/// [`StaticState::credential_public_key`] is [`CompressedPubKey`] that can own or borrow its data, [`Self::id`] is
+/// based on the [`CredentialId`] passed to [`Self::new`] which itself must be from [`Authentication::raw_id`], and
+/// [`Self::user_id`] is based on the [`UserHandle`] passed to [`Self::new`] which itself must be the value in
+/// persistent storage associated with the `CredentialId`. When [`AuthenticatorAssertion::user_handle`] is `Some`,
+/// this can be used for `Self::user_id` so long as it matches the value in persistent storage. Note it MUST be
+/// `Some` when using discoverable requests (i.e., [`PublicKeyCredentialRequestOptions::allow_credentials`] is
+/// empty); and when using non-discoverable requests (i.e., `PublicKeyCredentialRequestOptions::allow_credentials`
+/// is non-empty), one should already have the user handle (e.g., in a session cookie) which can also be used.
+///
+/// Note `PublicKey` should be `CompressedPubKey` for this to be useful.
+///
+/// The only way to create this is via `Self::new`.
+#[derive(Debug)]
+pub struct AuthenticatedCredential<'cred, 'user, PublicKey> {
+ /// The credential ID.
+ ///
+ /// For client-side credentials, this is a unique identifier; but for server-side
+ /// credentials, this _is_ the credential (i.e., the encrypted private key and necessary information).
+ id: CredentialId<&'cred [u8]>,
+ /// The identifier for the user.
+ ///
+ /// Unlike [`Self::id`] which is globally unique for an RP, this is unique up to "user" (i.e.,
+ /// multiple [`CredentialId`]s will often exist for the same `UserHandle`).
+ user_id: UserHandle<&'user [u8]>,
+ /// Immutable state returned during registration.
+ static_state: StaticState<PublicKey>,
+ /// State that can change during authentication ceremonies.
+ dynamic_state: DynamicState,
+}
+impl<'cred, 'user, PublicKey> AuthenticatedCredential<'cred, 'user, PublicKey> {
+ /// The credential ID.
+ ///
+ /// For client-side credentials, this is a unique identifier; but for server-side
+ /// credentials, this _is_ the credential (i.e., the encrypted private key and necessary information).
+ #[inline]
+ #[must_use]
+ pub const fn id(&self) -> CredentialId<&'cred [u8]> {
+ self.id
+ }
+ /// The identifier for the user.
+ ///
+ /// Unlike [`Self::id`] which is globally unique for an RP, this is unique up to "user" (i.e.,
+ /// multiple [`CredentialId`]s will often exist for the same `UserHandle`).
+ #[inline]
+ #[must_use]
+ pub const fn user_id(&self) -> UserHandle<&'user [u8]> {
+ self.user_id
+ }
+ /// Immutable state returned during registration.
+ #[inline]
+ #[must_use]
+ pub const fn static_state(&self) -> &StaticState<PublicKey> {
+ &self.static_state
+ }
+ /// State that can change during authentication ceremonies.
+ #[inline]
+ #[must_use]
+ pub const fn dynamic_state(&self) -> DynamicState {
+ self.dynamic_state
+ }
+ /// Constructs an `AuthenticatedCredential` based on the passed arguments.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff the passed arguments are invalid. Read [`CredentialErr`]
+ /// for more information.
+ #[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))]
+ #[cfg(any(feature = "bin", feature = "custom"))]
+ #[inline]
+ pub fn new<'a: 'cred, 'b: 'user>(
+ id: CredentialId<&'a [u8]>,
+ user_id: UserHandle<&'b [u8]>,
+ static_state: StaticState<PublicKey>,
+ dynamic_state: DynamicState,
+ ) -> Result<Self, CredentialErr> {
+ verify_static_and_dynamic_state(&static_state, dynamic_state).map(|()| Self {
+ id,
+ user_id,
+ static_state,
+ dynamic_state,
+ })
+ }
+ /// Returns the contained data consuming `self`.
+ #[expect(
+ clippy::type_complexity,
+ reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
+ )]
+ #[inline]
+ #[must_use]
+ pub fn into_parts(
+ self,
+ ) -> (
+ CredentialId<&'cred [u8]>,
+ UserHandle<&'user [u8]>,
+ StaticState<PublicKey>,
+ DynamicState,
+ ) {
+ (self.id, self.user_id, self.static_state, self.dynamic_state)
+ }
+ /// Returns the contained data.
+ #[expect(
+ clippy::type_complexity,
+ reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
+ )]
+ #[inline]
+ #[must_use]
+ pub const fn as_parts(
+ &self,
+ ) -> (
+ CredentialId<&'cred [u8]>,
+ UserHandle<&'user [u8]>,
+ &StaticState<PublicKey>,
+ DynamicState,
+ ) {
+ (
+ self.id,
+ self.user_id,
+ self.static_state(),
+ self.dynamic_state,
+ )
+ }
+}
+/// Convenience aggregate error that rolls up all errors into one.
+#[derive(Debug)]
+pub enum AggErr {
+ /// Variant when [`AsciiDomain::try_from`] errors.
+ AsciiDomain(AsciiDomainErr),
+ /// Variant when [`Url::from_str`] errors.
+ Url(UrlErr),
+ /// Variant when [`Scheme::try_from`] errors.
+ Scheme(SchemeParseErr),
+ /// Variant when [`DomainOrigin::try_from`] errors.
+ DomainOrigin(DomainOriginParseErr),
+ /// Variant when [`Port::from_str`] errors.
+ Port(PortParseErr),
+ /// Variant when [`PublicKeyCredentialRequestOptions::start_ceremony`] errors.
+ RequestOptions(RequestOptionsErr),
+ /// Variant when [`PublicKeyCredentialRequestOptions::second_factor`] errors.
+ SecondFactor(SecondFactorErr),
+ /// Variant when [`PublicKeyCredentialCreationOptions::start_ceremony`] errors.
+ CreationOptions(CreationOptionsErr),
+ /// Variant when [`Nickname::try_from`] errors.
+ Nickname(NicknameErr),
+ /// Variant when [`UserHandle::rand`] or [`UserHandle::decode`] error.
+ UserHandle(UserHandleErr),
+ /// Variant when [`Username::try_from`] errors.
+ Username(UsernameErr),
+ /// Variant when [`RegistrationServerState::verify`] errors.
+ RegCeremony(RegCeremonyErr),
+ /// Variant when [`AuthenticationServerState::verify`] errors.
+ AuthCeremony(AuthCeremonyErr),
+ /// Variant when [`AttestationObject::try_from`] errors.
+ AttestationObject(AttestationObjectErr),
+ /// Variant when [`register::AuthenticatorData::try_from`] errors.
+ RegAuthenticatorData(RegAuthDataErr),
+ /// Variant when [`auth::AuthenticatorData::try_from`] errors.
+ AuthAuthenticatorData(AuthAuthDataErr),
+ /// Variant when [`CollectedClientData::from_client_data_json`] errors.
+ CollectedClientData(CollectedClientDataErr),
+ /// Variant when [`CollectedClientData::from_client_data_json_relaxed`] errors or any of the [`Deserialize`]
+ /// implementations error when relying on [`Deserializer`] or [`StreamDeserializer`].
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ SerdeJson(SerdeJsonErr),
+ /// Variant when [`Aaguid::try_from`] errors.
+ Aaguid(AaguidErr),
+ /// Variant when [`AuthTransports::decode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+ #[cfg(feature = "bin")]
+ DecodeAuthTransports(DecodeAuthTransportsErr),
+ /// Variant when [`StaticState::decode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+ #[cfg(feature = "bin")]
+ DecodeStaticState(DecodeStaticStateErr),
+ /// Variant when [`DynamicState::decode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+ #[cfg(feature = "bin")]
+ DecodeDynamicState(DecodeDynamicStateErr),
+ /// Variant when [`Nickname::decode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+ #[cfg(feature = "bin")]
+ DecodeNickname(DecodeNicknameErr),
+ /// Variant when [`Username::decode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+ #[cfg(feature = "bin")]
+ DecodeUsername(DecodeUsernameErr),
+ /// Variant when [`RegistrationServerState::decode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+ #[cfg(feature = "serializable_server_state")]
+ DecodeRegistrationServerState(DecodeRegistrationServerStateErr),
+ /// Variant when [`AuthenticationServerState::decode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+ #[cfg(feature = "serializable_server_state")]
+ DecodeAuthenticationServerState(DecodeAuthenticationServerStateErr),
+ /// Variant when [`RegistrationServerState::encode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+ #[cfg(feature = "serializable_server_state")]
+ EncodeRegistrationServerState(SystemTimeError),
+ /// Variant when [`AuthenticationServerState::encode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+ #[cfg(feature = "serializable_server_state")]
+ EncodeAuthenticationServerState(EncodeAuthenticationServerStateErr),
+ /// Variant when [`AuthenticatedCredential::new`] errors.
+ #[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))]
+ #[cfg(any(feature = "bin", feature = "custom"))]
+ Credential(CredentialErr),
+ /// Variant when [`CredentialId::try_from`] or [`CredentialId::decode`] errors.
+ #[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))]
+ #[cfg(any(feature = "bin", feature = "custom"))]
+ CredentialId(CredentialIdErr),
+}
+impl From<AsciiDomainErr> for AggErr {
+ #[inline]
+ fn from(value: AsciiDomainErr) -> Self {
+ Self::AsciiDomain(value)
+ }
+}
+impl From<UrlErr> for AggErr {
+ #[inline]
+ fn from(value: UrlErr) -> Self {
+ Self::Url(value)
+ }
+}
+impl From<SchemeParseErr> for AggErr {
+ #[inline]
+ fn from(value: SchemeParseErr) -> Self {
+ Self::Scheme(value)
+ }
+}
+impl From<DomainOriginParseErr> for AggErr {
+ #[inline]
+ fn from(value: DomainOriginParseErr) -> Self {
+ Self::DomainOrigin(value)
+ }
+}
+impl From<PortParseErr> for AggErr {
+ #[inline]
+ fn from(value: PortParseErr) -> Self {
+ Self::Port(value)
+ }
+}
+impl From<RequestOptionsErr> for AggErr {
+ #[inline]
+ fn from(value: RequestOptionsErr) -> Self {
+ Self::RequestOptions(value)
+ }
+}
+impl From<SecondFactorErr> for AggErr {
+ #[inline]
+ fn from(value: SecondFactorErr) -> Self {
+ Self::SecondFactor(value)
+ }
+}
+impl From<CreationOptionsErr> for AggErr {
+ #[inline]
+ fn from(value: CreationOptionsErr) -> Self {
+ Self::CreationOptions(value)
+ }
+}
+impl From<NicknameErr> for AggErr {
+ #[inline]
+ fn from(value: NicknameErr) -> Self {
+ Self::Nickname(value)
+ }
+}
+impl From<UserHandleErr> for AggErr {
+ #[inline]
+ fn from(value: UserHandleErr) -> Self {
+ Self::UserHandle(value)
+ }
+}
+impl From<UsernameErr> for AggErr {
+ #[inline]
+ fn from(value: UsernameErr) -> Self {
+ Self::Username(value)
+ }
+}
+impl From<RegCeremonyErr> for AggErr {
+ #[inline]
+ fn from(value: RegCeremonyErr) -> Self {
+ Self::RegCeremony(value)
+ }
+}
+impl From<AuthCeremonyErr> for AggErr {
+ #[inline]
+ fn from(value: AuthCeremonyErr) -> Self {
+ Self::AuthCeremony(value)
+ }
+}
+impl From<AttestationObjectErr> for AggErr {
+ #[inline]
+ fn from(value: AttestationObjectErr) -> Self {
+ Self::AttestationObject(value)
+ }
+}
+impl From<RegAuthDataErr> for AggErr {
+ #[inline]
+ fn from(value: RegAuthDataErr) -> Self {
+ Self::RegAuthenticatorData(value)
+ }
+}
+impl From<AuthAuthDataErr> for AggErr {
+ #[inline]
+ fn from(value: AuthAuthDataErr) -> Self {
+ Self::AuthAuthenticatorData(value)
+ }
+}
+impl From<CollectedClientDataErr> for AggErr {
+ #[inline]
+ fn from(value: CollectedClientDataErr) -> Self {
+ Self::CollectedClientData(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+#[cfg(feature = "serde_relaxed")]
+impl From<SerdeJsonErr> for AggErr {
+ #[inline]
+ fn from(value: SerdeJsonErr) -> Self {
+ Self::SerdeJson(value)
+ }
+}
+impl From<AaguidErr> for AggErr {
+ #[inline]
+ fn from(value: AaguidErr) -> Self {
+ Self::Aaguid(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+#[cfg(feature = "bin")]
+impl From<DecodeAuthTransportsErr> for AggErr {
+ #[inline]
+ fn from(value: DecodeAuthTransportsErr) -> Self {
+ Self::DecodeAuthTransports(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+#[cfg(feature = "bin")]
+impl From<DecodeStaticStateErr> for AggErr {
+ #[inline]
+ fn from(value: DecodeStaticStateErr) -> Self {
+ Self::DecodeStaticState(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+#[cfg(feature = "bin")]
+impl From<DecodeDynamicStateErr> for AggErr {
+ #[inline]
+ fn from(value: DecodeDynamicStateErr) -> Self {
+ Self::DecodeDynamicState(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+#[cfg(feature = "bin")]
+impl From<DecodeNicknameErr> for AggErr {
+ #[inline]
+ fn from(value: DecodeNicknameErr) -> Self {
+ Self::DecodeNickname(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+#[cfg(feature = "bin")]
+impl From<DecodeUsernameErr> for AggErr {
+ #[inline]
+ fn from(value: DecodeUsernameErr) -> Self {
+ Self::DecodeUsername(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+#[cfg(feature = "serializable_server_state")]
+impl From<DecodeRegistrationServerStateErr> for AggErr {
+ #[inline]
+ fn from(value: DecodeRegistrationServerStateErr) -> Self {
+ Self::DecodeRegistrationServerState(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+#[cfg(feature = "serializable_server_state")]
+impl From<DecodeAuthenticationServerStateErr> for AggErr {
+ #[inline]
+ fn from(value: DecodeAuthenticationServerStateErr) -> Self {
+ Self::DecodeAuthenticationServerState(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+#[cfg(feature = "serializable_server_state")]
+impl From<SystemTimeError> for AggErr {
+ #[inline]
+ fn from(value: SystemTimeError) -> Self {
+ Self::EncodeRegistrationServerState(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+#[cfg(feature = "serializable_server_state")]
+impl From<EncodeAuthenticationServerStateErr> for AggErr {
+ #[inline]
+ fn from(value: EncodeAuthenticationServerStateErr) -> Self {
+ Self::EncodeAuthenticationServerState(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))]
+#[cfg(any(feature = "bin", feature = "custom"))]
+impl From<CredentialErr> for AggErr {
+ #[inline]
+ fn from(value: CredentialErr) -> Self {
+ Self::Credential(value)
+ }
+}
+#[cfg_attr(docsrs, doc(cfg(any(feature = "bin", feature = "custom"))))]
+#[cfg(any(feature = "bin", feature = "custom"))]
+impl From<CredentialIdErr> for AggErr {
+ #[inline]
+ fn from(value: CredentialIdErr) -> Self {
+ Self::CredentialId(value)
+ }
+}
+impl Display for AggErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::AsciiDomain(err) => err.fmt(f),
+ Self::Url(err) => err.fmt(f),
+ Self::Scheme(err) => err.fmt(f),
+ Self::DomainOrigin(ref err) => err.fmt(f),
+ Self::Port(ref err) => err.fmt(f),
+ Self::RequestOptions(err) => err.fmt(f),
+ Self::SecondFactor(err) => err.fmt(f),
+ Self::CreationOptions(err) => err.fmt(f),
+ Self::Nickname(err) => err.fmt(f),
+ Self::UserHandle(err) => err.fmt(f),
+ Self::Username(err) => err.fmt(f),
+ Self::RegCeremony(ref err) => err.fmt(f),
+ Self::AuthCeremony(ref err) => err.fmt(f),
+ Self::AttestationObject(err) => err.fmt(f),
+ Self::RegAuthenticatorData(err) => err.fmt(f),
+ Self::AuthAuthenticatorData(err) => err.fmt(f),
+ Self::CollectedClientData(ref err) => err.fmt(f),
+ #[cfg(feature = "serde_relaxed")]
+ Self::SerdeJson(ref err) => err.fmt(f),
+ Self::Aaguid(err) => err.fmt(f),
+ #[cfg(feature = "bin")]
+ Self::DecodeAuthTransports(err) => err.fmt(f),
+ #[cfg(feature = "bin")]
+ Self::DecodeStaticState(err) => err.fmt(f),
+ #[cfg(feature = "bin")]
+ Self::DecodeDynamicState(err) => err.fmt(f),
+ #[cfg(feature = "bin")]
+ Self::DecodeNickname(err) => err.fmt(f),
+ #[cfg(feature = "bin")]
+ Self::DecodeUsername(err) => err.fmt(f),
+ #[cfg(feature = "serializable_server_state")]
+ Self::DecodeRegistrationServerState(err) => err.fmt(f),
+ #[cfg(feature = "serializable_server_state")]
+ Self::DecodeAuthenticationServerState(err) => err.fmt(f),
+ #[cfg(feature = "serializable_server_state")]
+ Self::EncodeRegistrationServerState(ref err) => err.fmt(f),
+ #[cfg(feature = "serializable_server_state")]
+ Self::EncodeAuthenticationServerState(ref err) => err.fmt(f),
+ #[cfg(any(feature = "bin", feature = "custom"))]
+ Self::Credential(err) => err.fmt(f),
+ #[cfg(any(feature = "bin", feature = "custom"))]
+ Self::CredentialId(err) => err.fmt(f),
+ }
+ }
+}
+impl Error for AggErr {}
+/// Calculates the number of bytes needed to encode an input of length `n` bytes into base64url.
+///
+/// # Panics
+///
+/// `panic`s iff `n >= 0x4000`.
+const fn base64url_nopad_len(n: usize) -> usize {
+ // `usize::MAX >= u16::MAX`; thus we conservatively cap `n` to the largest value that would not overflow
+ // from multiplying by 4 on all platforms.
+ assert!(
+ n < 0x4000,
+ "webauthn_rp::base64url_nopad_len must be passed an integer smaller than 0x4000"
+ );
+ // 256^n is the number of distinct values of the input. Let the base64 encoding in a URL safe
+ // way without padding of the input be O. There are 64 possible values each byte in O can be; thus we must find
+ // the minimum nonnegative integer m such that:
+ // 64^m = (2^6)^m = 2^(6m) >= 256^n = (2^8)^n = 2^(8n)
+ // <==>
+ // lg(2^(6m)) = 6m >= lg(2^(8n)) = 8n lg is defined on all positive reals which 2^(6m) and 2^(8n) are
+ // <==>
+ // m >= 8n/6 = 4n/3
+ // Clearly that corresponds to m = ⌈4n/3⌉; thus:
+ (n << 2).div_ceil(3)
+}
+/// Calculates the number of bytes a base64url-encoded input of length `n` bytes will be decoded into.
+///
+/// `Some` is returned iff the value could be calculated without overflow; otherwise `None` is returned.
+///
+/// Note this does not verify if `n` is a possible value.
+#[expect(
+ clippy::unseparated_literal_suffix,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+#[cfg(feature = "serde")]
+fn base64url_nopad_decode_len(n: usize) -> Option<usize> {
+ // 64^n is the number of distinct values of the input. Let the decoded output be O.
+ // There are 256 possible values each byte in O can be; thus we must find
+ // the maximum nonnegative integer m such that:
+ // 256^m = (2^8)^m = 2^(8m) <= 64^n = (2^6)^n = 2^(6n)
+ // <==>
+ // lg(2^(8m)) = 8m <= lg(2^(6n)) = 6n lg is defined on all positive reals which 2^(8m) and 2^(6n) are
+ // <==>
+ // m <= 6n/8 = 3n/4
+ // Clearly that corresponds to m = ⌊3n/4⌋; thus:
+ n.checked_mul(3).map(|len| len >> 2u8)
+}
+// We cache a `static` instance for performance reasons.
+/// Encoder/decoder for base64url.
+const BASE64URL_NOPAD_ENC: &Encoding = &BASE64URL_NOPAD;
diff --git a/src/request.rs b/src/request.rs
@@ -0,0 +1,3855 @@
+#[cfg(doc)]
+use super::{
+ request::{
+ auth::{
+ AllowedCredential, AllowedCredentials, CredentialSpecificExtension,
+ PublicKeyCredentialRequestOptions,
+ },
+ register::PublicKeyCredentialCreationOptions,
+ },
+ response::register::ClientExtensionsOutputs,
+};
+use crate::{
+ request::{
+ auth::AuthenticationServerState,
+ error::{AsciiDomainErr, DomainOriginParseErr, PortParseErr, SchemeParseErr, UrlErr},
+ register::{RegistrationServerState, RegistrationVerificationOptions},
+ },
+ response::{
+ AuthData as _, AuthDataContainer, AuthResponse, AuthTransports, Backup, CeremonyErr,
+ CredentialId, Origin, Response, SentChallenge,
+ },
+};
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+use core::hash::{BuildHasher, Hash, Hasher};
+use core::{
+ borrow::Borrow,
+ fmt::{self, Display, Formatter},
+ num::NonZeroU32,
+ str::FromStr,
+};
+use rand::Rng as _;
+use rsa::sha2::{Digest as _, Sha256};
+#[cfg(feature = "serializable_server_state")]
+use std::time::SystemTime;
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+use std::{collections::HashSet, time::Instant};
+use url::Url as Uri;
+/// Contains functionality for beginning the
+/// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony).
+///
+/// # Examples
+///
+/// ```
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
+/// # use webauthn_rp::{
+/// # request::{
+/// # auth::{AllowedCredentials, PublicKeyCredentialRequestOptions},
+/// # register::UserHandle,
+/// # AsciiDomain, Credentials, PublicKeyCredentialDescriptor, RpId,
+/// # },
+/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
+/// # AggErr,
+/// # };
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// let mut ceremonies = FixedCapHashSet::new(128);
+/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+/// let (server, client) = PublicKeyCredentialRequestOptions::passkey(&rp_id).start_ceremony()?;
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// assert!(matches!(
+/// ceremonies.insert_or_replace_all_expired(server),
+/// InsertResult::Success
+/// ));
+/// # #[cfg(feature = "serde")]
+/// assert!(serde_json::to_string(&client).is_ok());
+/// let user_handle = get_user_handle();
+/// # #[cfg(feature = "custom")]
+/// let creds = get_registered_credentials((&user_handle).into())?;
+/// # #[cfg(feature = "custom")]
+/// let (server_2, client_2) =
+/// PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?.start_ceremony()?;
+/// # #[cfg(all(feature = "custom", not(feature = "serializable_server_state")))]
+/// assert!(matches!(
+/// ceremonies.insert_or_replace_all_expired(server_2),
+/// InsertResult::Success
+/// ));
+/// # #[cfg(all(feature = "custom", feature = "serde"))]
+/// assert!(serde_json::to_string(&client_2).is_ok());
+/// /// Extract `UserHandle` from session cookie.
+/// fn get_user_handle() -> UserHandle<Vec<u8>> {
+/// // ⋮
+/// # UserHandle::new()
+/// }
+/// # #[cfg(feature = "custom")]
+/// /// Fetch the `AllowedCredentials` associated with `user`.
+/// fn get_registered_credentials(user: UserHandle<&[u8]>) -> Result<AllowedCredentials, AggErr> {
+/// // ⋮
+/// # let mut creds = AllowedCredentials::new();
+/// # creds.push(
+/// # PublicKeyCredentialDescriptor {
+/// # id: CredentialId::try_from(vec![0; CRED_ID_MIN_LEN])?,
+/// # transports: AuthTransports::NONE,
+/// # }
+/// # .into(),
+/// # );
+/// # Ok(creds)
+/// }
+/// # Ok::<_, AggErr>(())
+/// ```
+pub mod auth;
+/// Contains error types.
+pub mod error;
+/// Contains functionality for beginning the
+/// [registration ceremony](https://www.w3.org/TR/webauthn-3/#registration-ceremony).
+///
+/// # Examples
+///
+/// ```
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// # use webauthn_rp::request::{FixedCapHashSet, InsertResult};
+/// # use webauthn_rp::{
+/// # request::{
+/// # register::{
+/// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle,
+/// # },
+/// # AsciiDomain, PublicKeyCredentialDescriptor, RpId
+/// # },
+/// # response::{AuthTransports, CredentialId, CRED_ID_MIN_LEN},
+/// # AggErr,
+/// # };
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// let mut ceremonies = FixedCapHashSet::new(128);
+/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+/// let user_handle = get_user_handle();
+/// let handle = (&user_handle).into();
+/// let user = get_user_entity(handle)?;
+/// let creds = get_registered_credentials(handle)?;
+/// let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, user.clone(), creds)
+/// .start_ceremony()?;
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// assert!(matches!(
+/// ceremonies.insert_or_replace_all_expired(server),
+/// InsertResult::Success
+/// ));
+/// # #[cfg(feature = "serde")]
+/// assert!(serde_json::to_string(&client).is_ok());
+/// let creds_2 = get_registered_credentials(handle)?;
+/// let (server_2, client_2) =
+/// PublicKeyCredentialCreationOptions::second_factor(&rp_id, user, creds_2).start_ceremony()?;
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// assert!(matches!(
+/// ceremonies.insert_or_replace_all_expired(server_2),
+/// InsertResult::Success
+/// ));
+/// # #[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>> {
+/// // ⋮
+/// # UserHandle::new()
+/// }
+/// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`.
+/// ///
+/// /// If this is the first time a credential is being registered, then `PublicKeyCredentialUserEntity`
+/// /// will need to be constructed with `name` and `display_name` passed from the client and `UserHandle::new`
+/// /// used for `id`. Once created, this info can be stored such that the entity information
+/// /// does not need to be requested for subsequent registrations.
+/// fn get_user_entity(user: UserHandle<&[u8]>) -> Result<PublicKeyCredentialUserEntity<&[u8]>, AggErr> {
+/// // ⋮
+/// # Ok(PublicKeyCredentialUserEntity {
+/// # name: "foo".try_into()?,
+/// # id: user,
+/// # display_name: None,
+/// # })
+/// }
+/// /// Fetch the `PublicKeyCredentialDescriptor`s associated with `user`.
+/// ///
+/// /// This doesn't need to be called when this is the first credential registered for `user`; instead
+/// /// an empty `Vec` should be passed.
+/// fn get_registered_credentials(
+/// user: UserHandle<&[u8]>,
+/// ) -> Result<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, AggErr> {
+/// // ⋮
+/// # Ok(Vec::new())
+/// }
+/// # Ok::<_, AggErr>(())
+/// ```
+pub mod register;
+/// Contains functionality to serialize data to a client.
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+#[cfg(feature = "serde")]
+mod ser;
+/// Contains functionality to (de)serialize data needed for [`RegistrationServerState`] and
+/// [`AuthenticationServerState`] to a data store.
+#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+#[cfg(feature = "serializable_server_state")]
+pub(super) mod ser_server_state;
+// `Challenge` must _never_ be constructable directly or indirectly; thus its tuple field must always be private,
+// and it must never implement `trait`s (e.g., `Clone`) that would allow indirect creation. It must only ever
+// be constructed via `Self::new` or `Self::default`. In contrast downstream code must be able to construct
+// `SentChallenge` since it is used during ceremony validation; thus we must keep `Challenge` and `SentChallenge`
+// as separate types.
+/// [Cryptographic challenge](https://www.w3.org/TR/webauthn-3/#sctn-cryptographic-challenges).
+#[derive(Debug)]
+pub struct Challenge(u128);
+impl Challenge {
+ // This won't `panic` since `16 < 0x4000`.
+ /// The number of bytes a `Challenge` takes to encode in base64url.
+ pub(super) const BASE64_LEN: usize = super::base64url_nopad_len(16);
+ /// Returns a new `Challenge` based on a randomly-generated `u128`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::Challenge;
+ /// // The probability of a `Challenge` being 0 (assuming a good entropy
+ /// // source) is 2^-128 ≈ 2.9 x 10^-39.
+ /// assert_ne!(Challenge::new().into_data(), 0);
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ Self(rand::thread_rng().r#gen())
+ }
+ /// Returns the contained `u128` consuming `self`.
+ #[inline]
+ #[must_use]
+ pub const fn into_data(self) -> u128 {
+ self.0
+ }
+ /// Returns the contained `u128`.
+ #[inline]
+ #[must_use]
+ pub const fn as_data(&self) -> u128 {
+ self.0
+ }
+}
+impl Default for Challenge {
+ /// Same as [`Self::new`].
+ #[inline]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+impl From<Challenge> for u128 {
+ #[inline]
+ fn from(value: Challenge) -> Self {
+ value.0
+ }
+}
+impl From<&Challenge> for u128 {
+ #[inline]
+ fn from(value: &Challenge) -> Self {
+ value.0
+ }
+}
+/// A [domain](https://url.spec.whatwg.org/#concept-domain) in representation format consisting of only and any
+/// ASCII.
+///
+/// The only ASCII character disallowed in a label is `'.'` since it is used exclusively as a separator. Every
+/// label must have length inclusively between 1 and 63, and the total length of the domain must be at most 253
+/// when a trailing `'.'` does not exist; otherwise the max length is 254. The root domain (i.e., `'.'`) is not
+/// allowed.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AsciiDomain(String);
+impl AsRef<str> for AsciiDomain {
+ #[inline]
+ fn as_ref(&self) -> &str {
+ self.0.as_str()
+ }
+}
+impl Borrow<str> for AsciiDomain {
+ #[inline]
+ fn borrow(&self) -> &str {
+ self.0.as_str()
+ }
+}
+impl From<AsciiDomain> for String {
+ #[inline]
+ fn from(value: AsciiDomain) -> Self {
+ value.0
+ }
+}
+impl PartialEq<&Self> for AsciiDomain {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<AsciiDomain> for &AsciiDomain {
+ #[inline]
+ fn eq(&self, other: &AsciiDomain) -> bool {
+ **self == *other
+ }
+}
+impl TryFrom<String> for AsciiDomain {
+ type Error = AsciiDomainErr;
+ /// Verifies `value` is an ASCII domain in representation format converting any uppercase ASCII into
+ /// lowercase.
+ ///
+ /// Note it is _strongly_ encouraged for `value` to only contain letters, numbers, hyphens, and underscores;
+ /// otherwise certain applications may consider it not a domain. If the original domain contains non-ASCII, then
+ /// one must encode it in Punycode _before_ calling this function. Domains that have a trailing `'.'` will be
+ /// considered differently than domains without it; thus one will likely want to trim it if it does exist.
+ /// Because this allows any ASCII, one may want to ensure `value` is not an IPv4 address.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{error::AsciiDomainErr, AsciiDomain};
+ /// // Root `'.'` is not removed if it exists.
+ /// assert_ne!("example.com", AsciiDomain::try_from("example.com.".to_owned())?.as_ref());
+ /// // Root domain (i.e., `'.'`) is not allowed.
+ /// assert!(AsciiDomain::try_from(".".to_owned()).is_err());
+ /// // Uppercase is transformed into lowercase.
+ /// assert_eq!("example.com", AsciiDomain::try_from("ExAmPle.CoM".to_owned())?.as_ref());
+ /// // The only ASCII character not allowed in a domain label is `'.'` as it is used exclusively to delimit
+ /// // labels.
+ /// assert_eq!("\x00", AsciiDomain::try_from("\x00".to_owned())?.as_ref());
+ /// // Empty labels are not allowed.
+ /// assert!(AsciiDomain::try_from("example..com".to_owned()).is_err());
+ /// // Labels cannot have length greater than 63.
+ /// let mut long_label = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned();
+ /// assert_eq!(long_label.len(), 64);
+ /// assert!(AsciiDomain::try_from(long_label.clone()).is_err());
+ /// long_label.pop();
+ /// assert_eq!(long_label, AsciiDomain::try_from(long_label.clone())?.as_ref());
+ /// // The maximum length of a domain is 254 if a trailing `'.'` exists; otherwise the max length is 253.
+ /// let mut long_domain = format!("{long_label}.{long_label}.{long_label}.{long_label}");
+ /// long_domain.pop();
+ /// long_domain.push('.');
+ /// assert_eq!(long_domain.len(), 255);
+ /// assert!(AsciiDomain::try_from(long_domain.clone()).is_err());
+ /// long_domain.pop();
+ /// long_domain.pop();
+ /// long_domain.push('.');
+ /// assert_eq!(long_domain.len(), 254);
+ /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone())?.as_ref());
+ /// long_domain.pop();
+ /// long_domain.push('a');
+ /// assert_eq!(long_domain.len(), 254);
+ /// assert!(AsciiDomain::try_from(long_domain.clone()).is_err());
+ /// long_domain.pop();
+ /// assert_eq!(long_domain.len(), 253);
+ /// assert_eq!(long_domain, AsciiDomain::try_from(long_domain.clone())?.as_ref());
+ /// // Only ASCII is allowed; thus if a domain needs to be Punycode-encoded, then it must be _before_ calling
+ /// // this function.
+ /// assert!(AsciiDomain::try_from("λ.com".to_owned()).is_err());
+ /// assert_eq!("xn--wxa.com", AsciiDomain::try_from("xn--wxa.com".to_owned())?.as_ref());
+ /// # Ok::<_, AsciiDomainErr>(())
+ /// ```
+ #[expect(
+ unsafe_code,
+ reason = "need to transform uppercase ASCII into lowercase"
+ )]
+ #[expect(
+ clippy::arithmetic_side_effects,
+ reason = "comment justifies its correctness"
+ )]
+ #[inline]
+ fn try_from(mut value: String) -> Result<Self, Self::Error> {
+ value
+ .as_bytes()
+ .last()
+ .ok_or(AsciiDomainErr::Empty)
+ .and_then(|b| {
+ let len = value.len();
+ if *b == b'.' {
+ if len == 1 {
+ Err(AsciiDomainErr::RootDomain)
+ } else if len > 254 {
+ Err(AsciiDomainErr::Len)
+ } else {
+ Ok(())
+ }
+ } else if len > 253 {
+ Err(AsciiDomainErr::Len)
+ } else {
+ Ok(())
+ }
+ })
+ .and_then(|()| {
+ // SAFETY:
+ // The only possible mutation we perform is converting uppercase ASCII into lowercase which
+ // is entirely safe since ASCII is a subset of UTF-8, and ASCII characters are always encoded
+ // as a single UTF-8 code unit.
+ let utf8 = unsafe { value.as_bytes_mut() };
+ utf8.iter_mut()
+ .try_fold(0u8, |label_len, byt| {
+ let b = *byt;
+ if b == b'.' {
+ if label_len == 0 {
+ Err(AsciiDomainErr::EmptyLabel)
+ } else {
+ Ok(0)
+ }
+ } else if label_len == 63 {
+ Err(AsciiDomainErr::LabelLen)
+ } else if b.is_ascii() {
+ *byt = b.to_ascii_lowercase();
+ // This won't overflow since `label_len < 63`.
+ Ok(label_len + 1)
+ } else {
+ Err(AsciiDomainErr::NotAscii)
+ }
+ })
+ .map(|_| Self(value))
+ })
+ }
+}
+/// The output of the [URL serializer](https://url.spec.whatwg.org/#concept-url-serializer).
+///
+/// The returned URL must consist of a [scheme](https://url.spec.whatwg.org/#concept-url-scheme) and
+/// optional [path](https://url.spec.whatwg.org/#url-path) but nothing else.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Url(String);
+impl AsRef<str> for Url {
+ #[inline]
+ fn as_ref(&self) -> &str {
+ self.0.as_str()
+ }
+}
+impl Borrow<str> for Url {
+ #[inline]
+ fn borrow(&self) -> &str {
+ self.0.as_str()
+ }
+}
+impl From<Url> for String {
+ #[inline]
+ fn from(value: Url) -> Self {
+ value.0
+ }
+}
+impl PartialEq<&Self> for Url {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<Url> for &Url {
+ #[inline]
+ fn eq(&self, other: &Url) -> bool {
+ **self == *other
+ }
+}
+impl FromStr for Url {
+ type Err = UrlErr;
+ #[inline]
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Uri::from_str(s).map_err(|_e| UrlErr).and_then(|url| {
+ if url.scheme().is_empty()
+ || url.has_host()
+ || url.query().is_some()
+ || url.fragment().is_some()
+ {
+ Err(UrlErr)
+ } else {
+ Ok(Self(url.into()))
+ }
+ })
+ }
+}
+/// [RP ID](https://w3c.github.io/webauthn/#rp-id).
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum RpId {
+ /// An ASCII domain.
+ ///
+ /// Note web platforms MUST use this variant; and if possible, non-web platforms should too. Also despite
+ /// the spec currently requiring RP IDs to be
+ /// [valid domain strings](https://url.spec.whatwg.org/#valid-domain-string), this is unnecessarily strict
+ /// and will likely be relaxed in a [future version](https://github.com/w3c/webauthn/issues/2206); thus
+ /// any ASCII domain is allowed.
+ Domain(AsciiDomain),
+ /// A URL with only scheme and path.
+ Url(Url),
+}
+impl RpId {
+ /// Validates `hash` is the same as the SHA-256 hash of `self`.
+ fn validate_rp_id_hash<E>(&self, hash: &[u8]) -> Result<(), CeremonyErr<E>> {
+ if hash == Sha256::digest(self.as_ref()).as_slice() {
+ Ok(())
+ } else {
+ Err(CeremonyErr::RpIdHashMismatch)
+ }
+ }
+}
+impl AsRef<str> for RpId {
+ #[inline]
+ fn as_ref(&self) -> &str {
+ match *self {
+ Self::Domain(ref dom) => dom.as_ref(),
+ Self::Url(ref url) => url.as_ref(),
+ }
+ }
+}
+impl Borrow<str> for RpId {
+ #[inline]
+ fn borrow(&self) -> &str {
+ match *self {
+ Self::Domain(ref dom) => dom.borrow(),
+ Self::Url(ref url) => url.borrow(),
+ }
+ }
+}
+impl From<RpId> for String {
+ #[inline]
+ fn from(value: RpId) -> Self {
+ match value {
+ RpId::Domain(dom) => dom.into(),
+ RpId::Url(url) => url.into(),
+ }
+ }
+}
+impl PartialEq<&Self> for RpId {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<RpId> for &RpId {
+ #[inline]
+ fn eq(&self, other: &RpId) -> bool {
+ **self == *other
+ }
+}
+/// A URI scheme. This can be used to make
+/// [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) more convenient.
+#[derive(Clone, Copy, Debug, Default)]
+pub enum Scheme<'a> {
+ /// A scheme must not exist when validating the origin.
+ None,
+ /// Any scheme, or no scheme at all, is allowed to exist when validating the origin.
+ Any,
+ /// The HTTPS scheme must exist when validating the origin.
+ #[default]
+ Https,
+ /// The SSH scheme must exist when validating the origin.
+ Ssh,
+ /// The contained `str` scheme must exist when validating the origin.
+ Other(&'a str),
+ /// [`Self::None`] or [`Self::Https`].
+ NoneHttps,
+ /// [`Self::None`] or [`Self::Ssh`].
+ NoneSsh,
+ /// [`Self::None`] or [`Self::Other`].
+ NoneOther(&'a str),
+}
+impl Scheme<'_> {
+ /// `self` is any `Scheme`; however `other` is assumed to only be a `Scheme` from a `DomainOrigin` returned
+ /// from `DomainOrigin::try_from`. The latter implies that `other` is only `Scheme::None`, `Scheme::Https`,
+ /// `Scheme::Ssh`, or `Scheme::Other`; furthermore when `Scheme::Other`, it won't contain a `str` that is
+ /// empty or equal to "https" or "ssh".
+ #[expect(clippy::unreachable, reason = "there is a bug, so we want to crash")]
+ fn is_equal_to_origin_scheme(self, other: Self) -> bool {
+ match self {
+ Self::None => matches!(other, Self::None),
+ Self::Any => true,
+ Self::Https => matches!(other, Self::Https),
+ Self::Ssh => matches!(other, Self::Ssh),
+ Self::Other(scheme) => match other {
+ Self::None => false,
+ // We want to crash and burn since there is a bug in code.
+ Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => {
+ unreachable!("there is a bug in DomainOrigin::try_from")
+ }
+ Self::Https => scheme == "https",
+ Self::Ssh => scheme == "ssh",
+ Self::Other(scheme_other) => scheme == scheme_other,
+ },
+ Self::NoneHttps => match other {
+ Self::None | Self::Https => true,
+ Self::Ssh | Self::Other(_) => false,
+ // We want to crash and burn since there is a bug in code.
+ Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => {
+ unreachable!("there is a bug in DomainOrigin::try_from")
+ }
+ },
+ Self::NoneSsh => match other {
+ Self::None | Self::Ssh => true,
+ // We want to crash and burn since there is a bug in code.
+ Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => {
+ unreachable!("there is a bug in DomainOrigin::try_from")
+ }
+ Self::Https | Self::Other(_) => false,
+ },
+ Self::NoneOther(scheme) => match other {
+ Self::None => true,
+ // We want to crash and burn since there is a bug in code.
+ Self::Any | Self::NoneHttps | Self::NoneSsh | Self::NoneOther(_) => {
+ unreachable!("there is a bug in DomainOrigin::try_from")
+ }
+ Self::Https => scheme == "https",
+ Self::Ssh => scheme == "ssh",
+ Self::Other(scheme_other) => scheme == scheme_other,
+ },
+ }
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a str> for Scheme<'b> {
+ type Error = SchemeParseErr;
+ /// `"https"` and `"ssh"` get mapped to [`Self::Https`] and [`Self::Ssh`] respectively. All other
+ /// values get mapped to [`Self::Other`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `s` is empty.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::Scheme;
+ /// assert!(matches!(Scheme::try_from("https")?, Scheme::Https));
+ /// assert!(matches!(Scheme::try_from("https ")?, Scheme::Other(scheme) if scheme == "https "));
+ /// assert!(matches!(Scheme::try_from("ssh")?, Scheme::Ssh));
+ /// assert!(matches!(Scheme::try_from("Ssh")?, Scheme::Other(scheme) if scheme == "Ssh"));
+ /// // Even though one can construct an empty `Scheme` via `Scheme::Other` or `Scheme::NoneOther`,
+ /// // one cannot parse one.
+ /// assert!(Scheme::try_from("").is_err());
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+ match value {
+ "" => Err(SchemeParseErr),
+ "https" => Ok(Self::Https),
+ "ssh" => Ok(Self::Ssh),
+ _ => Ok(Self::Other(value)),
+ }
+ }
+}
+/// A TCP/UDP port. This can be used to make
+/// [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) more convenient.
+#[derive(Clone, Copy, Debug, Default)]
+pub enum Port {
+ /// A port must not exist when validating the origin.
+ #[default]
+ None,
+ /// Any port, or no port at all, is allowed to exist when validating the origin.
+ Any,
+ /// The contained `u16` port must exist when validating the origin.
+ Val(u16),
+ /// [`Self::None`] or [`Self::Val`].
+ NoneVal(u16),
+}
+impl Port {
+ /// `self` is any `Port`; however `other` is assumed to only be a `Port` from a `DomainOrigin` returned
+ /// from `DomainOrigin::try_from`. The latter implies that `other` is only `Port::None` or `Port::Val`.
+ #[expect(clippy::unreachable, reason = "there is a bug, so we want to crash")]
+ fn is_equal_to_origin_port(self, other: Self) -> bool {
+ match self {
+ Self::None => matches!(other, Self::None),
+ Self::Any => true,
+ Self::Val(port) => match other {
+ Self::None => false,
+ // There is a bug in code so we want to crash and burn.
+ Self::Any | Self::NoneVal(_) => {
+ unreachable!("there is a bug in DomainOrigin::try_from")
+ }
+ Self::Val(port_other) => port == port_other,
+ },
+ Self::NoneVal(port) => match other {
+ Self::None => true,
+ // There is a bug in code so we want to crash and burn.
+ Self::Any | Self::NoneVal(_) => {
+ unreachable!("there is a bug in DomainOrigin::try_from")
+ }
+ Self::Val(port_other) => port == port_other,
+ },
+ }
+ }
+}
+impl FromStr for Port {
+ type Err = PortParseErr;
+ /// Parses `s` as a 16-bit unsigned integer without leading 0s returning [`Self::Val`] with the contained
+ /// `u16`.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `s` is not a valid 16-bit unsigned integer in decimal notation without leading 0s.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{error::PortParseErr, Port};
+ /// assert!(matches!("443".parse()?, Port::Val(443)));
+ /// // TCP/UDP ports have to be in canonical form:
+ /// assert!("022"
+ /// .parse::<Port>()
+ /// .map_or_else(|err| matches!(err, PortParseErr::NotCanonical), |_| false));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ s.parse().map_err(PortParseErr::ParseInt).and_then(|port| {
+ if s.len()
+ == match port {
+ ..=9 => 1,
+ 10..=99 => 2,
+ 100..=999 => 3,
+ 1_000..=9_999 => 4,
+ 10_000.. => 5,
+ }
+ {
+ Ok(Self::Val(port))
+ } else {
+ Err(PortParseErr::NotCanonical)
+ }
+ })
+ }
+}
+/// A [`tuple origin`](https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-tuple).
+///
+/// This can be used to make [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin)
+/// more convenient.
+#[derive(Clone, Copy, Debug)]
+pub struct DomainOrigin<'a, 'b> {
+ /// The scheme.
+ pub scheme: Scheme<'a>,
+ /// The host.
+ pub host: &'b str,
+ /// The TCP/UDP port.
+ pub port: Port,
+}
+impl<'b> DomainOrigin<'_, 'b> {
+ /// Returns a `DomainOrigin` with [`Self::scheme`] as [`Scheme::Https`], [`Self::host`] as `host`, and
+ /// [`Self::port`] as [`Port::None`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # extern crate alloc;
+ /// # use alloc::borrow::Cow;
+ /// # use webauthn_rp::{request::DomainOrigin, response::Origin};
+ /// assert_eq!(
+ /// DomainOrigin::new("www.example.com"),
+ /// Origin(Cow::Borrowed("https://www.example.com"))
+ /// );
+ /// // `DomainOrigin::new` does not allow _any_ port to exist.
+ /// assert_ne!(
+ /// DomainOrigin::new("www.example.com"),
+ /// Origin(Cow::Borrowed("https://www.example.com:443"))
+ /// );
+ /// ```
+ #[must_use]
+ #[inline]
+ pub const fn new<'c: 'b>(host: &'c str) -> Self {
+ Self {
+ scheme: Scheme::Https,
+ host,
+ port: Port::None,
+ }
+ }
+ /// Returns a `DomainOrigin` with [`Self::scheme`] as [`Scheme::Https`], [`Self::host`] as `host`, and
+ /// [`Self::port`] as [`Port::Any`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # extern crate alloc;
+ /// # use alloc::borrow::Cow;
+ /// # use webauthn_rp::{request::DomainOrigin, response::Origin};
+ /// // Any port is allowed to exist.
+ /// assert_eq!(
+ /// DomainOrigin::new_ignore_port("www.example.com"),
+ /// Origin(Cow::Borrowed("https://www.example.com:1234"))
+ /// );
+ /// // A port doesn't have to exist at all either.
+ /// assert_eq!(
+ /// DomainOrigin::new_ignore_port("www.example.com"),
+ /// Origin(Cow::Borrowed("https://www.example.com"))
+ /// );
+ /// ```
+ #[must_use]
+ #[inline]
+ pub const fn new_ignore_port<'c: 'b>(host: &'c str) -> Self {
+ Self {
+ scheme: Scheme::Https,
+ host,
+ port: Port::Any,
+ }
+ }
+}
+impl PartialEq<Origin<'_>> for DomainOrigin<'_, '_> {
+ /// Returns `true` iff [`DomainOrigin::scheme`], [`DomainOrigin::host`], and [`DomainOrigin::port`] are the
+ /// same after calling [`DomainOrigin::try_from`] on `other.0.as_str()`.
+ ///
+ /// Note that [`Scheme`] and [`Port`] need not be the same variant. For example [`Scheme::Https`] and
+ /// [`Scheme::Other`] containing `"https"` will be treated the same.
+ #[inline]
+ fn eq(&self, other: &Origin<'_>) -> bool {
+ DomainOrigin::try_from(other.0.as_ref()).is_ok_and(|dom| {
+ self.scheme.is_equal_to_origin_scheme(dom.scheme)
+ && self.host == dom.host
+ && self.port.is_equal_to_origin_port(dom.port)
+ })
+ }
+}
+impl PartialEq<Origin<'_>> for &DomainOrigin<'_, '_> {
+ #[inline]
+ fn eq(&self, other: &Origin<'_>) -> bool {
+ **self == *other
+ }
+}
+impl PartialEq<&Origin<'_>> for DomainOrigin<'_, '_> {
+ #[inline]
+ fn eq(&self, other: &&Origin<'_>) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<DomainOrigin<'_, '_>> for Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &DomainOrigin<'_, '_>) -> bool {
+ *other == *self
+ }
+}
+impl PartialEq<DomainOrigin<'_, '_>> for &Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &DomainOrigin<'_, '_>) -> bool {
+ *other == **self
+ }
+}
+impl PartialEq<&DomainOrigin<'_, '_>> for Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &&DomainOrigin<'_, '_>) -> bool {
+ **other == *self
+ }
+}
+impl<'a: 'b + 'c, 'b, 'c> TryFrom<&'a str> for DomainOrigin<'b, 'c> {
+ type Error = DomainOriginParseErr;
+ /// `value` is parsed according to the following extended regex:
+ ///
+ /// `^([^:]*:\/\/)?[^:]*(:.*)?$`
+ ///
+ /// where the `[^:]*` of the first capturing group is parsed according to [`Scheme::try_from`], and
+ /// the `.*` of the second capturing group is parsed according to [`Port::from_str`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `Scheme::try_from` or `Port::from_str` fail when applicable.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{DomainOrigin, Port, Scheme};
+ /// assert!(
+ /// DomainOrigin::try_from("https://www.example.com:443").map_or(false, |dom| matches!(
+ /// dom.scheme,
+ /// Scheme::Https
+ /// ) && dom.host
+ /// == "www.example.com"
+ /// && matches!(dom.port, Port::Val(port) if port == 443))
+ /// );
+ /// // Parsing is done in a case sensitive way.
+ /// assert!(DomainOrigin::try_from("Https://www.EXample.com").map_or(
+ /// false,
+ /// |dom| matches!(dom.scheme, Scheme::Other(scheme) if scheme == "Https")
+ /// && dom.host == "www.EXample.com"
+ /// && matches!(dom.port, Port::None)
+ /// ));
+ /// ```
+ #[inline]
+ fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+ // Any string that contains `':'` is not a [valid domain](https://url.spec.whatwg.org/#valid-domain), and
+ // and `"//"` never exists in a `Port`; thus if `"://"` exists, it's either invalid or delimits the scheme
+ // from the rest of the origin.
+ match value.split_once("://") {
+ None => Ok((Scheme::None, value)),
+ Some((poss_scheme, rem)) => Scheme::try_from(poss_scheme)
+ .map_err(DomainOriginParseErr::Scheme)
+ .map(|scheme| (scheme, rem)),
+ }
+ .and_then(|(scheme, rem)| {
+ // `':'` never exists in a valid domain; thus if it exists, it's either invalid or
+ // separates the domain from the port.
+ rem.split_once(':')
+ .map_or_else(
+ || Ok((rem, Port::None)),
+ |(rem2, poss_port)| {
+ Port::from_str(poss_port)
+ .map_err(DomainOriginParseErr::Port)
+ .map(|port| (rem2, port))
+ },
+ )
+ .map(|(host, port)| Self { scheme, host, port })
+ })
+ }
+}
+/// [`PublicKeyCredentialDescriptor`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptor)
+/// associated with a registered credential.
+#[derive(Debug)]
+pub struct PublicKeyCredentialDescriptor<T> {
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-id).
+ pub id: CredentialId<T>,
+ /// [`transports`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports).
+ pub transports: AuthTransports,
+}
+/// [`UserVerificationRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement).
+#[derive(Clone, Copy, Debug)]
+pub enum UserVerificationRequirement {
+ /// [`required`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-required).
+ Required,
+ /// [`discouraged`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-discouraged).
+ ///
+ /// Note some authenticators always require user verification when registering a credential (e.g.,
+ /// [CTAP 2.0](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html)
+ /// authenticators that have had a PIN enabled).
+ Discouraged,
+ /// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred).
+ Preferred,
+}
+/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints).
+#[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).
+ SecurityKey,
+ /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-client-device).
+ ClientDevice,
+ /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid).
+ Hybrid,
+ /// [`Self::SecurityKey`] and [`Self::ClientDevice`].
+ SecurityKeyClientDevice,
+ /// [`Self::ClientDevice`] and [`Self::SecurityKey`].
+ ClientDeviceSecurityKey,
+ /// [`Self::SecurityKey`] and [`Self::Hybrid`].
+ SecurityKeyHybrid,
+ /// [`Self::Hybrid`] and [`Self::SecurityKey`].
+ HybridSecurityKey,
+ /// [`Self::ClientDevice`] and [`Self::Hybrid`].
+ ClientDeviceHybrid,
+ /// [`Self::Hybrid`] and [`Self::ClientDevice`].
+ HybridClientDevice,
+ /// [`Self::SecurityKeyClientDevice`] and [`Self::Hybrid`].
+ SecurityKeyClientDeviceHybrid,
+ /// [`Self::SecurityKeyHybrid`] and [`Self::ClientDevice`].
+ SecurityKeyHybridClientDevice,
+ /// [`Self::ClientDeviceSecurityKey`] and [`Self::Hybrid`].
+ ClientDeviceSecurityKeyHybrid,
+ /// [`Self::ClientDeviceHybrid`] and [`Self::SecurityKey`].
+ ClientDeviceHybridSecurityKey,
+ /// [`Self::HybridSecurityKey`] and [`Self::ClientDevice`].
+ HybridSecurityKeyClientDevice,
+ /// [`Self::HybridClientDevice`] and [`Self::SecurityKey`].
+ HybridClientDeviceSecurityKey,
+}
+/// Controls if the response to a requested extension is required to be sent back.
+///
+/// Note when requiring an extension, the extension must not only be sent back but also
+/// contain at least one expected field
+/// (e.g., [`ClientExtensionsOutputs::cred_props`] must be
+/// `Some(CredentialPropertiesOutput { rk: Some(_) })`.
+///
+/// If one wants to additionally control the values of an extension, use [`ExtensionInfo`].
+#[derive(Clone, Copy, Debug)]
+pub enum ExtensionReq {
+ /// The response to a requested extension is required to be sent back.
+ Require,
+ /// The response to a requested extension is allowed, but not required, to be sent back.
+ Allow,
+}
+/// Dictates how an extension should be processed.
+///
+/// If one wants to only control if the extension should be returned, use [`ExtensionReq`].
+#[derive(Clone, Copy, Debug)]
+pub enum ExtensionInfo {
+ /// Require the associated extension and enforce its value.
+ RequireEnforceValue,
+ /// Require the associated extension but don't enforce its value.
+ RequireDontEnforceValue,
+ /// Allow the associated extension to exist and enforce its value when it does exist.
+ AllowEnforceValue,
+ /// Allow the associated extension to exist but don't enforce its value.
+ AllowDontEnforceValue,
+}
+impl Display for ExtensionInfo {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::RequireEnforceValue => "require the corresponding extension response and enforce its value",
+ Self::RequireDontEnforceValue => "require the corresponding extension response but don't enforce its value",
+ Self::AllowEnforceValue => "don't require the corresponding extension response; but if sent, enforce its value",
+ Self::AllowDontEnforceValue => "don't require the corresponding extension response; and if sent, don't enforce its value",
+ })
+ }
+}
+/// [`CredentialMediationRequirement`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
+#[derive(Clone, Copy, Debug)]
+pub enum CredentialMediationRequirement {
+ /// [`silent`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-silent).
+ Silent,
+ /// [`optional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-optional)
+ Optional,
+ /// [`conditional`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-conditional)
+ ///
+ /// Note that when registering a new credential with [`PublicKeyCredentialCreationOptions::mediation`] set to
+ /// `Self::Conditional`, [`UserVerificationRequirement::Discouraged`] MUST be used unless user verification
+ /// can be explicitly performed during the ceremony.
+ Conditional,
+ /// [`required`](https://www.w3.org/TR/credential-management-1/#dom-credentialmediationrequirement-required)
+ Required,
+}
+/// Backup requirements for the credential.
+#[derive(Clone, Copy, Debug, Default)]
+pub enum BackupReq {
+ #[default]
+ /// No requirements (i.e., any [`Backup`] is allowed).
+ None,
+ /// Credential must not be eligible for backup.
+ NotEligible,
+ /// Credential must be eligible for backup.
+ ///
+ /// Note the existence of a backup is ignored. If a backup must exist, then use [`Self::Exists`].
+ Eligible,
+ /// Credential must be backed up.
+ Exists,
+}
+impl From<Backup> for BackupReq {
+ /// One may want to create `BackupReq` based on the previous `Backup` such that the subsequent `Backup` is
+ /// essentially unchanged.
+ ///
+ /// Specifically this transforms [`Backup::NotEligible`] to [`Self::NotEligible`] and [`Backup::Eligible`] and
+ /// [`Backup::Exists`] to [`Self::Eligible`]. Note this means that a credential that
+ /// is eligible to be backed up but currently does not have a backup will be allowed to change such that it
+ /// is backed up. Similarly, a credential that is backed up is allowed to change such that a backup no longer
+ /// exists.
+ #[inline]
+ fn from(value: Backup) -> Self {
+ if matches!(value, Backup::NotEligible) {
+ Self::NotEligible
+ } else {
+ Self::Eligible
+ }
+ }
+}
+/// A container of "credentials".
+///
+/// This is mainly a way to unify [`Vec`] of [`PublicKeyCredentialDescriptor`]
+/// and [`AllowedCredentials`]. This can be useful in situations when one only
+/// deals with [`AllowedCredential`]s with empty [`CredentialSpecificExtension`]s
+/// essentially making them the same as [`PublicKeyCredentialDescriptor`]s.
+///
+/// # Examples
+///
+/// ```
+/// # use webauthn_rp::{
+/// # request::{
+/// # auth::AllowedCredentials, register::UserHandle, Credentials, PublicKeyCredentialDescriptor,
+/// # },
+/// # response::{AuthTransports, CredentialId},
+/// # };
+/// /// Fetches all credentials under `user_handle` to be allowed during authentication for non-discoverable
+/// /// requests.
+/// # #[cfg(feature = "custom")]
+/// fn get_allowed_credentials(user_handle: UserHandle<&[u8]>) -> AllowedCredentials {
+/// get_credentials(user_handle)
+/// }
+/// /// Fetches all credentials under `user_handle` to be excluded during registration.
+/// # #[cfg(feature = "custom")]
+/// fn get_excluded_credentials(
+/// user_handle: UserHandle<&[u8]>,
+/// ) -> Vec<PublicKeyCredentialDescriptor<Vec<u8>>> {
+/// get_credentials(user_handle)
+/// }
+/// /// Used to fetch the excluded `PublicKeyCredentialDescriptor`s associated with `user_handle` during
+/// /// registration as well as the `AllowedCredentials` containing `AllowedCredential`s with no credential-specific
+/// /// extensions which is used for non-discoverable requests.
+/// # #[cfg(feature = "custom")]
+/// fn get_credentials<T>(user_handle: UserHandle<&[u8]>) -> T
+/// where
+/// T: Credentials,
+/// PublicKeyCredentialDescriptor<Vec<u8>>: Into<T::Credential>,
+/// {
+/// let iter = get_cred_parts(user_handle);
+/// let len = iter.size_hint().0;
+/// iter.fold(T::with_capacity(len), |mut creds, parts| {
+/// creds.push(
+/// PublicKeyCredentialDescriptor {
+/// id: parts.0,
+/// transports: parts.1,
+/// }
+/// .into(),
+/// );
+/// creds
+/// })
+/// }
+/// /// Fetches all `CredentialId`s and associated `AuthTransports` under `user_handle`
+/// /// from the database.
+/// # #[cfg(feature = "custom")]
+/// fn get_cred_parts(
+/// user_handle: UserHandle<&[u8]>,
+/// ) -> impl Iterator<Item = (CredentialId<Vec<u8>>, AuthTransports)> {
+/// // ⋮
+/// # [(
+/// # CredentialId::try_from(vec![0; 16]).unwrap(),
+/// # AuthTransports::NONE,
+/// # )]
+/// # .into_iter()
+/// }
+/// ```
+pub trait Credentials: Sized {
+ /// The "credential"s that make up `Self`.
+ type Credential;
+ /// Returns `Self`.
+ #[inline]
+ #[must_use]
+ fn new() -> Self {
+ Self::with_capacity(0)
+ }
+ /// Returns `Self` with at least `capacity` allocated.
+ fn with_capacity(capacity: usize) -> Self;
+ /// Adds `cred` to `self`.
+ ///
+ /// Returns `true` iff `cred` was added.
+ fn push(&mut self, cred: Self::Credential) -> bool;
+ /// Returns the number of [`Self::Credential`]s in `Self`.
+ fn len(&self) -> usize;
+ /// Returns `true` iff [`Self::len`] is `0`.
+ #[inline]
+ fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+}
+impl<T> Credentials for Vec<T> {
+ type Credential = T;
+ #[inline]
+ fn with_capacity(capacity: usize) -> Self {
+ Self::with_capacity(capacity)
+ }
+ #[inline]
+ fn push(&mut self, cred: Self::Credential) -> bool {
+ self.push(cred);
+ true
+ }
+ #[inline]
+ fn len(&self) -> usize {
+ self.len()
+ }
+}
+/// Additional options that control how [`Ceremony::partial_validate`] works.
+struct CeremonyOptions<'origins, 'top_origins, O, T> {
+ /// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
+ ///
+ /// When this is empty, the origin that will be used will be based on
+ /// the [`RpId`] passed to [`RegistrationServerState::verify`]. If [`RpId::Domain`], then the [`DomainOrigin`] returned from
+ /// passing [`AsciiDomain::as_ref`] to [`DomainOrigin::new`] will be used; otherwise the [`Url`] in
+ /// [`RpId::Url`] will be used.
+ allowed_origins: &'origins [O],
+ /// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin)
+ /// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
+ ///
+ /// When this is `Some`, [`CollectedClientData::cross_origin`] is allowed to be `true`. When the contained
+ /// `slice` is empty, [`CollectedClientData::top_origin`] must be `None`. When this is `None`,
+ /// `CollectedClientData::cross_origin` must be `false` and `CollectedClientData::top_origin` must be `None`.
+ allowed_top_origins: Option<&'top_origins [T]>,
+ /// The required [`Backup`] state of the credential.
+ backup_requirement: BackupReq,
+ /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`.
+ #[cfg(feature = "serde_relaxed")]
+ client_data_json_relaxed: bool,
+}
+impl<'o, 't, O, T> From<&RegistrationVerificationOptions<'o, 't, O, T>>
+ for CeremonyOptions<'o, 't, O, T>
+{
+ fn from(value: &RegistrationVerificationOptions<'o, 't, O, T>) -> Self {
+ Self {
+ allowed_origins: value.allowed_origins,
+ allowed_top_origins: value.allowed_top_origins,
+ backup_requirement: value.backup_requirement,
+ #[cfg(feature = "serde_relaxed")]
+ client_data_json_relaxed: value.client_data_json_relaxed,
+ }
+ }
+}
+/// Functionality common to both registration and authentication ceremonies.
+///
+/// Designed to be implemented on the _request_ side.
+trait Ceremony {
+ /// The type of response that is associated with the ceremony.
+ type R: Response;
+ /// Challenge.
+ fn rand_challenge(&self) -> SentChallenge;
+ /// `Instant` the ceremony was expires.
+ #[cfg(not(feature = "serializable_server_state"))]
+ fn expiry(&self) -> Instant;
+ /// `Instant` the ceremony was expires.
+ #[cfg(feature = "serializable_server_state")]
+ fn expiry(&self) -> SystemTime;
+ /// User verification requirement.
+ fn user_verification(&self) -> UserVerificationRequirement;
+ /// Performs validation of ceremony criteria common to both ceremony types.
+ #[expect(
+ clippy::type_complexity,
+ reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
+ )]
+ #[expect(clippy::too_many_lines, reason = "102 lines is fine")]
+ fn partial_validate<'a, O: PartialEq<Origin<'a>>, T: PartialEq<Origin<'a>>>(
+ &self,
+ rp_id: &RpId,
+ resp: &'a Self::R,
+ key: <<Self::R as Response>::Auth as AuthResponse>::CredKey<'_>,
+ options: &CeremonyOptions<'_, '_, O, T>,
+ ) -> Result<
+ <<Self::R as Response>::Auth as AuthResponse>::Auth<'a>,
+ CeremonyErr<
+ <<<Self::R as Response>::Auth as AuthResponse>::Auth<'a> as AuthDataContainer<'a>>::Err,
+ >,
+ > {
+ // [Registration ceremony](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential)
+ // is handled by:
+ //
+ // 1. Calling code.
+ // 2. Client code and the construction of `resp` (hopefully via [`Registration::deserialize`]).
+ // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAttestation::deserialize`]).
+ // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]).
+ // 5. Below via [`CollectedClientData::from_client_data_json_relaxed`].
+ // 6. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`].
+ // 7. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`].
+ // 8. Below.
+ // 9. Below.
+ // 10. Below.
+ // 11. Below.
+ // 12. Below via [`AuthenticatorAttestation::new`].
+ // 13. Below via [`AttestationObject::parse_data`].
+ // 14. Below.
+ // 15. [`RegistrationServerState::verify`].
+ // 16. Below.
+ // 17. Below via [`AuthenticatorData::from_cbor`].
+ // 18. Below.
+ // 19. Below.
+ // 20. [`RegistrationServerState::verify`].
+ // 21. Below via [`AttestationObject::parse_data`].
+ // 22. Below via [`AttestationObject::parse_data`].
+ // 23. N/A since only none and self attestations are supported.
+ // 24. Always satisfied since only none and self attestations are supported (Item 3 is N/A).
+ // 25. Below via [`AttestedCredentialData::from_cbor`].
+ // 26. Calling code.
+ // 27. [`RegistrationServerState::verify`].
+ // 28. N/A since only none and self attestations are supported.
+ // 29. [`RegistrationServerState::verify`].
+ //
+ //
+ // [Authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion)
+ // is handled by:
+ //
+ // 1. Calling code.
+ // 2. Client code and the construction of `resp` (hopefully via [`Authentication::deserialize`]).
+ // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAssertion::deserialize`]).
+ // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]).
+ // 5. [`AuthenticationServerState::verify`].
+ // 6. [`AuthenticationServerState::verify`].
+ // 7. Informative only in that it defines variables.
+ // 8. Below via [`CollectedClientData::from_client_data_json_relaxed`].
+ // 9. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`].
+ // 10. Below via [`CollectedClientData::from_client_data_json_relaxed`] or [`CollectedClientData::from_client_data_json_relaxed`].
+ // 11. Below.
+ // 12. Below.
+ // 13. Below.
+ // 14. Below.
+ // 15. Below.
+ // 16. Below via [`AuthenticatorData::from_cbor`].
+ // 17. Below.
+ // 18. Below via [`AuthenticatorData::from_cbor`].
+ // 19. Below.
+ // 20. Below via [`AuthenticatorAssertion::new`].
+ // 21. Below.
+ // 22. [`AuthenticationServerState::verify`].
+ // 23. [`AuthenticationServerState::verify`].
+ // 24. [`AuthenticationServerState::verify`].
+ // 25. [`AuthenticationServerState::verify`].
+
+ // Enforce timeout.
+ #[cfg(not(feature = "serializable_server_state"))]
+ let active = self.expiry() >= Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let active = self.expiry() >= SystemTime::now();
+ if active {
+ #[cfg(feature = "serde_relaxed")]
+ let relaxed = options.client_data_json_relaxed;
+ #[cfg(not(feature = "serde_relaxed"))]
+ let relaxed = false;
+ resp.auth()
+ // Steps 5–7, 12–13, 17, 21–22, and 25 of the registration ceremony.
+ // Steps 8–10, 16, 18, and 20–21 of the authentication ceremony.
+ .parse_data_and_verify_sig(key, relaxed)
+ .map_err(CeremonyErr::AuthResp)
+ .and_then(|(client_data_json, auth_response)| {
+ if options.allowed_origins.is_empty() {
+ if match *rp_id {
+ RpId::Domain(ref dom) => {
+ // Steps 9 and 12 of the registration and authentication ceremonies
+ // respectively.
+ DomainOrigin::new(dom.as_ref()) == client_data_json.origin
+ }
+ // Steps 9 and 12 of the registration and authentication ceremonies
+ // respectively.
+ RpId::Url(ref url) => url == client_data_json.origin,
+ } {
+ Ok(())
+ } else {
+ Err(CeremonyErr::OriginMismatch)
+ }
+ } else {
+ options
+ .allowed_origins
+ .iter()
+ // Steps 9 and 12 of the registration and authentication ceremonies
+ // respectively.
+ .find(|o| **o == client_data_json.origin)
+ .ok_or(CeremonyErr::OriginMismatch)
+ .map(|_| ())
+ }
+ .and_then(|()| {
+ // Steps 10–11 of the registration ceremony.
+ // Steps 13–14 of the authentication ceremony.
+ match options.allowed_top_origins {
+ None => {
+ if client_data_json.cross_origin {
+ Err(CeremonyErr::CrossOrigin)
+ } else if client_data_json.top_origin.is_some() {
+ Err(CeremonyErr::TopOriginMismatch)
+ } else {
+ Ok(())
+ }
+ }
+ Some(top_origins) => client_data_json.top_origin.map_or(Ok(()), |t| {
+ top_origins
+ .iter()
+ .find(|top| **top == t)
+ .ok_or(CeremonyErr::TopOriginMismatch)
+ .map(|_| ())
+ }),
+ }
+ .and_then(|()| {
+ // Steps 8 and 11 of the registration and authentication ceremonies
+ // respectively.
+ if self.rand_challenge() == client_data_json.challenge {
+ let auth_data = auth_response.authenticator_data();
+ rp_id
+ // Steps 14 and 15 of the registration and authentication ceremonies
+ // respectively.
+ .validate_rp_id_hash(auth_data.rp_hash())
+ .and_then(|()| {
+ let flag = auth_data.flag();
+ // Steps 16 and 17 of the registration and authentication ceremonies
+ // respectively.
+ if flag.user_verified
+ || !matches!(
+ self.user_verification(),
+ UserVerificationRequirement::Required
+ )
+ {
+ // Steps 18–19 of the registration ceremony.
+ // Step 19 of the authentication ceremony.
+ match options.backup_requirement {
+ BackupReq::None => Ok(()),
+ BackupReq::NotEligible => {
+ if matches!(flag.backup, Backup::NotEligible) {
+ Ok(())
+ } else {
+ Err(CeremonyErr::BackupEligible)
+ }
+ }
+ BackupReq::Eligible => {
+ if matches!(flag.backup, Backup::NotEligible) {
+ Err(CeremonyErr::BackupNotEligible)
+ } else {
+ Ok(())
+ }
+ }
+ BackupReq::Exists => {
+ if matches!(flag.backup, Backup::Exists) {
+ Ok(())
+ } else {
+ Err(CeremonyErr::BackupDoesNotExist)
+ }
+ }
+ }
+ } else {
+ Err(CeremonyErr::UserNotVerified)
+ }
+ })
+ .map(|()| auth_response)
+ } else {
+ Err(CeremonyErr::ChallengeMismatch)
+ }
+ })
+ })
+ })
+ } else {
+ Err(CeremonyErr::Timeout)
+ }
+ }
+}
+/// `300_000` milliseconds is equal to five minutes.
+#[expect(unsafe_code, reason = "we want a const, and this is completely safe")]
+pub(super) const THREE_HUNDRED_THOUSAND: NonZeroU32 = {
+ // SAFETY:
+ // `300_000 > 0`.
+ // Compilation always fails when using a constant value of 0,
+ // so there is _nothing_ wrong with this use of `unsafe`.
+ unsafe { NonZeroU32::new_unchecked(300_000) }
+};
+/// [`Hasher`] whose `write_*` methods simply store up to 64 bits of the passed argument _as is_ overwriting
+/// any previous state.
+///
+/// This is designed to only be used indirectly via a hash map whose keys are randomly generated on the server
+/// based on at least 64 bits—the size of the integer returned from [`Self::finish`]—of entropy.
+/// This makes this `Hasher` usable (and ideal) in only the most niche circumstances.
+///
+/// [`RegistrationServerState`] and [`AuthenticationServerState`] both implement [`Hash`] by simply writing the
+/// contained [`Challenge`]; thus when they are stored in a hashed collection (e.g., [`FixedCapHashSet`]), one can
+/// optimize without fear by using this `Hasher` since `Challenge`s are immutable and can only ever be created on
+/// the server via [`Challenge::new`] (and equivalently [`Challenge::default`]). `RegistrationServerState` and
+/// `AuthenticationServerState` are also immutable and only constructable via
+/// [`PublicKeyCredentialCreationOptions::start_ceremony`] and
+/// [`PublicKeyCredentialRequestOptions::start_ceremony`] respectively. Since `Challenge` is already based on
+/// a random `u128`, other `Hasher`s will be slower and likely produce lower-quality hashes (and never
+/// higher quality).
+#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+#[derive(Debug)]
+pub struct IdentityHasher(u64);
+// Note it is _not_ required for `write_*` methods to do the same thing as other `write_*` methods
+// (e.g., `Self::write_u64` may not be the same thing as 8 calls to `Self::write_u8`).
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+impl Hasher for IdentityHasher {
+ /// Returns `0` if no `write_*` calls have been made; otherwise returns the result of the most recent
+ /// `write_*` call.
+ #[inline]
+ fn finish(&self) -> u64 {
+ self.0
+ }
+ /// Writes `i` to `self`.
+ #[inline]
+ fn write_u64(&mut self, i: u64) {
+ self.0 = i;
+ }
+ /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i8(&mut self, i: i8) {
+ self.write_u64(i as u64);
+ }
+ /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i16(&mut self, i: i16) {
+ self.write_u64(i as u64);
+ }
+ /// Sign-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i32(&mut self, i: i32) {
+ self.write_u64(i as u64);
+ }
+ /// Redirects to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i64(&mut self, i: i64) {
+ self.write_u64(i as u64);
+ }
+ /// Truncates `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ clippy::cast_sign_loss,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_i128(&mut self, i: i128) {
+ self.write_u64(i as u64);
+ }
+ /// Zero-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[inline]
+ fn write_u8(&mut self, i: u8) {
+ self.write_u64(u64::from(i));
+ }
+ /// Zero-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[inline]
+ fn write_u16(&mut self, i: u16) {
+ self.write_u64(u64::from(i));
+ }
+ /// Zero-extends `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[inline]
+ fn write_u32(&mut self, i: u32) {
+ self.write_u64(u64::from(i));
+ }
+ /// Truncates `i` to a [`u64`] before redirecting to [`Self::write_u64`].
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "we simply need to convert into a u64 in a deterministic way"
+ )]
+ #[inline]
+ fn write_u128(&mut self, i: u128) {
+ self.write_u64(i as u64);
+ }
+ /// This does nothing iff `bytes.len() < 8`; otherwise the first 8 bytes are converted
+ /// to a [`u64`] that is written via [`Self::write_u64`];
+ #[expect(clippy::host_endian_bytes, reason = "endianness does not matter")]
+ #[inline]
+ fn write(&mut self, bytes: &[u8]) {
+ if let Some(data) = bytes.get(..8) {
+ let mut val = [0; 8];
+ val.copy_from_slice(data);
+ self.write_u64(u64::from_ne_bytes(val));
+ }
+ }
+}
+/// [`BuildHasher`] of an [`IdentityHasher`].
+///
+/// This MUST only be used with hash maps with keys that are randomly generated on the server based on at least 64
+/// bits of entropy.
+#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+#[derive(Clone, Copy, Debug)]
+pub struct BuildIdentityHasher;
+#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
+#[cfg(not(feature = "serializable_server_state"))]
+impl BuildHasher for BuildIdentityHasher {
+ type Hasher = IdentityHasher;
+ #[inline]
+ fn build_hasher(&self) -> Self::Hasher {
+ IdentityHasher(0)
+ }
+}
+/// Prevent users from implementing [`ServerState`].
+mod private {
+ /// Marker trait used as a supertrait of `ServerState`.
+ pub trait Sealed {}
+ impl Sealed for super::AuthenticationServerState {}
+ impl Sealed for super::RegistrationServerState {}
+}
+/// Subset of data shared by both [`RegistrationServerState`] and [`AuthenticationServerState`].
+///
+/// This trait is sealed and cannot be implemented for types outside of `webauthn_rp`.
+pub trait ServerState: private::Sealed {
+ /// Returns the `Instant` the ceremony expires.
+ ///
+ /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead.
+ #[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
+ #[cfg(any(doc, not(feature = "serializable_server_state")))]
+ fn expiration(&self) -> Instant;
+ /// Returns the `SystemTime` the ceremony expires.
+ #[cfg(all(not(doc), feature = "serializable_server_state"))]
+ fn expiration(&self) -> SystemTime;
+ /// Returns the `SentChallenge` associated with the ceremony.
+ fn sent_challenge(&self) -> SentChallenge;
+}
+/// Fixed-capacity hash set that only inserts items when there is available capacity.
+///
+/// This should only be used if _both_ of the following conditions are met:
+///
+/// * Application is already protected from memory exhaustion attacks (e.g., users must be connected
+/// via VPN).
+/// * There are legitimate reasons for an in-memory collection to grow unbounded.
+///
+/// The first point is necessary; otherwise an attacker could trivially slow down the application
+/// by causing repeated inserts forcing repeated calls to functions like [`Self::retain`]. The second
+/// point is necessary; otherwise any in-memory collection would suffice.
+///
+/// When `T` is a [`ServerState`], there are legitimate reasons why a ceremony will never finish (e.g.,
+/// an outage could kill a user's connection after starting a ceremony). The longer the application
+/// runs the more such instances occur to the point where the in-memory collection is full of expired
+/// ceremonies. Since this should rarely occur and as long as [`Self::capacity`] is appropriate,
+/// [`Self::insert`] should almost always succeed; however very rarely there will be a point when
+/// one will have to [`Self::remove_expired_ceremonies`]. A vast majority of the time a user
+/// will complete the ceremony which requires ownership of the `ServerState` which in turn requires
+/// [`Self::take`] which will add an available slot.
+#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+#[derive(Debug)]
+pub struct FixedCapHashSet<T, S = BuildIdentityHasher>(HashSet<T, S>);
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+impl<T> FixedCapHashSet<T, BuildIdentityHasher> {
+ /// Creates an empty `FixedCapHashSet` with at least the specified capacity.
+ ///
+ /// The hash set will be able to hold at least `capacity` elements without reallocating. This method is allowed
+ /// to allocate for more elements than `capacity`.
+ #[inline]
+ #[must_use]
+ pub fn new(capacity: usize) -> Self {
+ Self(HashSet::with_capacity_and_hasher(
+ capacity,
+ BuildIdentityHasher,
+ ))
+ }
+}
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+impl<T, S> FixedCapHashSet<T, S> {
+ /// Creates an empty `FixedCapHashSet` with at least the specified capacity, using `hasher` to hash the keys.
+ ///
+ /// The hash set will be able to hold at least `capacity` elements without reallocating. This method is allowed
+ /// to allocate for more elements than `capacity`.
+ #[inline]
+ #[must_use]
+ pub fn new_with_hasher(capacity: usize, hasher: S) -> Self {
+ Self(HashSet::with_capacity_and_hasher(capacity, hasher))
+ }
+ /// Returns the immutable capacity.
+ ///
+ /// This number is a lower bound; the `FixedCapHashSet` might be able to hold more, but is guaranteed to be
+ /// able to hold at least this many.
+ #[inline]
+ #[must_use]
+ pub fn capacity(&self) -> usize {
+ self.0.capacity()
+ }
+ /// Clears the set, removing all values.
+ #[inline]
+ pub fn clear(&mut self) {
+ self.0.clear();
+ }
+ /// Returns the number of elements in the set.
+ #[inline]
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+ /// Returns `true` iff the set contains no elements.
+ #[inline]
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+ /// Retains only the elements specified by the predicate.
+ ///
+ /// In other words, remove all elements `e` for which `f(&e)` returns `false`. The elements are visited in
+ /// unsorted (and unspecified) order.
+ #[inline]
+ pub fn retain<F>(&mut self, f: F)
+ where
+ F: FnMut(&T) -> bool,
+ {
+ self.0.retain(f);
+ }
+}
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+impl<T: ServerState, S> FixedCapHashSet<T, S> {
+ /// Removes all expired ceremonies.
+ #[inline]
+ pub fn remove_expired_ceremonies(&mut self) {
+ // Even though it's more accurate to check the current `Instant` for each ceremony, we elect to capture
+ // the `Instant` we begin iteration for performance reasons. It's unlikely an appreciable amount of
+ // additional ceremonies would be removed.
+ let now = Instant::now();
+ self.retain(|v| v.expiration() >= now);
+ }
+}
+/// Result returned from [`FixedCapHashSet::insert`], [`FixedCapHashSet::insert_or_replace_expired`], and
+/// [`FixedCapHashSet::insert_or_replace_all_expired`].
+#[cfg_attr(docsrs, doc(cfg(not(feature = "serializable_server_state"))))]
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+#[derive(Clone, Copy, Debug)]
+pub enum InsertResult {
+ /// Value was successfully inserted.
+ Success,
+ /// Value was not inserted since the capacity was full.
+ CapacityFull,
+ /// Value was not inserted since the value already existed.
+ ///
+ /// When the keys are based on [`Challenge`]s, this should almost never occur since `Challenge`s
+ /// are 16 bytes of random data.
+ Duplicate,
+}
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+impl<T: Eq + Hash, S: BuildHasher> FixedCapHashSet<T, S> {
+ /// Returns `true` iff the set contains a value.
+ ///
+ /// The value may be any borrowed form of the set's value type, but `Hash` and `Eq` on the borrowed form _must_
+ /// match those for the value type.
+ #[inline]
+ #[must_use]
+ pub fn contains<Q>(&self, value: &Q) -> bool
+ where
+ T: Borrow<Q>,
+ Q: Eq + Hash + ?Sized,
+ {
+ self.0.contains(value)
+ }
+ /// Returns a reference to the value in the set, if any, that is equal to the given value.
+ ///
+ /// The value may be any borrowed form of the set's value type, but `Hash` and `Eq` on the borrowed form _must_
+ /// match those for the value type.
+ #[inline]
+ #[must_use]
+ pub fn get<Q>(&self, value: &Q) -> Option<&T>
+ where
+ T: Borrow<Q>,
+ Q: Eq + Hash + ?Sized,
+ {
+ self.0.get(value)
+ }
+ /// Removes a value from the set. Returns whether the value was present in the set.
+ ///
+ /// The value may be any borrowed form of the set's value type, but `Hash` and `Eq` on the borrowed form _must_
+ /// match those for the value type.
+ #[inline]
+ pub fn remove<Q>(&mut self, value: &Q) -> bool
+ where
+ T: Borrow<Q>,
+ Q: Eq + Hash + ?Sized,
+ {
+ self.0.remove(value)
+ }
+ /// Removes and returns the value in the set, if any, that is equal to the given one.
+ ///
+ /// The value may be any borrowed form of the set's value type, but `Hash` and `Eq` on the borrowed form _must_
+ /// match those for the value type.
+ #[inline]
+ pub fn take<Q>(&mut self, value: &Q) -> Option<T>
+ where
+ T: Borrow<Q>,
+ Q: Eq + Hash + ?Sized,
+ {
+ self.0.take(value)
+ }
+ /// Adds `value` to the set iff [`Self::capacity`] `>` [`Self::len`].
+ #[inline]
+ pub fn insert(&mut self, value: T) -> InsertResult {
+ if self.len() == self.capacity() {
+ InsertResult::CapacityFull
+ } else if self.0.insert(value) {
+ InsertResult::Success
+ } else {
+ InsertResult::Duplicate
+ }
+ }
+}
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+impl<T: Borrow<SentChallenge> + Eq + Hash + ServerState, S: BuildHasher> FixedCapHashSet<T, S> {
+ /// Adds a ceremony to the set.
+ ///
+ /// This will only insert `value` iff [`Self::capacity`] `>` [`Self::len`]. When [`Self::len`] `==`
+ /// [`Self::capacity`], this will iterate items in the set until an expired ceremony is encountered; at which
+ /// point, the expired ceremony will be removed before `value` is inserted. In the event no expired ceremonies
+ /// exist, [`InsertResult::CapacityFull`] will be returned.
+ ///
+ /// If one wants to avoid the potentially expensive operation of iterating the set for an expired ceremony,
+ /// call [`Self::insert`]. Alternatively one can call [`Self::insert_or_replace_all_expired`] to avoid
+ /// repeatedly iterating the hash set once its capacity is full.
+ #[inline]
+ pub fn insert_or_replace_expired(&mut self, value: T) -> InsertResult {
+ if self.len() == self.capacity() {
+ let now = Instant::now();
+ self.0
+ .iter()
+ .try_fold((), |(), v| {
+ if v.expiration() < now {
+ Err(v.sent_challenge())
+ } else {
+ Ok(())
+ }
+ })
+ .map_or_else(
+ |chall| {
+ self.remove(&chall);
+ if self.0.insert(value) {
+ InsertResult::Success
+ } else {
+ InsertResult::Duplicate
+ }
+ },
+ |()| InsertResult::CapacityFull,
+ )
+ } else if self.0.insert(value) {
+ InsertResult::Success
+ } else {
+ InsertResult::Duplicate
+ }
+ }
+ /// Adds a ceremony to the set.
+ ///
+ /// This will only insert `value` iff [`Self::capacity`] `>` [`Self::len`]. When [`Self::len`] `==`
+ /// [`Self::capacity`], this will [`Self::remove_expired_ceremonies`] before `value` is inserted. In the event
+ /// no expired ceremonies exist, [`InsertResult::CapacityFull`] will be returned.
+ #[inline]
+ pub fn insert_or_replace_all_expired(&mut self, value: T) -> InsertResult {
+ if self.len() == self.capacity() {
+ self.remove_expired_ceremonies();
+ }
+ if self.len() == self.capacity() {
+ InsertResult::CapacityFull
+ } else if self.0.insert(value) {
+ InsertResult::Success
+ } else {
+ InsertResult::Duplicate
+ }
+ }
+}
+#[cfg(test)]
+mod tests {
+ #[cfg(feature = "custom")]
+ use super::{
+ super::{
+ AggErr, AuthenticatedCredential,
+ response::{
+ AuthTransports, AuthenticatorAttachment, Backup, CredentialId,
+ auth::{Authentication, AuthenticatorAssertion},
+ register::{
+ AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation,
+ AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputs,
+ CompressedP256PubKey, CompressedP384PubKey, CompressedPubKey,
+ CredentialProtectionPolicy, DynamicState, Ed25519PubKey, Registration,
+ RsaPubKey, StaticState, UncompressedPubKey,
+ },
+ },
+ },
+ AsciiDomain, Challenge, Credentials, ExtensionInfo, ExtensionReq,
+ PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement,
+ auth::{
+ AllowedCredential, AllowedCredentials, AuthenticationVerificationOptions,
+ CredentialSpecificExtension, Extension as AuthExt, PrfInputOwned,
+ PublicKeyCredentialRequestOptions,
+ },
+ register::{
+ CredProtect, Extension as RegExt, PublicKeyCredentialCreationOptions,
+ PublicKeyCredentialUserEntity, RegistrationVerificationOptions, UserHandle,
+ },
+ };
+ #[cfg(feature = "custom")]
+ use ed25519_dalek::{Signer, SigningKey};
+ #[cfg(feature = "custom")]
+ use p256::{
+ ecdsa::{DerSignature as P256DerSig, SigningKey as P256Key},
+ elliptic_curve::sec1::Tag,
+ };
+ #[cfg(feature = "custom")]
+ use p384::ecdsa::{DerSignature as P384DerSig, SigningKey as P384Key};
+ #[cfg(feature = "custom")]
+ use rsa::{
+ BigUint, RsaPrivateKey,
+ pkcs1v15::SigningKey as RsaKey,
+ sha2::{Digest, Sha256},
+ signature::{Keypair, SignatureEncoding},
+ traits::PublicKeyParts,
+ };
+ #[cfg(feature = "custom")]
+ const CBOR_UINT: u8 = 0b000_00000;
+ #[cfg(feature = "custom")]
+ const CBOR_NEG: u8 = 0b001_00000;
+ #[cfg(feature = "custom")]
+ const CBOR_BYTES: u8 = 0b010_00000;
+ #[cfg(feature = "custom")]
+ const CBOR_TEXT: u8 = 0b011_00000;
+ #[cfg(feature = "custom")]
+ const CBOR_MAP: u8 = 0b101_00000;
+ #[cfg(feature = "custom")]
+ const CBOR_SIMPLE: u8 = 0b111_00000;
+ #[cfg(feature = "custom")]
+ const CBOR_TRUE: u8 = CBOR_SIMPLE | 21;
+ #[test]
+ #[cfg(feature = "custom")]
+ fn ed25519_reg() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ let id = UserHandle::try_from([0; 1].as_slice())?;
+ let mut opts = PublicKeyCredentialCreationOptions::passkey(
+ &rp_id,
+ PublicKeyCredentialUserEntity {
+ name: "foo".try_into()?,
+ id,
+ display_name: None,
+ },
+ Vec::new(),
+ );
+ opts.challenge = Challenge(0);
+ opts.extensions = RegExt {
+ cred_props: None,
+ cred_protect: CredProtect::UserVerificationRequired(ExtensionInfo::RequireEnforceValue),
+ min_pin_length: Some((10, ExtensionInfo::RequireEnforceValue)),
+ prf: Some(ExtensionInfo::RequireEnforceValue),
+ };
+ let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
+ let mut attestation_object = Vec::new();
+ attestation_object.extend_from_slice(
+ [
+ CBOR_MAP | 3,
+ CBOR_TEXT | 3,
+ b'f',
+ b'm',
+ b't',
+ CBOR_TEXT | 6,
+ b'p',
+ b'a',
+ b'c',
+ b'k',
+ b'e',
+ b'd',
+ CBOR_TEXT | 7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ CBOR_MAP | 2,
+ CBOR_TEXT | 3,
+ b'a',
+ b'l',
+ b'g',
+ // COSE EdDSA.
+ CBOR_NEG | 7,
+ CBOR_TEXT | 3,
+ b's',
+ b'i',
+ b'g',
+ CBOR_BYTES | 24,
+ 64,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ CBOR_TEXT | 8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ CBOR_BYTES | 24,
+ // Length is 154.
+ 154,
+ // RP ID HASH.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // FLAGS.
+ // UP, UV, AT, and ED (right-to-left).
+ 0b1100_0101,
+ // COUNTER.
+ // 0 as 32-bit big endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ // AAGUID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // L.
+ // CREDENTIAL ID length is 16 as 16-bit big endian.
+ 0,
+ 16,
+ // CREDENTIAL ID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ CBOR_MAP | 4,
+ // COSE kty.
+ CBOR_UINT | 1,
+ // COSE OKP.
+ CBOR_UINT | 1,
+ // COSE alg.
+ CBOR_UINT | 3,
+ // COSE EdDSA.
+ CBOR_NEG | 7,
+ // COSE OKP crv.
+ CBOR_NEG,
+ // COSE Ed25519.
+ CBOR_UINT | 6,
+ // COSE OKP x.
+ CBOR_NEG | 1,
+ CBOR_BYTES | 24,
+ // Length is 32.
+ 32,
+ // Compressed-y coordinate.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ CBOR_MAP | 3,
+ CBOR_TEXT | 11,
+ b'c',
+ b'r',
+ b'e',
+ b'd',
+ b'P',
+ b'r',
+ b'o',
+ b't',
+ b'e',
+ b'c',
+ b't',
+ // userVerificationRequired.
+ CBOR_UINT | 3,
+ // CBOR text of length 11.
+ CBOR_TEXT | 11,
+ b'h',
+ b'm',
+ b'a',
+ b'c',
+ b'-',
+ b's',
+ b'e',
+ b'c',
+ b'r',
+ b'e',
+ b't',
+ CBOR_TRUE,
+ CBOR_TEXT | 12,
+ b'm',
+ b'i',
+ b'n',
+ b'P',
+ b'i',
+ b'n',
+ b'L',
+ b'e',
+ b'n',
+ b'g',
+ b't',
+ b'h',
+ CBOR_UINT | 16,
+ ]
+ .as_slice(),
+ );
+ attestation_object
+ .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
+ let sig_key = SigningKey::from_bytes(&[0; 32]);
+ let ver_key = sig_key.verifying_key();
+ let pub_key = ver_key.as_bytes();
+ attestation_object[107..139]
+ .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ attestation_object[188..220].copy_from_slice(pub_key);
+ let sig = sig_key.sign(&attestation_object[107..]);
+ attestation_object[32..96].copy_from_slice(sig.to_bytes().as_slice());
+ attestation_object.truncate(261);
+ assert!(matches!(opts.start_ceremony()?.0.verify(
+ &rp_id,
+ id,
+ &Registration {
+ response: AuthenticatorAttestation::new(
+ client_data_json,
+ attestation_object,
+ AuthTransports::NONE,
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ client_extension_results: ClientExtensionsOutputs {
+ cred_props: None,
+ prf: Some(AuthenticationExtensionsPrfOutputs { enabled: true, }),
+ },
+ },
+ &RegistrationVerificationOptions::<&str, &str>::default(),
+ )?.static_state.credential_public_key, UncompressedPubKey::Ed25519(k) if k.into_inner() == pub_key));
+ Ok(())
+ }
+ #[test]
+ #[cfg(feature = "custom")]
+ fn ed25519_auth() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ let mut creds = AllowedCredentials::with_capacity(1);
+ creds.push(AllowedCredential {
+ credential: PublicKeyCredentialDescriptor {
+ id: CredentialId::try_from(vec![0; 16])?,
+ transports: AuthTransports::NONE,
+ },
+ extension: CredentialSpecificExtension {
+ prf: Some(PrfInputOwned {
+ first: Vec::new(),
+ second: Some(Vec::new()),
+ ext_info: ExtensionReq::Require,
+ }),
+ },
+ });
+ let mut opts = PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?;
+ opts.user_verification = UserVerificationRequirement::Required;
+ opts.challenge = Challenge(0);
+ opts.extensions = AuthExt { prf: None };
+ let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
+ let mut authenticator_data = Vec::with_capacity(164);
+ authenticator_data.extend_from_slice(
+ [
+ // rpIdHash.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // flags.
+ // UP, UV, and ED (right-to-left).
+ 0b1000_0101,
+ // signCount.
+ // 0 as 32-bit big endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ CBOR_MAP | 1,
+ CBOR_TEXT | 11,
+ b'h',
+ b'm',
+ b'a',
+ b'c',
+ b'-',
+ b's',
+ b'e',
+ b'c',
+ b'r',
+ b'e',
+ b't',
+ CBOR_BYTES | 24,
+ // Length is 80.
+ 80,
+ // Two HMAC outputs concatenated and encrypted.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ authenticator_data[..32]
+ .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ authenticator_data
+ .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
+ let ed_priv = SigningKey::from([0; 32]);
+ let sig = ed_priv.sign(authenticator_data.as_slice()).to_vec();
+ authenticator_data.truncate(132);
+ assert!(!opts.start_ceremony()?.0.verify(
+ &rp_id,
+ &Authentication {
+ raw_id: CredentialId::try_from(vec![0; 16])?,
+ response: AuthenticatorAssertion::new(
+ client_data_json,
+ authenticator_data,
+ sig,
+ Some(UserHandle::try_from(vec![0])?),
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ &mut AuthenticatedCredential::new(
+ CredentialId::try_from([0; 16].as_slice())?,
+ UserHandle::try_from([0].as_slice())?,
+ StaticState {
+ credential_public_key: CompressedPubKey::<_, &[u8], &[u8], &[u8]>::Ed25519(
+ Ed25519PubKey::from(ed_priv.verifying_key().to_bytes()),
+ ),
+ extensions: AuthenticatorExtensionOutputStaticState {
+ cred_protect: CredentialProtectionPolicy::None,
+ hmac_secret: Some(true),
+ },
+ },
+ DynamicState {
+ user_verified: true,
+ backup: Backup::NotEligible,
+ sign_count: 0,
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ )?,
+ &AuthenticationVerificationOptions::<&str, &str>::default(),
+ )?);
+ Ok(())
+ }
+ #[test]
+ #[cfg(feature = "custom")]
+ fn p256_reg() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ let id = UserHandle::try_from([0; 1].as_slice())?;
+ let mut opts = PublicKeyCredentialCreationOptions::passkey(
+ &rp_id,
+ PublicKeyCredentialUserEntity {
+ name: "foo".try_into()?,
+ id,
+ display_name: None,
+ },
+ Vec::new(),
+ );
+ opts.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
+ let mut attestation_object = Vec::with_capacity(210);
+ attestation_object.extend_from_slice(
+ [
+ CBOR_MAP | 3,
+ CBOR_TEXT | 3,
+ b'f',
+ b'm',
+ b't',
+ CBOR_TEXT | 4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ CBOR_TEXT | 7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ CBOR_MAP,
+ CBOR_TEXT | 8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ CBOR_BYTES | 24,
+ // Length is 148.
+ 148,
+ // RP ID HASH.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // FLAGS.
+ // UP, UV, and AT (right-to-left).
+ 0b0100_0101,
+ // COUNTER.
+ // 0 as 32-bit big endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ // AAGUID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // L.
+ // CREDENTIAL ID length is 16 as 16-bit big endian.
+ 0,
+ 16,
+ // CREDENTIAL ID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ CBOR_MAP | 5,
+ // COSE kty.
+ CBOR_UINT | 1,
+ // COSE EC2.
+ CBOR_UINT | 2,
+ // COSE alg.
+ CBOR_UINT | 3,
+ // COSE ES256.
+ CBOR_NEG | 6,
+ // COSE EC2 crv.
+ CBOR_NEG,
+ // COSE P-256.
+ CBOR_UINT | 1,
+ // COSE EC2 x.
+ CBOR_NEG | 1,
+ CBOR_BYTES | 24,
+ // Length is 32.
+ 32,
+ // X-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // COSE EC2 y.
+ CBOR_NEG | 2,
+ CBOR_BYTES | 24,
+ // Length is 32.
+ 32,
+ // Y-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ attestation_object[30..62]
+ .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ let p256_key = P256Key::from_bytes(
+ &[
+ 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115,
+ 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95,
+ ]
+ .into(),
+ )
+ .unwrap()
+ .verifying_key()
+ .to_encoded_point(false);
+ let x = p256_key.x().unwrap();
+ let y = p256_key.y().unwrap();
+ attestation_object[111..143].copy_from_slice(x);
+ attestation_object[146..].copy_from_slice(y);
+ assert!(matches!(opts.start_ceremony()?.0.verify(
+ &rp_id,
+ id,
+ &Registration {
+ response: AuthenticatorAttestation::new(
+ client_data_json,
+ attestation_object,
+ AuthTransports::NONE,
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ client_extension_results: ClientExtensionsOutputs {
+ cred_props: None,
+ prf: None,
+ },
+ },
+ &RegistrationVerificationOptions::<&str, &str>::default(),
+ )?.static_state.credential_public_key, UncompressedPubKey::P256(k) if k.x() == x.as_slice() && k.y() == y.as_slice()));
+ Ok(())
+ }
+ #[test]
+ #[cfg(feature = "custom")]
+ fn p256_auth() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ let mut opts = PublicKeyCredentialRequestOptions::passkey(&rp_id);
+ opts.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
+ let mut authenticator_data = Vec::with_capacity(69);
+ authenticator_data.extend_from_slice(
+ [
+ // rpIdHash.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // flags.
+ // UP and UV (right-to-left).
+ 0b0000_0101,
+ // signCount.
+ // 0 as 32-bit big endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ authenticator_data[..32]
+ .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ authenticator_data
+ .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
+ let p256_key = P256Key::from_bytes(
+ &[
+ 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115,
+ 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95,
+ ]
+ .into(),
+ )
+ .unwrap();
+ let der_sig: P256DerSig = p256_key.sign(authenticator_data.as_slice());
+ let pub_key = p256_key.verifying_key().to_encoded_point(true);
+ authenticator_data.truncate(37);
+ assert!(!opts.start_ceremony()?.0.verify(
+ &rp_id,
+ &Authentication {
+ raw_id: CredentialId::try_from(vec![0; 16])?,
+ response: AuthenticatorAssertion::new(
+ client_data_json,
+ authenticator_data,
+ der_sig.as_bytes().into(),
+ Some(UserHandle::try_from(vec![0])?),
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ &mut AuthenticatedCredential::new(
+ CredentialId::try_from([0; 16].as_slice())?,
+ UserHandle::try_from([0].as_slice())?,
+ StaticState {
+ credential_public_key: CompressedPubKey::<&[u8], _, &[u8], &[u8]>::P256(
+ CompressedP256PubKey::from((
+ (*pub_key.x().unwrap()).into(),
+ pub_key.tag() == Tag::CompressedOddY
+ )),
+ ),
+ extensions: AuthenticatorExtensionOutputStaticState {
+ cred_protect: CredentialProtectionPolicy::None,
+ hmac_secret: None,
+ },
+ },
+ DynamicState {
+ user_verified: true,
+ backup: Backup::NotEligible,
+ sign_count: 0,
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ )?,
+ &AuthenticationVerificationOptions::<&str, &str>::default(),
+ )?);
+ Ok(())
+ }
+ #[test]
+ #[cfg(feature = "custom")]
+ fn p384_reg() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ let id = UserHandle::try_from([0; 1].as_slice())?;
+ let mut opts = PublicKeyCredentialCreationOptions::passkey(
+ &rp_id,
+ PublicKeyCredentialUserEntity {
+ name: "foo".try_into()?,
+ id,
+ display_name: None,
+ },
+ Vec::new(),
+ );
+ opts.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
+ let mut attestation_object = Vec::with_capacity(243);
+ attestation_object.extend_from_slice(
+ [
+ CBOR_MAP | 3,
+ CBOR_TEXT | 3,
+ b'f',
+ b'm',
+ b't',
+ CBOR_TEXT | 4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ // CBOR text of length 7.
+ CBOR_TEXT | 7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ CBOR_MAP,
+ CBOR_TEXT | 8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ CBOR_BYTES | 24,
+ // Length is 181.
+ 181,
+ // RP ID HASH.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // FLAGS.
+ // UP, UV, and AT (right-to-left).
+ 0b0100_0101,
+ // COUNTER.
+ // 0 as 32-bit big-endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ // AAGUID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // L.
+ // CREDENTIAL ID length is 16 as 16-bit big endian.
+ 0,
+ 16,
+ // CREDENTIAL ID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ CBOR_MAP | 5,
+ // COSE kty.
+ CBOR_UINT | 1,
+ // COSE EC2.
+ CBOR_UINT | 2,
+ // COSE alg.
+ CBOR_UINT | 3,
+ CBOR_NEG | 24,
+ // COSE ES384.
+ 34,
+ // COSE EC2 crv.
+ CBOR_NEG,
+ // COSE P-384.
+ CBOR_UINT | 2,
+ // COSE EC2 x.
+ CBOR_NEG | 1,
+ CBOR_BYTES | 24,
+ // Length is 48.
+ 48,
+ // X-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // COSE EC2 y.
+ CBOR_NEG | 2,
+ CBOR_BYTES | 24,
+ // Length is 48.
+ 48,
+ // Y-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ attestation_object[30..62]
+ .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ let p384_key = P384Key::from_bytes(
+ &[
+ 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232,
+ 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1,
+ 32, 86, 220, 68, 182, 11, 105, 223, 75, 70,
+ ]
+ .into(),
+ )
+ .unwrap()
+ .verifying_key()
+ .to_encoded_point(false);
+ let x = p384_key.x().unwrap();
+ let y = p384_key.y().unwrap();
+ attestation_object[112..160].copy_from_slice(x);
+ attestation_object[163..].copy_from_slice(y);
+ assert!(matches!(opts.start_ceremony()?.0.verify(
+ &rp_id,
+ id,
+ &Registration {
+ response: AuthenticatorAttestation::new(
+ client_data_json,
+ attestation_object,
+ AuthTransports::NONE,
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ client_extension_results: ClientExtensionsOutputs {
+ cred_props: None,
+ prf: None,
+ },
+ },
+ &RegistrationVerificationOptions::<&str, &str>::default(),
+ )?.static_state.credential_public_key, UncompressedPubKey::P384(k) if k.x() == x.as_slice() && k.y() == y.as_slice()));
+ Ok(())
+ }
+ #[test]
+ #[cfg(feature = "custom")]
+ fn p384_auth() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ let mut opts = PublicKeyCredentialRequestOptions::passkey(&rp_id);
+ opts.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
+ let mut authenticator_data = Vec::with_capacity(69);
+ authenticator_data.extend_from_slice(
+ [
+ // rpIdHash.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // flags.
+ // UP and UV (right-to-left).
+ 0b0000_0101,
+ // signCount.
+ // 0 as 32-bit big-endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ authenticator_data[..32]
+ .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ authenticator_data
+ .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
+ let p384_key = P384Key::from_bytes(
+ &[
+ 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232,
+ 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1,
+ 32, 86, 220, 68, 182, 11, 105, 223, 75, 70,
+ ]
+ .into(),
+ )
+ .unwrap();
+ let der_sig: P384DerSig = p384_key.sign(authenticator_data.as_slice());
+ let pub_key = p384_key.verifying_key().to_encoded_point(true);
+ authenticator_data.truncate(37);
+ assert!(!opts.start_ceremony()?.0.verify(
+ &rp_id,
+ &Authentication {
+ raw_id: CredentialId::try_from(vec![0; 16])?,
+ response: AuthenticatorAssertion::new(
+ client_data_json,
+ authenticator_data,
+ der_sig.as_bytes().into(),
+ Some(UserHandle::try_from(vec![0])?),
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ &mut AuthenticatedCredential::new(
+ CredentialId::try_from([0; 16].as_slice())?,
+ UserHandle::try_from([0].as_slice())?,
+ StaticState {
+ credential_public_key: CompressedPubKey::<&[u8], &[u8], _, &[u8]>::P384(
+ CompressedP384PubKey::from((
+ (*pub_key.x().unwrap()).into(),
+ pub_key.tag() == Tag::CompressedOddY
+ )),
+ ),
+ extensions: AuthenticatorExtensionOutputStaticState {
+ cred_protect: CredentialProtectionPolicy::None,
+ hmac_secret: None,
+ },
+ },
+ DynamicState {
+ user_verified: true,
+ backup: Backup::NotEligible,
+ sign_count: 0,
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ )?,
+ &AuthenticationVerificationOptions::<&str, &str>::default(),
+ )?);
+ Ok(())
+ }
+ #[test]
+ #[cfg(feature = "custom")]
+ fn rsa_reg() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ let id = UserHandle::try_from([0; 1].as_slice())?;
+ let mut opts = PublicKeyCredentialCreationOptions::passkey(
+ &rp_id,
+ PublicKeyCredentialUserEntity {
+ name: "foo".try_into()?,
+ id,
+ display_name: None,
+ },
+ Vec::new(),
+ );
+ opts.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information.
+ let mut attestation_object = Vec::with_capacity(406);
+ attestation_object.extend_from_slice(
+ [
+ CBOR_MAP | 3,
+ CBOR_TEXT | 3,
+ b'f',
+ b'm',
+ b't',
+ CBOR_TEXT | 4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ CBOR_TEXT | 7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ CBOR_MAP,
+ CBOR_TEXT | 8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ CBOR_BYTES | 25,
+ // Length is 343 as 16-bit big-endian.
+ 1,
+ 87,
+ // RP ID HASH.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // FLAGS.
+ // UP, UV, and AT (right-to-left).
+ 0b0100_0101,
+ // COUNTER.
+ // 0 as 32-bit big-endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ // AAGUID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // L.
+ // CREDENTIAL ID length is 16 as 16-bit big endian.
+ 0,
+ 16,
+ // CREDENTIAL ID.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ CBOR_MAP | 4,
+ // COSE kty.
+ CBOR_UINT | 1,
+ // COSE RSA.
+ CBOR_UINT | 3,
+ // COSE alg.
+ CBOR_UINT | 3,
+ CBOR_NEG | 25,
+ // COSE RS256.
+ 1,
+ 0,
+ // COSE n.
+ CBOR_NEG,
+ CBOR_BYTES | 25,
+ // Length is 256 as 16-bit big-endian.
+ 1,
+ 0,
+ // N. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // COSE e.
+ CBOR_NEG | 1,
+ CBOR_BYTES | 3,
+ // 65537 as 24-bit big-endian.
+ 1,
+ 0,
+ 1,
+ ]
+ .as_slice(),
+ );
+ attestation_object[31..63]
+ .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ let n = [
+ 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107,
+ 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206,
+ 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165,
+ 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204,
+ 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64,
+ 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105,
+ 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55,
+ 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21,
+ 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157,
+ 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188,
+ 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56,
+ 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13,
+ 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132,
+ 153, 79, 0, 133, 78, 7, 218, 165, 241,
+ ];
+ let e = 65537;
+ let d = [
+ 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0,
+ 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168,
+ 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108,
+ 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102,
+ 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247,
+ 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166,
+ 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111,
+ 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242,
+ 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212,
+ 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83,
+ 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153,
+ 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142,
+ 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45,
+ 50, 23, 3, 25, 253, 87, 227, 79, 119, 161,
+ ];
+ let p = BigUint::from_slice(
+ [
+ 352691927, 1294578443, 816143558, 690659917, 1161596366, 1544791087, 3999549486,
+ 3319149924, 2349250979, 1304689381, 3959753736, 3377900978, 866506027, 1671521644,
+ 3926847564, 898221388, 3448219846, 494454484, 3915534864, 2869735916, 2456511629,
+ 3397234721, 3012775852, 3472309790, 1923617705, 2993441050, 3210302569, 3605331368,
+ 3352563766, 688081007, 4104512503, 4145593376,
+ ]
+ .as_slice(),
+ );
+ let p_2 = BigUint::from_slice(
+ [
+ 4039514409, 964284038, 3230008587, 3320139220, 3562360334, 3165876926, 212773653,
+ 2752465512, 2973674888, 1717425549, 2084262803, 3585031058, 4162394935, 1428626842,
+ 1015474994, 3283774155, 2840050110, 190639246, 147241978, 2994256073, 4081014755,
+ 3102401369, 3547397148, 1545029057, 895305733, 2689179461, 1593439337, 3960057302,
+ 193068804, 2835123424, 4054880057, 4200258364,
+ ]
+ .as_slice(),
+ );
+ let rsa_key = RsaKey::<Sha256>::new(
+ RsaPrivateKey::from_components(
+ BigUint::from_bytes_le(n.as_slice()),
+ e.into(),
+ BigUint::from_bytes_le(d.as_slice()),
+ vec![p, p_2],
+ )
+ .unwrap(),
+ )
+ .verifying_key();
+ let n = rsa_key.as_ref().n().to_bytes_be();
+ attestation_object[113..369].copy_from_slice(n.as_slice());
+ assert!(matches!(opts.start_ceremony()?.0.verify(
+ &rp_id,
+ id,
+ &Registration {
+ response: AuthenticatorAttestation::new(
+ client_data_json,
+ attestation_object,
+ AuthTransports::NONE,
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ client_extension_results: ClientExtensionsOutputs {
+ cred_props: None,
+ prf: None,
+ },
+ },
+ &RegistrationVerificationOptions::<&str, &str>::default(),
+ )?.static_state.credential_public_key, UncompressedPubKey::Rsa(k) if *k.n() == n.as_slice() && k.e() == e));
+ Ok(())
+ }
+ #[test]
+ #[cfg(feature = "custom")]
+ fn rsa_auth() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ let mut opts = PublicKeyCredentialRequestOptions::passkey(&rp_id);
+ opts.challenge = Challenge(0);
+ let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec();
+ // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information.
+ let mut authenticator_data = Vec::with_capacity(69);
+ authenticator_data.extend_from_slice(
+ [
+ // rpIdHash.
+ // This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // flags.
+ // UP and UV (right-to-left).
+ 0b0000_0101,
+ // signCount.
+ // 0 as 32-bit big-endian.
+ 0,
+ 0,
+ 0,
+ 0,
+ ]
+ .as_slice(),
+ );
+ authenticator_data[..32]
+ .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice());
+ authenticator_data
+ .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
+ let n = [
+ 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107,
+ 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206,
+ 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165,
+ 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204,
+ 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64,
+ 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105,
+ 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55,
+ 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21,
+ 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157,
+ 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188,
+ 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56,
+ 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13,
+ 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132,
+ 153, 79, 0, 133, 78, 7, 218, 165, 241,
+ ];
+ let e = 65537;
+ let d = [
+ 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0,
+ 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168,
+ 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108,
+ 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102,
+ 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247,
+ 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166,
+ 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111,
+ 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242,
+ 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212,
+ 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83,
+ 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153,
+ 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142,
+ 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45,
+ 50, 23, 3, 25, 253, 87, 227, 79, 119, 161,
+ ];
+ let p = BigUint::from_slice(
+ [
+ 352691927, 1294578443, 816143558, 690659917, 1161596366, 1544791087, 3999549486,
+ 3319149924, 2349250979, 1304689381, 3959753736, 3377900978, 866506027, 1671521644,
+ 3926847564, 898221388, 3448219846, 494454484, 3915534864, 2869735916, 2456511629,
+ 3397234721, 3012775852, 3472309790, 1923617705, 2993441050, 3210302569, 3605331368,
+ 3352563766, 688081007, 4104512503, 4145593376,
+ ]
+ .as_slice(),
+ );
+ let p_2 = BigUint::from_slice(
+ [
+ 4039514409, 964284038, 3230008587, 3320139220, 3562360334, 3165876926, 212773653,
+ 2752465512, 2973674888, 1717425549, 2084262803, 3585031058, 4162394935, 1428626842,
+ 1015474994, 3283774155, 2840050110, 190639246, 147241978, 2994256073, 4081014755,
+ 3102401369, 3547397148, 1545029057, 895305733, 2689179461, 1593439337, 3960057302,
+ 193068804, 2835123424, 4054880057, 4200258364,
+ ]
+ .as_slice(),
+ );
+ let rsa_key = RsaKey::<Sha256>::new(
+ RsaPrivateKey::from_components(
+ BigUint::from_bytes_le(n.as_slice()),
+ e.into(),
+ BigUint::from_bytes_le(d.as_slice()),
+ vec![p, p_2],
+ )
+ .unwrap(),
+ );
+ let rsa_pub = rsa_key.verifying_key();
+ let sig = rsa_key.sign(authenticator_data.as_slice()).to_vec();
+ authenticator_data.truncate(37);
+ assert!(!opts.start_ceremony()?.0.verify(
+ &rp_id,
+ &Authentication {
+ raw_id: CredentialId::try_from(vec![0; 16])?,
+ response: AuthenticatorAssertion::new(
+ client_data_json,
+ authenticator_data,
+ sig,
+ Some(UserHandle::try_from(vec![0])?),
+ ),
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ &mut AuthenticatedCredential::new(
+ CredentialId::try_from([0; 16].as_slice())?,
+ UserHandle::try_from([0].as_slice())?,
+ StaticState {
+ credential_public_key: CompressedPubKey::<&[u8], &[u8], &[u8], _>::Rsa(
+ RsaPubKey::try_from((rsa_pub.as_ref().n().to_bytes_be(), e)).unwrap(),
+ ),
+ extensions: AuthenticatorExtensionOutputStaticState {
+ cred_protect: CredentialProtectionPolicy::None,
+ hmac_secret: None,
+ },
+ },
+ DynamicState {
+ user_verified: true,
+ backup: Backup::NotEligible,
+ sign_count: 0,
+ authenticator_attachment: AuthenticatorAttachment::None,
+ },
+ )?,
+ &AuthenticationVerificationOptions::<&str, &str>::default(),
+ )?);
+ Ok(())
+ }
+}
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -0,0 +1,1174 @@
+#[cfg(doc)]
+use super::{
+ super::response::{
+ auth::AuthenticatorData,
+ register::{DynamicState, StaticState},
+ Backup, CollectedClientData, Flag,
+ },
+ register::{self, PublicKeyCredentialCreationOptions},
+ AsciiDomain, DomainOrigin, Url,
+};
+use super::{
+ super::{
+ response::{
+ auth::{
+ error::{AuthCeremonyErr, ExtensionErr, OneOrTwo},
+ Authentication, AuthenticatorExtensionOutput, HmacSecret,
+ },
+ register::CompressedPubKey,
+ AuthenticatorAttachment,
+ },
+ AuthenticatedCredential,
+ },
+ auth::error::{RequestOptionsErr, SecondFactorErr},
+ BackupReq, Ceremony, CeremonyOptions, Challenge, CredentialId, Credentials, ExtensionReq, Hint,
+ Origin, PublicKeyCredentialDescriptor, RpId, SentChallenge, ServerState,
+ UserVerificationRequirement, THREE_HUNDRED_THOUSAND,
+};
+use core::{
+ borrow::Borrow,
+ cmp::Ordering,
+ hash::{Hash, Hasher},
+ num::{NonZeroU32, NonZeroU64},
+ time::Duration,
+};
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+use std::time::Instant;
+#[cfg(any(doc, feature = "serializable_server_state"))]
+use std::time::SystemTime;
+/// Contains error types.
+pub mod error;
+/// Contains functionality to serialize data to a client.
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+#[cfg(feature = "serde")]
+mod ser;
+/// Contains functionality to (de)serialize [`AuthenticationServerState`] to a data store.
+#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+#[cfg(feature = "serializable_server_state")]
+pub mod ser_server_state;
+/// Controls how [signature counter](https://www.w3.org/TR/webauthn-3/#signature-counter) is enforced.
+///
+/// Note that if the previous signature counter is positive and the new counter is not strictly greater, then the
+/// authenticator is likely a clone (i.e., there are at least two copies of the private key).
+#[derive(Clone, Copy, Debug, Default)]
+pub enum SignatureCounterEnforcement {
+ /// Fail the authentication ceremony if the counter is less than or equal to the previous value when the
+ /// previous value is positive.
+ #[default]
+ Fail,
+ /// When the counter is less than the previous value, don't fail and update the value.
+ ///
+ /// Note in the special case that the new signature counter is 0, [`DynamicState::sign_count`] _won't_
+ /// be updated since that would allow an attacker to permanently disable the counter.
+ Update,
+ /// When the counter is less than the previous value, don't fail but don't update the value.
+ Ignore,
+}
+impl SignatureCounterEnforcement {
+ /// Validates the signature counter based on `self`.
+ const fn validate(self, prev: u32, cur: u32) -> Result<u32, AuthCeremonyErr> {
+ if prev == 0 || cur > prev {
+ Ok(cur)
+ } else {
+ match self {
+ Self::Fail => Err(AuthCeremonyErr::SignatureCounter),
+ // When the new counter is `0`, we use the previous counter to avoid an attacker from
+ // being able to permanently disable it.
+ Self::Update => Ok(if cur == 0 { prev } else { cur }),
+ Self::Ignore => Ok(prev),
+ }
+ }
+ }
+}
+/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
+///
+/// This is only applicable if
+/// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
+/// is `true` when registering a new credential with [`register::Extension::prf`] and
+/// [`PublicKeyCredentialRequestOptions::user_verification`] is [`UserVerificationRequirement::Required`].
+///
+/// Unlike the spec, it is forbidden for
+/// [the decrypted outputs](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs) to be
+/// passed back in an effort to ensure sensitive data remains client-side. This means
+/// [`prf`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs) must not exist,
+/// be `null`, or be an
+/// [`AuthenticationExtensionsPRFOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs)
+/// such that [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) does not exist,
+/// is `null`, or is an
+/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues) such
+/// that [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is `null` and
+/// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) does not exist or is `null`.
+///
+/// For the owned analog, see [`PrfInputOwned`].
+///
+/// When relying on discoverable requests
+/// (i.e., [`PublicKeyCredentialRequestOptions::allow_credentials`] is empty),
+/// one will likely use a static PRF input for _all_ credentials since rolling over PRF inputs
+/// is not feasible. One uses this type for such a thing. In other words, `'a` will likely
+/// be `'static` and [`Self::second`] will likely be `None`.
+#[derive(Clone, Copy, Debug)]
+pub struct PrfInput<'a> {
+ /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first).
+ pub first: &'a [u8],
+ /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second).
+ pub second: Option<&'a [u8]>,
+ /// Response requirements.
+ pub ext_info: ExtensionReq,
+}
+/// Owned version of [`PrfInput`].
+///
+/// When relying on non-discoverable requests
+/// (i.e., [`PublicKeyCredentialRequestOptions::allow_credentials`] is non-empty),
+/// it's recommended to use credential-specific PRF inputs that are continuously rolled over.
+/// One uses this type for such a thing.
+#[derive(Debug)]
+pub struct PrfInputOwned {
+ /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first).
+ pub first: Vec<u8>,
+ /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second).
+ pub second: Option<Vec<u8>>,
+ /// Response requirements.
+ pub ext_info: ExtensionReq,
+}
+/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client.
+#[derive(Clone, Copy, Debug, Default)]
+pub struct Extension<'prf> {
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension).
+ ///
+ /// If both [`CredentialSpecificExtension::prf`] and this are [`Some`], then `CredentialSpecificExtension::prf`
+ /// takes priority.
+ pub prf: Option<PrfInput<'prf>>,
+}
+/// The [defined extensions](https://www.w3.org/TR/webauthn-3/#sctn-defined-extensions) to send to the client that
+/// are credential-specific which among other things implies a non-discoverable request.
+#[derive(Debug, Default)]
+pub struct CredentialSpecificExtension {
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension).
+ ///
+ /// If both [`Extension::prf`] and this are [`Some`], then this take priority.
+ pub prf: Option<PrfInputOwned>,
+}
+/// Registered credential used in
+/// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
+#[derive(Debug)]
+pub struct AllowedCredential {
+ /// The registered credential.
+ pub credential: PublicKeyCredentialDescriptor<Vec<u8>>,
+ /// Credential-specific extensions.
+ pub extension: CredentialSpecificExtension,
+}
+impl From<PublicKeyCredentialDescriptor<Vec<u8>>> for AllowedCredential {
+ #[inline]
+ fn from(credential: PublicKeyCredentialDescriptor<Vec<u8>>) -> Self {
+ Self {
+ credential,
+ extension: CredentialSpecificExtension::default(),
+ }
+ }
+}
+/// Queue of unique [`AllowedCredential`]s.
+#[derive(Debug, Default)]
+pub struct AllowedCredentials {
+ /// Allowed credentials.
+ creds: Vec<AllowedCredential>,
+ /// Number of `AllowedCredential`s that have PRF inputs.
+ ///
+ /// Useful to help serialization.
+ prf_count: usize,
+}
+impl Credentials for AllowedCredentials {
+ type Credential = AllowedCredential;
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{auth::AllowedCredentials, Credentials};
+ /// assert!(AllowedCredentials::with_capacity(1).as_ref().is_empty());
+ /// ```
+ #[inline]
+ #[must_use]
+ fn with_capacity(capacity: usize) -> Self {
+ Self {
+ creds: Vec::with_capacity(capacity),
+ prf_count: 0,
+ }
+ }
+ /// # Examples
+ ///
+ /// ```
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
+ /// # use webauthn_rp::{
+ /// # request::{auth::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials},
+ /// # response::{AuthTransports, CredentialId},
+ /// # };
+ /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
+ /// /// from the database.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
+ /// // ⋮
+ /// # AuthTransports::decode(32)
+ /// }
+ /// let mut creds = AllowedCredentials::with_capacity(1);
+ /// assert!(creds.as_ref().is_empty());
+ /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
+ /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
+ /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let transports = get_transports((&id).into())?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into()));
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let id_copy = CredentialId::try_from(vec![0; 16])?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let transports_2 = AuthTransports::NONE;
+ /// // Duplicate `CredentialId`s don't get added.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert!(!creds.push(
+ /// PublicKeyCredentialDescriptor {
+ /// id: id_copy,
+ /// transports: transports_2
+ /// }
+ /// .into()
+ /// ));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[expect(
+ clippy::arithmetic_side_effects,
+ reason = "comment explains how overflow is not possible"
+ )]
+ #[inline]
+ fn push(&mut self, cred: Self::Credential) -> bool {
+ self.creds
+ .iter()
+ .try_fold((), |(), c| {
+ if c.credential.id == cred.credential.id {
+ Err(())
+ } else {
+ Ok(())
+ }
+ })
+ .is_ok_and(|()| {
+ // This can't overflow since `self.creds.push` would `panic` since
+ // `self.prf_count <= self.creds.len()`.
+ self.prf_count += usize::from(cred.extension.prf.is_some());
+ self.creds.push(cred);
+ true
+ })
+ }
+ #[inline]
+ fn len(&self) -> usize {
+ self.creds.len()
+ }
+}
+impl AsRef<[AllowedCredential]> for AllowedCredentials {
+ #[inline]
+ fn as_ref(&self) -> &[AllowedCredential] {
+ self.creds.as_slice()
+ }
+}
+impl From<&AllowedCredentials> for Vec<CredInfo> {
+ #[inline]
+ fn from(value: &AllowedCredentials) -> Self {
+ let len = value.creds.len();
+ value
+ .creds
+ .iter()
+ .fold(Self::with_capacity(len), |mut creds, cred| {
+ creds.push(CredInfo {
+ id: cred.credential.id.clone(),
+ ext: (&cred.extension).into(),
+ });
+ creds
+ })
+ }
+}
+/// Helper that verifies the overlap of [`PublicKeyCredentialRequestOptions::start_ceremony`] and
+/// [`AuthenticationServerState::decode`].
+fn validate_options_helper(
+ ext: ServerExtensionInfo,
+ uv: UserVerificationRequirement,
+ creds: &[CredInfo],
+) -> Result<(), RequestOptionsErr> {
+ // If PRF is set, the user has to verify themselves.
+ ext.prf
+ .as_ref()
+ .map_or(Ok(()), |_| {
+ if matches!(uv, UserVerificationRequirement::Required) {
+ Ok(())
+ } else {
+ Err(RequestOptionsErr::PrfWithoutUserVerification)
+ }
+ })
+ .and_then(|()| {
+ creds.iter().try_fold((), |(), cred| {
+ // If PRF is set, the user has to verify themselves.
+ cred.ext.prf.as_ref().map_or(Ok(()), |_| {
+ if matches!(uv, UserVerificationRequirement::Required) {
+ Ok(())
+ } else {
+ Err(RequestOptionsErr::PrfWithoutUserVerification)
+ }
+ })
+ })
+ })
+}
+/// The [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions)
+/// to send to the client when authenticating a credential.
+///
+/// Upon saving the [`AuthenticationServerState`] returned from [`Self::start_ceremony`], one MUST send
+/// [`AuthenticationClientState`] to the client ASAP. After receiving the newly created [`Authentication`], it
+/// is validated using [`AuthenticationServerState::verify`].
+#[derive(Debug)]
+pub struct PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge).
+ pub challenge: Challenge,
+ /// [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout).
+ ///
+ /// Note we require a positive value despite the spec allowing an optional nonnegative value. This jives
+ /// with the fact that in-memory storage is required when `serializable_server_state` is not enabled
+ /// when authenticating credentials as no timeout would make out-of-memory (OOM) conditions more likely.
+ pub timeout: NonZeroU32,
+ /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-rpid).
+ ///
+ /// This MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] used when the credential was registered.
+ pub rp_id: &'rp_id RpId,
+ /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
+ pub allow_credentials: AllowedCredentials,
+ /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification).
+ pub user_verification: UserVerificationRequirement,
+ /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-hints).
+ pub hints: Hint,
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions).
+ pub extensions: Extension<'prf>,
+}
+impl<'rp_id, 'prf> PublicKeyCredentialRequestOptions<'rp_id, 'prf> {
+ /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to
+ /// [`UserVerificationRequirement::Required`] and [`Self::timeout`] set to 5 minutes,
+ ///
+ /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the
+ /// credential was registered.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId, UserVerificationRequirement};
+ /// assert!(matches!(
+ /// PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?)).user_verification,
+ /// UserVerificationRequirement::Required
+ /// ));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self {
+ Self {
+ challenge: Challenge::new(),
+ timeout: THREE_HUNDRED_THOUSAND,
+ rp_id,
+ allow_credentials: AllowedCredentials::new(),
+ user_verification: UserVerificationRequirement::Required,
+ hints: Hint::None,
+ extensions: Extension::default(),
+ }
+ }
+ /// Creates a `PublicKeyCredentialRequestOptions` with [`Self::user_verification`] set to
+ /// [`UserVerificationRequirement::Discouraged`] and [`Self::timeout`] set to 5 minutes.
+ ///
+ /// Note `rp_id` _must_ be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] when the
+ /// [`AllowedCredential`]s were registered.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`AllowedCredentials`] is empty.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
+ /// # use webauthn_rp::{
+ /// # request::{
+ /// # auth::{AllowedCredentials, PublicKeyCredentialRequestOptions},
+ /// # AsciiDomain, RpId, PublicKeyCredentialDescriptor, Credentials
+ /// # },
+ /// # response::{AuthTransports, CredentialId},
+ /// # };
+ /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
+ /// /// from the database.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
+ /// // ⋮
+ /// # AuthTransports::decode(32)
+ /// }
+ /// let mut creds = AllowedCredentials::with_capacity(1);
+ /// assert!(creds.as_ref().is_empty());
+ /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
+ /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
+ /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let transports = get_transports((&id).into())?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert!(creds.push(PublicKeyCredentialDescriptor { id, transports }.into()));
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(
+ /// PublicKeyCredentialRequestOptions::second_factor(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?), creds)?
+ /// .allow_credentials
+ /// .as_ref()
+ /// .len(),
+ /// 1
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ pub fn second_factor<'a: 'rp_id>(
+ rp_id: &'a RpId,
+ creds: AllowedCredentials,
+ ) -> Result<Self, SecondFactorErr> {
+ if creds.as_ref().is_empty() {
+ Err(SecondFactorErr)
+ } else {
+ let mut opts = Self::passkey(rp_id);
+ opts.allow_credentials = creds;
+ opts.user_verification = UserVerificationRequirement::Discouraged;
+ Ok(opts)
+ }
+ }
+ /// Begins the [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) consuming
+ /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `AuthenticationClientState` MUST be
+ /// sent ASAP. In order to complete authentication, the returned `AuthenticationServerState` MUST be saved so
+ /// that it can later be used to verify the credential assertion with [`AuthenticationServerState::verify`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` contains incompatible configuration.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # #[cfg(not(feature = "serializable_server_state"))]
+ /// # use std::time::Instant;
+ /// # #[cfg(not(feature = "serializable_server_state"))]
+ /// # use webauthn_rp::request::ServerState;
+ /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId};
+ /// # #[cfg(not(feature = "serializable_server_state"))]
+ /// assert!(
+ /// PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?))
+ /// .start_ceremony()?
+ /// .0
+ /// .expiration() > Instant::now()
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ pub fn start_ceremony(
+ self,
+ ) -> Result<
+ (
+ AuthenticationServerState,
+ AuthenticationClientState<'rp_id, 'prf>,
+ ),
+ RequestOptionsErr,
+ > {
+ let extensions = self.extensions.into();
+ let allow_credentials = Vec::from(&self.allow_credentials);
+ validate_options_helper(extensions, self.user_verification, &allow_credentials).and_then(
+ |()| {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let res = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let res = SystemTime::now();
+ res.checked_add(Duration::from_millis(NonZeroU64::from(self.timeout).get()))
+ .ok_or(RequestOptionsErr::InvalidTimeout)
+ .map(|expiration| {
+ (
+ AuthenticationServerState {
+ challenge: SentChallenge(self.challenge.0),
+ allow_credentials,
+ user_verification: self.user_verification,
+ extensions,
+ expiration,
+ },
+ AuthenticationClientState(self),
+ )
+ })
+ },
+ )
+ }
+}
+/// Container of a [`PublicKeyCredentialRequestOptions`] that has been used to start the authentication ceremony.
+/// This gets sent to the client ASAP.
+#[derive(Debug)]
+pub struct AuthenticationClientState<'rp_id, 'prf>(PublicKeyCredentialRequestOptions<'rp_id, 'prf>);
+impl AuthenticationClientState<'_, '_> {
+ /// Returns the `PublicKeyCredentialRequestOptions` that was used to start an authentication ceremony.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, RpId};
+ /// assert!(
+ /// PublicKeyCredentialRequestOptions::passkey(&RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?))
+ /// .start_ceremony()?
+ /// .1
+ /// .options()
+ /// .allow_credentials
+ /// .as_ref()
+ /// .is_empty()
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn options(&self) -> &PublicKeyCredentialRequestOptions<'_, '_> {
+ &self.0
+ }
+}
+/// `PrfInput` and `PrfInputOwned` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`]
+/// in an in-memory collection.
+#[derive(Clone, Copy, Debug)]
+enum ServerPrfInfo {
+ /// `PrfInput::second` was `None`.
+ One(ExtensionReq),
+ /// `PrfInput::second` was `Some`.
+ Two(ExtensionReq),
+}
+impl ServerPrfInfo {
+ /// Returns the `ExtensionReq` sent to the client.
+ const fn ext_info(self) -> ExtensionReq {
+ match self {
+ Self::One(info) | Self::Two(info) => info,
+ }
+ }
+ /// Validates `val` based on the passed arguments.
+ fn validate(
+ val: Option<Self>,
+ prf_capable: bool,
+ hmac: HmacSecret,
+ err_unsolicited: bool,
+ ) -> Result<(), ExtensionErr> {
+ match hmac {
+ HmacSecret::None => {
+ if prf_capable {
+ val.map_or(Ok(()), |input| {
+ if matches!(input.ext_info(), ExtensionReq::Allow) {
+ Ok(())
+ } else {
+ Err(ExtensionErr::MissingHmacSecret)
+ }
+ })
+ } else {
+ // We check if the PRF extension was requested on an incapable credential;
+ // if so, we error.
+ val.map_or(Ok(()), |_| Err(ExtensionErr::HmacSecretForPrfIncapableCred))
+ }
+ }
+ HmacSecret::One => {
+ if prf_capable {
+ val.map_or_else(
+ || {
+ if err_unsolicited {
+ Err(ExtensionErr::ForbiddenHmacSecret)
+ } else {
+ Ok(())
+ }
+ },
+ |input| match input {
+ Self::One(_) => Ok(()),
+ Self::Two(_) => Err(ExtensionErr::InvalidHmacSecretValue(
+ OneOrTwo::Two,
+ OneOrTwo::One,
+ )),
+ },
+ )
+ } else {
+ Err(ExtensionErr::HmacSecretForPrfIncapableCred)
+ }
+ }
+ HmacSecret::Two => {
+ if prf_capable {
+ val.map_or_else(
+ || {
+ if err_unsolicited {
+ Err(ExtensionErr::ForbiddenHmacSecret)
+ } else {
+ Ok(())
+ }
+ },
+ |input| match input {
+ Self::One(_) => Err(ExtensionErr::InvalidHmacSecretValue(
+ OneOrTwo::One,
+ OneOrTwo::Two,
+ )),
+ Self::Two(_) => Ok(()),
+ },
+ )
+ } else {
+ Err(ExtensionErr::HmacSecretForPrfIncapableCred)
+ }
+ }
+ }
+ }
+}
+impl From<PrfInput<'_>> for ServerPrfInfo {
+ fn from(value: PrfInput<'_>) -> Self {
+ value
+ .second
+ .map_or_else(|| Self::One(value.ext_info), |_| Self::Two(value.ext_info))
+ }
+}
+impl From<&PrfInputOwned> for ServerPrfInfo {
+ fn from(value: &PrfInputOwned) -> Self {
+ value
+ .second
+ .as_ref()
+ .map_or_else(|| Self::One(value.ext_info), |_| Self::Two(value.ext_info))
+ }
+}
+/// `Extension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`]
+/// in an in-memory collection.
+#[derive(Clone, Copy, Debug)]
+struct ServerExtensionInfo {
+ /// `Extension::prf`.
+ prf: Option<ServerPrfInfo>,
+}
+impl From<Extension<'_>> for ServerExtensionInfo {
+ fn from(value: Extension<'_>) -> Self {
+ Self {
+ prf: value.prf.map(ServerPrfInfo::from),
+ }
+ }
+}
+/// `CredentialSpecificExtension` without the actual data sent to reduce memory usage when storing [`AuthenticationServerState`]
+/// in an in-memory collection.
+#[derive(Clone, Copy, Debug)]
+struct ServerCredSpecificExtensionInfo {
+ /// `CredentialSpecificExtension::prf`.
+ prf: Option<ServerPrfInfo>,
+}
+impl From<&CredentialSpecificExtension> for ServerCredSpecificExtensionInfo {
+ fn from(value: &CredentialSpecificExtension) -> Self {
+ Self {
+ prf: value.prf.as_ref().map(ServerPrfInfo::from),
+ }
+ }
+}
+impl ServerExtensionInfo {
+ /// Validates the extensions.
+ ///
+ /// Note that this MUST only be called internally by `auth::validate_extensions`.
+ fn validate_extensions(
+ self,
+ auth_ext: AuthenticatorExtensionOutput,
+ error_unsolicited: bool,
+ prf_capable: bool,
+ ) -> Result<(), ExtensionErr> {
+ ServerPrfInfo::validate(
+ self.prf,
+ prf_capable,
+ auth_ext.hmac_secret,
+ error_unsolicited,
+ )
+ }
+}
+/// Validates the extensions.
+fn validate_extensions(
+ ext: ServerExtensionInfo,
+ cred_ext: Option<ServerCredSpecificExtensionInfo>,
+ auth_ext: AuthenticatorExtensionOutput,
+ error_unsolicited: bool,
+ prf_capable: bool,
+) -> Result<(), ExtensionErr> {
+ cred_ext.map_or_else(
+ || {
+ // No client-specific extensions, so we can simply focus on `ext`.
+ ext.validate_extensions(auth_ext, error_unsolicited, prf_capable)
+ },
+ |c_ext| {
+ // Must carefully process each extension based on overlap and which gets priority over the other.
+ c_ext.prf.as_ref().map_or_else(
+ || {
+ ServerPrfInfo::validate(
+ ext.prf,
+ prf_capable,
+ auth_ext.hmac_secret,
+ error_unsolicited,
+ )
+ },
+ |_| {
+ ServerPrfInfo::validate(
+ c_ext.prf,
+ prf_capable,
+ auth_ext.hmac_secret,
+ error_unsolicited,
+ )
+ },
+ )
+ },
+ )
+}
+/// [`AllowedCredential`] with less data to reduce memory usage when storing [`AuthenticationServerState`]
+/// in an in-memory collection.
+#[derive(Debug)]
+struct CredInfo {
+ /// The Credential ID.
+ id: CredentialId<Vec<u8>>,
+ /// Any credential-specific extensions.
+ ext: ServerCredSpecificExtensionInfo,
+}
+/// Controls how to handle a change in [`DynamicState::authenticator_attachment`].
+///
+/// Note when `DynamicState::authenticator_attachment` is [`AuthenticatorAttachment::None`], then it will
+/// be updated regardless. Similarly when [`Authentication::authenticator_attachment`] is
+/// `AuthenticatorAttachment::None`, it will never update `DynamicState::authenticator_attachment`.
+#[derive(Clone, Copy, Debug)]
+pub enum AuthenticatorAttachmentEnforcement {
+ /// Fail the authentication ceremony if [`AuthenticatorAttachment`] is not the same.
+ ///
+ /// The contained `bool` represents if `AuthenticatorAttachment` must be sent.
+ Fail(bool),
+ /// Update [`DynamicState::authenticator_attachment`] to the sent [`AuthenticatorAttachment`].
+ ///
+ /// The contained `bool` represents if `AuthenticatorAttachment` must be sent.
+ Update(bool),
+ /// Do not update [`DynamicState::authenticator_attachment`] to the sent [`AuthenticatorAttachment`].
+ ///
+ /// The contained `bool` represents if `AuthenticatorAttachment` must be sent.
+ Ignore(bool),
+}
+impl AuthenticatorAttachmentEnforcement {
+ /// Validates `cur` based on `self` and `prev`.
+ const fn validate(
+ self,
+ prev: AuthenticatorAttachment,
+ cur: AuthenticatorAttachment,
+ ) -> Result<AuthenticatorAttachment, AuthCeremonyErr> {
+ match cur {
+ AuthenticatorAttachment::None => match self {
+ Self::Fail(require) | Self::Update(require) | Self::Ignore(require) => {
+ if require {
+ Err(AuthCeremonyErr::MissingAuthenticatorAttachment)
+ } else {
+ // We don't overwrite the previous one with [`AuthenticatorAttachment::None`].
+ Ok(prev)
+ }
+ }
+ },
+ AuthenticatorAttachment::Platform => match self {
+ Self::Fail(_) => {
+ if matches!(prev, AuthenticatorAttachment::CrossPlatform) {
+ Err(AuthCeremonyErr::AuthenticatorAttachmentMismatch)
+ } else {
+ // We don't fail when we previously had [`AuthenticatorAttachment::None`].
+ Ok(cur)
+ }
+ }
+ Self::Update(_) => Ok(cur),
+ Self::Ignore(_) => {
+ if matches!(prev, AuthenticatorAttachment::None) {
+ // We overwrite the previous one when it is [`AuthenticatorAttachment::None`].
+ Ok(cur)
+ } else {
+ Ok(prev)
+ }
+ }
+ },
+ AuthenticatorAttachment::CrossPlatform => match self {
+ Self::Fail(_) => {
+ if matches!(prev, AuthenticatorAttachment::Platform) {
+ Err(AuthCeremonyErr::AuthenticatorAttachmentMismatch)
+ } else {
+ // We don't fail when we previously had [`AuthenticatorAttachment::None`].
+ Ok(cur)
+ }
+ }
+ Self::Update(_) => Ok(cur),
+ Self::Ignore(_) => {
+ if matches!(prev, AuthenticatorAttachment::None) {
+ // We overwrite the previous one when it is [`AuthenticatorAttachment::None`].
+ Ok(cur)
+ } else {
+ Ok(prev)
+ }
+ }
+ },
+ }
+ }
+}
+impl Default for AuthenticatorAttachmentEnforcement {
+ /// Returns [`Self::Ignore`] containing `false`.
+ #[inline]
+ fn default() -> Self {
+ Self::Ignore(false)
+ }
+}
+/// Additional verification options to perform in [`AuthenticationServerState::verify`].
+#[derive(Clone, Copy, Debug)]
+pub struct AuthenticationVerificationOptions<'origins, 'top_origins, O, T> {
+ /// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
+ ///
+ /// When this is empty, the origin that will be used will be based on
+ /// the [`RpId`] passed to [`AuthenticationServerState::verify`]. If [`RpId::Domain`], then the [`DomainOrigin`] returned from
+ /// passing [`AsciiDomain::as_ref`] to [`DomainOrigin::new`] will be used; otherwise the [`Url`] in
+ /// [`RpId::Url`] will be used.
+ pub allowed_origins: &'origins [O],
+ /// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin)
+ /// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
+ ///
+ /// When this is `Some`, [`CollectedClientData::cross_origin`] is allowed to be `true`. When the contained
+ /// `slice` is empty, [`CollectedClientData::top_origin`] must be `None`. When this is `None`,
+ /// `CollectedClientData::cross_origin` must be `false` and `CollectedClientData::top_origin` must be `None`.
+ pub allowed_top_origins: Option<&'top_origins [T]>,
+ /// The required [`Backup`] state of the credential.
+ ///
+ /// Note that `None` is _not_ the same as `Some(BackupReq::None)` as the latter indicates that any [`Backup`]
+ /// is allowed. This is rarely what you want; instead, `None` indicates that [`BackupReq::from`] applied to
+ /// [`DynamicState::backup`] will be used in [`AuthenticationServerState::verify`].
+ pub backup_requirement: Option<BackupReq>,
+ /// Error when unsolicited extensions are sent back iff `true`.
+ pub error_on_unsolicited_extensions: bool,
+ /// Dictates what happens when [`Authentication::authenticator_attachment`] is not the same as
+ /// [`DynamicState::authenticator_attachment`].
+ pub auth_attachment_enforcement: AuthenticatorAttachmentEnforcement,
+ /// [`DynamicState::user_verified`] will be set to `true` iff [`Flag::user_verified`] when `true`.
+ pub update_uv: bool,
+ /// Dictates what happens when [`AuthenticatorData::sign_count`] is not updated to a strictly greater value.
+ pub sig_counter_enforcement: SignatureCounterEnforcement,
+ /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ pub client_data_json_relaxed: bool,
+}
+impl<O, T> Default for AuthenticationVerificationOptions<'_, '_, O, T> {
+ /// Returns `Self` such that [`Self::allowed_origins`] is empty, [`Self::allowed_top_origins`] is `None`,
+ /// [`Self::backup_requirement`] is `None`, [`Self::error_on_unsolicited_extensions`] is `true`,
+ /// [`Self::auth_attachment_enforcement`] is [`AuthenticatorAttachmentEnforcement::default`],
+ /// [`Self::update_uv`] is `false`, [`Self::sig_counter_enforcement`] is
+ /// [`SignatureCounterEnforcement::Fail`], and [`Self::client_data_json_relaxed`] is `true`.
+ #[inline]
+ fn default() -> Self {
+ Self {
+ allowed_origins: &[],
+ allowed_top_origins: None,
+ backup_requirement: None,
+ error_on_unsolicited_extensions: true,
+ auth_attachment_enforcement: AuthenticatorAttachmentEnforcement::default(),
+ update_uv: false,
+ sig_counter_enforcement: SignatureCounterEnforcement::default(),
+ #[cfg(feature = "serde_relaxed")]
+ client_data_json_relaxed: true,
+ }
+ }
+}
+// This is essentially the `PublicKeyCredentialRequestOptions` used to create it; however to reduce
+// memory usage, we remove all unnecessary data making an instance of this 64 bytes in size when
+// `Self::allow_credentials` is empty on `x86_64-unknown-linux-gnu` platforms.
+//
+// The total memory used is dependent on the number of `AllowedCredential`s and the size of each `CredentialId`.
+// To be exact, it is the following:
+// 64 + i(32 + 56n + Σj_k from k=0 to k=m-1) where i is 0 iff `AllowedCredentials` has capacity 0; otherwise 1,
+// n is `AllowedCredentials` capacity, j_k is the kth `CredentialId` in `AllowedCredentials` and `m` is
+// `AllowedCredentials::len`.
+/// State needed to be saved when beginning the authentication ceremony.
+///
+/// Saves the necessary information associated with the [`PublicKeyCredentialRequestOptions`] used to create it
+/// via [`PublicKeyCredentialRequestOptions::start_ceremony`] so that authentication of a credential can be
+/// performed with [`Self::verify`].
+///
+/// `AuthenticationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the correct
+/// `AuthenticationServerState` associated with an [`Authentication`], one should use its corresponding
+/// [`Authentication::challenge`].
+#[derive(Debug)]
+pub struct AuthenticationServerState {
+ // This is a `SentChallenge` since we need `AuthenticationServerState` to be fetchable after receiving the
+ // response from the client. This response must obviously be constructable; thus its challenge is a
+ // `SentChallenge`.
+ //
+ // This must never be mutated since we want to ensure it is actually a `Challenge` (which
+ // can only be constructed via `Challenge::new`). This is guaranteed to be true iff
+ // `serializable_server_state` is not enabled.
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge).
+ challenge: SentChallenge,
+ /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials).
+ allow_credentials: Vec<CredInfo>,
+ /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification).
+ user_verification: UserVerificationRequirement,
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions).
+ extensions: ServerExtensionInfo,
+ /// `Instant` the ceremony expires.
+ #[cfg(not(feature = "serializable_server_state"))]
+ expiration: Instant,
+ /// `SystemTime` the ceremony expires.
+ #[cfg(feature = "serializable_server_state")]
+ expiration: SystemTime,
+}
+impl AuthenticationServerState {
+ /// Verifies `response` is valid based on `self` consuming `self` and updating `cred`. Returns `true`
+ /// iff `cred` was mutated.
+ ///
+ /// `rp_id` MUST be the same as the [`PublicKeyCredentialRequestOptions::rp_id`] used when starting the
+ /// ceremony.
+ ///
+ /// It is _essential_ to save [`AuthenticatedCredential::dynamic_state`] overwriting the original value iff `Ok(true)`
+ /// is returned.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `response` is not valid according to the
+ /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) or violates any
+ /// of the settings in `options`.
+ #[inline]
+ pub fn verify<
+ 'a,
+ 'user,
+ O: PartialEq<Origin<'a>>,
+ T: PartialEq<Origin<'a>>,
+ EdKey: AsRef<[u8]>,
+ P256Key: AsRef<[u8]>,
+ P384Key: AsRef<[u8]>,
+ RsaKey: AsRef<[u8]>,
+ >(
+ self,
+ rp_id: &RpId,
+ response: &'a Authentication,
+ cred: &mut AuthenticatedCredential<
+ 'a,
+ 'user,
+ CompressedPubKey<EdKey, P256Key, P384Key, RsaKey>,
+ >,
+ options: &AuthenticationVerificationOptions<'_, '_, O, T>,
+ ) -> Result<bool, AuthCeremonyErr> {
+ // [Authentication ceremony](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion)
+ // is handled by:
+ //
+ // 1. Calling code.
+ // 2. Client code and the construction of `resp` (hopefully via [`Authentication::deserialize`]).
+ // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAssertion::deserialize`]).
+ // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]).
+ // 5. Below.
+ // 6. Below.
+ // 7. Informative only in that it defines variables.
+ // 8. [`Self::partial_validate`].
+ // 9. [`Self::partial_validate`].
+ // 10. [`Self::partial_validate`].
+ // 11. [`Self::partial_validate`].
+ // 12. [`Self::partial_validate`].
+ // 13. [`Self::partial_validate`].
+ // 14. [`Self::partial_validate`].
+ // 15. [`Self::partial_validate`].
+ // 16. [`Self::partial_validate`].
+ // 17. [`Self::partial_validate`].
+ // 18. [`Self::partial_validate`].
+ // 19. [`Self::partial_validate`].
+ // 20. [`Self::partial_validate`].
+ // 21. [`Self::partial_validate`].
+ // 22. Below.
+ // 23. Below.
+ // 24. Below.
+ // 25. Below.
+
+ if self.allow_credentials.is_empty() {
+ // Step 6.
+ response
+ .response
+ .user_handle()
+ .as_ref()
+ .ok_or(AuthCeremonyErr::MissingUserHandle)
+ .and_then(|user| {
+ if cred.user_id() == user {
+ if cred.id == response.raw_id {
+ Ok(None)
+ } else {
+ Err(AuthCeremonyErr::CredentialIdMismatch)
+ }
+ } else {
+ Err(AuthCeremonyErr::UserHandleMismatch)
+ }
+ })
+ } else {
+ // Steps 5–6.
+ self.verify_nondiscoverable(response, cred)
+ }
+ .and_then(|ext| {
+ // Steps 8–21.
+ self.partial_validate(
+ rp_id,
+ response,
+ (&cred.static_state.credential_public_key).into(),
+ &CeremonyOptions {
+ allowed_origins: options.allowed_origins,
+ allowed_top_origins: options.allowed_top_origins,
+ backup_requirement: options
+ .backup_requirement
+ .unwrap_or_else(|| BackupReq::from(cred.dynamic_state.backup)),
+ #[cfg(feature = "serde_relaxed")]
+ client_data_json_relaxed: options.client_data_json_relaxed,
+ },
+ )
+ .map_err(AuthCeremonyErr::from)
+ .and_then(|auth_data| {
+ options
+ .auth_attachment_enforcement
+ .validate(
+ cred.dynamic_state.authenticator_attachment,
+ response.authenticator_attachment,
+ )
+ .and_then(|auth_attachment| {
+ // Step 23.
+ validate_extensions(
+ self.extensions,
+ ext,
+ auth_data.extensions(),
+ options.error_on_unsolicited_extensions,
+ cred.static_state.extensions.hmac_secret.unwrap_or_default(),
+ )
+ .map_err(AuthCeremonyErr::Extension)
+ .and_then(|()| {
+ // Step 22.
+ options
+ .sig_counter_enforcement
+ .validate(cred.dynamic_state.sign_count, auth_data.sign_count())
+ .and_then(|sig_counter| {
+ let flags = auth_data.flags();
+ let prev_dyn_state = cred.dynamic_state;
+ // Step 24 item 2.
+ cred.dynamic_state.backup = flags.backup;
+ if options.update_uv && flags.user_verified {
+ // Step 24 item 3.
+ cred.dynamic_state.user_verified = true;
+ }
+ // Step 24 item 1.
+ cred.dynamic_state.sign_count = sig_counter;
+ cred.dynamic_state.authenticator_attachment = auth_attachment;
+ // Step 25.
+ crate::verify_static_and_dynamic_state(
+ &cred.static_state,
+ cred.dynamic_state,
+ )
+ .map_err(|e| {
+ cred.dynamic_state = prev_dyn_state;
+ AuthCeremonyErr::Credential(e)
+ })
+ .map(|()| prev_dyn_state != cred.dynamic_state)
+ })
+ })
+ })
+ })
+ })
+ }
+ /// Retrieves the corresponding [`CredInfo`] used for a non-discoverable request that corresponds to
+ /// `response`. Since this is a non-discoverable request, one must have an external way of identifying the
+ /// `UserHandle`.
+ ///
+ /// This MUST be called iff a non-discoverable request was sent to the client (e.g.,
+ /// [`PublicKeyCredentialRequestOptions::second_factor`]).
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`AuthenticatedCredential::user_handle`] does not match [`Authentication::user_handle`] or
+ /// [`PublicKeyCredentialRequestOptions::allow_credentials`] does not have a [`CredInfo`] such that
+ /// [`CredInfo::id`] matches [`Authentication::raw_id`].
+ fn verify_nondiscoverable<'a, PublicKey>(
+ &self,
+ response: &'a Authentication,
+ cred: &AuthenticatedCredential<'a, '_, PublicKey>,
+ ) -> Result<Option<ServerCredSpecificExtensionInfo>, AuthCeremonyErr> {
+ response
+ .response
+ .user_handle()
+ .as_ref()
+ .map_or(Ok(()), |user| {
+ if user == cred.user_id() {
+ Ok(())
+ } else {
+ Err(AuthCeremonyErr::UserHandleMismatch)
+ }
+ })
+ .and_then(|()| {
+ self.allow_credentials
+ .iter()
+ .find(|c| c.id == response.raw_id)
+ .ok_or(AuthCeremonyErr::NoMatchingAllowedCredential)
+ .map(|c| Some(c.ext))
+ })
+ }
+}
+impl ServerState for AuthenticationServerState {
+ #[cfg(any(doc, not(feature = "serializable_server_state")))]
+ #[inline]
+ fn expiration(&self) -> Instant {
+ self.expiration
+ }
+ #[cfg(all(not(doc), feature = "serializable_server_state"))]
+ #[inline]
+ fn expiration(&self) -> SystemTime {
+ self.expiration
+ }
+ #[inline]
+ fn sent_challenge(&self) -> SentChallenge {
+ self.challenge
+ }
+}
+impl Ceremony for AuthenticationServerState {
+ type R = Authentication;
+ fn rand_challenge(&self) -> SentChallenge {
+ self.challenge
+ }
+ #[cfg(not(feature = "serializable_server_state"))]
+ fn expiry(&self) -> Instant {
+ self.expiration
+ }
+ #[cfg(feature = "serializable_server_state")]
+ fn expiry(&self) -> SystemTime {
+ self.expiration
+ }
+ fn user_verification(&self) -> UserVerificationRequirement {
+ self.user_verification
+ }
+}
+impl Borrow<SentChallenge> for AuthenticationServerState {
+ #[inline]
+ fn borrow(&self) -> &SentChallenge {
+ &self.challenge
+ }
+}
+impl PartialEq for AuthenticationServerState {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.challenge == other.challenge
+ }
+}
+impl PartialEq<&Self> for AuthenticationServerState {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<AuthenticationServerState> for &AuthenticationServerState {
+ #[inline]
+ fn eq(&self, other: &AuthenticationServerState) -> bool {
+ **self == *other
+ }
+}
+impl Eq for AuthenticationServerState {}
+impl Hash for AuthenticationServerState {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.challenge.hash(state);
+ }
+}
+impl PartialOrd for AuthenticationServerState {
+ #[inline]
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+impl Ord for AuthenticationServerState {
+ #[inline]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.challenge.cmp(&other.challenge)
+ }
+}
diff --git a/src/request/auth/error.rs b/src/request/auth/error.rs
@@ -0,0 +1,44 @@
+#[cfg(doc)]
+use super::{
+ AllowedCredentials, CredentialSpecificExtension, Extension, PublicKeyCredentialRequestOptions,
+ UserVerificationRequirement,
+};
+use core::{
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+#[cfg(doc)]
+use std::time::{Instant, SystemTime};
+/// Error returned from [`PublicKeyCredentialRequestOptions::second_factor`] when
+/// [`AllowedCredentials`] is empty.
+#[derive(Clone, Copy, Debug)]
+pub struct SecondFactorErr;
+impl Display for SecondFactorErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("allowed credentials was empty")
+ }
+}
+impl Error for SecondFactorErr {}
+/// Error returned by [`PublicKeyCredentialRequestOptions::start_ceremony`].
+#[derive(Clone, Copy, Debug)]
+pub enum RequestOptionsErr {
+ /// Error when [`Extension::prf`] or [`CredentialSpecificExtension::prf`] is [`Some`] but
+ /// [`PublicKeyCredentialRequestOptions::user_verification`] is not
+ /// [`UserVerificationRequirement::Required`].
+ PrfWithoutUserVerification,
+ /// [`PublicKeyCredentialRequestOptions::timeout`] could not be added to [`Instant::now`] or [`SystemTime::now`].
+ InvalidTimeout,
+}
+impl Display for RequestOptionsErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::PrfWithoutUserVerification => {
+ "prf extension was requested without requiring user verification"
+ }
+ Self::InvalidTimeout => "the timeout could not be added to the current Instant",
+ })
+ }
+}
+impl Error for RequestOptionsErr {}
diff --git a/src/request/auth/ser.rs b/src/request/auth/ser.rs
@@ -0,0 +1,399 @@
+use super::{
+ super::super::BASE64URL_NOPAD_ENC, AllowedCredential, AllowedCredentials,
+ AuthenticationClientState, Extension, PrfInput, PrfInputOwned,
+};
+use serde::ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer};
+impl Serialize for PrfInput<'_> {
+ /// Serializes `self` to conform with
+ /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{auth::PrfInput, ExtensionReq};
+ /// assert_eq!(
+ /// serde_json::to_string(&PrfInput {
+ /// first: [0; 4].as_slice(),
+ /// second: Some([2; 1].as_slice()),
+ /// ext_info: ExtensionReq::Require
+ /// })?,
+ /// r#"{"first":"AAAAAA","second":"Ag"}"#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[expect(
+ clippy::arithmetic_side_effects,
+ reason = "comment justifies how overflow is not possible"
+ )]
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ // The max value is 1 + 1 = 2, so overflow is not an issue.
+ .serialize_struct("PrfInput", 1 + usize::from(self.second.is_some()))
+ .and_then(|mut ser| {
+ ser.serialize_field("first", BASE64URL_NOPAD_ENC.encode(self.first).as_str())
+ .and_then(|()| {
+ self.second
+ .as_ref()
+ .map_or(Ok(()), |second| {
+ ser.serialize_field(
+ "second",
+ BASE64URL_NOPAD_ENC.encode(second).as_str(),
+ )
+ })
+ .and_then(|()| ser.end())
+ })
+ })
+ }
+}
+impl Serialize for PrfInputOwned {
+ /// See [`PrfInput::serialize`]
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ PrfInput {
+ first: self.first.as_slice(),
+ second: self.second.as_deref(),
+ ext_info: self.ext_info,
+ }
+ .serialize(serializer)
+ }
+}
+impl Serialize for AllowedCredential {
+ /// Serializes `self` to conform with
+ /// [`PublicKeyCredentialDescriptorJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptorjson).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
+ /// # use webauthn_rp::{
+ /// # request::{auth::AllowedCredential, PublicKeyCredentialDescriptor},
+ /// # response::{AuthTransports, CredentialId},
+ /// # };
+ /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
+ /// /// from the database.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
+ /// // ⋮
+ /// # AuthTransports::decode(32)
+ /// }
+ /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
+ /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
+ /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let transports = get_transports((&id).into())?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(
+ /// serde_json::to_string(&AllowedCredential::from(PublicKeyCredentialDescriptor {
+ /// id,
+ /// transports
+ /// })).unwrap(),
+ /// r#"{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}"#
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ self.credential.serialize(serializer)
+ }
+}
+impl Serialize for AllowedCredentials {
+ /// Serializes `self` to conform with
+ /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-allowcredentials).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
+ /// # use webauthn_rp::{
+ /// # request::{auth::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials},
+ /// # response::{AuthTransports, CredentialId},
+ /// # };
+ /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
+ /// /// from the database.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
+ /// // ⋮
+ /// # AuthTransports::decode(32)
+ /// }
+ /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
+ /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
+ /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let transports = get_transports((&id).into())?;
+ /// let mut creds = AllowedCredentials::with_capacity(1);
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// creds.push(PublicKeyCredentialDescriptor { id, transports }.into());
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(
+ /// serde_json::to_string(&creds).unwrap(),
+ /// r#"[{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}]"#
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ self.creds.serialize(serializer)
+ }
+}
+/// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential).
+struct PrfCreds<'a>(&'a AllowedCredentials);
+impl Serialize for PrfCreds<'_> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_map(Some(self.0.prf_count))
+ .and_then(|mut ser| {
+ self.0
+ .creds
+ .iter()
+ .try_fold((), |(), cred| {
+ cred.extension.prf.as_ref().map_or(Ok(()), |input| {
+ ser.serialize_entry(&cred.credential.id, input)
+ })
+ })
+ .and_then(|()| ser.end())
+ })
+ }
+}
+/// [`AuthenticationExtensionsPRFInputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfinputs).
+struct PrfInputs<'a, 'b> {
+ /// [`eval`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-eval).
+ eval: Option<PrfInput<'a>>,
+ /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential).
+ eval_by_credential: PrfCreds<'b>,
+}
+impl Serialize for PrfInputs<'_, '_> {
+ #[expect(
+ clippy::arithmetic_side_effects,
+ reason = "comment explains how overflow is not possible"
+ )]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct(
+ "PrfInputs",
+ // The max is 1 + 1 = 2, so overflow is not an issue.
+ usize::from(self.eval.is_some())
+ + usize::from(self.eval_by_credential.0.prf_count > 0),
+ )
+ .and_then(|mut ser| {
+ self.eval
+ .map_or(Ok(()), |eval| ser.serialize_field("eval", &eval))
+ .and_then(|()| {
+ if self.eval_by_credential.0.prf_count == 0 {
+ Ok(())
+ } else {
+ ser.serialize_field("evalByCredential", &self.eval_by_credential)
+ }
+ })
+ .and_then(|()| ser.end())
+ })
+ }
+}
+/// Serializes `self` to conform with
+/// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson).
+struct ExtensionHelper<'a, 'b> {
+ /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions).
+ extension: &'a Extension<'b>,
+ /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions).
+ ///
+ /// Some extensions contain records, so we need both this and above.
+ allow_credentials: &'a AllowedCredentials,
+}
+impl Serialize for ExtensionHelper<'_, '_> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let ext_count =
+ usize::from(self.extension.prf.is_some() || self.allow_credentials.prf_count > 0);
+ serializer
+ .serialize_struct("Extension", ext_count)
+ .and_then(|mut ser| {
+ if ext_count == 0 {
+ Ok(())
+ } else {
+ ser.serialize_field(
+ "prf",
+ &PrfInputs {
+ eval: self.extension.prf,
+ eval_by_credential: PrfCreds(self.allow_credentials),
+ },
+ )
+ }
+ .and_then(|()| ser.end())
+ })
+ }
+}
+impl Serialize for AuthenticationClientState<'_, '_> {
+ /// Serializes `self` to conform with
+ /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
+ /// # use webauthn_rp::{
+ /// # request::{
+ /// # auth::{
+ /// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension,
+ /// # PrfInput, PrfInputOwned, PublicKeyCredentialRequestOptions
+ /// # },
+ /// # AsciiDomain, ExtensionReq, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
+ /// # },
+ /// # response::{AuthTransports, CredentialId},
+ /// # };
+ /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
+ /// /// from the database.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
+ /// // ⋮
+ /// # AuthTransports::decode(32)
+ /// }
+ /// let mut creds = AllowedCredentials::with_capacity(1);
+ /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
+ /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
+ /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let transports = get_transports((&id).into())?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// creds.push(AllowedCredential {
+ /// credential: PublicKeyCredentialDescriptor { id, transports },
+ /// extension: CredentialSpecificExtension {
+ /// prf: Some(PrfInputOwned {
+ /// first: vec![2; 6],
+ /// second: Some(vec![3; 2]),
+ /// ext_info: ExtensionReq::Require,
+ /// }),
+ /// },
+ /// });
+ /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let mut options = PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?;
+ /// # #[cfg(not(all(feature = "bin", feature = "custom")))]
+ /// # let mut options = PublicKeyCredentialRequestOptions::passkey(&rp_id);
+ /// options.hints = Hint::SecurityKey;
+ /// // This is actually useless since `CredentialSpecificExtension` takes priority
+ /// // when the client receives the payload. We set it for illustration purposes only.
+ /// // If `creds` contained an `AllowedCredential` that didn't set
+ /// // `CredentialSpecificExtension::prf`, then this would be used for it.
+ /// options.extensions = Extension {
+ /// prf: Some(PrfInput {
+ /// first: [0; 4].as_slice(),
+ /// second: None,
+ /// ext_info: ExtensionReq::Require,
+ /// }),
+ /// };
+ /// // Since we are requesting the PRF extension, we must require user verification; otherwise
+ /// // `PublicKeyCredentialRequestOptions::start_ceremony` would error.
+ /// options.user_verification = UserVerificationRequirement::Required;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap();
+ /// let json = serde_json::json!({
+ /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA",
+ /// "timeout":300000,
+ /// "rpId":"example.com",
+ /// "allowCredentials":[
+ /// {
+ /// "type":"public-key",
+ /// "id":"AAAAAAAAAAAAAAAAAAAAAA",
+ /// "transports":["usb"]
+ /// }
+ /// ],
+ /// "userVerification":"required",
+ /// "hints":[
+ /// "security-key"
+ /// ],
+ /// "extensions":{
+ /// "prf":{
+ /// "eval":{
+ /// "first":"AAAAAA"
+ /// },
+ /// "evalByCredential":{
+ /// "AAAAAAAAAAAAAAAAAAAAAA":{
+ /// "first":"AgICAgIC",
+ /// "second":"AwM"
+ /// }
+ /// }
+ /// }
+ /// }
+ /// }).to_string();
+ /// // Since `Challenge`s are randomly generated, we don't know what it will be; thus
+ /// // we test the JSON string for everything except it.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(client_state.get(..14), json.get(..14));
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(client_state.get(36..), json.get(36..));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("AuthenticationClientState", 9)
+ .and_then(|mut ser| {
+ ser.serialize_field("challenge", &self.0.challenge)
+ .and_then(|()| {
+ ser.serialize_field("timeout", &self.0.timeout)
+ .and_then(|()| {
+ ser.serialize_field("rpId", &self.0.rp_id).and_then(|()| {
+ ser.serialize_field(
+ "allowCredentials",
+ &self.0.allow_credentials,
+ )
+ .and_then(|()| {
+ ser.serialize_field(
+ "userVerification",
+ &self.0.user_verification,
+ )
+ .and_then(|()| {
+ ser.serialize_field("hints", &self.0.hints).and_then(
+ |()| {
+ ser.serialize_field(
+ "extensions",
+ &ExtensionHelper {
+ extension: &self.0.extensions,
+ allow_credentials: &self
+ .0
+ .allow_credentials,
+ },
+ )
+ .and_then(|()| ser.end())
+ },
+ )
+ })
+ })
+ })
+ })
+ })
+ })
+ }
+}
diff --git a/src/request/auth/ser_server_state.rs b/src/request/auth/ser_server_state.rs
@@ -0,0 +1,232 @@
+#![expect(
+ clippy::question_mark_used,
+ clippy::unseparated_literal_suffix,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+use super::{
+ super::super::bin::{
+ Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible,
+ },
+ AuthenticationServerState, CredInfo, CredentialId, ExtensionReq, SentChallenge,
+ ServerCredSpecificExtensionInfo, ServerExtensionInfo, ServerPrfInfo,
+ SignatureCounterEnforcement, UserVerificationRequirement,
+};
+#[cfg(doc)]
+use super::{AllowedCredential, PublicKeyCredentialRequestOptions};
+use core::{
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+#[cfg(doc)]
+use std::time::UNIX_EPOCH;
+use std::time::{SystemTime, SystemTimeError};
+impl EncodeBuffer for ServerPrfInfo {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::One(req) => {
+ 0u8.encode_into_buffer(buffer);
+ req
+ }
+ Self::Two(req) => {
+ 1u8.encode_into_buffer(buffer);
+ req
+ }
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for ServerPrfInfo {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => ExtensionReq::decode_from_buffer(data).map(Self::One),
+ 1 => ExtensionReq::decode_from_buffer(data).map(Self::Two),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for ServerCredSpecificExtensionInfo {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.prf.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for ServerCredSpecificExtensionInfo {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ Option::decode_from_buffer(data).map(|prf| Self { prf })
+ }
+}
+impl EncodeBuffer for CredInfo {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ CredentialId::<&[u8]>::from(&self.id).encode_into_buffer(buffer);
+ self.ext.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for CredInfo {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ CredentialId::<Vec<u8>>::decode_from_buffer(data).and_then(|id| {
+ ServerCredSpecificExtensionInfo::decode_from_buffer(data).map(|ext| Self { id, ext })
+ })
+ }
+}
+impl EncodeBuffer for ServerExtensionInfo {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.prf.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for ServerExtensionInfo {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ Option::decode_from_buffer(data).map(|prf| Self { prf })
+ }
+}
+impl EncodeBuffer for SignatureCounterEnforcement {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::Fail => 0u8,
+ Self::Update => 1,
+ Self::Ignore => 2,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for SignatureCounterEnforcement {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Ok(Self::Fail),
+ 1 => Ok(Self::Update),
+ 2 => Ok(Self::Ignore),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBufferFallible for &[CredInfo] {
+ type Err = EncDecErr;
+ /// # Errors
+ ///
+ /// Errors iff `self.len() > usize::from(u16::MAX)`.
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), EncDecErr> {
+ u16::try_from(self.len())
+ .map_err(|_e| EncDecErr)
+ .map(|len| {
+ len.encode_into_buffer(buffer);
+ self.iter().fold((), |(), val| {
+ val.encode_into_buffer(buffer);
+ });
+ })
+ }
+}
+impl<'a> DecodeBuffer<'a> for Vec<CredInfo> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u16::decode_from_buffer(data).and_then(|len| {
+ let l = usize::from(len);
+ let mut creds = Self::with_capacity(l);
+ while creds.len() < l {
+ creds.push(CredInfo::decode_from_buffer(data)?);
+ }
+ Ok(creds)
+ })
+ }
+}
+/// Error returned from [`AuthenticationServerState::encode`].
+#[derive(Debug)]
+pub enum EncodeAuthenticationServerStateErr {
+ /// Variant returned when
+ /// [`AuthenticationServerState::expiration`](../struct.AuthenticationServerState.html#method.expiration-1)
+ /// is before [`UNIX_EPOCH`].
+ SystemTime(SystemTimeError),
+ /// Variant returned when the corresponding [`PublicKeyCredentialRequestOptions::allow_credentials`] has more
+ /// than [`u16::MAX`] [`AllowedCredential`]s.
+ AllowedCredentialsCount,
+}
+impl Display for EncodeAuthenticationServerStateErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::SystemTime(ref err) => err.fmt(f),
+ Self::AllowedCredentialsCount => {
+ f.write_str("there were more than 65,535 AllowedCredentials")
+ }
+ }
+ }
+}
+impl Error for EncodeAuthenticationServerStateErr {}
+impl Encode for AuthenticationServerState {
+ type Output<'a> = Vec<u8> where Self: 'a;
+ type Err = EncodeAuthenticationServerStateErr;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ // Length of the anticipated most common output:
+ // * 16 for `SentChallenge`
+ // * 2 + large range for `[CredInfo]` where we assume [`CredInfo`] being
+ // empty is the most common
+ // * 1 for `UserVerificationRequirement`
+ // * 1 or 3 for `ServerExtensionInfo` where we assume 1 is the most common
+ // * 12 for `SystemTime`
+ let mut buffer = Vec::with_capacity(16 + 2 + 1 + 1 + 12);
+ self.challenge.encode_into_buffer(&mut buffer);
+ self.allow_credentials
+ .as_slice()
+ .encode_into_buffer(&mut buffer)
+ .map_err(|_e| EncodeAuthenticationServerStateErr::AllowedCredentialsCount)
+ .and_then(|()| {
+ self.user_verification.encode_into_buffer(&mut buffer);
+ self.extensions.encode_into_buffer(&mut buffer);
+ self.expiration
+ .encode_into_buffer(&mut buffer)
+ .map_err(EncodeAuthenticationServerStateErr::SystemTime)
+ .map(|()| buffer)
+ })
+ }
+}
+/// Error returned from [`AuthenticationServerState::decode`].
+#[derive(Clone, Copy, Debug)]
+pub enum DecodeAuthenticationServerStateErr {
+ /// Variant returned when there was trailing data after decoding an [`AuthenticationServerState`].
+ TrailingData,
+ /// Variant returned for all other errors.
+ Other,
+}
+impl Display for DecodeAuthenticationServerStateErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::TrailingData => "trailing data after decoding AuthenticationServerState",
+ Self::Other => "AuthenticationServerState could not be decoded",
+ })
+ }
+}
+impl Error for DecodeAuthenticationServerStateErr {}
+impl Decode for AuthenticationServerState {
+ type Input<'a> = &'a [u8];
+ type Err = DecodeAuthenticationServerStateErr;
+ #[inline]
+ fn decode(mut input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ SentChallenge::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|challenge| {
+ Vec::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|allow_credentials| {
+ UserVerificationRequirement::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|user_verification| {
+ ServerExtensionInfo::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|extensions| {
+ super::validate_options_helper(extensions, user_verification, &allow_credentials).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|()| {
+ SystemTime::decode_from_buffer(&mut input).map_err(|_e| DecodeAuthenticationServerStateErr::Other).and_then(|expiration| {
+ if input.is_empty() {
+ Ok(Self {
+ challenge,
+ allow_credentials,
+ user_verification,
+ extensions,
+ expiration,
+ })
+ } else {
+ Err(DecodeAuthenticationServerStateErr::TrailingData)
+ }
+ })
+ })
+ })
+ })
+ })
+ })
+ }
+}
diff --git a/src/request/error.rs b/src/request/error.rs
@@ -0,0 +1,99 @@
+#[cfg(doc)]
+use super::{AsciiDomain, DomainOrigin, Port, RpId, Scheme, Url};
+#[cfg(doc)]
+use core::str::FromStr;
+use core::{
+ error::Error,
+ fmt::{self, Display, Formatter},
+ num::ParseIntError,
+};
+/// Error returned by [`AsciiDomain::try_from`] when the `String` is not a valid ASCII domain.
+#[derive(Clone, Copy, Debug)]
+pub enum AsciiDomainErr {
+ /// Variant returned when the domain is empty.
+ Empty,
+ /// Variant returned when the domain is the root domain (i.e., `'.'`).
+ RootDomain,
+ /// Variant returned when the domain is too long.
+ Len,
+ /// Variant returned when an empty label exists.
+ EmptyLabel,
+ /// Variant returned when a label is too long.
+ LabelLen,
+ /// Variant returned when a label contains a `u8` that is not valid ASCII.
+ NotAscii,
+}
+impl Display for AsciiDomainErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::Empty => "domain is empty",
+ Self::RootDomain => "domain is the root domain",
+ Self::Len => "domain is too long",
+ Self::EmptyLabel => "domain has an empty label",
+ Self::LabelLen => "domain has a label that is too long",
+ Self::NotAscii => "domain has a label that contains a non-ASCII byte",
+ })
+ }
+}
+impl Error for AsciiDomainErr {}
+/// Error returned by [`Url::from_str`] when the `str` passed to the
+/// [URL serializer](https://url.spec.whatwg.org/#concept-url-serializer) leads to a failure.
+#[derive(Clone, Copy, Debug)]
+pub struct UrlErr;
+impl Display for UrlErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("URL serializer failed")
+ }
+}
+impl Error for UrlErr {}
+/// Error returned by [`Scheme::try_from`] when the passed [`str`] is empty.
+#[derive(Clone, Copy, Debug)]
+pub struct SchemeParseErr;
+impl Display for SchemeParseErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("scheme was empty")
+ }
+}
+impl Error for SchemeParseErr {}
+/// Error returned by [`Port::from_str`] when the passed [`str`] is not a valid unsigned 16-bit integer in
+/// decimal form without leading 0s.
+#[derive(Debug)]
+pub enum PortParseErr {
+ /// Variant returned iff [`u16::from_str`] does.
+ ParseInt(ParseIntError),
+ /// Variant returned iff a `str` is a valid 16-bit unsigned integer with leading 0s.
+ NotCanonical,
+}
+impl Display for PortParseErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::ParseInt(ref err) => err.fmt(f),
+ Self::NotCanonical => {
+ f.write_str("string was a valid TCP/UDP port number, but it had leading 0s")
+ }
+ }
+ }
+}
+impl Error for PortParseErr {}
+/// Error returned by [`DomainOrigin::try_from`].
+#[derive(Debug)]
+pub enum DomainOriginParseErr {
+ /// Variant returned when there is an error parsing the scheme.
+ Scheme(SchemeParseErr),
+ /// Variant returned when there is an error parsing the port.
+ Port(PortParseErr),
+}
+impl Display for DomainOriginParseErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Scheme(err) => err.fmt(f),
+ Self::Port(ref err) => err.fmt(f),
+ }
+ }
+}
+impl Error for DomainOriginParseErr {}
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -0,0 +1,1674 @@
+extern crate alloc;
+use super::{
+ super::{
+ response::{
+ register::{
+ error::{ExtensionErr, RegCeremonyErr},
+ Attestation, AttestationFormat, AuthenticatorExtensionOutput,
+ AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputs,
+ CredentialProtectionPolicy, Registration, UncompressedPubKey,
+ },
+ AuthenticatorAttachment,
+ },
+ DynamicState, Metadata, RegisteredCredential, StaticState,
+ },
+ register::error::{CreationOptionsErr, NicknameErr, UserHandleErr, UsernameErr},
+ BackupReq, Ceremony, Challenge, CredentialMediationRequirement, ExtensionInfo, ExtensionReq,
+ Hint, Origin, PublicKeyCredentialDescriptor, RpId, SentChallenge, ServerState,
+ UserVerificationRequirement, THREE_HUNDRED_THOUSAND,
+};
+#[cfg(doc)]
+use crate::{
+ request::{auth::PublicKeyCredentialRequestOptions, AsciiDomain, DomainOrigin, Url},
+ response::{AuthTransports, AuthenticatorTransport, Backup, CollectedClientData},
+};
+use alloc::borrow::Cow;
+use core::{
+ borrow::Borrow,
+ cmp::Ordering,
+ fmt::{self, Display, Formatter},
+ hash::{Hash, Hasher},
+ num::{NonZeroU32, NonZeroU64},
+ time::Duration,
+};
+use precis_profiles::{precis_core::profile::Profile as _, UsernameCasePreserved};
+use rand::RngCore as _;
+#[cfg(any(doc, not(feature = "serializable_server_state")))]
+use std::time::Instant;
+#[cfg(any(doc, feature = "serializable_server_state"))]
+use std::time::SystemTime;
+/// Contains functionality to (de)serialize data to a data store.
+#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+#[cfg(feature = "bin")]
+pub mod bin;
+/// 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;
+/// Contains error types.
+pub mod error;
+/// Contains functionality to serialize data to a client.
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+#[cfg(feature = "serde")]
+mod ser;
+/// Contains functionality to (de)serialize [`RegistrationServerState`] to a data store.
+#[cfg_attr(docsrs, doc(cfg(feature = "serializable_server_state")))]
+#[cfg(feature = "serializable_server_state")]
+pub mod ser_server_state;
+/// Used by [`Extension::cred_protect`] to enforce the [`CredentialProtectionPolicy`] sent by the client via
+/// [`Registration`].
+#[derive(Clone, Copy, Debug, Default)]
+pub enum CredProtect {
+ /// No `credProtect` request.
+ #[default]
+ None,
+ /// Request
+ /// [`userVerificationOptional`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptional)
+ /// but allow any.
+ UserVerificationOptional(ExtensionInfo),
+ /// Request
+ /// [`userVerificationOptionalWithCredentialIDList`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptionalwithcredentialidlist);
+ /// and when enforcing the value, disallow [`CredentialProtectionPolicy::UserVerificationOptional`].
+ UserVerificationOptionalWithCredentialIdList(ExtensionInfo),
+ /// Request
+ /// [`userVerificationRequired`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationrequired);
+ /// and when enforcing the value, only allow [`CredentialProtectionPolicy::UserVerificationRequired`].
+ UserVerificationRequired(ExtensionInfo),
+}
+impl CredProtect {
+ /// Validates `other` is allowed based on `self`.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff other is a less "secure" policy than `self` when enforcing the value or other does not exist
+ /// despite requiring a value to be sent back.
+ ///
+ /// Note a missing response is OK when enforcing a value.
+ const fn validate(self, other: CredentialProtectionPolicy) -> Result<(), ExtensionErr> {
+ match self {
+ Self::None => Ok(()),
+ Self::UserVerificationOptional(info) => {
+ if matches!(other, CredentialProtectionPolicy::None) {
+ if matches!(
+ info,
+ ExtensionInfo::RequireEnforceValue | ExtensionInfo::RequireDontEnforceValue
+ ) {
+ Err(ExtensionErr::MissingCredProtect)
+ } else {
+ Ok(())
+ }
+ } else {
+ Ok(())
+ }
+ }
+ Self::UserVerificationOptionalWithCredentialIdList(info) => match info {
+ ExtensionInfo::RequireEnforceValue => match other {
+ CredentialProtectionPolicy::None => Err(ExtensionErr::MissingCredProtect),
+ CredentialProtectionPolicy::UserVerificationOptional => {
+ Err(ExtensionErr::InvalidCredProtectValue(self, other))
+ }
+ CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList
+ | CredentialProtectionPolicy::UserVerificationRequired => Ok(()),
+ },
+ ExtensionInfo::RequireDontEnforceValue => {
+ if matches!(other, CredentialProtectionPolicy::None) {
+ Err(ExtensionErr::MissingCredProtect)
+ } else {
+ Ok(())
+ }
+ }
+ ExtensionInfo::AllowEnforceValue => {
+ if matches!(other, CredentialProtectionPolicy::UserVerificationOptional) {
+ Err(ExtensionErr::InvalidCredProtectValue(self, other))
+ } else {
+ Ok(())
+ }
+ }
+ ExtensionInfo::AllowDontEnforceValue => Ok(()),
+ },
+ Self::UserVerificationRequired(info) => match info {
+ ExtensionInfo::RequireEnforceValue => match other {
+ CredentialProtectionPolicy::None => Err(ExtensionErr::MissingCredProtect),
+ CredentialProtectionPolicy::UserVerificationOptional
+ | CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => {
+ Err(ExtensionErr::InvalidCredProtectValue(self, other))
+ }
+ CredentialProtectionPolicy::UserVerificationRequired => Ok(()),
+ },
+ ExtensionInfo::RequireDontEnforceValue => {
+ if matches!(other, CredentialProtectionPolicy::None) {
+ Err(ExtensionErr::MissingCredProtect)
+ } else {
+ Ok(())
+ }
+ }
+ ExtensionInfo::AllowEnforceValue => {
+ if matches!(
+ other,
+ CredentialProtectionPolicy::None
+ | CredentialProtectionPolicy::UserVerificationRequired
+ ) {
+ Ok(())
+ } else {
+ Err(ExtensionErr::InvalidCredProtectValue(self, other))
+ }
+ }
+ ExtensionInfo::AllowDontEnforceValue => Ok(()),
+ },
+ }
+ }
+}
+impl Display for CredProtect {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::None => f.write_str("do not sent a credProtect request"),
+ Self::UserVerificationOptional(info) => {
+ write!(f, "request user verification optional and {info}")
+ }
+ Self::UserVerificationOptionalWithCredentialIdList(info) => write!(
+ f,
+ "user verification optional with credential ID list and {info}"
+ ),
+ Self::UserVerificationRequired(info) => {
+ write!(f, "user verification required and {info}")
+ }
+ }
+ }
+}
+/// String returned from the [Nickname Enforcement rule](https://www.rfc-editor.org/rfc/rfc8266#section-2.3)
+/// as defined in RFC 8266.
+///
+/// Note [string truncation](https://www.w3.org/TR/webauthn-3/#sctn-strings-truncation) is allowed, so one may
+/// want to enforce their own length requirements in the event [`Self::MAX_LEN`] is too long.
+#[derive(Clone, Debug)]
+pub struct Nickname<'a>(Cow<'a, str>);
+impl Nickname<'_> {
+ /// The maximum allowed length.
+ pub const MAX_LEN: usize = 1023;
+ /// Returns a `Nickname` that consumes `self`. When `self` owns the data, the data is simply moved;
+ /// when the data is borrowed, then it is cloned into an owned instance.
+ #[inline]
+ #[must_use]
+ pub fn into_owned<'a>(self) -> Nickname<'a> {
+ Nickname(Cow::Owned(self.0.into_owned()))
+ }
+}
+impl AsRef<str> for Nickname<'_> {
+ #[inline]
+ fn as_ref(&self) -> &str {
+ self.0.as_ref()
+ }
+}
+impl Borrow<str> for Nickname<'_> {
+ #[inline]
+ fn borrow(&self) -> &str {
+ self.0.as_ref()
+ }
+}
+impl<'a: 'b, 'b> From<Nickname<'a>> for Cow<'b, str> {
+ #[inline]
+ fn from(value: Nickname<'a>) -> Self {
+ value.0
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a str> for Nickname<'b> {
+ type Error = NicknameErr;
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{error::NicknameErr, Nickname};
+ /// assert_eq!(
+ /// Nickname::try_from("Srinivasa Ramanujan")?.as_ref(),
+ /// "Srinivasa Ramanujan"
+ /// );
+ /// assert_eq!(
+ /// Nickname::try_from("श्रीनिवास रामानुजन्")?.as_ref(),
+ /// "श्रीनिवास रामानुजन्"
+ /// );
+ /// // Empty strings are not valid.
+ /// assert!(Nickname::try_from("").map_or_else(
+ /// |e| matches!(e, NicknameErr::Rfc8266),
+ /// |_| false
+ /// ));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+ precis_profiles::Nickname::new()
+ .enforce(value)
+ .map_err(|_e| NicknameErr::Rfc8266)
+ .and_then(|val| {
+ if val.len() <= Self::MAX_LEN {
+ Ok(Self(val))
+ } else {
+ Err(NicknameErr::Len)
+ }
+ })
+ }
+}
+/// String returned from the
+/// [UsernameCasePreserved Enforcement rule](https://www.rfc-editor.org/rfc/rfc8265#section-3.4.3) as defined in
+/// RFC 8265.
+///
+/// Note [string truncation](https://www.w3.org/TR/webauthn-3/#sctn-strings-truncation) is allowed, so one may
+/// want to enforce their own length requirements in the event [`Self::MAX_LEN`] is too long.
+#[derive(Clone, Debug)]
+pub struct Username<'a>(Cow<'a, str>);
+impl Username<'_> {
+ /// The maximum allowed length.
+ pub const MAX_LEN: usize = 1023;
+ /// Returns a `Username` that consumes `self`. When `self` owns the data, the data is simply moved;
+ /// when the data is borrowed, then it is cloned into an owned instance.
+ #[inline]
+ #[must_use]
+ pub fn into_owned<'a>(self) -> Username<'a> {
+ Username(Cow::Owned(self.0.into_owned()))
+ }
+}
+impl AsRef<str> for Username<'_> {
+ #[inline]
+ fn as_ref(&self) -> &str {
+ self.0.as_ref()
+ }
+}
+impl Borrow<str> for Username<'_> {
+ #[inline]
+ fn borrow(&self) -> &str {
+ self.0.as_ref()
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a str> for Username<'b> {
+ type Error = UsernameErr;
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{error::UsernameErr, Username};
+ /// assert_eq!(
+ /// Username::try_from("leonhard.euler")?.as_ref(),
+ /// "leonhard.euler"
+ /// );
+ /// // Empty strings are not valid.
+ /// assert!(Username::try_from("").map_or_else(
+ /// |e| matches!(e, UsernameErr::Rfc8265),
+ /// |_| false
+ /// ));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+ UsernameCasePreserved::default()
+ .enforce(value)
+ .map_err(|_e| UsernameErr::Rfc8265)
+ .and_then(|val| {
+ if val.len() <= Self::MAX_LEN {
+ Ok(Self(val))
+ } else {
+ Err(UsernameErr::Len)
+ }
+ })
+ }
+}
+impl<'a: 'b, 'b> From<Username<'a>> for Cow<'b, str> {
+ #[inline]
+ fn from(value: Username<'a>) -> Self {
+ value.0
+ }
+}
+/// [`COSEAlgorithmIdentifier`](https://www.w3.org/TR/webauthn-3/#typedefdef-cosealgorithmidentifier).
+///
+/// Note the order of variants is the following:
+///
+/// [`Self::Eddsa`] `<` [`Self::Es256`] `<` [`Self::Es384`] `<` [`Self::Rs256`].
+///
+/// This is relevant for [`CoseAlgorithmIdentifiers`]. For example a `CoseAlgorithmIdentifiers`
+/// that contains `Self::Eddsa` will prioritize it over all others.
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+pub enum CoseAlgorithmIdentifier {
+ /// [EdDSA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms) with the added requirement that
+ /// Ed25519 be used for `crv` parameter [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier).
+ Eddsa,
+ /// [ES256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms) with the added requirements that
+ /// P-256 be used for `crv` parameter and the uncompressed form must be used
+ /// [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier).
+ Es256,
+ /// [ES384](https://www.iana.org/assignments/cose/cose.xhtml#algorithms) with the added requirements that
+ /// P-384 be used for `crv` parameter and the uncompressed form must be used
+ /// [per the spec](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier).
+ Es384,
+ /// [RS256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+ Rs256,
+}
+impl CoseAlgorithmIdentifier {
+ /// Transforms `self` into a `u8`.
+ const fn to_u8(self) -> u8 {
+ match self {
+ Self::Eddsa => 1,
+ Self::Es256 => 2,
+ Self::Es384 => 4,
+ Self::Rs256 => 8,
+ }
+ }
+}
+impl PartialEq<&Self> for CoseAlgorithmIdentifier {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<CoseAlgorithmIdentifier> for &CoseAlgorithmIdentifier {
+ #[inline]
+ fn eq(&self, other: &CoseAlgorithmIdentifier) -> bool {
+ **self == *other
+ }
+}
+/// Non-empty ordered set of [`CoseAlgorithmIdentifier`]s.
+#[derive(Clone, Copy, Debug)]
+pub struct CoseAlgorithmIdentifiers(u8);
+impl CoseAlgorithmIdentifiers {
+ /// Contains all [`CoseAlgorithmIdentifiers`].
+ pub const ALL: Self = Self(0)
+ .add(CoseAlgorithmIdentifier::Eddsa)
+ .add(CoseAlgorithmIdentifier::Es256)
+ .add(CoseAlgorithmIdentifier::Es384)
+ .add(CoseAlgorithmIdentifier::Rs256);
+ /// Returns a `CoseAlgorithmIdentifiers` containing all `CoseAlgorithmIdentifier`s in `self` plus `alg`.
+ const fn add(self, alg: CoseAlgorithmIdentifier) -> Self {
+ Self(self.0 | alg.to_u8())
+ }
+ /// Returns a copy of `self` with `alg` removed if there would be at least one `CoseAlgorithmIdentifier`
+ /// remaining; otherwise returns `self`.
+ #[inline]
+ #[must_use]
+ pub const fn remove(self, alg: CoseAlgorithmIdentifier) -> Self {
+ let val = alg.to_u8();
+ if self.0 == val {
+ self
+ } else {
+ Self(self.0 & !val)
+ }
+ }
+ /// Returns `true` iff `self` contains `alg`.
+ #[inline]
+ #[must_use]
+ pub const fn contains(self, alg: CoseAlgorithmIdentifier) -> bool {
+ let val = alg.to_u8();
+ self.0 & val == val
+ }
+ /// Validates `other` is allowed based on `self`.
+ const fn validate(self, other: UncompressedPubKey<'_>) -> Result<(), RegCeremonyErr> {
+ if match other {
+ UncompressedPubKey::Ed25519(_) => self.contains(CoseAlgorithmIdentifier::Eddsa),
+ UncompressedPubKey::P256(_) => self.contains(CoseAlgorithmIdentifier::Es256),
+ UncompressedPubKey::P384(_) => self.contains(CoseAlgorithmIdentifier::Es384),
+ UncompressedPubKey::Rsa(_) => self.contains(CoseAlgorithmIdentifier::Rs256),
+ } {
+ Ok(())
+ } else {
+ Err(RegCeremonyErr::PublicKeyAlgorithmMismatch)
+ }
+ }
+}
+impl Default for CoseAlgorithmIdentifiers {
+ /// Returns [`Self::ALL`].
+ #[inline]
+ fn default() -> Self {
+ Self::ALL
+ }
+}
+/// 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 {
+ /// [`credProps`](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension).
+ ///
+ /// The best one can do to ensure a server-side credential is created is by sending
+ /// [`ResidentKeyRequirement::Discouraged`]; however authenticators are still allowed
+ /// to create a client-side credential. To more definitively check that a server-side credential is
+ /// created, send this extension. Note that it can be difficult to impossible for a client/user agent to
+ /// know that a server-side credential is created; thus even when the response is
+ /// `Some(CredentialPropertiesOutput { rk: Some(false) })`, a client-side/"resident" credential could still
+ /// have been created. One may have better luck checking if [`AuthTransports::contains`]
+ /// [`AuthenticatorTransport::Internal`] and using that as an indicator if a client-side credential was created.
+ ///
+ /// In the event [`ClientExtensionsOutputs::cred_props`] is `Some(CredentialPropertiesOutput { rk: Some(false) })`
+ /// and [`ResidentKeyRequirement::Required`] was sent, an error will happen regardless of this value.
+ pub cred_props: Option<ExtensionReq>,
+ /// [`credProtect`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension).
+ pub cred_protect: CredProtect,
+ /// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension).
+ ///
+ /// When the value is enforced, that corresponds to
+ /// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension)
+ /// in [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) set to a value at least as large
+ /// as the contained `u8`.
+ pub min_pin_length: Option<(u8, ExtensionInfo)>,
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension).
+ ///
+ /// When the value is enforced, that corresponds to
+ /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled) and
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
+ /// in [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) set to `true`. In contrast
+ /// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) must not exist,
+ /// be `null`, or be an
+ /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues)
+ /// such that [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is `null` and
+ /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) does not exist or is
+ /// `null`. This is to ensure the decrypted outputs stay on the client.
+ ///
+ /// Note for
+ /// [CTAP 2.2](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
+ /// this is only used to instruct the authenticator to create the necessary `hmac-secret` since it does not
+ /// currently support PRF evaluation at creation time. This also requires [`UserVerificationRequirement::Required`].
+ pub prf: Option<ExtensionInfo>,
+}
+impl Extension {
+ /// Validates the extensions.
+ fn validate(
+ self,
+ client_ext: ClientExtensionsOutputs,
+ auth_ext: AuthenticatorExtensionOutput,
+ error_unsolicited: bool,
+ ) -> Result<(), ExtensionErr> {
+ if error_unsolicited {
+ self.validate_unsolicited(client_ext, auth_ext)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ self.validate_required(client_ext, auth_ext)
+ .and_then(|()| self.validate_value(client_ext, auth_ext))
+ })
+ }
+ /// Validates if there are any unsolicited extensions.
+ ///
+ /// Note no distinction is made between an extension that is empty and one that is not (i.e., we are checking
+ /// purely for the existence of extension keys).
+ fn validate_unsolicited(
+ mut self,
+ client_ext: ClientExtensionsOutputs,
+ auth_ext: AuthenticatorExtensionOutput,
+ ) -> Result<(), ExtensionErr> {
+ // For simpler code, we artificially set non-requested extensions after verifying there was not an error
+ // and recursively call this function. There are so few extensions and the checks are fast that there
+ // should be no worry of stack overflow or performance overhead.
+ if self.cred_props.is_some() {
+ if !matches!(self.cred_protect, CredProtect::None) {
+ if self.min_pin_length.is_some() {
+ // This is the last extension, so recursion stops here.
+ if self.prf.is_some() {
+ Ok(())
+ } else if client_ext.prf.is_some() {
+ Err(ExtensionErr::ForbiddenPrf)
+ } else if auth_ext.hmac_secret.is_some() {
+ Err(ExtensionErr::ForbiddenHmacSecret)
+ } else {
+ Ok(())
+ }
+ } else if auth_ext.min_pin_length.is_some() {
+ Err(ExtensionErr::ForbiddenMinPinLength)
+ } else {
+ // Pretend to set `minPinLength`, so we can check `prf`.
+ self.min_pin_length = Some((0, ExtensionInfo::RequireEnforceValue));
+ self.validate_unsolicited(client_ext, auth_ext)
+ }
+ } else if !matches!(auth_ext.cred_protect, CredentialProtectionPolicy::None) {
+ Err(ExtensionErr::ForbiddenCredProtect)
+ } else {
+ // Pretend to set `credProtect`, so we can check `minPinLength` and `prf` extensions.
+ self.cred_protect =
+ CredProtect::UserVerificationOptional(ExtensionInfo::RequireEnforceValue);
+ self.validate_unsolicited(client_ext, auth_ext)
+ }
+ } else if client_ext.cred_props.is_some() {
+ Err(ExtensionErr::ForbiddenCredProps)
+ } else {
+ // Pretend to set `credProps`; so we can check `credProtect`, `minPinLength`, and `prf` extensions.
+ self.cred_props = Some(ExtensionReq::Require);
+ self.validate_unsolicited(client_ext, auth_ext)
+ }
+ }
+ /// Validates if any required extensions don't have a corresponding response.
+ ///
+ /// Note empty extensions are treated as missing. For example when requiring the `credProps` extension,
+ /// all of the following responses would lead to a failure:
+ /// `{"clientExtensionResults":{}}`: no extensions.
+ /// `{"clientExtensionResults":{"prf":true}}`: only the `prf` extension.
+ /// `{"clientExtensionResults":{"credProps":{}}}`: empty `credProps` extension.
+ /// `{"clientExtensionResults":{"credProps":{"foo":false}}}`: `credProps` extension doesn't contain at least one
+ /// expected field (i.e., still "empty").
+ fn validate_required(
+ self,
+ client_ext: ClientExtensionsOutputs,
+ auth_ext: AuthenticatorExtensionOutput,
+ ) -> Result<(), ExtensionErr> {
+ // We don't check `self.cred_protect` since `CredProtect::validate` checks for both a required response
+ // and value enforcement; thus it only needs to be checked once (which it is in `Self::validate_value`).
+ self.cred_props
+ .map_or(Ok(()), |info| {
+ if matches!(info, ExtensionReq::Require) {
+ if client_ext
+ .cred_props
+ .is_some_and(|props| props.rk.is_some())
+ {
+ Ok(())
+ } else {
+ Err(ExtensionErr::MissingCredProps)
+ }
+ } else {
+ Ok(())
+ }
+ })
+ .and_then(|()| {
+ self.min_pin_length
+ .map_or(Ok(()), |info| {
+ if matches!(
+ info.1,
+ ExtensionInfo::RequireEnforceValue
+ | ExtensionInfo::RequireDontEnforceValue
+ ) {
+ auth_ext
+ .min_pin_length
+ .ok_or(ExtensionErr::MissingMinPinLength)
+ .map(|_| ())
+ } else {
+ Ok(())
+ }
+ })
+ .and_then(|()| {
+ self.prf.map_or(Ok(()), |info| {
+ if matches!(
+ info,
+ ExtensionInfo::RequireEnforceValue
+ | ExtensionInfo::RequireDontEnforceValue
+ ) {
+ if client_ext.prf.is_some() {
+ auth_ext
+ .hmac_secret
+ .ok_or(ExtensionErr::MissingHmacSecret)
+ .map(|_| ())
+ } else {
+ Err(ExtensionErr::MissingPrf)
+ }
+ } else {
+ Ok(())
+ }
+ })
+ })
+ })
+ }
+ /// Validates the value of any extensions sent from the client.
+ ///
+ /// Note missing and empty extensions are always OK.
+ fn validate_value(
+ self,
+ client_ext: ClientExtensionsOutputs,
+ auth_ext: AuthenticatorExtensionOutput,
+ ) -> Result<(), ExtensionErr> {
+ // This also checks for a missing response. Instead of duplicating that check, we only call
+ // `self.cred_protect.validate` once here and not also in `Self::validate_required`.
+ self.cred_protect
+ .validate(auth_ext.cred_protect)
+ .and_then(|()| {
+ self.min_pin_length
+ .map_or(Ok(()), |info| {
+ if matches!(
+ info.1,
+ ExtensionInfo::RequireEnforceValue | ExtensionInfo::AllowEnforceValue
+ ) {
+ auth_ext.min_pin_length.map_or(Ok(()), |pin| {
+ if pin >= info.0 {
+ Ok(())
+ } else {
+ Err(ExtensionErr::InvalidMinPinLength(info.0, pin))
+ }
+ })
+ } else {
+ Ok(())
+ }
+ })
+ .and_then(|()| {
+ self.prf.map_or(Ok(()), |info| {
+ if matches!(
+ info,
+ ExtensionInfo::RequireEnforceValue
+ | ExtensionInfo::AllowEnforceValue
+ ) {
+ client_ext
+ .prf
+ .map_or(Ok(()), |prf| {
+ if prf.enabled {
+ Ok(())
+ } else {
+ Err(ExtensionErr::InvalidPrfValue)
+ }
+ })
+ .and_then(|()| {
+ auth_ext.hmac_secret.map_or(Ok(()), |hmac| {
+ if hmac {
+ Ok(())
+ } else {
+ Err(ExtensionErr::InvalidHmacSecretValue)
+ }
+ })
+ })
+ } else {
+ Ok(())
+ }
+ })
+ })
+ })
+ }
+}
+/// 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;
+/// The minimum number of bytes a [`UserHandle`] can be made of per
+/// [WebAuthn](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id).
+pub const USER_HANDLE_MIN_LEN: usize = 1;
+#[derive(Clone, Copy, Debug)]
+/// A [user handle](https://www.w3.org/TR/webauthn-3/#user-handle) that is made up of
+/// [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] bytes.
+pub struct UserHandle<T>(T);
+impl<T> UserHandle<T> {
+ /// Returns the contained data consuming `self`.
+ #[inline]
+ pub fn into_inner(self) -> T {
+ self.0
+ }
+ /// Returns the contained data.
+ #[inline]
+ pub const fn inner(&self) -> &T {
+ &self.0
+ }
+}
+#[cfg(any(feature = "bin", feature = "custom"))]
+impl<'a> UserHandle<&'a [u8]> {
+ /// Creates a `UserHandle` from a `slice`.
+ fn from_slice<'b: 'a>(value: &'b [u8]) -> Result<Self, UserHandleErr> {
+ if (USER_HANDLE_MIN_LEN..=USER_HANDLE_MAX_LEN).contains(&value.len()) {
+ Ok(Self(value))
+ } else {
+ Err(UserHandleErr)
+ }
+ }
+}
+impl UserHandle<Vec<u8>> {
+ /// Returns a new `UserHandle` based on `len` randomly-generated [`u8`]s.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `len` is not inclusively between [`USER_HANDLE_MIN_LEN`] and [`USER_HANDLE_MAX_LEN`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MIN_LEN, USER_HANDLE_MAX_LEN};
+ /// assert_eq!(
+ /// UserHandle::rand(USER_HANDLE_MIN_LEN)
+ /// .unwrap_or_else(|_| unreachable!("there is a bug in UserHandle::rand"))
+ /// .as_ref()
+ /// .len(),
+ /// 1
+ /// );
+ /// // The probability of an all-zero `UserHandle` being generated (assuming a good entropy
+ /// // source) is 2^-512 ≈ 7.5 x 10^-155.
+ /// assert_ne!(
+ /// UserHandle::rand(USER_HANDLE_MAX_LEN)
+ /// .unwrap_or_else(|_| unreachable!("there is a bug in UserHandle::rand"))
+ /// .as_ref(),
+ /// [0; USER_HANDLE_MAX_LEN]
+ /// );
+ /// assert!(UserHandle::rand(0).is_err());
+ /// assert!(UserHandle::rand(65).is_err());
+ /// ```
+ #[inline]
+ pub fn rand(len: usize) -> Result<Self, UserHandleErr> {
+ if (USER_HANDLE_MIN_LEN..=USER_HANDLE_MAX_LEN).contains(&len) {
+ let mut data = vec![0; len];
+ // [`ThreadRng` is infallible](https://docs.rs/rand_core/latest/src/rand_core/block.rs.html#237);
+ // thus there is no point in calling `try_fill_bytes`.
+ rand::thread_rng().fill_bytes(data.as_mut_slice());
+ Ok(Self(data))
+ } else {
+ Err(UserHandleErr)
+ }
+ }
+ /// [Per WebAuthn](https://www.w3.org/TR/webauthn-3/#sctn-user-handle-privacy), user handles should be
+ /// a random 64 bytes; thus this is the same as [`Self::rand`] with `len` set to [`USER_HANDLE_MAX_LEN`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MAX_LEN};
+ /// // The probability of an all-zero `UserHandle` being generated (assuming a good entropy
+ /// // source) is 2^-512 ≈ 7.5 x 10^-155.
+ /// assert_ne!(UserHandle::new().as_ref(), [0; USER_HANDLE_MAX_LEN]);
+ /// ```
+ #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ Self::rand(64).unwrap_or_else(|_e| unreachable!("there is a bug in UserHandle::rand"))
+ }
+}
+impl Default for UserHandle<Vec<u8>> {
+ #[inline]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+impl<T: AsRef<[u8]>> AsRef<[u8]> for UserHandle<T> {
+ #[inline]
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+impl<T: Borrow<[u8]>> Borrow<[u8]> for UserHandle<T> {
+ #[inline]
+ fn borrow(&self) -> &[u8] {
+ self.0.borrow()
+ }
+}
+impl<'a: 'b, 'b> From<&'a UserHandle<Vec<u8>>> for UserHandle<&'b Vec<u8>> {
+ #[inline]
+ fn from(value: &'a UserHandle<Vec<u8>>) -> Self {
+ Self(&value.0)
+ }
+}
+impl<'a: 'b, 'b> From<UserHandle<&'a Vec<u8>>> for UserHandle<&'b [u8]> {
+ #[inline]
+ fn from(value: UserHandle<&'a Vec<u8>>) -> Self {
+ Self(value.0.as_slice())
+ }
+}
+impl<'a: 'b, 'b> From<&'a UserHandle<Vec<u8>>> for UserHandle<&'b [u8]> {
+ #[inline]
+ fn from(value: &'a UserHandle<Vec<u8>>) -> Self {
+ Self(value.0.as_slice())
+ }
+}
+impl From<UserHandle<&[u8]>> for UserHandle<Vec<u8>> {
+ #[inline]
+ fn from(value: UserHandle<&[u8]>) -> Self {
+ Self(value.0.to_owned())
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<UserHandle<T>> for UserHandle<T2> {
+ #[inline]
+ fn eq(&self, other: &UserHandle<T>) -> bool {
+ self.0 == other.0
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<UserHandle<T>> for &UserHandle<T2> {
+ #[inline]
+ fn eq(&self, other: &UserHandle<T>) -> bool {
+ **self == *other
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&UserHandle<T>> for UserHandle<T2> {
+ #[inline]
+ fn eq(&self, other: &&UserHandle<T>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq> Eq for UserHandle<T> {}
+impl<T: Hash> Hash for UserHandle<T> {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.0.hash(state);
+ }
+}
+/// [The `PublicKeyCredentialUserEntity`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity)
+/// sent to the client.
+#[derive(Clone, Debug)]
+pub struct PublicKeyCredentialUserEntity<'name, 'display_name, T> {
+ /// [`name`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name).
+ pub name: Username<'name>,
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id).
+ pub id: UserHandle<T>,
+ /// [`displayName`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname).
+ ///
+ /// `None` iff the display name should be the empty string.
+ pub display_name: Option<Nickname<'display_name>>,
+}
+impl PublicKeyCredentialUserEntity<'_, '_, Vec<u8>> {
+ /// Returns a `PublicKeyCredentialUserEntity` that consumes `self`. When `self` owns the data, the data is
+ /// simply moved; when the data is borrowed, then it is cloned into an owned instance.
+ #[inline]
+ #[must_use]
+ pub fn into_owned<'a, 'b>(self) -> PublicKeyCredentialUserEntity<'a, 'b, Vec<u8>> {
+ PublicKeyCredentialUserEntity {
+ name: self.name.into_owned(),
+ id: self.id,
+ display_name: self.display_name.map(Nickname::into_owned),
+ }
+ }
+}
+impl<'a: 'b, 'b, T: AsRef<[u8]>> From<&'a PublicKeyCredentialUserEntity<'_, '_, T>>
+ for PublicKeyCredentialUserEntity<'b, 'b, &'b [u8]>
+{
+ #[inline]
+ fn from(value: &'a PublicKeyCredentialUserEntity<'_, '_, T>) -> Self {
+ Self {
+ name: Username(Cow::Borrowed(&value.name.0)),
+ id: UserHandle(value.id.0.as_ref()),
+ display_name: value
+ .display_name
+ .as_ref()
+ .map(|v| Nickname(Cow::Borrowed(&v.0))),
+ }
+ }
+}
+/// [`ResidentKeyRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement) sent to the client.
+#[derive(Clone, Copy, Debug)]
+pub enum ResidentKeyRequirement {
+ /// [`required`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-required).
+ Required,
+ /// [`discouraged`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-discouraged).
+ Discouraged,
+ /// [`preferred`](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred).
+ Preferred,
+}
+/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints)
+/// 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).
+ SecurityKey,
+ /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid).
+ Hybrid,
+ /// [`Self::SecurityKey`] and [`Self::Hybrid`].
+ SecurityKeyHybrid,
+ /// [`Self::Hybrid`] and [`Self::SecurityKey`].
+ HybridSecurityKey,
+}
+impl From<CrossPlatformHint> for Hint {
+ #[inline]
+ fn from(value: CrossPlatformHint) -> Self {
+ match value {
+ CrossPlatformHint::None => Self::None,
+ CrossPlatformHint::SecurityKey => Self::SecurityKey,
+ CrossPlatformHint::Hybrid => Self::Hybrid,
+ CrossPlatformHint::SecurityKeyHybrid => Self::SecurityKeyHybrid,
+ CrossPlatformHint::HybridSecurityKey => Self::HybridSecurityKey,
+ }
+ }
+}
+/// [`PublicKeyCredentialHints`](https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints)
+/// 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).
+ ClientDevice,
+}
+impl From<PlatformHint> for Hint {
+ #[inline]
+ fn from(value: PlatformHint) -> Self {
+ match value {
+ PlatformHint::None => Self::None,
+ PlatformHint::ClientDevice => Self::ClientDevice,
+ }
+ }
+}
+/// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment)
+/// requirement with associated hints for further refinement.
+#[derive(Clone, Copy, Debug)]
+pub enum AuthenticatorAttachmentReq {
+ /// No attachment information (i.e., any [`AuthenticatorAttachment`]).
+ None(Hint),
+ /// [`platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-platform) is required
+ /// to be used.
+ Platform(PlatformHint),
+ /// [`cross-platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-cross-platform) is
+ /// required to be used.
+ CrossPlatform(CrossPlatformHint),
+}
+impl Default for AuthenticatorAttachmentReq {
+ #[inline]
+ fn default() -> Self {
+ Self::None(Hint::default())
+ }
+}
+impl AuthenticatorAttachmentReq {
+ /// Validates `self` against `other` ignoring [`Self::immutable_attachment`].
+ const fn validate(
+ self,
+ require_response: bool,
+ other: AuthenticatorAttachment,
+ ) -> Result<(), RegCeremonyErr> {
+ match self {
+ Self::None(_) => {
+ if require_response && matches!(other, AuthenticatorAttachment::None) {
+ Err(RegCeremonyErr::MissingAuthenticatorAttachment)
+ } else {
+ Ok(())
+ }
+ }
+ Self::Platform(_) => match other {
+ AuthenticatorAttachment::None => {
+ if require_response {
+ Err(RegCeremonyErr::MissingAuthenticatorAttachment)
+ } else {
+ Ok(())
+ }
+ }
+ AuthenticatorAttachment::Platform => Ok(()),
+ AuthenticatorAttachment::CrossPlatform => {
+ Err(RegCeremonyErr::AuthenticatorAttachmentMismatch)
+ }
+ },
+ Self::CrossPlatform(_) => match other {
+ AuthenticatorAttachment::None => {
+ if require_response {
+ Err(RegCeremonyErr::MissingAuthenticatorAttachment)
+ } else {
+ Ok(())
+ }
+ }
+ AuthenticatorAttachment::CrossPlatform => Ok(()),
+ AuthenticatorAttachment::Platform => {
+ Err(RegCeremonyErr::AuthenticatorAttachmentMismatch)
+ }
+ },
+ }
+ }
+}
+/// [`AuthenticatorSelectionCriteria`](https://www.w3.org/TR/webauthn-3/#dictionary-authenticatorSelection).
+#[derive(Clone, Copy, Debug)]
+pub struct AuthenticatorSelectionCriteria {
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment).
+ pub authenticator_attachment: AuthenticatorAttachmentReq,
+ /// [`residentKey`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey).
+ pub resident_key: ResidentKeyRequirement,
+ /// [`userVerification`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification).
+ pub user_verification: UserVerificationRequirement,
+}
+impl AuthenticatorSelectionCriteria {
+ /// Returns an `AuthenticatorSelectionCriteria` useful for passkeys (i.e., [`Self::resident_key`] is set to
+ /// [`ResidentKeyRequirement::Required`] and [`Self::user_verification`] is set to
+ /// [`UserVerificationRequirement::Required`]).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{
+ /// # register::{
+ /// # AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, ResidentKeyRequirement,
+ /// # },
+ /// # Hint, UserVerificationRequirement,
+ /// # };
+ /// let crit = AuthenticatorSelectionCriteria::passkey();
+ /// assert!(
+ /// matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ /// );
+ /// assert!(matches!(
+ /// crit.resident_key,
+ /// ResidentKeyRequirement::Required
+ /// ));
+ /// assert!(matches!(
+ /// crit.user_verification,
+ /// UserVerificationRequirement::Required
+ /// ));
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn passkey() -> Self {
+ Self {
+ authenticator_attachment: AuthenticatorAttachmentReq::default(),
+ resident_key: ResidentKeyRequirement::Required,
+ user_verification: UserVerificationRequirement::Required,
+ }
+ }
+ /// Returns an `AuthenticatorSelectionCriteria` useful for second-factor flows (i.e., [`Self::resident_key`]
+ /// is set to [`ResidentKeyRequirement::Discouraged`] and [`Self::user_verification`] is set to
+ /// [`UserVerificationRequirement::Discouraged`]).
+ ///
+ /// Note some authenticators require user verification during credential registration (e.g.,
+ /// [CTAP 2.0 authenticators](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#authenticatorMakeCredential)).
+ /// When an authenticator supports both CTAP 2.0 and
+ /// [Universal 2nd Factor (U2F)](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-overview-v1.2-ps-20170411.html#registration-creating-a-key-pair)
+ /// protocols, user agents will sometimes fall back to U2F when `UserVerificationRequirement::Discouraged`
+ /// is requested since the latter allows for registration without user verification. If the user agent does not
+ /// do this, then users will have an inconsistent experience when authenticating an already-registered
+ /// credential. If this is undesirable, one can use [`UserVerificationRequirement::Required`] for this and
+ /// [`PublicKeyCredentialRequestOptions::user_verification`] at the expense of requiring a user to verify
+ /// themselves twice: once for the first factor and again here.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{
+ /// # register::{
+ /// # AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, ResidentKeyRequirement,
+ /// # },
+ /// # Hint, UserVerificationRequirement,
+ /// # };
+ /// let crit = AuthenticatorSelectionCriteria::second_factor();
+ /// assert!(
+ /// matches!(crit.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None))
+ /// );
+ /// assert!(matches!(
+ /// crit.resident_key,
+ /// ResidentKeyRequirement::Discouraged
+ /// ));
+ /// assert!(matches!(
+ /// crit.user_verification,
+ /// UserVerificationRequirement::Discouraged
+ /// ));
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn second_factor() -> Self {
+ Self {
+ authenticator_attachment: AuthenticatorAttachmentReq::default(),
+ resident_key: ResidentKeyRequirement::Discouraged,
+ user_verification: UserVerificationRequirement::Discouraged,
+ }
+ }
+ /// Ensures a client-side credential was created when applicable. Also enforces `auth_attachment` when
+ /// applicable.
+ const fn validate(
+ self,
+ require_auth_attachment: bool,
+ auth_attachment: AuthenticatorAttachment,
+ ) -> Result<(), RegCeremonyErr> {
+ self.authenticator_attachment
+ .validate(require_auth_attachment, auth_attachment)
+ }
+}
+/// Helper that verifies the overlap of [`PublicKeyCredentialCreationOptions::start_ceremony`] and
+/// [`RegistrationServerState::decode`].
+const fn validate_options_helper(
+ auth_crit: AuthenticatorSelectionCriteria,
+ extensions: Extension,
+) -> Result<(), CreationOptionsErr> {
+ if matches!(
+ auth_crit.user_verification,
+ UserVerificationRequirement::Required
+ ) {
+ Ok(())
+ } else if extensions.prf.is_some() {
+ Err(CreationOptionsErr::PrfWithoutUserVerification)
+ } else if matches!(
+ extensions.cred_protect,
+ CredProtect::UserVerificationRequired(_)
+ ) {
+ Err(CreationOptionsErr::CredProtectRequiredWithoutUserVerification)
+ } else {
+ Ok(())
+ }
+}
+/// The [`PublicKeyCredentialCreationOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions)
+/// to send to the client when registering a new credential.
+///
+/// Upon saving the [`RegistrationServerState`] returned from [`Self::start_ceremony`], one MUST send
+/// [`RegistrationClientState`] to the client ASAP. After receiving the newly created [`Registration`], it is
+/// validated using [`RegistrationServerState::verify`].
+#[derive(Debug)]
+pub struct PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_handle>
+{
+ /// [`mediation`](https://www.w3.org/TR/credential-management-1/#dom-credentialcreationoptions-mediation).
+ pub mediation: CredentialMediationRequirement,
+ /// [`rp`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-rp).
+ pub rp_id: &'rp_id RpId,
+ /// [`user`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-user).
+ pub user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, &'user_handle [u8]>,
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge).
+ pub challenge: Challenge,
+ /// [`pubKeyCredParams`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-pubkeycredparams).
+ pub pub_key_cred_params: CoseAlgorithmIdentifiers,
+ /// [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-timeout).
+ ///
+ /// Note we require a positive value despite the spec allowing an optional nonnegative value. This jives
+ /// with the fact that in-memory storage is required when `serializable_server_state` is not enabled
+ /// when attesting credentials as no timeout would make out-of-memory (OOM) conditions more likely.
+ pub timeout: NonZeroU32,
+ /// [`excludeCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-excludecredentials).
+ pub exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ /// [`authenticatorSelection`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection).
+ pub authenticator_selection: AuthenticatorSelectionCriteria,
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions).
+ pub extensions: Extension,
+}
+impl<'rp_id, 'user_name, 'user_display_name, 'user_handle>
+ PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_handle>
+{
+ /// Most deployments of passkeys should use this function. Specifically deployments that are both userless and
+ /// passwordless and desire multi-factor authentication (MFA) to be done entirely on the authenticator. It
+ /// is important `exclude_credentials` contains the information for _all_ [`RegisteredCredential`]s registered to
+ /// [`PublicKeyCredentialUserEntity::id`] to avoid accidentally overwriting existing credentials that
+ /// have been previously registered.
+ ///
+ /// Creates a `PublicKeyCredentialCreationOptions` that requires the authenticator to create a client-side
+ /// discoverable credential enforcing any form of user verification. A five-minute timeout is set.
+ /// [`Extension::cred_protect`] with [`CredProtect::UserVerificationRequired`] and
+ /// [`ExtensionInfo::AllowEnforceValue`] is used. [`Self::mediation`] is
+ /// [`CredentialMediationRequirement::Optional`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{
+ /// # register::{
+ /// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
+ /// # },
+ /// # AsciiDomain, RpId, UserVerificationRequirement
+ /// # };
+ /// assert!(matches!(
+ /// PublicKeyCredentialCreationOptions::passkey(
+ /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
+ /// PublicKeyCredentialUserEntity {
+ /// name: "archimedes.of.syracuse".try_into()?,
+ /// id: (&UserHandle::new()).into(),
+ /// display_name: Some("Αρχιμήδης ο Συρακούσιος".try_into()?),
+ /// },
+ /// Vec::new()
+ /// )
+ /// .authenticator_selection.user_verification, UserVerificationRequirement::Required
+ /// ));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn passkey<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_handle>(
+ rp_id: &'a RpId,
+ user: PublicKeyCredentialUserEntity<'b, 'c, &'d [u8]>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ ) -> Self {
+ Self {
+ mediation: CredentialMediationRequirement::Optional,
+ rp_id,
+ user,
+ challenge: Challenge::new(),
+ pub_key_cred_params: CoseAlgorithmIdentifiers::default(),
+ timeout: THREE_HUNDRED_THOUSAND,
+ exclude_credentials,
+ authenticator_selection: AuthenticatorSelectionCriteria::passkey(),
+ extensions: Extension {
+ cred_props: None,
+ cred_protect: CredProtect::UserVerificationRequired(
+ ExtensionInfo::AllowEnforceValue,
+ ),
+ min_pin_length: None,
+ prf: None,
+ },
+ }
+ }
+ /// Deployments that want to incorporate a "something a user has" factor into a larger multi-factor
+ /// authentication (MFA) setup. Specifically deployments that are _not_ userless or passwordless. It
+ /// is important `exclude_credentials` contains the information for _all_ [`RegisteredCredential`]s registered
+ /// to [`PublicKeyCredentialUserEntity::id`] to avoid accidentally overwriting existing credentials that
+ /// have been previously registered.
+ ///
+ /// Creates a `PublicKeyCredentialCreationOptions` that prefers the authenticator to create a server-side
+ /// credential without requiring user verification. A five-minute timeout is set. [`Extension::cred_props`]
+ /// is [`ExtensionReq::Allow`]. [`Self::mediation`] is [`CredentialMediationRequirement::Optional`].
+ ///
+ /// Note some authenticators require user verification during credential registration (e.g.,
+ /// [CTAP 2.0 authenticators](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#authenticatorMakeCredential)).
+ /// When an authenticator supports both CTAP 2.0 and
+ /// [Universal 2nd Factor (U2F)](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-overview-v1.2-ps-20170411.html#registration-creating-a-key-pair)
+ /// protocols, user agents will sometimes fall back to U2F when [`UserVerificationRequirement::Discouraged`]
+ /// is requested since the latter allows for registration without user verification. If the user agent does not
+ /// do this, then users will have an inconsistent experience when authenticating an already-registered
+ /// credential. If this is undesirable, one can use [`UserVerificationRequirement::Required`] for this and
+ /// [`PublicKeyCredentialRequestOptions::user_verification`] at the expense of requiring a user to verify
+ /// themselves twice: once for the first factor and again here.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{register::{
+ /// # PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
+ /// # }, AsciiDomain, RpId};
+ /// assert_eq!(
+ /// PublicKeyCredentialCreationOptions::second_factor(
+ /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
+ /// PublicKeyCredentialUserEntity {
+ /// name: "carl.gauss".try_into()?,
+ /// id: (&UserHandle::new()).into(),
+ /// display_name: Some("Johann Carl Friedrich Gauß".try_into()?),
+ /// },
+ /// Vec::new()
+ /// )
+ /// .timeout
+ /// .get(),
+ /// 300_000
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn second_factor<'a: 'rp_id, 'b: 'user_name, 'c: 'user_display_name, 'd: 'user_handle>(
+ rp_id: &'a RpId,
+ user: PublicKeyCredentialUserEntity<'b, 'c, &'d [u8]>,
+ exclude_credentials: Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
+ ) -> Self {
+ let mut opts = Self::passkey(rp_id, user, exclude_credentials);
+ opts.authenticator_selection = AuthenticatorSelectionCriteria::second_factor();
+ opts.extensions.cred_props = Some(ExtensionReq::Allow);
+ opts.extensions.cred_protect = CredProtect::None;
+ opts
+ }
+ /// Begins the [registration ceremony](https://www.w3.org/TR/webauthn-3/#registration-ceremony) consuming
+ /// `self`. Note that the expiration [`Instant`]/[`SystemTime`] is saved, so `RegistrationClientState` MUST be
+ /// sent ASAP. In order to complete registration, the returned `RegistrationServerState` MUST be saved so that
+ /// it can later be used to verify the new credential with [`RegistrationServerState::verify`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` contains incompatible configuration.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # #[cfg(not(feature = "serializable_server_state"))]
+ /// # use std::time::Instant;
+ /// # #[cfg(not(feature = "serializable_server_state"))]
+ /// # use webauthn_rp::request::ServerState;
+ /// # use webauthn_rp::request::{
+ /// # register::{PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle},
+ /// # AsciiDomain, RpId
+ /// # };
+ /// # #[cfg(not(feature = "serializable_server_state"))]
+ /// assert!(
+ /// PublicKeyCredentialCreationOptions::passkey(
+ /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
+ /// PublicKeyCredentialUserEntity {
+ /// name: "bernard.riemann".try_into()?,
+ /// id: (&UserHandle::new()).into(),
+ /// display_name: Some("Georg Friedrich Bernhard Riemann".try_into()?)
+ /// },
+ /// Vec::new()
+ /// ).start_ceremony()?.0.expiration() > Instant::now()
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ pub fn start_ceremony(
+ mut self,
+ ) -> Result<
+ (
+ RegistrationServerState,
+ RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_handle>,
+ ),
+ CreationOptionsErr,
+ > {
+ validate_options_helper(self.authenticator_selection, self.extensions).and_then(|()| {
+ #[cfg(not(feature = "serializable_server_state"))]
+ let now = Instant::now();
+ #[cfg(feature = "serializable_server_state")]
+ let now = SystemTime::now();
+ now.checked_add(Duration::from_millis(NonZeroU64::from(self.timeout).get()))
+ .ok_or(CreationOptionsErr::InvalidTimeout)
+ .map(|expiration| {
+ // We remove duplicates. The order has no significance, so this is OK.
+ self.exclude_credentials
+ .sort_unstable_by(|a, b| a.id.as_ref().cmp(b.id.as_ref()));
+ self.exclude_credentials
+ .dedup_by(|a, b| a.id.as_ref() == b.id.as_ref());
+ (
+ RegistrationServerState {
+ mediation: self.mediation,
+ challenge: SentChallenge(self.challenge.0),
+ pub_key_cred_params: self.pub_key_cred_params,
+ authenticator_selection: self.authenticator_selection,
+ extensions: self.extensions,
+ expiration,
+ },
+ RegistrationClientState(self),
+ )
+ })
+ })
+ }
+}
+/// Container of a [`PublicKeyCredentialCreationOptions`] that has been used to start the registration ceremony.
+/// This gets sent to the client ASAP.
+#[derive(Debug)]
+pub struct RegistrationClientState<'rp_id, 'user_name, 'user_display_name, 'user_handle>(
+ PublicKeyCredentialCreationOptions<'rp_id, 'user_name, 'user_display_name, 'user_handle>,
+);
+impl RegistrationClientState<'_, '_, '_, '_> {
+ /// Returns the `PublicKeyCredentialCreationOptions` that was used to start a registration ceremony.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{register::{
+ /// # CoseAlgorithmIdentifiers, PublicKeyCredentialCreationOptions,
+ /// # PublicKeyCredentialUserEntity, UserHandle
+ /// # }, AsciiDomain, RpId};
+ /// assert_eq!(
+ /// PublicKeyCredentialCreationOptions::passkey(
+ /// &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
+ /// PublicKeyCredentialUserEntity {
+ /// name: "david.hilbert".try_into()?,
+ /// id: (&UserHandle::new()).into(),
+ /// display_name: Some("David Hilbert".try_into()?)
+ /// },
+ /// Vec::new()
+ /// )
+ /// .start_ceremony()?
+ /// .1
+ /// .options()
+ /// .rp_id.as_ref(),
+ /// "example.com"
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn options(&self) -> &PublicKeyCredentialCreationOptions<'_, '_, '_, '_> {
+ &self.0
+ }
+}
+/// Additional verification options to perform in [`RegistrationServerState::verify`].
+#[derive(Clone, Copy, Debug)]
+pub struct RegistrationVerificationOptions<'origins, 'top_origins, O, T> {
+ /// Origins to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
+ ///
+ /// When this is empty, the origin that will be used will be based on
+ /// the [`RpId`] passed to [`RegistrationServerState::verify`]. If [`RpId::Domain`], then the [`DomainOrigin`] returned from
+ /// passing [`AsciiDomain::as_ref`] to [`DomainOrigin::new`] will be used; otherwise the [`Url`] in
+ /// [`RpId::Url`] will be used.
+ pub allowed_origins: &'origins [O],
+ /// [Top-level origins](https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin)
+ /// to use for [origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin).
+ ///
+ /// When this is `Some`, [`CollectedClientData::cross_origin`] is allowed to be `true`. When the contained
+ /// `slice` is empty, [`CollectedClientData::top_origin`] must be `None`. When this is `None`,
+ /// `CollectedClientData::cross_origin` must be `false` and `CollectedClientData::top_origin` must be `None`.
+ pub allowed_top_origins: Option<&'top_origins [T]>,
+ /// The required [`Backup`] state of the credential.
+ pub backup_requirement: BackupReq,
+ /// Error when unsolicited extensions are sent back iff `true`.
+ pub error_on_unsolicited_extensions: bool,
+ /// [`AuthenticatorAttachment`] must be sent iff `true`.
+ pub require_authenticator_attachment: bool,
+ /// [`CollectedClientData::from_client_data_json_relaxed`] is used to extract [`CollectedClientData`] iff `true`.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ pub client_data_json_relaxed: bool,
+}
+impl<O, T> Default for RegistrationVerificationOptions<'_, '_, O, T> {
+ /// Returns `Self` such that [`Self::allowed_origins`] is empty, [`Self::allowed_top_origins`] is `None`,
+ /// [`Self::backup_requirement`] is [`BackupReq::None`], [`Self::error_on_unsolicited_extensions`] is `true`,
+ /// [`Self::require_authenticator_attachment`] is `false`, and [`Self::client_data_json_relaxed`] is
+ /// `true`.
+ #[inline]
+ fn default() -> Self {
+ Self {
+ allowed_origins: &[],
+ allowed_top_origins: None,
+ backup_requirement: BackupReq::default(),
+ error_on_unsolicited_extensions: true,
+ require_authenticator_attachment: false,
+ #[cfg(feature = "serde_relaxed")]
+ client_data_json_relaxed: true,
+ }
+ }
+}
+// This is essentially the `PublicKeyCredentialCreationOptions` used to create it; however to reduce
+// memory usage, we remove all unnecessary data making an instance of this 48 bytes in size on
+// `x86_64-unknown-linux-gnu` platforms.
+/// State needed to be saved when beginning the registration ceremony.
+///
+/// Saves the necessary information associated with the [`PublicKeyCredentialCreationOptions`] used to create it
+/// via [`PublicKeyCredentialCreationOptions::start_ceremony`] so that registration of a new credential can be
+/// performed with [`Self::verify`].
+///
+/// `RegistrationServerState` implements [`Borrow`] of [`SentChallenge`]; thus to obtain the correct
+/// `RegistrationServerState` associated with a [`Registration`], one should use its corresponding
+/// [`Registration::challenge`].
+#[derive(Debug)]
+pub struct RegistrationServerState {
+ /// [`mediation`](https://www.w3.org/TR/credential-management-1/#dom-credentialcreationoptions-mediation).
+ mediation: CredentialMediationRequirement,
+ // This is a `SentChallenge` since we need `RegistrationServerState` to be fetchable after receiving the
+ // response from the client. This response must obviously be constructable; thus its challenge is a
+ // `SentChallenge`.
+ //
+ // This must never be mutated since we want to ensure it is actually a `Challenge` (which
+ // can only be constructed via `Challenge::new`). This is guaranteed to be true iff
+ // `serializable_server_state` is not enabled. We avoid implementing `trait`s like `Hash` when that
+ // is enabled.
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge).
+ challenge: SentChallenge,
+ /// [`pubKeyCredParams`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-pubkeycredparams).
+ pub_key_cred_params: CoseAlgorithmIdentifiers,
+ /// [`authenticatorSelection`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection).
+ authenticator_selection: AuthenticatorSelectionCriteria,
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions).
+ extensions: Extension,
+ /// `Instant` the ceremony expires.
+ #[cfg(not(feature = "serializable_server_state"))]
+ expiration: Instant,
+ /// `SystemTime` the ceremony expires.
+ #[cfg(feature = "serializable_server_state")]
+ expiration: SystemTime,
+}
+impl RegistrationServerState {
+ /// Verifies `response` is valid based on `self` consuming `self` and returning a `RegisteredCredential` that
+ /// borrows the necessary data from `response` as well as borrowing `user_handle`.
+ ///
+ /// `rp_id` and `user_handle` MUST be the same as the [`PublicKeyCredentialCreationOptions::rp_id`] and
+ /// [`PublicKeyCredentialUserEntity::id`] used when starting the ceremony.
+ ///
+ /// It is _essential_ to ensure [`RegisteredCredential::id`] has not been previously registered; if
+ /// so, the ceremony SHOULD be aborted and a failure reported. When saving `RegisteredCredential`, one may
+ /// want to save the [`RpId`] and [`PublicKeyCredentialUserEntity`] information; however since [`RpId`] is
+ /// likely static, that may not be necessary. User information is also likely static for a given [`UserHandle`]
+ /// (which is saved in `RegisteredCredential`); so if such info is saved, one may want to save it once per
+ /// `UserHandle` and not per `RegisteredCredential`.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `response` is not valid according to the
+ /// [registration ceremony criteria](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential)
+ /// or violates any of the settings in `options`.
+ #[inline]
+ pub fn verify<'a, 'b, O: PartialEq<Origin<'b>>, T: PartialEq<Origin<'b>>>(
+ self,
+ rp_id: &RpId,
+ user_handle: UserHandle<&'a [u8]>,
+ response: &'b Registration,
+ options: &RegistrationVerificationOptions<'_, '_, O, T>,
+ ) -> Result<RegisteredCredential<'b, 'a>, RegCeremonyErr> {
+ // [Registration ceremony](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential)
+ // is handled by:
+ //
+ // 1. Calling code.
+ // 2. Client code and the construction of `resp` (hopefully via [`Registration::deserialize`]).
+ // 3. Client code and the construction of `resp` (hopefully via [`AuthenticatorAttestation::deserialize`]).
+ // 4. Client code and the construction of `resp` (hopefully via [`ClientExtensionsOutputs::deserialize`]).
+ // 5. [`Self::partial_validate`].
+ // 6. [`Self::partial_validate`].
+ // 7. [`Self::partial_validate`].
+ // 8. [`Self::partial_validate`].
+ // 9. [`Self::partial_validate`].
+ // 10. [`Self::partial_validate`].
+ // 11. [`Self::partial_validate`].
+ // 12. [`Self::partial_validate`].
+ // 13. [`Self::partial_validate`].
+ // 14. [`Self::partial_validate`].
+ // 15. Below.
+ // 16. [`Self::partial_validate`].
+ // 17. [`Self::partial_validate`].
+ // 18. [`Self::partial_validate`].
+ // 19. [`Self::partial_validate`].
+ // 20. Below.
+ // 21. [`Self::partial_validate`].
+ // 22. [`Self::partial_validate`].
+ // 23. N/A since only none and self attestations are supported.
+ // 24. Always satisfied since only none and self attestations are supported (Item 3 is N/A).
+ // 25. [`Self::partial_validate`].
+ // 26. Calling code.
+ // 27. Below.
+ // 28. N/A since only none and self attestations are supported.
+ // 29. Below.
+
+ // Steps 5–14, 16–19, 21–22, and 25.
+ self.partial_validate(rp_id, response, (), &options.into())
+ .map_err(RegCeremonyErr::from)
+ .and_then(|attestation_object| {
+ let auth_data = attestation_object.auth_data();
+ let flags = auth_data.flags();
+ // Step 15.
+ if matches!(self.mediation, CredentialMediationRequirement::Conditional)
+ || flags.user_present
+ {
+ self.authenticator_selection
+ // Verify any required authenticator attachment modality.
+ .validate(
+ options.require_authenticator_attachment,
+ response.authenticator_attachment,
+ )
+ .and_then(|()| {
+ let attested_credential_data = auth_data.attested_credential_data();
+ self.pub_key_cred_params
+ // Step 20.
+ .validate(attested_credential_data.credential_public_key)
+ .and_then(|()| {
+ let extensions = auth_data.extensions();
+ // Step 27.
+ self.extensions
+ .validate(
+ response.client_extension_results,
+ extensions,
+ options.error_on_unsolicited_extensions,
+ )
+ .map_err(RegCeremonyErr::Extension)
+ .and_then(|()| {
+ // Step 29.
+ RegisteredCredential::new(
+ attested_credential_data.credential_id,
+ response.response.transports(),
+ user_handle,
+ StaticState {
+ credential_public_key: attested_credential_data
+ .credential_public_key,
+ extensions:
+ AuthenticatorExtensionOutputStaticState {
+ cred_protect: extensions.cred_protect,
+ hmac_secret: extensions.hmac_secret,
+ },
+ },
+ DynamicState {
+ user_verified: flags.user_verified,
+ backup: flags.backup,
+ sign_count: auth_data.sign_count(),
+ authenticator_attachment: response
+ .authenticator_attachment,
+ },
+ Metadata {
+ attestation: match attestation_object
+ .attestation()
+ {
+ AttestationFormat::None => {
+ Attestation::None
+ }
+ AttestationFormat::Packed(_) => {
+ Attestation::Surrogate
+ }
+ },
+ aaguid: attested_credential_data.aaguid,
+ extensions: extensions.into(),
+ client_extension_results: response
+ .client_extension_results,
+ resident_key: self
+ .authenticator_selection
+ .resident_key,
+ },
+ )
+ .map_err(RegCeremonyErr::Credential)
+ })
+ })
+ })
+ } else {
+ Err(RegCeremonyErr::UserNotPresent)
+ }
+ })
+ }
+}
+impl ServerState for RegistrationServerState {
+ #[cfg(any(doc, not(feature = "serializable_server_state")))]
+ #[inline]
+ fn expiration(&self) -> Instant {
+ self.expiration
+ }
+ #[cfg(all(not(doc), feature = "serializable_server_state"))]
+ #[inline]
+ fn expiration(&self) -> SystemTime {
+ self.expiration
+ }
+ #[inline]
+ fn sent_challenge(&self) -> SentChallenge {
+ self.challenge
+ }
+}
+impl Ceremony for RegistrationServerState {
+ type R = Registration;
+ fn rand_challenge(&self) -> SentChallenge {
+ self.challenge
+ }
+ #[cfg(not(feature = "serializable_server_state"))]
+ fn expiry(&self) -> Instant {
+ self.expiration
+ }
+ #[cfg(feature = "serializable_server_state")]
+ fn expiry(&self) -> SystemTime {
+ self.expiration
+ }
+ fn user_verification(&self) -> UserVerificationRequirement {
+ self.authenticator_selection.user_verification
+ }
+}
+impl Borrow<SentChallenge> for RegistrationServerState {
+ #[inline]
+ fn borrow(&self) -> &SentChallenge {
+ &self.challenge
+ }
+}
+impl PartialEq for RegistrationServerState {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.challenge == other.challenge
+ }
+}
+impl PartialEq<&Self> for RegistrationServerState {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<RegistrationServerState> for &RegistrationServerState {
+ #[inline]
+ fn eq(&self, other: &RegistrationServerState) -> bool {
+ **self == *other
+ }
+}
+impl Eq for RegistrationServerState {}
+impl Hash for RegistrationServerState {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.challenge.hash(state);
+ }
+}
+impl PartialOrd for RegistrationServerState {
+ #[inline]
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+impl Ord for RegistrationServerState {
+ #[inline]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.challenge.cmp(&other.challenge)
+ }
+}
diff --git a/src/request/register/bin.rs b/src/request/register/bin.rs
@@ -0,0 +1,126 @@
+extern crate alloc;
+use super::{
+ super::super::bin::{Decode, Encode},
+ Nickname, NicknameErr, UserHandle, UserHandleErr, Username, UsernameErr,
+};
+use alloc::borrow::Cow;
+use core::{
+ convert::Infallible,
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+impl<T: AsRef<[u8]>> Encode for UserHandle<T> {
+ type Output<'a> = &'a [u8] where Self: 'a;
+ type Err = Infallible;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ Ok(self.as_ref())
+ }
+}
+impl Decode for UserHandle<Vec<u8>> {
+ type Input<'a> = Vec<u8>;
+ type Err = UserHandleErr;
+ #[inline]
+ fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ match UserHandle::<&[u8]>::from_slice(input.as_slice()) {
+ Ok(_) => Ok(Self(input)),
+ Err(e) => Err(e),
+ }
+ }
+}
+impl Encode for Nickname<'_> {
+ type Output<'a> = &'a str where Self: 'a;
+ type Err = Infallible;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ Ok(self.as_ref())
+ }
+}
+/// Error returned from [`Nickname::decode`].
+#[derive(Clone, Copy, Debug)]
+pub enum DecodeNicknameErr {
+ /// Variant returned when the encoded data could not be decoded
+ /// into a [`Nickname`].
+ Nickname(NicknameErr),
+ /// Variant returned when the [`Nickname`] was not encoded
+ /// into its canonical form.
+ NotCanonical,
+}
+impl Display for DecodeNicknameErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Nickname(e) => e.fmt(f),
+ Self::NotCanonical => f.write_str("Nickname was not encoded in its canonical form"),
+ }
+ }
+}
+impl Error for DecodeNicknameErr {}
+impl Decode for Nickname<'_> {
+ type Input<'a> = String;
+ type Err = DecodeNicknameErr;
+ #[inline]
+ fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ match Nickname::try_from(input.as_str()).map_err(DecodeNicknameErr::Nickname) {
+ Ok(v) => match v.0 {
+ Cow::Borrowed(name) => {
+ if name == input.as_str() {
+ Ok(Self(Cow::Owned(input)))
+ } else {
+ Err(DecodeNicknameErr::NotCanonical)
+ }
+ }
+ Cow::Owned(_) => Err(DecodeNicknameErr::NotCanonical),
+ },
+ Err(e) => Err(e),
+ }
+ }
+}
+impl Encode for Username<'_> {
+ type Output<'a> = &'a str where Self: 'a;
+ type Err = Infallible;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ Ok(self.as_ref())
+ }
+}
+/// Error returned from [`Username::decode`].
+#[derive(Clone, Copy, Debug)]
+pub enum DecodeUsernameErr {
+ /// Variant returned when the encoded data could not be decoded
+ /// into a [`Username`].
+ Username(UsernameErr),
+ /// Variant returned when the [`Username`] was not encoded
+ /// into its canonical form.
+ NotCanonical,
+}
+impl Display for DecodeUsernameErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Username(e) => e.fmt(f),
+ Self::NotCanonical => f.write_str("Username was not encoded in its canonical form"),
+ }
+ }
+}
+impl Error for DecodeUsernameErr {}
+impl Decode for Username<'_> {
+ type Input<'a> = String;
+ type Err = DecodeUsernameErr;
+ #[inline]
+ fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ match Username::try_from(input.as_str()).map_err(DecodeUsernameErr::Username) {
+ Ok(v) => match v.0 {
+ Cow::Borrowed(name) => {
+ if name == input.as_str() {
+ Ok(Self(Cow::Owned(input)))
+ } else {
+ Err(DecodeUsernameErr::NotCanonical)
+ }
+ }
+ Cow::Owned(_) => Err(DecodeUsernameErr::NotCanonical),
+ },
+ Err(e) => Err(e),
+ }
+ }
+}
diff --git a/src/request/register/custom.rs b/src/request/register/custom.rs
@@ -0,0 +1,18 @@
+use super::{UserHandle, UserHandleErr};
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for UserHandle<&'b [u8]> {
+ type Error = UserHandleErr;
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ Self::from_slice(value)
+ }
+}
+impl TryFrom<Vec<u8>> for UserHandle<Vec<u8>> {
+ type Error = UserHandleErr;
+ #[inline]
+ fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
+ match UserHandle::<&[u8]>::try_from(value.as_slice()) {
+ Ok(_) => Ok(Self(value)),
+ Err(e) => Err(e),
+ }
+ }
+}
diff --git a/src/request/register/error.rs b/src/request/register/error.rs
@@ -0,0 +1,86 @@
+#[cfg(doc)]
+use super::{
+ AuthenticatorSelectionCriteria, CredProtect, Extension, Nickname,
+ PublicKeyCredentialCreationOptions, UserHandle, UserVerificationRequirement, Username,
+ USER_HANDLE_MAX_LEN, USER_HANDLE_MIN_LEN,
+};
+use core::{
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+#[cfg(doc)]
+use std::time::{Instant, SystemTime};
+/// Error returned by [`Nickname::try_from`].
+#[derive(Clone, Copy, Debug)]
+pub enum NicknameErr {
+ /// Error returned when the [Nickname Enforcement rule](https://www.rfc-editor.org/rfc/rfc8266#section-2.3)
+ /// fails.
+ Rfc8266,
+ /// Error returned when the length of the transformed string would exceed [`Nickname::MAX_LEN`].
+ Len,
+}
+impl Display for NicknameErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::Rfc8266 => "nickname does not conform to RFC 8266",
+ Self::Len => "length of nickname is too long",
+ })
+ }
+}
+impl Error for NicknameErr {}
+/// Error returned by [`Username::try_from`].
+#[derive(Clone, Copy, Debug)]
+pub enum UsernameErr {
+ /// Error returned when the
+ /// [UsernameCasePreserved Enforcement rule](https://www.rfc-editor.org/rfc/rfc8265#section-3.4.3) fails.
+ Rfc8265,
+ /// Error returned when the length of the transformed string would exceed [`Nickname::MAX_LEN`].
+ Len,
+}
+impl Display for UsernameErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::Rfc8265 => "nickname does not conform to RFC 8265",
+ Self::Len => "length of username is too long",
+ })
+ }
+}
+impl Error for UsernameErr {}
+/// Error returned from [`UserHandle::rand`] when a `UserHandle` was attempted to be created
+/// with less than [`USER_HANDLE_MIN_LEN`] or more than [`USER_HANDLE_MAX_LEN`] bytes.
+#[derive(Clone, Copy, Debug)]
+pub struct UserHandleErr;
+impl Display for UserHandleErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(
+ "user handle of less than 16 bytes or more than 64 bytes was attempted to be created",
+ )
+ }
+}
+impl Error for UserHandleErr {}
+/// Error returned by [`PublicKeyCredentialCreationOptions::start_ceremony`].
+#[derive(Clone, Copy, Debug)]
+pub enum CreationOptionsErr {
+ /// Error when [`Extension::prf`] is [`Some`] but [`AuthenticatorSelectionCriteria::user_verification`] is not
+ /// [`UserVerificationRequirement::Required`].
+ PrfWithoutUserVerification,
+ /// Error when [`Extension::cred_protect`] is [`CredProtect::UserVerificationRequired`] but [`AuthenticatorSelectionCriteria::user_verification`] is not
+ /// [`UserVerificationRequirement::Required`].
+ CredProtectRequiredWithoutUserVerification,
+ /// [`PublicKeyCredentialCreationOptions::timeout`] could not be added to [`Instant::now`] or [`SystemTime::now`].
+ InvalidTimeout,
+}
+impl Display for CreationOptionsErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::PrfWithoutUserVerification => "prf extension was requested without requiring user verification",
+ Self::CredProtectRequiredWithoutUserVerification => "credProtect extension with a value of user verification required was requested without requiring user verification",
+ Self::InvalidTimeout => "the timeout could not be added to the current Instant",
+ })
+ }
+}
+impl Error for CreationOptionsErr {}
diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs
@@ -0,0 +1,1094 @@
+#![expect(
+ clippy::question_mark_used,
+ clippy::unseparated_literal_suffix,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+extern crate alloc;
+use super::{
+ super::super::BASE64URL_NOPAD_ENC, AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria,
+ CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, CrossPlatformHint, Extension,
+ ExtensionInfo, Hint, Nickname, PlatformHint, PublicKeyCredentialUserEntity,
+ RegistrationClientState, ResidentKeyRequirement, RpId, UserHandle, Username,
+ USER_HANDLE_MAX_LEN, USER_HANDLE_MIN_LEN,
+};
+use alloc::borrow::Cow;
+#[cfg(doc)]
+use core::str::FromStr;
+use core::{
+ fmt::{self, Formatter},
+ marker::PhantomData,
+ str,
+};
+use serde::{
+ de::{Deserialize, Deserializer, Error, MapAccess, Unexpected, Visitor},
+ ser::{Serialize, SerializeSeq as _, SerializeStruct, Serializer},
+};
+impl Serialize for Nickname<'_> {
+ /// Serializes `self` as a [`prim@str`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::Nickname;
+ /// assert_eq!(
+ /// serde_json::to_string(&Nickname::try_from("Giuseppe Luigi Lagrangia")?).unwrap(),
+ /// r#""Giuseppe Luigi Lagrangia""#
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.0.as_ref())
+ }
+}
+impl Serialize for Username<'_> {
+ /// Serializes `self` as a [`prim@str`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::Username;
+ /// assert_eq!(
+ /// serde_json::to_string(&Username::try_from("john.von.neumann")?).unwrap(),
+ /// r#""john.von.neumann""#
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.0.as_ref())
+ }
+}
+/// [EdDSA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
+const EDDSA: i16 = -8i16;
+/// [ES256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
+const ES256: i16 = -7i16;
+/// [ES384](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
+const ES384: i16 = -35i16;
+/// [RS256](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
+const RS256: i16 = -257i16;
+impl Serialize for CoseAlgorithmIdentifier {
+ /// Serializes `self` into a `struct` based on
+ /// [`PublicKeyCredentialParameters`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialparameters).
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("PublicKeyCredentialParameters", 2)
+ .and_then(|mut ser| {
+ ser.serialize_field("type", "public-key").and_then(|()| {
+ ser.serialize_field(
+ "alg",
+ &match *self {
+ Self::Eddsa => EDDSA,
+ Self::Es256 => ES256,
+ Self::Es384 => ES384,
+ Self::Rs256 => RS256,
+ },
+ )
+ .and_then(|()| ser.end())
+ })
+ })
+ }
+}
+impl Serialize for CoseAlgorithmIdentifiers {
+ /// Serializes `self` to conform with
+ /// [`pubKeyCredParams`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-pubkeycredparams).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{CoseAlgorithmIdentifier,CoseAlgorithmIdentifiers};
+ /// assert_eq!(
+ /// serde_json::to_string(&CoseAlgorithmIdentifiers::ALL)?,
+ /// r#"[{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},{"type":"public-key","alg":-257}]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&CoseAlgorithmIdentifiers::default().remove(CoseAlgorithmIdentifier::Es384))?,
+ /// r#"[{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-257}]"#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[expect(
+ clippy::as_conversions,
+ reason = "u8::count_ones returns a u32, and it's always going to fit in a usize"
+ )]
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // `self.0.count_ones()` is at most 4 which is guaranteed to be a valid `usize`.
+ serializer
+ .serialize_seq(Some(self.0.count_ones() as usize))
+ .and_then(|mut ser| {
+ if self.contains(CoseAlgorithmIdentifier::Eddsa) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Eddsa)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(CoseAlgorithmIdentifier::Es256) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Es256)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(CoseAlgorithmIdentifier::Es384) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Es384)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(CoseAlgorithmIdentifier::Rs256) {
+ ser.serialize_element(&CoseAlgorithmIdentifier::Rs256)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| ser.end())
+ })
+ })
+ })
+ })
+ }
+}
+/// `"name"`.
+const NAME: &str = "name";
+/// `"id"`.
+const ID: &str = "id";
+/// `newtype` around `RpId` to be used to serialize `PublicKeyCredentialRpEntity`.
+struct PublicKeyCredentialRpEntity<'a>(&'a RpId);
+impl Serialize for PublicKeyCredentialRpEntity<'_> {
+ /// Serializes `self` to conform with
+ /// [`PublicKeyCredentialRpEntity`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrpentity).
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("PublicKeyCredentialRpEntity", 2)
+ .and_then(|mut ser| {
+ ser.serialize_field(NAME, self.0)
+ .and_then(|()| ser.serialize_field(ID, self.0).and_then(|()| ser.end()))
+ })
+ }
+}
+impl<T: AsRef<[u8]>> Serialize for UserHandle<T> {
+ /// Serializes `self` to conform with
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentityjson-id).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::UserHandle;
+ /// # #[cfg(feature = "custom")]
+ /// // We create this manually purely for example. One should almost always
+ /// // randomly generate this (e.g., `UserHandle::new`).
+ /// let id = UserHandle::try_from(vec![0])?;
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(serde_json::to_string(&id).unwrap(), r#""AA""#);
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(BASE64URL_NOPAD_ENC.encode(self.0.as_ref()).as_str())
+ }
+}
+/// `"displayName"`.
+const DISPLAY_NAME: &str = "displayName";
+impl<T: AsRef<[u8]>> Serialize for PublicKeyCredentialUserEntity<'_, '_, T> {
+ /// Serializes `self` to conform with
+ /// [`PublicKeyCredentialUserEntityJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentityjson).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{PublicKeyCredentialUserEntity, UserHandle};
+ /// # #[cfg(feature = "custom")]
+ /// // We create this manually purely for example. One should almost always
+ /// // randomly generate this (e.g., `UserHandle::new`).
+ /// let id = UserHandle::try_from(vec![0])?;
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(
+ /// serde_json::to_string(&PublicKeyCredentialUserEntity {
+ /// name: "georg.cantor".try_into()?,
+ /// id: id.clone(),
+ /// display_name: Some("Гео́рг Ка́нтор".try_into()?),
+ /// })
+ /// .unwrap(),
+ /// r#"{"name":"georg.cantor","id":"AA","displayName":"Гео́рг Ка́нтор"}"#
+ /// );
+ /// // The display name gets serialized as an empty string
+ /// // iff `Self::display_name` is `None`.
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(
+ /// serde_json::to_string(&PublicKeyCredentialUserEntity {
+ /// name: "georg.cantor".try_into()?,
+ /// id,
+ /// display_name: None,
+ /// })
+ /// .unwrap(),
+ /// r#"{"name":"georg.cantor","id":"AA","displayName":""}"#
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("PublicKeyCredentialUserEntity", 3)
+ .and_then(|mut ser| {
+ ser.serialize_field(NAME, &self.name).and_then(|()| {
+ ser.serialize_field(ID, &self.id).and_then(|()| {
+ ser.serialize_field(
+ DISPLAY_NAME,
+ self.display_name.as_ref().map_or("", |val| val.as_ref()),
+ )
+ .and_then(|()| ser.end())
+ })
+ })
+ })
+ }
+}
+impl Serialize for ResidentKeyRequirement {
+ /// Serializes `self` to conform with
+ /// [`ResidentKeyRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::ResidentKeyRequirement;
+ /// assert_eq!(
+ /// serde_json::to_string(&ResidentKeyRequirement::Required)?,
+ /// r#""required""#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&ResidentKeyRequirement::Discouraged)?,
+ /// r#""discouraged""#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&ResidentKeyRequirement::Preferred)?,
+ /// r#""preferred""#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(match *self {
+ Self::Required => "required",
+ Self::Discouraged => "discouraged",
+ Self::Preferred => "preferred",
+ })
+ }
+}
+impl Serialize for CrossPlatformHint {
+ /// Serializes `self` to conform with
+ /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::CrossPlatformHint;
+ /// assert_eq!(
+ /// serde_json::to_string(&CrossPlatformHint::None)?,
+ /// r#"[]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&CrossPlatformHint::SecurityKey)?,
+ /// r#"["security-key"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&CrossPlatformHint::Hybrid)?,
+ /// r#"["hybrid"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&CrossPlatformHint::SecurityKeyHybrid)?,
+ /// r#"["security-key","hybrid"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&CrossPlatformHint::HybridSecurityKey)?,
+ /// r#"["hybrid","security-key"]"#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ Hint::from(*self).serialize(serializer)
+ }
+}
+impl Serialize for PlatformHint {
+ /// Serializes `self` to conform with
+ /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::PlatformHint;
+ /// assert_eq!(
+ /// serde_json::to_string(&PlatformHint::None)?,
+ /// r#"[]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&PlatformHint::ClientDevice)?,
+ /// r#"["client-device"]"#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ Hint::from(*self).serialize(serializer)
+ }
+}
+impl Serialize for AuthenticatorSelectionCriteria {
+ /// Serializes `self` to conform with
+ /// [`AuthenticatorSelectionCriteria`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, CrossPlatformHint};
+ /// assert_eq!(
+ /// serde_json::to_string(&AuthenticatorSelectionCriteria::passkey())?,
+ /// r#"{"residentKey":"required","requireResidentKey":true,"userVerification":"required"}"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&AuthenticatorSelectionCriteria::second_factor())?,
+ /// r#"{"residentKey":"discouraged","requireResidentKey":false,"userVerification":"discouraged"}"#
+ /// );
+ /// let mut crit = AuthenticatorSelectionCriteria::passkey();
+ /// crit.authenticator_attachment = AuthenticatorAttachmentReq::CrossPlatform(
+ /// CrossPlatformHint::SecurityKey,
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&crit)?,
+ /// r#"{"authenticatorAttachment":"cross-platform","residentKey":"required","requireResidentKey":true,"userVerification":"required"}"#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let count = if matches!(
+ self.authenticator_attachment,
+ AuthenticatorAttachmentReq::None(_)
+ ) {
+ 3
+ } else {
+ 4
+ };
+ serializer
+ .serialize_struct("AuthenticatorSelectionCriteria", count)
+ .and_then(|mut ser| {
+ if count == 3 {
+ Ok(())
+ } else {
+ ser.serialize_field(
+ "authenticatorAttachment",
+ if matches!(
+ self.authenticator_attachment,
+ AuthenticatorAttachmentReq::Platform(_)
+ ) {
+ "platform"
+ } else {
+ "cross-platform"
+ },
+ )
+ }
+ .and_then(|()| {
+ ser.serialize_field("residentKey", &self.resident_key)
+ .and_then(|()| {
+ ser.serialize_field(
+ "requireResidentKey",
+ &matches!(self.resident_key, ResidentKeyRequirement::Required),
+ )
+ .and_then(|()| {
+ ser.serialize_field("userVerification", &self.user_verification)
+ .and_then(|()| ser.end())
+ })
+ })
+ })
+ })
+ }
+}
+/// Helper that serializes prf registration information to conform with
+/// [`AuthenticationExtensionsPRFInputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfinputs).
+///
+/// Since CTAP 2.2 does not allow PRF evaluation at creation time, we send an empty map.
+struct Prf;
+impl Serialize for Prf {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("Prf", 0)
+ .and_then(SerializeStruct::end)
+ }
+}
+impl Serialize for Extension {
+ /// Serializes `self` to conform with
+ /// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{
+ /// # register::{CredProtect, Extension},
+ /// # ExtensionInfo, ExtensionReq,
+ /// # };
+ /// assert_eq!(serde_json::to_string(&Extension::default())?, r#"{}"#);
+ /// assert_eq!(
+ /// serde_json::to_string(&Extension {
+ /// cred_props: Some(ExtensionReq::Allow),
+ /// cred_protect: CredProtect::UserVerificationRequired(ExtensionInfo::RequireEnforceValue),
+ /// min_pin_length: Some((16, ExtensionInfo::AllowDontEnforceValue)),
+ /// prf: Some(ExtensionInfo::AllowEnforceValue)
+ /// })?,
+ /// r#"{"credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":true,"minPinLength":true,"prf":{}}"#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[expect(
+ clippy::arithmetic_side_effects,
+ reason = "comment explains how overflow is not possible"
+ )]
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ /// `credProps` key name.
+ const CRED_PROPS: &str = "credProps";
+ /// `minPinLength` key name.
+ const MIN_PIN_LENGTH: &str = "minPinLength";
+ /// `prf` key name.
+ const PRF: &str = "prf";
+ // The max is 1 + 2 + 1 + 1 = 5, so overflow is no concern.
+ let count = usize::from(self.cred_props.is_some())
+ + if matches!(self.cred_protect, CredProtect::None) {
+ 0
+ } else {
+ 2
+ }
+ + usize::from(self.min_pin_length.is_some())
+ + usize::from(self.prf.is_some());
+ serializer
+ .serialize_struct("Extension", count)
+ .and_then(|mut ser| {
+ self.cred_props
+ .map_or(Ok(()), |_| ser.serialize_field(CRED_PROPS, &true))
+ .and_then(|()| {
+ if matches!(self.cred_protect, CredProtect::None) {
+ Ok(())
+ } else {
+ let ext_info;
+ // [`credProtect`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension)
+ // is serialized by serializing its fields directly and not as a map of fields.
+ ser.serialize_field(
+ "credentialProtectionPolicy",
+ match self.cred_protect {
+ CredProtect::None => unreachable!(
+ "Extensions is incorrectly serializing credProtect"
+ ),
+ CredProtect::UserVerificationOptional(info) => {
+ ext_info = info;
+ "userVerificationOptional"
+ }
+ CredProtect::UserVerificationOptionalWithCredentialIdList(
+ info,
+ ) => {
+ ext_info = info;
+ "userVerificationOptionalWithCredentialIDList"
+ }
+ CredProtect::UserVerificationRequired(info) => {
+ ext_info = info;
+ "userVerificationRequired"
+ }
+ },
+ )
+ .and_then(|()| {
+ ser.serialize_field(
+ "enforceCredentialProtectionPolicy",
+ &matches!(
+ ext_info,
+ ExtensionInfo::RequireEnforceValue
+ | ExtensionInfo::AllowEnforceValue
+ ),
+ )
+ })
+ }
+ .and_then(|()| {
+ self.min_pin_length
+ .map_or(Ok(()), |_| ser.serialize_field(MIN_PIN_LENGTH, &true))
+ .and_then(|()| {
+ self.prf
+ .map_or(Ok(()), |_| ser.serialize_field(PRF, &Prf))
+ .and_then(|()| ser.end())
+ })
+ })
+ })
+ })
+ }
+}
+impl Serialize for RegistrationClientState<'_, '_, '_, '_> {
+ /// Serializes `self` to conform with
+ /// [`PublicKeyCredentialCreationOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptionsjson).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
+ /// # use webauthn_rp::{
+ /// # request::{
+ /// # register::{
+ /// # AuthenticatorAttachmentReq, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, UserHandle
+ /// # },
+ /// # AsciiDomain, ExtensionInfo, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
+ /// # },
+ /// # response::{AuthTransports, CredentialId},
+ /// # };
+ /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
+ /// /// from the database.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
+ /// // ⋮
+ /// # AuthTransports::decode(32)
+ /// }
+ /// let mut creds = Vec::with_capacity(1);
+ /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
+ /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
+ /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let transports = get_transports((&id).into())?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// creds.push(PublicKeyCredentialDescriptor { id, transports });
+ /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+ /// let user_handle = UserHandle::new();
+ /// let mut options = PublicKeyCredentialCreationOptions::passkey(&rp_id, PublicKeyCredentialUserEntity { name: "pierre.de.fermat".try_into()?, id: (&user_handle).into(), display_name: Some("Pierre de Fermat".try_into()?) }, creds);
+ /// options.authenticator_selection.authenticator_attachment = AuthenticatorAttachmentReq::None(Hint::SecurityKey);
+ /// options.extensions.min_pin_length = Some((16, ExtensionInfo::RequireEnforceValue));
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap();
+ /// let json = serde_json::json!({
+ /// "rp":{
+ /// "name":"example.com",
+ /// "id":"example.com"
+ /// },
+ /// "user":{
+ /// "name":"pierre.de.fermat",
+ /// "id":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ /// "displayName":"Pierre de Fermat"
+ /// },
+ /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA",
+ /// "pubKeyCredParams":[
+ /// {
+ /// "type":"public-key",
+ /// "alg":-8
+ /// },
+ /// {
+ /// "type":"public-key",
+ /// "alg":-7
+ /// },
+ /// {
+ /// "type":"public-key",
+ /// "alg":-35
+ /// },
+ /// {
+ /// "type":"public-key",
+ /// "alg":-257
+ /// },
+ /// ],
+ /// "timeout":300000,
+ /// "excludeCredentials":[
+ /// {
+ /// "type":"public-key",
+ /// "id":"AAAAAAAAAAAAAAAAAAAAAA",
+ /// "transports":["usb"]
+ /// }
+ /// ],
+ /// "authenticatorSelection":{
+ /// "residentKey":"required",
+ /// "requireResidentKey":true,
+ /// "userVerification":"required"
+ /// },
+ /// "hints":[
+ /// "security-key"
+ /// ],
+ /// "attestation":"none",
+ /// "attestationFormats":[
+ /// "none"
+ /// ],
+ /// "extensions":{
+ /// "credentialProtectionPolicy":"userVerificationRequired",
+ /// "enforceCredentialProtectionPolicy":true,
+ /// "minPinLength":true
+ /// }
+ /// }).to_string();
+ /// // Since `Challenge`s are randomly generated, we don't know what it will be.
+ /// // Similarly since we randomly generated a 64-byte `UserHandle`, we don't know what
+ /// // it will be; thus we test the JSON string for everything except those two.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(client_state.get(..88), json.get(..88));
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(client_state.get(174..212), json.get(174..212));
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(client_state.get(245..), json.get(245..));
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ /// "none".
+ const NONE: &str = "none";
+ serializer
+ .serialize_struct("RegistrationClientState", 11)
+ .and_then(|mut ser| {
+ ser.serialize_field("rp", &PublicKeyCredentialRpEntity(self.0.rp_id))
+ .and_then(|()| {
+ ser.serialize_field("user", &self.0.user).and_then(|()| {
+ ser.serialize_field("challenge", &self.0.challenge)
+ .and_then(|()| {
+ ser.serialize_field(
+ "pubKeyCredParams",
+ &self.0.pub_key_cred_params,
+ )
+ .and_then(|()| {
+ ser.serialize_field("timeout", &self.0.timeout).and_then(
+ |()| {
+ ser.serialize_field(
+ "excludeCredentials",
+ self.0.exclude_credentials.as_slice(),
+ )
+ .and_then(|()| {
+ ser.serialize_field(
+ "authenticatorSelection",
+ &self.0.authenticator_selection,
+ )
+ .and_then(|()| {
+ ser.serialize_field("hints", &match self.0.authenticator_selection.authenticator_attachment {
+ AuthenticatorAttachmentReq::None(hint) => hint,
+ AuthenticatorAttachmentReq::Platform(hint) => hint.into(),
+ AuthenticatorAttachmentReq::CrossPlatform(hint) => hint.into(),
+ }).and_then(|()| {
+ ser.serialize_field("attestation", NONE).and_then(|()| {
+ ser.serialize_field("attestationFormats", [NONE].as_slice()).and_then(|()| {
+ ser.serialize_field("extensions", &self.0.extensions).and_then(|()| ser.end())
+ })
+ })
+ })
+ })
+ })
+ },
+ )
+ })
+ })
+ })
+ })
+ })
+ }
+}
+impl<'de: 'a, 'a> Deserialize<'de> for Nickname<'a> {
+ /// Deserializes [`prim@str`] and parses it according to [`Self::try_from`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::Nickname;
+ /// assert_eq!(
+ /// serde_json::from_str::<Nickname>(r#""Henri Poincaré""#)?.as_ref(),
+ /// "Henri Poincaré"
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ ///```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `Nickname`.
+ struct NicknameVisitor<'b>(PhantomData<fn() -> &'b ()>);
+ impl<'d: 'b, 'b> Visitor<'d> for NicknameVisitor<'b> {
+ type Value = Nickname<'b>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("Nickname")
+ }
+ fn visit_borrowed_str<E>(self, v: &'d str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Nickname::try_from(v).map_err(E::custom)
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Nickname::try_from(v)
+ .map_err(E::custom)
+ .map(|name| Nickname(Cow::Owned(name.0.into_owned())))
+ }
+ }
+ deserializer.deserialize_str(NicknameVisitor(PhantomData))
+ }
+}
+impl<'de: 'a, 'a> Deserialize<'de> for Username<'a> {
+ /// Deserializes [`prim@str`] and parses it according to [`Self::try_from`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::Username;
+ /// assert_eq!(
+ /// serde_json::from_str::<Username>(r#""augustin.cauchy""#)?.as_ref(),
+ /// "augustin.cauchy"
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ ///```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `Username`.
+ struct UsernameVisitor<'b>(PhantomData<fn() -> &'b ()>);
+ impl<'d: 'b, 'b> Visitor<'d> for UsernameVisitor<'b> {
+ type Value = Username<'b>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("Username")
+ }
+ fn visit_borrowed_str<E>(self, v: &'d str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Username::try_from(v).map_err(E::custom)
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Username::try_from(v)
+ .map_err(E::custom)
+ .map(|name| Username(Cow::Owned(name.0.into_owned())))
+ }
+ }
+ deserializer.deserialize_str(UsernameVisitor(PhantomData))
+ }
+}
+impl<'de> Deserialize<'de> for UserHandle<Vec<u8>> {
+ /// Deserializes [`prim@str`] based on
+ /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-userhandle).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::UserHandle;
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(
+ /// serde_json::from_str::<UserHandle<Vec<u8>>>(r#""AA""#).unwrap(),
+ /// UserHandle::try_from(vec![0; 1])?
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ ///```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `UserHandle`.
+ struct UserHandleVisitor;
+ impl Visitor<'_> for UserHandleVisitor {
+ type Value = UserHandle<Vec<u8>>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("UserHandle")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ // `USER_HANDLE_MIN_LEN` and `USER_HANDLE_MAX_LEN` are less than
+ // `0x4000`, so this won't `panic`.
+ if (crate::base64url_nopad_len(USER_HANDLE_MIN_LEN)
+ ..=crate::base64url_nopad_len(USER_HANDLE_MAX_LEN))
+ .contains(&v.len())
+ {
+ BASE64URL_NOPAD_ENC
+ .decode(v.as_bytes())
+ .map_err(E::custom)
+ .map(UserHandle)
+ } else {
+ Err(E::invalid_value(
+ Unexpected::Str(v),
+ &"1 to 64 bytes encoded in base64url without padding",
+ ))
+ }
+ }
+ }
+ deserializer.deserialize_str(UserHandleVisitor)
+ }
+}
+impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de>
+ for PublicKeyCredentialUserEntity<'name, 'display_name, Vec<u8>>
+{
+ /// Deserializes a `struct` based on
+ /// [`PublicKeyCredentialUserEntityJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentityjson).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::register::{Nickname, PublicKeyCredentialUserEntity};
+ /// assert_eq!(
+ /// serde_json::from_str::<PublicKeyCredentialUserEntity<Vec<u8>>>(
+ /// serde_json::json!({
+ /// "name": "pythagoras.of.samos",
+ /// "id": "AA",
+ /// "displayName": "Πυθαγόρας ο Σάμιος"
+ /// })
+ /// .to_string()
+ /// .as_str()
+ /// )?
+ /// .display_name
+ /// .as_ref()
+ /// .map(Nickname::as_ref),
+ /// Some("Πυθαγόρας ο Σάμιος")
+ /// );
+ /// // Display name is `None` iff the empty string was sent.
+ /// assert!(
+ /// serde_json::from_str::<PublicKeyCredentialUserEntity<Vec<u8>>>(
+ /// serde_json::json!({
+ /// "name": "pythagoras.of.samos",
+ /// "id": "AA",
+ /// "displayName": ""
+ /// })
+ /// .to_string()
+ /// .as_str()
+ /// )?
+ /// .display_name.is_none()
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ ///```
+ #[expect(clippy::too_many_lines, reason = "116 lines is fine")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `PublicKeyCredentialUserEntity`.
+ 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>>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("PublicKeyCredentialUserEntity")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Fields for `PublicKeyCredentialUserEntityJSON`.
+ enum Field {
+ /// `"name"` field.
+ Name,
+ /// `"id"` field.
+ Id,
+ /// `"displayName"` field.
+ DisplayName,
+ }
+ impl<'e> Deserialize<'e> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("'name', 'id', or 'displayName'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ NAME => Ok(Field::Name),
+ ID => Ok(Field::Id),
+ DISPLAY_NAME => Ok(Field::DisplayName),
+ _ => Err(E::unknown_field(v, FIELDS)),
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ /// `display_name`.
+ struct DisplayName<'c>(Option<Nickname<'c>>);
+ impl<'e: 'c, 'c> Deserialize<'e> for DisplayName<'c> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `DisplayName`.
+ struct DisplayNameVisitor<'a>(PhantomData<fn() -> &'a ()>);
+ impl<'d: 'a, 'a> Visitor<'d> for DisplayNameVisitor<'a> {
+ type Value = DisplayName<'a>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("Nickname or empty string")
+ }
+ fn visit_borrowed_str<E>(self, v: &'d str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v.is_empty() {
+ Ok(DisplayName(None))
+ } else {
+ Nickname::try_from(v)
+ .map_err(E::custom)
+ .map(|name| DisplayName(Some(name)))
+ }
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v.is_empty() {
+ Ok(DisplayName(None))
+ } else {
+ Nickname::try_from(v).map_err(E::custom).map(|name| {
+ DisplayName(Some(Nickname(Cow::Owned(name.0.into_owned()))))
+ })
+ }
+ }
+ }
+ deserializer.deserialize_str(DisplayNameVisitor(PhantomData))
+ }
+ }
+ let mut nam = None;
+ let mut ident = None;
+ let mut display = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::Name => {
+ if nam.is_some() {
+ return Err(Error::duplicate_field(NAME));
+ }
+ nam = Some(map.next_value()?);
+ }
+ Field::Id => {
+ if ident.is_some() {
+ return Err(Error::duplicate_field(ID));
+ }
+ ident = map.next_value().map(Some)?;
+ }
+ Field::DisplayName => {
+ if display.is_some() {
+ return Err(Error::duplicate_field(DISPLAY_NAME));
+ }
+ display = map.next_value::<DisplayName<'_>>().map(|val| Some(val.0))?;
+ }
+ }
+ }
+ nam.ok_or_else(|| Error::missing_field(NAME))
+ .and_then(|name| {
+ ident
+ .ok_or_else(|| Error::missing_field(ID))
+ .and_then(|id| {
+ display
+ .ok_or_else(|| Error::missing_field(DISPLAY_NAME))
+ .map(|display_name| PublicKeyCredentialUserEntity {
+ name,
+ id,
+ display_name,
+ })
+ })
+ })
+ }
+ }
+ /// Fields for `PublicKeyCredentialUserEntityJSON`.
+ const FIELDS: &[&str; 3] = &[NAME, ID, DISPLAY_NAME];
+ deserializer.deserialize_struct(
+ "PublicKeyCredentialUserEntity",
+ FIELDS,
+ PublicKeyCredentialUserEntityVisitor(PhantomData),
+ )
+ }
+}
+impl<'de> Deserialize<'de> for CoseAlgorithmIdentifier {
+ /// Deserializes [`i16`] based on
+ /// [COSE Algorithms](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `CoseAlgorithmIdentifier`.
+ ///
+ /// We visit all signed integral types sans `i8` just in case a `Deserializer` only implements one of them.
+ struct CoseAlgorithmIdentifierVisitor;
+ impl Visitor<'_> for CoseAlgorithmIdentifierVisitor {
+ type Value = CoseAlgorithmIdentifier;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("CoseAlgorithmIdentifier")
+ }
+ fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ EDDSA => Ok(CoseAlgorithmIdentifier::Eddsa),
+ ES256 => Ok(CoseAlgorithmIdentifier::Es256),
+ ES384 => Ok(CoseAlgorithmIdentifier::Es384),
+ RS256 => Ok(CoseAlgorithmIdentifier::Rs256),
+ _ => Err(E::invalid_value(
+ Unexpected::Signed(i64::from(v)),
+ &format!("{EDDSA}, {ES256}, {ES384}, or {RS256}").as_str(),
+ )),
+ }
+ }
+ fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ i16::try_from(v)
+ .map_err(E::custom)
+ .and_then(|val| self.visit_i16(val))
+ }
+ fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ i16::try_from(v)
+ .map_err(E::custom)
+ .and_then(|val| self.visit_i16(val))
+ }
+ }
+ deserializer.deserialize_i16(CoseAlgorithmIdentifierVisitor)
+ }
+}
diff --git a/src/request/register/ser_server_state.rs b/src/request/register/ser_server_state.rs
@@ -0,0 +1,262 @@
+#![expect(
+ clippy::unseparated_literal_suffix,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+use super::{
+ super::super::bin::{
+ Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible as _,
+ },
+ AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, CoseAlgorithmIdentifiers,
+ CredProtect, CredentialMediationRequirement, CrossPlatformHint, Extension, ExtensionInfo, Hint,
+ PlatformHint, RegistrationServerState, ResidentKeyRequirement, SentChallenge,
+ UserVerificationRequirement,
+};
+use core::{
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+use std::time::{SystemTime, SystemTimeError};
+impl EncodeBuffer for CoseAlgorithmIdentifiers {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.0.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for CoseAlgorithmIdentifiers {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| {
+ if val | Self::ALL.0 == Self::ALL.0 {
+ Ok(Self(val))
+ } else {
+ Err(EncDecErr)
+ }
+ })
+ }
+}
+impl EncodeBuffer for PlatformHint {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::None => 0u8,
+ Self::ClientDevice => 1,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for PlatformHint {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Ok(Self::None),
+ 1 => Ok(Self::ClientDevice),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for CrossPlatformHint {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::None => 0u8,
+ Self::SecurityKey => 1,
+ Self::Hybrid => 2,
+ Self::SecurityKeyHybrid => 3,
+ Self::HybridSecurityKey => 4,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for CrossPlatformHint {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Ok(Self::None),
+ 1 => Ok(Self::SecurityKey),
+ 2 => Ok(Self::Hybrid),
+ 3 => Ok(Self::SecurityKeyHybrid),
+ 4 => Ok(Self::HybridSecurityKey),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for AuthenticatorAttachmentReq {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::None(hint) => {
+ 0u8.encode_into_buffer(buffer);
+ hint.encode_into_buffer(buffer);
+ }
+ Self::Platform(hint) => {
+ 1u8.encode_into_buffer(buffer);
+ hint.encode_into_buffer(buffer);
+ }
+ Self::CrossPlatform(hint) => {
+ 2u8.encode_into_buffer(buffer);
+ hint.encode_into_buffer(buffer);
+ }
+ }
+ }
+}
+impl<'a> DecodeBuffer<'a> for AuthenticatorAttachmentReq {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Hint::decode_from_buffer(data).map(Self::None),
+ 1 => PlatformHint::decode_from_buffer(data).map(Self::Platform),
+ 2 => CrossPlatformHint::decode_from_buffer(data).map(Self::CrossPlatform),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for AuthenticatorSelectionCriteria {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.authenticator_attachment.encode_into_buffer(buffer);
+ self.resident_key.encode_into_buffer(buffer);
+ self.user_verification.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for AuthenticatorSelectionCriteria {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ AuthenticatorAttachmentReq::decode_from_buffer(data).and_then(|authenticator_attachment| {
+ ResidentKeyRequirement::decode_from_buffer(data).and_then(|resident_key| {
+ UserVerificationRequirement::decode_from_buffer(data).map(|user_verification| {
+ Self {
+ authenticator_attachment,
+ resident_key,
+ user_verification,
+ }
+ })
+ })
+ })
+ }
+}
+impl EncodeBuffer for CredProtect {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::None => 0u8.encode_into_buffer(buffer),
+ Self::UserVerificationOptional(info) => {
+ 1u8.encode_into_buffer(buffer);
+ info.encode_into_buffer(buffer);
+ }
+ Self::UserVerificationOptionalWithCredentialIdList(info) => {
+ 2u8.encode_into_buffer(buffer);
+ info.encode_into_buffer(buffer);
+ }
+ Self::UserVerificationRequired(info) => {
+ 3u8.encode_into_buffer(buffer);
+ info.encode_into_buffer(buffer);
+ }
+ }
+ }
+}
+impl<'a> DecodeBuffer<'a> for CredProtect {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Ok(Self::None),
+ 1 => ExtensionInfo::decode_from_buffer(data).map(Self::UserVerificationOptional),
+ 2 => ExtensionInfo::decode_from_buffer(data)
+ .map(Self::UserVerificationOptionalWithCredentialIdList),
+ 3 => ExtensionInfo::decode_from_buffer(data).map(Self::UserVerificationRequired),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for Extension {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.cred_props.encode_into_buffer(buffer);
+ self.cred_protect.encode_into_buffer(buffer);
+ self.min_pin_length.encode_into_buffer(buffer);
+ self.prf.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for Extension {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ Option::decode_from_buffer(data).and_then(|cred_props| {
+ CredProtect::decode_from_buffer(data).and_then(|cred_protect| {
+ Option::decode_from_buffer(data).and_then(|min_pin_length| {
+ Option::decode_from_buffer(data).map(|prf| Self {
+ cred_props,
+ cred_protect,
+ min_pin_length,
+ prf,
+ })
+ })
+ })
+ })
+ }
+}
+impl Encode for RegistrationServerState {
+ type Output<'a>
+ = Vec<u8>
+ where
+ Self: 'a;
+ type Err = SystemTimeError;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ // Length of the anticipated most common output:
+ // * 1 for `CredentialMediationRequirement`
+ // * 16 for `SentChallenge`
+ // * 1 for `CoseAlgorithmIdentifiers`
+ // * 6 for `AuthenticatorSelectionCriteria`
+ // * 4–8 for `Extension` where we assume 4 is the most common
+ // * 12 for `SystemTime`
+ let mut buffer = Vec::with_capacity(1 + 16 + 1 + 6 + 4 + 12);
+ self.mediation.encode_into_buffer(&mut buffer);
+ self.challenge.encode_into_buffer(&mut buffer);
+ self.pub_key_cred_params.encode_into_buffer(&mut buffer);
+ self.authenticator_selection.encode_into_buffer(&mut buffer);
+ self.extensions.encode_into_buffer(&mut buffer);
+ self.expiration
+ .encode_into_buffer(&mut buffer)
+ .map(|()| buffer)
+ }
+}
+/// Error returned from [`RegistrationServerState::decode`].
+#[derive(Clone, Copy, Debug)]
+pub enum DecodeRegistrationServerStateErr {
+ /// Variant returned when there was trailing data after decoding a [`RegistrationServerState`].
+ TrailingData,
+ /// Variant returned for all other errors.
+ Other,
+}
+impl Display for DecodeRegistrationServerStateErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::TrailingData => "trailing data existed after decoding a RegistrationServerState",
+ Self::Other => "RegistrationServerState could not be decoded",
+ })
+ }
+}
+impl Error for DecodeRegistrationServerStateErr {}
+impl Decode for RegistrationServerState {
+ type Input<'a> = &'a [u8];
+ type Err = DecodeRegistrationServerStateErr;
+ #[inline]
+ fn decode(mut input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ CredentialMediationRequirement::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|mediation| {
+ SentChallenge::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|challenge| {
+ CoseAlgorithmIdentifiers::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|pub_key_cred_params| {
+ AuthenticatorSelectionCriteria::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(
+ |authenticator_selection| {
+ Extension::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|extensions| {
+ super::validate_options_helper(authenticator_selection, extensions)
+ .map_err(|_e| DecodeRegistrationServerStateErr::Other)
+ .and_then(|()| {
+ SystemTime::decode_from_buffer(&mut input).map_err(|_e| DecodeRegistrationServerStateErr::Other).and_then(|expiration| {
+ if input.is_empty() {
+ Ok(Self { mediation, challenge, pub_key_cred_params, authenticator_selection, extensions, expiration, })
+ } else {
+ Err(DecodeRegistrationServerStateErr::TrailingData)
+ }
+ })
+ })
+ })
+ },
+ )
+ })
+ })
+ })
+ }
+}
diff --git a/src/request/ser.rs b/src/request/ser.rs
@@ -0,0 +1,319 @@
+use super::{
+ super::BASE64URL_NOPAD_ENC, Challenge, CredentialId, Hint, PublicKeyCredentialDescriptor, RpId,
+ UserVerificationRequirement,
+};
+use core::str;
+use serde::ser::{Serialize, SerializeSeq as _, SerializeStruct as _, Serializer};
+impl Serialize for Challenge {
+ /// Serializes `self` to conform with
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-challenge).
+ ///
+ /// Specifically `self` is interpreted as a little-endian array of 16 bytes that is then transformed into a
+ /// base64url-encoded string.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::Challenge;
+ /// # // `Challenge::BASE64_LEN` is 22, but we add two for the quotes.
+ /// assert_eq!(serde_json::to_string(&Challenge::new())?.len(), 24);
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
+ #[expect(
+ clippy::little_endian_bytes,
+ reason = "SentChallenge::deserialize and Challenge::serialize need to be consistent across architectures"
+ )]
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut data = [0; Self::BASE64_LEN];
+ BASE64URL_NOPAD_ENC.encode_mut(self.0.to_le_bytes().as_slice(), data.as_mut_slice());
+ serializer.serialize_str(
+ str::from_utf8(data.as_slice())
+ // There is a bug, so crash and burn.
+ .unwrap_or_else(|_| unreachable!("there is a bug in Challenge::serialize")),
+ )
+ }
+}
+impl Serialize for RpId {
+ /// Serializes `self` as a [`prim@str`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::{AsciiDomain, RpId};
+ /// assert_eq!(
+ /// serde_json::to_string(&RpId::Domain(AsciiDomain::try_from("www.example.com".to_owned()).unwrap())).unwrap(),
+ /// r#""www.example.com""#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&RpId::Url("ssh:foo".parse().unwrap())).unwrap(),
+ /// r#""ssh:foo""#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.as_ref())
+ }
+}
+impl<T> Serialize for PublicKeyCredentialDescriptor<T>
+where
+ CredentialId<T>: Serialize,
+{
+ /// Serializes `self` to conform with
+ /// [`PublicKeyCredentialDescriptorJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptorjson).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
+ /// # use webauthn_rp::{
+ /// # request::PublicKeyCredentialDescriptor,
+ /// # response::{AuthTransports, CredentialId},
+ /// # };
+ /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
+ /// /// from the database.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
+ /// // ⋮
+ /// # AuthTransports::decode(32)
+ /// }
+ /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
+ /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
+ /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let id = CredentialId::try_from(vec![0; 16])?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let transports = get_transports((&id).into())?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(
+ /// serde_json::to_string(&PublicKeyCredentialDescriptor { id, transports }).unwrap(),
+ /// r#"{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}"#
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("PublicKeyCredentialDescriptor", 3)
+ .and_then(|mut ser| {
+ ser.serialize_field("type", "public-key").and_then(|()| {
+ ser.serialize_field("id", &self.id).and_then(|()| {
+ ser.serialize_field("transports", &self.transports)
+ .and_then(|()| ser.end())
+ })
+ })
+ })
+ }
+}
+impl Serialize for UserVerificationRequirement {
+ /// Serializes `self` to conform with
+ /// [`UserVerificationRequirement`](https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::UserVerificationRequirement;
+ /// assert_eq!(
+ /// serde_json::to_string(&UserVerificationRequirement::Required)?,
+ /// r#""required""#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&UserVerificationRequirement::Discouraged)?,
+ /// r#""discouraged""#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&UserVerificationRequirement::Preferred)?,
+ /// r#""preferred""#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(match *self {
+ Self::Required => "required",
+ Self::Discouraged => "discouraged",
+ Self::Preferred => "preferred",
+ })
+ }
+}
+impl Serialize for Hint {
+ /// Serializes `self` to conform with
+ /// [`hints`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptionsjson-hints).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::Hint;
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::None)?,
+ /// r#"[]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::SecurityKey)?,
+ /// r#"["security-key"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::ClientDevice)?,
+ /// r#"["client-device"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::Hybrid)?,
+ /// r#"["hybrid"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::SecurityKeyClientDevice)?,
+ /// r#"["security-key","client-device"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::ClientDeviceSecurityKey)?,
+ /// r#"["client-device","security-key"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::SecurityKeyHybrid)?,
+ /// r#"["security-key","hybrid"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::HybridSecurityKey)?,
+ /// r#"["hybrid","security-key"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::ClientDeviceHybrid)?,
+ /// r#"["client-device","hybrid"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::HybridClientDevice)?,
+ /// r#"["hybrid","client-device"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::SecurityKeyClientDeviceHybrid)?,
+ /// r#"["security-key","client-device","hybrid"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::SecurityKeyHybridClientDevice)?,
+ /// r#"["security-key","hybrid","client-device"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::ClientDeviceSecurityKeyHybrid)?,
+ /// r#"["client-device","security-key","hybrid"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::ClientDeviceHybridSecurityKey)?,
+ /// r#"["client-device","hybrid","security-key"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::HybridSecurityKeyClientDevice)?,
+ /// r#"["hybrid","security-key","client-device"]"#
+ /// );
+ /// assert_eq!(
+ /// serde_json::to_string(&Hint::HybridClientDeviceSecurityKey)?,
+ /// r#"["hybrid","client-device","security-key"]"#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ /// [`security-key`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-security-key).
+ const SECURITY_KEY: &str = "security-key";
+ /// [`client-device`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-client-device).
+ const CLIENT_DEVICE: &str = "client-device";
+ /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialhints-hybrid).
+ const HYBRID: &str = "hybrid";
+ let count = match *self {
+ Self::None => 0,
+ Self::SecurityKey | Self::ClientDevice | Self::Hybrid => 1,
+ Self::SecurityKeyClientDevice
+ | Self::ClientDeviceSecurityKey
+ | Self::SecurityKeyHybrid
+ | Self::HybridSecurityKey
+ | Self::ClientDeviceHybrid
+ | Self::HybridClientDevice => 2,
+ Self::SecurityKeyClientDeviceHybrid
+ | Self::SecurityKeyHybridClientDevice
+ | Self::ClientDeviceSecurityKeyHybrid
+ | Self::ClientDeviceHybridSecurityKey
+ | Self::HybridSecurityKeyClientDevice
+ | Self::HybridClientDeviceSecurityKey => 3,
+ };
+ serializer.serialize_seq(Some(count)).and_then(|mut ser| {
+ match *self {
+ Self::None => Ok(()),
+ Self::SecurityKey => ser.serialize_element(SECURITY_KEY),
+ Self::ClientDevice => ser.serialize_element(CLIENT_DEVICE),
+ Self::Hybrid => ser.serialize_element(HYBRID),
+ Self::SecurityKeyClientDevice => ser
+ .serialize_element(SECURITY_KEY)
+ .and_then(|()| ser.serialize_element(CLIENT_DEVICE)),
+ Self::ClientDeviceSecurityKey => ser
+ .serialize_element(CLIENT_DEVICE)
+ .and_then(|()| ser.serialize_element(SECURITY_KEY)),
+ Self::SecurityKeyHybrid => ser
+ .serialize_element(SECURITY_KEY)
+ .and_then(|()| ser.serialize_element(HYBRID)),
+ Self::HybridSecurityKey => ser
+ .serialize_element(HYBRID)
+ .and_then(|()| ser.serialize_element(SECURITY_KEY)),
+ Self::ClientDeviceHybrid => ser
+ .serialize_element(CLIENT_DEVICE)
+ .and_then(|()| ser.serialize_element(HYBRID)),
+ Self::HybridClientDevice => ser
+ .serialize_element(HYBRID)
+ .and_then(|()| ser.serialize_element(CLIENT_DEVICE)),
+ Self::SecurityKeyClientDeviceHybrid => {
+ ser.serialize_element(SECURITY_KEY).and_then(|()| {
+ ser.serialize_element(CLIENT_DEVICE)
+ .and_then(|()| ser.serialize_element(HYBRID))
+ })
+ }
+ Self::SecurityKeyHybridClientDevice => {
+ ser.serialize_element(SECURITY_KEY).and_then(|()| {
+ ser.serialize_element(HYBRID)
+ .and_then(|()| ser.serialize_element(CLIENT_DEVICE))
+ })
+ }
+ Self::ClientDeviceSecurityKeyHybrid => {
+ ser.serialize_element(CLIENT_DEVICE).and_then(|()| {
+ ser.serialize_element(SECURITY_KEY)
+ .and_then(|()| ser.serialize_element(HYBRID))
+ })
+ }
+ Self::ClientDeviceHybridSecurityKey => {
+ ser.serialize_element(CLIENT_DEVICE).and_then(|()| {
+ ser.serialize_element(HYBRID)
+ .and_then(|()| ser.serialize_element(SECURITY_KEY))
+ })
+ }
+ Self::HybridSecurityKeyClientDevice => {
+ ser.serialize_element(HYBRID).and_then(|()| {
+ ser.serialize_element(SECURITY_KEY)
+ .and_then(|()| ser.serialize_element(CLIENT_DEVICE))
+ })
+ }
+ Self::HybridClientDeviceSecurityKey => {
+ ser.serialize_element(HYBRID).and_then(|()| {
+ ser.serialize_element(CLIENT_DEVICE)
+ .and_then(|()| ser.serialize_element(SECURITY_KEY))
+ })
+ }
+ }
+ .and_then(|()| ser.end())
+ })
+ }
+}
diff --git a/src/request/ser_server_state.rs b/src/request/ser_server_state.rs
@@ -0,0 +1,242 @@
+use super::{
+ super::bin::{DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible},
+ CredentialMediationRequirement, ExtensionInfo, ExtensionReq, Hint, SentChallenge,
+ UserVerificationRequirement,
+};
+use core::{convert::Infallible, time::Duration};
+use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};
+/// [`ExtensionInfo::RequireEnforceValue`] tag.
+const EXT_INFO_REQUIRE_ENFORCE: u8 = 0;
+/// [`ExtensionInfo::RequireDontEnforceValue`] tag.
+const EXT_INFO_REQUIRE_DONT_ENFORCE: u8 = 1;
+/// [`ExtensionInfo::AllowEnforceValue`] tag.
+const EXT_INFO_ALLOW_ENFORCE: u8 = 2;
+/// [`ExtensionInfo::AllowDontEnforceValue`] tag.
+const EXT_INFO_ALLOW_DONT_ENFORCE: u8 = 3;
+impl EncodeBuffer for ExtensionInfo {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::RequireEnforceValue => EXT_INFO_REQUIRE_ENFORCE,
+ Self::RequireDontEnforceValue => EXT_INFO_REQUIRE_DONT_ENFORCE,
+ Self::AllowEnforceValue => EXT_INFO_ALLOW_ENFORCE,
+ Self::AllowDontEnforceValue => EXT_INFO_ALLOW_DONT_ENFORCE,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for ExtensionInfo {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ EXT_INFO_REQUIRE_ENFORCE => Ok(Self::RequireEnforceValue),
+ EXT_INFO_REQUIRE_DONT_ENFORCE => Ok(Self::RequireDontEnforceValue),
+ EXT_INFO_ALLOW_ENFORCE => Ok(Self::AllowEnforceValue),
+ EXT_INFO_ALLOW_DONT_ENFORCE => Ok(Self::AllowDontEnforceValue),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+/// [`ExtensionReq::Require`] tag.
+const EXT_REQ_REQUIRE: u8 = 0;
+/// [`ExtensionReq::Allow`] tag.
+const EXT_REQ_ALLOW: u8 = 1;
+impl EncodeBuffer for ExtensionReq {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::Require => EXT_REQ_REQUIRE,
+ Self::Allow => EXT_REQ_ALLOW,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for ExtensionReq {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ EXT_REQ_REQUIRE => Ok(Self::Require),
+ EXT_REQ_ALLOW => Ok(Self::Allow),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+/// [`Hint::None`] tag.
+const HINT_NONE: u8 = 0;
+/// [`Hint::SecurityKey`] tag.
+const HINT_SEC_KEY: u8 = 1;
+/// [`Hint::ClientDevice`] tag.
+const HINT_CLIENT_DEV: u8 = 2;
+/// [`Hint::Hybrid`] tag.
+const HINT_HYBRID: u8 = 3;
+/// [`Hint::SecurityKeyClientDevice`] tag.
+const HINT_SEC_KEY_CLIENT_DEV: u8 = 4;
+/// [`Hint::ClientDeviceSecurityKey`] tag.
+const HINT_CLIENT_DEV_SEC_KEY: u8 = 5;
+/// [`Hint::SecurityKeyHybrid`] tag.
+const HINT_SEC_KEY_HYBRID: u8 = 6;
+/// [`Hint::HybridSecurityKey`] tag.
+const HINT_HYBRID_SEC_KEY: u8 = 7;
+/// [`Hint::ClientDeviceHybrid`] tag.
+const HINT_CLIENT_DEV_HYBRID: u8 = 8;
+/// [`Hint::HybridClientDevice`] tag.
+const HINT_HYBRID_CLIENT_DEV: u8 = 9;
+/// [`Hint::SecurityKeyClientDeviceHybrid`] tag.
+const HINT_SEC_KEY_CLIENT_DEV_HYBRID: u8 = 10;
+/// [`Hint::SecurityKeyHybridClientDevice`] tag.
+const HINT_SEC_KEY_HYBRID_CLIENT_DEV: u8 = 11;
+/// [`Hint::ClientDeviceSecurityKeyHybrid`] tag.
+const HINT_CLIENT_DEV_SEC_KEY_HYBRID: u8 = 12;
+/// [`Hint::ClientDeviceHybridSecurityKey`] tag.
+const HINT_CLIENT_DEV_HYBRID_SEC_KEY: u8 = 13;
+/// [`Hint::HybridSecurityKeyClientDevice`] tag.
+const HINT_HYBRID_SEC_KEY_CLIENT_DEV: u8 = 14;
+/// [`Hint::HybridClientDeviceSecurityKey`] tag.
+const HINT_HYBRID_CLIENT_DEV_SEC_KEY: u8 = 15;
+impl EncodeBuffer for Hint {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::None => HINT_NONE,
+ Self::SecurityKey => HINT_SEC_KEY,
+ Self::ClientDevice => HINT_CLIENT_DEV,
+ Self::Hybrid => HINT_HYBRID,
+ Self::SecurityKeyClientDevice => HINT_SEC_KEY_CLIENT_DEV,
+ Self::ClientDeviceSecurityKey => HINT_CLIENT_DEV_SEC_KEY,
+ Self::SecurityKeyHybrid => HINT_SEC_KEY_HYBRID,
+ Self::HybridSecurityKey => HINT_HYBRID_SEC_KEY,
+ Self::ClientDeviceHybrid => HINT_CLIENT_DEV_HYBRID,
+ Self::HybridClientDevice => HINT_HYBRID_CLIENT_DEV,
+ Self::SecurityKeyClientDeviceHybrid => HINT_SEC_KEY_CLIENT_DEV_HYBRID,
+ Self::SecurityKeyHybridClientDevice => HINT_SEC_KEY_HYBRID_CLIENT_DEV,
+ Self::ClientDeviceSecurityKeyHybrid => HINT_CLIENT_DEV_SEC_KEY_HYBRID,
+ Self::ClientDeviceHybridSecurityKey => HINT_CLIENT_DEV_HYBRID_SEC_KEY,
+ Self::HybridSecurityKeyClientDevice => HINT_HYBRID_SEC_KEY_CLIENT_DEV,
+ Self::HybridClientDeviceSecurityKey => HINT_HYBRID_CLIENT_DEV_SEC_KEY,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for Hint {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ HINT_NONE => Ok(Self::None),
+ HINT_SEC_KEY => Ok(Self::SecurityKey),
+ HINT_CLIENT_DEV => Ok(Self::ClientDevice),
+ HINT_HYBRID => Ok(Self::Hybrid),
+ HINT_SEC_KEY_CLIENT_DEV => Ok(Self::SecurityKeyClientDevice),
+ HINT_CLIENT_DEV_SEC_KEY => Ok(Self::ClientDeviceSecurityKey),
+ HINT_SEC_KEY_HYBRID => Ok(Self::SecurityKeyHybrid),
+ HINT_HYBRID_SEC_KEY => Ok(Self::HybridSecurityKey),
+ HINT_CLIENT_DEV_HYBRID => Ok(Self::ClientDeviceHybrid),
+ HINT_HYBRID_CLIENT_DEV => Ok(Self::HybridClientDevice),
+ HINT_SEC_KEY_CLIENT_DEV_HYBRID => Ok(Self::SecurityKeyClientDeviceHybrid),
+ HINT_SEC_KEY_HYBRID_CLIENT_DEV => Ok(Self::SecurityKeyHybridClientDevice),
+ HINT_CLIENT_DEV_SEC_KEY_HYBRID => Ok(Self::ClientDeviceSecurityKeyHybrid),
+ HINT_CLIENT_DEV_HYBRID_SEC_KEY => Ok(Self::ClientDeviceHybridSecurityKey),
+ HINT_HYBRID_SEC_KEY_CLIENT_DEV => Ok(Self::HybridSecurityKeyClientDevice),
+ HINT_HYBRID_CLIENT_DEV_SEC_KEY => Ok(Self::HybridClientDeviceSecurityKey),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for SentChallenge {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.0.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for SentChallenge {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u128::decode_from_buffer(data).map(Self)
+ }
+}
+impl Encode for SentChallenge {
+ type Output<'a> = u128 where Self: 'a;
+ type Err = Infallible;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ Ok(self.0)
+ }
+}
+/// [`UserVerificationRequirement::Required`] tag.
+const USER_VER_REQ_REQUIRED: u8 = 0;
+/// [`UserVerificationRequirement::Discouraged`] tag.
+const USER_VER_REQ_DISCOURAGED: u8 = 1;
+/// [`UserVerificationRequirement::Preferred`] tag.
+const USER_VER_REQ_PREFERRED: u8 = 2;
+impl EncodeBuffer for UserVerificationRequirement {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::Required => USER_VER_REQ_REQUIRED,
+ Self::Discouraged => USER_VER_REQ_DISCOURAGED,
+ Self::Preferred => USER_VER_REQ_PREFERRED,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for UserVerificationRequirement {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ USER_VER_REQ_REQUIRED => Ok(Self::Required),
+ USER_VER_REQ_DISCOURAGED => Ok(Self::Discouraged),
+ USER_VER_REQ_PREFERRED => Ok(Self::Preferred),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+/// [`CredentialMediationRequirement::Silent`] tag.
+const CRED_MED_REQ_SILENT: u8 = 0;
+/// [`CredentialMediationRequirement::Optional`] tag.
+const CRED_MED_REQ_OPTIONAL: u8 = 1;
+/// [`CredentialMediationRequirement::Conditional`] tag.
+const CRED_MED_REQ_CONDITIONAL: u8 = 2;
+/// [`CredentialMediationRequirement::Required`] tag.
+const CRED_MED_REQ_REQUIRED: u8 = 3;
+impl EncodeBuffer for CredentialMediationRequirement {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::Silent => CRED_MED_REQ_SILENT,
+ Self::Optional => CRED_MED_REQ_OPTIONAL,
+ Self::Conditional => CRED_MED_REQ_CONDITIONAL,
+ Self::Required => CRED_MED_REQ_REQUIRED,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for CredentialMediationRequirement {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ CRED_MED_REQ_SILENT => Ok(Self::Silent),
+ CRED_MED_REQ_OPTIONAL => Ok(Self::Optional),
+ CRED_MED_REQ_CONDITIONAL => Ok(Self::Conditional),
+ CRED_MED_REQ_REQUIRED => Ok(Self::Required),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBufferFallible for SystemTime {
+ type Err = SystemTimeError;
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Err> {
+ self.duration_since(UNIX_EPOCH).map(|dur| {
+ dur.as_secs().encode_into_buffer(buffer);
+ dur.subsec_nanos().encode_into_buffer(buffer);
+ })
+ }
+}
+impl<'a> DecodeBuffer<'a> for SystemTime {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u64::decode_from_buffer(data).and_then(|secs| {
+ u32::decode_from_buffer(data).and_then(|nanos| {
+ if nanos < 1_000_000_000 {
+ UNIX_EPOCH
+ .checked_add(Duration::new(secs, nanos))
+ .ok_or(EncDecErr)
+ } else {
+ Err(EncDecErr)
+ }
+ })
+ })
+ }
+}
diff --git a/src/response.rs b/src/response.rs
@@ -0,0 +1,1684 @@
+extern crate alloc;
+use crate::{
+ request::{register::{PublicKeyCredentialUserEntity, UserHandle}, Challenge, RpId, Url},
+ response::{
+ auth::error::{
+ AuthCeremonyErr, AuthenticatorDataErr as AuthAuthDataErr,
+ AuthenticatorExtensionOutputErr as AuthAuthExtErr,
+ },
+ error::{CollectedClientDataErr, CredentialIdErr},
+ register::error::{AttestationObjectErr, AttestedCredentialDataErr, AuthenticatorDataErr as RegAuthDataErr, AuthenticatorExtensionOutputErr as RegAuthExtErr, PubKeyErr, RegCeremonyErr},
+ },
+ BASE64URL_NOPAD_ENC,
+};
+use alloc::borrow::Cow;
+use core::{
+ borrow::Borrow,
+ cmp::Ordering,
+ convert::Infallible,
+ fmt::{self, Display, Formatter},
+ hash::{Hash, Hasher},
+ str,
+};
+use rsa::sha2::{digest::OutputSizeUser as _, Sha256};
+#[cfg(feature = "serde_relaxed")]
+use ser_relaxed::SerdeJsonErr;
+/// Contains functionality for completing the
+/// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony).
+///
+/// # Examples
+///
+/// ```no_run
+/// # use data_encoding::BASE64URL_NOPAD;
+/// # #[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},
+/// # response::{auth::{error::AuthCeremonyErr, Authentication}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKey, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId},
+/// # AuthenticatedCredential, CredentialErr
+/// # };
+/// # #[derive(Debug)]
+/// # enum E {
+/// # CollectedClientData(CollectedClientDataErr),
+/// # RpId(AsciiDomainErr),
+/// # RequestOptions(RequestOptionsErr),
+/// # SerdeJson(serde_json::Error),
+/// # MissingUserHandle,
+/// # MissingCeremony,
+/// # UnknownCredential,
+/// # Credential(CredentialErr),
+/// # AuthCeremony(AuthCeremonyErr),
+/// # }
+/// # impl From<AsciiDomainErr> for E {
+/// # fn from(value: AsciiDomainErr) -> Self {
+/// # Self::RpId(value)
+/// # }
+/// # }
+/// # impl From<CollectedClientDataErr> for E {
+/// # fn from(value: CollectedClientDataErr) -> Self {
+/// # Self::CollectedClientData(value)
+/// # }
+/// # }
+/// # impl From<RequestOptionsErr> for E {
+/// # fn from(value: RequestOptionsErr) -> Self {
+/// # Self::RequestOptions(value)
+/// # }
+/// # }
+/// # impl From<serde_json::Error> for E {
+/// # fn from(value: serde_json::Error) -> Self {
+/// # Self::SerdeJson(value)
+/// # }
+/// # }
+/// # impl From<CredentialErr> for E {
+/// # fn from(value: CredentialErr) -> Self {
+/// # Self::Credential(value)
+/// # }
+/// # }
+/// # impl From<AuthCeremonyErr> for E {
+/// # fn from(value: AuthCeremonyErr) -> Self {
+/// # Self::AuthCeremony(value)
+/// # }
+/// # }
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// let mut ceremonies = FixedCapHashSet::new(128);
+/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+/// let (server, client) = PublicKeyCredentialRequestOptions::passkey(&rp_id).start_ceremony()?;
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// assert!(matches!(
+/// ceremonies.insert_or_replace_all_expired(server),
+/// InsertResult::Success
+/// ));
+/// # #[cfg(feature = "serde")]
+/// let authentication = serde_json::from_str::<Authentication>(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)?;
+/// # #[cfg(feature = "serde")]
+/// let (static_state, dynamic_state) = get_credential(authentication.raw_id(), user_handle).ok_or(E::UnknownCredential)?;
+/// # #[cfg(all(feature = "custom", feature = "serde"))]
+/// let mut cred = AuthenticatedCredential::new(authentication.raw_id(), user_handle, static_state, dynamic_state)?;
+/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom", feature = "serde"))]
+/// if ceremonies.take(&authentication.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, &authentication, &mut cred, &AuthenticationVerificationOptions::<&str, &str>::default())? {
+/// update_cred(authentication.raw_id(), cred.dynamic_state());
+/// }
+/// /// Send `AuthenticationClientState` and receive `Authentication` JSON from client.
+/// # #[cfg(feature = "serde")]
+/// fn get_authentication_json(client: AuthenticationClientState<'_, '_>) -> String {
+/// // ⋮
+/// # let client_data_json = BASE64URL_NOPAD.encode(serde_json::json!({
+/// # "type": "webauthn.get",
+/// # "challenge": client.options().challenge,
+/// # "origin": format!("https://{}", client.options().rp_id.as_ref()),
+/// # "crossOrigin": false
+/// # }).to_string().as_bytes());
+/// # serde_json::json!({
+/// # "id": "AAAAAAAAAAAAAAAAAAAAAA",
+/// # "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+/// # "response": {
+/// # "clientDataJSON": client_data_json,
+/// # "authenticatorData": "",
+/// # "signature": "",
+/// # "userHandle": "AA"
+/// # },
+/// # "clientExtensionResults": {},
+/// # "type": "public-key"
+/// # }).to_string()
+/// }
+/// /// Gets the `AuthenticatedCredential` parts associated with `id` and `user_handle` from the database.
+/// fn get_credential(id: CredentialId<&[u8]>, user_handle: UserHandle<&[u8]>) -> Option<(StaticState<CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>>, DynamicState)> {
+/// // ⋮
+/// # Some((StaticState { credential_public_key: CompressedPubKey::Ed25519(Ed25519PubKey::from([0; 32])), extensions: AuthenticatorExtensionOutputStaticState { cred_protect: CredentialProtectionPolicy::UserVerificationRequired, hmac_secret: None, } }, DynamicState { user_verified: true, backup: Backup::NotEligible, sign_count: 1, authenticator_attachment: AuthenticatorAttachment::None }))
+/// }
+/// /// Updates the current `DynamicState` associated with `id` in the database to
+/// /// `dyn_state`.
+/// fn update_cred(id: CredentialId<&[u8]>, dyn_state: DynamicState) {
+/// // ⋮
+/// }
+/// # Ok::<_, E>(())
+/// ```
+pub mod auth;
+/// Contains functionality to (de)serialize data to a data store.
+#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+#[cfg(feature = "bin")]
+pub mod bin;
+/// Contains constants useful for
+/// [CTAP2 canonical CBOR encoding form](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#ctap2-canonical-cbor-encoding-form).
+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;
+/// Contains error types.
+pub mod error;
+/// Contains functionality for completing the
+/// [registration ceremony](https://www.w3.org/TR/webauthn-3/#registration-ceremony).
+///
+/// # Examples
+///
+/// ```no_run
+/// # use data_encoding::BASE64URL_NOPAD;
+/// # #[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},
+/// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData},
+/// # RegisteredCredential
+/// # };
+/// # #[derive(Debug)]
+/// # enum E {
+/// # CollectedClientData(CollectedClientDataErr),
+/// # RpId(AsciiDomainErr),
+/// # CreationOptions(CreationOptionsErr),
+/// # SerdeJson(serde_json::Error),
+/// # MissingCeremony,
+/// # RegCeremony(RegCeremonyErr),
+/// # }
+/// # impl From<AsciiDomainErr> for E {
+/// # fn from(value: AsciiDomainErr) -> Self {
+/// # Self::RpId(value)
+/// # }
+/// # }
+/// # impl From<CollectedClientDataErr> for E {
+/// # fn from(value: CollectedClientDataErr) -> Self {
+/// # Self::CollectedClientData(value)
+/// # }
+/// # }
+/// # impl From<CreationOptionsErr> for E {
+/// # fn from(value: CreationOptionsErr) -> Self {
+/// # Self::CreationOptions(value)
+/// # }
+/// # }
+/// # impl From<serde_json::Error> for E {
+/// # fn from(value: serde_json::Error) -> Self {
+/// # Self::SerdeJson(value)
+/// # }
+/// # }
+/// # impl From<RegCeremonyErr> for E {
+/// # fn from(value: RegCeremonyErr) -> Self {
+/// # Self::RegCeremony(value)
+/// # }
+/// # }
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// let mut ceremonies = FixedCapHashSet::new(128);
+/// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
+/// let user_handle = get_user_handle();
+/// let handle = (&user_handle).into();
+/// let user = get_user_entity(handle);
+/// let creds = get_registered_credentials(handle);
+/// let (server, client) = PublicKeyCredentialCreationOptions::passkey(&rp_id, user, creds).start_ceremony()?;
+/// # #[cfg(not(feature = "serializable_server_state"))]
+/// assert!(matches!(
+/// ceremonies.insert_or_replace_all_expired(server),
+/// InsertResult::Success
+/// ));
+/// # #[cfg(feature = "serde_relaxed")]
+/// let registration = serde_json::from_str::<Registration>(get_registration_json(client).as_str())?;
+/// let ver_opts = RegistrationVerificationOptions::<&str, &str>::default();
+/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom", feature = "serde_relaxed"))]
+/// insert_cred(ceremonies.take(®istration.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, handle, ®istration, &ver_opts)?);
+/// /// Extract `UserHandle` from session cookie if this is not the first credential registered.
+/// fn get_user_handle() -> UserHandle<Vec<u8>> {
+/// // ⋮
+/// # UserHandle::new()
+/// }
+/// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`.
+/// ///
+/// /// If this is the first time a credential is being registered, then `PublicKeyCredentialUserEntity`
+/// /// will need to be constructed with `name` and `display_name` passed from the client and `UserHandle::new`
+/// /// used for `id`. Once created, this info can be stored such that the entity information
+/// /// does not need to be requested for subsequent registrations.
+/// fn get_user_entity(user: UserHandle<&[u8]>) -> PublicKeyCredentialUserEntity<&[u8]> {
+/// // ⋮
+/// # PublicKeyCredentialUserEntity {
+/// # name: "foo".try_into().unwrap(),
+/// # id: user,
+/// # display_name: None,
+/// # }
+/// }
+/// /// Send `RegistrationClientState` and receive `Registration` JSON from client.
+/// # #[cfg(feature = "serde")]
+/// fn get_registration_json(client: RegistrationClientState<'_, '_, '_, '_>) -> String {
+/// // ⋮
+/// # let client_data_json = BASE64URL_NOPAD.encode(serde_json::json!({
+/// # "type": "webauthn.create",
+/// # "challenge": client.options().challenge,
+/// # "origin": format!("https://{}", client.options().rp_id.as_ref()),
+/// # "crossOrigin": false
+/// # }).to_string().as_bytes());
+/// # serde_json::json!({
+/// # "response": {
+/// # "clientDataJSON": client_data_json,
+/// # "attestationObject": ""
+/// # }
+/// # }).to_string()
+/// }
+/// /// Fetch the `PublicKeyCredentialDescriptor`s associated with `user`.
+/// ///
+/// /// This doesn't need to be called when this is the first credential registered for `user`; instead
+/// /// an empty `Vec` should be passed.
+/// fn get_registered_credentials(
+/// user: UserHandle<&[u8]>,
+/// ) -> Vec<PublicKeyCredentialDescriptor<Vec<u8>>> {
+/// // ⋮
+/// # Vec::new()
+/// }
+/// /// Inserts `RegisteredCredential::into_parts` into the database.
+/// fn insert_cred(cred: RegisteredCredential<'_, '_>) {
+/// // ⋮
+/// }
+/// # Ok::<_, E>(())
+/// ```
+pub mod register;
+/// Contains functionality to (de)serialize data to/from a client.
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+#[cfg(feature = "serde")]
+mod ser;
+/// Contains functionality to deserialize data from a client in a "relaxed" way.
+#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+#[cfg(feature = "serde_relaxed")]
+pub mod ser_relaxed;
+/// [Backup eligibility](https://www.w3.org/TR/webauthn-3/#backup-eligibility) and
+/// [backup state](https://www.w3.org/TR/webauthn-3/#backup-state).
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Backup {
+ /// [BE and BS](https://www.w3.org/TR/webauthn-3/#authdata-flags) flags are `0`.
+ NotEligible,
+ /// [BE and BS](https://www.w3.org/TR/webauthn-3/#authdata-flags) flags are `1` and `0` respectively.
+ Eligible,
+ /// [BE and BS](https://www.w3.org/TR/webauthn-3/#authdata-flags) flags are `1`.
+ Exists,
+}
+impl PartialEq<&Self> for Backup {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<Backup> for &Backup {
+ #[inline]
+ fn eq(&self, other: &Backup) -> bool {
+ **self == *other
+ }
+}
+/// [`AuthenticatorTransport`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport).
+#[derive(Clone, Copy, Debug)]
+pub enum AuthenticatorTransport {
+ /// [`ble`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-ble).
+ Ble,
+ /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-hybrid).
+ Hybrid,
+ /// [`internal`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-internal).
+ Internal,
+ /// [`nfc`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-nfc).
+ Nfc,
+ /// [`smart-card`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-smart-card).
+ SmartCard,
+ /// [`usb`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-usb).
+ Usb,
+}
+impl AuthenticatorTransport {
+ /// Returns the encoded [`u8`] that `self` represents.
+ const fn to_u8(self) -> u8 {
+ match self {
+ Self::Ble => 0x1,
+ Self::Hybrid => 0x2,
+ Self::Internal => 0x4,
+ Self::Nfc => 0x8,
+ Self::SmartCard => 0x10,
+ Self::Usb => 0x20,
+ }
+ }
+}
+/// Set of [`AuthenticatorTransport`]s.
+#[derive(Clone, Copy, Debug)]
+pub struct AuthTransports(u8);
+impl AuthTransports {
+ /// An empty `AuthTransports`.
+ #[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
+ #[cfg(feature = "custom")]
+ pub const NONE: Self = Self::new();
+ /// An `AuthTransports` containing all possible [`AuthenticatorTransport`]s.
+ #[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
+ #[cfg(feature = "custom")]
+ pub const ALL: Self = Self::all();
+ /// Construct an empty `AuthTransports`.
+ #[cfg(any(feature = "bin", feature = "custom", feature = "serde"))]
+ pub(super) const fn new() -> Self {
+ Self(0)
+ }
+ #[cfg(any(feature = "bin", feature = "custom"))]
+ /// Construct an `AuthTransports` containing all `AuthenticatorTransport`s.
+ const fn all() -> Self {
+ Self::new()
+ .add_transport(AuthenticatorTransport::Ble)
+ .add_transport(AuthenticatorTransport::Hybrid)
+ .add_transport(AuthenticatorTransport::Internal)
+ .add_transport(AuthenticatorTransport::Nfc)
+ .add_transport(AuthenticatorTransport::SmartCard)
+ .add_transport(AuthenticatorTransport::Usb)
+ }
+ /// Returns the number of [`AuthenticatorTransport`]s in `self`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::AuthTransports;
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(AuthTransports::ALL.count(), 6);
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn count(self) -> u32 {
+ self.0.count_ones()
+ }
+ /// Returns `true` iff there are no [`AuthenticatorTransport`]s in `self`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::AuthTransports;
+ /// # #[cfg(feature = "custom")]
+ /// assert!(AuthTransports::NONE.is_empty());
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn is_empty(self) -> bool {
+ self.0 == 0
+ }
+ /// Returns `true` iff `self` contains `transport`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::{AuthTransports, AuthenticatorTransport};
+ /// # #[cfg(feature = "custom")]
+ /// assert!(AuthTransports::ALL.contains(AuthenticatorTransport::Ble));
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn contains(self, transport: AuthenticatorTransport) -> bool {
+ let val = transport.to_u8();
+ self.0 & val == val
+ }
+ /// Returns a copy of `self` with `transport` added.
+ ///
+ /// `self` is returned iff `transport` already exists.
+ #[cfg(any(feature = "bin", feature = "custom", feature = "serde"))]
+ const fn add_transport(self, transport: AuthenticatorTransport) -> Self {
+ Self(self.0 | transport.to_u8())
+ }
+ /// Returns a copy of `self` with `transport` added.
+ ///
+ /// `self` is returned iff `transport` already exists.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::{AuthTransports, AuthenticatorTransport};
+ /// assert_eq!(
+ /// AuthTransports::NONE
+ /// .add(AuthenticatorTransport::Usb)
+ /// .count(),
+ /// 1
+ /// );
+ /// assert_eq!(
+ /// AuthTransports::ALL.add(AuthenticatorTransport::Usb).count(),
+ /// 6
+ /// );
+ /// ```
+ #[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
+ #[cfg(feature = "custom")]
+ #[inline]
+ #[must_use]
+ pub const fn add(self, transport: AuthenticatorTransport) -> Self {
+ self.add_transport(transport)
+ }
+ /// Returns a copy of `self` with `transport` removed.
+ ///
+ /// `self` is returned iff `transport` did not exist.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::{AuthTransports, AuthenticatorTransport};
+ /// assert_eq!(
+ /// AuthTransports::ALL
+ /// .remove(AuthenticatorTransport::Internal)
+ /// .count(),
+ /// 5
+ /// );
+ /// assert_eq!(
+ /// AuthTransports::NONE.remove(AuthenticatorTransport::Usb).count(),
+ /// 0
+ /// );
+ /// ```
+ #[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
+ #[cfg(feature = "custom")]
+ #[inline]
+ #[must_use]
+ pub const fn remove(self, transport: AuthenticatorTransport) -> Self {
+ Self(self.0 & !transport.to_u8())
+ }
+}
+/// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment).
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum AuthenticatorAttachment {
+ /// No attachment information.
+ None,
+ /// [`platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-platform).
+ Platform,
+ /// [`cross-platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-cross-platform).
+ CrossPlatform,
+}
+impl PartialEq<&Self> for AuthenticatorAttachment {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<AuthenticatorAttachment> for &AuthenticatorAttachment {
+ #[inline]
+ fn eq(&self, other: &AuthenticatorAttachment) -> bool {
+ **self == *other
+ }
+}
+/// The maximum number of bytes that can make up a 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).
+///
+/// The spec does not call out this value directly instead it states the following:
+///
+/// > Credential IDs are generated by authenticators in two forms:
+/// >
+/// > * At least 16 bytes that include at least 100 bits of entropy, or
+/// > * The [public key credential source](https://www.w3.org/TR/webauthn-3/#public-key-credential-source),
+/// > without its Credential ID or mutable items, encrypted so only its managing
+/// > authenticator can decrypt it. This form allows the authenticator to be nearly
+/// > stateless, by having the Relying Party store any necessary state.
+///
+/// One of the immutable items of the public key credential source is the private key
+/// which for any real-world signature algorithm will always be at least 16 bytes.
+pub const CRED_ID_MIN_LEN: usize = 16;
+/// A [Credential ID](https://www.w3.org/TR/webauthn-3/#credential-id) that is made up of
+/// [`CRED_ID_MIN_LEN`]–[`CRED_ID_MAX_LEN`] bytes.
+#[derive(Clone, Copy, Debug)]
+pub struct CredentialId<T>(T);
+impl<T> CredentialId<T> {
+ /// Returns the contained data consuming `self`.
+ #[inline]
+ pub fn into_inner(self) -> T {
+ self.0
+ }
+ /// Returns the contained data.
+ #[inline]
+ pub const fn inner(&self) -> &T {
+ &self.0
+ }
+}
+impl<'a> CredentialId<&'a [u8]> {
+ /// Creates a `CredentialId` from a `slice`.
+ fn from_slice<'b: 'a>(value: &'b [u8]) -> Result<Self, CredentialIdErr> {
+ if (CRED_ID_MIN_LEN..=CRED_ID_MAX_LEN).contains(&value.len()) {
+ Ok(Self(value))
+ } else {
+ Err(CredentialIdErr)
+ }
+ }
+}
+impl<T: AsRef<[u8]>> AsRef<[u8]> for CredentialId<T> {
+ #[inline]
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+impl<T: Borrow<[u8]>> Borrow<[u8]> for CredentialId<T> {
+ #[inline]
+ fn borrow(&self) -> &[u8] {
+ self.0.borrow()
+ }
+}
+impl<'a: 'b, 'b> From<&'a CredentialId<Vec<u8>>> for CredentialId<&'b Vec<u8>> {
+ #[inline]
+ fn from(value: &'a CredentialId<Vec<u8>>) -> Self {
+ Self(&value.0)
+ }
+}
+impl<'a: 'b, 'b> From<CredentialId<&'a Vec<u8>>> for CredentialId<&'b [u8]> {
+ #[inline]
+ fn from(value: CredentialId<&'a Vec<u8>>) -> Self {
+ Self(value.0.as_slice())
+ }
+}
+impl<'a: 'b, 'b> From<&'a CredentialId<Vec<u8>>> for CredentialId<&'b [u8]> {
+ #[inline]
+ fn from(value: &'a CredentialId<Vec<u8>>) -> Self {
+ Self(value.0.as_slice())
+ }
+}
+impl From<CredentialId<&[u8]>> for CredentialId<Vec<u8>> {
+ #[inline]
+ fn from(value: CredentialId<&[u8]>) -> Self {
+ Self(value.0.to_owned())
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CredentialId<T>> for CredentialId<T2> {
+ #[inline]
+ fn eq(&self, other: &CredentialId<T>) -> bool {
+ self.0 == other.0
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CredentialId<T>> for &CredentialId<T2> {
+ #[inline]
+ fn eq(&self, other: &CredentialId<T>) -> bool {
+ **self == *other
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&CredentialId<T>> for CredentialId<T2> {
+ #[inline]
+ fn eq(&self, other: &&CredentialId<T>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq> Eq for CredentialId<T> {}
+impl<T: Hash> Hash for CredentialId<T> {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.0.hash(state);
+ }
+}
+// We define a separate type to ensure challenges sent to the client are always randomly generated;
+// otherwise one could deserialize arbitrary data into a `Challenge`.
+/// Copy of [`Challenge`] sent back from the client.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct SentChallenge(pub u128);
+impl PartialEq<&Self> for SentChallenge {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<SentChallenge> for &SentChallenge {
+ #[inline]
+ fn eq(&self, other: &SentChallenge) -> bool {
+ **self == *other
+ }
+}
+impl PartialOrd for SentChallenge {
+ #[inline]
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+impl Ord for SentChallenge {
+ #[inline]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.0.cmp(&other.0)
+ }
+}
+impl Hash for SentChallenge {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ state.write_u128(self.0);
+ }
+}
+/// An [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin) or
+/// [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin).
+#[derive(Debug, Eq)]
+pub struct Origin<'a>(pub Cow<'a, str>);
+impl PartialEq<Origin<'_>> for Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &Origin<'_>) -> bool {
+ self.0 == other.0
+ }
+}
+impl PartialEq<&Origin<'_>> for Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &&Origin<'_>) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<Origin<'_>> for &Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &Origin<'_>) -> bool {
+ **self == *other
+ }
+}
+impl PartialEq<str> for Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &str) -> bool {
+ self.0.as_ref() == other
+ }
+}
+impl PartialEq<Origin<'_>> for str {
+ #[inline]
+ fn eq(&self, other: &Origin<'_>) -> bool {
+ *other == *self
+ }
+}
+impl PartialEq<&str> for Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &&str) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<Origin<'_>> for &str {
+ #[inline]
+ fn eq(&self, other: &Origin<'_>) -> bool {
+ **self == *other
+ }
+}
+impl PartialEq<String> for Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &String) -> bool {
+ self.0 == *other
+ }
+}
+impl PartialEq<Origin<'_>> for String {
+ #[inline]
+ fn eq(&self, other: &Origin<'_>) -> bool {
+ *other == *self
+ }
+}
+impl PartialEq<Url> for Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &Url) -> bool {
+ self.0.as_ref() == other.as_ref()
+ }
+}
+impl PartialEq<Origin<'_>> for Url {
+ #[inline]
+ fn eq(&self, other: &Origin<'_>) -> bool {
+ *other == *self
+ }
+}
+impl PartialEq<&Url> for Origin<'_> {
+ #[inline]
+ fn eq(&self, other: &&Url) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<Origin<'_>> for &Url {
+ #[inline]
+ fn eq(&self, other: &Origin<'_>) -> bool {
+ **self == *other
+ }
+}
+/// [Authenticator data flags](https://www.w3.org/TR/webauthn-3/#authdata-flags).
+#[derive(Clone, Copy, Debug)]
+pub struct Flag {
+ /// [`UP` flag](https://www.w3.org/TR/webauthn-3/#authdata-flags-up).
+ ///
+ /// Note this is always `true` when part of [`auth::AuthenticatorData::flags`].
+ pub user_present: bool,
+ /// [`UV` flag](https://www.w3.org/TR/webauthn-3/#concept-user-verified).
+ pub user_verified: bool,
+ /// [`BE`](https://www.w3.org/TR/webauthn-3/#backup-eligibility) and
+ /// [`BS`](https://www.w3.org/TR/webauthn-3/#backup-state) flags.
+ pub backup: Backup,
+}
+/// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+pub(super) trait AuthData<'a>: Sized {
+ /// Error returned by [`Self::user_is_not_present`].
+ ///
+ /// This should be [`Infallible`] in the event user must not always be present.
+ type UpBitErr;
+ /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata).
+ type CredData;
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions).
+ type Ext: AuthExtOutput + Copy;
+ /// Errors iff the user must always be present.
+ fn user_is_not_present() -> Result<(), Self::UpBitErr>;
+ /// `true` iff `AT` bit (i.e., bit 6) in [`Self::flag_data`] can and must be set to 1.
+ fn contains_at_bit() -> bool;
+ /// Constructor.
+ fn new(rp_id_hash: &'a [u8], flags: Flag, sign_count: u32, attested_credential_data: Self::CredData, extensions: Self::Ext) -> Self;
+ /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash).
+ fn rp_hash(&self) -> &'a [u8];
+ /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags).
+ fn flag(&self) -> Flag;
+}
+/// [`CollectedClientData`](https://www.w3.org/TR/webauthn-3/#dictdef-collectedclientdata).
+#[derive(Debug)]
+pub struct CollectedClientData<'a> {
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge).
+ pub challenge: SentChallenge,
+ /// [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin).
+ pub origin: Origin<'a>,
+ /// [`crossOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-crossorigin).
+ pub cross_origin: bool,
+ /// [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin).
+ ///
+ /// When `CollectedClientData` is constructed via [`Self::from_client_data_json`], this can only be
+ /// `Some` if [`Self::cross_origin`]; and if `Some`, it will be different than [`Self::origin`].
+ pub top_origin: Option<Origin<'a>>,
+}
+impl<'a> CollectedClientData<'a> {
+ /// Parses `json` based on the
+ /// [limited verification algorithm](https://www.w3.org/TR/webauthn-3/#clientdatajson-verification).
+ ///
+ /// Additionally, [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin) is only
+ /// allowed to exist if it has a different value than
+ /// [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin) and
+ /// [`crossOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-crossorigin) is `true`.
+ ///
+ /// `REGISTRATION` iff [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type) must be
+ /// `"webauthn.create"`; otherwise it must be `"webauthn.get"`.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `json` cannot be parsed based on the aforementioned requirements.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::{error::CollectedClientDataErr, CollectedClientData};
+ /// assert!(!CollectedClientData::from_client_data_json::<true>(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice())?.cross_origin);
+ /// assert!(!CollectedClientData::from_client_data_json::<false>(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice())?.cross_origin);
+ /// # Ok::<_, CollectedClientDataErr>(())
+ /// ```
+ #[inline]
+ pub fn from_client_data_json<'b: 'a, const REGISTRATION: bool>(json: &'b [u8]) -> Result<Self, CollectedClientDataErr> {
+ LimitedVerificationParser::<REGISTRATION>::parse(json)
+ }
+ /// Parses `json` in a "relaxed" way.
+ ///
+ /// Unlike [`Self::from_client_data_json`] which requires `json` to be an output from the
+ /// [JSON-compatible serialization of client data](https://www.w3.org/TR/webauthn-3/#clientdatajson-serialization),
+ /// this parses `json` based entirely on the
+ /// [`CollectedClientData`](https://www.w3.org/TR/webauthn-3/#dictdef-collectedclientdata) Web IDL `dictionary`.
+ ///
+ /// L1 clients predate the JSON-compatible serialization of client data; additionally there are L2 and L3
+ /// clients that don't adhere to the JSON-compatible serialization of client data despite being required to.
+ /// These clients serialize `CollectedClientData` so that it's valid JSON and conforms to the Web IDL `dictionary`
+ /// and nothing more. Furthermore, when not relying on the
+ /// [limited verification algorithm](https://www.w3.org/TR/webauthn-3/#clientdatajson-verification), the spec
+ /// requires the data to be decoded in a way equivalent to
+ /// [UTF-8 decode](https://encoding.spec.whatwg.org/#utf-8-decode) which both interprets a leading zero
+ /// width no-breaking space (i.e., U+FEFF) as a byte-order mark (BOM) as well as replaces any sequences of
+ /// invalid UTF-8 code units with the replacement character (i.e., U+FFFD). That is precisely what this
+ /// function does.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff any of the following is true:
+ /// * The payload is not valid JSON _after_ ignoring a leading U+FEFF and replacing any sequences of invalid
+ /// UTF-8 code units with U+FFFD.
+ /// * The JSON does not conform to the Web IDL `dictionary`.
+ /// * [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type) is not `"webauthn.create"`
+ /// or `"webauthn.get"` when `REGISTRATION` and `!REGISTRATION` respectively.
+ /// * [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) is not a
+ /// base64url-encoded [`Challenge`].
+ /// * Existence of duplicate keys for the keys that are expected.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::{ser_relaxed::SerdeJsonErr, CollectedClientData};
+ /// assert!(!CollectedClientData::from_client_data_json_relaxed::<true>(b"\xef\xbb\xbf{
+ /// \"type\": \"webauthn.create\",
+ /// \"origin\": \"https://example.com\",
+ /// \"f\xffo\": 123,
+ /// \"topOrigin\": \"https://example.com\",
+ /// \"challenge\": \"AAAAAAAAAAAAAAAAAAAAAA\"
+ /// }")?.cross_origin);
+ /// # Ok::<_, SerdeJsonErr>(())
+ /// ```
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ #[inline]
+ pub fn from_client_data_json_relaxed<'b: 'a, const REGISTRATION: bool>(json: &'b [u8]) -> Result<Self, SerdeJsonErr> {
+ ser_relaxed::RelaxedClientDataJsonParser::<REGISTRATION>::parse(json)
+ }
+}
+/// Parser of
+/// [`JSON-compatible serialization of client data`](https://www.w3.org/TR/webauthn-3/#collectedclientdata-json-compatible-serialization-of-client-data).
+trait ClientDataJsonParser {
+ /// Error returned by [`Self::parse`].
+ type Err;
+ /// Parses `json` into `CollectedClientData` based on the value of
+ /// [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type).
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `json` cannot be parsed into a `CollectedClientData`.
+ fn parse(json: &[u8]) -> Result<CollectedClientData<'_>, Self::Err>;
+}
+/// [`ClientDataJsonParser`] based on the
+/// [limited verification algorithm](https://www.w3.org/TR/webauthn-3/#clientdatajson-verification)
+/// with the following additional requirements:
+/// * Unknown keys are not allowed.
+/// * The entire payload is parsed; thus the payload is guaranteed to be valid UTF-8 and JSON.
+/// * [`CollectedClientData::top_origin`] can only be `Some` if
+/// [`crossOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-crossorigin).
+/// * If `CollectedClientData::top_origin` is `Some`, then it does not equal [`CollectedClientData::origin`].
+///
+/// `REGISTRATION` iff [`ClientDataJsonParser::parse`] requires
+/// [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type) to be `"webauthn.create"`;
+/// otherwise it must be `"webauthn.get"`.
+struct LimitedVerificationParser<const REGISTRATION: bool>;
+impl<const R: bool> LimitedVerificationParser<R> {
+ /// Parses `val` as a JSON string with possibly trailing data. `val` MUST NOT begin with an opening quote. Upon
+ /// encountering the first non-escaped quote, the parsed value is returned in addition to the remaining
+ /// portion of `val` _after_ the closing quote. The limited verification algorithm is adhered to; thus the
+ /// _only_ Unicode scalar values that are allowed (and must) be hex-escaped are U+0000 to U+001F inclusively.
+ /// Similarly only `b'\\'` and `b'"'` are allowed (and must) be escaped with `b'\\'`.
+ #[expect(clippy::arithmetic_side_effects, clippy::indexing_slicing, reason = "comments justify their correctness")]
+ fn parse_string(val: &[u8]) -> Result<(Cow<'_, str>, &'_ [u8]), CollectedClientDataErr> {
+ /// Tracks the state of the current Unicode scalar value that is being parsed.
+ enum State {
+ /// We are not parsing `'"'`, `'\\'`, or U+0000 to U+001F.
+ Normal,
+ /// We just encountered the escape character.
+ Escape,
+ /// We just encountered `b"\\u"`.
+ UnicodeEscape,
+ /// We just encountered `b"\\u0"`.
+ UnicodeHex1,
+ /// We just encountered `b"\\u00"`.
+ UnicodeHex2,
+ /// We just encountered `b"\\u000"` or `b"\\u001"`. The contained `u8` is `0` iff the former; otherwise
+ /// `0x10`.
+ UnicodeHex3(u8),
+ }
+ // We parse this as UTF-8 only at the end iff it is not empty. This contains all the potential Unicode scalar
+ // values after de-escaping.
+ let mut utf8 = Vec::new();
+ // This tracks the start index of the next slice to add. We add slices iff we encounter the escape character or
+ // we return the parsed `Cow` (i.e., encounter an unescaped `b'"'`).
+ let mut cur_idx = 0;
+ // The state of the yet-to-be-parsed Unicode scalar value.
+ let mut state = State::Normal;
+ for (counter, &b) in val.iter().enumerate() {
+ match state {
+ State::Normal => {
+ match b {
+ b'"' => {
+ if utf8.is_empty() {
+ // `cur_idx` is 0 or 1. The latter is true iff `val` starts with a
+ // `b'\\'` or `b'"'` but contains no other escaped characters.
+ return str::from_utf8(&val[cur_idx..counter])
+ .map_err(CollectedClientDataErr::Utf8)
+ // `val.len() > counter`, so indexing is fine and overflow cannot happen.
+ .map(|v| (Cow::Borrowed(v), &val[counter + 1..]));
+ }
+ utf8.extend_from_slice(&val[cur_idx..counter]);
+ return String::from_utf8(utf8)
+ .map_err(CollectedClientDataErr::Utf8Owned)
+ // `val.len() > counter`, so indexing is fine and overflow cannot happen.
+ .map(|v| (Cow::Owned(v), &val[counter + 1..]));
+ }
+ b'\\' => {
+ // Write the current slice of data.
+ utf8.extend_from_slice(&val[cur_idx..counter]);
+ state = State::Escape;
+ }
+ // ASCII is a subset of UTF-8 and this is a subset of ASCII. The code unit that is used for an
+ // ASCII Unicode scalar value _never_ appears in multi-code-unit Unicode scalar values; thus we
+ // error immediately.
+ ..=0x1f => return Err(CollectedClientDataErr::InvalidEscapedString),
+ _ => (),
+ }
+ }
+ State::Escape => {
+ match b {
+ b'"' | b'\\' => {
+ // We start the next slice here since we need to add it.
+ cur_idx = counter;
+ state = State::Normal;
+ }
+ b'u' => {
+ state = State::UnicodeEscape;
+ }
+ _ => {
+ return Err(CollectedClientDataErr::InvalidEscapedString);
+ }
+ }
+ }
+ State::UnicodeEscape => {
+ if b != b'0' {
+ return Err(CollectedClientDataErr::InvalidEscapedString);
+ }
+ state = State::UnicodeHex1;
+ }
+ State::UnicodeHex1 => {
+ if b != b'0' {
+ return Err(CollectedClientDataErr::InvalidEscapedString);
+ }
+ state = State::UnicodeHex2;
+ }
+ State::UnicodeHex2 => {
+ state = State::UnicodeHex3(match b {
+ b'0' => 0,
+ b'1' => 0x10,
+ _ => return Err(CollectedClientDataErr::InvalidEscapedString),
+ });
+ }
+ State::UnicodeHex3(v) => {
+ match b {
+ // Only and all _lowercase_ hex is allowed.
+ b'0'..=b'9' | b'a'..=b'f' => {
+ // When `b < b'a'`, then `b >= b'0'`; and `b'a' > 87`; thus underflow cannot happen.
+ // Note `b'a' - 10 == 87`.
+ utf8.push(v | (b - if b < b'a' { b'0' } else { 87 }));
+ // `counter < val.len()`, so overflow cannot happen.
+ cur_idx = counter + 1;
+ state = State::Normal;
+ }
+ _ => return Err(CollectedClientDataErr::InvalidEscapedString),
+ }
+ }
+ }
+ }
+ // We never encountered an unescaped `b'"'`; thus we could not parse a string.
+ Err(CollectedClientDataErr::InvalidObject)
+ }
+}
+impl<const R: bool> ClientDataJsonParser for LimitedVerificationParser<R> {
+ type Err = CollectedClientDataErr;
+ #[expect(clippy::panic_in_result_fn, reason = "want to crash when there is a bug")]
+ #[expect(clippy::little_endian_bytes, reason = "Challenge::serialize and this need to be consistent across architectures")]
+ #[expect(clippy::too_many_lines, reason = "110 lines is fine")]
+ fn parse(json: &[u8]) -> Result<CollectedClientData<'_>, Self::Err> {
+ // `{"type":"webauthn.<create|get>","challenge":"<22 bytes>","origin":"<bytes>","crossOrigin":<true|false>[,"topOrigin":"<bytes>"][,<anything>]}`.
+ /// First portion of `value`.
+ const HEADER: &[u8; 18] = br#"{"type":"webauthn."#;
+ /// `get`.
+ const GET: &[u8; 3] = b"get";
+ /// `create`.
+ const CREATE: &[u8; 6] = b"create";
+ /// Value after type before the start of the base64url-encoded challenge.
+ const AFTER_TYPE: &[u8; 15] = br#"","challenge":""#;
+ /// Value after challenge before the start of the origin value.
+ const AFTER_CHALLENGE: &[u8; 12] = br#"","origin":""#;
+ /// Value after origin before the start of the crossOrigin value.
+ const AFTER_ORIGIN: &[u8; 15] = br#","crossOrigin":"#;
+ /// `true`.
+ const TRUE: &[u8; 4] = b"true";
+ /// `false`.
+ const FALSE: &[u8; 5] = b"false";
+ /// Value after crossOrigin before the start of the topOrigin value.
+ const AFTER_CROSS: &[u8; 13] = br#""topOrigin":""#;
+ json.split_last().ok_or(CollectedClientDataErr::Len).and_then(|(last, last_rem)| {
+ if *last == b'}' {
+ last_rem.split_at_checked(HEADER.len()).ok_or(CollectedClientDataErr::Len).and_then(|(header, header_rem)| {
+ if header == HEADER {
+ if R {
+ header_rem.split_at_checked(CREATE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(create, create_rem)| {
+ if create == CREATE {
+ Ok(create_rem)
+ } else {
+ Err(CollectedClientDataErr::Type)
+ }
+ })
+ } else {
+ header_rem.split_at_checked(GET.len()).ok_or(CollectedClientDataErr::Len).and_then(|(get, get_rem)| {
+ if get == GET {
+ Ok(get_rem)
+ } else {
+ Err(CollectedClientDataErr::Type)
+ }
+ })
+ }.and_then(|type_rem| {
+ type_rem.split_at_checked(AFTER_TYPE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(chall_key, chall_key_rem)| {
+ if chall_key == AFTER_TYPE {
+ chall_key_rem.split_at_checked(Challenge::BASE64_LEN).ok_or(CollectedClientDataErr::Len).and_then(|(base64_chall, base64_chall_rem)| {
+ let mut chall = [0; 16];
+ BASE64URL_NOPAD_ENC.decode_mut(base64_chall, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).and_then(|chall_len| {
+ assert_eq!(chall_len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut");
+ base64_chall_rem.split_at_checked(AFTER_CHALLENGE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(origin_key, origin_key_rem)| {
+ if origin_key == AFTER_CHALLENGE {
+ Self::parse_string(origin_key_rem).and_then(|(origin, origin_rem)| {
+ origin_rem.split_at_checked(AFTER_ORIGIN.len()).ok_or(CollectedClientDataErr::Len).and_then(|(cross_key, cross_key_rem)| {
+ if cross_key == AFTER_ORIGIN {
+ // `FALSE.len() > TRUE.len()`, so we check for `FALSE` in `and_then`.
+ cross_key_rem.split_at_checked(TRUE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(cross_true, cross_true_rem)| {
+ if cross_true == TRUE {
+ Ok((true, cross_true_rem))
+ } else {
+ cross_key_rem.split_at_checked(FALSE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(cross_false, cross_false_rem)| {
+ if cross_false == FALSE {
+ Ok((false, cross_false_rem))
+ } else {
+ Err(CollectedClientDataErr::CrossOrigin)
+ }
+ })
+ }.and_then(|(cross, cross_rem)| {
+ cross_rem.split_first().map_or(Ok((cross, None)), |(comma, comma_rem)| {
+ if *comma == b',' {
+ comma_rem.split_at_checked(AFTER_CROSS.len()).map_or(Ok((cross, None)), |(top, top_rem)| {
+ if top == AFTER_CROSS {
+ if cross {
+ Self::parse_string(top_rem).and_then(|(top_origin, top_origin_rem)| {
+ top_origin_rem.first().map_or(Ok(()), |v| {
+ if *v == b',' {
+ Ok(())
+ } else {
+ Err(CollectedClientDataErr::InvalidObject)
+ }
+ }).and_then(|()| {
+ if origin == top_origin {
+ Err(CollectedClientDataErr::TopOriginSameAsOrigin)
+ } else {
+ Ok((true, Some(Origin(top_origin))))
+ }
+ })
+ })
+ } else {
+ Err(CollectedClientDataErr::TopOriginWithoutCrossOrigin)
+ }
+ } else {
+ Ok((cross, None))
+ }
+ })
+ } else {
+ Err(CollectedClientDataErr::InvalidObject)
+ }
+ }).map(|(cross_origin, top_origin)| CollectedClientData { challenge: SentChallenge(u128::from_le_bytes(chall)), origin: Origin(origin), cross_origin, top_origin, })
+ })
+ })
+ } else {
+ Err(CollectedClientDataErr::CrossOriginKey)
+ }
+ })
+ })
+ } else {
+ Err(CollectedClientDataErr::OriginKey)
+ }
+ })
+ })
+ })
+ } else {
+ Err(CollectedClientDataErr::ChallengeKey)
+ }
+ })
+ })
+ } else {
+ Err(CollectedClientDataErr::InvalidStart)
+ }
+ })
+ } else {
+ Err(CollectedClientDataErr::InvalidObject)
+ }
+ })
+ }
+}
+/// Authenticator extension outputs;
+pub(super) trait AuthExtOutput {
+ /// MUST return `true` iff there is no data.
+ fn missing(self) -> bool;
+}
+/// Successful return type from [`FromCbor::from_cbor`].
+struct CborSuccess<'a, T> {
+ /// Value parsed from the slice.
+ value: T,
+ /// Remaining unprocessed data.
+ remaining: &'a [u8],
+}
+/// Types that parse
+/// [CTAP2 canonical CBOR encoding form](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#ctap2-canonical-cbor-encoding-form)
+/// data without necessarily consuming all the data.
+///
+/// The purpose of this `trait` is to allow chains of types to progressively consume `cbor` by passing
+/// [`CborSuccess::remaining`] into the next `FromCbor` type.
+trait FromCbor<'a>: Sized {
+ /// Error when conversion fails.
+ type Err;
+ /// Parses `cbor` into `Self`.
+ ///
+ /// # Errors
+ ///
+ /// Errors if `cbor` cannot be parsed into `Self:`.
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err>;
+}
+/// Error returned from [`A::from_cbor`] `where A: AuthData`.
+enum AuthenticatorDataErr<UpErr, CredData, AuthExt> {
+ /// The `slice` had an invalid length.
+ Len,
+ /// [UP](https://www.w3.org/TR/webauthn-3/#authdata-flags-at) bit was 0.
+ UserNotPresent(UpErr),
+ /// Bit 1 in [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags) is not 0.
+ FlagsBit1Not0,
+ /// Bit 5 in [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags) is not 0.
+ FlagsBit5Not0,
+ /// [AT](https://www.w3.org/TR/webauthn-3/#authdata-flags-at) bit was 0 during registration or was 1
+ /// during authentication.
+ AttestedCredentialData,
+ /// [BE](https://www.w3.org/TR/webauthn-3/#authdata-flags-be) and
+ /// [BS](https://www.w3.org/TR/webauthn-3/#authdata-flags-bs) bits were 0 and 1 respectively.
+ BackupWithoutEligibility,
+ /// Error returned from [`AttestedCredentialData::from_cbor`].
+ AttestedCredential(CredData),
+ /// Error returned from [`register::AuthenticatorExtensionOutput::from_cbor`] and
+ /// [`auth::AuthenticatorExtensionOutput::from_cbor`].
+ AuthenticatorExtension(AuthExt),
+ /// [ED](https://www.w3.org/TR/webauthn-3/#authdata-flags-ed) bit was 0, but
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) existed.
+ NoExtensionBitWithData,
+ /// [ED](https://www.w3.org/TR/webauthn-3/#authdata-flags-ed) bit was 1, but
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) did not exist.
+ ExtensionBitWithoutData,
+ /// There was trailing data that could not be deserialized.
+ TrailingData,
+}
+impl<U, C: Display, A: Display> Display for AuthenticatorDataErr<U, C, A> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Len => f.write_str("authenticator data had an invalid length"),
+ Self::UserNotPresent(_) => f.write_str("user was not present"),
+ Self::FlagsBit1Not0 => f.write_str("flags 1-bit was 1"),
+ Self::FlagsBit5Not0 => f.write_str("flags 5-bit was 1"),
+ Self::AttestedCredentialData => f.write_str("attested credential data was included during authentication or was not included during registration"),
+ Self::BackupWithoutEligibility => {
+ f.write_str("backup state bit was 1 despite backup eligibility being 0")
+ }
+ Self::AttestedCredential(ref err) => err.fmt(f),
+ Self::AuthenticatorExtension(ref err) => err.fmt(f),
+ Self::NoExtensionBitWithData => {
+ f.write_str("extension bit was 0 despite extensions existing")
+ }
+ Self::ExtensionBitWithoutData => {
+ f.write_str("extension bit was 1 despite no extensions existing")
+ }
+ Self::TrailingData => {
+ f.write_str("slice had trailing data that could not be deserialized")
+ }
+ }
+ }
+}
+impl From<AuthenticatorDataErr<Infallible, AttestedCredentialDataErr, RegAuthExtErr>> for RegAuthDataErr {
+ #[inline]
+ fn from(value: AuthenticatorDataErr<Infallible, AttestedCredentialDataErr, RegAuthExtErr>) -> Self {
+ match value {
+ AuthenticatorDataErr::Len => Self::Len,
+ AuthenticatorDataErr::UserNotPresent(v) => match v {},
+ AuthenticatorDataErr::FlagsBit1Not0 => Self::FlagsBit1Not0,
+ AuthenticatorDataErr::FlagsBit5Not0 => Self::FlagsBit5Not0,
+ AuthenticatorDataErr::AttestedCredentialData => Self::AttestedCredentialDataNotIncluded,
+ AuthenticatorDataErr::BackupWithoutEligibility => Self::BackupWithoutEligibility,
+ AuthenticatorDataErr::AttestedCredential(err) => Self::AttestedCredential(err),
+ AuthenticatorDataErr::AuthenticatorExtension(err) => Self::AuthenticatorExtension(err),
+ AuthenticatorDataErr::NoExtensionBitWithData => Self::NoExtensionBitWithData,
+ AuthenticatorDataErr::ExtensionBitWithoutData => Self::ExtensionBitWithoutData,
+ AuthenticatorDataErr::TrailingData => Self::TrailingData,
+ }
+ }
+}
+impl From<AuthenticatorDataErr<(), Infallible, AuthAuthExtErr>> for AuthAuthDataErr {
+ #[inline]
+ fn from(value: AuthenticatorDataErr<(), Infallible, AuthAuthExtErr>) -> Self {
+ match value {
+ AuthenticatorDataErr::Len => Self::Len,
+ AuthenticatorDataErr::UserNotPresent(()) => Self::UserNotPresent,
+ AuthenticatorDataErr::FlagsBit1Not0 => Self::FlagsBit1Not0,
+ AuthenticatorDataErr::FlagsBit5Not0 => Self::FlagsBit5Not0,
+ AuthenticatorDataErr::AttestedCredentialData => Self::AttestedCredentialDataIncluded,
+ AuthenticatorDataErr::AttestedCredential(val) => match val {},
+ AuthenticatorDataErr::BackupWithoutEligibility => Self::BackupWithoutEligibility,
+ AuthenticatorDataErr::AuthenticatorExtension(err) => Self::AuthenticatorExtension(err),
+ AuthenticatorDataErr::NoExtensionBitWithData => Self::NoExtensionBitWithData,
+ AuthenticatorDataErr::ExtensionBitWithoutData => Self::ExtensionBitWithoutData,
+ AuthenticatorDataErr::TrailingData => Self::TrailingData,
+ }
+ }
+}
+impl<'a, A> FromCbor<'a> for A
+where
+ A: AuthData<'a>,
+ A::CredData: FromCbor<'a>,
+ A::Ext: FromCbor<'a>,
+{
+ type Err = AuthenticatorDataErr<A::UpBitErr, <A::CredData as FromCbor<'a>>::Err, <A::Ext as FromCbor<'a>>::Err>;
+ #[expect(clippy::big_endian_bytes, reason = "CBOR integers are in big-endian")]
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// Length of `signCount`.
+ const SIGN_COUNT_LEN: usize = 4;
+ /// `UP` bit (i.e., bit 0) set to 1.
+ const UP: u8 = 0b0000_0001;
+ /// `RFU1` bit (i.e., bit 1) set to 1.
+ const RFU1: u8 = UP << 1;
+ /// `UV` bit (i.e., bit 2) set to 1.
+ const UV: u8 = RFU1 << 1;
+ /// `BE` bit (i.e., bit 3) set to 1.
+ const BE: u8 = UV << 1;
+ /// `BS` bit (i.e., bit 4) set to 1.
+ const BS: u8 = BE << 1;
+ /// `RFU2` bit (i.e., bit 5) set to 1.
+ const RFU2: u8 = BS << 1;
+ /// `AT` bit (i.e., bit 6) set to 1.
+ const AT: u8 = RFU2 << 1;
+ /// `ED` bit (i.e., bit 7) set to 1.
+ const ED: u8 = AT << 1;
+ cbor.split_at_checked(Sha256::output_size()).ok_or_else(|| AuthenticatorDataErr::Len).and_then(|(rp_id_slice, rp_id_rem)| {
+ rp_id_rem.split_first().ok_or_else(|| AuthenticatorDataErr::Len).and_then(|(&flag, flag_rem)| {
+ let user_present = flag & UP == UP;
+ if user_present {
+ Ok(())
+ } else {
+ A::user_is_not_present().map_err(AuthenticatorDataErr::UserNotPresent)
+ }
+ .and_then(|()| {
+ if flag & RFU1 == 0 {
+ if flag & RFU2 == 0 {
+ let at_bit = A::contains_at_bit();
+ if flag & AT == AT {
+ if at_bit {
+ Ok(())
+ } else {
+ Err(AuthenticatorDataErr::AttestedCredentialData)
+ }
+ } else if at_bit {
+ Err(AuthenticatorDataErr::AttestedCredentialData)
+ } else {
+ Ok(())
+ }.and_then(|()| {
+ let bs = flag & BS == BS;
+ if flag & BE == BE {
+ if bs {
+ Ok(Backup::Exists)
+ } else {
+ Ok(Backup::Eligible)
+ }
+ } else if bs {
+ Err(AuthenticatorDataErr::BackupWithoutEligibility)
+ } else {
+ Ok(Backup::NotEligible)
+ }
+ .and_then(|backup| {
+ flag_rem.split_at_checked(SIGN_COUNT_LEN).ok_or_else(|| AuthenticatorDataErr::Len).and_then(|(count_slice, count_rem)| {
+ A::CredData::from_cbor(count_rem).map_err(AuthenticatorDataErr::AttestedCredential).and_then(|att_data| {
+ A::Ext::from_cbor(att_data.remaining).map_err(AuthenticatorDataErr::AuthenticatorExtension).and_then(|ext| {
+ if ext.remaining.is_empty() {
+ let ed = flag & ED == ED;
+ if ext.value.missing() {
+ if ed {
+ Err(AuthenticatorDataErr::ExtensionBitWithoutData)
+ } else {
+ Ok(())
+ }
+ } else if ed {
+ Ok(())
+ } else {
+ Err(AuthenticatorDataErr::NoExtensionBitWithData)
+ }.map(|()| {
+ let mut sign_count = [0; SIGN_COUNT_LEN];
+ sign_count.copy_from_slice(count_slice);
+ // `signCount` is in big-endian.
+ CborSuccess { value: A::new(rp_id_slice, Flag { user_present, user_verified: flag & UV == UV, backup, }, u32::from_be_bytes(sign_count), att_data.value, ext.value), remaining: ext.remaining, }
+ })
+ } else {
+ Err(AuthenticatorDataErr::TrailingData)
+ }
+ })
+ })
+ })
+ })
+ })
+ } else {
+ Err(AuthenticatorDataErr::FlagsBit5Not0)
+ }
+ } else {
+ Err(AuthenticatorDataErr::FlagsBit1Not0)
+ }
+ })
+ })
+ })
+ }
+}
+/// Data returned by [`AuthDataContainer::from_data`].
+pub(super) struct ParsedAuthData<'a, A> {
+ /// The data the CBOR is parsed into.
+ data: A,
+ /// The raw authenticator data and 32-bytes of trailing data.
+ auth_data_and_32_trailing_bytes: &'a [u8],
+}
+/// Error returned by [`AuthResponse::parse_data_and_verify_sig`].
+pub(super) enum AuthRespErr<AuthDataErr> {
+ /// Variant returned when parsing
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson)
+ /// into [`CollectedClientData`] fails.
+ CollectedClientData(CollectedClientDataErr),
+ /// Variant returned when parsing
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson)
+ /// in a "relaxed" way into [`CollectedClientData`] fails.
+ #[cfg(feature = "serde_relaxed")]
+ CollectedClientDataRelaxed(SerdeJsonErr),
+ /// Variant returned when parsing [`AuthResponse::Auth`] fails.
+ Auth(AuthDataErr),
+ /// Variant when the [`CompressedPubKey`] or [`UncompressePubKey`] is not valid.
+ PubKey(PubKeyErr),
+ /// Variant returned when the signature, if one exists, associated with
+ /// [`Self::AuthResponse`] is invalid.
+ Signature,
+}
+impl<A: Display> Display for AuthRespErr<A> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::CollectedClientData(ref err) => write!(f, "CollectedClientData could not be parsed: {err}"),
+ #[cfg(feature = "serde_relaxed")]
+ Self::CollectedClientDataRelaxed(ref err) => write!(f, "CollectedClientData could not be parsed: {err}"),
+ Self::Auth(ref err) => write!(f, "auth data could not be parsed: {err}"),
+ Self::PubKey(err) => err.fmt(f),
+ Self::Signature => f.write_str("the signature over the authenticator data and CollectedClientData could not be verified"),
+ }
+ }
+}
+impl From<AuthRespErr<AttestationObjectErr>> for RegCeremonyErr {
+ #[inline]
+ fn from(value: AuthRespErr<AttestationObjectErr>) -> Self {
+ match value {
+ AuthRespErr::CollectedClientData(err) => Self::CollectedClientData(err),
+ #[cfg(feature = "serde_relaxed")]
+ AuthRespErr::CollectedClientDataRelaxed(err) => Self::CollectedClientDataRelaxed(err),
+ AuthRespErr::Auth(err) => Self::AttestationObject(err),
+ AuthRespErr::PubKey(err) => Self::PubKey(err),
+ AuthRespErr::Signature => Self::AttestationSignature,
+ }
+ }
+}
+impl From<AuthRespErr<AuthAuthDataErr>> for AuthCeremonyErr {
+ #[inline]
+ fn from(value: AuthRespErr<AuthAuthDataErr>) -> Self {
+ match value {
+ AuthRespErr::CollectedClientData(err) => Self::CollectedClientData(err),
+ #[cfg(feature = "serde_relaxed")]
+ AuthRespErr::CollectedClientDataRelaxed(err) => Self::CollectedClientDataRelaxed(err),
+ AuthRespErr::Auth(err) => Self::AuthenticatorData(err),
+ AuthRespErr::PubKey(err) => Self::PubKey(err),
+ AuthRespErr::Signature => Self::AssertionSignature,
+ }
+ }
+}
+/// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data)
+/// container.
+///
+/// Note [`Self::Auth`] may be `Self`.
+pub(super) trait AuthDataContainer<'a>: Sized {
+ /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+ type Auth: AuthData<'a>;
+ /// Error returned from [`Self::from_data`].
+ type Err;
+ /// Converts `data` into [`ParsedAuthData`].
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `data` cannot be converted into `ParsedAuthData`.
+ fn from_data(data: &'a [u8]) -> Result<ParsedAuthData<'a, Self>, Self::Err>;
+ /// Returns the contained
+ /// [authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+ fn authenticator_data(&self) -> &Self::Auth;
+}
+/// [`AuthenticatorResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorresponse).
+pub(super) trait AuthResponse {
+ /// [Attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object) or
+ /// [authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+ type Auth<'a>: AuthDataContainer<'a> where Self: 'a;
+ /// Public key to use to verify the contained signature.
+ type CredKey<'a>;
+ /// Parses
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson)
+ /// based on `RELAXED` and [`Self::Auth`] via [`AuthDataContainer::from_data`] in addition to
+ /// verifying any possible signature over the concatenation of the raw
+ /// [`AuthDataContainer::Auth`] and `clientDataJSON` using `key` or the contained
+ /// public key if one exists. If `Self` contains a public key and should not be passed one, then it should set
+ /// [`Self::CredKey`] to `()`.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff parsing `clientDataJSON` errors, [`AuthDataContainer::from_data`] does, or the signature
+ /// is invalid.
+ ///
+ /// # Panics
+ ///
+ /// `panic`s iff `relaxed` and `serde_relaxed` is not enabled.
+ #[expect(
+ clippy::type_complexity,
+ reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable"
+ )]
+ fn parse_data_and_verify_sig(&self, key: Self::CredKey<'_>, relaxed: bool) -> Result<(CollectedClientData<'_>, Self::Auth<'_>), AuthRespErr<<Self::Auth<'_> as AuthDataContainer<'_>>::Err>>;
+}
+/// Ceremony response (i.e., [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#publickeycredential)).
+pub(super) trait Response {
+ /// [`AuthenticatorResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorresponse).
+ type Auth: AuthResponse;
+ /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
+ fn auth(&self) -> &Self::Auth;
+}
+/// Error returned from [`Ceremony::partial_validate`].
+pub(super) enum CeremonyErr<AuthDataErr> {
+ /// Timeout occurred.
+ Timeout,
+ /// Read [`AuthRespErr`] for information.
+ AuthResp(AuthRespErr<AuthDataErr>),
+ /// Origin did not validate.
+ OriginMismatch,
+ /// Cross origin was `true` but was not allowed to be.
+ CrossOrigin,
+ /// Top origin did not validate.
+ TopOriginMismatch,
+ /// Challenges don't match.
+ ChallengeMismatch,
+ /// `rpIdHash` does not match the SHA-256 hash of the [`RpId`].
+ RpIdHashMismatch,
+ /// User was not verified despite being required to.
+ UserNotVerified,
+ /// [`Backup::NotEligible`] was not sent back despite [`BackupReq::NotEligible`].
+ BackupEligible,
+ /// [`Backup::NotEligible`] was sent back despite [`BackupReq::Eligible`].
+ BackupNotEligible,
+ /// [`Backup::Exists`] was not sent back despite [`BackupReq::Exists`].
+ BackupDoesNotExist,
+}
+impl<A: Display> Display for CeremonyErr<A> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Timeout => f.write_str("ceremony timed out"),
+ Self::AuthResp(ref err) => err.fmt(f),
+ Self::OriginMismatch => {
+ f.write_str("the origin sent from the client is not an allowed origin")
+ }
+ Self::CrossOrigin => {
+ f.write_str("cross origin was from the client, but it is not allowed")
+ }
+ Self::TopOriginMismatch => {
+ f.write_str("the top origin sent from the client is not an allowed top origin")
+ }
+ Self::ChallengeMismatch => f.write_str(
+ "the challenge sent to the client does not match the challenge sent back",
+ ),
+ Self::RpIdHashMismatch => f.write_str(
+ "the SHA-256 hash of the RP ID doesn't match the hash sent from the client",
+ ),
+ Self::UserNotVerified => f.write_str("user was not verified despite being required to"),
+ Self::BackupEligible => f.write_str("credential is eligible to be backed up despite requiring that it not be"),
+ Self::BackupNotEligible => f.write_str("credential is not eligible to be backed up despite requiring that it be"),
+ Self::BackupDoesNotExist => f.write_str("credential backup does not exist despite requiring that a backup exist"),
+ }
+ }
+}
+impl From<CeremonyErr<AttestationObjectErr>> for RegCeremonyErr {
+ #[inline]
+ fn from(value: CeremonyErr<AttestationObjectErr>) -> Self {
+ match value {
+ CeremonyErr::Timeout => Self::Timeout,
+ CeremonyErr::AuthResp(err) => err.into(),
+ CeremonyErr::OriginMismatch => Self::OriginMismatch,
+ CeremonyErr::CrossOrigin => Self::CrossOrigin,
+ CeremonyErr::TopOriginMismatch => Self::TopOriginMismatch,
+ CeremonyErr::ChallengeMismatch => Self::ChallengeMismatch,
+ CeremonyErr::RpIdHashMismatch => Self::RpIdHashMismatch,
+ CeremonyErr::UserNotVerified => Self::UserNotVerified,
+ CeremonyErr::BackupEligible => Self::BackupEligible,
+ CeremonyErr::BackupNotEligible => Self::BackupNotEligible,
+ CeremonyErr::BackupDoesNotExist => Self::BackupDoesNotExist,
+ }
+ }
+}
+impl From<CeremonyErr<AuthAuthDataErr>> for AuthCeremonyErr {
+ #[inline]
+ fn from(value: CeremonyErr<AuthAuthDataErr>) -> Self {
+ match value {
+ CeremonyErr::Timeout => Self::Timeout,
+ CeremonyErr::AuthResp(err) => err.into(),
+ CeremonyErr::OriginMismatch => Self::OriginMismatch,
+ CeremonyErr::CrossOrigin => Self::CrossOrigin,
+ CeremonyErr::TopOriginMismatch => Self::TopOriginMismatch,
+ CeremonyErr::ChallengeMismatch => Self::ChallengeMismatch,
+ CeremonyErr::RpIdHashMismatch => Self::RpIdHashMismatch,
+ CeremonyErr::UserNotVerified => Self::UserNotVerified,
+ CeremonyErr::BackupEligible => Self::BackupEligible,
+ CeremonyErr::BackupNotEligible => Self::BackupNotEligible,
+ CeremonyErr::BackupDoesNotExist => Self::BackupDoesNotExist,
+ }
+ }
+}
+/// [`AllAcceptedCredentialsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions).
+///
+/// This can be sent to _an already authenticated user_ to inform what credentials are currently registered.
+/// This can be useful when a user deletes credentials on the RP's side but does not do so on the authenticator.
+/// When the client forwards this response to the authenticator, it can remove all credentials that don't have
+/// a [`CredentialId`] in [`Self::all_accepted_credential_ids`].
+#[derive(Debug)]
+pub struct AllAcceptedCredentialsOptions<'rp, 'user> {
+ /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-rpid).
+ pub rp_id: &'rp RpId,
+ /// [`userId`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-userid).
+ pub user_id: UserHandle<&'user [u8]>,
+ /// [`allAcceptedCredentialIds`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-allacceptedcredentialids).
+ pub all_accepted_credential_ids: Vec<CredentialId<Vec<u8>>>,
+}
+/// [`CurrentUserDetailsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions).
+///
+/// This can be sent to _an already authenticated user_ to inform the user information.
+/// This can be useful when a user updates their user information on the RP's side but does not do so on the authenticator.
+/// When the client forwards this response to the authenticator, it can update the user info for the associated credential.
+#[derive(Debug)]
+pub struct CurrentUserDetailsOptions<'rp, 'user_name, 'user_display_name, 'user_handle> {
+ /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-rpid).
+ pub rp_id: &'rp RpId,
+ /// [`userId`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-userid),
+ /// [`name`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-name), and
+ /// [`displayName`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-displayname).
+ pub user: PublicKeyCredentialUserEntity<'user_name, 'user_display_name, &'user_handle [u8]>,
+}
+#[cfg(test)]
+mod tests {
+ use super::{CollectedClientDataErr, ClientDataJsonParser, LimitedVerificationParser};
+ #[test]
+ fn parse_string() {
+ assert!(LimitedVerificationParser::<true>::parse_string(br#"abc""#)
+ .map_or(false, |tup| { tup.0 == "abc" && tup.1 == br#""# }));
+ assert!(LimitedVerificationParser::<false>::parse_string(br#"abc"23"#)
+ .map_or(false, |tup| { tup.0 == "abc" && tup.1 == br#"23"# }));
+ assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\"c"23"#)
+ .map_or(false, |tup| { tup.0 == r#"ab"c"# && tup.1 == br#"23"# }));
+ assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\\c"23"#)
+ .map_or(false, |tup| { tup.0 == r#"ab\c"# && tup.1 == br#"23"# }));
+ assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\u001fc"23"#)
+ .map_or(false, |tup| { tup.0 == "ab\u{001f}c" && tup.1 == br#"23"# }));
+ assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\u000dc"23"#)
+ .map_or(false, |tup| { tup.0 == "ab\u{000d}c" && tup.1 == br#"23"# }));
+ assert!(
+ LimitedVerificationParser::<true>::parse_string(b"\\\\\\\\\\\\a\\\\\\\\a\\\\\"").map_or(false, |tup| {
+ tup.0 == "\\\\\\a\\\\a\\" && tup.1.is_empty()
+ })
+ );
+ assert!(
+ LimitedVerificationParser::<false>::parse_string(b"\\\\\\\\\\a\\\\\\\\a\\\\\"").map_or_else(
+ |e| matches!(e, CollectedClientDataErr::InvalidEscapedString),
+ |_| false
+ )
+ );
+ assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\u0020c"23"#).map_or_else(
+ |err| matches!(err, CollectedClientDataErr::InvalidEscapedString),
+ |_| false
+ ));
+ assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\ac"23"#).map_or_else(
+ |err| matches!(err, CollectedClientDataErr::InvalidEscapedString),
+ |_| false
+ ));
+ assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\""#).map_or_else(
+ |err| matches!(err, CollectedClientDataErr::InvalidObject),
+ |_| false
+ ));
+ assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\u001Fc"23"#).map_or_else(
+ |err| matches!(err, CollectedClientDataErr::InvalidEscapedString),
+ |_| false
+ ));
+ assert!(LimitedVerificationParser::<true>::parse_string([0, b'"'].as_slice()).map_or_else(
+ |err| matches!(err, CollectedClientDataErr::InvalidEscapedString),
+ |_| false
+ ));
+ assert!(LimitedVerificationParser::<false>::parse_string([b'a', 255, b'"'].as_slice())
+ .map_or_else(|err| matches!(err, CollectedClientDataErr::Utf8(_)), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse_string([b'a', b'"', 255].as_slice()).is_ok());
+ assert!(
+ LimitedVerificationParser::<false>::parse_string(br#"""#).map_or(false, |tup| tup.0.is_empty() && tup.1.is_empty())
+ );
+ }
+ #[test]
+ fn c_data_json() {
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,{}}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.map_or(false, |v| v == "bob")));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob",a}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.map_or(false, |v| v == "bob")));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0.is_empty() && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Type), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\\e.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\"e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\"e.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0013e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\u{0013}e.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\3e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse([].as_slice())
+ .map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidStart), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOriginKey), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOrigin), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true","topOrigin":"https://abc.com"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<false>::parse(b"{\"type\":\"webauthn.get\",\"challenge\":\"AAAAAAAAAAAAAAAAAAAAAA\",\"origin\":\"https://example.com\",\"crossOrigin\":false,\xff}".as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.map_or(false, |v| v == "bob")));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0.is_empty() && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Type), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(
+ br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false}"#
+ .as_slice()
+ )
+ .map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\\e.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\"e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\"e.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0013e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\u{0013}e.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\3e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse([].as_slice())
+ .map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidStart), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOriginKey), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOrigin), |_| false));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"https://example.com"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::TopOriginSameAsOrigin), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"foo":true}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none()));
+ assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challengE":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"foo":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false));
+ assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create"challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossorigin":false,"foo":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false));
+ }
+}
diff --git a/src/response/auth.rs b/src/response/auth.rs
@@ -0,0 +1,531 @@
+#[cfg(all(doc, feature = "serde_relaxed"))]
+use super::super::request::FixedCapHashSet;
+#[cfg(doc)]
+use super::super::{
+ request::auth::{AuthenticationServerState, PublicKeyCredentialRequestOptions},
+ AuthenticatedCredential, RegisteredCredential, StaticState,
+};
+#[cfg(feature = "serde_relaxed")]
+use super::ser_relaxed::SerdeJsonErr;
+use super::{
+ super::UserHandle,
+ auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr},
+ cbor,
+ error::CollectedClientDataErr,
+ register::CompressedPubKey,
+ AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment,
+ CborSuccess, CollectedClientData, CredentialId, Flag, FromCbor, ParsedAuthData, Response,
+ SentChallenge,
+};
+use core::convert::Infallible;
+use ed25519_dalek::{Signature, Verifier as _};
+use p256::ecdsa::DerSignature as P256DerSig;
+use p384::ecdsa::DerSignature as P384DerSig;
+use rsa::{
+ pkcs1v15,
+ sha2::{digest::Digest as _, Sha256},
+};
+/// Contains error types.
+pub mod error;
+/// Contains functionality to deserialize data from a client.
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+#[cfg(feature = "serde")]
+pub(super) mod ser;
+/// Contains functionality to deserialize data from a client in a "relaxed" way.
+#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+#[cfg(feature = "serde_relaxed")]
+pub mod ser_relaxed;
+/// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension).
+#[derive(Clone, Copy, Debug)]
+pub enum HmacSecret {
+ /// No `hmac-secret` response.
+ None,
+ /// One encrypted `hmac-secret`.
+ One,
+ /// Two encrypted `hmac-secret`s.
+ Two,
+}
+impl FromCbor<'_> for HmacSecret {
+ type Err = AuthenticatorExtensionOutputErr;
+ fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
+ /// AES block size.
+ const AES_BLOCK_SIZE: usize = 16;
+ /// HMAC-SHA-256 output length.
+ const HMAC_SHA_256_LEN: usize = 32;
+ /// Length of two HMAC-SHA-256 outputs concatenated together.
+ const TWO_HMAC_SHA_256_LEN: usize = HMAC_SHA_256_LEN << 1;
+ // We need the smallest multiple of `AES_BLOCK_SIZE` that
+ // is strictly greater than `HMAC_SHA_256_LEN`.
+ /// AES-256 output length on a 32-byte input.
+ #[expect(
+ clippy::integer_division_remainder_used,
+ reason = "doesn't need to be constant time"
+ )]
+ const ONE_SECRET_LEN: usize =
+ HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (HMAC_SHA_256_LEN % AES_BLOCK_SIZE));
+ // We need the smallest multiple of `AES_BLOCK_SIZE` that
+ // is strictly greater than `TWO_HMAC_SHA_256_LEN`.
+ /// AES-256 output length on a 64-byte input.
+ #[expect(
+ clippy::integer_division_remainder_used,
+ reason = "doesn't need to be constant time"
+ )]
+ const TWO_SECRET_LEN: usize =
+ TWO_HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (TWO_HMAC_SHA_256_LEN % AES_BLOCK_SIZE));
+ cbor.split_at_checked(cbor::HMAC_SECRET.len())
+ .ok_or(AuthenticatorExtensionOutputErr::Len)
+ .and_then(|(key, key_rem)| {
+ if key == cbor::HMAC_SECRET {
+ key_rem
+ .split_first()
+ .ok_or(AuthenticatorExtensionOutputErr::Len)
+ .and_then(|(bytes, bytes_rem)| {
+ if *bytes == cbor::BYTES_INFO_24 {
+ bytes_rem
+ .split_first()
+ .ok_or(AuthenticatorExtensionOutputErr::Len)
+ .and_then(|(&len, len_rem)| {
+ len_rem.split_at_checked(usize::from(len)).ok_or(AuthenticatorExtensionOutputErr::Len).and_then(|(_, remaining)| {
+ match usize::from(len) {
+ ONE_SECRET_LEN => {
+ Ok(CborSuccess {
+ value: Self::One,
+ remaining,
+ })
+ }
+ TWO_SECRET_LEN => {
+ Ok(CborSuccess {
+ value: Self::Two,
+ remaining,
+ })
+ }
+ _ => Err(AuthenticatorExtensionOutputErr::HmacSecretValue),
+ }
+ })
+ })
+ } else {
+ Err(AuthenticatorExtensionOutputErr::HmacSecretType)
+ }
+ })
+ } else {
+ Err(AuthenticatorExtensionOutputErr::Unsupported)
+ }
+ })
+ }
+}
+/// [Authenticator extension output](https://www.w3.org/TR/webauthn-3/#authenticator-extension-output).
+#[derive(Clone, Copy, Debug)]
+pub struct AuthenticatorExtensionOutput {
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension).
+ pub hmac_secret: HmacSecret,
+}
+impl AuthExtOutput for AuthenticatorExtensionOutput {
+ fn missing(self) -> bool {
+ matches!(self.hmac_secret, HmacSecret::None)
+ }
+}
+impl FromCbor<'_> for AuthenticatorExtensionOutput {
+ type Err = AuthenticatorExtensionOutputErr;
+ fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
+ // We don't allow unsupported extensions; thus the only possibilities is any ordered element of
+ // the power set of {"hmac-secret":<HmacSecret>}.
+ cbor.split_first().map_or_else(
+ || {
+ Ok(CborSuccess {
+ value: Self {
+ hmac_secret: HmacSecret::None,
+ },
+ remaining: cbor,
+ })
+ },
+ |(map, map_rem)| {
+ if *map == cbor::MAP_1 {
+ HmacSecret::from_cbor(map_rem).map(|success| CborSuccess {
+ value: Self {
+ hmac_secret: success.value,
+ },
+ remaining: success.remaining,
+ })
+ } else {
+ Err(AuthenticatorExtensionOutputErr::CborHeader)
+ }
+ },
+ )
+ }
+}
+/// Unit type for `AuthData::CredData`.
+pub(crate) struct NoCred;
+impl FromCbor<'_> for NoCred {
+ type Err = Infallible;
+ fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
+ Ok(CborSuccess {
+ value: Self,
+ remaining: cbor,
+ })
+ }
+}
+/// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+#[derive(Clone, Copy, Debug)]
+pub struct AuthenticatorData<'a> {
+ /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash).
+ rp_id_hash: &'a [u8],
+ /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags).
+ flags: Flag,
+ /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount).
+ sign_count: u32,
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions).
+ extensions: AuthenticatorExtensionOutput,
+}
+impl<'a> AuthenticatorData<'a> {
+ /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash).
+ #[inline]
+ #[must_use]
+ pub const fn rp_id_hash(&self) -> &'a [u8] {
+ self.rp_id_hash
+ }
+ /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags).
+ #[inline]
+ #[must_use]
+ pub const fn flags(&self) -> Flag {
+ self.flags
+ }
+ /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount).
+ #[inline]
+ #[must_use]
+ pub const fn sign_count(&self) -> u32 {
+ self.sign_count
+ }
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions).
+ #[inline]
+ #[must_use]
+ pub const fn extensions(&self) -> AuthenticatorExtensionOutput {
+ self.extensions
+ }
+}
+impl<'a> AuthData<'a> for AuthenticatorData<'a> {
+ type UpBitErr = ();
+ type CredData = NoCred;
+ type Ext = AuthenticatorExtensionOutput;
+ fn contains_at_bit() -> bool {
+ false
+ }
+ fn user_is_not_present() -> Result<(), Self::UpBitErr> {
+ Err(())
+ }
+ fn new(
+ rp_id_hash: &'a [u8],
+ flags: Flag,
+ sign_count: u32,
+ _: Self::CredData,
+ extensions: Self::Ext,
+ ) -> Self {
+ Self {
+ rp_id_hash,
+ flags,
+ sign_count,
+ extensions,
+ }
+ }
+ fn rp_hash(&self) -> &'a [u8] {
+ self.rp_id_hash
+ }
+ fn flag(&self) -> Flag {
+ self.flags
+ }
+}
+impl<'a> AuthDataContainer<'a> for AuthenticatorData<'a> {
+ type Auth = Self;
+ type Err = AuthenticatorDataErr;
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
+ fn from_data(data: &'a [u8]) -> Result<ParsedAuthData<'a, Self>, Self::Err> {
+ // `data.len().checked_sub(Sha256::output_size()).unwrap()` is less than `data.len()`,
+ // so indexing is fine.
+ Self::try_from(&data[..data.len().checked_sub(Sha256::output_size()).unwrap_or_else(|| unreachable!("AuthenticatorData::from_data must be passed a slice with 32 bytes of trailing data"))]).map(|auth_data| ParsedAuthData { data: auth_data, auth_data_and_32_trailing_bytes: data, })
+ }
+ fn authenticator_data(&self) -> &Self::Auth {
+ self
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> {
+ type Error = AuthenticatorDataErr;
+ /// Deserializes `value` based on the
+ /// [authenticator data structure](https://www.w3.org/TR/webauthn-3/#table-authData).
+ #[expect(
+ clippy::panic_in_result_fn,
+ reason = "we want to crash when there is a bug"
+ )]
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ Self::from_cbor(value)
+ .map_err(AuthenticatorDataErr::from)
+ .map(|auth_data| {
+ assert!(
+ auth_data.remaining.is_empty(),
+ "there is a bug in AuthenticatorData::from_cbor"
+ );
+ auth_data.value
+ })
+ }
+}
+/// [`AuthenticatorAssertionResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse).
+#[derive(Debug)]
+pub struct AuthenticatorAssertion {
+ /// [`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)
+ /// followed by the SHA-256 hash of [`Self::client_data_json`].
+ authenticator_data_and_c_data_hash: Vec<u8>,
+ /// [`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>>>,
+}
+impl AuthenticatorAssertion {
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
+ #[inline]
+ #[must_use]
+ pub fn client_data_json(&self) -> &[u8] {
+ self.client_data_json.as_slice()
+ }
+ /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata).
+ #[expect(
+ clippy::arithmetic_side_effects,
+ clippy::indexing_slicing,
+ reason = "comment justifies their correctness"
+ )]
+ #[inline]
+ #[must_use]
+ pub fn authenticator_data(&self) -> &[u8] {
+ // We only allow creation via [`Self::new`] which creates [`Self::authenticator_data_and_c_data_hash`]
+ // by appending the SHA-256 hash of [`Self::client_data_json`] to the authenticator data that was passed;
+ // thus indexing is fine and subtraction won't cause underflow.
+ &self.authenticator_data_and_c_data_hash
+ [..self.authenticator_data_and_c_data_hash.len() - Sha256::output_size()]
+ }
+ /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature).
+ #[inline]
+ #[must_use]
+ pub fn signature(&self) -> &[u8] {
+ self.signature.as_slice()
+ }
+ /// [`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)
+ }
+ /// 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>,
+ mut 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 {
+ client_data_json,
+ authenticator_data_and_c_data_hash: authenticator_data,
+ signature,
+ user_handle,
+ }
+ }
+}
+impl AuthResponse for AuthenticatorAssertion {
+ type Auth<'a>
+ = AuthenticatorData<'a>
+ where
+ Self: 'a;
+ type CredKey<'a> = CompressedPubKey<&'a [u8], &'a [u8], &'a [u8], &'a [u8]>;
+ fn parse_data_and_verify_sig(
+ &self,
+ key: Self::CredKey<'_>,
+ relaxed: bool,
+ ) -> Result<
+ (CollectedClientData<'_>, Self::Auth<'_>),
+ AuthRespErr<<Self::Auth<'_> as AuthDataContainer<'_>>::Err>,
+ > {
+ /// Always `panic`s.
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[cfg(not(feature = "serde_relaxed"))]
+ fn get_client_collected_data(_: &[u8]) -> ! {
+ unreachable!("AuthenticatorAssertion::parse_data_and_verify_sig must not be passed true 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],
+ ) -> Result<
+ CollectedClientData<'_>,
+ AuthRespErr<
+ <<AuthenticatorAssertion as AuthResponse>::Auth<'_> as AuthDataContainer<'_>>::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())
+ } else {
+ CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice())
+ .map_err(AuthRespErr::CollectedClientData)
+ }
+ .and_then(|client_data_json| {
+ Self::Auth::from_data(self.authenticator_data_and_c_data_hash.as_slice())
+ .map_err(AuthRespErr::Auth)
+ .and_then(|val| {
+ match key {
+ CompressedPubKey::Ed25519(k) => k
+ .into_ver_key()
+ .map_err(AuthRespErr::PubKey)
+ .and_then(|ver_key| {
+ Signature::from_slice(self.signature.as_slice())
+ .and_then(|sig| {
+ ver_key.verify(
+ self.authenticator_data_and_c_data_hash.as_slice(),
+ &sig,
+ )
+ })
+ .map_err(|_e| AuthRespErr::Signature)
+ }),
+ CompressedPubKey::P256(k) => k
+ .into_ver_key()
+ .map_err(AuthRespErr::PubKey)
+ .and_then(|ver_key| {
+ P256DerSig::from_bytes(self.signature.as_slice())
+ .and_then(|sig| {
+ ver_key.verify(
+ self.authenticator_data_and_c_data_hash.as_slice(),
+ &sig,
+ )
+ })
+ .map_err(|_e| AuthRespErr::Signature)
+ }),
+ CompressedPubKey::P384(k) => k
+ .into_ver_key()
+ .map_err(AuthRespErr::PubKey)
+ .and_then(|ver_key| {
+ P384DerSig::from_bytes(self.signature.as_slice())
+ .and_then(|sig| {
+ ver_key.verify(
+ self.authenticator_data_and_c_data_hash.as_slice(),
+ &sig,
+ )
+ })
+ .map_err(|_e| AuthRespErr::Signature)
+ }),
+ CompressedPubKey::Rsa(k) => k
+ .as_ver_key()
+ .map_err(AuthRespErr::PubKey)
+ .and_then(|ver_key| {
+ pkcs1v15::Signature::try_from(self.signature.as_slice())
+ .and_then(|sig| {
+ ver_key.verify(
+ self.authenticator_data_and_c_data_hash.as_slice(),
+ &sig,
+ )
+ })
+ .map_err(|_e| AuthRespErr::Signature)
+ }),
+ }
+ .map(|()| (client_data_json, val.data))
+ })
+ })
+ }
+}
+/// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for authentication ceremonies.
+#[expect(
+ clippy::field_scoped_visibility_modifiers,
+ reason = "no invariants to uphold"
+)]
+#[derive(Debug)]
+pub struct Authentication {
+ /// [`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,
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
+ pub(crate) authenticator_attachment: AuthenticatorAttachment,
+}
+impl Authentication {
+ /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
+ #[inline]
+ #[must_use]
+ pub fn raw_id(&self) -> CredentialId<&[u8]> {
+ (&self.raw_id).into()
+ }
+ /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
+ #[inline]
+ #[must_use]
+ pub const fn response(&self) -> &AuthenticatorAssertion {
+ &self.response
+ }
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
+ #[inline]
+ #[must_use]
+ pub const fn authenticator_attachment(&self) -> AuthenticatorAttachment {
+ self.authenticator_attachment
+ }
+ /// Constructs an `Authentication`.
+ #[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
+ #[cfg(feature = "custom")]
+ #[inline]
+ #[must_use]
+ pub const fn new(
+ raw_id: CredentialId<Vec<u8>>,
+ response: AuthenticatorAssertion,
+ authenticator_attachment: AuthenticatorAttachment,
+ ) -> Self {
+ Self {
+ raw_id,
+ response,
+ authenticator_attachment,
+ }
+ }
+ /// Convenience function for
+ /// `CollectedClientData::from_client_data_json::<false>(self.response().client_data_json()).map(|c| c.challenge)`.
+ ///
+ /// This is useful when wanting to extract the corresponding [`AuthenticationServerState`] from
+ /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`CollectedClientData::from_client_data_json`] does.
+ #[inline]
+ pub fn challenge(&self) -> Result<SentChallenge, CollectedClientDataErr> {
+ CollectedClientData::from_client_data_json::<false>(
+ self.response.client_data_json.as_slice(),
+ )
+ .map(|c| c.challenge)
+ }
+ /// Convenience function for
+ /// `CollectedClientData::from_client_data_json_relaxed::<false>(self.response().client_data_json()).map(|c| c.challenge)`.
+ ///
+ /// This is useful when wanting to extract the corresponding [`AuthenticationServerState`] from
+ /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`CollectedClientData::from_client_data_json_relaxed`] does.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ #[inline]
+ pub fn challenge_relaxed(&self) -> Result<SentChallenge, SerdeJsonErr> {
+ CollectedClientData::from_client_data_json_relaxed::<false>(
+ self.response.client_data_json.as_slice(),
+ )
+ .map(|c| c.challenge)
+ }
+}
+impl Response for Authentication {
+ type Auth = AuthenticatorAssertion;
+ fn auth(&self) -> &Self::Auth {
+ &self.response
+ }
+}
diff --git a/src/response/auth/error.rs b/src/response/auth/error.rs
@@ -0,0 +1,297 @@
+#[cfg(feature = "serde_relaxed")]
+use super::super::SerdeJsonErr;
+use super::super::{
+ super::{CredentialErr, CredentialId},
+ AuthRespErr, AuthenticatorDataErr as AuthDataErr, CeremonyErr, CollectedClientDataErr,
+ PubKeyErr, RpId,
+};
+#[cfg(doc)]
+use super::{
+ super::{
+ super::{
+ request::{
+ auth::{
+ AllowedCredential, AllowedCredentials, AuthenticationServerState,
+ AuthenticationVerificationOptions, Extension,
+ PublicKeyCredentialRequestOptions,
+ },
+ BackupReq, UserVerificationRequirement,
+ },
+ AuthenticatedCredential, DynamicState, StaticState,
+ },
+ Backup,
+ },
+ Authentication, AuthenticatorAssertion, AuthenticatorAttachment, AuthenticatorData,
+ AuthenticatorExtensionOutput, CollectedClientData, CompressedPubKey, Flag, HmacSecret,
+ Signature,
+};
+use core::{
+ convert::Infallible,
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+/// Error returned in [`AuthenticatorDataErr::AuthenticatorExtension`].
+#[derive(Clone, Copy, Debug)]
+pub enum AuthenticatorExtensionOutputErr {
+ /// The `slice` had an invalid length.
+ Len,
+ /// The first byte did not represent a map of one key pair.
+ CborHeader,
+ /// `hmac-secret` was not a byte string with additional info 24.
+ HmacSecretType,
+ /// `hmac-secret` was not a byte string of length 48 or 80.
+ HmacSecretValue,
+ /// An unsupported extension existed.
+ Unsupported,
+ /// Fewer extensions existed than expected.
+ Missing,
+}
+impl Display for AuthenticatorExtensionOutputErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::Len => "CBOR authenticator extensions had an invalid length",
+ Self::CborHeader => {
+ "CBOR authenticator extensions did not represent a map of one key pair"
+ }
+ Self::HmacSecretType => "CBOR authenticator extension 'hmac-secret' was not a byte string with additional info 24",
+ Self::HmacSecretValue => "CBOR authenticator extension 'hmac-secret' was not a byte string of length 48 or 80",
+ Self::Unsupported => "CBOR authenticator extension had an unsupported extension",
+ Self::Missing => "CBOR authenticator extensions had fewer extensions than expected",
+ })
+ }
+}
+impl Error for AuthenticatorExtensionOutputErr {}
+/// Error returned from [`AuthenticatorData::try_from`].
+#[derive(Clone, Copy, Debug)]
+pub enum AuthenticatorDataErr {
+ /// The `slice` had an invalid length.
+ Len,
+ /// [`Flag::user_present`] was `false`.
+ UserNotPresent,
+ /// Bit 1 in [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags) is not 0.
+ FlagsBit1Not0,
+ /// Bit 5 in [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags) is not 0.
+ FlagsBit5Not0,
+ /// [AT](https://www.w3.org/TR/webauthn-3/#authdata-flags-at) was 1.
+ AttestedCredentialDataIncluded,
+ /// [BE](https://www.w3.org/TR/webauthn-3/#authdata-flags-be) and
+ /// [BS](https://www.w3.org/TR/webauthn-3/#authdata-flags-bs) bits were 0 and 1 respectively.
+ BackupWithoutEligibility,
+ /// Error returned when [`AuthenticatorExtensionOutput`] is malformed.
+ AuthenticatorExtension(AuthenticatorExtensionOutputErr),
+ /// [ED](https://www.w3.org/TR/webauthn-3/#authdata-flags-ed) bit was 0, but
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) existed.
+ NoExtensionBitWithData,
+ /// [ED](https://www.w3.org/TR/webauthn-3/#authdata-flags-ed) bit was 1, but
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) did not exist.
+ ExtensionBitWithoutData,
+ /// There was data remaining that could not be deserialized.
+ TrailingData,
+}
+impl Display for AuthenticatorDataErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Len => AuthDataErr::<(), Infallible, AuthenticatorExtensionOutputErr>::Len.fmt(f),
+ Self::UserNotPresent => f.write_str("the user was not present"),
+ Self::FlagsBit1Not0 => {
+ AuthDataErr::<(), Infallible, AuthenticatorExtensionOutputErr>::FlagsBit1Not0.fmt(f)
+ }
+ Self::FlagsBit5Not0 => {
+ AuthDataErr::<(), Infallible, AuthenticatorExtensionOutputErr>::FlagsBit5Not0.fmt(f)
+ }
+ Self::AttestedCredentialDataIncluded => {
+ f.write_str("attested credential data was included")
+ }
+ Self::BackupWithoutEligibility => AuthDataErr::<
+ (),
+ Infallible,
+ AuthenticatorExtensionOutputErr,
+ >::BackupWithoutEligibility
+ .fmt(f),
+ Self::AuthenticatorExtension(err) => err.fmt(f),
+ Self::NoExtensionBitWithData => AuthDataErr::<
+ (),
+ Infallible,
+ AuthenticatorExtensionOutputErr,
+ >::NoExtensionBitWithData
+ .fmt(f),
+ Self::ExtensionBitWithoutData => AuthDataErr::<
+ (),
+ Infallible,
+ AuthenticatorExtensionOutputErr,
+ >::ExtensionBitWithoutData
+ .fmt(f),
+ Self::TrailingData => {
+ AuthDataErr::<(), Infallible, AuthenticatorExtensionOutputErr>::TrailingData.fmt(f)
+ }
+ }
+ }
+}
+impl Error for AuthenticatorDataErr {}
+/// One or two.
+#[derive(Clone, Copy, Debug)]
+pub enum OneOrTwo {
+ /// One.
+ One,
+ /// Two.
+ Two,
+}
+impl Display for OneOrTwo {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::One => "one",
+ Self::Two => "two",
+ })
+ }
+}
+/// Error in [`AuthCeremonyErr::Extension`].
+#[derive(Clone, Copy, Debug)]
+pub enum ExtensionErr {
+ /// [`AuthenticatorExtensionOutput::hmac_secret`] was sent from the client but was not supposed to be.
+ ForbiddenHmacSecret,
+ /// [`AuthenticatorExtensionOutput::hmac_secret`] was sent from the client for a credential that is not PRF
+ /// capable.
+ HmacSecretForPrfIncapableCred,
+ /// [`Extension::prf`] was requested, but the required response was not sent back.
+ MissingHmacSecret,
+ /// [`Extension::prf`] was requested with the first number of PRF inputs, but the second number of
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
+ /// outputs were sent.
+ InvalidHmacSecretValue(OneOrTwo, OneOrTwo),
+}
+impl Display for ExtensionErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::ForbiddenHmacSecret => {
+ f.write_str("hmac-secret info was sent from the client, but it is not allowed")
+ }
+ Self::HmacSecretForPrfIncapableCred => f.write_str(
+ "hmac-secret info was sent from the client for a PRF-incapable credential",
+ ),
+ Self::MissingHmacSecret => f.write_str("hmac-secret was not sent from the client"),
+ Self::InvalidHmacSecretValue(sent, recv) => write!(
+ f,
+ "{sent} PRF input(s) were sent, but {recv} hmac-secret output(s) were received"
+ ),
+ }
+ }
+}
+impl Error for ExtensionErr {}
+/// Error returned by [`AuthenticationServerState::verify`].
+#[derive(Debug)]
+pub enum AuthCeremonyErr {
+ /// [`PublicKeyCredentialRequestOptions::timeout`] was exceeded.
+ Timeout,
+ /// [`AuthenticatorAssertion::client_data_json`] could not be parsed by
+ /// [`CollectedClientData::from_client_data_json`].
+ CollectedClientData(CollectedClientDataErr),
+ /// [`AuthenticatorAssertion::client_data_json`] could not be parsed by
+ /// [`CollectedClientData::from_client_data_json_relaxed`].
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ CollectedClientDataRelaxed(SerdeJsonErr),
+ /// [`AuthenticatorAssertion::authenticator_data`] could not be parsed into an
+ /// [`AuthenticatorData`].
+ AuthenticatorData(AuthenticatorDataErr),
+ /// [`CompressedPubKey`] was not valid.
+ PubKey(PubKeyErr),
+ /// [`CompressedPubKey`] was not able to verify [`AuthenticatorAssertion::signature`].
+ AssertionSignature,
+ /// [`CollectedClientData::origin`] does not match one of the values in
+ /// [`AuthenticationVerificationOptions::allowed_origins`].
+ OriginMismatch,
+ /// [`CollectedClientData::cross_origin`] was `true`, but
+ /// [`AuthenticationVerificationOptions::allowed_top_origins`] was `None`.
+ CrossOrigin,
+ /// [`CollectedClientData::top_origin`] does not match one of the values in
+ /// [`AuthenticationVerificationOptions::allowed_top_origins`].
+ TopOriginMismatch,
+ /// [`PublicKeyCredentialRequestOptions::challenge`] and [`CollectedClientData::challenge`] don't match.
+ ChallengeMismatch,
+ /// The SHA-256 hash of the [`RpId`] does not match [`AuthenticatorData::rp_id_hash`].
+ RpIdHashMismatch,
+ /// [`PublicKeyCredentialRequestOptions::user_verification`] was set to
+ /// [`UserVerificationRequirement::Required`], but [`Flag::user_verified`] was `false`.
+ UserNotVerified,
+ /// [`Backup::NotEligible`] was not sent back despite [`BackupReq::NotEligible`].
+ BackupEligible,
+ /// [`Backup::NotEligible`] was sent back despite [`BackupReq::Eligible`].
+ BackupNotEligible,
+ /// [`Backup::Exists`] was not sent back despite [`BackupReq::Exists`].
+ BackupDoesNotExist,
+ /// [`AuthenticatorAttachment`] was not sent back despite being required.
+ MissingAuthenticatorAttachment,
+ /// [`AuthenticatorAttachment`] modality changed despite it being forbidden to do so.
+ AuthenticatorAttachmentMismatch,
+ /// Variant returned when there is an issue with [`Extension`]s.
+ Extension(ExtensionErr),
+ /// [`AuthenticatorData::sign_count`] was not strictly greater than [`DynamicState::sign_count`].
+ SignatureCounter,
+ /// [`AuthenticatorAssertion::user_handle`] did not match [`AuthenticatedCredential::user_id`].
+ UserHandleMismatch,
+ /// [`Authentication::raw_id`] did not match [`AuthenticatedCredential::id`].
+ CredentialIdMismatch,
+ /// [`AllowedCredentials`] did not have a matching [`CredentialId`] as
+ /// [`Authentication::raw_id`].
+ NoMatchingAllowedCredential,
+ /// [`AllowedCredentials`] is empty (i.e., a discoverable request was issued), but
+ /// [`AuthenticatorAssertion::user_handle`] was [`None`].
+ MissingUserHandle,
+ /// Variant returned when [`AuthenticatedCredential`] cannot be updated due to invalid state.
+ Credential(CredentialErr),
+}
+impl Display for AuthCeremonyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Timeout => CeremonyErr::<AuthenticatorDataErr>::Timeout.fmt(f),
+ Self::CollectedClientData(ref err) => write!(f, "clientDataJSON could not be parsed: {err}"),
+ #[cfg(feature = "serde_relaxed")]
+ Self::CollectedClientDataRelaxed(ref err) => write!(f, "clientDataJSON could not be parsed: {err}"),
+ Self::AuthenticatorData(err) => err.fmt(f),
+ Self::PubKey(err) => err.fmt(f),
+ Self::AssertionSignature => AuthRespErr::<AuthenticatorDataErr>::Signature.fmt(f),
+ Self::OriginMismatch => CeremonyErr::<AuthenticatorDataErr>::OriginMismatch.fmt(f),
+ Self::CrossOrigin => CeremonyErr::<AuthenticatorDataErr>::CrossOrigin.fmt(f),
+ Self::TopOriginMismatch => CeremonyErr::<AuthenticatorDataErr>::TopOriginMismatch.fmt(f),
+ Self::BackupEligible => CeremonyErr::<AuthenticatorDataErr>::BackupEligible.fmt(f),
+ Self::BackupNotEligible => CeremonyErr::<AuthenticatorDataErr>::BackupNotEligible.fmt(f),
+ Self::BackupDoesNotExist => CeremonyErr::<AuthenticatorDataErr>::BackupDoesNotExist.fmt(f),
+ Self::ChallengeMismatch => CeremonyErr::<AuthenticatorDataErr>::ChallengeMismatch.fmt(f),
+ Self::RpIdHashMismatch => CeremonyErr::<AuthenticatorDataErr>::RpIdHashMismatch.fmt(f),
+ Self::UserNotVerified => CeremonyErr::<AuthenticatorDataErr>::UserNotVerified.fmt(f),
+ Self::MissingAuthenticatorAttachment=> f.write_str(
+ "authenticator attachment was not sent back despite being required",
+ ),
+ Self::AuthenticatorAttachmentMismatch => f.write_str(
+ "authenticator attachment modality changed despite not being allowed to",
+ ),
+ Self::Extension(err) => err.fmt(f),
+ Self::SignatureCounter => f.write_str(
+ "the signature counter sent back is not strictly greater than the previous counter",
+ ),
+ Self::UserHandleMismatch => f.write_str("the user handle does not match"),
+ Self::CredentialIdMismatch => f.write_str("the credential ID does not match"),
+ Self::NoMatchingAllowedCredential => f.write_str("none of the credentials used to start the non-discoverable request have the same Credential ID as the credential used to finish the ceremony"),
+ Self::MissingUserHandle => f.write_str("the credential used to finish the ceremony did not have a user handle despite a discoverable request being issued"),
+ Self::Credential(err) => err.fmt(f),
+ }
+ }
+}
+impl Error for AuthCeremonyErr {}
+/// [`UnknownCredentialOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-unknowncredentialoptions).
+///
+/// This can be sent to the client when an authentication ceremony fails due to an unknown [`CredentialId`]. This
+/// can be due to the user deleting a credential on the RP's side but not deleting it on the authenticator. This
+/// response can be forwarded to the authenticator which can subsequently delete the credential.
+#[derive(Debug)]
+pub struct UnknownCredentialOptions<'rp, 'cred> {
+ /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dictdef-unknowncredentialoptions-rpid).
+ pub rp_id: &'rp RpId,
+ /// [`credentialId`](https://www.w3.org/TR/webauthn-3/#dictdef-unknowncredentialoptions-credentialid).
+ pub credential_id: CredentialId<&'cred [u8]>,
+}
diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs
@@ -0,0 +1,1666 @@
+#![expect(
+ clippy::question_mark_used,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+use super::{
+ super::{
+ super::response::ser::{Base64DecodedVal, PublicKeyCredential},
+ ser::{
+ AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues,
+ ClientExtensions,
+ },
+ BASE64URL_NOPAD_ENC,
+ },
+ error::UnknownCredentialOptions,
+ Authentication, AuthenticatorAssertion,
+};
+#[cfg(doc)]
+use super::{AuthenticatorAttachment, CredentialId, UserHandle};
+use core::{
+ fmt::{self, Formatter},
+ marker::PhantomData,
+ str,
+};
+#[cfg(doc)]
+use data_encoding::BASE64URL_NOPAD;
+use rsa::sha2::{digest::OutputSizeUser as _, Sha256};
+use serde::{
+ de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor},
+ ser::{Serialize, SerializeStruct as _, Serializer},
+};
+/// `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;
+ 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>,
+ {
+ /// Fields in `AuthenticatorAssertionResponseJSON`.
+ enum Field<const IGNORE_UNKNOWN: bool> {
+ /// `clientDataJSON`.
+ ClientDataJson,
+ /// `authenticatorData`.
+ AuthenticatorData,
+ /// `signature`.
+ Signature,
+ /// `userHandle`.
+ UserHandle,
+ /// Unknown field.
+ Other,
+ }
+ impl<'e, const I: bool> Deserialize<'e> for Field<I> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor<const IGNORE_UNKNOWN: bool>;
+ 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}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ CLIENT_DATA_JSON => Ok(Field::ClientDataJson),
+ AUTHENTICATOR_DATA => Ok(Field::AuthenticatorData),
+ SIGNATURE => Ok(Field::Signature),
+ USER_HANDLE => Ok(Field::UserHandle),
+ _ => {
+ if IG {
+ Ok(Field::Other)
+ } else {
+ Err(E::unknown_field(v, AUTH_ASSERT_FIELDS))
+ }
+ }
+ }
+ }
+ }
+ 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,
+ clippy::unreachable,
+ reason = "we want to crash when there is a bug"
+ )]
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ crate::base64url_nopad_decode_len(v.len()).ok_or_else(|| E::invalid_value(Unexpected::Str(v), &"a shorter 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.checked_add(Sha256::output_size()).unwrap_or_else(|| unreachable!("there is a bug webauthn_rp::base64url_nopad_decode_len"))];
+ auth_data.truncate(len);
+ BASE64URL_NOPAD_ENC.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;
+ let mut user_handle = None;
+ while let Some(key) = map.next_key::<Field<R>>()? {
+ match key {
+ Field::ClientDataJson => {
+ if client_data.is_some() {
+ return Err(Error::duplicate_field(CLIENT_DATA_JSON));
+ }
+ client_data = map
+ .next_value::<Base64DecodedVal>()
+ .map(|c_data| c_data.0)
+ .map(Some)?;
+ }
+ Field::AuthenticatorData => {
+ if auth.is_some() {
+ return Err(Error::duplicate_field(AUTHENTICATOR_DATA));
+ }
+ auth = map
+ .next_value::<AuthData>()
+ .map(|auth_data| Some(auth_data.0))?;
+ }
+ Field::Signature => {
+ if sig.is_some() {
+ return Err(Error::duplicate_field(SIGNATURE));
+ }
+ sig = map
+ .next_value::<Base64DecodedVal>()
+ .map(|signature| signature.0)
+ .map(Some)?;
+ }
+ Field::UserHandle => {
+ if user_handle.is_some() {
+ return Err(Error::duplicate_field(USER_HANDLE));
+ }
+ user_handle = map.next_value().map(Some)?;
+ }
+ Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
+ }
+ }
+ client_data
+ .ok_or_else(|| Error::missing_field(CLIENT_DATA_JSON))
+ .and_then(|client_data_json| {
+ auth.ok_or_else(|| Error::missing_field(AUTHENTICATOR_DATA))
+ .and_then(|authenticator_data| {
+ sig.ok_or_else(|| Error::missing_field(SIGNATURE))
+ .map(|signature| {
+ AuthenticatorAssertion::new(
+ client_data_json,
+ authenticator_data,
+ signature,
+ user_handle.flatten(),
+ )
+ })
+ })
+ })
+ }
+}
+/// `"clientDataJSON"`.
+const CLIENT_DATA_JSON: &str = "clientDataJSON";
+/// `"authenticatorData"`.
+const AUTHENTICATOR_DATA: &str = "authenticatorData";
+/// `"signature"`.
+const SIGNATURE: &str = "signature";
+/// `"userHandle"`.
+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 {
+ /// Deserializes a `struct` based on
+ /// [`AuthenticatorAssertionResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorassertionresponsejson).
+ ///
+ /// Note unknown keys and duplicate keys are forbidden;
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-clientdatajson),
+ /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-authenticatordata),
+ /// and
+ /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-signature) are
+ /// base64url-decoded;
+ /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-userhandle) is
+ /// `null` or deserialized via [`UserHandle::deserialize`]; and all `required` fields in the
+ /// `AuthenticatorAssertionResponseJSON` Web IDL `dictionary` exist (and are not `null`).
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_struct(
+ "AuthenticatorAssertion",
+ AUTH_ASSERT_FIELDS,
+ AuthenticatorAssertionVisitor::<false>,
+ )
+ }
+}
+/// Empty map of client extensions.
+pub(super) struct ClientExtensionsOutputs;
+/// `Visitor` for `ClientExtensionsOutputs`.
+///
+/// Unknown fields are ignored iff `RELAXED`.
+pub(super) struct ClientExtensionsOutputsVisitor<const RELAXED: bool, PRF>(
+ pub PhantomData<fn() -> PRF>,
+);
+impl<'d, const R: bool, P> Visitor<'d> for ClientExtensionsOutputsVisitor<R, P>
+where
+ P: for<'a> Deserialize<'a>,
+{
+ type Value = ClientExtensionsOutputs;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("ClientExtensionsOutputs")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Allowed fields.
+ enum Field<const IGNORE_UNKNOWN: bool> {
+ /// `prf`.
+ Prf,
+ /// Unknown field.
+ Other,
+ }
+ impl<'e, const I: bool> Deserialize<'e> for Field<I> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ ///
+ /// Unknown fields are ignored iff `IGNORE_UNKNOWN`.
+ struct FieldVisitor<const IGNORE_UNKNOWN: bool>;
+ impl<const IG: bool> Visitor<'_> for FieldVisitor<IG> {
+ type Value = Field<IG>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{PRF}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ PRF => Ok(Field::Prf),
+ _ => {
+ if IG {
+ Ok(Field::Other)
+ } else {
+ Err(E::unknown_field(v, EXT_FIELDS))
+ }
+ }
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor::<I>)
+ }
+ }
+ let mut prf = None;
+ while let Some(key) = map.next_key::<Field<R>>()? {
+ match key {
+ Field::Prf => {
+ if prf.is_some() {
+ return Err(Error::duplicate_field(PRF));
+ }
+ prf = map.next_value::<Option<P>>().map(Some)?;
+ }
+ Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
+ }
+ }
+ Ok(ClientExtensionsOutputs)
+ }
+}
+impl ClientExtensions for ClientExtensionsOutputs {
+ fn empty() -> Self {
+ Self
+ }
+}
+/// `"prf"`
+const PRF: &str = "prf";
+/// `AuthenticationExtensionsClientOutputsJSON` fields.
+pub(super) const EXT_FIELDS: &[&str; 1] = &[PRF];
+impl<'de> Deserialize<'de> for ClientExtensionsOutputs {
+ /// Deserializes a `struct` based on
+ /// [`AuthenticationExtensionsClientOutputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientoutputsjson).
+ ///
+ /// Note that unknown and duplicate keys are forbidden and
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf) is `null`
+ /// or deserialized via [`AuthenticationExtensionsPrfOutputs::deserialize`].
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_struct(
+ "ClientExtensionsOutputs",
+ EXT_FIELDS,
+ ClientExtensionsOutputsVisitor::<
+ false,
+ AuthenticationExtensionsPrfOutputsHelper<
+ false,
+ false,
+ AuthenticationExtensionsPrfValues,
+ >,
+ >(PhantomData),
+ )
+ }
+}
+impl<'de> Deserialize<'de> for Authentication {
+ /// Deserializes a `struct` based on
+ /// [`AuthenticationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson).
+ ///
+ /// Note that unknown and duplicate keys are forbidden;
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-id) and
+ /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-rawid) are deserialized
+ /// via [`CredentialId::deserialize`];
+ /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-response) is deserialized
+ /// via [`AuthenticatorAssertion::deserialize`];
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-authenticatorattachment)
+ /// is `null` or deserialized via [`AuthenticatorAttachment::deserialize`];
+ /// [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-clientextensionresults)
+ /// is deserialized such that it is an empty map or a map that only contains
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf) which additionally must be
+ /// `null` or an
+ /// [`AuthenticationExtensionsPRFOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs)
+ /// such that unknown and duplicate keys are forbidden,
+ /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled)
+ /// is forbidden (including being assigned `null`),
+ /// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) must not exist,
+ /// be `null`, or be an
+ /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues)
+ /// with no unknown or duplicate keys,
+ /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) must exist but be
+ /// `null`, and
+ /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) can exist but
+ /// must be `null` if so; all `required` fields in the `AuthenticationResponseJSON` Web IDL `dictionary` exist
+ /// (and are not `null`); [`type`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-type) is
+ /// `"public-key"`; and the decoded `id` and decoded `rawId` are the same.
+ #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ PublicKeyCredential::<false, false, AuthenticatorAssertion, ClientExtensionsOutputs>::deserialize(
+ deserializer,
+ )
+ .map(|cred| Self {
+ raw_id: cred.id.unwrap_or_else(|| {
+ unreachable!("there is a bug in PublicKeyCredential::deserialize")
+ }),
+ response: cred.response,
+ authenticator_attachment: cred.authenticator_attachment,
+ })
+ }
+}
+impl Serialize for UnknownCredentialOptions<'_, '_> {
+ /// Serializes `self` to conform with
+ /// [`UnknownCredentialOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-unknowncredentialoptions).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use core::str::FromStr;
+ /// # use webauthn_rp::{request::{AsciiDomain, RpId}, response::{auth::error::UnknownCredentialOptions, CredentialId}};
+ /// # #[cfg(feature = "custom")]
+ /// let credential_id = CredentialId::try_from(vec![0; 16])?;
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(
+ /// serde_json::to_string(&UnknownCredentialOptions {
+ /// rp_id: &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
+ /// credential_id: (&credential_id).into(),
+ /// })
+ /// .unwrap(),
+ /// r#"{"rpId":"example.com","credentialId":"AAAAAAAAAAAAAAAAAAAAAA"}"#
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("UnknownCredentialOptions", 2)
+ .and_then(|mut ser| {
+ ser.serialize_field("rpId", self.rp_id).and_then(|()| {
+ ser.serialize_field("credentialId", &self.credential_id)
+ .and_then(|()| ser.end())
+ })
+ })
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::{
+ super::{Authentication, AuthenticatorAttachment},
+ BASE64URL_NOPAD_ENC,
+ };
+ use rsa::sha2::{Digest as _, Sha256};
+ use serde::de::{Error as _, Unexpected};
+ use serde_json::Error;
+ #[test]
+ fn eddsa_authentication_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let auth_data = [
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0000_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ ];
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(auth_data.as_slice());
+ let b64_sig = BASE64URL_NOPAD_ENC.encode([].as_slice());
+ let b64_user = BASE64URL_NOPAD_ENC.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
+ )));
+ // `id` and `rawId` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Bytes(
+ BASE64URL_NOPAD_ENC
+ .decode("ABABABABABABABABABABAA".as_bytes())
+ .unwrap()
+ .as_slice(),
+ ),
+ &format!("id and rawId to match: CredentialId({:?})", [0; 16]).as_str(),
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "ABABABABABABABABABABAA",
+ "response": {
+ "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
+ );
+ // missing `id`.
+ err = Error::missing_field("id").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ serde_json::json!({
+ "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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `id`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"id")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ serde_json::json!({
+ "id": null,
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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 `rawId`.
+ err = Error::missing_field("rawId").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ 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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `rawId`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"rawId")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ 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()
+ )
+ .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::<Authentication>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<Authentication>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<Authentication>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<Authentication>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<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());
+ // `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());
+ // `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
+ )));
+ // Unknown `authenticatorAttachment`.
+ err = Error::invalid_value(
+ Unexpected::Str("Platform"),
+ &"'platform' or 'cross-platform'",
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": "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::<Authentication>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<Authentication>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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
+ );
+ // Missing `response`.
+ err = Error::missing_field("response").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ 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"), &"AuthenticatorAssertion")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ 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::<Authentication>(
+ 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`.
+ err = Error::missing_field("clientExtensionResults")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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,
+ },
+ "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"),
+ &"clientExtensionResults to be a map of allowed client extensions",
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": null,
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `type`.
+ err = Error::missing_field("type").to_string().into_bytes();
+ assert_eq!(
+ 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": {},
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `type`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"type to be 'public-key'")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": {},
+ "type": null
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // 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::<Authentication>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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"), &"PublicKeyCredential")
+ .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()],
+ 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()],
+ err
+ );
+ // Unknown field in `response`.
+ err = Error::unknown_field(
+ "foo",
+ [
+ "clientDataJSON",
+ "authenticatorData",
+ "signature",
+ "userHandle",
+ ]
+ .as_slice(),
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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,
+ "foo": true,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field in `response`.
+ err = Error::duplicate_field("userHandle")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\",
+ \"userHandle\": \"{b64_user}\"
+ }},
+ \"clientExtensionResults\": {{}},
+ \"type\": \"public-key\"
+
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown field in `PublicKeyCredential`.
+ err = Error::unknown_field(
+ "foo",
+ [
+ "id",
+ "type",
+ "rawId",
+ "response",
+ "authenticatorAttachment",
+ "clientExtensionResults",
+ ]
+ .as_slice(),
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": {},
+ "type": "public-key",
+ "foo": true,
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field in `PublicKeyCredential`.
+ err = Error::duplicate_field("id").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ 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
+ );
+ }
+ #[test]
+ fn client_extensions() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let auth_data = [
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0000_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ ];
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(auth_data.as_slice());
+ let b64_sig = BASE64URL_NOPAD_ENC.encode([].as_slice());
+ let b64_user = BASE64URL_NOPAD_ENC.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
+ )));
+ // `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());
+ // 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::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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field.
+ err = Error::duplicate_field("prf").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"prf\": null,
+ \"prf\": null
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // Duplicate field in `prf`.
+ err = Error::duplicate_field("results").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"prf\": {{
+ \"results\": null,
+ \"results\": null
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `first`.
+ err = Error::missing_field("first").to_string().into_bytes();
+ assert_eq!(
+ 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": {},
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // `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());
+ // Non-`null` `first`.
+ err = Error::invalid_type(Unexpected::Option, &"null")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": ""
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Non-`null` `second`.
+ err = Error::invalid_type(Unexpected::Option, &"null")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": ""
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown `prf` field.
+ err = Error::unknown_field("enabled", ["results"].as_slice())
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": {
+ "enabled": true,
+ "results": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown `results` field.
+ err = Error::unknown_field("Second", ["first", "second"].as_slice())
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field in `results`.
+ err = Error::duplicate_field("first").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Authentication>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"prf\": {{
+ \"results\": {{
+ \"first\": null,
+ \"first\": null
+ }}
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ }
+}
diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs
@@ -0,0 +1,1282 @@
+#[cfg(doc)]
+use super::super::Challenge;
+use super::{
+ super::{
+ auth::ser::{
+ AuthenticatorAssertionVisitor, ClientExtensionsOutputs, ClientExtensionsOutputsVisitor,
+ AUTH_ASSERT_FIELDS, EXT_FIELDS,
+ },
+ ser::{AuthenticationExtensionsPrfOutputsHelper, ClientExtensions, PublicKeyCredential},
+ ser_relaxed::AuthenticationExtensionsPrfValuesRelaxed,
+ },
+ Authentication, AuthenticatorAssertion,
+};
+use core::marker::PhantomData;
+#[cfg(doc)]
+use data_encoding::BASE64URL_NOPAD;
+use serde::de::{Deserialize, Deserializer};
+/// `newtype` around `ClientExtensionsOutputs` with a "relaxed" [`Self::deserialize`] implementation.
+struct ClientExtensionsOutputsRelaxed(pub ClientExtensionsOutputs);
+impl ClientExtensions for ClientExtensionsOutputsRelaxed {
+ fn empty() -> Self {
+ Self(ClientExtensionsOutputs::empty())
+ }
+}
+impl<'de> Deserialize<'de> for ClientExtensionsOutputsRelaxed {
+ /// Same as [`ClientExtensionsOutputs::deserialize`] except unknown keys are ignored.
+ ///
+ /// Note that duplicate keys are still forbidden.
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer
+ .deserialize_struct(
+ "ClientExtensionsOutputsRelaxed",
+ EXT_FIELDS,
+ ClientExtensionsOutputsVisitor::<
+ true,
+ AuthenticationExtensionsPrfOutputsHelper<
+ true,
+ false,
+ AuthenticationExtensionsPrfValuesRelaxed,
+ >,
+ >(PhantomData),
+ )
+ .map(Self)
+ }
+}
+/// `newtype` around `AuthenticatorAssertion` with a "relaxed" [`Self::deserialize`] implementation.
+#[derive(Debug)]
+pub struct AuthenticatorAssertionRelaxed(pub AuthenticatorAssertion);
+impl<'de> Deserialize<'de> for AuthenticatorAssertionRelaxed {
+ /// Same as [`AuthenticatorAssertion::deserialize`] except unknown keys are ignored.
+ ///
+ /// Note that duplicate keys are still forbidden.
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer
+ .deserialize_struct(
+ "AuthenticatorAssertionRelaxed",
+ AUTH_ASSERT_FIELDS,
+ AuthenticatorAssertionVisitor::<true>,
+ )
+ .map(Self)
+ }
+}
+/// `newtype` around `Authentication` with a "relaxed" [`Self::deserialize`] implementation.
+#[derive(Debug)]
+pub struct AuthenticationRelaxed(pub Authentication);
+impl<'de> Deserialize<'de> for AuthenticationRelaxed {
+ /// 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`];
+ /// [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-clientextensionresults)
+ /// is deserialized such unknown keys are ignored but duplicate keys are forbidden,
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf) is `null` or an
+ /// [`AuthenticationExtensionsPRFOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs)
+ /// such that unknown keys are allowed but duplicate keys are forbidden,
+ /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled)
+ /// is forbidden (including being assigned `null`),
+ /// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) must not exist,
+ /// be `null`, or be an
+ /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues)
+ /// where unknown keys are ignored, duplicate keys are forbidden,
+ /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is not required but
+ /// if it exists it must be `null`, and
+ /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) can exist but
+ /// must be `null` if so; and only
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-id) and `response` are required. For
+ /// the other fields, they are allowed to not exist or be `null`.
+ ///
+ /// Note that duplicate keys are still forbidden, and data matching still applies when applicable.
+ #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ PublicKeyCredential::<
+ true,
+ false,
+ AuthenticatorAssertionRelaxed,
+ ClientExtensionsOutputsRelaxed,
+ >::deserialize(deserializer)
+ .map(|cred| {
+ Self(Authentication {
+ raw_id: cred.id.unwrap_or_else(|| {
+ unreachable!("there is a bug in PublicKeyCredential::deserialize")
+ }),
+ response: cred.response.0,
+ authenticator_attachment: cred.authenticator_attachment,
+ })
+ })
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::{
+ super::{super::BASE64URL_NOPAD_ENC, AuthenticatorAttachment},
+ AuthenticationRelaxed,
+ };
+ use rsa::sha2::{Digest as _, Sha256};
+ use serde::de::{Error as _, Unexpected};
+ use serde_json::Error;
+ #[test]
+ fn eddsa_authentication_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let auth_data = [
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0000_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ ];
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(auth_data.as_slice());
+ let b64_sig = BASE64URL_NOPAD_ENC.encode([].as_slice());
+ let b64_user = BASE64URL_NOPAD_ENC.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
+ )));
+ // `id` and `rawId` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Bytes(
+ BASE64URL_NOPAD_ENC
+ .decode("ABABABABABABABABABABAA".as_bytes())
+ .unwrap()
+ .as_slice(),
+ ),
+ &format!("id and rawId to match: CredentialId({:?})", [0; 16]).as_str(),
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "ABABABABABABABABABABAA",
+ "response": {
+ "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
+ );
+ // missing `id`.
+ err = Error::missing_field("id").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::json!({
+ "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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `id`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"id")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::json!({
+ "id": null,
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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 `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());
+ // `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());
+ // Missing `authenticatorData`.
+ err = Error::missing_field("authenticatorData")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<AuthenticationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<AuthenticationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<AuthenticationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<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());
+ // `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());
+ // `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
+ )));
+ // Unknown `authenticatorAttachment`.
+ err = Error::invalid_value(
+ Unexpected::Str("Platform"),
+ &"'platform' or 'cross-platform'",
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": "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::<AuthenticationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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::<AuthenticationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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
+ );
+ // Missing `response`.
+ err = Error::missing_field("response").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ 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"), &"AuthenticatorAssertion")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ 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::<AuthenticationRelaxed>(
+ 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::<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());
+ // `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());
+ // 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());
+ // `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());
+ // 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::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "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"), &"PublicKeyCredential")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ 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::<AuthenticationRelaxed>(
+ 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::<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());
+ // Duplicate field in `response`.
+ err = Error::duplicate_field("userHandle")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\",
+ \"userHandle\": \"{b64_user}\"
+ }},
+ \"clientExtensionResults\": {{}},
+ \"type\": \"public-key\"
+
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown field in `PublicKeyCredential`.
+ 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": "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>(
+ 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
+ );
+ }
+ #[test]
+ fn client_extensions() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let auth_data = [
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0000_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ ];
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(auth_data.as_slice());
+ let b64_sig = BASE64URL_NOPAD_ENC.encode([].as_slice());
+ let b64_user = BASE64URL_NOPAD_ENC.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
+ )));
+ // `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());
+ // 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());
+ // Duplicate field.
+ let mut err = Error::duplicate_field("prf").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"prf\": null,
+ \"prf\": null
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // Duplicate field in `prf`.
+ err = Error::duplicate_field("results").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<AuthenticationRelaxed>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"prf\": {{
+ \"results\": null,
+ \"results\": null
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // `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());
+ // `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());
+ // Non-`null` `first`.
+ err = Error::invalid_type(Unexpected::Option, &"null")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": ""
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Non-`null` `second`.
+ err = Error::invalid_type(Unexpected::Option, &"null")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": ""
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `enabled` is still not allowed.
+ err = Error::unknown_field("enabled", ["results"].as_slice())
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ 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": {
+ "enabled": true,
+ "results": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // 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
+ }
+ }
+ },
+ "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>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"signature\": \"{b64_sig}\",
+ \"userHandle\": \"{b64_user}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"prf\": {{
+ \"results\": {{
+ \"first\": null,
+ \"first\": null
+ }}
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ }
+}
diff --git a/src/response/bin.rs b/src/response/bin.rs
@@ -0,0 +1,150 @@
+use super::{
+ super::bin::{
+ Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible as _,
+ },
+ AuthTransports, AuthenticatorAttachment, Backup, CredentialId, CredentialIdErr,
+};
+use core::{
+ convert::Infallible,
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+/// [`Backup::NotEligible`] tag.
+const BACKUP_NOT_ELIGIBLE: u8 = 0;
+/// [`Backup::Eligible`] tag.
+const BACKUP_ELIGIBLE: u8 = 1;
+/// [`Backup::Exists`] tag.
+const BACKUP_EXISTS: u8 = 2;
+impl EncodeBuffer for Backup {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::NotEligible => BACKUP_NOT_ELIGIBLE,
+ Self::Eligible => BACKUP_ELIGIBLE,
+ Self::Exists => BACKUP_EXISTS,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for Backup {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ BACKUP_NOT_ELIGIBLE => Ok(Self::NotEligible),
+ BACKUP_ELIGIBLE => Ok(Self::Eligible),
+ BACKUP_EXISTS => Ok(Self::Exists),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+/// Error returned from [`AuthTransports::decode`].
+#[derive(Clone, Copy, Debug)]
+pub struct DecodeAuthTransportsErr;
+impl Display for DecodeAuthTransportsErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("AuthTransports could not be decoded")
+ }
+}
+impl Error for DecodeAuthTransportsErr {}
+impl Encode for AuthTransports {
+ type Output<'a>
+ = u8
+ where
+ Self: 'a;
+ type Err = Infallible;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ Ok(self.0)
+ }
+}
+impl Decode for AuthTransports {
+ type Input<'a> = u8;
+ type Err = DecodeAuthTransportsErr;
+ #[inline]
+ fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ if input <= Self::all().0 {
+ Ok(Self(input))
+ } else {
+ Err(DecodeAuthTransportsErr)
+ }
+ }
+}
+impl EncodeBuffer for AuthTransports {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.0.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for AuthTransports {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| Self::decode(val).map_err(|_e| EncDecErr))
+ }
+}
+impl<T: AsRef<[u8]>> Encode for CredentialId<T> {
+ type Output<'a>
+ = &'a [u8]
+ where
+ Self: 'a;
+ type Err = Infallible;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ Ok(self.0.as_ref())
+ }
+}
+impl Decode for CredentialId<Vec<u8>> {
+ type Input<'a> = Vec<u8>;
+ type Err = CredentialIdErr;
+ #[inline]
+ fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ match CredentialId::<&[u8]>::from_slice(input.as_slice()) {
+ 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>) {
+ // Max length is 1023, so this won't error.
+ self.0
+ .encode_into_buffer(buffer)
+ .unwrap_or_else(|_e| unreachable!("there is a bug in [u8]::encode_into_buffer"));
+ }
+}
+impl<'a> DecodeBuffer<'a> for CredentialId<Vec<u8>> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ <&[u8]>::decode_from_buffer(data).and_then(|val| {
+ CredentialId::<&[u8]>::from_slice(val)
+ .map_err(|_e| EncDecErr)
+ .map(|_| Self(val.to_owned()))
+ })
+ }
+}
+/// [`AuthenticatorAttachment::None`] tag.
+const AUTH_ATTACH_NONE: u8 = 0;
+/// [`AuthenticatorAttachment::Platform`] tag.
+const AUTH_ATTACH_PLATFORM: u8 = 1;
+/// [`AuthenticatorAttachment::CrossPlatform`] tag.
+const AUTH_ATTACH_CROSS_PLATFORM: u8 = 2;
+impl EncodeBuffer for AuthenticatorAttachment {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::None => AUTH_ATTACH_NONE,
+ Self::Platform => AUTH_ATTACH_PLATFORM,
+ Self::CrossPlatform => AUTH_ATTACH_CROSS_PLATFORM,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for AuthenticatorAttachment {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ AUTH_ATTACH_NONE => Ok(Self::None),
+ AUTH_ATTACH_PLATFORM => Ok(Self::Platform),
+ AUTH_ATTACH_CROSS_PLATFORM => Ok(Self::CrossPlatform),
+ _ => Err(EncDecErr),
+ })
+ }
+}
diff --git a/src/response/cbor.rs b/src/response/cbor.rs
@@ -0,0 +1,72 @@
+/// A [`u64`].
+const UINT: u8 = 0b000_00000;
+/// A negative integer whose value _m ∈ [-2^64 - 1, -1]_ is represented as _|m| - 1_.
+const NEG: u8 = 0b001_00000;
+/// A byte string.
+pub(super) const BYTES: u8 = 0b010_00000;
+/// A text string.
+const TEXT: u8 = 0b011_00000;
+/// A map of key-value pairs.
+const MAP: u8 = 0b101_00000;
+/// Simple values.
+const SIMPLE: u8 = 0b111_00000;
+/// [`UINT`] value `1`.
+pub(super) const ONE: u8 = UINT | 1;
+/// [`UINT`] value `2`.
+pub(super) const TWO: u8 = UINT | 2;
+/// [`UINT`] value `3`.
+pub(super) const THREE: u8 = UINT | 3;
+/// [`UINT`] value `6`.
+pub(super) const SIX: u8 = UINT | 6;
+/// [`NEG`] value `-1`.
+pub(super) const NEG_ONE: u8 = NEG;
+/// [`NEG`] value `-2`.
+pub(super) const NEG_TWO: u8 = NEG | 1;
+/// [`NEG`] value `-3`.
+pub(super) const NEG_THREE: u8 = NEG | 2;
+/// [`NEG`] value `-7`.
+pub(super) const NEG_SEVEN: u8 = NEG | 6;
+/// [`NEG`] value `-8`.
+pub(super) const NEG_EIGHT: u8 = NEG | 7;
+/// [`NEG`] value less than `-24` but greater than `-257`.
+pub(super) const NEG_INFO_24: u8 = NEG | 24;
+/// [`NEG`] value less than `-256` but greater than `-65537`.
+pub(super) const NEG_INFO_25: u8 = NEG | 25;
+/// [`BYTES`] length greater than `23` but less than `256`.
+pub(super) const BYTES_INFO_24: u8 = BYTES | 24;
+/// [`BYTES`] length greater than `255` but less than `65536`.
+pub(super) const BYTES_INFO_25: u8 = BYTES | 25;
+/// [`TEXT`] length `3`.
+pub(super) const TEXT_3: u8 = TEXT | 3;
+/// [`TEXT`] length `4`.
+pub(super) const TEXT_4: u8 = TEXT | 4;
+/// [`TEXT`] length `6`.
+pub(super) const TEXT_6: u8 = TEXT | 6;
+/// [`TEXT`] length `7`.
+pub(super) const TEXT_7: u8 = TEXT | 7;
+/// [`TEXT`] length `8`.
+pub(super) const TEXT_8: u8 = TEXT | 8;
+/// [`TEXT`] length `11`.
+pub(super) const TEXT_11: u8 = TEXT | 11;
+/// [`TEXT`] length `12`.
+pub(super) const TEXT_12: u8 = TEXT | 12;
+/// [`MAP`] length `0`.
+pub(super) const MAP_0: u8 = MAP;
+/// [`MAP`] length `1`.
+pub(super) const MAP_1: u8 = MAP | 1;
+/// [`MAP`] length `2`.
+pub(super) const MAP_2: u8 = MAP | 2;
+/// [`MAP`] length `3`.
+pub(super) const MAP_3: u8 = MAP | 3;
+/// [`MAP`] length `4`.
+pub(super) const MAP_4: u8 = MAP | 4;
+/// [`MAP`] length `5`.
+pub(super) const MAP_5: u8 = MAP | 5;
+/// [`SIMPLE`] value `false`.
+pub(super) const SIMPLE_FALSE: u8 = SIMPLE | 20;
+/// [`SIMPLE`] value `true`.
+pub(super) const SIMPLE_TRUE: u8 = SIMPLE | 21;
+/// [`TEXT`] value hmac-secret.
+pub(super) const HMAC_SECRET: [u8; 12] = [
+ TEXT_11, b'h', b'm', b'a', b'c', b'-', b's', b'e', b'c', b'r', b'e', b't',
+];
diff --git a/src/response/custom.rs b/src/response/custom.rs
@@ -0,0 +1,18 @@
+use super::{CredentialId, CredentialIdErr};
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for CredentialId<&'b [u8]> {
+ type Error = CredentialIdErr;
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ Self::from_slice(value).map_err(CredentialIdErr::from)
+ }
+}
+impl TryFrom<Vec<u8>> for CredentialId<Vec<u8>> {
+ type Error = CredentialIdErr;
+ #[inline]
+ fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
+ match CredentialId::<&[u8]>::try_from(value.as_slice()) {
+ Ok(_) => Ok(Self(value)),
+ Err(e) => Err(e),
+ }
+ }
+}
diff --git a/src/response/error.rs b/src/response/error.rs
@@ -0,0 +1,95 @@
+extern crate alloc;
+#[cfg(doc)]
+use super::{Challenge, CollectedClientData, CredentialId};
+use super::{CRED_ID_MAX_LEN, CRED_ID_MIN_LEN};
+use alloc::string::FromUtf8Error;
+use core::{
+ error::Error,
+ fmt::{self, Display, Formatter},
+ str::Utf8Error,
+};
+/// Error returned when a [`CredentialId`] does not have length inclusively between [`CRED_ID_MIN_LEN`] and
+/// [`CRED_ID_MAX_LEN`].
+#[derive(Clone, Copy, Debug)]
+pub struct CredentialIdErr;
+impl Display for CredentialIdErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "CredentialId did not have length inclusively between {CRED_ID_MIN_LEN} and {CRED_ID_MAX_LEN}",
+ )
+ }
+}
+impl Error for CredentialIdErr {}
+/// Error returned from [`CollectedClientData::from_client_data_json`].
+#[derive(Debug)]
+pub enum CollectedClientDataErr {
+ /// The `slice` had invalid length.
+ Len,
+ /// The `slice` did not begin with `{"type":"webauthn.`.
+ InvalidStart,
+ /// [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type)
+ /// was not `"webauthn.create"` during registration or `"webauthn.get"`
+ /// during authentication.
+ Type,
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge)
+ /// without whitespace was not the second key in the object.
+ ChallengeKey,
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge)
+ /// was not a valid base64url-encoding of [`Challenge`].
+ Challenge,
+ /// [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin)
+ /// without whitespace was not the third key in the object.
+ OriginKey,
+ /// [`crossOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-crossorigin)
+ /// without whitespace was not the fourth key in the object.
+ CrossOriginKey,
+ /// [`crossOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-crossorigin)
+ /// was not `true` or `false`.
+ CrossOrigin,
+ /// The object was not a valid JSON object.
+ InvalidObject,
+ /// [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin) or
+ /// [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin) was
+ /// not escaped correctly (i.e., a Unicode scalar value in U+0000–U+001F was not escaped or another Unicode
+ /// scalar value, sans `"` and `\`, was escaped).
+ InvalidEscapedString,
+ /// [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin) or
+ /// [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin) was
+ /// not valid UTF-8.
+ Utf8(Utf8Error),
+ /// [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin) or
+ /// [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin) was
+ /// not valid UTF-8.
+ Utf8Owned(FromUtf8Error),
+ /// [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin) existed
+ /// despite [`crossOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-crossorigin)
+ /// being `false`.
+ TopOriginWithoutCrossOrigin,
+ /// [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin) was the same value
+ /// as [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin).
+ TopOriginSameAsOrigin,
+}
+impl Display for CollectedClientDataErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Len => f.write_str("clientDataJSON had an invalid length"),
+ Self::InvalidStart => f.write_str(r#"clientDataJSON does not start with '{"type":"webauthn.'"#),
+ Self::Type => f.write_str(r#"clientDataJSON 'type' did not have value '"webauthn.create"' during registration or '"webauthn.get"' during authentication"#),
+ Self::ChallengeKey => f.write_str("clientDataJSON 'challenge' was not the second key in the object, or it had whitespace around it"),
+ Self::Challenge => f.write_str("clientDataJSON 'challenge' was not a valid base64url encoding of 16 bytes"),
+ Self::OriginKey => f.write_str("clientDataJSON 'origin' was not the third key in the object, or it had whitespace around it"),
+ Self::CrossOriginKey => f.write_str("clientDataJSON 'crossOrigin' was not the fourth key in the object, or it had whitespace around it"),
+ Self::CrossOrigin => f.write_str("clientDataJSON 'crossOrigin' was not false or true"),
+ Self::InvalidObject => f.write_str("clientDataJSON was an invalid object"),
+ Self::InvalidEscapedString => f.write_str("clientDataJSON 'origin' or 'topOrigin' was not escaped correctly"),
+ Self::Utf8(err) => write!(f, "clientDataJSON 'origin' or 'topOrigin' was not valid UTF-8: {err}"),
+ Self::Utf8Owned(ref err) => write!(f, "clientDataJSON 'origin' or 'topOrigin' was not valid UTF-8: {err}"),
+ Self::TopOriginWithoutCrossOrigin => f.write_str("clientDataJSON 'topOrigin' existed despite 'crossOrigin' being false"),
+ Self::TopOriginSameAsOrigin => f.write_str("clientDataJSON 'origin' and 'topOrigin' were the same"),
+ }
+ }
+}
+impl Error for CollectedClientDataErr {}
diff --git a/src/response/register.rs b/src/response/register.rs
@@ -0,0 +1,3202 @@
+#[cfg(all(doc, feature = "serde_relaxed"))]
+use super::super::request::FixedCapHashSet;
+#[cfg(feature = "serde_relaxed")]
+use super::ser_relaxed::SerdeJsonErr;
+#[cfg(all(doc, feature = "bin"))]
+use super::{
+ super::bin::{Decode, Encode},
+ register::bin::MetadataOwned,
+};
+use super::{
+ super::request::register::ResidentKeyRequirement,
+ cbor,
+ error::CollectedClientDataErr,
+ register::error::{
+ AaguidErr, AttestationErr, AttestationObjectErr, AttestedCredentialDataErr,
+ AuthenticatorDataErr, AuthenticatorExtensionOutputErr, CompressedP256PubKeyErr,
+ CompressedP384PubKeyErr, CoseKeyErr, Ed25519PubKeyErr, Ed25519SignatureErr, PubKeyErr,
+ RsaPubKeyErr, UncompressedP256PubKeyErr, UncompressedP384PubKeyErr,
+ },
+ AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthTransports,
+ AuthenticatorAttachment, Backup, CborSuccess, CollectedClientData, CredentialId, Flag,
+ FromCbor, ParsedAuthData, Response, SentChallenge,
+};
+#[cfg(doc)]
+use super::{
+ super::{
+ request::{
+ auth::{AuthenticationVerificationOptions, PublicKeyCredentialRequestOptions},
+ register::{Extension, RegistrationServerState},
+ BackupReq,
+ },
+ AuthenticatedCredential, RegisteredCredential,
+ },
+ AuthenticatorTransport,
+};
+use core::{
+ cmp::Ordering,
+ convert::Infallible,
+ fmt::{self, Display, Formatter},
+};
+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,
+};
+use p384::{
+ ecdsa::{DerSignature as P384Sig, VerifyingKey as P384VerKey},
+ AffinePoint as P384Affine, EncodedPoint as P384Pt, NistP384,
+};
+use rsa::{
+ pkcs1v15::{self, VerifyingKey as RsaVerKey},
+ sha2::{digest::Digest as _, Sha256},
+ BigUint, RsaPublicKey,
+};
+/// Contains functionality to (de)serialize data to a data store.
+#[cfg_attr(docsrs, doc(cfg(feature = "bin")))]
+#[cfg(feature = "bin")]
+pub mod bin;
+/// Contains error types.
+pub mod error;
+/// Contains functionality to deserialize data from a client.
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+#[cfg(feature = "serde")]
+mod ser;
+/// Contains functionality to deserialize data from a client in a "relaxed" way.
+#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+#[cfg(feature = "serde_relaxed")]
+pub mod ser_relaxed;
+/// [`credentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#dom-authenticationextensionsclientinputs-credentialprotectionpolicy).
+#[derive(Clone, Copy, Debug)]
+pub enum CredentialProtectionPolicy {
+ /// `credProtect` was not sent.
+ None,
+ /// [`userVerificationOptional`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptional).
+ UserVerificationOptional,
+ /// [`userVerificationOptionalWithCredentialIDList`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptionalwithcredentialidlist).
+ UserVerificationOptionalWithCredentialIdList,
+ /// [`userVerificationRequired`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationrequired).
+ UserVerificationRequired,
+}
+impl Display for CredentialProtectionPolicy {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::None => "no credential protection policy sent",
+ Self::UserVerificationOptional => "user verification optional",
+ Self::UserVerificationOptionalWithCredentialIdList => {
+ "user verification optional with credential ID list"
+ }
+ Self::UserVerificationRequired => "user verification required",
+ })
+ }
+}
+/// [Authenticator extension output](https://www.w3.org/TR/webauthn-3/#authenticator-extension-output).
+#[derive(Clone, Copy, Debug)]
+pub struct AuthenticatorExtensionOutput {
+ /// [`credProtect`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension).
+ pub cred_protect: CredentialProtectionPolicy,
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension).
+ pub hmac_secret: Option<bool>,
+ /// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension).
+ pub min_pin_length: Option<u8>,
+}
+impl AuthExtOutput for AuthenticatorExtensionOutput {
+ fn missing(self) -> bool {
+ matches!(self.cred_protect, CredentialProtectionPolicy::None)
+ && self.hmac_secret.is_none()
+ && self.min_pin_length.is_none()
+ }
+}
+/// [`AuthenticatorExtensionOutput`] extensions that are saved in [`StaticState`] because they are used during
+/// authentication ceremonies.
+#[derive(Clone, Copy, Debug)]
+pub struct AuthenticatorExtensionOutputStaticState {
+ /// [`AuthenticatorExtensionOutput::cred_protect`].
+ pub cred_protect: CredentialProtectionPolicy,
+ /// [`AuthenticatorExtensionOutput::hmac_secret`].
+ pub hmac_secret: Option<bool>,
+}
+/// [`AuthenticatorExtensionOutput`] extensions that are saved in [`Metadata`] because they are purely informative
+/// and not used during authentication ceremonies.
+#[derive(Clone, Copy, Debug)]
+pub struct AuthenticatorExtensionOutputMetadata {
+ /// [`AuthenticatorExtensionOutput::min_pin_length`].
+ pub min_pin_length: Option<u8>,
+}
+impl From<AuthenticatorExtensionOutput> for AuthenticatorExtensionOutputMetadata {
+ #[inline]
+ fn from(value: AuthenticatorExtensionOutput) -> Self {
+ Self {
+ min_pin_length: value.min_pin_length,
+ }
+ }
+}
+impl From<AuthenticatorExtensionOutput> for AuthenticatorExtensionOutputStaticState {
+ #[inline]
+ fn from(value: AuthenticatorExtensionOutput) -> Self {
+ Self {
+ cred_protect: value.cred_protect,
+ hmac_secret: value.hmac_secret,
+ }
+ }
+}
+impl FromCbor<'_> for CredentialProtectionPolicy {
+ type Err = AuthenticatorExtensionOutputErr;
+ fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
+ /// [`userVerificationOptional`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptional).
+ const USER_VERIFICATION_OPTIONAL: u8 = cbor::ONE;
+ /// [`userVerificationOptionalWithCredentialIDList`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptionalwithcredentialidlist).
+ const USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST: u8 = cbor::TWO;
+ /// [`userVerificationRequired`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationrequired).
+ const USER_VERIFICATION_REQUIRED: u8 = cbor::THREE;
+ /// `credProtect` key.
+ const KEY: [u8; 12] = [
+ cbor::TEXT_11,
+ b'c',
+ b'r',
+ b'e',
+ b'd',
+ b'P',
+ b'r',
+ b'o',
+ b't',
+ b'e',
+ b'c',
+ b't',
+ ];
+ cbor.split_at_checked(KEY.len()).map_or(
+ Ok(CborSuccess {
+ value: Self::None,
+ remaining: cbor,
+ }),
+ |(key, key_rem)| {
+ if key == KEY {
+ key_rem
+ .split_first()
+ .ok_or(AuthenticatorExtensionOutputErr::Len)
+ .and_then(|(uv, remaining)| {
+ match *uv {
+ USER_VERIFICATION_OPTIONAL => Ok(Self::UserVerificationOptional),
+ USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST => {
+ Ok(Self::UserVerificationOptionalWithCredentialIdList)
+ }
+ USER_VERIFICATION_REQUIRED => Ok(Self::UserVerificationRequired),
+ _ => Err(AuthenticatorExtensionOutputErr::CredProtectValue),
+ }
+ .map(|value| CborSuccess { value, remaining })
+ })
+ } else {
+ Ok(CborSuccess {
+ value: Self::None,
+ remaining: cbor,
+ })
+ }
+ },
+ )
+ }
+}
+/// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension).
+enum HmacSecret {
+ /// No `hmac-secret` extension.
+ None,
+ /// `hmac-secret` set to the contained `bool`.
+ Val(bool),
+}
+impl FromCbor<'_> for HmacSecret {
+ type Err = AuthenticatorExtensionOutputErr;
+ fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
+ cbor.split_at_checked(cbor::HMAC_SECRET.len()).map_or(
+ Ok(CborSuccess {
+ value: Self::None,
+ remaining: cbor,
+ }),
+ |(key, key_rem)| {
+ if key == cbor::HMAC_SECRET {
+ key_rem
+ .split_first()
+ .ok_or(AuthenticatorExtensionOutputErr::Len)
+ .and_then(|(hmac, remaining)| {
+ match *hmac {
+ cbor::SIMPLE_FALSE => Ok(Self::Val(false)),
+ cbor::SIMPLE_TRUE => Ok(Self::Val(true)),
+ _ => Err(AuthenticatorExtensionOutputErr::HmacSecretValue),
+ }
+ .map(|value| CborSuccess { value, remaining })
+ })
+ } else {
+ Ok(CborSuccess {
+ value: Self::None,
+ remaining: cbor,
+ })
+ }
+ },
+ )
+ }
+}
+/// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension).
+enum MinPinLength {
+ /// No `minPinLength` extension.
+ None,
+ /// `minPinLength` with the value of the contained `u8`.
+ Val(u8),
+}
+impl FromCbor<'_> for MinPinLength {
+ type Err = AuthenticatorExtensionOutputErr;
+ fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
+ /// `minPinLength` key.
+ const KEY: [u8; 13] = [
+ cbor::TEXT_12,
+ b'm',
+ b'i',
+ b'n',
+ b'P',
+ b'i',
+ b'n',
+ b'L',
+ b'e',
+ b'n',
+ b'g',
+ b't',
+ b'h',
+ ];
+ cbor.split_at_checked(KEY.len()).map_or(
+ Ok(CborSuccess {
+ value: Self::None,
+ remaining: cbor,
+ }),
+ |(key, key_rem)| {
+ if key == KEY {
+ key_rem
+ .split_first()
+ .ok_or(AuthenticatorExtensionOutputErr::Len)
+ .and_then(|(&key_len, remaining)| match key_len.cmp(&24) {
+ Ordering::Less => Ok(CborSuccess {
+ value: Self::Val(key_len),
+ remaining,
+ }),
+ Ordering::Equal => remaining
+ .split_first()
+ .ok_or(AuthenticatorExtensionOutputErr::Len)
+ .and_then(|(&key_24, rem)| {
+ if key_24 > 23 {
+ Ok(CborSuccess {
+ value: Self::Val(key_24),
+ remaining: rem,
+ })
+ } else {
+ Err(AuthenticatorExtensionOutputErr::MinPinLengthValue)
+ }
+ }),
+ Ordering::Greater => {
+ Err(AuthenticatorExtensionOutputErr::MinPinLengthValue)
+ }
+ })
+ } else {
+ Ok(CborSuccess {
+ value: Self::None,
+ remaining: cbor,
+ })
+ }
+ },
+ )
+ }
+}
+impl FromCbor<'_> for AuthenticatorExtensionOutput {
+ type Err = AuthenticatorExtensionOutputErr;
+ #[expect(
+ clippy::too_many_lines,
+ reason = "don't want to move logic into outer scope"
+ )]
+ fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
+ // We don't allow unsupported extensions; thus the only possibilities is any ordered element of
+ // the power set of {"credProtect":<1, 2, or 3>, "hmac-secret":<true or false>, "minPinLength":<0-255>}.
+ // Since the keys are the same type (text), order is first done based on length; and then
+ // byte-wise lexical order is followed; thus `credProtect` must come before `hmac-secret` which
+ // must come before `minPinLength`.
+ cbor.split_first().map_or_else(
+ || {
+ Ok(CborSuccess {
+ value: Self {
+ cred_protect: CredentialProtectionPolicy::None,
+ hmac_secret: None,
+ min_pin_length: None,
+ },
+ remaining: cbor,
+ })
+ },
+ |(map, map_rem)| match *map {
+ cbor::MAP_1 => {
+ CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| {
+ if matches!(cred_success.value, CredentialProtectionPolicy::None) {
+ HmacSecret::from_cbor(cred_success.remaining).and_then(|hmac_success| {
+ match hmac_success.value {
+ HmacSecret::None => MinPinLength::from_cbor(
+ hmac_success.remaining,
+ )
+ .and_then(|pin_success| match pin_success.value {
+ MinPinLength::None => {
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ MinPinLength::Val(min_pin_len) => Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: None,
+ min_pin_length: Some(min_pin_len),
+ },
+ remaining: pin_success.remaining,
+ }),
+ }),
+ HmacSecret::Val(hmac) => Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: Some(hmac),
+ min_pin_length: None,
+ },
+ remaining: hmac_success.remaining,
+ }),
+ }
+ })
+ } else {
+ Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: None,
+ min_pin_length: None,
+ },
+ remaining: cred_success.remaining,
+ })
+ }
+ })
+ }
+ cbor::MAP_2 => {
+ CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| {
+ if matches!(cred_success.value, CredentialProtectionPolicy::None) {
+ HmacSecret::from_cbor(cred_success.remaining).and_then(|hmac_success| {
+ match hmac_success.value {
+ HmacSecret::None => {
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ HmacSecret::Val(hmac) => MinPinLength::from_cbor(
+ hmac_success.remaining,
+ )
+ .and_then(|pin_success| match pin_success.value {
+ MinPinLength::None => {
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ MinPinLength::Val(min_pin_len) => Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: Some(hmac),
+ min_pin_length: Some(min_pin_len),
+ },
+ remaining: pin_success.remaining,
+ }),
+ }),
+ }
+ })
+ } else {
+ HmacSecret::from_cbor(cred_success.remaining).and_then(|hmac_success| {
+ match hmac_success.value {
+ HmacSecret::None => MinPinLength::from_cbor(
+ hmac_success.remaining,
+ )
+ .and_then(|pin_success| match pin_success.value {
+ MinPinLength::None => {
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ MinPinLength::Val(min_pin_len) => Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: None,
+ min_pin_length: Some(min_pin_len),
+ },
+ remaining: pin_success.remaining,
+ }),
+ }),
+ HmacSecret::Val(hmac) => Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: Some(hmac),
+ min_pin_length: None,
+ },
+ remaining: hmac_success.remaining,
+ }),
+ }
+ })
+ }
+ })
+ }
+ cbor::MAP_3 => {
+ CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| {
+ if matches!(cred_success.value, CredentialProtectionPolicy::None) {
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ } else {
+ HmacSecret::from_cbor(cred_success.remaining).and_then(|hmac_success| {
+ match hmac_success.value {
+ HmacSecret::None => {
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ HmacSecret::Val(hmac) => MinPinLength::from_cbor(
+ hmac_success.remaining,
+ )
+ .and_then(|pin_success| match pin_success.value {
+ MinPinLength::None => {
+ Err(AuthenticatorExtensionOutputErr::Missing)
+ }
+ MinPinLength::Val(min_pin_len) => Ok(CborSuccess {
+ value: Self {
+ cred_protect: cred_success.value,
+ hmac_secret: Some(hmac),
+ min_pin_length: Some(min_pin_len),
+ },
+ remaining: pin_success.remaining,
+ }),
+ }),
+ }
+ })
+ }
+ })
+ }
+ _ => Err(AuthenticatorExtensionOutputErr::CborHeader),
+ },
+ )
+ }
+}
+/// 32-bytes representing an alleged Ed25519 public key (i.e., compressed y-coordinate).
+#[derive(Clone, Copy, Debug)]
+pub struct Ed25519PubKey<T>(T);
+impl<T> Ed25519PubKey<T> {
+ /// Returns the contained data consuming `self`.
+ #[inline]
+ pub fn into_inner(self) -> T {
+ self.0
+ }
+ /// Returns the contained data.
+ #[inline]
+ pub const fn inner(&self) -> &T {
+ &self.0
+ }
+}
+impl<T: AsRef<[u8]>> Ed25519PubKey<T> {
+ /// Returns the compressed y-coordinate.
+ #[inline]
+ #[must_use]
+ pub fn compressed_y_coordinate(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+impl Ed25519PubKey<&[u8]> {
+ /// Validates `self` is in fact a valid Ed25519 public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid Ed25519 public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ self.into_ver_key().map(|_| ())
+ }
+ /// Converts `self` into [`VerifyingKey`].
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ pub(super) fn into_ver_key(self) -> Result<VerifyingKey, PubKeyErr> {
+ VerifyingKey::from_bytes(
+ self.0
+ .try_into()
+ .unwrap_or_else(|_e| unreachable!("&Array::try_from has a bug")),
+ )
+ .map_err(|_e| PubKeyErr::Ed25519)
+ }
+}
+impl Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> {
+ /// Validates `self` is in fact a valid Ed25519 public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid Ed25519 public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ self.into_ver_key().map(|_| ())
+ }
+ /// Converts `self` into [`VerifyingKey`].
+ fn into_ver_key(self) -> Result<VerifyingKey, PubKeyErr> {
+ VerifyingKey::from_bytes(&self.0).map_err(|_e| PubKeyErr::Ed25519)
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for Ed25519PubKey<&'b [u8]> {
+ type Error = Ed25519PubKeyErr;
+ /// Interprets `value` as the compressed y-coordinate of an Ed25519 public key.
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ if value.len() == ed25519_dalek::PUBLIC_KEY_LENGTH {
+ Ok(Self(value))
+ } else {
+ Err(Ed25519PubKeyErr)
+ }
+ }
+}
+impl From<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>
+ for Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>
+{
+ #[inline]
+ fn from(value: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]) -> Self {
+ Self(value)
+ }
+}
+impl<'a: 'b, 'b> From<&'a Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>>
+ for Ed25519PubKey<&'b [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>
+{
+ #[inline]
+ fn from(value: &'a Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>) -> Self {
+ Self(&value.0)
+ }
+}
+impl<'a: 'b, 'b> From<Ed25519PubKey<&'a [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>>
+ for Ed25519PubKey<&'b [u8]>
+{
+ #[inline]
+ fn from(value: Ed25519PubKey<&'a [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>) -> Self {
+ Self(value.0.as_slice())
+ }
+}
+impl<'a: 'b, 'b> From<&'a Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>>
+ for Ed25519PubKey<&'b [u8]>
+{
+ #[inline]
+ fn from(value: &'a Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>) -> Self {
+ Self(value.0.as_slice())
+ }
+}
+impl<'a: 'b, 'b> From<Ed25519PubKey<&'a [u8]>>
+ for Ed25519PubKey<&'b [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>
+{
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[inline]
+ fn from(value: Ed25519PubKey<&'a [u8]>) -> Self {
+ Self(
+ value
+ .0
+ .try_into()
+ .unwrap_or_else(|_e| unreachable!("there is a bug in &Array::try_from")),
+ )
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<Ed25519PubKey<T>> for Ed25519PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &Ed25519PubKey<T>) -> bool {
+ self.0 == other.0
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<Ed25519PubKey<T>> for &Ed25519PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &Ed25519PubKey<T>) -> bool {
+ **self == *other
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&Ed25519PubKey<T>> for Ed25519PubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &&Ed25519PubKey<T>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq> Eq for Ed25519PubKey<T> {}
+/// Two 32-byte regions representing the big-endian x and y coordinates of an alleged P-256 public key.
+#[derive(Clone, Copy, Debug)]
+pub struct UncompressedP256PubKey<'a>(&'a [u8], &'a [u8]);
+impl<'a> UncompressedP256PubKey<'a> {
+ /// Returns the big-endian x-coordinate.
+ #[inline]
+ #[must_use]
+ pub const fn x(self) -> &'a [u8] {
+ self.0
+ }
+ /// Returns the big-endian y-coordinate.
+ #[inline]
+ #[must_use]
+ pub const fn y(self) -> &'a [u8] {
+ self.1
+ }
+ /// Validates `self` is in fact a valid P-256 public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid P-256 public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ self.into_ver_key().map(|_| ())
+ }
+ /// Converts `self` into [`P256VerKey`].
+ fn into_ver_key(self) -> Result<P256VerKey, PubKeyErr> {
+ P256VerKey::from_encoded_point(&P256Pt::from_affine_coordinates(
+ self.0.into(),
+ self.1.into(),
+ false,
+ ))
+ .map_err(|_e| PubKeyErr::P256)
+ }
+}
+impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP256PubKey<'b> {
+ type Error = UncompressedP256PubKeyErr;
+ /// The first item is the big-endian x-coordinate, and the second item is the big-endian y-coordinate.
+ #[inline]
+ fn try_from((x, y): (&'a [u8], &'a [u8])) -> Result<Self, Self::Error> {
+ /// Number of bytes each coordinate is made of.
+ const COORD_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT;
+ if x.len() == COORD_LEN {
+ if y.len() == COORD_LEN {
+ Ok(Self(x, y))
+ } else {
+ Err(UncompressedP256PubKeyErr::Y)
+ }
+ } else {
+ Err(UncompressedP256PubKeyErr::X)
+ }
+ }
+}
+impl PartialEq<UncompressedP256PubKey<'_>> for UncompressedP256PubKey<'_> {
+ #[inline]
+ fn eq(&self, other: &UncompressedP256PubKey<'_>) -> bool {
+ self.0 == other.0 && self.1 == other.1
+ }
+}
+impl PartialEq<UncompressedP256PubKey<'_>> for &UncompressedP256PubKey<'_> {
+ #[inline]
+ fn eq(&self, other: &UncompressedP256PubKey<'_>) -> bool {
+ **self == *other
+ }
+}
+impl PartialEq<&UncompressedP256PubKey<'_>> for UncompressedP256PubKey<'_> {
+ #[inline]
+ fn eq(&self, other: &&UncompressedP256PubKey<'_>) -> bool {
+ *self == **other
+ }
+}
+impl Eq for UncompressedP256PubKey<'_> {}
+/// 32-bytes representing the big-endian x-coordinate and a `bool` representing whether the y-coordinate
+/// is odd of an alleged P-256 public key.
+#[derive(Clone, Copy, Debug)]
+pub struct CompressedP256PubKey<T> {
+ /// 32-byte x-coordinate.
+ x: T,
+ /// `true` iff the y-coordinate is odd.
+ y_is_odd: bool,
+}
+impl<T> CompressedP256PubKey<T> {
+ /// Returns [`Self::x`] and [`Self::y_is_odd`] consuming `self`.
+ #[inline]
+ pub fn into_parts(self) -> (T, bool) {
+ (self.x, self.y_is_odd)
+ }
+ /// Returns [`Self::x`] and [`Self::y_is_odd`].
+ #[inline]
+ pub const fn as_parts(&self) -> (&T, bool) {
+ (&self.x, self.y_is_odd)
+ }
+ /// Returns the 32-byte big-endian x-coordinate.
+ #[inline]
+ pub const fn x(&self) -> &T {
+ &self.x
+ }
+ /// `true` iff the y-coordinate is odd.
+ #[inline]
+ pub const fn y_is_odd(&self) -> bool {
+ self.y_is_odd
+ }
+}
+impl CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]> {
+ /// Validates `self` is in fact a valid P-256 public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid P-256 public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ self.into_ver_key().map(|_| ())
+ }
+ /// Converts `self` into [`P256VerKey`].
+ pub(super) fn into_ver_key(self) -> Result<P256VerKey, PubKeyErr> {
+ P256Affine::decompress(&self.x.into(), u8::from(self.y_is_odd).into())
+ .into_option()
+ .ok_or(PubKeyErr::P256)
+ .and_then(|pt| P256VerKey::from_affine(pt).map_err(|_e| PubKeyErr::P256))
+ }
+}
+impl CompressedP256PubKey<&[u8]> {
+ /// Validates `self` is in fact a valid P-256 public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid P-256 public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ self.into_ver_key().map(|_| ())
+ }
+ /// Converts `self` into [`P256VerKey`].
+ pub(super) fn into_ver_key(self) -> Result<P256VerKey, PubKeyErr> {
+ P256Affine::decompress(self.x.into(), u8::from(self.y_is_odd).into())
+ .into_option()
+ .ok_or(PubKeyErr::P256)
+ .and_then(|pt| P256VerKey::from_affine(pt).map_err(|_e| PubKeyErr::P256))
+ }
+}
+impl<'a: 'b, 'b> TryFrom<(&'a [u8], bool)> for CompressedP256PubKey<&'b [u8]> {
+ type Error = CompressedP256PubKeyErr;
+ #[inline]
+ fn try_from((x, y_is_odd): (&'a [u8], bool)) -> Result<Self, Self::Error> {
+ /// The number of bytes the x-coordinate is.
+ const X_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT;
+ if x.len() == X_LEN {
+ Ok(Self { x, y_is_odd })
+ } else {
+ Err(CompressedP256PubKeyErr)
+ }
+ }
+}
+impl From<([u8; <NistP256 as Curve>::FieldBytesSize::INT], bool)>
+ for CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]>
+{
+ #[inline]
+ fn from((x, y_is_odd): ([u8; <NistP256 as Curve>::FieldBytesSize::INT], bool)) -> Self {
+ Self { x, y_is_odd }
+ }
+}
+impl<'a: 'b, 'b> From<&'a CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]>>
+ for CompressedP256PubKey<&'b [u8; <NistP256 as Curve>::FieldBytesSize::INT]>
+{
+ #[inline]
+ fn from(
+ value: &'a CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]>,
+ ) -> Self {
+ Self {
+ x: &value.x,
+ y_is_odd: value.y_is_odd,
+ }
+ }
+}
+impl<'a: 'b, 'b> From<CompressedP256PubKey<&'a [u8; <NistP256 as Curve>::FieldBytesSize::INT]>>
+ for CompressedP256PubKey<&'b [u8]>
+{
+ #[inline]
+ fn from(
+ value: CompressedP256PubKey<&'a [u8; <NistP256 as Curve>::FieldBytesSize::INT]>,
+ ) -> Self {
+ Self {
+ x: value.x.as_slice(),
+ y_is_odd: value.y_is_odd,
+ }
+ }
+}
+impl<'a: 'b, 'b> From<&'a CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]>>
+ for CompressedP256PubKey<&'b [u8]>
+{
+ #[inline]
+ fn from(
+ value: &'a CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]>,
+ ) -> Self {
+ Self {
+ x: value.x.as_slice(),
+ y_is_odd: value.y_is_odd,
+ }
+ }
+}
+impl<'a: 'b, 'b> From<CompressedP256PubKey<&'a [u8]>>
+ for CompressedP256PubKey<&'b [u8; <NistP256 as Curve>::FieldBytesSize::INT]>
+{
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[inline]
+ fn from(value: CompressedP256PubKey<&'a [u8]>) -> Self {
+ Self {
+ x: value
+ .x
+ .try_into()
+ .unwrap_or_else(|_e| unreachable!("&Array::try_from has a bug")),
+ y_is_odd: value.y_is_odd,
+ }
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CompressedP256PubKey<T>>
+ for CompressedP256PubKey<T2>
+{
+ #[inline]
+ fn eq(&self, other: &CompressedP256PubKey<T>) -> bool {
+ self.x == other.x && self.y_is_odd == other.y_is_odd
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CompressedP256PubKey<T>>
+ for &CompressedP256PubKey<T2>
+{
+ #[inline]
+ fn eq(&self, other: &CompressedP256PubKey<T>) -> bool {
+ **self == *other
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&CompressedP256PubKey<T>>
+ for CompressedP256PubKey<T2>
+{
+ #[inline]
+ fn eq(&self, other: &&CompressedP256PubKey<T>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq> Eq for CompressedP256PubKey<T> {}
+/// Two 48-byte regions representing the big-endian x and y coordinates of an alleged P-384 public key.
+#[derive(Clone, Copy, Debug)]
+pub struct UncompressedP384PubKey<'a>(&'a [u8], &'a [u8]);
+impl<'a> UncompressedP384PubKey<'a> {
+ /// Returns the big-endian x-coordinate.
+ #[inline]
+ #[must_use]
+ pub const fn x(self) -> &'a [u8] {
+ self.0
+ }
+ /// Returns the big-endian y-coordinate.
+ #[inline]
+ #[must_use]
+ pub const fn y(self) -> &'a [u8] {
+ self.1
+ }
+ /// Validates `self` is in fact a valid P-384 public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid P-384 public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ self.into_ver_key().map(|_| ())
+ }
+ /// Converts `self` into [`P384VerKey`].
+ fn into_ver_key(self) -> Result<P384VerKey, PubKeyErr> {
+ P384VerKey::from_encoded_point(&P384Pt::from_affine_coordinates(
+ self.0.into(),
+ self.1.into(),
+ false,
+ ))
+ .map_err(|_e| PubKeyErr::P384)
+ }
+}
+impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP384PubKey<'b> {
+ type Error = UncompressedP384PubKeyErr;
+ /// The first item is the big-endian x-coordinate, and the second item is the big-endian y-coordinate.
+ #[inline]
+ fn try_from((x, y): (&'a [u8], &'a [u8])) -> Result<Self, Self::Error> {
+ /// Number of bytes each coordinate is made of.
+ const COORD_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT;
+ if x.len() == COORD_LEN {
+ if y.len() == COORD_LEN {
+ Ok(Self(x, y))
+ } else {
+ Err(UncompressedP384PubKeyErr::Y)
+ }
+ } else {
+ Err(UncompressedP384PubKeyErr::X)
+ }
+ }
+}
+impl PartialEq<UncompressedP384PubKey<'_>> for UncompressedP384PubKey<'_> {
+ #[inline]
+ fn eq(&self, other: &UncompressedP384PubKey<'_>) -> bool {
+ self.0 == other.0 && self.1 == other.1
+ }
+}
+impl PartialEq<UncompressedP384PubKey<'_>> for &UncompressedP384PubKey<'_> {
+ #[inline]
+ fn eq(&self, other: &UncompressedP384PubKey<'_>) -> bool {
+ **self == *other
+ }
+}
+impl PartialEq<&UncompressedP384PubKey<'_>> for UncompressedP384PubKey<'_> {
+ #[inline]
+ fn eq(&self, other: &&UncompressedP384PubKey<'_>) -> bool {
+ *self == **other
+ }
+}
+impl Eq for UncompressedP384PubKey<'_> {}
+/// 48-bytes representing the big-endian x-coordinate and a `bool` representing whether the y-coordinate
+/// is odd of an alleged P-384 public key.
+#[derive(Clone, Copy, Debug)]
+pub struct CompressedP384PubKey<T> {
+ /// 48-byte x-coordinate.
+ x: T,
+ /// `true` iff the y-coordinate is odd.
+ y_is_odd: bool,
+}
+impl<T> CompressedP384PubKey<T> {
+ /// Returns [`Self::x`] and [`Self::y_is_odd`] consuming `self`.
+ #[inline]
+ pub fn into_parts(self) -> (T, bool) {
+ (self.x, self.y_is_odd)
+ }
+ /// Returns [`Self::x`] and [`Self::y_is_odd`].
+ #[inline]
+ pub const fn as_parts(&self) -> (&T, bool) {
+ (&self.x, self.y_is_odd)
+ }
+ /// Returns the 48-byte big-endian x-coordinate.
+ #[inline]
+ pub const fn x(&self) -> &T {
+ &self.x
+ }
+ /// `true` iff the y-coordinate is odd.
+ #[inline]
+ pub const fn y_is_odd(&self) -> bool {
+ self.y_is_odd
+ }
+}
+impl CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]> {
+ /// Validates `self` is in fact a valid P-384 public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid P-384 public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ self.into_ver_key().map(|_| ())
+ }
+ /// Converts `self` into [`P384VerKey`].
+ pub(super) fn into_ver_key(self) -> Result<P384VerKey, PubKeyErr> {
+ P384Affine::decompress(&self.x.into(), u8::from(self.y_is_odd).into())
+ .into_option()
+ .ok_or(PubKeyErr::P384)
+ .and_then(|pt| P384VerKey::from_affine(pt).map_err(|_e| PubKeyErr::P384))
+ }
+}
+impl CompressedP384PubKey<&[u8]> {
+ /// Validates `self` is in fact a valid P-384 public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid P-384 public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ self.into_ver_key().map(|_| ())
+ }
+ /// Converts `self` into [`P384VerKey`].
+ pub(super) fn into_ver_key(self) -> Result<P384VerKey, PubKeyErr> {
+ P384Affine::decompress(self.x.into(), u8::from(self.y_is_odd).into())
+ .into_option()
+ .ok_or(PubKeyErr::P384)
+ .and_then(|pt| P384VerKey::from_affine(pt).map_err(|_e| PubKeyErr::P384))
+ }
+}
+impl<'a: 'b, 'b> TryFrom<(&'a [u8], bool)> for CompressedP384PubKey<&'b [u8]> {
+ type Error = CompressedP384PubKeyErr;
+ #[inline]
+ fn try_from((x, y_is_odd): (&'a [u8], bool)) -> Result<Self, Self::Error> {
+ /// Number of bytes of the x-coordinate is.
+ const X_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT;
+ if x.len() == X_LEN {
+ Ok(Self { x, y_is_odd })
+ } else {
+ Err(CompressedP384PubKeyErr)
+ }
+ }
+}
+impl From<([u8; <NistP384 as Curve>::FieldBytesSize::INT], bool)>
+ for CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]>
+{
+ #[inline]
+ fn from((x, y_is_odd): ([u8; <NistP384 as Curve>::FieldBytesSize::INT], bool)) -> Self {
+ Self { x, y_is_odd }
+ }
+}
+impl<'a: 'b, 'b> From<&'a CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]>>
+ for CompressedP384PubKey<&'b [u8; <NistP384 as Curve>::FieldBytesSize::INT]>
+{
+ #[inline]
+ fn from(
+ value: &'a CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]>,
+ ) -> Self {
+ Self {
+ x: &value.x,
+ y_is_odd: value.y_is_odd,
+ }
+ }
+}
+impl<'a: 'b, 'b> From<CompressedP384PubKey<&'a [u8; <NistP384 as Curve>::FieldBytesSize::INT]>>
+ for CompressedP384PubKey<&'b [u8]>
+{
+ #[inline]
+ fn from(
+ value: CompressedP384PubKey<&'a [u8; <NistP384 as Curve>::FieldBytesSize::INT]>,
+ ) -> Self {
+ Self {
+ x: value.x.as_slice(),
+ y_is_odd: value.y_is_odd,
+ }
+ }
+}
+impl<'a: 'b, 'b> From<&'a CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]>>
+ for CompressedP384PubKey<&'b [u8]>
+{
+ #[inline]
+ fn from(
+ value: &'a CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]>,
+ ) -> Self {
+ Self {
+ x: value.x.as_slice(),
+ y_is_odd: value.y_is_odd,
+ }
+ }
+}
+impl<'a: 'b, 'b> From<CompressedP384PubKey<&'a [u8]>>
+ for CompressedP384PubKey<&'b [u8; <NistP384 as Curve>::FieldBytesSize::INT]>
+{
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[inline]
+ fn from(value: CompressedP384PubKey<&'a [u8]>) -> Self {
+ Self {
+ x: value
+ .x
+ .try_into()
+ .unwrap_or_else(|_e| unreachable!("&Array::try_from has a bug")),
+ y_is_odd: value.y_is_odd,
+ }
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CompressedP384PubKey<T>>
+ for CompressedP384PubKey<T2>
+{
+ #[inline]
+ fn eq(&self, other: &CompressedP384PubKey<T>) -> bool {
+ self.x == other.x && self.y_is_odd == other.y_is_odd
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CompressedP384PubKey<T>>
+ for &CompressedP384PubKey<T2>
+{
+ #[inline]
+ fn eq(&self, other: &CompressedP384PubKey<T>) -> bool {
+ **self == *other
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&CompressedP384PubKey<T>>
+ for CompressedP384PubKey<T2>
+{
+ #[inline]
+ fn eq(&self, other: &&CompressedP384PubKey<T>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq> Eq for CompressedP384PubKey<T> {}
+/// The minimum RSA public key exponent allowed by [`RsaPubKey`].
+///
+/// [RFC 8017 § 3.1](https://www.rfc-editor.org/rfc/rfc8017#section-3.1) states the smallest valid RSA public
+/// exponent is 3.
+pub const MIN_RSA_E: u32 = 3;
+/// The most bits an RSA public key modulus is allowed to consist of per [`RsaPubKey`].
+///
+/// [RFC 8230 § 6.1](https://www.rfc-editor.org/rfc/rfc8230#section-6.1) recommends allowing moduli up to 16K bits.
+pub const MAX_RSA_N_BITS: usize = 0x4000;
+/// [`MAX_RSA_N_BITS`] as bytes.
+pub const MAX_RSA_N_BYTES: usize = MAX_RSA_N_BITS >> 3;
+/// The fewest bits an RSA public key modulus is allowed to consist of per [`RsaPubKey`].
+///
+/// [RFC 8230 § 6.1](https://www.rfc-editor.org/rfc/rfc8230#section-6.1) requires the modulus to be at least 2048
+/// bits.
+pub const MIN_RSA_N_BITS: usize = 0x800;
+/// [`MIN_RSA_N_BITS`] as bytes.
+pub const MIN_RSA_N_BYTES: usize = MIN_RSA_N_BITS >> 3;
+/// [`MIN_RSA_N_BYTES`]–[`MAX_RSA_N_BYTES`] bytes representing the big-endian modulus and a `u32` `>=`
+/// [`MIN_RSA_E`] representing the exponent of an alleged RSA public key.
+#[derive(Clone, Copy, Debug)]
+pub struct RsaPubKey<T>(T, u32);
+impl<T> RsaPubKey<T> {
+ /// Returns [`Self::n`] and [`Self::e`] consuming `self`.
+ #[inline]
+ pub fn into_parts(self) -> (T, u32) {
+ (self.0, self.1)
+ }
+ /// Returns [`Self::n`] and [`Self::e`].
+ #[inline]
+ pub const fn as_parts(&self) -> (&T, u32) {
+ (&self.0, self.1)
+ }
+ /// Returns the big-endian modulus.
+ #[inline]
+ pub const fn n(&self) -> &T {
+ &self.0
+ }
+ /// Returns the exponent.
+ #[inline]
+ pub const fn e(&self) -> u32 {
+ self.1
+ }
+}
+impl<T: AsRef<[u8]>> RsaPubKey<T> {
+ /// Validates `self` is in fact a valid RSA public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid RSA public key.
+ #[inline]
+ pub fn validate(&self) -> Result<(), PubKeyErr> {
+ self.as_ver_key().map(|_| ())
+ }
+ /// Converts `self` into [`RsaVerKey`].
+ pub(super) fn as_ver_key(&self) -> Result<RsaVerKey<Sha256>, PubKeyErr> {
+ RsaPublicKey::new_with_max_size(
+ BigUint::from_bytes_be(self.0.as_ref()),
+ self.1.into(),
+ MAX_RSA_N_BITS,
+ )
+ .map_err(|_e| PubKeyErr::Rsa)
+ .map(RsaVerKey::new)
+ }
+}
+impl<'a: 'b, 'b> TryFrom<(&'a [u8], u32)> for RsaPubKey<&'b [u8]> {
+ type Error = RsaPubKeyErr;
+ /// The first item is the big-endian modulus, and the second item is the exponent.
+ ///
+ /// Note `n` is _not_ checked for leading 0s before its length is checked to be within [`MIN_RSA_N_BYTES`]
+ /// and [`MAX_RSA_N_BYTES`] inclusively. If this is not desired, then calling code must remove leading 0s
+ /// before calling. Several encodings require leading 0s to not exist so one would likely already be
+ /// verifying there are no leading 0s.
+ #[inline]
+ fn try_from((n, e): (&'a [u8], u32)) -> Result<Self, Self::Error> {
+ if (MIN_RSA_N_BYTES..=MAX_RSA_N_BYTES).contains(&n.len()) {
+ if e >= MIN_RSA_E {
+ Ok(Self(n, e))
+ } else {
+ Err(RsaPubKeyErr::E)
+ }
+ } else {
+ Err(RsaPubKeyErr::N)
+ }
+ }
+}
+impl TryFrom<(Vec<u8>, u32)> for RsaPubKey<Vec<u8>> {
+ type Error = RsaPubKeyErr;
+ /// Similar to [`RsaPubKey::try_from`] except `n` is a `Vec`.
+ #[inline]
+ fn try_from((n, e): (Vec<u8>, u32)) -> Result<Self, Self::Error> {
+ match RsaPubKey::<&[u8]>::try_from((n.as_slice(), e)) {
+ Ok(_) => Ok(Self(n, e)),
+ Err(err) => Err(err),
+ }
+ }
+}
+impl<'a: 'b, 'b> From<&'a RsaPubKey<Vec<u8>>> for RsaPubKey<&'b Vec<u8>> {
+ #[inline]
+ fn from(value: &'a RsaPubKey<Vec<u8>>) -> Self {
+ Self(&value.0, value.1)
+ }
+}
+impl<'a: 'b, 'b> From<RsaPubKey<&'a Vec<u8>>> for RsaPubKey<&'b [u8]> {
+ #[inline]
+ fn from(value: RsaPubKey<&'a Vec<u8>>) -> Self {
+ Self(value.0.as_slice(), value.1)
+ }
+}
+impl<'a: 'b, 'b> From<&'a RsaPubKey<Vec<u8>>> for RsaPubKey<&'b [u8]> {
+ #[inline]
+ fn from(value: &'a RsaPubKey<Vec<u8>>) -> Self {
+ Self(value.0.as_slice(), value.1)
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<RsaPubKey<T>> for RsaPubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &RsaPubKey<T>) -> bool {
+ self.0 == other.0 && self.1 == other.1
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<RsaPubKey<T>> for &RsaPubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &RsaPubKey<T>) -> bool {
+ **self == *other
+ }
+}
+impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&RsaPubKey<T>> for RsaPubKey<T2> {
+ #[inline]
+ fn eq(&self, other: &&RsaPubKey<T>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq> Eq for RsaPubKey<T> {}
+/// `kty` COSE key common parameter as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters).
+const KTY: u8 = cbor::ONE;
+/// `OKP` COSE key type as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type).
+const OKP: u8 = cbor::ONE;
+/// `EC2` COSE key type as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type).
+const EC2: u8 = cbor::TWO;
+/// `RSA` COSE key type as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type).
+const RSA: u8 = cbor::THREE;
+/// `alg` COSE key common parameter as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters).
+const ALG: u8 = cbor::THREE;
+/// `EdDSA` COSE algorithm as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+const EDDSA: u8 = cbor::NEG_EIGHT;
+/// `ES256` COSE algorithm as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+const ES256: u8 = cbor::NEG_SEVEN;
+/// `ES384` COSE algorithm as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+///
+/// This is -35 encoded in cbor which is encoded as |-35| - 1 = 35 - 1 = 34. Note
+/// this must be preceded with `cbor::NEG_INFO_24`.
+const ES384: u8 = 34;
+/// `RS256` COSE algorithm as defined by
+/// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms).
+///
+/// This is -257 encoded in cbor which is encoded as |-257| - 1 = 257 - 1 = 256 = [1, 0] in big endian.
+/// Note this must be preceded with `cbor::NEG_INFO_25`.
+const RS256: [u8; 2] = [1, 0];
+impl<'a> FromCbor<'a> for Ed25519PubKey<&'a [u8]> {
+ type Err = CoseKeyErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// `crv` COSE key type parameter for [`OKP`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const CRV: u8 = cbor::NEG_ONE;
+ /// `Ed25519` COSE elliptic curve as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves).
+ const ED25519: u8 = cbor::SIX;
+ /// `x` COSE key type parameter for [`OKP`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const X: u8 = cbor::NEG_TWO;
+ // `32 as u8` is OK.
+ /// `ed25519_dalek::PUBLIC_KEY_LENGTH` as a `u8`.
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "explained above and want a const"
+ )]
+ const KEY_LEN_U8: u8 = ed25519_dalek::PUBLIC_KEY_LENGTH as u8;
+ /// COSE header.
+ /// {kty:OKP,alg:EdDSA,crv:Ed25519,x:<CompressedEdwardsYPoint>}.
+ /// `kty` and `alg` come before `crv` and `x` since map order first
+ /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s.
+ /// `kty` comes before `alg` since order is done byte-wise and
+ /// 1 is before 3. `crv` is before `x` since `0b001_00000` comes before
+ /// `0b001_00001` byte-wise.
+ const HEADER: [u8; 10] = [
+ cbor::MAP_4,
+ KTY,
+ OKP,
+ ALG,
+ EDDSA,
+ CRV,
+ ED25519,
+ X,
+ cbor::BYTES_INFO_24,
+ KEY_LEN_U8,
+ ];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(ed25519_dalek::PUBLIC_KEY_LENGTH)
+ .ok_or(CoseKeyErr::Len)
+ .map(|(key, remaining)| CborSuccess {
+ value: Self(key),
+ remaining,
+ })
+ } else {
+ Err(CoseKeyErr::Ed25519CoseEncoding)
+ }
+ })
+ }
+}
+impl<'a> FromCbor<'a> for UncompressedP256PubKey<'a> {
+ type Err = CoseKeyErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// `crv` COSE key type parameter for [`EC2`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const CRV: u8 = cbor::NEG_ONE;
+ /// `P-256` COSE elliptic curve as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves).
+ const P256: u8 = cbor::ONE;
+ /// `x` COSE key type parameter for [`EC2`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const X: u8 = cbor::NEG_TWO;
+ /// `y` COSE key type parameter for [`EC2`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const Y: u8 = cbor::NEG_THREE;
+ /// Number of bytes the x-coordinate takes.
+ const X_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT;
+ // `32 as u8` is OK.
+ /// `X_LEN` as a `u8`.
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "explained above and want a const"
+ )]
+ const X_LEN_U8: u8 = X_LEN as u8;
+ /// Number of bytes the y-coordinate takes.
+ const Y_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT;
+ // `32 as u8` is OK.
+ /// `Y_LEN` as a `u8`.
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "explained above and want a const"
+ )]
+ const Y_LEN_U8: u8 = Y_LEN as u8;
+ /// COSE header.
+ // {kty:EC2,alg:ES256,crv:P-256,x:<affine x-coordinate>,...}.
+ /// `kty` and `alg` come before `crv`, `x`, and `y` since map order first
+ /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s.
+ /// `kty` comes before `alg` since order is done byte-wise and
+ /// 1 is before 3. `crv` is before `x` which is before `y` since
+ /// `0b001_00000` comes before `0b001_00001` which comes before
+ /// `0b001_00010` byte-wise.
+ const HEADER: [u8; 10] = [
+ cbor::MAP_5,
+ KTY,
+ EC2,
+ ALG,
+ ES256,
+ CRV,
+ P256,
+ X,
+ cbor::BYTES_INFO_24,
+ X_LEN_U8,
+ ];
+ /// {...y:<affine y-coordinate>}.
+ const Y_META: [u8; 3] = [Y, cbor::BYTES_INFO_24, Y_LEN_U8];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(X_LEN)
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(x, x_rem)| {
+ x_rem
+ .split_at_checked(Y_META.len())
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(y_meta, y_meta_rem)| {
+ if y_meta == Y_META {
+ y_meta_rem
+ .split_at_checked(Y_LEN)
+ .ok_or(CoseKeyErr::Len)
+ .map(|(y, remaining)| CborSuccess {
+ value: Self(x, y),
+ remaining,
+ })
+ } else {
+ Err(CoseKeyErr::P256CoseEncoding)
+ }
+ })
+ })
+ } else {
+ Err(CoseKeyErr::P256CoseEncoding)
+ }
+ })
+ }
+}
+impl<'a> FromCbor<'a> for UncompressedP384PubKey<'a> {
+ type Err = CoseKeyErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// `crv` COSE key type parameter for [`EC2`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const CRV: u8 = cbor::NEG_ONE;
+ /// `P-384` COSE elliptic curve as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves).
+ const P384: u8 = cbor::TWO;
+ /// `x` COSE key type parameter for [`EC2`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const X: u8 = cbor::NEG_TWO;
+ /// `y` COSE key type parameter for [`EC2`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const Y: u8 = cbor::NEG_THREE;
+ /// Number of bytes the x-coordinate takes.
+ const X_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT;
+ // `48 as u8` is OK.
+ /// `X_LEN` as a `u8`.
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "explained above and want a const"
+ )]
+ const X_LEN_U8: u8 = X_LEN as u8;
+ /// Number of bytes the y-coordinate takes.
+ const Y_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT;
+ // `48 as u8` is OK.
+ /// `Y_LEN` as a `u8`.
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "explained above and want a const"
+ )]
+ const Y_LEN_U8: u8 = Y_LEN as u8;
+ /// COSE header.
+ // {kty:EC2,alg:ES384,crv:P-384,x:<affine x-coordinate>,...}.
+ /// `kty` and `alg` come before `crv`, `x`, and `y` since map order first
+ /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s.
+ /// `kty` comes before `alg` since order is done byte-wise and
+ /// 1 is before 3. `crv` is before `x` which is before `y` since
+ /// `0b001_00000` comes before `0b001_00001` which comes before
+ /// `0b001_00010` byte-wise.
+ const HEADER: [u8; 11] = [
+ cbor::MAP_5,
+ KTY,
+ EC2,
+ ALG,
+ cbor::NEG_INFO_24,
+ ES384,
+ CRV,
+ P384,
+ X,
+ cbor::BYTES_INFO_24,
+ X_LEN_U8,
+ ];
+ /// {...y:<affine y-coordinate>}.
+ const Y_META: [u8; 3] = [Y, cbor::BYTES_INFO_24, Y_LEN_U8];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(X_LEN)
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(x, x_rem)| {
+ x_rem
+ .split_at_checked(Y_META.len())
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|(y_meta, y_meta_rem)| {
+ if y_meta == Y_META {
+ y_meta_rem
+ .split_at_checked(Y_LEN)
+ .ok_or(CoseKeyErr::Len)
+ .map(|(y, remaining)| CborSuccess {
+ value: Self(x, y),
+ remaining,
+ })
+ } else {
+ Err(CoseKeyErr::P384CoseEncoding)
+ }
+ })
+ })
+ } else {
+ Err(CoseKeyErr::P384CoseEncoding)
+ }
+ })
+ }
+}
+impl<'a> FromCbor<'a> for RsaPubKey<&'a [u8]> {
+ type Err = CoseKeyErr;
+ #[expect(
+ clippy::arithmetic_side_effects,
+ clippy::big_endian_bytes,
+ clippy::indexing_slicing,
+ clippy::missing_asserts_for_indexing,
+ reason = "comments justify their correctness"
+ )]
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// `n` COSE key type parameter for [`RSA`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const N: u8 = cbor::NEG_ONE;
+ /// `e` COSE key type parameter for [`RSA`] as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters).
+ const E: u8 = cbor::NEG_TWO;
+ /// COSE header.
+ /// {kty:RSA,alg:RS256,n:<RSA modulus>,...}.
+ /// `kty` and `alg` come before `n` and `e` since map order first
+ /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s.
+ /// `kty` comes before `alg` since order is done byte-wise and
+ /// 1 is before 3. `n` is before `e` since `0b001_00000` comes before
+ /// `0b001_00001` byte-wise.
+ ///
+ /// Note `RS256` COSE algorithm as defined by
+ /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)
+ /// is encoded as -257 which is encoded in CBOR as `[cbor::NEG_INFO_25, 1, 0]` since
+ /// |-257| - 1 = 256 which takes two bytes to encode as 1, 0 in big-endian.
+ /// Ditto for a byte string of length 0x100 to 0xFFFF inclusively replacing `cbor::NEG_INFO_25` with
+ /// `cbor::BYTES_INFO_25`.
+ ///
+ /// Recall that [`RsaPubKey`] requires the modulus to be at least 256 bytes in length but no greater than
+ /// 2048 bytes in length; thus we know a valid and allowed `n` will have length whose metadata
+ /// takes exactly two bytes.
+ const HEADER: [u8; 9] = [
+ cbor::MAP_4,
+ KTY,
+ RSA,
+ ALG,
+ cbor::NEG_INFO_25,
+ 1,
+ 0,
+ N,
+ cbor::BYTES_INFO_25,
+ ];
+ cbor.split_at_checked(HEADER.len()).ok_or(CoseKeyErr::Len).and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem.split_at_checked(2).ok_or(CoseKeyErr::Len).and_then(|(n_len_slice, n_len_rem)| {
+ let mut len = [0; 2];
+ len.copy_from_slice(n_len_slice);
+ // cbor uints are in big-endian.
+ let n_len = usize::from(u16::from_be_bytes(len));
+ if n_len > 255 {
+ n_len_rem.split_at_checked(n_len).ok_or(CoseKeyErr::Len).and_then(|(n, n_rem)| {
+ // `n` has length at least 256, so this is fine.
+ // We ensure the leading byte is not 0; otherwise the modulus is not properly
+ // encoded. Note the modulus can never be 0.
+ if n[0] > 0 {
+ n_rem.split_at_checked(2).ok_or(CoseKeyErr::RsaCoseEncoding).and_then(|(e_meta, e_meta_rem)| {
+ // `e_meta.len() == 2`, so this is fine.
+ if e_meta[0] == E {
+ // `e_meta.len() == 2`, so this is fine.
+ let e_meta_len = e_meta[1];
+ if e_meta_len & cbor::BYTES == cbor::BYTES {
+ let e_len = usize::from(e_meta_len ^ cbor::BYTES);
+ if e_len < 5 {
+ e_meta_rem.split_at_checked(e_len).ok_or(CoseKeyErr::Len).and_then(|(e_slice, remaining)| {
+ e_slice.first().ok_or(CoseKeyErr::Len).and_then(|e_first| {
+ // We ensure the leading byte is not 0; otherwise the
+ // exponent is not properly encoded. Note the exponent
+ // can never be 0.
+ if *e_first > 0 {
+ let mut e = [0; 4];
+ // `e_slice.len()` is `e_len` which is less than 5.
+ // We also know it is greater than 0 since `e_slice.first()` did not err.
+ // Thus this won't `panic`.
+ e[4 - e_len..].copy_from_slice(e_slice);
+ Self::try_from((n, u32::from_be_bytes(e))).map_err(CoseKeyErr::RsaPubKey).map(|value| CborSuccess { value, remaining, } )
+ } else {
+ Err(CoseKeyErr::RsaCoseEncoding)
+ }
+ })
+ })
+ } else {
+ Err(CoseKeyErr::RsaExponentTooLarge)
+ }
+ } else {
+ Err(CoseKeyErr::RsaCoseEncoding)
+ }
+ } else {
+ Err(CoseKeyErr::RsaCoseEncoding)
+ }
+ })
+ } else {
+ Err(CoseKeyErr::RsaCoseEncoding)
+ }
+ })
+ } else {
+ Err(CoseKeyErr::RsaCoseEncoding)
+ }
+ })
+ } else {
+ Err(CoseKeyErr::RsaCoseEncoding)
+ }
+ })
+ }
+}
+/// An alleged uncompressed public key that borrows the key data.
+///
+/// Note [`Self::Ed25519`] is compressed.
+#[derive(Clone, Copy, Debug)]
+pub enum UncompressedPubKey<'a> {
+ /// An alleged Ed25519 public key.
+ Ed25519(Ed25519PubKey<&'a [u8]>),
+ /// An alleged uncompressed P-256 public key.
+ P256(UncompressedP256PubKey<'a>),
+ /// An alleged uncompressed P-384 public key.
+ P384(UncompressedP384PubKey<'a>),
+ /// An alleged RSA public key.
+ Rsa(RsaPubKey<&'a [u8]>),
+}
+impl UncompressedPubKey<'_> {
+ /// Validates `self` is in fact a valid public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ match self {
+ Self::Ed25519(k) => k.validate(),
+ Self::P256(k) => k.validate(),
+ Self::P384(k) => k.validate(),
+ Self::Rsa(k) => k.validate(),
+ }
+ }
+}
+impl PartialEq<UncompressedPubKey<'_>> for UncompressedPubKey<'_> {
+ #[inline]
+ fn eq(&self, other: &UncompressedPubKey<'_>) -> bool {
+ match *self {
+ Self::Ed25519(k) => matches!(*other, UncompressedPubKey::Ed25519(k2) if k == k2),
+ Self::P256(k) => matches!(*other, UncompressedPubKey::P256(k2) if k == k2),
+ Self::P384(k) => matches!(*other, UncompressedPubKey::P384(k2) if k == k2),
+ Self::Rsa(k) => matches!(*other, UncompressedPubKey::Rsa(k2) if k == k2),
+ }
+ }
+}
+impl PartialEq<&UncompressedPubKey<'_>> for UncompressedPubKey<'_> {
+ #[inline]
+ fn eq(&self, other: &&UncompressedPubKey<'_>) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<UncompressedPubKey<'_>> for &UncompressedPubKey<'_> {
+ #[inline]
+ fn eq(&self, other: &UncompressedPubKey<'_>) -> bool {
+ **self == *other
+ }
+}
+impl Eq for UncompressedPubKey<'_> {}
+/// An alleged compressed public key.
+///
+/// Note [`Self::Rsa`] is uncompressed.
+#[derive(Clone, Copy, Debug)]
+pub enum CompressedPubKey<T, T2, T3, T4> {
+ /// An alleged Ed25519 public key.
+ Ed25519(Ed25519PubKey<T>),
+ /// An alleged compressed P-256 public key.
+ P256(CompressedP256PubKey<T2>),
+ /// An alleged compressed P-384 public key.
+ P384(CompressedP384PubKey<T3>),
+ /// An alleged RSA public key.
+ Rsa(RsaPubKey<T4>),
+}
+impl CompressedPubKey<&[u8], &[u8], &[u8], &[u8]> {
+ /// Validates `self` is in fact a valid public key.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `self` is not a valid public key.
+ #[inline]
+ pub fn validate(self) -> Result<(), PubKeyErr> {
+ match self {
+ Self::Ed25519(k) => k.validate(),
+ Self::P256(k) => k.validate(),
+ Self::P384(k) => k.validate(),
+ Self::Rsa(k) => k.validate(),
+ }
+ }
+}
+impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8]>>
+ From<&'a CompressedPubKey<T, T2, T3, T4>>
+ for CompressedPubKey<&'b [u8], &'b [u8], &'b [u8], &'b [u8]>
+{
+ #[inline]
+ fn from(value: &'a CompressedPubKey<T, T2, T3, T4>) -> Self {
+ match *value {
+ CompressedPubKey::Ed25519(ref val) => Self::Ed25519(Ed25519PubKey(val.0.as_ref())),
+ CompressedPubKey::P256(ref val) => Self::P256(CompressedP256PubKey {
+ x: val.x.as_ref(),
+ y_is_odd: val.y_is_odd,
+ }),
+ CompressedPubKey::P384(ref val) => Self::P384(CompressedP384PubKey {
+ x: val.x.as_ref(),
+ y_is_odd: val.y_is_odd,
+ }),
+ CompressedPubKey::Rsa(ref val) => Self::Rsa(RsaPubKey(val.0.as_ref(), val.1)),
+ }
+ }
+}
+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>
+{
+ #[inline]
+ fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool {
+ match *self {
+ Self::Ed25519(ref val) => {
+ matches!(*other, CompressedPubKey::Ed25519(ref val2) if val == val2)
+ }
+ Self::P256(ref val) => {
+ matches!(*other, CompressedPubKey::P256(ref val2) if val == val2)
+ }
+ Self::P384(ref val) => {
+ matches!(*other, CompressedPubKey::P384(ref val2) if val == val2)
+ }
+ Self::Rsa(ref val) => matches!(*other, CompressedPubKey::Rsa(ref val2) if val == val2),
+ }
+ }
+}
+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>
+{
+ #[inline]
+ fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool {
+ **self == *other
+ }
+}
+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>
+{
+ #[inline]
+ fn eq(&self, other: &&CompressedPubKey<T, T2, T3, T4>) -> bool {
+ *self == **other
+ }
+}
+impl<T: Eq, T2: Eq, T3: Eq, T4: Eq> Eq for CompressedPubKey<T, T2, T3, T4> {}
+impl<'a> FromCbor<'a> for UncompressedPubKey<'a> {
+ type Err = CoseKeyErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ // {kty:<type>...}.
+ cbor.get(2)
+ .ok_or(CoseKeyErr::Len)
+ .and_then(|kty| match *kty {
+ OKP => Ed25519PubKey::from_cbor(cbor).map(|key| CborSuccess {
+ value: Self::Ed25519(key.value),
+ remaining: key.remaining,
+ }),
+ // {kty:EC2,alg:ES256|ES384,...}
+ EC2 => cbor.get(4).ok_or(CoseKeyErr::Len).and_then(|alg| {
+ if *alg == ES256 {
+ UncompressedP256PubKey::from_cbor(cbor).map(|key| CborSuccess {
+ value: Self::P256(key.value),
+ remaining: key.remaining,
+ })
+ } else {
+ UncompressedP384PubKey::from_cbor(cbor).map(|key| CborSuccess {
+ value: Self::P384(key.value),
+ remaining: key.remaining,
+ })
+ }
+ }),
+ RSA => RsaPubKey::from_cbor(cbor).map(|key| CborSuccess {
+ value: Self::Rsa(key.value),
+ remaining: key.remaining,
+ }),
+ _ => Err(CoseKeyErr::CoseKeyType),
+ })
+ }
+}
+/// Length of AAGUID.
+const AAGUID_LEN: usize = 16;
+/// 16 bytes representing an
+/// [Authenticator Attestation Globally Unique Identifier (AAGUID)](https://www.w3.org/TR/webauthn-3/#aaguid).
+#[derive(Clone, Copy, Debug)]
+pub struct Aaguid<'a>(&'a [u8]);
+impl<'a> Aaguid<'a> {
+ /// Returns the contained data.
+ #[inline]
+ #[must_use]
+ pub const fn data(self) -> &'a [u8] {
+ self.0
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for Aaguid<'b> {
+ type Error = AaguidErr;
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ if value.len() == AAGUID_LEN {
+ Ok(Self(value))
+ } else {
+ Err(AaguidErr)
+ }
+ }
+}
+/// [Attested credential data](https://www.w3.org/TR/webauthn-3/#attested-credential-data).
+#[derive(Debug)]
+pub struct AttestedCredentialData<'a> {
+ /// [`aaguid`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-aaguid).
+ pub aaguid: Aaguid<'a>,
+ /// [`credentialId`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialid).
+ pub credential_id: CredentialId<&'a [u8]>,
+ /// [`credentialPublicKey`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialpublickey).
+ pub credential_public_key: UncompressedPubKey<'a>,
+}
+
+impl<'a> FromCbor<'a> for AttestedCredentialData<'a> {
+ type Err = AttestedCredentialDataErr;
+ #[expect(clippy::big_endian_bytes, reason = "CBOR integers are big-endian")]
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// Number of bytes the `CredentialId` length is encoded into.
+ const CRED_LEN_LEN: usize = 2;
+ cbor.split_at_checked(AAGUID_LEN)
+ .ok_or(AttestedCredentialDataErr::Len)
+ .and_then(|(aaguid, aaguid_rem)| {
+ aaguid_rem
+ .split_at_checked(CRED_LEN_LEN)
+ .ok_or(AttestedCredentialDataErr::Len)
+ .and_then(|(cred_len_slice, cred_len_rem)| {
+ let mut cred_len = [0; CRED_LEN_LEN];
+ cred_len.copy_from_slice(cred_len_slice);
+ // `credentialIdLength` is in big-endian.
+ cred_len_rem
+ .split_at_checked(usize::from(u16::from_be_bytes(cred_len)))
+ .ok_or(AttestedCredentialDataErr::Len)
+ .and_then(|(cred_id, cred_id_rem)| {
+ CredentialId::from_slice(cred_id)
+ .map_err(AttestedCredentialDataErr::CredentialId)
+ .and_then(|credential_id| {
+ UncompressedPubKey::from_cbor(cred_id_rem)
+ .map_err(AttestedCredentialDataErr::CoseKey)
+ .map(|cose| CborSuccess {
+ value: Self {
+ aaguid: Aaguid(aaguid),
+ credential_id,
+ credential_public_key: cose.value,
+ },
+ remaining: cose.remaining,
+ })
+ })
+ })
+ })
+ })
+ }
+}
+/// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+#[derive(Debug)]
+pub struct AuthenticatorData<'a> {
+ /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash).
+ rp_id_hash: &'a [u8],
+ /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags).
+ flags: Flag,
+ /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount).
+ sign_count: u32,
+ /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata).
+ attested_credential_data: AttestedCredentialData<'a>,
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions).
+ extensions: AuthenticatorExtensionOutput,
+}
+impl<'a> AuthenticatorData<'a> {
+ /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash).
+ #[inline]
+ #[must_use]
+ pub const fn rp_id_hash(&self) -> &'a [u8] {
+ self.rp_id_hash
+ }
+ /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags).
+ #[inline]
+ #[must_use]
+ pub const fn flags(&self) -> Flag {
+ self.flags
+ }
+ /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount).
+ #[inline]
+ #[must_use]
+ pub const fn sign_count(&self) -> u32 {
+ self.sign_count
+ }
+ /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata).
+ #[inline]
+ #[must_use]
+ pub const fn attested_credential_data(&self) -> &AttestedCredentialData<'a> {
+ &self.attested_credential_data
+ }
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions).
+ #[inline]
+ #[must_use]
+ pub const fn extensions(&self) -> AuthenticatorExtensionOutput {
+ self.extensions
+ }
+}
+impl<'a> AuthData<'a> for AuthenticatorData<'a> {
+ type UpBitErr = Infallible;
+ type CredData = AttestedCredentialData<'a>;
+ type Ext = AuthenticatorExtensionOutput;
+ fn contains_at_bit() -> bool {
+ true
+ }
+ fn user_is_not_present() -> Result<(), Self::UpBitErr> {
+ Ok(())
+ }
+ fn new(
+ rp_id_hash: &'a [u8],
+ flags: Flag,
+ sign_count: u32,
+ attested_credential_data: Self::CredData,
+ extensions: Self::Ext,
+ ) -> Self {
+ Self {
+ rp_id_hash,
+ flags,
+ sign_count,
+ attested_credential_data,
+ extensions,
+ }
+ }
+ fn rp_hash(&self) -> &'a [u8] {
+ self.rp_id_hash
+ }
+ fn flag(&self) -> Flag {
+ self.flags
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> {
+ type Error = AuthenticatorDataErr;
+ /// Deserializes `value` based on the
+ /// [authenticator data structure](https://www.w3.org/TR/webauthn-3/#table-authData).
+ #[expect(
+ clippy::panic_in_result_fn,
+ reason = "we want to crash when there is a bug"
+ )]
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ Self::from_cbor(value)
+ .map_err(AuthenticatorDataErr::from)
+ .map(|success| {
+ assert!(
+ success.remaining.is_empty(),
+ "there is a bug in AuthenticatorData::from_cbor"
+ );
+ success.value
+ })
+ }
+}
+/// [None](https://www.w3.org/TR/webauthn-3/#sctn-none-attestation).
+struct NoneAttestation;
+impl FromCbor<'_> for NoneAttestation {
+ type Err = AttestationErr;
+ fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> {
+ cbor.split_first()
+ .ok_or(AttestationErr::Len)
+ .and_then(|(map, remaining)| {
+ if *map == cbor::MAP_0 {
+ Ok(CborSuccess {
+ value: Self,
+ remaining,
+ })
+ } else {
+ Err(AttestationErr::NoneFormat)
+ }
+ })
+ }
+}
+/// A 64-byte slice that allegedly represents an Ed25519 signature.
+#[derive(Clone, Copy, Debug)]
+pub struct Ed25519Signature<'a>(&'a [u8]);
+impl<'a> Ed25519Signature<'a> {
+ /// Returns signature.
+ #[inline]
+ #[must_use]
+ pub const fn data(self) -> &'a [u8] {
+ self.0
+ }
+ /// Transforms `self` into `Signature`.
+ fn into_sig(self) -> Signature {
+ let mut sig = [0; ed25519_dalek::SIGNATURE_LENGTH];
+ sig.copy_from_slice(self.0);
+ Signature::from_bytes(&sig)
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for Ed25519Signature<'b> {
+ type Error = Ed25519SignatureErr;
+ /// Interprets `value` as an Ed25519 signature.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `value.len() != 64`.
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ if value.len() == ed25519_dalek::SIGNATURE_LENGTH {
+ Ok(Self(value))
+ } else {
+ Err(Ed25519SignatureErr)
+ }
+ }
+}
+impl<'a> FromCbor<'a> for Ed25519Signature<'a> {
+ type Err = AttestationErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ // `64 as u8` is OK.
+ /// CBOR metadata describing the signature.
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "comments justify their correctness"
+ )]
+ const HEADER: [u8; 2] = [cbor::BYTES_INFO_24, ed25519_dalek::SIGNATURE_LENGTH as u8];
+ cbor.split_at_checked(HEADER.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(ed25519_dalek::SIGNATURE_LENGTH)
+ .ok_or(AttestationErr::Len)
+ .map(|(sig, remaining)| CborSuccess {
+ value: Self(sig),
+ remaining,
+ })
+ } else {
+ Err(AttestationErr::PackedFormatCborEd25519Signature)
+ }
+ })
+ }
+}
+/// An alleged DER-encoded P-256 signature using SHA-256.
+struct P256DerSig<'a>(&'a [u8]);
+impl<'a> FromCbor<'a> for P256DerSig<'a> {
+ type Err = AttestationErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ // ```asn
+ // Signature ::= SEQUENCE {
+ // r INTEGER,
+ // s INTEGER
+ // }
+ // ```
+ // We assume `r` and `s` can be _any_ 32-byte unsigned integer; thus when DER-encoded without metadata,
+ // the number of bytes is inclusively between 1 and 33—INTEGER is a signed integer; thus a 0 byte must
+ // be prepended iff the high bit is 1 (i.e., 32-byte unsigned integers with the high bit set take 33 bytes).
+ // With metadata this makes the lengths inclusively between 3 and 35—we add 1 for the INTEGER tag and 1
+ // for the number of bytes it takes to encode the integer. The total encoded length is thus inclusively
+ // between 8 = 1 + 1 + 2(3) and 72 = 1 + 1 + 2(35)—we add 1 for the CONSTRUCTED SEQUENCE tag and 1 for
+ // the number of bytes to encode the sequence. Instead of handling that specific range of lengths,
+ // we handle all lengths inclusively between 0 and 255.
+ cbor.split_first()
+ .ok_or(AttestationErr::Len)
+ .and_then(|(bytes, bytes_rem)| {
+ if bytes & cbor::BYTES == cbor::BYTES {
+ let len_info = bytes ^ cbor::BYTES;
+ match len_info {
+ ..=23 => Ok((bytes_rem, len_info)),
+ 24 => bytes_rem.split_first().ok_or(AttestationErr::Len).and_then(
+ |(&len, len_rem)| {
+ if len > 23 {
+ Ok((len_rem, len))
+ } else {
+ Err(AttestationErr::PackedFormatCborP256Signature)
+ }
+ },
+ ),
+ _ => Err(AttestationErr::PackedFormatCborP256Signature),
+ }
+ .and_then(|(rem, len)| {
+ rem.split_at_checked(usize::from(len))
+ .ok_or(AttestationErr::Len)
+ .map(|(sig, remaining)| CborSuccess {
+ value: Self(sig),
+ remaining,
+ })
+ })
+ } else {
+ Err(AttestationErr::PackedFormatCborP256Signature)
+ }
+ })
+ }
+}
+/// An alleged DER-encoded P-384 signature using SHA-384.
+struct P384DerSig<'a>(&'a [u8]);
+impl<'a> FromCbor<'a> for P384DerSig<'a> {
+ type Err = AttestationErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ // ```asn
+ // Signature ::= SEQUENCE {
+ // r INTEGER,
+ // s INTEGER
+ // }
+ // ```
+ // We assume `r` and `s` can be _any_ 48-byte unsigned integer; thus when DER-encoded without metadata,
+ // the number of bytes is inclusively between 1 and 49—INTEGER is a signed integer; thus a 0 byte must
+ // be prepended iff the high bit is 1 (i.e., 48-byte unsigned integers with the high bit set take 49 bytes).
+ // With metadata this makes the lengths inclusively between 3 and 51—we add 1 for the INTEGER tag and 1
+ // for the number of bytes it takes to encode the integer. The total encoded length is thus inclusively
+ // between 8 = 1 + 1 + 2(3) and 104 = 1 + 1 + 2(51)—we add 1 for the CONSTRUCTED SEQUENCE tag and 1 for
+ // the number of bytes to encode the sequence. Instead of handling that specific range of lengths,
+ // we handle all lengths inclusively between 0 and 255.
+ cbor.split_first()
+ .ok_or(AttestationErr::Len)
+ .and_then(|(bytes, bytes_rem)| {
+ if bytes & cbor::BYTES == cbor::BYTES {
+ let len_info = bytes ^ cbor::BYTES;
+ match len_info {
+ ..=23 => Ok((bytes_rem, len_info)),
+ 24 => bytes_rem.split_first().ok_or(AttestationErr::Len).and_then(
+ |(&len, len_rem)| {
+ if len > 23 {
+ Ok((len_rem, len))
+ } else {
+ Err(AttestationErr::PackedFormatCborP384Signature)
+ }
+ },
+ ),
+ _ => Err(AttestationErr::PackedFormatCborP384Signature),
+ }
+ .and_then(|(rem, len)| {
+ rem.split_at_checked(usize::from(len))
+ .ok_or(AttestationErr::Len)
+ .map(|(sig, remaining)| CborSuccess {
+ value: Self(sig),
+ remaining,
+ })
+ })
+ } else {
+ Err(AttestationErr::PackedFormatCborP384Signature)
+ }
+ })
+ }
+}
+/// An alleged RSASSA-PKCS1-v1_5 signature using SHA-256.
+struct RsaPkcs1v15Sig<'a>(&'a [u8]);
+impl<'a> FromCbor<'a> for RsaPkcs1v15Sig<'a> {
+ type Err = AttestationErr;
+ #[expect(clippy::big_endian_bytes, reason = "CBOR integers are big-endian")]
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ // RSASSA-PKCS1-v1_5 signatures are the same length as the modulus. We only allow moduli consisting of
+ // [`MIN_RSA_N_BYTES`] to [`MAX_RSA_N_BYTES`] bytes inclusively. This means
+ // all signatures will use the same CBOR metadata tag (i.e., `cbor::BYTES_INFO_25`).
+ cbor.split_first()
+ .ok_or(AttestationErr::Len)
+ .and_then(|(bytes, bytes_rem)| {
+ if *bytes == cbor::BYTES_INFO_25 {
+ bytes_rem
+ .split_at_checked(2)
+ .ok_or(AttestationErr::Len)
+ .and_then(|(len_slice, len_rem)| {
+ let mut cbor_len = [0; 2];
+ cbor_len.copy_from_slice(len_slice);
+ // CBOR uints are big-endian.
+ let len = usize::from(u16::from_be_bytes(cbor_len));
+ if len > 255 {
+ len_rem
+ .split_at_checked(len)
+ .ok_or(AttestationErr::Len)
+ .map(|(sig, remaining)| CborSuccess {
+ value: Self(sig),
+ remaining,
+ })
+ } else {
+ Err(AttestationErr::PackedFormatCborRs256Signature)
+ }
+ })
+ } else {
+ Err(AttestationErr::PackedFormatCborRs256Signature)
+ }
+ })
+ }
+}
+/// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) signature.
+#[derive(Clone, Copy, Debug)]
+pub enum Sig<'a> {
+ /// Alleged Ed25519 signature.
+ Ed25519(Ed25519Signature<'a>),
+ /// Alleged DER-encoded P-256 signature using SHA-256.
+ P256(&'a [u8]),
+ /// Alleged DER-encoded P-384 signature using SHA-384.
+ P384(&'a [u8]),
+ /// Alleged RSASSA-PKCS1-v1_5 signature using SHA-256.
+ Rs256(&'a [u8]),
+}
+/// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation).
+#[derive(Clone, Copy, Debug)]
+pub struct PackedAttestation<'a> {
+ /// [Attestation signature](https://www.w3.org/TR/webauthn-3/#attestation-signature).
+ pub signature: Sig<'a>,
+}
+impl<'a> FromCbor<'a> for PackedAttestation<'a> {
+ type Err = AttestationErr;
+ #[expect(
+ clippy::too_many_lines,
+ reason = "don't want to move code to an outer scope"
+ )]
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ /// Parses `data` as packed attestation up until `x5c` _without_ the `cbor::MAP_2` or `cbor::MAP_3` header.
+ fn parse_to_cert_chain(data: &[u8]) -> Result<CborSuccess<'_, Sig<'_>>, AttestationErr> {
+ // {"alg":CoseAlgId,"sig":sig_bytes...}
+ /// "alg" key.
+ const ALG: [u8; 4] = [cbor::TEXT_3, b'a', b'l', b'g'];
+ /// "sig" key.
+ const SIG: [u8; 4] = [cbor::TEXT_3, b's', b'i', b'g'];
+ data.split_at_checked(ALG.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(alg, alg_rem)| {
+ if alg == ALG {
+ alg_rem.split_first().ok_or(AttestationErr::Len).and_then(
+ |(cose, cose_rem)| match *cose {
+ EDDSA => cose_rem
+ .split_at_checked(SIG.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(sig, sig_rem)| {
+ if sig == SIG {
+ Ed25519Signature::from_cbor(sig_rem).map(|success| {
+ CborSuccess {
+ value: Sig::Ed25519(success.value),
+ remaining: success.remaining,
+ }
+ })
+ } else {
+ Err(AttestationErr::PackedFormatMissingSig)
+ }
+ }),
+ ES256 => cose_rem
+ .split_at_checked(SIG.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(sig, sig_rem)| {
+ if sig == SIG {
+ P256DerSig::from_cbor(sig_rem).map(|success| {
+ CborSuccess {
+ value: Sig::P256(success.value.0),
+ remaining: success.remaining,
+ }
+ })
+ } else {
+ Err(AttestationErr::PackedFormatMissingSig)
+ }
+ }),
+ cbor::NEG_INFO_24 => cose_rem
+ .split_first()
+ .ok_or(AttestationErr::Len)
+ .and_then(|(len, len_rem)| {
+ if *len == ES384 {
+ len_rem
+ .split_at_checked(SIG.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(sig, sig_rem)| {
+ if sig == SIG {
+ P384DerSig::from_cbor(sig_rem).map(
+ |success| CborSuccess {
+ value: Sig::P384(success.value.0),
+ remaining: success.remaining,
+ },
+ )
+ } else {
+ Err(AttestationErr::PackedFormatMissingSig)
+ }
+ })
+ } else {
+ Err(AttestationErr::PackedFormatUnsupportedAlg)
+ }
+ }),
+ cbor::NEG_INFO_25 => cose_rem
+ .split_at_checked(2)
+ .ok_or(AttestationErr::Len)
+ .and_then(|(len, len_rem)| {
+ if len == RS256 {
+ len_rem
+ .split_at_checked(SIG.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(sig, sig_rem)| {
+ if sig == SIG {
+ RsaPkcs1v15Sig::from_cbor(sig_rem).map(
+ |success| CborSuccess {
+ value: Sig::Rs256(success.value.0),
+ remaining: success.remaining,
+ },
+ )
+ } else {
+ Err(AttestationErr::PackedFormatMissingSig)
+ }
+ })
+ } else {
+ Err(AttestationErr::PackedFormatUnsupportedAlg)
+ }
+ }),
+ _ => Err(AttestationErr::PackedFormatUnsupportedAlg),
+ },
+ )
+ } else {
+ Err(AttestationErr::PackedFormatMissingAlg)
+ }
+ })
+ }
+ cbor.split_first()
+ .ok_or(AttestationErr::Len)
+ .and_then(|(map, map_rem)| match *map {
+ cbor::MAP_2 => parse_to_cert_chain(map_rem).map(|success| CborSuccess {
+ value: Self {
+ signature: success.value,
+ },
+ remaining: success.remaining,
+ }),
+ _ => Err(AttestationErr::PackedFormat),
+ })
+ }
+}
+/// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids).
+#[derive(Clone, Copy, Debug)]
+pub enum AttestationFormat<'a> {
+ /// [None](https://www.w3.org/TR/webauthn-3/#sctn-none-attestation).
+ None,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation).
+ Packed(PackedAttestation<'a>),
+}
+impl<'a> FromCbor<'a> for AttestationFormat<'a> {
+ type Err = AttestationErr;
+ fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> {
+ // Note we assume that cbor starts _after_ `cbor::MAP_3`.
+ // {"fmt":"none"|"packed", "attStmt": NoneAttestation|PackedAttestation, ...}.
+ /// "fmt" key.
+ const FMT: [u8; 4] = [cbor::TEXT_3, b'f', b'm', b't'];
+ /// "none" value.
+ const NONE: [u8; 5] = [cbor::TEXT_4, b'n', b'o', b'n', b'e'];
+ /// "packed" value.
+ const PACKED: [u8; 7] = [cbor::TEXT_6, b'p', b'a', b'c', b'k', b'e', b'd'];
+ /// "attStmt" key.
+ const ATT_STMT: [u8; 8] = [cbor::TEXT_7, b'a', b't', b't', b'S', b't', b'm', b't'];
+ cbor.split_at_checked(FMT.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(fmt, fmt_rem)| {
+ if fmt == FMT {
+ fmt_rem
+ .split_at_checked(NONE.len())
+ .ok_or(AttestationErr::Len)
+ // `PACKED.len() > NONE.len()`, so we check for `PACKED` in `and_then`.
+ .and_then(|(none, none_rem)| {
+ if none == NONE {
+ none_rem
+ .split_at_checked(ATT_STMT.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(att, att_rem)| {
+ if att == ATT_STMT {
+ NoneAttestation::from_cbor(att_rem).map(|success| {
+ CborSuccess {
+ value: Self::None,
+ remaining: success.remaining,
+ }
+ })
+ } else {
+ Err(AttestationErr::MissingStatement)
+ }
+ })
+ } else {
+ fmt_rem
+ .split_at_checked(PACKED.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(packed, packed_rem)| {
+ if packed == PACKED {
+ packed_rem
+ .split_at_checked(ATT_STMT.len())
+ .ok_or(AttestationErr::Len)
+ .and_then(|(att, att_rem)| {
+ if att == ATT_STMT {
+ PackedAttestation::from_cbor(att_rem).map(
+ |success| CborSuccess {
+ value: Self::Packed(success.value),
+ remaining: success.remaining,
+ },
+ )
+ } else {
+ Err(AttestationErr::MissingStatement)
+ }
+ })
+ } else {
+ Err(AttestationErr::UnsupportedFormat)
+ }
+ })
+ }
+ })
+ } else {
+ Err(AttestationErr::MissingFormat)
+ }
+ })
+ }
+}
+/// [Attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object).
+#[derive(Debug)]
+pub struct AttestationObject<'a> {
+ /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids).
+ attestation: AttestationFormat<'a>,
+ /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+ auth_data: AuthenticatorData<'a>,
+}
+impl<'a> AttestationObject<'a> {
+ /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids).
+ #[inline]
+ #[must_use]
+ pub const fn attestation(&self) -> AttestationFormat<'a> {
+ self.attestation
+ }
+ /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data).
+ #[inline]
+ #[must_use]
+ pub const fn auth_data(&self) -> &AuthenticatorData<'a> {
+ &self.auth_data
+ }
+ /// Deserializes `data` based on the
+ /// [attestation object layout](https://www.w3.org/TR/webauthn-3/#attestation-object)
+ /// returning [`Self`] and the index within `data` that the authenticator data portion
+ /// begins.
+ ///
+ /// This mainly exists to unify [`Self::from_data`], [`Self::try_from`], and
+ /// [`ser::AuthenticatorAttestationVisitor::visit_map`].
+ #[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"
+ )]
+ #[expect(clippy::big_endian_bytes, reason = "cbor lengths are in big-endian")]
+ fn parse_data<'b: 'a>(data: &'b [u8]) -> Result<(Self, usize), AttestationObjectErr> {
+ /// `authData` key.
+ const AUTH_DATA_KEY: [u8; 9] =
+ [cbor::TEXT_8, b'a', b'u', b't', b'h', b'D', b'a', b't', b'a'];
+ // {"fmt":<AttestationFormat>, "attStmt":<AttestationStatement>, "authData":<AuthenticatorData>}.
+ data.split_first().ok_or(AttestationObjectErr::Len).and_then(|(map, map_rem)| {
+ if *map == cbor::MAP_3 {
+ AttestationFormat::from_cbor(map_rem).map_err(AttestationObjectErr::Attestation).and_then(|att| {
+ att.remaining.split_at_checked(AUTH_DATA_KEY.len()).ok_or(AttestationObjectErr::Len).and_then(|(key, key_rem)| {
+ if key == AUTH_DATA_KEY {
+ key_rem.split_first().ok_or(AttestationObjectErr::Len).and_then(|(bytes, bytes_rem)| {
+ if bytes & cbor::BYTES == cbor::BYTES {
+ match bytes ^ cbor::BYTES {
+ 24 => bytes_rem.split_first().ok_or(AttestationObjectErr::Len).and_then(|(&len, auth_data)| {
+ if len > 23 {
+ Ok((usize::from(len), auth_data))
+ } else {
+ Err(AttestationObjectErr::AuthDataLenInfo)
+ }
+ }),
+ 25 => bytes_rem.split_at_checked(2).ok_or(AttestationObjectErr::Len).and_then(|(len_slice, auth_data)| {
+ let mut auth_len = [0; 2];
+ auth_len.copy_from_slice(len_slice);
+ // Length is encoded as big-endian.
+ let len = usize::from(u16::from_be_bytes(auth_len));
+ if len > 255 {
+ Ok((len, auth_data))
+ } else {
+ Err(AttestationObjectErr::AuthDataLenInfo)
+ }
+ }),
+ _ => Err(AttestationObjectErr::AuthDataLenInfo),
+ }.and_then(|(cbor_auth_data_len, auth_slice)| {
+ if cbor_auth_data_len == auth_slice.len() {
+ AuthenticatorData::from_cbor(auth_slice).map_err(|e| AttestationObjectErr::AuthData(e.into())).and_then(|auth_data| {
+ assert!(auth_data.remaining.is_empty(), "there is a bug in AuthenticatorData::from_cbor");
+ match att.value {
+ AttestationFormat::None => Ok(()),
+ AttestationFormat::Packed(ref val) => match val.signature {
+ Sig::Ed25519(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::Ed25519(_)) {
+ Ok(())
+ } else {
+ Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch)
+ },
+ Sig::P256(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::P256(_)) {
+ Ok(())
+ } else {
+ Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch)
+ },
+ Sig::P384(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::P384(_)) {
+ Ok(())
+ } else {
+ Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch)
+ },
+ Sig::Rs256(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::Rsa(_)) {
+ Ok(())
+ } else {
+ Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch)
+ },
+ },
+ // `cbor_auth_data_len == `auth_slice.len()` and `auth_slice.len() < data.len()`, so underflow won't happen.
+ }.map(|()| (Self { attestation: att.value, auth_data: auth_data.value }, data.len() - cbor_auth_data_len))
+ })
+ } else {
+ Err(AttestationObjectErr::CborAuthDataLenMismatch)
+ }
+ })
+ } else {
+ Err(AttestationObjectErr::AuthDataType)
+ }
+ })
+ } else {
+ Err(AttestationObjectErr::MissingAuthData)
+ }
+ })
+ })
+ } else {
+ Err(AttestationObjectErr::NotAMapOf3)
+ }
+ })
+ }
+}
+impl<'a> AuthDataContainer<'a> for AttestationObject<'a> {
+ type Auth = AuthenticatorData<'a>;
+ type Err = AttestationObjectErr;
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
+ fn from_data(data: &'a [u8]) -> Result<ParsedAuthData<'a, Self>, Self::Err> {
+ // `data.len().checked_sub(Sha256::output_size())` is clearly less than `data.len()`;
+ // thus indexing wont `panic`.
+ Self::parse_data(&data[..data.len().checked_sub(Sha256::output_size()).unwrap_or_else(|| unreachable!("AttestationObject::from_data must be passed a slice with 32 bytes of trailing data"))]).map(|(attest, auth_idx)| ParsedAuthData { data: attest, auth_data_and_32_trailing_bytes: &data[auth_idx..], })
+ }
+ fn authenticator_data(&self) -> &Self::Auth {
+ &self.auth_data
+ }
+}
+impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AttestationObject<'b> {
+ type Error = AttestationObjectErr;
+ /// Deserializes `value` based on the
+ /// [attestation object layout](https://www.w3.org/TR/webauthn-3/#attestation-object).
+ #[inline]
+ fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
+ Self::parse_data(value).map(|(val, _)| val)
+ }
+}
+/// [`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).
+ 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`].
+ attestation_object_and_c_data_hash: Vec<u8>,
+ /// [`getTransports`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-gettransports).
+ transports: AuthTransports,
+}
+impl AuthenticatorAttestation {
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-clientdatajson).
+ #[inline]
+ #[must_use]
+ pub fn client_data_json(&self) -> &[u8] {
+ self.client_data_json.as_slice()
+ }
+ /// [attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object).
+ #[expect(
+ clippy::arithmetic_side_effects,
+ clippy::indexing_slicing,
+ reason = "comment justifies their correctness"
+ )]
+ #[inline]
+ #[must_use]
+ pub fn attestation_object(&self) -> &[u8] {
+ // We only allow creation via [`Self::new`] which creates [`Self::attestation_object_and_c_data_hash`]
+ // by appending the SHA-256 hash of [`Self::client_data_json`] to the attestation object that was passed;
+ // thus indexing is fine and subtraction won't cause underflow.
+ &self.attestation_object_and_c_data_hash
+ [..self.attestation_object_and_c_data_hash.len() - Sha256::output_size()]
+ }
+ /// [`getTransports`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-gettransports).
+ #[inline]
+ #[must_use]
+ pub const fn transports(&self) -> AuthTransports {
+ self.transports
+ }
+ /// Constructs an instance of `Self` with the contained data.
+ ///
+ /// Note calling code is encouraged to ensure `attestation_object` has at least 32 bytes
+ /// of available capacity; if not, a reallocation will occur which may hinder performance
+ /// depending on its size.
+ #[inline]
+ #[must_use]
+ pub fn new(
+ client_data_json: Vec<u8>,
+ mut attestation_object: Vec<u8>,
+ transports: AuthTransports,
+ ) -> Self {
+ attestation_object
+ .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
+ Self {
+ client_data_json,
+ attestation_object_and_c_data_hash: attestation_object,
+ transports,
+ }
+ }
+}
+impl AuthResponse for AuthenticatorAttestation {
+ type Auth<'a>
+ = AttestationObject<'a>
+ where
+ Self: 'a;
+ type CredKey<'a> = ();
+ #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
+ fn parse_data_and_verify_sig(
+ &self,
+ (): Self::CredKey<'_>,
+ relaxed: bool,
+ ) -> Result<
+ (CollectedClientData<'_>, Self::Auth<'_>),
+ AuthRespErr<<Self::Auth<'_> as AuthDataContainer<'_>>::Err>,
+ > {
+ if relaxed {
+ #[cfg(not(feature = "serde_relaxed"))]
+ unreachable!("AuthenticatorAttestation::parse_data_and_verify_sig: must be passed true when serde_relaxed is not enabled");
+ #[cfg(feature = "serde_relaxed")]
+ CollectedClientData::from_client_data_json_relaxed::<true>(
+ self.client_data_json.as_slice(),
+ ).map_err(AuthRespErr::CollectedClientDataRelaxed)
+ } else {
+ CollectedClientData::from_client_data_json::<true>(self.client_data_json.as_slice()).map_err(AuthRespErr::CollectedClientData)
+ }
+ .and_then(|client_data_json| {
+ Self::Auth::from_data(self.attestation_object_and_c_data_hash.as_slice())
+ .map_err(AuthRespErr::Auth)
+ .and_then(|val| {
+ match val.data.auth_data.attested_credential_data.credential_public_key {
+ UncompressedPubKey::Ed25519(key) => key.into_ver_key().map_err(AuthRespErr::PubKey).and_then(|ver_key| {
+ match val.data.attestation {
+ AttestationFormat::None => Ok(()),
+ AttestationFormat::Packed(packed) => match packed.signature {
+ Sig::Ed25519(sig) => ver_key.verify(val.auth_data_and_32_trailing_bytes, &sig.into_sig()).map_err(|_e| AuthRespErr::Signature),
+ Sig::P256(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ }
+ }
+ }),
+ UncompressedPubKey::P256(key) => key.into_ver_key().map_err(AuthRespErr::PubKey).and_then(|ver_key| {
+ match val.data.attestation {
+ AttestationFormat::None => Ok(()),
+ AttestationFormat::Packed(packed) => match packed.signature {
+ Sig::P256(sig) => P256Sig::from_bytes(sig).map_err(|_e| AuthRespErr::Signature).and_then(|s| ver_key.verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)),
+ Sig::Ed25519(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ }
+ }
+ }),
+ UncompressedPubKey::P384(key) => key.into_ver_key().map_err(AuthRespErr::PubKey).and_then(|ver_key| {
+ match val.data.attestation {
+ AttestationFormat::None => Ok(()),
+ AttestationFormat::Packed(packed) => match packed.signature {
+ Sig::P384(sig) => P384Sig::from_bytes(sig).map_err(|_e| AuthRespErr::Signature).and_then(|s| ver_key.verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)),
+ Sig::Ed25519(_) | Sig::P256(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ }
+ }
+ }),
+ UncompressedPubKey::Rsa(key) => key.as_ver_key().map_err(AuthRespErr::PubKey).and_then(|ver_key| {
+ match val.data.attestation {
+ AttestationFormat::None => Ok(()),
+ AttestationFormat::Packed(packed) => match packed.signature {
+ Sig::Rs256(sig) => pkcs1v15::Signature::try_from(sig).map_err(|_e| AuthRespErr::Signature).and_then(|s| ver_key.verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)),
+ Sig::Ed25519(_) | Sig::P256(_) | Sig::P384(_) => unreachable!("there is a bug in AttestationObject::from_data"),
+ }
+ }
+ }),
+ }.map(|()| (client_data_json, val.data))
+ })
+ })
+ }
+}
+/// [`CredentialPropertiesOutput`](https://www.w3.org/TR/webauthn-3/#dictdef-credentialpropertiesoutput).
+///
+/// Note [`Self::rk`] is frequently unreliable. For example there are times it is `Some(false)` despite the
+/// credential being stored client-side. One may have better luck checking if [`AuthTransports::contains`]
+/// [`AuthenticatorTransport::Internal`] and using that as an indicator if a client-side credential was created.
+#[derive(Clone, Copy, Debug)]
+pub struct CredentialPropertiesOutput {
+ /// [`rk`](https://www.w3.org/TR/webauthn-3/#dom-credentialpropertiesoutput-rk).
+ pub rk: Option<bool>,
+}
+/// [`AuthenticationExtensionsPRFOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs).
+///
+/// Note since this is a server-side library, we don't store
+/// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results)
+/// since it contains sensitive data that should remain client-side.
+#[derive(Clone, Copy, Debug)]
+pub struct AuthenticationExtensionsPrfOutputs {
+ /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled).
+ pub enabled: bool,
+}
+/// [`AuthenticationExtensionsClientOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientoutputs).
+#[derive(Clone, Copy, Debug)]
+pub struct ClientExtensionsOutputs {
+ /// [`credProps`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-credprops).
+ pub cred_props: Option<CredentialPropertiesOutput>,
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf).
+ pub prf: Option<AuthenticationExtensionsPrfOutputs>,
+}
+/// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for registration ceremonies.
+#[expect(
+ clippy::field_scoped_visibility_modifiers,
+ reason = "no invariants to uphold"
+)]
+#[derive(Debug)]
+pub struct Registration {
+ /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
+ pub(crate) response: AuthenticatorAttestation,
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
+ pub(crate) authenticator_attachment: AuthenticatorAttachment,
+ /// [`getClientExtensionResults()`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults).
+ pub(crate) client_extension_results: ClientExtensionsOutputs,
+}
+impl Registration {
+ /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
+ #[inline]
+ #[must_use]
+ pub const fn response(&self) -> &AuthenticatorAttestation {
+ &self.response
+ }
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
+ #[inline]
+ #[must_use]
+ pub const fn authenticator_attachment(&self) -> AuthenticatorAttachment {
+ self.authenticator_attachment
+ }
+ /// [`getClientExtensionResults()`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults).
+ #[inline]
+ #[must_use]
+ pub const fn client_extension_results(&self) -> ClientExtensionsOutputs {
+ self.client_extension_results
+ }
+ /// Constructs a `Registration` based on the passed arguments.
+ #[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
+ #[cfg(feature = "custom")]
+ #[inline]
+ #[must_use]
+ pub const fn new(
+ response: AuthenticatorAttestation,
+ authenticator_attachment: AuthenticatorAttachment,
+ client_extension_results: ClientExtensionsOutputs,
+ ) -> Self {
+ Self {
+ response,
+ authenticator_attachment,
+ client_extension_results,
+ }
+ }
+ /// Convenience function for
+ /// `CollectedClientData::from_client_data_json::<true>(self.response().client_data_json()).map(|c| c.challenge)`.
+ ///
+ /// This is useful when wanting to extract the corresponding [`RegistrationServerState`] from
+ /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`CollectedClientData::from_client_data_json`] does.
+ #[inline]
+ pub fn challenge(&self) -> Result<SentChallenge, CollectedClientDataErr> {
+ CollectedClientData::from_client_data_json::<true>(
+ self.response.client_data_json.as_slice(),
+ )
+ .map(|c| c.challenge)
+ }
+ /// Convenience function for
+ /// `CollectedClientData::from_client_data_json_relaxed::<true>(self.response().client_data_json()).map(|c| c.challenge)`.
+ ///
+ /// This is useful when wanting to extract the corresponding [`RegistrationServerState`] from
+ /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff [`CollectedClientData::from_client_data_json_relaxed`] does.
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ #[inline]
+ pub fn challenge_relaxed(&self) -> Result<SentChallenge, SerdeJsonErr> {
+ CollectedClientData::from_client_data_json_relaxed::<true>(
+ self.response.client_data_json.as_slice(),
+ )
+ .map(|c| c.challenge)
+ }
+}
+impl Response for Registration {
+ type Auth = AuthenticatorAttestation;
+ fn auth(&self) -> &Self::Auth {
+ &self.response
+ }
+}
+/// [Attestation statement](https://www.w3.org/TR/webauthn-3/#attestation-statement).
+#[derive(Clone, Copy, Debug)]
+pub enum Attestation {
+ /// [None](https://www.w3.org/TR/webauthn-3/#none).
+ None,
+ /// [Self](https://www.w3.org/TR/webauthn-3/#self).
+ Surrogate,
+}
+/// Metadata associated with a [`RegisteredCredential`].
+///
+/// This information exists purely for informative reasons as it is not used in any way during authentication
+/// ceremonies; consequently, one may not want to store this information.
+#[derive(Clone, Copy, Debug)]
+pub struct Metadata<'a> {
+ /// [Attestation statement](https://www.w3.org/TR/webauthn-3/#attestation-statement).
+ pub attestation: Attestation,
+ /// [`aaguid`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-aaguid).
+ pub aaguid: Aaguid<'a>,
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) output during registration that is
+ /// never used during authentication ceremonies.
+ pub extensions: AuthenticatorExtensionOutputMetadata,
+ /// [`getClientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults)
+ /// output during registration that is never used during authentication ceremonies.
+ pub client_extension_results: ClientExtensionsOutputs,
+ /// `ResidentKeyRequirement` sent during registration.
+ pub resident_key: ResidentKeyRequirement,
+}
+impl Metadata<'_> {
+ /// Transforms `self` into a JSON object conforming to the following pseudo-schema:
+ ///
+ /// ```json
+ /// // MetadataJSON:
+ /// {
+ /// "attestation": "none" | "self",
+ /// "aaguid": "<32-uppercase-hexadecimal digits>",
+ /// "extensions": {
+ /// "min_pin_length": null | <8-bit unsigned integer without leading 0s>
+ /// },
+ /// "client_extension_results": {
+ /// "cred_props": null | CredPropsJSON,
+ /// "prf": null | PrfJSON
+ /// },
+ /// "resident_key": "required" | "discouraged" | "preferred"
+ /// }
+ /// // CredPropsJSON:
+ /// {
+ /// "rk": null | false | true
+ /// }
+ /// // PrfJSON:
+ /// {
+ /// "enabled": false | true
+ /// }
+ /// ```
+ /// where unnecessary whitespace does not exist.
+ ///
+ /// This primarily exists so that one can store the data in a human-readable way without the need of
+ /// bringing the data back into the application. This allows one to read the data using some external
+ /// means purely for informative reasons. If one wants the ability to "act" on the data; then one should
+ /// [`Metadata::encode`] the data instead, or in addition to, that way it can be [`MetadataOwned::decode`]d.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use core::str::FromStr;
+ /// # use webauthn_rp::{
+ /// # request::register::ResidentKeyRequirement,
+ /// # response::register::{
+ /// # Aaguid, Attestation,
+ /// # AuthenticatorExtensionOutputMetadata, ClientExtensionsOutputs, CredentialPropertiesOutput,
+ /// # Metadata,
+ /// # },
+ /// # };
+ /// let metadata = Metadata {
+ /// attestation: Attestation::None,
+ /// aaguid: Aaguid::try_from([15; 16].as_slice())?,
+ /// extensions: AuthenticatorExtensionOutputMetadata {
+ /// min_pin_length: Some(16),
+ /// },
+ /// client_extension_results: ClientExtensionsOutputs {
+ /// cred_props: Some(CredentialPropertiesOutput {
+ /// rk: Some(true),
+ /// }),
+ /// prf: None,
+ /// },
+ /// resident_key: ResidentKeyRequirement::Required
+ /// };
+ /// let json = serde_json::json!({
+ /// "attestation": "none",
+ /// "aaguid": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F",
+ /// "extensions": {
+ /// "min_pin_length": 16
+ /// },
+ /// "client_extension_results": {
+ /// "cred_props": {
+ /// "rk": true
+ /// },
+ /// "prf": null
+ /// },
+ /// "resident_key": "required"
+ /// });
+ /// assert_eq!(metadata.into_json(), json.to_string());
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[expect(unsafe_code, reason = "comment justifies its correctness and reason")]
+ #[expect(
+ clippy::arithmetic_side_effects,
+ clippy::integer_division,
+ clippy::integer_division_remainder_used,
+ reason = "comments justify their correctness"
+ )]
+ #[expect(
+ clippy::else_if_without_else,
+ reason = "don't want an empty else branch"
+ )]
+ #[inline]
+ #[must_use]
+ pub fn into_json(self) -> String {
+ // Maximum capacity needed is not _that_ much larger than the minimum, 184. An example is the
+ // following:
+ // `{"attestation":"none","aaguid":"00000000000000000000000000000000","extensions":{"min_pin_length":null},"client_extension_results":{"cred_props":{"rk":false},"prf":{"enabled":false}},"resident_key":"discouraged"}`.
+ // We use a raw `Vec` instead of a `String` since we need to transform some binary values into ASCII which
+ // is easier to do as bytes.
+ let mut buffer = Vec::with_capacity(211);
+ buffer.extend_from_slice(br#"{"attestation":"#);
+ buffer.extend_from_slice(match self.attestation {
+ Attestation::None => br#""none","aaguid":""#,
+ Attestation::Surrogate => br#""self","aaguid":""#,
+ });
+ self.aaguid.0.iter().fold((), |(), byt| {
+ // Get the first nibble.
+ let nib_fst = byt & 0xf0;
+ // We simply add the appropriate offset. For decimal digits this means simply adding `b'0'`; but
+ // for uppercase hexadecimal, this means adding 55 since `b'A'` is 65.
+ // Overflow cannot occur since this maxes at `b'F'`.
+ buffer.push(nib_fst + if nib_fst < 0xa { b'0' } else { 55 });
+ // Get the second nibble.
+ let nib_snd = byt & 0xf;
+ // We simply add the appropriate offset. For decimal digits this means simply adding `b'0'`; but
+ // for uppercase hexadecimal, this means adding 55 since `b'A'` is 65.
+ // Overflow cannot occur since this maxes at `b'F'`.
+ buffer.push(nib_snd + if nib_snd < 0xa { b'0' } else { 55 });
+ });
+ buffer.extend_from_slice(br#"","extensions":{"min_pin_length":"#);
+ match self.extensions.min_pin_length {
+ None => buffer.extend_from_slice(b"null"),
+ Some(pin) => {
+ // Clearly correct.
+ let dig_1 = pin / 100;
+ // Clearly correct.
+ let dig_2 = (pin % 100) / 10;
+ // Clearly correct.
+ let dig_3 = pin % 10;
+ if dig_1 > 0 {
+ // We simply add the appropriate offset which is `b'0` for decimal digits.
+ // Overflow cannot occur since this maxes at `b'9'`.
+ buffer.push(dig_1 + b'0');
+ // We simply add the appropriate offset which is `b'0` for decimal digits.
+ // Overflow cannot occur since this maxes at `b'9'`.
+ buffer.push(dig_2 + b'0');
+ } else if dig_2 > 0 {
+ // We simply add the appropriate offset which is `b'0` for decimal digits.
+ // Overflow cannot occur since this maxes at `b'9'`.
+ buffer.push(dig_2 + b'0');
+ }
+ // We simply add the appropriate offset which is `b'0` for decimal digits.
+ // Overflow cannot occur since this maxes at `b'9'`.
+ buffer.push(dig_3 + b'0');
+ }
+ }
+ buffer.extend_from_slice(br#"},"client_extension_results":{"cred_props":"#);
+ match self.client_extension_results.cred_props {
+ None => buffer.extend_from_slice(b"null"),
+ Some(props) => {
+ buffer.extend_from_slice(br#"{"rk":"#);
+ match props.rk {
+ None => buffer.extend_from_slice(b"null}"),
+ Some(rk) => buffer.extend_from_slice(if rk { b"true}" } else { b"false}" }),
+ }
+ }
+ }
+ buffer.extend_from_slice(br#","prf":"#);
+ match self.client_extension_results.prf {
+ None => buffer.extend_from_slice(b"null"),
+ Some(prf) => {
+ buffer.extend_from_slice(if prf.enabled {
+ br#"{"enabled":true}"#
+ } else {
+ br#"{"enabled":false}"#
+ });
+ }
+ }
+ buffer.extend_from_slice(br#"},"resident_key":"#);
+ buffer.extend_from_slice(match self.resident_key {
+ ResidentKeyRequirement::Required => br#""required"}"#,
+ ResidentKeyRequirement::Discouraged => br#""discouraged"}"#,
+ ResidentKeyRequirement::Preferred => br#""preferred"}"#,
+ });
+ // SAFETY:
+ // Clearly above only appends ASCII, a subset of UTF-8, to `buffer`; thus `buffer`
+ // is valid UTF-8.
+ unsafe { String::from_utf8_unchecked(buffer) }
+ }
+}
+/// [`RegisteredCredential`] and [`AuthenticatedCredential`] static state.
+///
+/// `PublicKey` needs to be [`UncompressedPubKey`] or [`CompressedPubKey`] for this type to be of any use.
+#[derive(Clone, Copy, Debug)]
+pub struct StaticState<PublicKey> {
+ /// [`credentialPublicKey`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialpublickey).
+ pub credential_public_key: PublicKey,
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) output during registration that are
+ /// used during authentication ceremonies.
+ pub extensions: AuthenticatorExtensionOutputStaticState,
+}
+/// [`RegisteredCredential`] and [`AuthenticatedCredential`] dynamic state.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct DynamicState {
+ /// [`UV`](https://www.w3.org/TR/webauthn-3/#authdata-flags-uv).
+ ///
+ /// Once this is `true`, it will remain `true`. It will be set to `true` when `false` iff
+ /// [`Flag::user_verified`] and [`AuthenticationVerificationOptions::update_uv`].
+ pub user_verified: bool,
+ /// This can only be updated if [`BackupReq`] allows for it.
+ pub backup: Backup,
+ /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount).
+ ///
+ /// This is only updated if the authenticator supports
+ /// [signature counters](https://www.w3.org/TR/webauthn-3/#signature-counter), and the behavior of how it is
+ /// updated is controlled by [`AuthenticationVerificationOptions::sig_counter_enforcement`].
+ pub sign_count: u32,
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
+ ///
+ /// [`AuthenticationVerificationOptions::auth_attachment_enforcement`] controls if/how this updated.
+ pub authenticator_attachment: AuthenticatorAttachment,
+}
+impl PartialEq<&Self> for DynamicState {
+ #[inline]
+ fn eq(&self, other: &&Self) -> bool {
+ *self == **other
+ }
+}
+impl PartialEq<DynamicState> for &DynamicState {
+ #[inline]
+ fn eq(&self, other: &DynamicState) -> bool {
+ **self == *other
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::{
+ super::{
+ super::{
+ request::{AsciiDomain, RpId},
+ AggErr,
+ },
+ auth::{AuthenticatorAssertion, AuthenticatorData},
+ },
+ AttestationFormat, AttestationObject, AuthDataContainer as _, AuthTransports,
+ AuthenticatorAttestation, Backup, Sig, UncompressedPubKey,
+ };
+ use data_encoding::HEXLOWER;
+ use ed25519_dalek::Verifier as _;
+ use p256::ecdsa::{DerSignature as P256Sig, SigningKey as P256Key};
+ use rsa::sha2::{Digest as _, Sha256};
+ /// https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-none-es256
+ #[test]
+ fn es256_test_vector() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?);
+ let credential_private_key = HEXLOWER
+ .decode(b"6e68e7a58484a3264f66b77f5d6dc5bc36a47085b615c9727ab334e8c369c2ee".as_slice())
+ .unwrap();
+ let aaguid = HEXLOWER
+ .decode(b"8446ccb9ab1db374750b2367ff6f3a1f".as_slice())
+ .unwrap();
+ let credential_id = HEXLOWER
+ .decode(b"f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4".as_slice())
+ .unwrap();
+ let client_data_json = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c4535513d3d227d".as_slice()).unwrap();
+ let attestation_object = HEXLOWER.decode(b"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220".as_slice()).unwrap();
+ let key = *P256Key::from_slice(credential_private_key.as_slice())
+ .unwrap()
+ .verifying_key();
+ let enc_key = key.to_encoded_point(false);
+ let auth_attest =
+ AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0));
+ let att_obj = AttestationObject::from_data(
+ auth_attest.attestation_object_and_c_data_hash.as_slice(),
+ )?;
+ assert_eq!(
+ aaguid,
+ att_obj.data.auth_data.attested_credential_data.aaguid.0
+ );
+ assert_eq!(
+ credential_id,
+ att_obj
+ .data
+ .auth_data
+ .attested_credential_data
+ .credential_id
+ .0
+ );
+ assert!(
+ matches!(att_obj.data.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if enc_key.x().unwrap().as_slice() == pub_key.0 && enc_key.y().unwrap().as_slice() == pub_key.1)
+ );
+ assert_eq!(
+ att_obj.data.auth_data.rp_id_hash,
+ Sha256::digest(rp_id.as_ref()).as_slice()
+ );
+ assert!(att_obj.data.auth_data.flags.user_present);
+ assert!(matches!(att_obj.data.attestation, AttestationFormat::None));
+ let authenticator_data = HEXLOWER
+ .decode(
+ b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000"
+ .as_slice(),
+ )
+ .unwrap();
+ let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d".as_slice()).unwrap();
+ let signature = HEXLOWER.decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87".as_slice()).unwrap();
+ let auth_assertion =
+ AuthenticatorAssertion::new(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(),
+ Sha256::digest(rp_id.as_ref()).as_slice()
+ );
+ assert!(auth_data.flags().user_present);
+ assert!(match att_obj.data.auth_data.flags.backup {
+ Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible),
+ Backup::Eligible => !matches!(auth_data.flags().backup, Backup::NotEligible),
+ Backup::Exists => matches!(auth_data.flags().backup, Backup::Exists),
+ });
+ let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap();
+ let mut msg = auth_assertion.authenticator_data().to_owned();
+ msg.extend_from_slice(Sha256::digest(auth_assertion.client_data_json()).as_slice());
+ assert!(key.verify(msg.as_slice(), &sig).is_ok());
+ Ok(())
+ }
+ /// https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-packed-self-es256
+ #[test]
+ fn es256_self_attest_test_vector() -> Result<(), AggErr> {
+ let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?);
+ let credential_private_key = HEXLOWER
+ .decode(b"b4bbfa5d68e1693b6ef5a19a0e60ef7ee2cbcac81f7fec7006ac3a21e0c5116a".as_slice())
+ .unwrap();
+ let aaguid = HEXLOWER
+ .decode(b"df850e09db6afbdfab51697791506cfc".as_slice())
+ .unwrap();
+ let credential_id = HEXLOWER
+ .decode(b"455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c".as_slice())
+ .unwrap();
+ let client_data_json = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f3078594856673d3d227d".as_slice()).unwrap();
+ let attestation_object = HEXLOWER.decode(b"a363666d74667061636b65646761747453746d74a263616c67266373696758483046022100ae045923ded832b844cae4d5fc864277c0dc114ad713e271af0f0d371bd3ac540221009077a088ed51a673951ad3ba2673d5029bab65b64f4ea67b234321f86fcfac5d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2".as_slice()).unwrap();
+ let key = *P256Key::from_slice(credential_private_key.as_slice())
+ .unwrap()
+ .verifying_key();
+ let enc_key = key.to_encoded_point(false);
+ let auth_attest =
+ AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0));
+ let (att_obj, auth_idx) = AttestationObject::parse_data(auth_attest.attestation_object())?;
+ assert_eq!(aaguid, att_obj.auth_data.attested_credential_data.aaguid.0);
+ assert_eq!(
+ credential_id,
+ att_obj.auth_data.attested_credential_data.credential_id.0
+ );
+ assert!(
+ matches!(att_obj.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if enc_key.x().unwrap().as_slice() == pub_key.0 && enc_key.y().unwrap().as_slice() == pub_key.1)
+ );
+ assert_eq!(
+ att_obj.auth_data.rp_id_hash,
+ Sha256::digest(rp_id.as_ref()).as_slice()
+ );
+ assert!(att_obj.auth_data.flags.user_present);
+ assert!(match att_obj.attestation {
+ AttestationFormat::None => false,
+ AttestationFormat::Packed(attest) => {
+ match attest.signature {
+ Sig::Ed25519(_) | Sig::P384(_) | Sig::Rs256(_) => false,
+ Sig::P256(sig) => {
+ let s = P256Sig::from_bytes(sig).unwrap();
+ key.verify(
+ &auth_attest.attestation_object_and_c_data_hash[auth_idx..],
+ &s,
+ )
+ .is_ok()
+ }
+ }
+ }
+ });
+ let authenticator_data = HEXLOWER
+ .decode(
+ b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000"
+ .as_slice(),
+ )
+ .unwrap();
+ let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d".as_slice()).unwrap();
+ let signature = HEXLOWER.decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744".as_slice()).unwrap();
+ let auth_assertion =
+ AuthenticatorAssertion::new(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(),
+ Sha256::digest(rp_id.as_ref()).as_slice()
+ );
+ assert!(auth_data.flags().user_present);
+ assert!(match att_obj.auth_data.flags.backup {
+ Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible),
+ Backup::Eligible | Backup::Exists =>
+ !matches!(auth_data.flags().backup, Backup::NotEligible),
+ });
+ let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap();
+ let mut msg = auth_assertion.authenticator_data().to_owned();
+ msg.extend_from_slice(Sha256::digest(auth_assertion.client_data_json()).as_slice());
+ assert!(key.verify(msg.as_slice(), &sig).is_ok());
+ Ok(())
+ }
+}
diff --git a/src/response/register/bin.rs b/src/response/register/bin.rs
@@ -0,0 +1,598 @@
+#![expect(
+ clippy::unseparated_literal_suffix,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+use super::{
+ super::super::bin::{
+ Decode, DecodeBuffer, EncDecErr, Encode, EncodeBuffer, EncodeBufferFallible as _,
+ },
+ Aaguid, Attestation, AuthenticationExtensionsPrfOutputs, AuthenticatorAttachment,
+ AuthenticatorExtensionOutputMetadata, AuthenticatorExtensionOutputStaticState, Backup,
+ ClientExtensionsOutputs, CompressedP256PubKey, CompressedP384PubKey, CompressedPubKey,
+ CredentialPropertiesOutput, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, Metadata,
+ ResidentKeyRequirement, RsaPubKey, StaticState, UncompressedP256PubKey, UncompressedP384PubKey,
+ UncompressedPubKey,
+};
+use core::{
+ convert::Infallible,
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+use p256::{
+ elliptic_curve::{generic_array::typenum::ToInt as _, Curve},
+ NistP256,
+};
+use p384::NistP384;
+impl EncodeBuffer for CredentialProtectionPolicy {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::None => 0u8,
+ Self::UserVerificationOptional => 1,
+ Self::UserVerificationOptionalWithCredentialIdList => 2,
+ Self::UserVerificationRequired => 3,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for CredentialProtectionPolicy {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Ok(Self::None),
+ 1 => Ok(Self::UserVerificationOptional),
+ 2 => Ok(Self::UserVerificationOptionalWithCredentialIdList),
+ 3 => Ok(Self::UserVerificationRequired),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for ResidentKeyRequirement {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::Required => 0u8,
+ Self::Discouraged => 1,
+ Self::Preferred => 2,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for ResidentKeyRequirement {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Ok(Self::Required),
+ 1 => Ok(Self::Discouraged),
+ 2 => Ok(Self::Preferred),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for Ed25519PubKey<&[u8]> {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ // We don't rely on `[u8]::encode_into_buffer` since
+ // we always know the `slice` has length 32; thus
+ // we want to "pretend" this is an array (i.e., don't encode the length).
+ buffer.extend_from_slice(self.0);
+ }
+}
+impl<'a> DecodeBuffer<'a> for Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ <[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>::decode_from_buffer(data).map(Self)
+ }
+}
+impl EncodeBuffer for UncompressedP256PubKey<'_> {
+ #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ /// Number of bytes the y-coordinate takes.
+ const Y_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT;
+ /// The index of the least significant byte of the y-coordinate.
+ const ODD_BYTE_INDEX: usize = Y_LEN - 1;
+ // We don't rely on `[u8]::encode_into_buffer` since
+ // we always know the `slice` has length 32; thus
+ // we want to "pretend" this is an array (i.e., don't encode the length).
+ buffer.extend_from_slice(self.0);
+ // `self.1.len() == 32` and `ODD_BYTE_INDEX == 31`, so indexing is fine.
+ (self.1[ODD_BYTE_INDEX] & 1 == 1).encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ <[u8; <NistP256 as Curve>::FieldBytesSize::INT]>::decode_from_buffer(data)
+ .and_then(|x| bool::decode_from_buffer(data).map(|y_is_odd| Self { x, y_is_odd }))
+ }
+}
+impl EncodeBuffer for UncompressedP384PubKey<'_> {
+ #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ /// Number of bytes the y-coordinate takes.
+ const Y_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT;
+ /// The index of the least significant byte of the y-coordinate.
+ const ODD_BYTE_INDEX: usize = Y_LEN - 1;
+ // We don't rely on `[u8]::encode_into_buffer` since
+ // we always know the `slice` has length 48; thus
+ // we want to "pretend" this is an array (i.e., don't encode the length).
+ buffer.extend_from_slice(self.0);
+ // `self.1.len() == 48` and `ODD_BYTE_INDEX == 47`, so indexing is fine.
+ (self.1[ODD_BYTE_INDEX] & 1 == 1).encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ // Only `array`s that implement `Default` implement `DecodeBuffer`;
+ // thus we must manually implement it.
+ let mut x = [0; <NistP384 as Curve>::FieldBytesSize::INT];
+ data.split_at_checked(x.len())
+ .ok_or(EncDecErr)
+ .and_then(|(x_slice, rem)| {
+ *data = rem;
+ bool::decode_from_buffer(data).map(|y_is_odd| {
+ x.copy_from_slice(x_slice);
+ Self { x, y_is_odd }
+ })
+ })
+ }
+}
+impl EncodeBuffer for RsaPubKey<&[u8]> {
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ // Max length is 2048, so this won't error.
+ self.0
+ .encode_into_buffer(buffer)
+ .unwrap_or_else(|_e| unreachable!("there is a bug in [u8]::encode_into_buffer"));
+ self.1.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for RsaPubKey<Vec<u8>> {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ Vec::decode_from_buffer(data).and_then(|n| {
+ u32::decode_from_buffer(data)
+ .and_then(|e| Self::try_from((n, e)).map_err(|_e| EncDecErr))
+ })
+ }
+}
+impl EncodeBuffer for UncompressedPubKey<'_> {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::Ed25519(key) => {
+ 0u8.encode_into_buffer(buffer);
+ key.encode_into_buffer(buffer);
+ }
+ Self::P256(key) => {
+ 1u8.encode_into_buffer(buffer);
+ key.encode_into_buffer(buffer);
+ }
+ Self::P384(key) => {
+ 2u8.encode_into_buffer(buffer);
+ key.encode_into_buffer(buffer);
+ }
+ Self::Rsa(key) => {
+ 3u8.encode_into_buffer(buffer);
+ key.encode_into_buffer(buffer);
+ }
+ }
+ }
+}
+impl<'a> DecodeBuffer<'a>
+ for CompressedPubKey<
+ [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
+ [u8; <NistP256 as Curve>::FieldBytesSize::INT],
+ [u8; <NistP384 as Curve>::FieldBytesSize::INT],
+ Vec<u8>,
+ >
+{
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Ed25519PubKey::decode_from_buffer(data).map(Self::Ed25519),
+ 1 => CompressedP256PubKey::decode_from_buffer(data).map(Self::P256),
+ 2 => CompressedP384PubKey::decode_from_buffer(data).map(Self::P384),
+ 3 => RsaPubKey::decode_from_buffer(data).map(Self::Rsa),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for AuthenticatorExtensionOutputStaticState {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.cred_protect.encode_into_buffer(buffer);
+ self.hmac_secret.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for AuthenticatorExtensionOutputStaticState {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ CredentialProtectionPolicy::decode_from_buffer(data).and_then(|cred_protect| {
+ Option::decode_from_buffer(data).map(|hmac_secret| Self {
+ cred_protect,
+ hmac_secret,
+ })
+ })
+ }
+}
+impl EncodeBuffer for Attestation {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ match *self {
+ Self::None => 0u8,
+ Self::Surrogate => 1,
+ }
+ .encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for Attestation {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ u8::decode_from_buffer(data).and_then(|val| match val {
+ 0 => Ok(Self::None),
+ 1 => Ok(Self::Surrogate),
+ _ => Err(EncDecErr),
+ })
+ }
+}
+impl EncodeBuffer for Aaguid<'_> {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ buffer.extend_from_slice(self.0);
+ }
+}
+/// Owned version of [`Aaguid`] that exists for [`MetadataOwned::aaguid`].
+#[derive(Clone, Copy, Debug)]
+pub struct AaguidOwned(pub [u8; super::AAGUID_LEN]);
+impl<'a: 'b, 'b> From<&'a AaguidOwned> for Aaguid<'b> {
+ #[inline]
+ fn from(value: &'a AaguidOwned) -> Self {
+ Self(value.0.as_slice())
+ }
+}
+impl<'a> DecodeBuffer<'a> for AaguidOwned {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ <[u8; super::AAGUID_LEN]>::decode_from_buffer(data).map(Self)
+ }
+}
+impl EncodeBuffer for AuthenticatorExtensionOutputMetadata {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.min_pin_length.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for AuthenticatorExtensionOutputMetadata {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ Option::decode_from_buffer(data).map(|min_pin_length| Self { min_pin_length })
+ }
+}
+impl EncodeBuffer for CredentialPropertiesOutput {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.rk.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for CredentialPropertiesOutput {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ Option::decode_from_buffer(data).map(|rk| Self { rk })
+ }
+}
+impl EncodeBuffer for AuthenticationExtensionsPrfOutputs {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.enabled.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for AuthenticationExtensionsPrfOutputs {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ bool::decode_from_buffer(data).map(|enabled| Self { enabled })
+ }
+}
+impl EncodeBuffer for ClientExtensionsOutputs {
+ fn encode_into_buffer(&self, buffer: &mut Vec<u8>) {
+ self.cred_props.encode_into_buffer(buffer);
+ self.prf.encode_into_buffer(buffer);
+ }
+}
+impl<'a> DecodeBuffer<'a> for ClientExtensionsOutputs {
+ type Err = EncDecErr;
+ fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> {
+ Option::decode_from_buffer(data).and_then(|cred_props| {
+ Option::decode_from_buffer(data).map(|prf| Self { cred_props, prf })
+ })
+ }
+}
+impl Encode for Metadata<'_> {
+ type Output<'a>
+ = Vec<u8>
+ where
+ Self: 'a;
+ type Err = Infallible;
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ // Length of the anticipated most common output:
+ // * 1 for `Attestation`
+ // * 16 for `Aaguid`.
+ // * 1 or 2 for `AuthenticatorExtensionOutputMetadata` where we assume 1 is the most common
+ // * 2–5 for `ClientExtensionsOutputs` where we assume 2 is the most common
+ // * 1 for `ResidentKeyRequirement`
+ let mut buffer = Vec::with_capacity(1 + 16 + 1 + 2 + 1);
+ self.attestation.encode_into_buffer(&mut buffer);
+ self.aaguid.encode_into_buffer(&mut buffer);
+ self.extensions.encode_into_buffer(&mut buffer);
+ self.client_extension_results
+ .encode_into_buffer(&mut buffer);
+ self.resident_key.encode_into_buffer(&mut buffer);
+ Ok(buffer)
+ }
+}
+/// Owned version of [`Metadata`] that exists to [`Self::decode`] the output of [`Metadata::encode`].
+#[derive(Debug)]
+pub struct MetadataOwned {
+ /// [`Metadata::attestation`].
+ pub attestation: Attestation,
+ /// [`Metadata::aaguid`].
+ pub aaguid: AaguidOwned,
+ /// [`Metadata::extensions`].
+ pub extensions: AuthenticatorExtensionOutputMetadata,
+ /// [`Metadata::client_extension_results`].
+ pub client_extension_results: ClientExtensionsOutputs,
+ /// [`Metadata::resident_key`].
+ pub resident_key: ResidentKeyRequirement,
+}
+impl<'a: 'b, 'b> From<&'a MetadataOwned> for Metadata<'b> {
+ #[inline]
+ fn from(value: &'a MetadataOwned) -> Self {
+ Self {
+ attestation: value.attestation,
+ aaguid: (&value.aaguid).into(),
+ extensions: value.extensions,
+ client_extension_results: value.client_extension_results,
+ resident_key: value.resident_key,
+ }
+ }
+}
+/// Error returned from [`MetadataOwned::decode`].
+#[derive(Clone, Copy, Debug)]
+pub enum DecodeMetadataOwnedErr {
+ /// Variant returned when [`MetadataOwned::attestation`] could not be decoded.
+ Attestation,
+ /// Variant returned when [`MetadataOwned::aaguid`] could not be decoded.
+ Aaguid,
+ /// Variant returned when [`MetadataOwned::extensions`] could not be decoded.
+ Extensions,
+ /// Variant returned when [`MetadataOwned::client_extension_results`] could not be decoded.
+ ClientExtensionResults,
+ /// Variant returned when [`MetadataOwned::resident_key`] could not be decoded.
+ ResidentKey,
+ /// Variant returned when [`MetadataOwned`] was decoded with trailing data.
+ TrailingData,
+}
+impl Display for DecodeMetadataOwnedErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::Attestation => "attestation could not be decoded",
+ Self::Aaguid => "aaguid could not be decoded",
+ Self::Extensions => "extensions could not be decoded",
+ Self::ClientExtensionResults => "client_extension_results could not be decoded",
+ Self::ResidentKey => "resident_key could not be decoded",
+ Self::TrailingData => "trailing data existed after decoding MetadataOwned",
+ })
+ }
+}
+impl Error for DecodeMetadataOwnedErr {}
+impl Decode for MetadataOwned {
+ type Input<'a> = &'a [u8];
+ type Err = DecodeMetadataOwnedErr;
+ #[inline]
+ fn decode(mut input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ Attestation::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeMetadataOwnedErr::Attestation)
+ .and_then(|attestation| {
+ AaguidOwned::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeMetadataOwnedErr::Aaguid)
+ .and_then(|aaguid| {
+ AuthenticatorExtensionOutputMetadata::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeMetadataOwnedErr::Extensions)
+ .and_then(|extensions| {
+ ClientExtensionsOutputs::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeMetadataOwnedErr::ClientExtensionResults)
+ .and_then(|client_extension_results| {
+ ResidentKeyRequirement::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeMetadataOwnedErr::ResidentKey)
+ .and_then(|resident_key| {
+ if input.is_empty() {
+ Ok(Self {
+ attestation,
+ aaguid,
+ extensions,
+ client_extension_results,
+ resident_key,
+ })
+ } else {
+ Err(DecodeMetadataOwnedErr::TrailingData)
+ }
+ })
+ })
+ })
+ })
+ })
+ }
+}
+impl Encode for StaticState<UncompressedPubKey<'_>> {
+ type Output<'a>
+ = Vec<u8>
+ where
+ Self: 'a;
+ type Err = Infallible;
+ /// Transforms `self` into a `Vec` that can subsequently be [`StaticState::decode`]d into a [`StaticState`] of
+ /// [`CompressedPubKey`].
+ #[expect(
+ clippy::arithmetic_side_effects,
+ reason = "comment justifies its correctness"
+ )]
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ let mut buffer = Vec::with_capacity(
+ // The maximum value is 1 + 2 + 2048 + 4 + 1 + 1 + 1 = 2058 so overflow cannot happen.
+ // `key.0.len() <= MAX_RSA_N_BYTES` which is 2048.
+ match self.credential_public_key {
+ UncompressedPubKey::Ed25519(_) => 33,
+ UncompressedPubKey::P256(_) => 34,
+ UncompressedPubKey::P384(_) => 50,
+ UncompressedPubKey::Rsa(key) => 1 + 2 + key.0.len() + 4,
+ } + 1
+ + 1
+ + usize::from(self.extensions.hmac_secret.is_some()),
+ );
+ self.credential_public_key.encode_into_buffer(&mut buffer);
+ self.extensions.encode_into_buffer(&mut buffer);
+ Ok(buffer)
+ }
+}
+/// Error returned from [`StaticState::decode`].
+#[derive(Clone, Copy, Debug)]
+pub enum DecodeStaticStateErr {
+ /// Variant returned when [`StaticState::credential_public_key`] could not be decoded.
+ CredentialPublicKey,
+ /// Variant returned when [`StaticState::extensions`] could not be decoded.
+ Extensions,
+ /// Variant returned when there was trailing data after decoding a [`StaticState`].
+ TrailingData,
+}
+impl Display for DecodeStaticStateErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::CredentialPublicKey => "credential_public_key could not be decoded",
+ Self::Extensions => "extensions could not be decoded",
+ Self::TrailingData => "there was trailing data after decoding a StaticState",
+ })
+ }
+}
+impl Error for DecodeStaticStateErr {}
+impl Decode
+ for StaticState<
+ CompressedPubKey<
+ [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
+ [u8; <NistP256 as Curve>::FieldBytesSize::INT],
+ [u8; <NistP384 as Curve>::FieldBytesSize::INT],
+ Vec<u8>,
+ >,
+ >
+{
+ type Input<'a> = &'a [u8];
+ type Err = DecodeStaticStateErr;
+ /// Interprets `input` as the [`StaticState::Output`] of [`StaticState::encode`].
+ #[inline]
+ fn decode(mut input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ CompressedPubKey::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeStaticStateErr::CredentialPublicKey)
+ .and_then(|credential_public_key| {
+ AuthenticatorExtensionOutputStaticState::decode_from_buffer(&mut input)
+ .map_err(|_e| DecodeStaticStateErr::Extensions)
+ .and_then(|extensions| {
+ if input.is_empty() {
+ Ok(Self {
+ credential_public_key,
+ extensions,
+ })
+ } else {
+ Err(DecodeStaticStateErr::TrailingData)
+ }
+ })
+ })
+ }
+}
+impl Encode for DynamicState {
+ type Output<'a>
+ = [u8; 7]
+ where
+ Self: 'a;
+ type Err = Infallible;
+ #[expect(
+ clippy::little_endian_bytes,
+ reason = "need cross-platform correctness"
+ )]
+ #[inline]
+ fn encode(&self) -> Result<Self::Output<'_>, Self::Err> {
+ let mut buffer = [
+ u8::from(self.user_verified),
+ match self.backup {
+ Backup::NotEligible => 0,
+ Backup::Eligible => 1,
+ Backup::Exists => 2,
+ },
+ 0,
+ 0,
+ 0,
+ 0,
+ match self.authenticator_attachment {
+ AuthenticatorAttachment::None => 0,
+ AuthenticatorAttachment::Platform => 1,
+ AuthenticatorAttachment::CrossPlatform => 2,
+ },
+ ];
+ buffer[2..6].copy_from_slice(self.sign_count.to_le_bytes().as_slice());
+ Ok(buffer)
+ }
+}
+/// Error returned from [`DynamicState::decode`].
+#[derive(Clone, Copy, Debug)]
+pub enum DecodeDynamicStateErr {
+ /// Variant returned when [`DynamicState::user_verified`] could not be decoded.
+ UserVerified,
+ /// Variant returned when [`DynamicState::backup`] could not be decoded.
+ Backup,
+ /// Variant returned when [`DynamicState::sign_count`] could not be decoded.
+ SignCount,
+ /// Variant returned when [`DynamicState::authenticator_attachment`] could not be decoded.
+ AuthenticatorAttachment,
+}
+impl Display for DecodeDynamicStateErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::UserVerified => "user_verified could not be decoded",
+ Self::Backup => "backup could not be decoded",
+ Self::SignCount => "sign_count could not be decoded",
+ Self::AuthenticatorAttachment => "authenticator_attachment could not be decoded",
+ })
+ }
+}
+impl Error for DecodeDynamicStateErr {}
+impl Decode for DynamicState {
+ type Input<'a> = [u8; 7];
+ type Err = DecodeDynamicStateErr;
+ #[expect(
+ clippy::panic_in_result_fn,
+ reason = "want to crash when there is a bug"
+ )]
+ #[inline]
+ fn decode(input: Self::Input<'_>) -> Result<Self, Self::Err> {
+ let mut buffer = input.as_slice();
+ bool::decode_from_buffer(&mut buffer)
+ .map_err(|_e| DecodeDynamicStateErr::UserVerified)
+ .and_then(|user_verified| {
+ Backup::decode_from_buffer(&mut buffer)
+ .map_err(|_e| DecodeDynamicStateErr::Backup)
+ .and_then(|backup| {
+ u32::decode_from_buffer(&mut buffer)
+ .map_err(|_e| DecodeDynamicStateErr::SignCount)
+ .and_then(|sign_count| {
+ AuthenticatorAttachment::decode_from_buffer(&mut buffer)
+ .map_err(|_e| DecodeDynamicStateErr::AuthenticatorAttachment)
+ .map(|authenticator_attachment| {
+ assert!(
+ buffer.is_empty(),
+ "there is a bug in DynamicState::decode"
+ );
+ Self {
+ user_verified,
+ backup,
+ sign_count,
+ authenticator_attachment,
+ }
+ })
+ })
+ })
+ })
+ }
+}
diff --git a/src/response/register/error.rs b/src/response/register/error.rs
@@ -0,0 +1,624 @@
+#[cfg(feature = "serde_relaxed")]
+use super::super::SerdeJsonErr;
+#[cfg(doc)]
+use super::{
+ super::super::{
+ request::{
+ register::{
+ AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, Extension,
+ PublicKeyCredentialCreationOptions, RegistrationServerState,
+ RegistrationVerificationOptions,
+ },
+ BackupReq, CredentialMediationRequirement, UserVerificationRequirement,
+ },
+ RegisteredCredential,
+ },
+ Aaguid, Attestation, AttestationObject, AttestedCredentialData, AuthenticatorAttachment,
+ AuthenticatorAttestation, AuthenticatorData, AuthenticatorExtensionOutput, Backup,
+ ClientExtensionsOutputs, CollectedClientData, CompressedP256PubKey, CompressedP384PubKey,
+ Ed25519PubKey, Ed25519Signature, Flag, Metadata, PackedAttestation, RsaPubKey,
+ UncompressedP256PubKey, UncompressedP384PubKey, UncompressedPubKey, MAX_RSA_N_BYTES, MIN_RSA_E,
+ MIN_RSA_N_BYTES,
+};
+use super::{
+ super::{
+ super::{request::register::CredProtect, CredentialErr},
+ error::{CollectedClientDataErr, CredentialIdErr},
+ AuthRespErr, AuthenticatorDataErr as AuthDataErr, CeremonyErr,
+ },
+ CredentialProtectionPolicy,
+};
+use core::{
+ convert::Infallible,
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
+/// Error returned from [`Ed25519PubKey::try_from`] when the `slice` is not 32-bytes in length.
+#[derive(Clone, Copy, Debug)]
+pub struct Ed25519PubKeyErr;
+impl Display for Ed25519PubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("the Ed25519 public key is not 32-bytes in length")
+ }
+}
+impl Error for Ed25519PubKeyErr {}
+/// Error returned from [`UncompressedP256PubKey::try_from`].
+#[derive(Clone, Copy, Debug)]
+pub enum UncompressedP256PubKeyErr {
+ /// Variant returned when the x-coordinate is not 32-bytes in length.
+ X,
+ /// Variant returned when the y-coordinate is not 32-bytes in length.
+ Y,
+}
+impl Display for UncompressedP256PubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::X => "the P-256 public key x-coordinate is not 32-bytes in length",
+ Self::Y => "the P-256 public key y-coordinate is not 32-bytes in length",
+ })
+ }
+}
+impl Error for UncompressedP256PubKeyErr {}
+/// Error returned from [`CompressedP256PubKey::try_from`] when the x-coordinate
+/// is not exactly 32 bytes in length.
+#[derive(Clone, Copy, Debug)]
+pub struct CompressedP256PubKeyErr;
+impl Display for CompressedP256PubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("the compressed P-256 public key x-coordinate is not 32-bytes in length")
+ }
+}
+impl Error for CompressedP256PubKeyErr {}
+/// Error returned from [`UncompressedP384PubKey::try_from`].
+#[derive(Clone, Copy, Debug)]
+pub enum UncompressedP384PubKeyErr {
+ /// Variant returned when the x-coordinate is not 48-bytes in length.
+ X,
+ /// Variant returned when the y-coordinate is not 48-bytes in length.
+ Y,
+}
+impl Display for UncompressedP384PubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::X => "the P-384 public key x-coordinate is not 48-bytes in length",
+ Self::Y => "the P-384 public key y-coordinate is not 48-bytes in length",
+ })
+ }
+}
+impl Error for UncompressedP384PubKeyErr {}
+/// Error returned from [`CompressedP384PubKey::try_from`] when the x-coordinate
+/// is not exactly 48 bytes in length.
+#[derive(Clone, Copy, Debug)]
+pub struct CompressedP384PubKeyErr;
+impl Display for CompressedP384PubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("the compressed P-384 public key x-coordinate is not 48-bytes in length")
+ }
+}
+impl Error for CompressedP384PubKeyErr {}
+/// Error returned from [`RsaPubKey::try_from`].
+#[derive(Clone, Copy, Debug)]
+pub enum RsaPubKeyErr {
+ /// Variant returned when the modulus has length less than [`MIN_RSA_N_BYTES`] or greater than
+ /// [`MAX_RSA_N_BYTES`].
+ N,
+ /// Variant returned when the exponent is less than [`MIN_RSA_E`].
+ E,
+}
+impl Display for RsaPubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::N => "the RSA public key modulus had length less than 256 or greater than 2048",
+ Self::E => "the RSA public key exponent was less than 3",
+ })
+ }
+}
+impl Error for RsaPubKeyErr {}
+/// Error returned when an alleged public key is not valid.
+#[derive(Clone, Copy, Debug)]
+pub enum PubKeyErr {
+ /// Error when [`Ed25519PubKey`] is not valid.
+ Ed25519,
+ /// Error when [`UncompressedP256PubKey`] or [`CompressedP256PubKey`] is not valid.
+ P256,
+ /// Error when [`UncompressedP384PubKey`] or [`CompressedP384PubKey`] is not valid.
+ P384,
+ /// Error when [`RsaPubKey`] is not valid.
+ Rsa,
+}
+impl Display for PubKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::Ed25519 => "Ed25519 public key is invalid",
+ Self::P256 => "P-256 public key is invalid",
+ Self::P384 => "P-384 public key is invalid",
+ Self::Rsa => "RSA public key is invalid",
+ })
+ }
+}
+impl Error for PubKeyErr {}
+/// Error returned from [`Ed25519Signature::try_from`].
+#[derive(Clone, Copy, Debug)]
+pub struct Ed25519SignatureErr;
+impl Display for Ed25519SignatureErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("the Ed25519 signature is not 64-bytes in length")
+ }
+}
+impl Error for Ed25519SignatureErr {}
+/// Error returned from [`Aaguid::try_from`] when the slice is not exactly
+/// 16-bytes in length.
+#[derive(Clone, Copy, Debug)]
+pub struct AaguidErr;
+impl Display for AaguidErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str("the AAGUID is not 16-bytes in length")
+ }
+}
+impl Error for AaguidErr {}
+/// Error returned in [`AuthenticatorDataErr::AuthenticatorExtension`].
+#[derive(Clone, Copy, Debug)]
+pub enum AuthenticatorExtensionOutputErr {
+ /// The `slice` had an invalid length.
+ Len,
+ /// The first byte did not represent a map of one, two, or three key pairs.
+ CborHeader,
+ /// `credProtect` had an invalid value.
+ CredProtectValue,
+ /// `hmac-secret` had an invalid value.
+ HmacSecretValue,
+ /// `minPinLength` had an invalid value.
+ MinPinLengthValue,
+ /// Fewer extensions existed than expected.
+ Missing,
+}
+impl Display for AuthenticatorExtensionOutputErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::Len => "CBOR authenticator extensions had an invalid length",
+ Self::CborHeader => "CBOR authenticator extensions did not represent a map of one, two, or three key pairs",
+ Self::CredProtectValue => "CBOR authenticator extension 'credProtect' had an invalid value",
+ Self::HmacSecretValue => "CBOR authenticator extension 'hmac-secret' had an invalid value",
+ Self::MinPinLengthValue => "CBOR authenticator extension 'minPinLength' had an invalid value",
+ Self::Missing => "CBOR authenticator extensions had fewer extensions than expected",
+ })
+ }
+}
+impl Error for AuthenticatorExtensionOutputErr {}
+/// Error returned in [`AttestedCredentialDataErr::CoseKey`].
+#[derive(Clone, Copy, Debug)]
+pub enum CoseKeyErr {
+ /// The `slice` had an invalid length.
+ Len,
+ /// The COSE Key type was not `OKP`, `EC2`, or `RSA`.
+ CoseKeyType,
+ /// The `slice` was malformed and did not conform to an Ed25519 public key encoded as a COSE Key per
+ /// [RFC 9052](https://www.rfc-editor.org/rfc/rfc9052) and [RFC 9053](https://www.rfc-editor.org/rfc/rfc9053).
+ Ed25519CoseEncoding,
+ /// The `slice` was malformed and did not conform to an ECDSA public key based on curve P-256 and SHA-256
+ /// encoded as a COSE Key per [RFC 9052](https://www.rfc-editor.org/rfc/rfc9052) and
+ /// [RFC 9053](https://www.rfc-editor.org/rfc/rfc9053).
+ P256CoseEncoding,
+ /// The `slice` was malformed and did not conform to an ECDSA public key based on curve P-384 and SHA-384
+ /// encoded as a COSE Key per [RFC 9052](https://www.rfc-editor.org/rfc/rfc9052) and
+ /// [RFC 9053](https://www.rfc-editor.org/rfc/rfc9053).
+ P384CoseEncoding,
+ /// The `slice` was malformed and did not conform to an RSASSA-PKCS1-v1.5 public key using SHA-256 encoded as a
+ /// COSE Key per [RFC 8230](https://www.rfc-editor.org/rfc/rfc8230.html).
+ RsaCoseEncoding,
+ /// The RSA public key exponent is too large.
+ RsaExponentTooLarge,
+ /// The RSA public key was invalid.
+ RsaPubKey(RsaPubKeyErr),
+}
+impl Display for CoseKeyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Len => f.write_str("COSE key data had an invalid length"),
+ Self::CoseKeyType => f.write_str("COSE key type was not 'OKP', 'EC2', or 'RSA'"),
+ Self::Ed25519CoseEncoding => f.write_str("Ed25519 COSE key was not encoded correctly"),
+ Self::P256CoseEncoding => {
+ f.write_str("ECDSA with P-256 and SHA-256 COSE key was not encoded correctly")
+ }
+ Self::P384CoseEncoding => {
+ f.write_str("ECDSA with P-384 and SHA-384 COSE key was not encoded correctly")
+ }
+ Self::RsaCoseEncoding => {
+ f.write_str("RSASSA-PKCS1-v1.5 using SHA-256 COSE key was not encoded correctly")
+ }
+ Self::RsaExponentTooLarge => f.write_str("RSA public key exponent is too large"),
+ Self::RsaPubKey(err) => err.fmt(f),
+ }
+ }
+}
+impl Error for CoseKeyErr {}
+/// Error returned in [`AuthenticatorDataErr::AttestedCredential`].
+#[derive(Clone, Copy, Debug)]
+pub enum AttestedCredentialDataErr {
+ /// The `slice` had an invalid length.
+ Len,
+ /// Error when the claimed credential ID length is not valid.
+ CredentialId(CredentialIdErr),
+ /// Error related to the credential public key.
+ CoseKey(CoseKeyErr),
+}
+impl Display for AttestedCredentialDataErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Len => f.write_str("attested credential data had an invalid length"),
+ Self::CredentialId(err) => err.fmt(f),
+ Self::CoseKey(err) => err.fmt(f),
+ }
+ }
+}
+impl Error for AttestedCredentialDataErr {}
+/// Error returned from [`AuthenticatorData::try_from`].
+#[derive(Clone, Copy, Debug)]
+pub enum AuthenticatorDataErr {
+ /// The `slice` had an invalid length.
+ Len,
+ /// Bit 1 in [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags) is not 0.
+ FlagsBit1Not0,
+ /// Bit 5 in [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags) is not 0.
+ FlagsBit5Not0,
+ /// [AT](https://www.w3.org/TR/webauthn-3/#authdata-flags-at) bit was 0.
+ AttestedCredentialDataNotIncluded,
+ /// [BE](https://www.w3.org/TR/webauthn-3/#authdata-flags-be) and
+ /// [BS](https://www.w3.org/TR/webauthn-3/#authdata-flags-bs) bits were 0 and 1 respectively.
+ BackupWithoutEligibility,
+ /// Error returned when [`AttestedCredentialData`] is malformed.
+ AttestedCredential(AttestedCredentialDataErr),
+ /// Error returned when [`AuthenticatorExtensionOutput`] is malformed.
+ AuthenticatorExtension(AuthenticatorExtensionOutputErr),
+ /// [ED](https://www.w3.org/TR/webauthn-3/#authdata-flags-ed) bit was 0, but
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) existed.
+ NoExtensionBitWithData,
+ /// [ED](https://www.w3.org/TR/webauthn-3/#authdata-flags-ed) bit was 1, but
+ /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) did not exist.
+ ExtensionBitWithoutData,
+ /// There was data remaining that could not be deserialized.
+ TrailingData,
+}
+impl Display for AuthenticatorDataErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Len => AuthDataErr::<
+ Infallible,
+ AttestedCredentialDataErr,
+ AuthenticatorExtensionOutputErr,
+ >::Len
+ .fmt(f),
+ Self::FlagsBit1Not0 => AuthDataErr::<
+ Infallible,
+ AttestedCredentialDataErr,
+ AuthenticatorExtensionOutputErr,
+ >::FlagsBit1Not0
+ .fmt(f),
+ Self::FlagsBit5Not0 => AuthDataErr::<
+ Infallible,
+ AttestedCredentialDataErr,
+ AuthenticatorExtensionOutputErr,
+ >::FlagsBit5Not0
+ .fmt(f),
+ Self::AttestedCredentialDataNotIncluded => {
+ f.write_str("attested credential data was not included")
+ }
+ Self::BackupWithoutEligibility => AuthDataErr::<
+ Infallible,
+ AttestedCredentialDataErr,
+ AuthenticatorExtensionOutputErr,
+ >::BackupWithoutEligibility
+ .fmt(f),
+ Self::AttestedCredential(err) => err.fmt(f),
+ Self::AuthenticatorExtension(err) => err.fmt(f),
+ Self::NoExtensionBitWithData => AuthDataErr::<
+ Infallible,
+ AttestedCredentialDataErr,
+ AuthenticatorExtensionOutputErr,
+ >::NoExtensionBitWithData
+ .fmt(f),
+ Self::ExtensionBitWithoutData => AuthDataErr::<
+ Infallible,
+ AttestedCredentialDataErr,
+ AuthenticatorExtensionOutputErr,
+ >::ExtensionBitWithoutData
+ .fmt(f),
+ Self::TrailingData => AuthDataErr::<
+ Infallible,
+ AttestedCredentialDataErr,
+ AuthenticatorExtensionOutputErr,
+ >::TrailingData
+ .fmt(f),
+ }
+ }
+}
+impl Error for AuthenticatorDataErr {}
+/// Error returned in [`AttestationObjectErr::Attestation`].
+#[derive(Clone, Copy, Debug)]
+pub enum AttestationErr {
+ /// The `slice` had an invalid length.
+ Len,
+ /// The attestation format does not exist.
+ MissingFormat,
+ /// The attestation format is not supported.
+ UnsupportedFormat,
+ /// The attestation statement does not exist.
+ MissingStatement,
+ /// [None](https://www.w3.org/TR/webauthn-3/#sctn-none-attestation) has the wrong format.
+ NoneFormat,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) was not a map of two or three key-value pairs.
+ PackedFormat,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) did not have an algorithm.
+ PackedFormatMissingAlg,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) had an unsupported algorithm.
+ PackedFormatUnsupportedAlg,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) did not have a signature.
+ PackedFormatMissingSig,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) Ed25519 signature CBOR was invalid.
+ PackedFormatCborEd25519Signature,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) P-256 signature CBOR was invalid.
+ PackedFormatCborP256Signature,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) P-256 signature was invalid.
+ PackedFormatP256,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) P-384 signature CBOR was invalid.
+ PackedFormatCborP384Signature,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) P-384 signature was invalid.
+ PackedFormatP384,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) RS256 signature CBOR was invalid.
+ PackedFormatCborRs256Signature,
+ /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) RS256 signature was invalid.
+ PackedFormatRs256,
+}
+impl Display for AttestationErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(match *self {
+ Self::Len => "CBOR attestation had an invalid length",
+ Self::MissingFormat => "CBOR attestation did not have an attestation format",
+ Self::UnsupportedFormat => "CBOR attestation format is not supported",
+ Self::MissingStatement => "CBOR attestation did not have an attestation statement",
+ Self::NoneFormat => "CBOR attestation had the wrong format for the none attestation",
+ Self::PackedFormat => "CBOR attestation had the wrong number of key-value pairs for the packed attestation",
+ Self::PackedFormatMissingAlg => "CBOR attestation did not have an algorithm for the packed attestation",
+ Self::PackedFormatUnsupportedAlg => "CBOR attestation had an unsupported algorithm for the packed attestation",
+ Self::PackedFormatMissingSig => "CBOR attestation did not have a signature for the packed attestation",
+ Self::PackedFormatCborEd25519Signature => "CBOR attestation Ed25519 signature had the wrong CBOR format for the packed attestation",
+ Self::PackedFormatCborP256Signature => "CBOR attestation P-256 signature had the wrong CBOR format for the packed attestation",
+ Self::PackedFormatP256 => "CBOR attestation P-256 signature was invalid for the packed attestation",
+ Self::PackedFormatCborP384Signature => "CBOR attestation P-384 signature had the wrong CBOR format for the packed attestation",
+ Self::PackedFormatP384 => "CBOR attestation P-384 signature was invalid for the packed attestation",
+ Self::PackedFormatCborRs256Signature => "CBOR attestation RS256 signature had the wrong CBOR format for the packed attestation",
+ Self::PackedFormatRs256 => "CBOR attestation RS256 signature was invalid for the packed attestation",
+ })
+ }
+}
+impl Error for AttestationErr {}
+/// Error returned by [`AttestationObject::try_from`].
+#[derive(Clone, Copy, Debug)]
+pub enum AttestationObjectErr {
+ /// The `slice` had an invalid length.
+ Len,
+ /// The `slice` was not a CBOR map with three key-value pairs.
+ NotAMapOf3,
+ /// Error when [`PackedAttestation::signature`] does not match the type of
+ /// [`AttestedCredentialData::credential_public_key`] when self attestation
+ /// is used.
+ SelfAttestationAlgorithmMismatch,
+ /// The third key was not "authData".
+ MissingAuthData,
+ /// `authData` did not have a byte string data type.
+ AuthDataType,
+ /// `authData` length with additional info 24 or 25 did not have a conforming length.
+ AuthDataLenInfo,
+ /// Error related to the encoding of [`Attestation`].
+ Attestation(AttestationErr),
+ /// [`AuthenticatorData`] length did not match the length encoded in the CBOR data.
+ CborAuthDataLenMismatch,
+ /// Error from [`AuthenticatorData::try_from`].
+ AuthData(AuthenticatorDataErr),
+}
+impl Display for AttestationObjectErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Len => f.write_str("CBOR attestation object had an invalid length"),
+ Self::NotAMapOf3 => {
+ f.write_str("CBOR attestation object was not a map of three key-value pairs")
+ }
+ Self::MissingAuthData => {
+ f.write_str("CBOR attestation object did not have 'authData' as its third key")
+ }
+ Self::AuthDataType => {
+ f.write_str("CBOR attestation object authenticator data invalid type")
+ }
+ Self::AuthDataLenInfo => f.write_str(
+ "CBOR attestation object authenticator data had an invalid encoded length",
+ ),
+ Self::Attestation(err) => err.fmt(f),
+ Self::CborAuthDataLenMismatch => f.write_str(
+ "CBOR attestation object authenticator data length did not match the CBOR value",
+ ),
+ Self::AuthData(err) => err.fmt(f),
+ Self::SelfAttestationAlgorithmMismatch => f.write_str("CBOR attestation object packed attestation contained a self attestation whose format did not match the type of the public key"),
+ }
+ }
+}
+impl Error for AttestationObjectErr {}
+/// Error in [`RegCeremonyErr::Extension`].
+#[derive(Clone, Copy, Debug)]
+pub enum ExtensionErr {
+ /// [`ClientExtensionsOutputs::cred_props`] was sent from the client but was not supposed to be.
+ ForbiddenCredProps,
+ /// [`ClientExtensionsOutputs::prf`] was sent from the client but was not supposed to be.
+ ForbiddenPrf,
+ /// [`AuthenticatorExtensionOutput::cred_protect`] was sent from the client but was not supposed to be.
+ ForbiddenCredProtect,
+ /// [`AuthenticatorExtensionOutput::hmac_secret`] was sent from the client but was not supposed to be.
+ ForbiddenHmacSecret,
+ /// [`AuthenticatorExtensionOutput::min_pin_length`] was sent from the client but was not supposed to be.
+ ForbiddenMinPinLength,
+ /// [`Extension::cred_props`] was requested, but the required response was not sent back.
+ MissingCredProps,
+ /// [`Extension::prf`] was requested, but the required response was not sent back.
+ MissingPrf,
+ /// [`Extension::cred_protect`] was requested, but the required response was not sent back.
+ MissingCredProtect,
+ /// [`Extension::prf`] was requested, but the required response was not sent back.
+ MissingHmacSecret,
+ /// [`Extension::min_pin_length`] was requested, but the required response was not sent back.
+ MissingMinPinLength,
+ /// [`Extension::cred_protect`] was requested with the first policy, but the second policy was sent back.
+ InvalidCredProtectValue(CredProtect, CredentialProtectionPolicy),
+ /// [`Extension::prf`] was requested, but
+ /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled) was `false`.
+ InvalidPrfValue,
+ /// [`Extension::prf`] was requested, but
+ /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension)
+ /// was `false`.
+ InvalidHmacSecretValue,
+ /// [`Extension::min_pin_length`] was requested, but
+ /// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension)
+ /// was sent set to the second value which is strictly less than the required first value.
+ InvalidMinPinLength(u8, u8),
+}
+impl Display for ExtensionErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::ForbiddenCredProps => {
+ f.write_str("credProps was sent from the client, but it is not allowed")
+ }
+ Self::ForbiddenPrf => {
+ f.write_str("prf info was sent from the client, but it is not allowed")
+ }
+ Self::ForbiddenCredProtect => {
+ f.write_str("credProtect was sent from the client, but it is not allowed")
+ }
+ Self::ForbiddenHmacSecret => {
+ f.write_str("hmac-secret info was sent from the client, but it is not allowed")
+ }
+ Self::ForbiddenMinPinLength => {
+ f.write_str("minPinLength info was sent from the client, but it is not allowed")
+ }
+ Self::MissingCredProps => f.write_str("credProps was not sent from the client"),
+ Self::MissingPrf => f.write_str("prf was not sent from the client"),
+ Self::MissingCredProtect => f.write_str("credProtect was not sent from the client"),
+ Self::MissingHmacSecret => f.write_str("hmac-secret was not sent from the client"),
+ Self::MissingMinPinLength => f.write_str("minPinLength was not sent from the client"),
+ Self::InvalidCredProtectValue(sent, rec) => write!(
+ f,
+ "credProtect was sent with {sent}, but {rec} was received",
+ ),
+ Self::InvalidPrfValue => f.write_str("prf was false"),
+ Self::InvalidHmacSecretValue => f.write_str("hmac-secret was false"),
+ Self::InvalidMinPinLength(sent, rec) => write!(
+ f,
+ "minPinLength was sent, but {rec} is strictly smaller than the required {sent}"
+ ),
+ }
+ }
+}
+impl Error for ExtensionErr {}
+/// Error returned by [`RegistrationServerState::verify`].
+#[derive(Debug)]
+pub enum RegCeremonyErr {
+ /// [`PublicKeyCredentialCreationOptions::timeout`] was exceeded.
+ Timeout,
+ /// [`AuthenticatorAttestation::client_data_json`] could not be parsed by
+ /// [`CollectedClientData::from_client_data_json`].
+ CollectedClientData(CollectedClientDataErr),
+ /// [`AuthenticatorAttestation::client_data_json`] could not be parsed by
+ /// [`CollectedClientData::from_client_data_json_relaxed`].
+ #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+ #[cfg(feature = "serde_relaxed")]
+ CollectedClientDataRelaxed(SerdeJsonErr),
+ /// [`AuthenticatorAttestation::attestation_object`] could not be parsed into
+ /// [`AttestationObject`].
+ AttestationObject(AttestationObjectErr),
+ /// [`UncompressedPubKey`] was not valid.
+ PubKey(PubKeyErr),
+ /// [`PackedAttestation::signature`] was not valid.
+ AttestationSignature,
+ /// [`CollectedClientData::origin`] does not match one of the values in
+ /// [`RegistrationVerificationOptions::allowed_origins`].
+ OriginMismatch,
+ /// [`CollectedClientData::cross_origin`] was `true`, but
+ /// [`RegistrationVerificationOptions::allowed_top_origins`] was `None`.
+ CrossOrigin,
+ /// [`CollectedClientData::top_origin`] does not match one of the values in
+ /// [`RegistrationVerificationOptions::allowed_top_origins`].
+ TopOriginMismatch,
+ /// [`PublicKeyCredentialCreationOptions::challenge`] and [`CollectedClientData::challenge`] don't match.
+ ChallengeMismatch,
+ /// The SHA-256 hash of [`PublicKeyCredentialCreationOptions::rp_id`] does not match [`AuthenticatorData::rp_id_hash`].
+ RpIdHashMismatch,
+ /// [`Flag::user_present`] was `false` despite [`PublicKeyCredentialCreationOptions::mediation`]
+ /// being something other than [`CredentialMediationRequirement::Conditional`].
+ UserNotPresent,
+ /// [`AuthenticatorSelectionCriteria::user_verification`] was set to [`UserVerificationRequirement::Required`],
+ /// but [`Flag::user_verified`] was `false`.
+ UserNotVerified,
+ /// [`Backup::NotEligible`] was not sent back despite [`BackupReq::NotEligible`].
+ BackupEligible,
+ /// [`Backup::NotEligible`] was sent back despite [`BackupReq::Eligible`].
+ BackupNotEligible,
+ /// [`Backup::Exists`] was not sent back despite [`BackupReq::Exists`].
+ BackupDoesNotExist,
+ /// [`AuthenticatorAttachment`] was not sent back despite being required.
+ MissingAuthenticatorAttachment,
+ /// [`AuthenticatorAttachmentReq::Platform`] or [`AuthenticatorAttachmentReq::CrossPlatform`] was sent
+ /// but [`AuthenticatorAttachment`] was not the same.
+ AuthenticatorAttachmentMismatch,
+ /// Variant returned when there is an issue with [`Extension`]s.
+ Extension(ExtensionErr),
+ /// [`PublicKeyCredentialCreationOptions::pub_key_cred_params`] does not contain an algorithm associated with
+ /// [`AttestedCredentialData::credential_public_key`].
+ PublicKeyAlgorithmMismatch,
+ /// Variant returned when [`RegisteredCredential`] cannot be created due to invalid state.
+ Credential(CredentialErr),
+}
+impl Display for RegCeremonyErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Timeout => CeremonyErr::<AttestationObjectErr>::Timeout.fmt(f),
+ Self::CollectedClientData(ref err) => write!(f, "clientDataJSON could not be parsed: {err}"),
+ #[cfg(feature = "serde_relaxed")]
+ Self::CollectedClientDataRelaxed(ref err) => write!(f, "clientDataJSON could not be parsed: {err}"),
+ Self::AttestationObject(err) => err.fmt(f),
+ Self::PubKey(err) => err.fmt(f),
+ Self::AttestationSignature => AuthRespErr::<AttestationObjectErr>::Signature.fmt(f),
+ Self::OriginMismatch => CeremonyErr::<AttestationObjectErr>::OriginMismatch.fmt(f),
+ Self::CrossOrigin => CeremonyErr::<AttestationObjectErr>::CrossOrigin.fmt(f),
+ Self::TopOriginMismatch => CeremonyErr::<AttestationObjectErr>::TopOriginMismatch.fmt(f),
+ Self::BackupEligible => CeremonyErr::<AttestationObjectErr>::BackupEligible.fmt(f),
+ Self::BackupNotEligible => CeremonyErr::<AttestationObjectErr>::BackupNotEligible.fmt(f),
+ Self::BackupDoesNotExist => CeremonyErr::<AttestationObjectErr>::BackupDoesNotExist.fmt(f),
+ Self::ChallengeMismatch => CeremonyErr::<AttestationObjectErr>::ChallengeMismatch.fmt(f),
+ Self::RpIdHashMismatch => CeremonyErr::<AttestationObjectErr>::RpIdHashMismatch.fmt(f),
+ Self::UserNotPresent => f.write_str("user was not present despite mediation not being conditional"),
+ Self::UserNotVerified => CeremonyErr::<AttestationObjectErr>::UserNotVerified.fmt(f),
+ Self::MissingAuthenticatorAttachment => f.write_str("the authenticator attachment modality was not sent despite being required"),
+ Self::AuthenticatorAttachmentMismatch => f.write_str("the kind of authenticator requested (e.g., platform) was not used"),
+ Self::Extension(ext) => ext.fmt(f),
+ Self::PublicKeyAlgorithmMismatch => f.write_str(
+ "the allowed public key algorithms does not contain the algorithm sent from the client",
+ ),
+ Self::Credential(err) => err.fmt(f),
+ }
+ }
+}
+impl Error for RegCeremonyErr {}
diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs
@@ -0,0 +1,5502 @@
+#![expect(
+ clippy::question_mark_used,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+use super::{
+ super::{
+ super::request::register::CoseAlgorithmIdentifier,
+ ser::{
+ AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues,
+ Base64DecodedVal, ClientExtensions, PublicKeyCredential,
+ },
+ BASE64URL_NOPAD_ENC,
+ },
+ AttestationObject, AttestedCredentialData, AuthTransports, AuthenticationExtensionsPrfOutputs,
+ AuthenticatorAttestation, ClientExtensionsOutputs, CredentialPropertiesOutput, FromCbor as _,
+ Registration, UncompressedPubKey,
+};
+#[cfg(doc)]
+use super::{AuthenticatorAttachment, CredentialId};
+use core::{
+ fmt::{self, Formatter},
+ marker::PhantomData,
+ str,
+};
+use rsa::sha2::{digest::OutputSizeUser as _, Sha256};
+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
+/// the public key in the attestation object.
+mod spki {
+ use super::super::{
+ Ed25519PubKey, RsaPubKey, RsaPubKeyErr, UncompressedP256PubKey, UncompressedP384PubKey,
+ UncompressedPubKey,
+ };
+ use core::fmt::{self, Display, Formatter};
+ use p256::{
+ elliptic_curve::{generic_array::typenum::type_operators::ToInt as _, Curve},
+ NistP256,
+ };
+ use p384::NistP384;
+ /// Value assigned to the integer type under the universal tag class per
+ /// [ITU-T X.680](https://www.itu.int/rec/T-REC-X.680-202102-I/en).
+ const INTEGER: u8 = 2;
+ /// Value assigned to the bitstring type under the universal tag class per
+ /// [ITU-T X.680](https://www.itu.int/rec/T-REC-X.680-202102-I/en).
+ const BITSTRING: u8 = 3;
+ /// Value assigned to the null type under the universal tag class per
+ /// [ITU-T X.680](https://www.itu.int/rec/T-REC-X.680-202102-I/en).
+ const NULL: u8 = 5;
+ /// Value assigned to the object identifier type under the universal tag class per
+ /// [ITU-T X.680](https://www.itu.int/rec/T-REC-X.680-202102-I/en).
+ const OID: u8 = 6;
+ /// Value assigned to the sequence type under the universal tag class per
+ /// [ITU-T X.680](https://www.itu.int/rec/T-REC-X.680-202102-I/en).
+ const SEQUENCE: u8 = 16;
+ /// Value assigned to a constructed [`SEQUENCE`] per
+ /// [ITU-T X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en).
+ ///
+ /// All sequences are constructed once encoded, so this will likely always be used instead of
+ /// `SEQUENCE`.
+ const CONSTRUCTED_SEQUENCE: u8 = SEQUENCE | 0b0010_0000;
+ /// Length of the header before the compressed y-coordinate in a DER-encoded ASN.1 `SubjectPublicKeyInfo`
+ /// for an Ed25519 public key.
+ const ED25519_HEADER_LEN: usize = 12;
+ /// Length of a DER-encoded ASN.1 `SubjectPublicKeyInfo` for Ed25519 public key.
+ const ED25519_LEN: usize = ED25519_HEADER_LEN + ed25519_dalek::PUBLIC_KEY_LENGTH;
+ /// `ED25519_LEN` as a `u8`.
+ // `44 as u8` is clearly OK.
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "comments above justify their correctness"
+ )]
+ const ED25519_LEN_U8: u8 = ED25519_LEN as u8;
+ /// Length of the header before the uncompressed SEC- 1 pubic key in a DER-encoded ASN.1 `SubjectPublicKeyInfo`
+ /// for an uncompressed ECDSA public key based on secp256r1/P-256.
+ const P256_HEADER_LEN: usize = 27;
+ /// Number of bytes the x-coordinate takes in an uncompressed P-256 public key.
+ pub const P256_X_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT;
+ /// Number of bytes the y-coordinate takes in an uncompressed P-256 public key.
+ const P256_Y_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT;
+ /// Number of bytes the x and y coordinates take in an uncompressed P-256 public key when concatenated together.
+ const P256_COORD_LEN: usize = P256_X_LEN + P256_Y_LEN;
+ /// Length of a DER-encoded ASN.1 `SubjectPublicKeyInfo` for an uncompressed SEC-1 ECDSA public key
+ /// based on secp256r1/P-256.
+ const P256_LEN: usize = P256_HEADER_LEN + P256_COORD_LEN;
+ /// `P256_LEN` as a `u8`.
+ // `91 as u8` is clearly OK.
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "comments above justify their correctness"
+ )]
+ const P256_LEN_U8: u8 = P256_LEN as u8;
+ /// Length of the header before the uncompressed SEC- 1 pubic key in a DER-encoded ASN.1 `SubjectPublicKeyInfo`
+ /// for an uncompressed ECDSA public key based on secp384r1/P-384.
+ const P384_HEADER_LEN: usize = 24;
+ /// Number of bytes the x-coordinate takes in an uncompressed P-384 public key.
+ pub const P384_X_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT;
+ /// Number of bytes the y-coordinate takes in an uncompressed P-384 public key.
+ const P384_Y_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT;
+ /// Number of bytes the x and y coordinates take in an uncompressed P-384 public key when concatenated together.
+ const P384_COORD_LEN: usize = P384_X_LEN + P384_Y_LEN;
+ /// Length of a DER-encoded ASN.1 `SubjectPublicKeyInfo` for an uncompressed SEC-1 ECDSA public key
+ /// based on secp384r1/P-384.
+ const P384_LEN: usize = P384_HEADER_LEN + P384_COORD_LEN;
+ /// `P384_LEN` as a `u8`.
+ // `120 as u8` is clearly OK.
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "comments above justify their correctness"
+ )]
+ const P384_LEN_U8: u8 = P384_LEN as u8;
+ /// Error returned from [`SubjectPublicKeyInfo::from_der`].
+ pub enum SubjectPublicKeyInfoErr {
+ /// The DER-encoded `SubjectPublicKeyInfo` had an invalid length.
+ Len,
+ /// The length of the DER-encoded Ed25519 key was invalid.
+ Ed25519Len,
+ /// The header of the DER-encoded Ed25519 key was invalid.
+ Ed25519Header,
+ /// The length of the DER-encoded P-256 key was invalid.
+ P256Len,
+ /// The header of the DER-encoded P-256 key was invalid.
+ P256Header,
+ /// The length of the DER-encoded P-384 key was invalid.
+ P384Len,
+ /// The header of the DER-encoded P-384 key was invalid.
+ P384Header,
+ /// The length of the DER-encoded RSA key was invalid.
+ RsaLen,
+ /// The DER-encoding of the RSA key was invalid.
+ RsaEncoding,
+ /// The exponent of the DER-encoded RSA key was too large.
+ RsaExponentTooLarge,
+ /// The DER-encoded RSA key was not a valid [`RsaPubKey`].
+ RsaPubKey(RsaPubKeyErr),
+ }
+ impl Display for SubjectPublicKeyInfoErr {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Len => {
+ f.write_str("the DER-encoded SubjectPublicKeyInfo had an invalid length")
+ }
+ Self::Ed25519Len => {
+ f.write_str("length of the DER-encoded Ed25519 key was invalid")
+ }
+ Self::Ed25519Header => {
+ f.write_str("header of the DER-encoded Ed25519 key was invalid")
+ }
+ Self::P256Len => f.write_str("length of the DER-encoded P-256 key was invalid"),
+ Self::P256Header => f.write_str("header of the DER-encoded P-256 key was invalid"),
+ Self::P384Len => f.write_str("length of the DER-encoded P-384 key was invalid"),
+ Self::P384Header => f.write_str("header of the DER-encoded P-384 key was invalid"),
+ Self::RsaLen => f.write_str("length of the DER-encoded RSA key was invalid"),
+ Self::RsaEncoding => {
+ f.write_str("the DER-encoding of the RSA public key was invalid")
+ }
+ Self::RsaExponentTooLarge => {
+ f.write_str("the DER-encoded RSA public key had an exponent that was too large")
+ }
+ Self::RsaPubKey(err) => {
+ write!(f, "the DER-encoded RSA public was not valid: {err}")
+ }
+ }
+ }
+ }
+ /// Types that can be deserialized from the DER-encoded ASN.1 `SubjectPublicKeyInfo` as defined in
+ /// [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1) and other applicable RFCs
+ /// and documents (e.g., [ITU-T X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en)).
+ pub trait SubjectPublicKeyInfo<'a>: Sized {
+ /// Transforms the DER-encoded ASN.1 `SubjectPublicKeyInfo` into `Self`.
+ ///
+ /// # Errors
+ ///
+ /// Errors iff `der` does not conform.
+ fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr>;
+ }
+ impl<'a> SubjectPublicKeyInfo<'a> for Ed25519PubKey<&'a [u8]> {
+ fn from_der<'b: 'a>(der: &'a [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
+ /// ```asn
+ /// SubjectPublicKeyInfo ::= SEQUENCE {
+ /// algorithm AlgorithmIdentifier,
+ /// subjectPublicKey BIT STRING
+ /// }
+ ///
+ /// AlgorithmIdentifier ::= SEQUENCE {
+ /// algorithm OBJECT IDENTIFIER,
+ /// parameters ANY DEFINED BY algorithm OPTIONAL
+ /// }
+ /// ```
+ ///
+ /// [RFC 8410](https://www.rfc-editor.org/rfc/rfc8410#section-3) requires parameters to not exist
+ /// in `AlgorithmIdentifier`.
+ ///
+ /// RFC 8410 defines the OID as 1.3.101.112 which is encoded as 43.101.112
+ /// per [X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en).
+ ///
+ /// RFC 8410 defines the bitstring as a reinterpretation of the byte string.
+ const HEADER: [u8; ED25519_HEADER_LEN] = [
+ CONSTRUCTED_SEQUENCE,
+ // `ED25519_LEN_U8` is the length of the entire payload; thus we subtract
+ // the "consumed" length.
+ ED25519_LEN_U8 - 2,
+ CONSTRUCTED_SEQUENCE,
+ // AlgorithmIdentifier only contains the OID; thus it's length is 5.
+ 3 + 2,
+ OID,
+ 3,
+ 43,
+ 101,
+ 112,
+ BITSTRING,
+ // `ED25519_LEN_U8` is the length of the entire payload; thus we subtract
+ // the "consumed" length.
+ ED25519_LEN_U8 - 11,
+ // The number of unused bits.
+ 0,
+ ];
+ der.split_at_checked(HEADER.len())
+ .ok_or(SubjectPublicKeyInfoErr::Ed25519Len)
+ .and_then(|(header, rem)| {
+ if header == HEADER {
+ if rem.len() == ed25519_dalek::PUBLIC_KEY_LENGTH {
+ Ok(Self(rem))
+ } else {
+ Err(SubjectPublicKeyInfoErr::Ed25519Len)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::Ed25519Header)
+ }
+ })
+ }
+ }
+ impl<'a> SubjectPublicKeyInfo<'a> for UncompressedP256PubKey<'a> {
+ fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
+ // ```asn
+ // SubjectPublicKeyInfo ::= SEQUENCE {
+ // algorithm AlgorithmIdentifier,
+ // subjectPublicKey BIT STRING
+ // }
+ //
+ // AlgorithmIdentifier ::= SEQUENCE {
+ // algorithm OBJECT IDENTIFIER,
+ // parameters ANY DEFINED BY algorithm OPTIONAL
+ // }
+ //
+ // ECParameters ::= CHOICE {
+ // namedCurve OBJECT IDENTIFIER
+ // -- implicitCurve NULL
+ // -- specifiedCurve SpecifiedECDomain
+ // }
+ // ```
+ // [RFC 5480](https://www.rfc-editor.org/rfc/rfc5480#section-2.1.1) requires parameters to exist and
+ // be of the `ECParameters` form with the requirement that `namedCurve` is chosen.
+ //
+ // RFC 5480 defines the OID for id-ecPublicKey as 1.2.840.10045.2.1 and the OID for the namedCurve
+ // secp256r1 as 1.2.840.10045.3.1.7. The former OID is encoded as 42.134.72.206.61.2.1 and the latter
+ // is encoded as 42.134.72.206.61.3.1.7 per [X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en).
+ //
+ // [RFC 5480](https://www.rfc-editor.org/rfc/rfc5480#section-5) only requires support for the
+ // uncompressed form and only states that the compressed form MAY be supported. In practice this means
+ // DER-encoded payloads almost always are of the uncompressed form for compatibility reasons. This in
+ // conjunction with the fact the COSE key is required to be in the uncompressed form means we only support
+ // DER-encoded payloads containing uncompressed keys.
+ //
+ // [SEC 1](https://secg.org/sec1-v2.pdf) defines the point as an octet string, and the conversion
+ // to a bitstring simply requires reinterpreting the octet string as a bitstring.
+
+ /// Header of the DER-encoded payload before the public key.
+ const HEADER: [u8; P256_HEADER_LEN] = [
+ CONSTRUCTED_SEQUENCE,
+ // `P256_LEN_U8` is the length of the entire payload; thus we subtract
+ // the "consumed" length.
+ P256_LEN_U8 - 2,
+ CONSTRUCTED_SEQUENCE,
+ 7 + 2 + 8 + 2,
+ OID,
+ 7,
+ 42,
+ 134,
+ 72,
+ 206,
+ 61,
+ 2,
+ 1,
+ OID,
+ 8,
+ 42,
+ 134,
+ 72,
+ 206,
+ 61,
+ 3,
+ 1,
+ 7,
+ BITSTRING,
+ // `P256_LEN_U8` is the length of the entire payload; thus we subtract
+ // the "consumed" length.
+ P256_LEN_U8 - 25,
+ // The number of unused bits.
+ 0,
+ // SEC-1 tag for an uncompressed key.
+ 4,
+ ];
+ der.split_at_checked(HEADER.len())
+ .ok_or(SubjectPublicKeyInfoErr::P256Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(P256_X_LEN)
+ .ok_or(SubjectPublicKeyInfoErr::P256Len)
+ .and_then(|(x, y)| {
+ if y.len() == P256_Y_LEN {
+ Ok(Self(x, y))
+ } else {
+ Err(SubjectPublicKeyInfoErr::P256Len)
+ }
+ })
+ } else {
+ Err(SubjectPublicKeyInfoErr::P256Header)
+ }
+ })
+ }
+ }
+ impl<'a> SubjectPublicKeyInfo<'a> for UncompressedP384PubKey<'a> {
+ fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
+ // ```asn
+ // SubjectPublicKeyInfo ::= SEQUENCE {
+ // algorithm AlgorithmIdentifier,
+ // subjectPublicKey BIT STRING
+ // }
+ //
+ // AlgorithmIdentifier ::= SEQUENCE {
+ // algorithm OBJECT IDENTIFIER,
+ // parameters ANY DEFINED BY algorithm OPTIONAL
+ // }
+ //
+ // ECParameters ::= CHOICE {
+ // namedCurve OBJECT IDENTIFIER
+ // -- implicitCurve NULL
+ // -- specifiedCurve SpecifiedECDomain
+ // }
+ // ```
+ // [RFC 5480](https://www.rfc-editor.org/rfc/rfc5480#section-2.1.1) requires parameters to exist and
+ // be of the `ECParameters` form with the requirement that `namedCurve` is chosen.
+ //
+ // RFC 5480 defines the OID for id-ecPublicKey as 1.2.840.10045.2.1 and the OID for the namedCurve
+ // secp384r1 as 1.3.132.0.34. The former OID is encoded as 42.134.72.206.61.2.1 and the latter
+ // is encoded as 43.129.4.0.34 per [X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en).
+ //
+ // [RFC 5480](https://www.rfc-editor.org/rfc/rfc5480#section-5) only requires support for the
+ // uncompressed form and only states that the compressed form MAY be supported. In practice this means
+ // DER-encoded payloads almost always are of the uncompressed form for compatibility reasons. This in
+ // conjunction with the fact the COSE key is required to be in the uncompressed form means we only support
+ // DER-encoded payloads containing uncompressed keys.
+ //
+ // [SEC 1](https://secg.org/sec1-v2.pdf) defines the point as an octet string, and the conversion
+ // to a bitstring simply requires reinterpreting the octet string as a bitstring.
+
+ /// Header of the DER-encoded payload before the public key.
+ const HEADER: [u8; P384_HEADER_LEN] = [
+ CONSTRUCTED_SEQUENCE,
+ // `P384_LEN_U8` is the length of the entire payload; thus we subtract
+ // the "consumed" length.
+ P384_LEN_U8 - 2,
+ CONSTRUCTED_SEQUENCE,
+ 7 + 2 + 5 + 2,
+ OID,
+ 7,
+ 42,
+ 134,
+ 72,
+ 206,
+ 61,
+ 2,
+ 1,
+ OID,
+ 5,
+ 43,
+ 129,
+ 4,
+ 0,
+ 34,
+ BITSTRING,
+ // `P384_LEN_U8` is the length of the entire payload; thus we subtract
+ // the "consumed" length.
+ P384_LEN_U8 - 22,
+ // The number of unused bits.
+ 0,
+ // SEC-1 tag for an uncompressed key.
+ 4,
+ ];
+ der.split_at_checked(HEADER.len())
+ .ok_or(SubjectPublicKeyInfoErr::P384Len)
+ .and_then(|(header, header_rem)| {
+ if header == HEADER {
+ header_rem
+ .split_at_checked(P384_X_LEN)
+ .ok_or(SubjectPublicKeyInfoErr::P384Len)
+ .and_then(|(x, y)| {
+ if y.len() == P384_Y_LEN {
+ Ok(Self(x, y))
+ } else {
+ Err(SubjectPublicKeyInfoErr::P384Len)
+ }
+ })
+ } else {
+ Err(SubjectPublicKeyInfoErr::P384Header)
+ }
+ })
+ }
+ }
+ impl<'a> SubjectPublicKeyInfo<'a> for RsaPubKey<&'a [u8]> {
+ #[expect(
+ clippy::arithmetic_side_effects,
+ clippy::big_endian_bytes,
+ clippy::indexing_slicing,
+ clippy::missing_asserts_for_indexing,
+ reason = "comments justify their correctness"
+ )]
+ #[expect(
+ clippy::too_many_lines,
+ reason = "rsa keys are the only type that would benefit from a modular SubjectPublicKeyInfo (similar to FromCbor). if more types benefit in the future, then this will be done."
+ )]
+ fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
+ // ```asn
+ // SubjectPublicKeyInfo ::= SEQUENCE {
+ // algorithm AlgorithmIdentifier,
+ // subjectPublicKey BIT STRING
+ // }
+ //
+ // AlgorithmIdentifier ::= SEQUENCE {
+ // algorithm OBJECT IDENTIFIER,
+ // parameters ANY DEFINED BY algorithm OPTIONAL
+ // }
+ //
+ // RSAPublicKey ::= SEQUENCE {
+ // modulus INTEGER, -- n
+ // publicExponent INTEGER --e
+ // }
+ //
+ // pkcs-1 OBJECT IDENTIFIER ::= { iso(1) member-body(2) us(840)
+ // rsadsi(113549) pkcs(1) 1
+ // }
+ //
+ // rsaEncryption OBJECT IDENTIFIER ::= { pkcs-1 1}
+ // ```
+ // [RFC 3279](https://www.rfc-editor.org/rfc/rfc3279#section-2.3.1) requires parameters to exist and
+ // be null.
+ //
+ // RFC 3279 defines the OID for rsaEncryption as 1.2.840.113549.1.1.1 which is encoded as
+ // 42.134.72.134.247.13.1.1.1 per [X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en).
+ //
+ // Note we only allow moduli that are 256 to 2048 bytes in length inclusively. Additionally
+ // we only allow `u32` exponents; consequently all lengths that include the modulus will always be
+ // encoded with two bytes.
+
+ /// `AlgorithmIdentifier` header including the `BITSTRING` and number of bytes the length
+ /// is encoded in of the `BITSTRING` type of `subjectPublicKey`
+ /// (130-128 = 2 bytes to encode the length).
+ const ALG_OID_HEADER: [u8; 17] = [
+ CONSTRUCTED_SEQUENCE,
+ 9 + 2 + 2,
+ OID,
+ 9,
+ 42,
+ 134,
+ 72,
+ 134,
+ 247,
+ 13,
+ 1,
+ 1,
+ 1,
+ NULL,
+ 0,
+ BITSTRING,
+ 130,
+ ];
+ /// `CONSTRUCTED_SEQUENCE` whose length is encoded in two bytes.
+ const SEQ_LONG: [u8; 2] = [CONSTRUCTED_SEQUENCE, 130];
+ /// `INTEGER` whose length is encoded in two bytes.
+ const INT_LONG: [u8; 2] = [INTEGER, 130];
+ der.split_at_checked(SEQ_LONG.len())
+ .ok_or(SubjectPublicKeyInfoErr::RsaLen)
+ .and_then(|(seq, seq_rem)| {
+ if seq == SEQ_LONG {
+ seq_rem
+ .split_at_checked(2)
+ .ok_or(SubjectPublicKeyInfoErr::RsaLen)
+ .and_then(|(seq_len, seq_len_rem)| {
+ let mut len = [0; 2];
+ len.copy_from_slice(seq_len);
+ let rem_len = usize::from(u16::from_be_bytes(len));
+ if rem_len == seq_len_rem.len() {
+ if rem_len > 255 {
+ // We can safely split here since we know `seq_len_rem` is at least
+ // 256 which is greater than `ALG_OID_HEADER.len()`.
+ let (a_oid, a_oid_rem) = seq_len_rem.split_at(ALG_OID_HEADER.len());
+ if a_oid == ALG_OID_HEADER {
+ // `a_oid_rem.len()` is at least 239, so splitting is fine.
+ let (bit_str_len_enc, bit_str_val) = a_oid_rem.split_at(2);
+ let mut bit_string_len = [0; 2];
+ bit_string_len.copy_from_slice(bit_str_len_enc);
+ let bit_str_val_len = usize::from(u16::from_be_bytes(bit_string_len));
+ if bit_str_val_len == bit_str_val.len() {
+ if bit_str_val_len > 255 {
+ // `bit_str_val.len() > 255`, so splitting is fine.
+ let (unused_bits, bits_rem) = bit_str_val.split_at(1);
+ if unused_bits == [0] {
+ // We can safely split here since we know `bits_rem.len()` is at least
+ // 255.
+ let (rsa_seq, rsa_seq_rem) = bits_rem.split_at(SEQ_LONG.len());
+ if rsa_seq == SEQ_LONG {
+ // `rsa_seq_rem.len()` is at least 253, so splitting is fine.
+ let (rsa_seq_len_enc, rsa_seq_len_enc_rem) = rsa_seq_rem.split_at(2);
+ let mut rsa_seq_len = [0; 2];
+ rsa_seq_len.copy_from_slice(rsa_seq_len_enc);
+ let rsa_key_info_len = usize::from(u16::from_be_bytes(rsa_seq_len));
+ if rsa_key_info_len == rsa_seq_len_enc_rem.len() {
+ if rsa_key_info_len > 255 {
+ // We can safely split here since we know `rsa_seq_len_enc_rem.len()`
+ // is at least 256.
+ let (n_meta, n_meta_rem) = rsa_seq_len_enc_rem.split_at(INT_LONG.len());
+ if n_meta == INT_LONG {
+ // `n_meta_rem.len()` is at least 254, so splitting is fine.
+ let (n_len_enc, n_len_enc_rem) = n_meta_rem.split_at(2);
+ let mut n_len = [0; 2];
+ n_len.copy_from_slice(n_len_enc);
+ let mod_len = usize::from(u16::from_be_bytes(n_len));
+ if mod_len > 255 {
+ n_len_enc_rem.split_at_checked(mod_len).ok_or(SubjectPublicKeyInfoErr::RsaLen).and_then(|(mut n, n_rem)| {
+ // `n.len() > 255`, so indexing is fine.
+ let n_first = n[0];
+ // DER integers are signed; thus the most significant bit must be 0.
+ // DER integers are minimally encoded; thus when a leading 0 exists,
+ // the second byte must be at least 128.
+ // `n.len() > 255`, so indexing is fine.
+ if n_first < 128 && (n_first != 0 || n[1] > 127) {
+ if n_first == 0 {
+ // `n.len() > 255`, so indexing is fine.
+ // We must remove the leading 0.
+ n = &n[1..];
+ }
+ n_rem.split_first().ok_or(SubjectPublicKeyInfoErr::RsaLen).and_then(|(e_type, e_type_rem)| {
+ if *e_type == INTEGER {
+ e_type_rem.split_first().ok_or(SubjectPublicKeyInfoErr::RsaLen).and_then(|(e_len, e_len_rem)| {
+ let e_len_usize = usize::from(*e_len);
+ if e_len_usize == e_len_rem.len() {
+ e_len_rem.first().ok_or(SubjectPublicKeyInfoErr::RsaLen).and_then(|&e_first| {
+ // DER integers are signed; thus the most significant bit must be 0.
+ if e_first < 128 {
+ // `RsaPubKey` only allows `u32` exponents, which means we only care
+ // about lengths up to 5.
+ match e_len_usize {
+ 1 => Ok(u32::from(e_first)),
+ 2..=5 => if e_first == 0 {
+ // DER integers are minimally encoded; thus when a leading
+ // 0 exists, the second byte must be at least 128.
+ // We know the length is at least 2; thus this won't `panic`.
+ if e_len_rem[1] > 127 {
+ let mut e = [0; 4];
+ if e_len_usize == 5 {
+ // We know the length is at least 2; thus this won't `panic`.
+ e.copy_from_slice(&e_len_rem[1..]);
+ } else {
+ // `e.len() == 4` and `e_len_usize` is at most 4; thus underflow
+ // won't occur nor will indexing `panic`. `e` is big-endian,
+ // so we start from the right.
+ e[4 - e_len_usize..].copy_from_slice(e_len_rem);
+ }
+ Ok(u32::from_be_bytes(e))
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ } else if e_len_usize == 5 {
+ // 5 bytes are only possible for `INTEGER`s that
+ // are greater than `i32::MAX`, which will be encoded
+ // with a leading 0.
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ } else {
+ let mut e = [0; 4];
+ // `e.len() == 4` and `e_len_usize` is at most 4; thus underflow
+ // won't occur nor will indexing `panic`. `e` is big-endian,
+ // so we start from the right.
+ e[4 - e_len_usize..].copy_from_slice(e_len_rem);
+ Ok(u32::from_be_bytes(e))
+ },
+ _ => Err(SubjectPublicKeyInfoErr::RsaExponentTooLarge),
+ }.and_then(|e| Self::try_from((n, e)).map_err(SubjectPublicKeyInfoErr::RsaPubKey))
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ })
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaLen)
+ }
+ })
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ })
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ })
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaLen)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaLen)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaLen)
+ }
+ })
+ } else {
+ Err(SubjectPublicKeyInfoErr::RsaEncoding)
+ }
+ })
+ }
+ }
+ impl<'a> SubjectPublicKeyInfo<'a> for UncompressedPubKey<'a> {
+ fn from_der<'b: 'a>(der: &'b [u8]) -> Result<Self, SubjectPublicKeyInfoErr> {
+ // The lengths of the three key types do not overlap.
+ match der.len() {
+ // The minimum modulus we support for RSA is 2048 bits which is 256 bytes;
+ // thus clearly its encoding will be at least 256 which is greater than
+ // all of the other values.
+ ED25519_LEN => Ed25519PubKey::from_der(der).map(Self::Ed25519),
+ P256_LEN => UncompressedP256PubKey::from_der(der).map(Self::P256),
+ P384_LEN => UncompressedP384PubKey::from_der(der).map(Self::P384),
+ 256.. => RsaPubKey::from_der(der).map(Self::Rsa),
+ _ => Err(SubjectPublicKeyInfoErr::Len),
+ }
+ }
+ }
+}
+/// Helper type returned from [`AuthenticatorAttestationVisitor::visit_map`].
+///
+/// The purpose of this type is to hopefully avoid re-parsing the raw attestation object multiple times. In
+/// particular [`Registration`] and [`super::ser_relaxed::RegistrationRelaxed`] will attempt to validate `id` is the
+/// same as the [`CredentialId`] within the attestation object.
+pub(super) struct AuthAttest {
+ /// The data we care about.
+ pub attest: AuthenticatorAttestation,
+ /// [`CredentialId`] information. This is `None` iff `authenticatorData`, `publicKey`, and
+ /// `publicKeyAlgorithm` do not exist and we are performing a `RELAXED` parsing. When `Some`, the first
+ /// `usize` is the starting index of `CredentialId` within the attestation object; and the second `usize` is
+ /// 1 past the last index of `CredentialId`.
+ pub cred_info: Option<(usize, usize)>,
+}
+/// Fields in `AuthenticatorAttestationResponseJSON`.
+enum AttestField<const IGNORE_UNKNOWN: bool> {
+ /// `clientDataJSON`.
+ ClientDataJson,
+ /// `attestationObject`.
+ AttestationObject,
+ /// `authenticatorData`.
+ AuthenticatorData,
+ /// `transports`.
+ Transports,
+ /// `publicKey`.
+ PublicKey,
+ /// `publicKeyAlgorithm`.
+ PublicKeyAlgorithm,
+ /// Unknown fields.
+ Other,
+}
+impl<'e, const I: bool> Deserialize<'e> for AttestField<I> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `AttestField`.
+ struct AttestFieldVisitor<const IGNORE_UNKNOWN: bool>;
+ 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}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ CLIENT_DATA_JSON => Ok(AttestField::ClientDataJson),
+ ATTESTATION_OBJECT => Ok(AttestField::AttestationObject),
+ AUTHENTICATOR_DATA => Ok(AttestField::AuthenticatorData),
+ TRANSPORTS => Ok(AttestField::Transports),
+ PUBLIC_KEY => Ok(AttestField::PublicKey),
+ PUBLIC_KEY_ALGORITHM => Ok(AttestField::PublicKeyAlgorithm),
+ _ => {
+ if IG {
+ Ok(AttestField::Other)
+ } else {
+ Err(E::unknown_field(v, AUTH_ATTEST_FIELDS))
+ }
+ }
+ }
+ }
+ }
+ deserializer.deserialize_identifier(AttestFieldVisitor::<I>)
+ }
+}
+/// Attestation object. We use this instead of `Base64DecodedVal` since we want to manually
+/// allocate the `Vec` in order to avoid re-allocation. Internally `AuthenticatorAttestation::new`
+/// appends the SHA-256 hash to the passed attestation object `Vec` to avoid temporarily allocating
+/// 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>);
+impl<'e> Deserialize<'e> for AttObj {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `AttObj`.
+ struct AttObjVisitor;
+ impl Visitor<'_> for AttObjVisitor {
+ type Value = AttObj;
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("base64url-encoded attestation object")
+ }
+ #[expect(
+ clippy::panic_in_result_fn,
+ reason = "we want to crash when there is a bug"
+ )]
+ #[expect(
+ clippy::arithmetic_side_effects,
+ reason = "comment justifies their correctness"
+ )]
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ // If the encoded length is greater than `usize::MAX / 3`, then it's quite close
+ // or more than `isize::MAX` which is the max capacity of a `str` and `Vec`. At which
+ // point, we just error instead of attempting to decode such a large payload.
+ crate::base64url_nopad_decode_len(v.len())
+ .ok_or_else(|| {
+ E::invalid_value(Unexpected::Str(v), &"a shorter 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 attestation object by `AuthenticatorAttestation::new`.
+ let mut att_obj = vec![0; len + Sha256::output_size()];
+ att_obj.truncate(len);
+ BASE64URL_NOPAD_ENC
+ .decode_mut(v.as_bytes(), &mut att_obj)
+ .map_err(|e| E::custom(e.error))
+ .map(|dec_len| {
+ assert_eq!(
+ len, dec_len,
+ "there is a bug in BASE64URL_NOPAD::decode_mut"
+ );
+ AttObj(att_obj)
+ })
+ })
+ }
+ }
+ deserializer.deserialize_str(AttObjVisitor)
+ }
+}
+/// `Visitor` for `AuthenticatorAttestation`.
+///
+/// Unknown fields are ignored and only `clientDataJSON` and `attestationObject` are required iff `RELAXED`.
+pub(super) struct AuthenticatorAttestationVisitor<const RELAXED: bool>;
+impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
+ type Value = AuthAttest;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("AuthenticatorAttestation")
+ }
+ #[expect(clippy::too_many_lines, reason = "find it easier to reason about")]
+ #[expect(
+ clippy::arithmetic_side_effects,
+ clippy::indexing_slicing,
+ reason = "comments justify their correctness"
+ )]
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ use spki::SubjectPublicKeyInfo as _;
+ let mut client_data = None;
+ let mut attest = None;
+ let mut auth = None;
+ let mut pub_key = None;
+ let mut key_alg = None;
+ let mut trans = None;
+ while let Some(key) = map.next_key::<AttestField<R>>()? {
+ match key {
+ AttestField::ClientDataJson => {
+ if client_data.is_some() {
+ return Err(Error::duplicate_field(CLIENT_DATA_JSON));
+ }
+ client_data = map
+ .next_value::<Base64DecodedVal>()
+ .map(|c_data| Some(c_data.0))?;
+ }
+ AttestField::AttestationObject => {
+ if attest.is_some() {
+ return Err(Error::duplicate_field(ATTESTATION_OBJECT));
+ }
+ attest = map.next_value::<AttObj>().map(|att_obj| Some(att_obj.0))?;
+ }
+ AttestField::AuthenticatorData => {
+ if auth.is_some() {
+ return Err(Error::duplicate_field(AUTHENTICATOR_DATA));
+ }
+ auth = map.next_value::<Option<Base64DecodedVal>>().map(Some)?;
+ }
+ AttestField::Transports => {
+ if trans.is_some() {
+ return Err(Error::duplicate_field(TRANSPORTS));
+ }
+ trans = map.next_value::<Option<_>>().map(Some)?;
+ }
+ AttestField::PublicKey => {
+ if pub_key.is_some() {
+ return Err(Error::duplicate_field(PUBLIC_KEY));
+ }
+ pub_key = map.next_value::<Option<Base64DecodedVal>>().map(Some)?;
+ }
+ AttestField::PublicKeyAlgorithm => {
+ if key_alg.is_some() {
+ return Err(Error::duplicate_field(PUBLIC_KEY_ALGORITHM));
+ }
+ key_alg = map
+ .next_value::<Option<CoseAlgorithmIdentifier>>()
+ .map(Some)?;
+ }
+ AttestField::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
+ }
+ }
+ // Note the order of this matters from a performance perspective. In particular `auth` must be evaluated
+ // before `pub_key` which must be evaluated before `key_alg` as this allows us to parse the attestation
+ // object at most once and allow us to prioritize parsing `authenticatorData` over the attestation object.
+ client_data.ok_or_else(|| Error::missing_field(CLIENT_DATA_JSON)).and_then(|client_data_json| attest.ok_or_else(|| Error::missing_field(ATTESTATION_OBJECT)).and_then(|attestation_object| {
+ trans.ok_or(false).and_then(|opt_trans| opt_trans.ok_or(true)).or_else(
+ |flag| {
+ if R {
+ Ok(AuthTransports::new())
+ } else if flag {
+ Err(Error::invalid_type(Unexpected::Other("null"), &format!("{TRANSPORTS} to be a sequence of AuthenticatorTransports").as_str()))
+ } else {
+ Err(Error::missing_field(TRANSPORTS))
+ }
+ },
+ ).and_then(|transports| {
+ auth.ok_or(false).and_then(|opt_auth| opt_auth.ok_or(true)).as_ref().map_or_else(
+ |flag| {
+ if R {
+ Ok(None)
+ } else if *flag {
+ Err(Error::invalid_type(Unexpected::Other("null"), &format!("{AUTHENTICATOR_DATA} to be a base64url-encoded AuthenticatorData").as_str()))
+ } else {
+ Err(Error::missing_field(AUTHENTICATOR_DATA))
+ }
+ },
+ |a_data| {
+ if a_data.0.len() > 37 {
+ // The last portion of attestation object is always authenticator data.
+ attestation_object.len().checked_sub(a_data.0.len()).ok_or_else(|| Error::invalid_value(Unexpected::Bytes(a_data.0.as_slice()), &format!("authenticator data to match the authenticator data portion of attestation object: {attestation_object:?}").as_str())).and_then(|idx| {
+ // Indexing is fine; otherwise the above check would have returned `None`.
+ if *a_data.0 == attestation_object[idx..] {
+ // We know `a_data.len() > 37`; thus indexing is fine.
+ // We start at 37 since that is the beginning of `attestedCredentialData`.
+ // Recall the first 32 bytes are `rpIdHash`, then a 1 byte `flags`, then a
+ // 4-byte big-endian integer `signCount`.
+ // The starting index of `credentialId` is 18 within `attestedCredentialData`.
+ // Recall the first 16 bytes are `aaguid`, then a 2-byte big-endian integer
+ // `credentialIdLength`. Consequently the starting index within
+ // `attestation_object` is `idx + 37 + 18` = `idx + 55`. Overflow cannot occur
+ // since we successfully parsed `AttestedCredentialData`.
+ AttestedCredentialData::from_cbor(&a_data.0[37..]).map_err(Error::custom).map(|success| Some((success.value, idx + 55)))
+ } else {
+ Err(Error::invalid_value(Unexpected::Bytes(a_data.0.as_slice()), &format!("authenticator data to match the authenticator data portion of attestation object: {:?}", &attestation_object[idx..]).as_str()))
+ }
+ })
+ } else {
+ Err(Error::invalid_value(Unexpected::Bytes(a_data.0.as_slice()), &"authenticator data to be long enough to contain attested credential data"))
+ }
+ }
+ ).and_then(|attested_info| {
+ pub_key.ok_or(false).and_then(|opt_key| opt_key.ok_or(true)).map_or_else(
+ |flag| {
+ if R {
+ attested_info.as_ref().map_or(Ok(None), |&(ref attested_data, cred_id_start)| Ok(Some((match attested_data.credential_public_key {
+ UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
+ UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
+ UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
+ UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
+ // Overflow won't occur since this is correct as
+ // `AttestedCredentialData::from_cbor` would have erred if not.
+ }, cred_id_start, cred_id_start + attested_data.credential_id.0.len()))))
+ } else {
+ // `publicKey` is only allowed to not exist when `CoseAlgorithmIdentifier::Eddsa`,
+ // `CoseAlgorithmIdentifier::Es256`, or `CoseAlgorithmIdentifier::Rs256` is not
+ // used.
+ attested_info.as_ref().map_or_else(
+ || AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
+ if matches!(att_obj.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P384(_)) {
+ // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
+ // is the start of the raw authenticator data which itself contains the raw Credential ID.
+ Ok(Some((CoseAlgorithmIdentifier::Es384, auth_idx, auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
+ } else {
+ Err(Error::missing_field(PUBLIC_KEY))
+ }
+ }),
+ |&(ref attested_data, cred_id_start)| if matches!(attested_data.credential_public_key, UncompressedPubKey::P384(_)) {
+ // Overflow won't occur since this is correct. This is correct since we successfully parsed
+ // `AttestedCredentialData` and calculated `cred_id_start` from it.
+ Ok(Some((CoseAlgorithmIdentifier::Es384, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
+ } else if flag {
+ Err(Error::invalid_type(Unexpected::Other("null"), &format!("{PUBLIC_KEY} to be a base64url-encoded DER-encoded SubjectPublicKeyInfo").as_str()))
+ } else {
+ Err(Error::missing_field(PUBLIC_KEY))
+ }
+ )
+ }
+ },
+ |der| {
+ UncompressedPubKey::from_der(der.0.as_slice()).map_err(Error::custom).and_then(|key| {
+ attested_info.as_ref().map_or_else(
+ || AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
+ if key == att_obj.auth_data.attested_credential_data.credential_public_key {
+ // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
+ // is the start of the raw authenticator data which itself contains the raw Credential ID.
+ Ok((auth_idx, auth_idx+ att_obj.auth_data.attested_credential_data.credential_id.0.len()))
+ } else {
+ Err(Error::invalid_value(Unexpected::Bytes(der.0.as_slice()), &format!("DER-encoded public key to match the public key within the attestation object: {:?}", att_obj.auth_data.attested_credential_data.credential_public_key).as_str()))
+ }
+ }),
+ |&(ref attested_data, cred_id_start)| {
+ if key == attested_data.credential_public_key {
+ // Overflow won't occur since this is correct. This is correct since we successfully parsed
+ // `AttestedCredentialData` and calculated `cred_id_start` from it.
+ Ok((cred_id_start, cred_id_start + attested_data.credential_id.0.len()))
+ } else {
+ Err(Error::invalid_value(Unexpected::Bytes(der.0.as_slice()), &format!("DER-encoded public key to match the public key within the attestation object: {:?}", attested_data.credential_public_key).as_str()))
+ }
+ }
+ ).map(|(start, last)| Some((match key {
+ UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
+ UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
+ UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
+ UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
+ }, start, last)))
+ })
+ }
+ ).and_then(|cred_key_alg_cred_info| {
+ key_alg.ok_or(false).and_then(|opt_alg| opt_alg.ok_or(true)).map_or_else(
+ |flag| {
+ if R {
+ Ok(cred_key_alg_cred_info.map(|info| (info.1, info.2)))
+ } else if flag {
+ Err(Error::invalid_type(Unexpected::Other("null"), &format!("{PUBLIC_KEY_ALGORITHM} to be a base64url-encoded DER-encoded SubjectPublicKeyInfo").as_str()))
+ } else {
+ Err(Error::missing_field(PUBLIC_KEY_ALGORITHM))
+ }
+ },
+ |alg| {
+ cred_key_alg_cred_info.map_or_else(
+ || AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
+ let att_obj_alg = match att_obj.auth_data.attested_credential_data.credential_public_key {
+ UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
+ UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
+ UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
+ UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
+ };
+ if alg == att_obj_alg {
+ // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
+ // is the start of the raw authenticator data which itself contains the raw Credential ID.
+ Ok(Some((auth_idx, auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
+ } else {
+ Err(Error::invalid_value(Unexpected::Other(format!("{alg:?}").as_str()), &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {att_obj_alg:?}").as_str()))
+ }
+ }),
+ |(a, start, last)| if alg == a {
+ Ok(Some((start, last)))
+ } else {
+ Err(Error::invalid_value(Unexpected::Other(format!("{alg:?}").as_str()), &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {a:?}").as_str()))
+ },
+ )
+ }
+ ).map(|cred_info| AuthAttest{ attest: AuthenticatorAttestation::new(client_data_json, attestation_object, transports), cred_info, })
+ })
+ })
+ })
+ }))
+ }
+}
+/// `"clientDataJSON"`
+const CLIENT_DATA_JSON: &str = "clientDataJSON";
+/// `"attestationObject"`
+const ATTESTATION_OBJECT: &str = "attestationObject";
+/// `"authenticatorData"`
+const AUTHENTICATOR_DATA: &str = "authenticatorData";
+/// `"transports"`
+const TRANSPORTS: &str = "transports";
+/// `"publicKey"`
+const PUBLIC_KEY: &str = "publicKey";
+/// `"publicKeyAlgorithm"`
+const PUBLIC_KEY_ALGORITHM: &str = "publicKeyAlgorithm";
+/// Fields in `AuthenticatorAttestationResponseJSON`.
+pub(super) const AUTH_ATTEST_FIELDS: &[&str; 6] = &[
+ CLIENT_DATA_JSON,
+ ATTESTATION_OBJECT,
+ AUTHENTICATOR_DATA,
+ TRANSPORTS,
+ PUBLIC_KEY,
+ PUBLIC_KEY_ALGORITHM,
+];
+impl<'de> Deserialize<'de> for AuthAttest {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_struct(
+ "AuthenticatorAttestation",
+ AUTH_ATTEST_FIELDS,
+ AuthenticatorAttestationVisitor::<false>,
+ )
+ }
+}
+impl<'de> Deserialize<'de> for AuthenticatorAttestation {
+ /// Deserializes a `struct` based on
+ /// [`AuthenticatorAttestationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorattestationresponsejson).
+ ///
+ /// Note unknown keys and duplicate keys are forbidden;
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-clientdatajson),
+ /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-authenticatordata),
+ /// [`publicKey`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-publickey)
+ /// and
+ /// [`attestationObject`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-attestationobject)
+ /// are base64url-decoded;
+ /// [`transports`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-transports)
+ /// is deserialized via [`AuthTransports::deserialize`]; the decoded `publicKey` is parsed according to the
+ /// applicable DER-encoded ASN.1 `SubjectPublicKeyInfo` schema;
+ /// [`publicKeyAlgorithm`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-publickeyalgorithm)
+ /// is deserialized via [`CoseAlgorithmIdentifier::deserialize`]; all `required` fields in the
+ /// `AuthenticatorAttestationResponseJSON` Web IDL `dictionary` exist (and must not be `null`); `publicKey` exists when
+ /// Ed25519, P-256 with SHA-256, or RSASSA-PKCS1-v1_5 with SHA-256 is used (and must not be `null`)
+ /// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#sctn-public-key-easy); the `publicKeyAlgorithm` aligns
+ /// with
+ /// [`credentialPublicKey`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialpublickey)
+ /// within
+ /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata) within the
+ /// decoded `authenticatorData`; the decoded `publicKey` is the same as `credentialPublicKey` within
+ /// `attestedCredentialData` within the decoded `authenticatorData`; and the decoded `authenticatorData` is the
+ /// same as [`authData`](https://www.w3.org/TR/webauthn-3/#attestation-object) within the decoded
+ /// `attestationObject`.
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ AuthAttest::deserialize(deserializer).map(|val| val.attest)
+ }
+}
+/// `Visitor` for `CredentialPropertiesOutput`.
+///
+/// Unknown fields are ignored iff `RELAXED`.
+pub(super) struct CredentialPropertiesOutputVisitor<const RELAXED: bool>;
+impl<'d, const R: bool> Visitor<'d> for CredentialPropertiesOutputVisitor<R> {
+ type Value = CredentialPropertiesOutput;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("CredentialPropertiesOutput")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Allowed fields.
+ enum Field<const IGNORE_UNKNOWN: bool> {
+ /// `rk` field.
+ Rk,
+ /// Unknown field.
+ Other,
+ }
+ impl<'e, const I: bool> Deserialize<'e> for Field<I> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ ///
+ /// Unknown fields are ignored iff `IGNORE_UNKNOWN`.
+ struct FieldVisitor<const IGNORE_UNKNOWN: bool>;
+ impl<const IG: bool> Visitor<'_> for FieldVisitor<IG> {
+ type Value = Field<IG>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{RK}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ RK => Ok(Field::Rk),
+ _ => {
+ if IG {
+ Ok(Field::Other)
+ } else {
+ Err(E::unknown_field(v, PROPS_FIELDS))
+ }
+ }
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut rk = None;
+ while let Some(key) = map.next_key::<Field<R>>()? {
+ match key {
+ Field::Rk => {
+ if rk.is_some() {
+ return Err(Error::duplicate_field(RK));
+ }
+ rk = map.next_value().map(Some)?;
+ }
+ Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
+ }
+ }
+ Ok(CredentialPropertiesOutput { rk: rk.flatten() })
+ }
+}
+/// `"rk"`
+const RK: &str = "rk";
+/// `CredentialPropertiesOutput` fields.
+pub(super) const PROPS_FIELDS: &[&str; 1] = &[RK];
+impl<'de> Deserialize<'de> for CredentialPropertiesOutput {
+ /// Deserializes a `struct` based on
+ /// [`CredentialPropertiesOutput`](https://www.w3.org/TR/webauthn-3/#dictdef-credentialpropertiesoutput).
+ ///
+ /// Note unknown and duplicate keys are forbidden.
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_struct(
+ "CredentialPropertiesOutput",
+ PROPS_FIELDS,
+ CredentialPropertiesOutputVisitor::<false>,
+ )
+ }
+}
+impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfOutputs {
+ /// Deserializes a `struct` based on
+ /// [`AuthenticationExtensionsPRFOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs).
+ ///
+ /// Note unknown and duplicate keys are forbidden;
+ /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled)
+ /// must exist (and not be `null`); and
+ /// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) must not exist,
+ /// be `null`, or be an
+ /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues)
+ /// with no unknown or duplicate keys,
+ /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) must exist but be
+ /// `null`, and
+ /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) can exist but
+ /// must be `null` if so.
+ #[inline]
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ AuthenticationExtensionsPrfOutputsHelper::<false, true, AuthenticationExtensionsPrfValues>::deserialize(deserializer).map(|val| Self {
+ enabled: val.0.unwrap_or_else(|| {
+ unreachable!(
+ "there is a bug in AuthenticationExtensionsPrfOutputsHelper::deserialize"
+ )
+ }),
+ })
+ }
+}
+/// `Visitor` for `ClientExtensionsOutputs`.
+///
+/// Unknown fields are ignored iff `RELAXED`.
+pub(super) struct ClientExtensionsOutputsVisitor<const RELAXED: bool, PROPS, PRF>(
+ pub PhantomData<fn() -> (PROPS, PRF)>,
+);
+impl<'d, const R: bool, C, P> Visitor<'d> for ClientExtensionsOutputsVisitor<R, C, P>
+where
+ C: for<'a> Deserialize<'a> + Into<CredentialPropertiesOutput>,
+ P: for<'a> Deserialize<'a> + Into<AuthenticationExtensionsPrfOutputs>,
+{
+ type Value = ClientExtensionsOutputs;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("ClientExtensionsOutputs")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Allowed fields.
+ enum Field<const IGNORE_UNKNOWN: bool> {
+ /// `credProps` field.
+ CredProps,
+ /// `prf` field.
+ Prf,
+ /// Unknown field.
+ Other,
+ }
+ impl<'e, const I: bool> Deserialize<'e> for Field<I> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ ///
+ /// Unknown fields are ignored iff `IGNORE_UNKNOWN`.
+ struct FieldVisitor<const IGNORE_UNKNOWN: bool>;
+ impl<const IG: bool> Visitor<'_> for FieldVisitor<IG> {
+ type Value = Field<IG>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{CRED_PROPS}' or '{PRF}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ CRED_PROPS => Ok(Field::CredProps),
+ PRF => Ok(Field::Prf),
+ _ => {
+ if IG {
+ Ok(Field::Other)
+ } else {
+ Err(E::unknown_field(v, EXT_FIELDS))
+ }
+ }
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut cred_props = None;
+ let mut prf = None;
+ while let Some(key) = map.next_key::<Field<R>>()? {
+ match key {
+ Field::CredProps => {
+ if cred_props.is_some() {
+ return Err(Error::duplicate_field(CRED_PROPS));
+ }
+ cred_props = map.next_value::<Option<C>>().map(Some)?;
+ }
+ Field::Prf => {
+ if prf.is_some() {
+ return Err(Error::duplicate_field(PRF));
+ }
+ prf = map.next_value::<Option<P>>().map(Some)?;
+ }
+ Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
+ }
+ }
+ Ok(ClientExtensionsOutputs {
+ cred_props: cred_props.flatten().map(Into::into),
+ prf: prf.flatten().map(Into::into),
+ })
+ }
+}
+impl ClientExtensions for ClientExtensionsOutputs {
+ fn empty() -> Self {
+ Self {
+ prf: None,
+ cred_props: None,
+ }
+ }
+}
+/// `"credProps"`
+const CRED_PROPS: &str = "credProps";
+/// `"prf"`
+const PRF: &str = "prf";
+/// `AuthenticationExtensionsClientOutputsJSON` fields.
+pub(super) const EXT_FIELDS: &[&str; 2] = &[CRED_PROPS, PRF];
+impl<'de> Deserialize<'de> for ClientExtensionsOutputs {
+ /// Deserializes a `struct` based on
+ /// [`AuthenticationExtensionsClientOutputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientoutputsjson).
+ ///
+ /// Note that unknown and duplicate keys are forbidden;
+ /// [`credProps`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-credprops) is
+ /// `null` or deserialized via [`CredentialPropertiesOutput::deserialize`]; and
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf) is `null`
+ /// or deserialized via [`AuthenticationExtensionsPrfOutputs::deserialize`].
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_struct(
+ "ClientExtensionsOutputs",
+ EXT_FIELDS,
+ ClientExtensionsOutputsVisitor::<
+ false,
+ CredentialPropertiesOutput,
+ AuthenticationExtensionsPrfOutputs,
+ >(PhantomData),
+ )
+ }
+}
+impl<'de> Deserialize<'de> for Registration {
+ /// Deserializes a `struct` based on
+ /// [`RegistrationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson).
+ ///
+ /// Note that unknown and duplicate keys are forbidden;
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-id) and
+ /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-rawid) are deserialized
+ /// via [`CredentialId::deserialize`];
+ /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-response) is deserialized
+ /// via [`AuthenticatorAttestation::deserialize`];
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-authenticatorattachment)
+ /// is `null` or deserialized via [`AuthenticatorAttachment::deserialize`];
+ /// [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-clientextensionresults)
+ /// is deserialized via [`ClientExtensionsOutputs::deserialize`]; all `required` fields in the
+ /// `RegistrationResponseJSON` Web IDL `dictionary` exist (and are not `null`);
+ /// [`type`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-type) is `"public-key"`;
+ /// and the decoded `id`, decoded `rawId`, and
+ /// [`credentialId`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialid) within
+ /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata) within
+ /// [`authData`](https://www.w3.org/TR/webauthn-3/#attestation-object) within the decoded
+ /// [`attestationObject`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-attestationobject)
+ /// are all the same.
+ #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
+ #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ PublicKeyCredential::<false, true, AuthAttest, ClientExtensionsOutputs>::deserialize(deserializer).and_then(|cred| {
+ let id = cred.id.unwrap_or_else(|| unreachable!("there is a bug in PublicKeyCredential::deserialize"));
+ cred.response.cred_info.map_or_else(
+ || AttestationObject::try_from(cred.response.attest.attestation_object()).map_err(Error::custom).and_then(|att_obj| {
+ if id == att_obj.auth_data.attested_credential_data.credential_id {
+ Ok(())
+ } else {
+ Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", att_obj.auth_data.attested_credential_data.credential_id.0).as_str()))
+ }
+ }),
+ // `start` and `last` were calculated based on `cred.response.attest.attestation_object()`
+ // and represent the starting and ending index of the `CredentialId`; therefore this is correct
+ // let alone won't `panic`.
+ |(start, last)| if id.0 == cred.response.attest.attestation_object()[start..last] {
+ Ok(())
+ } else {
+ Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", &cred.response.attest.attestation_object()[start..last]).as_str()))
+ },
+ ).map(|()| Self { response: cred.response.attest, authenticator_attachment: cred.authenticator_attachment, client_extension_results: cred.client_extension_results, })
+ })
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::{
+ super::{
+ super::BASE64URL_NOPAD_ENC, cbor, AuthenticatorAttachment, Ed25519PubKey, Registration,
+ RsaPubKey, UncompressedP256PubKey, UncompressedP384PubKey, ALG, EC2, EDDSA, ES256,
+ ES384, KTY, OKP, RSA,
+ },
+ spki::SubjectPublicKeyInfo,
+ CoseAlgorithmIdentifier,
+ };
+ use ed25519_dalek::{pkcs8::EncodePublicKey, VerifyingKey};
+ use p256::{
+ elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _},
+ EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key,
+ };
+ use p384::{EncodedPoint as P384Pt, PublicKey as P384PubKey, SecretKey as P384Key};
+ use rsa::{
+ 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]));
+ }
+ #[test]
+ fn p256_spki() {
+ let key = P256Key::from_bytes(
+ &[
+ 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115,
+ 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95,
+ ]
+ .into(),
+ )
+ .unwrap()
+ .public_key();
+ let enc_key = key.to_encoded_point(false);
+ assert!(
+ UncompressedP256PubKey::from_der(key.to_public_key_der().unwrap().as_bytes()).map_or(
+ false,
+ |k| k.0 == enc_key.x().unwrap().as_slice()
+ && k.1 == enc_key.y().unwrap().as_slice()
+ )
+ );
+ }
+ #[test]
+ fn p384_spki() {
+ let key = P384Key::from_bytes(
+ &[
+ 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232,
+ 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1,
+ 32, 86, 220, 68, 182, 11, 105, 223, 75, 70,
+ ]
+ .into(),
+ )
+ .unwrap()
+ .public_key();
+ let enc_key = key.to_encoded_point(false);
+ assert!(
+ UncompressedP384PubKey::from_der(key.to_public_key_der().unwrap().as_bytes()).map_or(
+ false,
+ |k| k.0 == enc_key.x().unwrap().as_slice()
+ && k.1 == enc_key.y().unwrap().as_slice()
+ )
+ );
+ }
+ #[test]
+ fn rsa_spki() {
+ let n = [
+ 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107,
+ 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206,
+ 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165,
+ 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204,
+ 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64,
+ 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105,
+ 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55,
+ 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21,
+ 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157,
+ 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188,
+ 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56,
+ 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13,
+ 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132,
+ 153, 79, 0, 133, 78, 7, 218, 165, 241,
+ ];
+ let e = 65537u32;
+ let d = [
+ 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0,
+ 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168,
+ 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108,
+ 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102,
+ 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247,
+ 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166,
+ 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111,
+ 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242,
+ 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212,
+ 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83,
+ 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153,
+ 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142,
+ 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45,
+ 50, 23, 3, 25, 253, 87, 227, 79, 119, 161,
+ ];
+ let p = BigUint::from_slice(
+ [
+ 352691927, 1294578443, 816143558, 690659917, 1161596366, 1544791087, 3999549486,
+ 3319149924, 2349250979, 1304689381, 3959753736, 3377900978, 866506027, 1671521644,
+ 3926847564, 898221388, 3448219846, 494454484, 3915534864, 2869735916, 2456511629,
+ 3397234721, 3012775852, 3472309790, 1923617705, 2993441050, 3210302569, 3605331368,
+ 3352563766, 688081007, 4104512503, 4145593376,
+ ]
+ .as_slice(),
+ );
+ let p_2 = BigUint::from_slice(
+ [
+ 4039514409, 964284038, 3230008587, 3320139220, 3562360334, 3165876926, 212773653,
+ 2752465512, 2973674888, 1717425549, 2084262803, 3585031058, 4162394935, 1428626842,
+ 1015474994, 3283774155, 2840050110, 190639246, 147241978, 2994256073, 4081014755,
+ 3102401369, 3547397148, 1545029057, 895305733, 2689179461, 1593439337, 3960057302,
+ 193068804, 2835123424, 4054880057, 4200258364,
+ ]
+ .as_slice(),
+ );
+ let key = RsaPrivateKey::from_components(
+ BigUint::from_bytes_le(n.as_slice()),
+ e.into(),
+ BigUint::from_bytes_le(d.as_slice()),
+ vec![p, p_2],
+ )
+ .unwrap()
+ .to_public_key();
+ assert!(
+ RsaPubKey::from_der(key.to_public_key_der().unwrap().as_bytes())
+ .map_or(false, |k| k.0 == key.n().to_bytes_be()
+ && BigUint::from(k.1) == *key.e())
+ );
+ }
+ #[test]
+ fn eddsa_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_24,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // Ed25519 COSE key.
+ cbor::MAP_4,
+ KTY,
+ OKP,
+ ALG,
+ EDDSA,
+ // `crv`.
+ cbor::NEG_ONE,
+ // `Ed25519`.
+ cbor::SIX,
+ // `x`.
+ cbor::NEG_TWO,
+ cbor::BYTES_INFO_24,
+ 32,
+ // Compressed y-coordinate.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = VerifyingKey::from_bytes(&[1; 32])
+ .unwrap()
+ .to_public_key_der()
+ .unwrap();
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 113..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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
+ )
+ && 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(
+ BASE64URL_NOPAD_ENC
+ .decode("ABABABABABABABABABABAA".as_bytes())
+ .unwrap()
+ .as_slice(),
+ ),
+ &format!("id and rawId to match: CredentialId({:?})", [0; 16]).as_str(),
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "ABABABABABABABABABABAA",
+ "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
+ );
+ // missing `id`.
+ err = Error::missing_field("id").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ 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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `id`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"id")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ 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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // missing `rawId`.
+ err = Error::missing_field("rawId").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ 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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `rawId`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"rawId")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ 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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `id` and the credential id in authenticator data mismatch.
+ err = Error::invalid_value(
+ Unexpected::Bytes(
+ BASE64URL_NOPAD_ENC
+ .decode("ABABABABABABABABABABAA".as_bytes())
+ .unwrap()
+ .as_slice(),
+ ),
+ &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", [0; 16]).as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "ABABABABABABABABABABAA",
+ "rawId": "ABABABABABABABABABABAA",
+ "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
+ );
+ // `authenticatorData` mismatches `authData` in attestation object.
+ let mut bad_auth = [0; 113];
+ bad_auth.copy_from_slice(&att_obj[att_obj.len() - 113..]);
+ bad_auth[113 - 32..].copy_from_slice([0; 32].as_slice());
+ err = Error::invalid_value(
+ Unexpected::Bytes(bad_auth.as_slice()),
+ &format!("authenticator data to match the authenticator data portion of attestation object: {:?}", &att_obj[att_obj.len() - bad_auth.len()..]).as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": BASE64URL_NOPAD_ENC.encode(bad_auth.as_slice()),
+ "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 `authenticatorData`.
+ err = Error::missing_field("authenticatorData")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ 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()
+ )
+ .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::<Registration>(
+ 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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `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!(
+ 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()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `publicKeyAlgorithm`.
+ err = Error::missing_field("publicKeyAlgorithm")
+ .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,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `publicKeyAlgorithm`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm")
+ .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": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `publicKey` mismatch.
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: Ed25519(Ed25519PubKey({:?}))",
+ &att_obj[att_obj.len() - 32..],
+ )
+ .as_str(),
+ )
+ .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": BASE64URL_NOPAD_ENC.encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes()[..err.len()],
+ err
+ );
+ // Missing `publicKey` when using EdDSA, ES256, or RS256.
+ err = Error::missing_field("publicKey").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": [],
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `publicKey` when using EdDSA, ES256, or RS256.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKey")
+ .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": null,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `transports`.
+ err = Error::missing_field("transports").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,
+ "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
+ );
+ // 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));
+ // `null` `transports`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"transports")
+ .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": null,
+ "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
+ );
+ // 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::<Registration>(
+ 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::<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"),
+ &"'platform' or 'cross-platform'",
+ )
+ .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,
+ },
+ "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::<Registration>(
+ 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::<Registration>(
+ 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::<Registration>(
+ 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::<Registration>(
+ 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::<Registration>(
+ 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::<Registration>(
+ 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::<Registration>(
+ 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`.
+ err = Error::missing_field("clientExtensionResults")
+ .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,
+ },
+ "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"),
+ &"clientExtensionResults to be a map of allowed client extensions",
+ )
+ .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": null,
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `type`.
+ err = Error::missing_field("type").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": {},
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `type`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"type to be 'public-key'")
+ .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": {},
+ "type": null
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // 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::<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()
+ )
+ .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::<Registration>(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::<Registration>(serde_json::json!({}).to_string().as_str())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown field in `response`.
+ err = Error::unknown_field(
+ "foo",
+ [
+ "clientDataJSON",
+ "attestationObject",
+ "authenticatorData",
+ "transports",
+ "publicKey",
+ "publicKeyAlgorithm",
+ ]
+ .as_slice(),
+ )
+ .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,
+ "foo": true,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field in `response`.
+ err = Error::duplicate_field("transports")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ 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`.
+ err = Error::unknown_field(
+ "foo",
+ [
+ "id",
+ "type",
+ "rawId",
+ "response",
+ "authenticatorAttachment",
+ "clientExtensionResults",
+ ]
+ .as_slice(),
+ )
+ .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": {},
+ "type": "public-key",
+ "foo": true,
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field in `PublicKeyCredential`.
+ err = Error::duplicate_field("id").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ 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
+ );
+ }
+ #[test]
+ fn client_extensions() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_24,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // Ed25519 COSE key.
+ cbor::MAP_4,
+ KTY,
+ OKP,
+ ALG,
+ EDDSA,
+ // `crv`.
+ cbor::NEG_ONE,
+ // `Ed25519`.
+ cbor::SIX,
+ // `x`.
+ cbor::NEG_TWO,
+ cbor::BYTES_INFO_24,
+ 32,
+ // Compressed y-coordinate.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = VerifyingKey::from_bytes(&[1; 32])
+ .unwrap()
+ .to_public_key_der()
+ .unwrap();
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 113..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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()));
+ // `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()));
+ // `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()));
+ // Unknown `clientExtensionResults`.
+ let mut err = Error::unknown_field("CredProps", ["credProps", "prf"].as_slice())
+ .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": {
+ "CredProps": {
+ "rk": true
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field.
+ err = Error::duplicate_field("credProps").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"transports\": [],
+ \"publicKey\": \"{b64_key}\",
+ \"publicKeyAlgorithm\": -8,
+ \"attestationObject\": \"{b64_aobj}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"credProps\": null,
+ \"credProps\": null
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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()));
+ // `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()));
+ // `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()));
+ // Invalid `rk`.
+ err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean")
+ .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": {
+ "credProps": {
+ "rk": 3
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown `credProps` field.
+ err = Error::unknown_field("Rk", ["rk"].as_slice())
+ .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": {
+ "credProps": {
+ "Rk": true,
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field in `credProps`.
+ err = Error::duplicate_field("rk").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"transports\": [],
+ \"publicKey\": \"{b64_key}\",
+ \"publicKeyAlgorithm\": -8,
+ \"attestationObject\": \"{b64_aobj}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"credProps\": {{
+ \"rk\": true,
+ \"rk\": true
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `enabled`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"a boolean")
+ .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": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `enabled`.
+ err = Error::missing_field("enabled").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": {}
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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
+ .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
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| !prf.enabled)));
+ // Invalid `enabled`.
+ err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean")
+ .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": 3
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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
+ .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",
+ )
+ .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": false,
+ "results": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field in `prf`.
+ err = Error::duplicate_field("enabled").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ format!(
+ "{{
+ \"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"rawId\": \"AAAAAAAAAAAAAAAAAAAAAA\",
+ \"response\": {{
+ \"clientDataJSON\": \"{b64_cdata}\",
+ \"authenticatorData\": \"{b64_adata}\",
+ \"transports\": [],
+ \"publicKey\": \"{b64_key}\",
+ \"publicKeyAlgorithm\": -8,
+ \"attestationObject\": \"{b64_aobj}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"prf\": {{
+ \"enabled\": true,
+ \"enabled\": true
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // 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",
+ "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
+ .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
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled)));
+ // Non-`null` `first`.
+ err = Error::invalid_type(Unexpected::Option, &"null")
+ .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": {
+ "first": ""
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Non-`null` `second`.
+ err = Error::invalid_type(Unexpected::Option, &"null")
+ .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": {
+ "first": null,
+ "second": ""
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown `prf` field.
+ err = Error::unknown_field("Results", ["enabled", "results"].as_slice())
+ .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": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Unknown `results` field.
+ err = Error::unknown_field("Second", ["first", "second"].as_slice())
+ .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": {
+ "first": null,
+ "Second": null
+ }
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field in `results`.
+ err = Error::duplicate_field("first").to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<Registration>(
+ format!(
+ "{{
+ \"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,
+ \"first\": null
+ }}
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ }
+ #[test]
+ fn es256_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let mut att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_24,
+ 148,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // P-256 COSE key.
+ cbor::MAP_5,
+ KTY,
+ EC2,
+ ALG,
+ ES256,
+ // `crv`.
+ cbor::NEG_ONE,
+ // `P-256`.
+ cbor::ONE,
+ // `x`.
+ cbor::NEG_TWO,
+ cbor::BYTES_INFO_24,
+ 32,
+ // x-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `y`.
+ cbor::NEG_THREE,
+ cbor::BYTES_INFO_24,
+ 32,
+ // y-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ];
+ let key = P256Key::from_bytes(
+ &[
+ 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115,
+ 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95,
+ ]
+ .into(),
+ )
+ .unwrap()
+ .public_key();
+ let enc_key = key.to_encoded_point(false);
+ let pub_key = key.to_public_key_der().unwrap();
+ let att_obj_len = att_obj.len();
+ att_obj[att_obj_len - 67..att_obj_len - 35]
+ .copy_from_slice(enc_key.x().unwrap().as_slice());
+ att_obj[att_obj_len - 32..].copy_from_slice(enc_key.y().unwrap().as_slice());
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 148..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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()));
+ // `publicKeyAlgorithm` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es256).as_str()
+ )
+ .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": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `publicKeyAlgorithm`.
+ err = Error::missing_field("publicKeyAlgorithm")
+ .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,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `publicKeyAlgorithm`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm")
+ .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": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `publicKey` mismatch.
+ let bad_pub_key = P256PubKey::from_encoded_point(&P256Pt::from_affine_coordinates(
+ &[
+ 66, 71, 188, 41, 125, 2, 226, 44, 148, 62, 63, 190, 172, 64, 33, 214, 6, 37, 148,
+ 23, 240, 235, 203, 84, 112, 219, 232, 197, 54, 182, 17, 235,
+ ]
+ .into(),
+ &[
+ 22, 172, 123, 13, 170, 242, 217, 248, 193, 209, 206, 163, 92, 4, 162, 168, 113, 63,
+ 2, 117, 16, 223, 239, 196, 109, 179, 10, 130, 43, 213, 205, 92,
+ ]
+ .into(),
+ false,
+ ))
+ .unwrap();
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: P256(UncompressedP256PubKey({:?}, {:?}))",
+ &att_obj[att_obj.len() - 67..att_obj.len() - 35],
+ &att_obj[att_obj.len() - 32..],
+ )
+ .as_str(),
+ )
+ .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": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes()[..err.len()],
+ err
+ );
+ // Missing `publicKey` when using EdDSA, ES256, or RS256.
+ err = Error::missing_field("publicKey").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": [],
+ "publicKeyAlgorithm": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `publicKey` when using EdDSA, ES256, or RS256.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKey")
+ .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": null,
+ "publicKeyAlgorithm": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ }
+ #[test]
+ fn es384_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let mut att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_24,
+ 181,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // P-384 COSE key.
+ cbor::MAP_5,
+ KTY,
+ EC2,
+ ALG,
+ cbor::NEG_INFO_24,
+ ES384,
+ // `crv`.
+ cbor::NEG_ONE,
+ // `P-384`.
+ cbor::TWO,
+ // `x`.
+ cbor::NEG_TWO,
+ cbor::BYTES_INFO_24,
+ 48,
+ // x-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `y`.
+ cbor::NEG_THREE,
+ cbor::BYTES_INFO_24,
+ 48,
+ // y-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ];
+ let key = P384Key::from_bytes(
+ &[
+ 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232,
+ 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1,
+ 32, 86, 220, 68, 182, 11, 105, 223, 75, 70,
+ ]
+ .into(),
+ )
+ .unwrap()
+ .public_key();
+ let enc_key = key.to_encoded_point(false);
+ let pub_key = key.to_public_key_der().unwrap();
+ let att_obj_len = att_obj.len();
+ att_obj[att_obj_len - 99..att_obj_len - 51]
+ .copy_from_slice(enc_key.x().unwrap().as_slice());
+ att_obj[att_obj_len - 48..].copy_from_slice(enc_key.y().unwrap().as_slice());
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 181..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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()));
+ // `publicKeyAlgorithm` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).as_str()
+ )
+ .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": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `publicKeyAlgorithm`.
+ err = Error::missing_field("publicKeyAlgorithm")
+ .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,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `publicKeyAlgorithm`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm")
+ .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": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `publicKey` mismatch.
+ let bad_pub_key = P384PubKey::from_encoded_point(&P384Pt::from_affine_coordinates(
+ &[
+ 192, 10, 27, 46, 66, 67, 80, 98, 33, 230, 156, 95, 1, 135, 150, 110, 64, 243, 22,
+ 118, 5, 255, 107, 44, 234, 111, 217, 105, 125, 114, 39, 7, 126, 2, 191, 111, 48,
+ 93, 234, 175, 18, 172, 59, 28, 97, 106, 178, 152,
+ ]
+ .into(),
+ &[
+ 57, 36, 196, 12, 109, 129, 253, 115, 88, 154, 6, 43, 195, 85, 169, 5, 230, 51, 28,
+ 205, 142, 28, 150, 35, 24, 222, 170, 253, 14, 248, 84, 151, 109, 191, 152, 111,
+ 222, 70, 134, 247, 109, 171, 211, 33, 214, 217, 200, 111,
+ ]
+ .into(),
+ false,
+ ))
+ .unwrap();
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: P384(UncompressedP384PubKey({:?}, {:?}))",
+ &att_obj[att_obj.len() - 99..att_obj.len() - 51],
+ &att_obj[att_obj.len() - 48..],
+ )
+ .as_str(),
+ )
+ .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": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -35,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes()[..err.len()],
+ 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());
+ // `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
+ err = Error::invalid_value(
+ Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).as_str()
+ )
+ .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": [],
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // `publicKeyAlgorithm` mismatch when `publicKey` is null.
+ err = Error::invalid_value(
+ Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).as_str()
+ )
+ .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": null,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ }
+ #[test]
+ fn rs256_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let mut att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_25,
+ 1,
+ 87,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // RSA COSE key.
+ cbor::MAP_4,
+ KTY,
+ RSA,
+ ALG,
+ cbor::NEG_INFO_25,
+ // RS256.
+ 1,
+ 0,
+ // `n`.
+ cbor::NEG_ONE,
+ cbor::BYTES_INFO_25,
+ 1,
+ 0,
+ // n. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `e`.
+ cbor::NEG_TWO,
+ cbor::BYTES | 3,
+ // e.
+ 1,
+ 0,
+ 1,
+ ];
+ let n = [
+ 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107,
+ 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206,
+ 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165,
+ 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204,
+ 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64,
+ 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105,
+ 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55,
+ 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21,
+ 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157,
+ 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188,
+ 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56,
+ 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13,
+ 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132,
+ 153, 79, 0, 133, 78, 7, 218, 165, 241,
+ ];
+ let e = 65537u32;
+ let d = [
+ 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0,
+ 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168,
+ 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108,
+ 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102,
+ 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247,
+ 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166,
+ 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111,
+ 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242,
+ 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212,
+ 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83,
+ 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153,
+ 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142,
+ 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45,
+ 50, 23, 3, 25, 253, 87, 227, 79, 119, 161,
+ ];
+ let p = BigUint::from_slice(
+ [
+ 352691927, 1294578443, 816143558, 690659917, 1161596366, 1544791087, 3999549486,
+ 3319149924, 2349250979, 1304689381, 3959753736, 3377900978, 866506027, 1671521644,
+ 3926847564, 898221388, 3448219846, 494454484, 3915534864, 2869735916, 2456511629,
+ 3397234721, 3012775852, 3472309790, 1923617705, 2993441050, 3210302569, 3605331368,
+ 3352563766, 688081007, 4104512503, 4145593376,
+ ]
+ .as_slice(),
+ );
+ let p_2 = BigUint::from_slice(
+ [
+ 4039514409, 964284038, 3230008587, 3320139220, 3562360334, 3165876926, 212773653,
+ 2752465512, 2973674888, 1717425549, 2084262803, 3585031058, 4162394935, 1428626842,
+ 1015474994, 3283774155, 2840050110, 190639246, 147241978, 2994256073, 4081014755,
+ 3102401369, 3547397148, 1545029057, 895305733, 2689179461, 1593439337, 3960057302,
+ 193068804, 2835123424, 4054880057, 4200258364,
+ ]
+ .as_slice(),
+ );
+ let key = RsaPrivateKey::from_components(
+ BigUint::from_bytes_le(n.as_slice()),
+ e.into(),
+ BigUint::from_bytes_le(d.as_slice()),
+ vec![p, p_2],
+ )
+ .unwrap()
+ .to_public_key();
+ let pub_key = key.to_public_key_der().unwrap();
+ let att_obj_len = att_obj.len();
+ att_obj[att_obj_len - 261..att_obj_len - 5]
+ .copy_from_slice(key.n().to_bytes_be().as_slice());
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 343..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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()));
+ // `publicKeyAlgorithm` mismatch.
+ let mut err = Error::invalid_value(
+ Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Rs256).as_str()
+ )
+ .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": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `publicKeyAlgorithm`.
+ err = Error::missing_field("publicKeyAlgorithm")
+ .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,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `publicKeyAlgorithm`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKeyAlgorithm")
+ .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": null,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `publicKey` mismatch.
+ let bad_pub_key = RsaPrivateKey::from_components(
+ BigUint::from_slice(
+ [
+ 1268883887, 2823353396, 2015459101, 2565332483, 1399879646, 2924146141,
+ 4220770383, 1927962357, 4262532606, 2135651080, 1832605590, 1515549926,
+ 3644825611, 4206568969, 2754000866, 320264886, 3679698234, 1661964299,
+ 959358615, 2210230033, 2052419982, 355790524, 3278273908, 2619188662,
+ 2625484501, 48052312, 1943153506, 1483277344, 3973029557, 4043176610,
+ 855443528, 2857170908, 3890300047, 301219953, 568959626, 3742057218,
+ 3248023740, 888348692, 4077005632, 3902164232, 2136970349, 581060407,
+ 881283894, 706789292, 3469945706, 3899549796, 3027774213, 3918538918,
+ 1736861679, 3096109311, 612338128, 3388510141, 3895712258, 2085822048,
+ 3004690797, 3572406263, 3744148684, 179106196, 1147050987, 3212056692,
+ 595539286, 1003275909, 17854028, 2642908175,
+ ]
+ .as_slice(),
+ ),
+ 65537u32.into(),
+ BigUint::from_slice(
+ [
+ 4219166081, 3411287400, 3981141108, 1678549103, 2990099628, 1028778896,
+ 672985971, 2520258231, 1054615108, 2922409705, 1844757795, 1160015252,
+ 1910592069, 468649647, 4013057473, 772236922, 1958956898, 2475335323,
+ 3977796915, 1829655286, 1576008336, 2187384383, 2445706978, 1642531745,
+ 1610593494, 4268513438, 3095769587, 1486118748, 4109728823, 2030327380,
+ 2959206188, 681254334, 1353008441, 725092776, 2634942185, 1480646512,
+ 390137741, 1392955456, 4172679229, 2746438782, 2237328976, 2974223876,
+ 2535267247, 3282201811, 1453825287, 3948348329, 3639451225, 1053160223,
+ 3867366405, 204601530, 2268984413, 4053930420, 2331079437, 2795201243,
+ 621559743, 1420993793, 693127368, 2379843661, 4078948854, 4130031519,
+ 1957410463, 3951952652, 1514579162, 1261104787,
+ ]
+ .as_slice(),
+ ),
+ vec![
+ BigUint::from_slice(
+ [
+ 477022167, 1829769280, 2090244202, 1551476276, 1157631474, 2890438663,
+ 3030138742, 490022796, 816963781, 1097260329, 1043839249, 132356315,
+ 2333006670, 2559626311, 4109838094, 1022025893, 518867669, 2331160934,
+ 796532648, 1910610894, 4103647079, 3748718875, 3000444664, 2030629908,
+ 2051410714, 1470584080, 3823425600, 150616493, 3406571229, 728760788,
+ 1642158920, 3248110052,
+ ]
+ .as_slice(),
+ ),
+ BigUint::from_slice(
+ [
+ 2563529193, 1846080031, 2674900518, 1429039465, 4196332559, 1876681390,
+ 2277818219, 2814016273, 3312979285, 3981345183, 451288984, 3552968165,
+ 2390674537, 2887399418, 103653441, 3997324899, 2875328107, 2697584733,
+ 2018692127, 116301540, 2576747710, 1194942447, 2615930724, 3775252553,
+ 808368511, 2384549107, 387191569, 980553943, 2487815891, 4238343336,
+ 3546626429, 3494710460,
+ ]
+ .as_slice(),
+ ),
+ ],
+ )
+ .unwrap()
+ .to_public_key();
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: Rsa(RsaPubKey({:?}, 65537))",
+ &att_obj[att_obj.len() - 261..att_obj.len() - 5],
+ )
+ .as_str(),
+ )
+ .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": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -257,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes()[..err.len()],
+ err
+ );
+ // Missing `publicKey` when using EdDSA, ES256, or RS256.
+ err = Error::missing_field("publicKey").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": [],
+ "publicKeyAlgorithm": -257,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `publicKey` when using EdDSA, ES256, or RS256.
+ err = Error::invalid_type(Unexpected::Other("null"), &"publicKey")
+ .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": null,
+ "publicKeyAlgorithm": -257,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ }
+}
diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs
@@ -0,0 +1,3954 @@
+#[cfg(doc)]
+use super::super::Challenge;
+use super::{
+ super::{
+ register::ser::{
+ AuthenticatorAttestationVisitor, ClientExtensionsOutputsVisitor, AUTH_ATTEST_FIELDS,
+ EXT_FIELDS,
+ },
+ ser::{AuthenticationExtensionsPrfOutputsHelper, ClientExtensions, PublicKeyCredential},
+ ser_relaxed::AuthenticationExtensionsPrfValuesRelaxed,
+ },
+ 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};
+/// `newtype` around `CredentialPropertiesOutput` with a "relaxed" [`Self::deserialize`] implementation.
+#[derive(Debug)]
+pub struct CredentialPropertiesOutputRelaxed(pub CredentialPropertiesOutput);
+impl From<CredentialPropertiesOutputRelaxed> for CredentialPropertiesOutput {
+ #[inline]
+ fn from(value: CredentialPropertiesOutputRelaxed) -> Self {
+ value.0
+ }
+}
+impl<'de> Deserialize<'de> for CredentialPropertiesOutputRelaxed {
+ /// Same as [`CredentialPropertiesOutput::deserialize`] except unknown keys are ignored.
+ ///
+ /// Note that duplicate keys are still forbidden.
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer
+ .deserialize_struct(
+ "CredentialPropertiesOutputRelaxed",
+ PROPS_FIELDS,
+ CredentialPropertiesOutputVisitor::<true>,
+ )
+ .map(Self)
+ }
+}
+/// `newtype` around `AuthenticationExtensionsPrfOutputs` with a "relaxed" [`Self::deserialize`] implementation.
+#[derive(Debug)]
+pub struct AuthenticationExtensionsPrfOutputsRelaxed(AuthenticationExtensionsPrfOutputs);
+impl From<AuthenticationExtensionsPrfOutputsRelaxed> for AuthenticationExtensionsPrfOutputs {
+ #[inline]
+ fn from(value: AuthenticationExtensionsPrfOutputsRelaxed) -> Self {
+ value.0
+ }
+}
+impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfOutputsRelaxed {
+ /// Same as [`AuthenticationExtensionsPrfOutputs::deserialize`] except unknown keys are ignored.
+ ///
+ /// Note that duplicate keys are still forbidden;
+ /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled) must still exist
+ /// (and not be `null`); and
+ /// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) must not exist,
+ /// be `null`, or be an
+ /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues)
+ /// such that unknown keys are ignored, duplicate keys are forbidden,
+ /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is not required but
+ /// if it exists it must be `null`, and
+ /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) can exist but
+ /// must be `null` if so.
+ #[inline]
+ #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ AuthenticationExtensionsPrfOutputsHelper::<
+ true,
+ true,
+ AuthenticationExtensionsPrfValuesRelaxed,
+ >::deserialize(deserializer)
+ .map(|v| {
+ Self(AuthenticationExtensionsPrfOutputs {
+ enabled: v.0.unwrap_or_else(|| {
+ unreachable!(
+ "there is a bug in AuthenticationExtensionsPrfOutputsHelper::deserialize"
+ )
+ }),
+ })
+ })
+ }
+}
+/// `newtype` around `ClientExtensionsOutputs` with a "relaxed" [`Self::deserialize`] implementation.
+#[derive(Debug)]
+pub struct ClientExtensionsOutputsRelaxed(pub ClientExtensionsOutputs);
+impl ClientExtensions for ClientExtensionsOutputsRelaxed {
+ fn empty() -> Self {
+ Self(ClientExtensionsOutputs::empty())
+ }
+}
+impl<'de> Deserialize<'de> for ClientExtensionsOutputsRelaxed {
+ /// Same as [`ClientExtensionsOutputs::deserialize`] except unknown keys are ignored,
+ /// [`credProps`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-credprops) is
+ /// `null` or deserialized via [`CredentialPropertiesOutputRelaxed::deserialize`], and
+ /// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf) is
+ /// `null` or deserialized via [`AuthenticationExtensionsPrfOutputsRelaxed::deserialize`].
+ ///
+ /// Note that duplicate keys are still forbidden.
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer
+ .deserialize_struct(
+ "ClientExtensionsOutputsRelaxed",
+ EXT_FIELDS,
+ ClientExtensionsOutputsVisitor::<
+ true,
+ CredentialPropertiesOutputRelaxed,
+ AuthenticationExtensionsPrfOutputsRelaxed,
+ >(PhantomData),
+ )
+ .map(Self)
+ }
+}
+/// `newtype` around `AuthAttest` with a "relaxed" [`Self::deserialize`] implementation.
+struct AuthAttestRelaxed(pub AuthAttest);
+impl<'de> Deserialize<'de> for AuthAttestRelaxed {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer
+ .deserialize_struct(
+ "AuthenticatorAttestation",
+ AUTH_ATTEST_FIELDS,
+ AuthenticatorAttestationVisitor::<true>,
+ )
+ .map(Self)
+ }
+}
+/// `newtype` around `AuthenticatorAttestation` with a "relaxed" [`Self::deserialize`] implementation.
+#[derive(Debug)]
+pub struct AuthenticatorAttestationRelaxed(pub AuthenticatorAttestation);
+impl<'de> Deserialize<'de> for AuthenticatorAttestationRelaxed {
+ /// Same as [`AuthenticatorAttestation::deserialize`] except unknown keys are ignored and only
+ /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-clientdatajson)
+ /// and
+ /// [`attestationObject`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-attestationobject)
+ /// are required (and must not be `null`). For the other fields, they are allowed to not exist or be `null`.
+ ///
+ /// Note that duplicate keys are still forbidden, and data matching still applies when applicable.
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ AuthAttestRelaxed::deserialize(deserializer).map(|v| Self(v.0.attest))
+ }
+}
+/// `newtype` around `Registration` with a "relaxed" [`Self::deserialize`] implementation.
+#[derive(Debug)]
+pub struct RegistrationRelaxed(pub Registration);
+impl<'de> Deserialize<'de> for RegistrationRelaxed {
+ /// Same as [`Registration::deserialize`] except unknown keys are ignored,
+ /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-response) is deserialized
+ /// via [`AuthenticatorAttestationRelaxed::deserialize`],
+ /// [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-clientextensionresults)
+ /// is `null` or deserialized via [`ClientExtensionsOutputsRelaxed::deserialize`], and only `response` is required.
+ /// For the other fields, they are allowed to not exist or be `null`.
+ ///
+ /// Note that duplicate keys are still forbidden, and data matching still applies when applicable.
+ #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ PublicKeyCredential::<true, true, AuthAttestRelaxed, ClientExtensionsOutputsRelaxed>::deserialize(deserializer).and_then(|cred| {
+ cred.id.map_or_else(|| Ok(()), |id| {
+ cred.response.0.cred_info.map_or_else(
+ || AttestationObject::try_from(cred.response.0.attest.attestation_object()).map_err(Error::custom).and_then(|att_obj| {
+ if id == att_obj.auth_data.attested_credential_data.credential_id {
+ Ok(())
+ } else {
+ Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", att_obj.auth_data.attested_credential_data.credential_id.0).as_str()))
+ }
+ }),
+ // `start` and `last` were calculated based on `cred.response.attest.attestation_object()`
+ // and represent the starting and ending index of the `CredentialId`; therefore this is correct
+ // let alone won't `panic`.
+ |(start, last)| if id.0 == cred.response.0.attest.attestation_object()[start..last] {
+ Ok(())
+ } else {
+ Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", &cred.response.0.attest.attestation_object()[start..last]).as_str()))
+ }
+ )
+ }).map(|()| {
+ Self(Registration { response: cred.response.0.attest, authenticator_attachment: cred.authenticator_attachment, client_extension_results: cred.client_extension_results.0 })
+ })
+ })
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::{
+ super::{
+ super::{super::request::register::CoseAlgorithmIdentifier, BASE64URL_NOPAD_ENC},
+ cbor, AuthenticatorAttachment, ALG, EC2, EDDSA, ES256, ES384, KTY, OKP, RSA,
+ },
+ RegistrationRelaxed,
+ };
+ use ed25519_dalek::{pkcs8::EncodePublicKey, VerifyingKey};
+ use p256::{
+ elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _},
+ EncodedPoint as P256Pt, PublicKey as P256PubKey, SecretKey as P256Key,
+ };
+ use p384::{EncodedPoint as P384Pt, PublicKey as P384PubKey, SecretKey as P384Key};
+ use rsa::{
+ sha2::{Digest as _, Sha256},
+ traits::PublicKeyParts,
+ BigUint, RsaPrivateKey,
+ };
+ use serde::de::{Error as _, Unexpected};
+ use serde_json::Error;
+ #[test]
+ fn eddsa_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_24,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // Ed25519 COSE key.
+ cbor::MAP_4,
+ KTY,
+ OKP,
+ ALG,
+ EDDSA,
+ // `crv`.
+ cbor::NEG_ONE,
+ // `Ed25519`.
+ cbor::SIX,
+ // `x`.
+ cbor::NEG_TWO,
+ cbor::BYTES_INFO_24,
+ 32,
+ // Compressed y-coordinate.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = VerifyingKey::from_bytes(&[1; 32])
+ .unwrap()
+ .to_public_key_der()
+ .unwrap();
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 113..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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
+ )
+ && 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(
+ BASE64URL_NOPAD_ENC
+ .decode("ABABABABABABABABABABAA".as_bytes())
+ .unwrap()
+ .as_slice(),
+ ),
+ &format!("id and rawId to match: CredentialId({:?})", [0; 16]).as_str(),
+ )
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "ABABABABABABABABABABAA",
+ "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
+ );
+ // 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());
+ // `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());
+ // 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());
+ // `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());
+ // `id` and the credential id in authenticator data mismatch.
+ err = Error::invalid_value(
+ Unexpected::Bytes(
+ BASE64URL_NOPAD_ENC
+ .decode("ABABABABABABABABABABAA".as_bytes())
+ .unwrap()
+ .as_slice(),
+ ),
+ &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", [0; 16]).as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "ABABABABABABABABABABAA",
+ "rawId": "ABABABABABABABABABABAA",
+ "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
+ );
+ // `authenticatorData` mismatches `authData` in attestation object.
+ let mut bad_auth = [0; 113];
+ bad_auth.copy_from_slice(&att_obj[att_obj.len() - 113..]);
+ bad_auth[113 - 32..].copy_from_slice([0; 32].as_slice());
+ err = Error::invalid_value(
+ Unexpected::Bytes(bad_auth.as_slice()),
+ &format!("authenticator data to match the authenticator data portion of attestation object: {:?}", &att_obj[att_obj.len() - bad_auth.len()..]).as_str(),
+ )
+ .to_string().into_bytes();
+ assert_eq!(
+ serde_json::from_str::<RegistrationRelaxed>(
+ serde_json::json!({
+ "id": "AAAAAAAAAAAAAAAAAAAAAA",
+ "rawId": "AAAAAAAAAAAAAAAAAAAAAA",
+ "response": {
+ "clientDataJSON": b64_cdata,
+ "authenticatorData": BASE64URL_NOPAD_ENC.encode(bad_auth.as_slice()),
+ "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 `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!(
+ 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()
+ .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()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: Ed25519(Ed25519PubKey({:?}))",
+ &att_obj[att_obj.len() - 32..],
+ )
+ .as_str(),
+ )
+ .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": BASE64URL_NOPAD_ENC.encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes()[..err.len()],
+ 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());
+ // `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());
+ // 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());
+ // 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!(
+ 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
+ );
+ }
+ #[test]
+ fn client_extensions() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_24,
+ 113,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // Ed25519 COSE key.
+ cbor::MAP_4,
+ KTY,
+ OKP,
+ ALG,
+ EDDSA,
+ // `crv`.
+ cbor::NEG_ONE,
+ // `Ed25519`.
+ cbor::SIX,
+ // `x`.
+ cbor::NEG_TWO,
+ cbor::BYTES_INFO_24,
+ 32,
+ // Compressed y-coordinate.
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ ];
+ let pub_key = VerifyingKey::from_bytes(&[1; 32])
+ .unwrap()
+ .to_public_key_der()
+ .unwrap();
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 113..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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
+ )
+ && 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()));
+ // `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()));
+ // 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());
+ // Duplicate field.
+ let mut err = Error::duplicate_field("credProps").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}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"credProps\": null,
+ \"credProps\": null
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .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()));
+ // `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()));
+ // Invalid `rk`.
+ err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean")
+ .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": {
+ "credProps": {
+ "rk": 3
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // Duplicate field in `credProps`.
+ err = Error::duplicate_field("rk").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}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"credProps\": {{
+ \"rk\": true,
+ \"rk\": true
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `enabled`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"a boolean")
+ .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": {
+ "prf": {
+ "enabled": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `enabled`.
+ err = Error::missing_field("enabled").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": {
+ "prf": {}
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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
+ .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
+ .0
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| !prf.enabled)));
+ // Invalid `enabled`.
+ err = Error::invalid_type(Unexpected::Unsigned(3), &"a boolean")
+ .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": {
+ "prf": {
+ "enabled": 3
+ }
+ },
+ "type": "public-key"
+ })
+ .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
+ .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",
+ )
+ .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": {
+ "prf": {
+ "enabled": false,
+ "results": null
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Duplicate field in `prf`.
+ err = Error::duplicate_field("enabled").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}\"
+ }},
+ \"clientExtensionResults\": {{
+ \"prf\": {{
+ \"enabled\": true,
+ \"enabled\": true
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // `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
+ .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
+ .0
+ .client_extension_results
+ .prf
+ .map_or(false, |prf| prf.enabled)));
+ // Non-`null` `first`.
+ err = Error::invalid_type(Unexpected::Option, &"null")
+ .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": {
+ "prf": {
+ "enabled": true,
+ "results": {
+ "first": ""
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Non-`null` `second`.
+ err = Error::invalid_type(Unexpected::Option, &"null")
+ .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": {
+ "prf": {
+ "enabled": true,
+ "results": {
+ "first": null,
+ "second": ""
+ },
+ }
+ },
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // 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
+ }
+ }
+ },
+ "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::<RegistrationRelaxed>(
+ format!(
+ "{{
+ \"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,
+ \"first\": null
+ }}
+ }}
+ }},
+ \"type\": \"public-key\"
+ }}"
+ )
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ }
+ #[test]
+ fn es256_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let mut att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_24,
+ 148,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // P-256 COSE key.
+ cbor::MAP_5,
+ KTY,
+ EC2,
+ ALG,
+ ES256,
+ // `crv`.
+ cbor::NEG_ONE,
+ // `P-256`.
+ cbor::ONE,
+ // `x`.
+ cbor::NEG_TWO,
+ cbor::BYTES_INFO_24,
+ 32,
+ // x-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `y`.
+ cbor::NEG_THREE,
+ cbor::BYTES_INFO_24,
+ 32,
+ // y-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ];
+ let key = P256Key::from_bytes(
+ &[
+ 137, 133, 36, 206, 163, 47, 255, 5, 76, 144, 163, 141, 40, 109, 108, 240, 246, 115,
+ 178, 237, 169, 68, 6, 129, 92, 21, 238, 127, 55, 158, 207, 95,
+ ]
+ .into(),
+ )
+ .unwrap()
+ .public_key();
+ let enc_key = key.to_encoded_point(false);
+ let pub_key = key.to_public_key_der().unwrap();
+ let att_obj_len = att_obj.len();
+ att_obj[att_obj_len - 67..att_obj_len - 35]
+ .copy_from_slice(enc_key.x().unwrap().as_slice());
+ att_obj[att_obj_len - 32..].copy_from_slice(enc_key.y().unwrap().as_slice());
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 148..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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
+ )
+ && 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()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es256).as_str()
+ )
+ .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
+ );
+ // 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.
+ let bad_pub_key = P256PubKey::from_encoded_point(&P256Pt::from_affine_coordinates(
+ &[
+ 66, 71, 188, 41, 125, 2, 226, 44, 148, 62, 63, 190, 172, 64, 33, 214, 6, 37, 148,
+ 23, 240, 235, 203, 84, 112, 219, 232, 197, 54, 182, 17, 235,
+ ]
+ .into(),
+ &[
+ 22, 172, 123, 13, 170, 242, 217, 248, 193, 209, 206, 163, 92, 4, 162, 168, 113, 63,
+ 2, 117, 16, 223, 239, 196, 109, 179, 10, 130, 43, 213, 205, 92,
+ ]
+ .into(),
+ false,
+ ))
+ .unwrap();
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: P256(UncompressedP256PubKey({:?}, {:?}))",
+ &att_obj[att_obj.len() - 67..att_obj.len() - 35],
+ &att_obj[att_obj.len() - 32..],
+ )
+ .as_str(),
+ )
+ .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": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes()[..err.len()],
+ 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());
+ // `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": -7,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok());
+ }
+ #[test]
+ fn es384_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let mut att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_24,
+ 181,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // P-384 COSE key.
+ cbor::MAP_5,
+ KTY,
+ EC2,
+ ALG,
+ cbor::NEG_INFO_24,
+ ES384,
+ // `crv`.
+ cbor::NEG_ONE,
+ // `P-384`.
+ cbor::TWO,
+ // `x`.
+ cbor::NEG_TWO,
+ cbor::BYTES_INFO_24,
+ 48,
+ // x-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `y`.
+ cbor::NEG_THREE,
+ cbor::BYTES_INFO_24,
+ 48,
+ // y-coordinate. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ];
+ let key = P384Key::from_bytes(
+ &[
+ 158, 99, 156, 49, 190, 211, 85, 167, 28, 2, 80, 57, 31, 22, 17, 38, 85, 78, 232,
+ 42, 45, 199, 154, 243, 136, 251, 84, 34, 5, 120, 208, 91, 61, 248, 64, 144, 87, 1,
+ 32, 86, 220, 68, 182, 11, 105, 223, 75, 70,
+ ]
+ .into(),
+ )
+ .unwrap()
+ .public_key();
+ let enc_key = key.to_encoded_point(false);
+ let pub_key = key.to_public_key_der().unwrap();
+ let att_obj_len = att_obj.len();
+ att_obj[att_obj_len - 99..att_obj_len - 51]
+ .copy_from_slice(enc_key.x().unwrap().as_slice());
+ att_obj[att_obj_len - 48..].copy_from_slice(enc_key.y().unwrap().as_slice());
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 181..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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
+ )
+ && 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()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).as_str()
+ )
+ .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
+ );
+ // 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.
+ let bad_pub_key = P384PubKey::from_encoded_point(&P384Pt::from_affine_coordinates(
+ &[
+ 192, 10, 27, 46, 66, 67, 80, 98, 33, 230, 156, 95, 1, 135, 150, 110, 64, 243, 22,
+ 118, 5, 255, 107, 44, 234, 111, 217, 105, 125, 114, 39, 7, 126, 2, 191, 111, 48,
+ 93, 234, 175, 18, 172, 59, 28, 97, 106, 178, 152,
+ ]
+ .into(),
+ &[
+ 57, 36, 196, 12, 109, 129, 253, 115, 88, 154, 6, 43, 195, 85, 169, 5, 230, 51, 28,
+ 205, 142, 28, 150, 35, 24, 222, 170, 253, 14, 248, 84, 151, 109, 191, 152, 111,
+ 222, 70, 134, 247, 109, 171, 211, 33, 214, 217, 200, 111,
+ ]
+ .into(),
+ false,
+ ))
+ .unwrap();
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: P384(UncompressedP384PubKey({:?}, {:?}))",
+ &att_obj[att_obj.len() - 99..att_obj.len() - 51],
+ &att_obj[att_obj.len() - 48..],
+ )
+ .as_str(),
+ )
+ .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": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -35,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes()[..err.len()],
+ 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());
+ // `publicKeyAlgorithm` mismatch when `publicKey` does not exist.
+ err = Error::invalid_value(
+ Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).as_str()
+ )
+ .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": [],
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ 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());
+ // `publicKeyAlgorithm` mismatch when `publicKey` is null.
+ err = Error::invalid_value(
+ Unexpected::Other(&format!("{:?}", CoseAlgorithmIdentifier::Eddsa).as_str()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Es384).as_str()
+ )
+ .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": null,
+ "publicKeyAlgorithm": -8,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ }
+ #[test]
+ fn rs256_registration_deserialize_data_mismatch() {
+ let c_data_json = serde_json::json!({}).to_string();
+ let mut att_obj = [
+ cbor::MAP_3,
+ cbor::TEXT_3,
+ b'f',
+ b'm',
+ b't',
+ cbor::TEXT_4,
+ b'n',
+ b'o',
+ b'n',
+ b'e',
+ cbor::TEXT_7,
+ b'a',
+ b't',
+ b't',
+ b'S',
+ b't',
+ b'm',
+ b't',
+ cbor::MAP_0,
+ cbor::TEXT_8,
+ b'a',
+ b'u',
+ b't',
+ b'h',
+ b'D',
+ b'a',
+ b't',
+ b'a',
+ cbor::BYTES_INFO_25,
+ 1,
+ 87,
+ // `rpIdHash`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `flags`.
+ 0b0100_0101,
+ // `signCount`.
+ 0,
+ 0,
+ 0,
+ 0,
+ // `aaguid`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `credentialIdLength`.
+ 0,
+ 16,
+ // `credentialId`.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // RSA COSE key.
+ cbor::MAP_4,
+ KTY,
+ RSA,
+ ALG,
+ cbor::NEG_INFO_25,
+ // RS256.
+ 1,
+ 0,
+ // `n`.
+ cbor::NEG_ONE,
+ cbor::BYTES_INFO_25,
+ 1,
+ 0,
+ // n. This will be overwritten later.
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ // `e`.
+ cbor::NEG_TWO,
+ cbor::BYTES | 3,
+ // e.
+ 1,
+ 0,
+ 1,
+ ];
+ let n = [
+ 111, 183, 124, 133, 38, 167, 70, 148, 44, 50, 30, 60, 121, 14, 38, 37, 96, 114, 107,
+ 195, 248, 64, 79, 36, 237, 140, 43, 27, 94, 74, 102, 152, 135, 102, 184, 150, 186, 206,
+ 185, 19, 165, 209, 48, 98, 98, 9, 3, 205, 208, 82, 250, 105, 132, 201, 73, 62, 60, 165,
+ 100, 128, 153, 9, 41, 118, 66, 95, 236, 214, 73, 135, 197, 68, 184, 10, 27, 116, 204,
+ 145, 50, 174, 58, 42, 183, 181, 119, 232, 126, 252, 217, 96, 162, 190, 103, 122, 64,
+ 87, 145, 45, 32, 207, 17, 239, 223, 3, 35, 14, 112, 119, 124, 141, 123, 208, 239, 105,
+ 81, 217, 151, 162, 190, 17, 88, 182, 176, 158, 81, 200, 42, 166, 133, 48, 23, 236, 55,
+ 117, 248, 233, 151, 203, 122, 155, 231, 46, 177, 20, 20, 151, 64, 222, 239, 226, 7, 21,
+ 254, 81, 202, 64, 232, 161, 235, 22, 51, 246, 207, 213, 0, 229, 138, 46, 222, 205, 157,
+ 108, 139, 253, 230, 80, 50, 2, 122, 212, 163, 100, 180, 114, 12, 113, 52, 56, 99, 188,
+ 42, 198, 212, 23, 182, 222, 56, 221, 200, 79, 96, 239, 221, 135, 10, 17, 106, 183, 56,
+ 104, 68, 94, 198, 196, 35, 200, 83, 204, 26, 185, 204, 212, 31, 183, 19, 111, 233, 13,
+ 72, 93, 53, 65, 111, 59, 242, 122, 160, 244, 162, 126, 38, 235, 156, 47, 88, 39, 132,
+ 153, 79, 0, 133, 78, 7, 218, 165, 241,
+ ];
+ let e = 65537u32;
+ let d = [
+ 145, 79, 21, 97, 233, 3, 192, 194, 177, 68, 181, 80, 120, 197, 23, 44, 185, 74, 144, 0,
+ 132, 149, 139, 11, 16, 224, 4, 112, 236, 94, 238, 97, 121, 124, 213, 145, 24, 253, 168,
+ 35, 190, 205, 132, 115, 33, 201, 38, 253, 246, 180, 66, 155, 165, 46, 3, 254, 68, 108,
+ 154, 247, 246, 45, 187, 0, 204, 96, 185, 157, 249, 174, 158, 38, 62, 244, 183, 76, 102,
+ 6, 219, 92, 212, 138, 59, 147, 163, 219, 111, 39, 105, 21, 236, 196, 38, 255, 114, 247,
+ 82, 104, 113, 204, 29, 152, 209, 219, 48, 239, 74, 129, 19, 247, 33, 239, 119, 166,
+ 216, 152, 94, 138, 238, 164, 242, 129, 50, 150, 57, 20, 53, 224, 56, 241, 138, 97, 111,
+ 215, 107, 212, 195, 146, 108, 143, 0, 229, 181, 171, 73, 152, 105, 146, 25, 243, 242,
+ 140, 252, 248, 162, 247, 63, 168, 180, 20, 153, 120, 10, 248, 211, 1, 71, 127, 212,
+ 249, 237, 203, 202, 48, 26, 216, 226, 228, 186, 13, 204, 70, 255, 240, 89, 255, 59, 83,
+ 31, 253, 55, 43, 158, 90, 248, 83, 32, 159, 105, 57, 134, 34, 96, 18, 255, 245, 153,
+ 162, 60, 91, 99, 220, 51, 44, 85, 114, 67, 125, 202, 65, 217, 245, 40, 8, 81, 165, 142,
+ 24, 245, 127, 122, 247, 152, 212, 75, 45, 59, 90, 184, 234, 31, 147, 36, 8, 212, 45,
+ 50, 23, 3, 25, 253, 87, 227, 79, 119, 161,
+ ];
+ let p = BigUint::from_slice(
+ [
+ 352691927, 1294578443, 816143558, 690659917, 1161596366, 1544791087, 3999549486,
+ 3319149924, 2349250979, 1304689381, 3959753736, 3377900978, 866506027, 1671521644,
+ 3926847564, 898221388, 3448219846, 494454484, 3915534864, 2869735916, 2456511629,
+ 3397234721, 3012775852, 3472309790, 1923617705, 2993441050, 3210302569, 3605331368,
+ 3352563766, 688081007, 4104512503, 4145593376,
+ ]
+ .as_slice(),
+ );
+ let p_2 = BigUint::from_slice(
+ [
+ 4039514409, 964284038, 3230008587, 3320139220, 3562360334, 3165876926, 212773653,
+ 2752465512, 2973674888, 1717425549, 2084262803, 3585031058, 4162394935, 1428626842,
+ 1015474994, 3283774155, 2840050110, 190639246, 147241978, 2994256073, 4081014755,
+ 3102401369, 3547397148, 1545029057, 895305733, 2689179461, 1593439337, 3960057302,
+ 193068804, 2835123424, 4054880057, 4200258364,
+ ]
+ .as_slice(),
+ );
+ let key = RsaPrivateKey::from_components(
+ BigUint::from_bytes_le(n.as_slice()),
+ e.into(),
+ BigUint::from_bytes_le(d.as_slice()),
+ vec![p, p_2],
+ )
+ .unwrap()
+ .to_public_key();
+ let pub_key = key.to_public_key_der().unwrap();
+ let att_obj_len = att_obj.len();
+ att_obj[att_obj_len - 261..att_obj_len - 5]
+ .copy_from_slice(key.n().to_bytes_be().as_slice());
+ let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes());
+ let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 343..]);
+ let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes());
+ let b64_aobj = BASE64URL_NOPAD_ENC.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
+ )
+ && 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()),
+ &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {:?}", CoseAlgorithmIdentifier::Rs256).as_str()
+ )
+ .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
+ );
+ // 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.
+ let bad_pub_key = RsaPrivateKey::from_components(
+ BigUint::from_slice(
+ [
+ 1268883887, 2823353396, 2015459101, 2565332483, 1399879646, 2924146141,
+ 4220770383, 1927962357, 4262532606, 2135651080, 1832605590, 1515549926,
+ 3644825611, 4206568969, 2754000866, 320264886, 3679698234, 1661964299,
+ 959358615, 2210230033, 2052419982, 355790524, 3278273908, 2619188662,
+ 2625484501, 48052312, 1943153506, 1483277344, 3973029557, 4043176610,
+ 855443528, 2857170908, 3890300047, 301219953, 568959626, 3742057218,
+ 3248023740, 888348692, 4077005632, 3902164232, 2136970349, 581060407,
+ 881283894, 706789292, 3469945706, 3899549796, 3027774213, 3918538918,
+ 1736861679, 3096109311, 612338128, 3388510141, 3895712258, 2085822048,
+ 3004690797, 3572406263, 3744148684, 179106196, 1147050987, 3212056692,
+ 595539286, 1003275909, 17854028, 2642908175,
+ ]
+ .as_slice(),
+ ),
+ 65537u32.into(),
+ BigUint::from_slice(
+ [
+ 4219166081, 3411287400, 3981141108, 1678549103, 2990099628, 1028778896,
+ 672985971, 2520258231, 1054615108, 2922409705, 1844757795, 1160015252,
+ 1910592069, 468649647, 4013057473, 772236922, 1958956898, 2475335323,
+ 3977796915, 1829655286, 1576008336, 2187384383, 2445706978, 1642531745,
+ 1610593494, 4268513438, 3095769587, 1486118748, 4109728823, 2030327380,
+ 2959206188, 681254334, 1353008441, 725092776, 2634942185, 1480646512,
+ 390137741, 1392955456, 4172679229, 2746438782, 2237328976, 2974223876,
+ 2535267247, 3282201811, 1453825287, 3948348329, 3639451225, 1053160223,
+ 3867366405, 204601530, 2268984413, 4053930420, 2331079437, 2795201243,
+ 621559743, 1420993793, 693127368, 2379843661, 4078948854, 4130031519,
+ 1957410463, 3951952652, 1514579162, 1261104787,
+ ]
+ .as_slice(),
+ ),
+ vec![
+ BigUint::from_slice(
+ [
+ 477022167, 1829769280, 2090244202, 1551476276, 1157631474, 2890438663,
+ 3030138742, 490022796, 816963781, 1097260329, 1043839249, 132356315,
+ 2333006670, 2559626311, 4109838094, 1022025893, 518867669, 2331160934,
+ 796532648, 1910610894, 4103647079, 3748718875, 3000444664, 2030629908,
+ 2051410714, 1470584080, 3823425600, 150616493, 3406571229, 728760788,
+ 1642158920, 3248110052,
+ ]
+ .as_slice(),
+ ),
+ BigUint::from_slice(
+ [
+ 2563529193, 1846080031, 2674900518, 1429039465, 4196332559, 1876681390,
+ 2277818219, 2814016273, 3312979285, 3981345183, 451288984, 3552968165,
+ 2390674537, 2887399418, 103653441, 3997324899, 2875328107, 2697584733,
+ 2018692127, 116301540, 2576747710, 1194942447, 2615930724, 3775252553,
+ 808368511, 2384549107, 387191569, 980553943, 2487815891, 4238343336,
+ 3546626429, 3494710460,
+ ]
+ .as_slice(),
+ ),
+ ],
+ )
+ .unwrap()
+ .to_public_key();
+ err = Error::invalid_value(
+ Unexpected::Bytes([0; 32].as_slice()),
+ &format!(
+ "DER-encoded public key to match the public key within the attestation object: Rsa(RsaPubKey({:?}, 65537))",
+ &att_obj[att_obj.len() - 261..att_obj.len() - 5],
+ )
+ .as_str(),
+ )
+ .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": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()),
+ "publicKeyAlgorithm": -257,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .unwrap_err().to_string().into_bytes()[..err.len()],
+ 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());
+ // `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": -257,
+ "attestationObject": b64_aobj,
+ },
+ "clientExtensionResults": {},
+ "type": "public-key"
+ })
+ .to_string()
+ .as_str()
+ )
+ .is_ok());
+ }
+}
diff --git a/src/response/ser.rs b/src/response/ser.rs
@@ -0,0 +1,1207 @@
+#![expect(
+ clippy::question_mark_used,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+extern crate alloc;
+use super::{
+ AllAcceptedCredentialsOptions, AuthTransports, AuthenticatorAttachment, AuthenticatorTransport,
+ Challenge, CredentialId, CurrentUserDetailsOptions, Origin, SentChallenge, BASE64URL_NOPAD_ENC,
+};
+use alloc::borrow::Cow;
+use core::{
+ fmt::{self, Formatter},
+ marker::PhantomData,
+};
+#[cfg(doc)]
+use data_encoding::BASE64URL_NOPAD;
+use serde::{
+ de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, SeqAccess, Unexpected, Visitor},
+ ser::{Serialize, SerializeSeq as _, SerializeStruct as _, Serializer},
+};
+/// [`"ble"`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-ble).
+const BLE: &str = "ble";
+/// [`"hybrid"`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-hybrid).
+const HYBRID: &str = "hybrid";
+/// [`"internal"`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-internal).
+const INTERNAL: &str = "internal";
+/// [`"nfc"`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-nfc).
+const NFC: &str = "nfc";
+/// [`"smart-card"`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-smart-card).
+const SMART_CARD: &str = "smart-card";
+/// [`"usb"`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-usb).
+const USB: &str = "usb";
+impl Serialize for AuthenticatorTransport {
+ /// Serializes `self` as
+ /// [`AuthenticatorTransport`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::AuthenticatorTransport;
+ /// assert_eq!(
+ /// serde_json::to_string(&AuthenticatorTransport::Usb)?,
+ /// r#""usb""#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(match *self {
+ Self::Ble => BLE,
+ Self::Hybrid => HYBRID,
+ Self::Internal => INTERNAL,
+ Self::Nfc => NFC,
+ Self::SmartCard => SMART_CARD,
+ Self::Usb => USB,
+ })
+ }
+}
+impl<'de> Deserialize<'de> for AuthenticatorTransport {
+ /// Deserializes [`prim@str`] based on
+ /// [`AuthenticatorTransport`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::AuthenticatorTransport;
+ /// assert!(matches!(
+ /// serde_json::from_str::<AuthenticatorTransport>(r#""usb""#)?,
+ /// AuthenticatorTransport::Usb
+ /// ));
+ /// // Case matters.
+ /// assert!(serde_json::from_str::<AuthenticatorTransport>(r#""Usb""#).is_err());
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `AuthenticatorTransport`.
+ struct AuthenticatorTransportVisitor;
+ impl Visitor<'_> for AuthenticatorTransportVisitor {
+ type Value = AuthenticatorTransport;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("AuthenticatorTransport")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ BLE => Ok(AuthenticatorTransport::Ble),
+ HYBRID => Ok(AuthenticatorTransport::Hybrid),
+ INTERNAL => Ok(AuthenticatorTransport::Internal),
+ NFC => Ok(AuthenticatorTransport::Nfc),
+ SMART_CARD => Ok(AuthenticatorTransport::SmartCard),
+ USB => Ok(AuthenticatorTransport::Usb),
+ _ => Err(E::invalid_value(
+ Unexpected::Str(v),
+ &format!("'{BLE}', '{HYBRID}', '{INTERNAL}', '{NFC}', '{SMART_CARD}', or '{USB}'").as_str(),
+ )),
+ }
+ }
+ }
+ deserializer.deserialize_str(AuthenticatorTransportVisitor)
+ }
+}
+impl Serialize for AuthTransports {
+ /// Serializes `self` based on
+ /// [`transports`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::{AuthTransports, AuthenticatorTransport};
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(
+ /// serde_json::to_string(&AuthTransports::ALL)?,
+ /// r#"["ble","hybrid","internal","nfc","smart-card","usb"]"#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[expect(clippy::unreachable, reason = "there is a bug, so we want to crash")]
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let count = usize::try_from(self.count())
+ .unwrap_or_else(|_e| unreachable!("there is a bug in AuthenticatorTransports::count"));
+ serializer.serialize_seq(Some(count)).and_then(|mut ser| {
+ if self.contains(AuthenticatorTransport::Ble) {
+ ser.serialize_element(&AuthenticatorTransport::Ble)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(AuthenticatorTransport::Hybrid) {
+ ser.serialize_element(&AuthenticatorTransport::Hybrid)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(AuthenticatorTransport::Internal) {
+ ser.serialize_element(&AuthenticatorTransport::Internal)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(AuthenticatorTransport::Nfc) {
+ ser.serialize_element(&AuthenticatorTransport::Nfc)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(AuthenticatorTransport::SmartCard) {
+ ser.serialize_element(&AuthenticatorTransport::SmartCard)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| {
+ if self.contains(AuthenticatorTransport::Usb) {
+ ser.serialize_element(&AuthenticatorTransport::Usb)
+ } else {
+ Ok(())
+ }
+ .and_then(|()| ser.end())
+ })
+ })
+ })
+ })
+ })
+ })
+ }
+}
+impl<'de> Deserialize<'de> for AuthTransports {
+ /// Deserializes a sequence based on
+ /// [`transports`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::{AuthTransports, AuthenticatorTransport};
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(
+ /// serde_json::from_str::<AuthTransports>(
+ /// r#"["ble","hybrid","internal","nfc","smart-card","usb"]"#
+ /// )
+ /// ?.count(),
+ /// 6
+ /// );
+ /// // Errors since `"foo"` is not valid.
+ /// assert!(serde_json::from_str::<AuthTransports>(r#"["foo"]"#).is_err());
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `AuthTransports`.
+ struct AuthTransportsVisitor;
+ impl<'d> Visitor<'d> for AuthTransportsVisitor {
+ type Value = AuthTransports;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("AuthTransports")
+ }
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'d>,
+ {
+ let mut transports = AuthTransports::new();
+ while let Some(val) = seq.next_element::<AuthenticatorTransport>()? {
+ transports = transports.add_transport(val);
+ }
+ Ok(transports)
+ }
+ }
+ deserializer.deserialize_seq(AuthTransportsVisitor)
+ }
+}
+impl<T: AsRef<[u8]>> Serialize for CredentialId<T> {
+ /// Serializes `self` into a [`prim@str`] based on
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptorjson-id).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::CredentialId;
+ /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
+ /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
+ /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(
+ /// serde_json::to_string(&CredentialId::try_from(vec![0; 16])?).unwrap(),
+ /// r#""AAAAAAAAAAAAAAAAAAAAAA""#
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ ///```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(BASE64URL_NOPAD_ENC.encode(self.0.as_ref()).as_str())
+ }
+}
+impl<'de> Deserialize<'de> for CredentialId<Vec<u8>> {
+ /// Deserializes [`prim@str`] based on
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptorjson-id).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::CredentialId;
+ /// # #[cfg(feature = "custom")]
+ /// assert_eq!(
+ /// serde_json::from_str::<CredentialId<_>>(r#""AAAAAAAAAAAAAAAAAAAAAA""#).unwrap(),
+ /// CredentialId::try_from(vec![0; 16])?
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ ///```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `CredentialId`.
+ struct CredentialIdVisitor;
+ impl Visitor<'_> for CredentialIdVisitor {
+ type Value = CredentialId<Vec<u8>>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("CredentialId")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ // `CRED_ID_MIN_LEN` and `CRED_ID_MAX_LEN` are less than
+ // `0x4000`, so this won't `panic`.
+ if (crate::base64url_nopad_len(super::CRED_ID_MIN_LEN)
+ ..=crate::base64url_nopad_len(super::CRED_ID_MAX_LEN))
+ .contains(&v.len())
+ {
+ BASE64URL_NOPAD_ENC
+ .decode(v.as_bytes())
+ .map_err(E::custom)
+ .map(CredentialId)
+ } else {
+ Err(E::invalid_value(
+ Unexpected::Str(v),
+ &"16 to 1023 bytes encoded in base64url without padding",
+ ))
+ }
+ }
+ }
+ deserializer.deserialize_str(CredentialIdVisitor)
+ }
+}
+impl<'de> Deserialize<'de> for AuthenticatorAttachment {
+ /// Deserializes [`prim@str`] based on
+ /// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::AuthenticatorAttachment;
+ /// assert!(matches!(
+ /// serde_json::from_str::<AuthenticatorAttachment>(r#""cross-platform""#)?,
+ /// AuthenticatorAttachment::CrossPlatform)
+ /// );
+ /// assert!(matches!(
+ /// serde_json::from_str::<AuthenticatorAttachment>(r#""platform""#)?,
+ /// AuthenticatorAttachment::Platform)
+ /// );
+ /// // Case matters.
+ /// assert!(serde_json::from_str::<AuthenticatorAttachment>(r#""Platform""#).is_err());
+ /// // `AuthenticatorAttachment::None` is not deserializable.
+ /// assert!(serde_json::from_str::<AuthenticatorAttachment>(r#""""#).is_err());
+ /// assert!(serde_json::from_str::<AuthenticatorAttachment>("null").is_err());
+ /// assert!(serde_json::from_str::<AuthenticatorAttachment>(r#""none""#).is_err());
+ /// assert!(serde_json::from_str::<AuthenticatorAttachment>(r#""None""#).is_err());
+ /// # Ok::<_, serde_json::Error>(())
+ ///```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `AuthenticatorAttachment`.
+ struct AuthenticatorAttachmentVisitor;
+ impl Visitor<'_> for AuthenticatorAttachmentVisitor {
+ type Value = AuthenticatorAttachment;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("AuthenticatorAttachment")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ /// `"platform"`
+ const PLATFORM: &str = "platform";
+ /// `"cross-platform"`
+ const CROSS_PLATFORM: &str = "cross-platform";
+ match v {
+ PLATFORM => Ok(AuthenticatorAttachment::Platform),
+ CROSS_PLATFORM => Ok(AuthenticatorAttachment::CrossPlatform),
+ _ => Err(E::invalid_value(
+ Unexpected::Str(v),
+ &format!("'{PLATFORM}' or '{CROSS_PLATFORM}'").as_str(),
+ )),
+ }
+ }
+ }
+ deserializer.deserialize_str(AuthenticatorAttachmentVisitor)
+ }
+}
+/// Container of data that was encoded in base64url.
+pub(super) struct Base64DecodedVal(pub Vec<u8>);
+impl<'de> Deserialize<'de> for Base64DecodedVal {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `Base64DecodedVal`.
+ struct Base64DecodedValVisitor;
+ impl Visitor<'_> for Base64DecodedValVisitor {
+ type Value = Base64DecodedVal;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("base64url-encoded data")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ BASE64URL_NOPAD_ENC
+ .decode(v.as_bytes())
+ .map_err(E::custom)
+ .map(Base64DecodedVal)
+ }
+ }
+ deserializer.deserialize_str(Base64DecodedValVisitor)
+ }
+}
+impl<'de> Deserialize<'de> for SentChallenge {
+ /// Deserializes `[u8]` or [`prim@str`] based on
+ /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge).
+ ///
+ /// Specifically a `[u8]` or `str` is base64url-decoded and interpreted as a little-endian
+ /// `u128`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::response::SentChallenge;
+ /// assert_eq!(
+ /// serde_json::from_slice::<SentChallenge>(br#""AAAAAAAAAAAAAAAAAAAAAA""#)?,
+ /// SentChallenge(0)
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ ///```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `SentChallenge`.
+ struct ChallengeVisitor;
+ impl Visitor<'_> for ChallengeVisitor {
+ type Value = SentChallenge;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str(
+ "base64 encoding of the 16-byte challenge in a URL safe way without padding",
+ )
+ }
+ #[expect(
+ clippy::panic_in_result_fn,
+ reason = "we want to crash when there is a bug"
+ )]
+ #[expect(
+ clippy::little_endian_bytes,
+ reason = "SentChallenge::deserialize and Challenge::serialize need to be consistent across architectures"
+ )]
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ // We try to avoid decoding when possible by at least ensuring the input length is correct.
+ if v.len() == Challenge::BASE64_LEN {
+ let mut data = [0; 16];
+ BASE64URL_NOPAD_ENC
+ .decode_mut(v, data.as_mut_slice())
+ .map_err(|err| E::custom(err.error))
+ .map(|len| {
+ assert_eq!(len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut");
+ SentChallenge(u128::from_le_bytes(data))
+ })
+ } else {
+ Err(E::invalid_value(
+ Unexpected::Bytes(v),
+ &"22 bytes encoded in base64url without padding",
+ ))
+ }
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ self.visit_bytes(v.as_bytes())
+ }
+ }
+ deserializer.deserialize_bytes(ChallengeVisitor)
+ }
+}
+impl<'de: 'a, 'a> Deserialize<'de> for Origin<'a> {
+ /// Deserializes [`prim@str`] by borrowing the data when possible.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # extern crate alloc;
+ /// # use alloc::borrow::Cow;
+ /// # use webauthn_rp::response::Origin;
+ /// let origin_borrowed = "https://example.com";
+ /// let origin_owned = "\\\\https://example.com";
+ /// assert!(
+ /// matches!(serde_json::from_str::<Origin<'_>>(format!("\"{origin_borrowed}\"").as_str())?.0, Cow::Borrowed(val) if val == origin_borrowed)
+ /// );
+ /// assert!(
+ /// matches!(serde_json::from_str::<Origin<'_>>(format!("\"{origin_owned}\"").as_str())?.0, Cow::Owned(val) if *val.as_bytes() == origin_owned.as_bytes()[1..])
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ ///```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `Origin`.
+ struct OriginVisitor<'b>(PhantomData<fn() -> &'b ()>);
+ impl<'d: 'b, 'b> Visitor<'d> for OriginVisitor<'b> {
+ type Value = Origin<'b>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("Origin")
+ }
+ fn visit_borrowed_str<E>(self, v: &'d str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Ok(Origin(Cow::Borrowed(v)))
+ }
+ fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Ok(Origin(Cow::Owned(v)))
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ self.visit_string(v.to_owned())
+ }
+ }
+ deserializer.deserialize_str(OriginVisitor(PhantomData))
+ }
+}
+/// `trait` that returns an empty instance of `Self`.
+pub(super) trait ClientExtensions: Sized {
+ /// Returns an empty instance of `Self`.
+ fn empty() -> Self;
+}
+/// Response for both registration and authentication ceremonies.
+///
+/// [`Self::raw_id`] is always `Some` when `!RELAXED` or `!REG`.
+///
+/// `RELAXED` and `REG` are used purely for deserialization purposes.
+pub(super) struct PublicKeyCredential<const RELAXED: bool, const REG: bool, AuthResp, Ext> {
+ /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
+ pub id: Option<CredentialId<Vec<u8>>>,
+ /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
+ pub response: AuthResp,
+ /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
+ pub authenticator_attachment: AuthenticatorAttachment,
+ /// [`getClientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults).
+ pub client_extension_results: Ext,
+}
+/// `Visitor` for `PublicKeyCredential`.
+///
+/// When `!RELAXED`, `REG` is ignored and all fields must exist and unknown fields are not allowed.
+/// When `RELAXED`, unknown fields are ignored.
+/// When `RELAXED` and `REG`, only `response` is required.
+/// When `RELAXED` and `!REG`, only `id` and `response` are required.
+struct PublicKeyCredentialVisitor<const RELAXED: bool, const REG: bool, R, E>(
+ pub PhantomData<fn() -> (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>,
+ E: for<'a> Deserialize<'a> + ClientExtensions,
+{
+ type Value = PublicKeyCredential<REL, REGI, R, E>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("PublicKeyCredential")
+ }
+ #[expect(
+ clippy::too_many_lines,
+ reason = "rather hide all the internal logic instead instead of moving into an outer scope"
+ )]
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// `PublicKeyCredentialJSON` fields.
+ enum Field<const IGNORE_UNKNOWN: bool> {
+ /// `id`.
+ Id,
+ /// `type`.
+ Type,
+ /// `rawId`.
+ RawId,
+ /// `response`.
+ Response,
+ /// `authenticatorAttachment`.
+ AuthenticatorAttachment,
+ /// `clientExtensionResults`.
+ ClientExtensionResults,
+ /// Unknown field.
+ Other,
+ }
+ impl<'e, const IGNORE: bool> Deserialize<'e> for Field<IGNORE> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor<const IGNORE_UNKNOWN: bool>;
+ 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}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ ID => Ok(Field::Id),
+ TYPE => Ok(Field::Type),
+ RAW_ID => Ok(Field::RawId),
+ RESPONSE => Ok(Field::Response),
+ AUTHENTICATOR_ATTACHMENT => Ok(Field::AuthenticatorAttachment),
+ CLIENT_EXTENSION_RESULTS => Ok(Field::ClientExtensionResults),
+ _ => {
+ if IGN {
+ Ok(Field::Other)
+ } else {
+ Err(E::unknown_field(v, REG_FIELDS))
+ }
+ }
+ }
+ }
+ }
+ 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;
+ let mut resp = None;
+ let mut attach = None;
+ let mut ext = None;
+ while let Some(key) = map.next_key::<Field<REL>>()? {
+ match key {
+ Field::Id => {
+ if opt_id.is_some() {
+ return Err(Error::duplicate_field(ID));
+ }
+ opt_id = map.next_value::<Option<_>>().map(Some)?;
+ }
+ Field::Type => {
+ if typ.is_some() {
+ return Err(Error::duplicate_field(TYPE));
+ }
+ typ = map.next_value::<Option<Type>>().map(Some)?;
+ }
+ Field::RawId => {
+ if raw.is_some() {
+ return Err(Error::duplicate_field(RAW_ID));
+ }
+ raw = map.next_value::<Option<CredentialId<_>>>().map(Some)?;
+ }
+ Field::Response => {
+ if resp.is_some() {
+ return Err(Error::duplicate_field(RESPONSE));
+ }
+ resp = map.next_value::<R>().map(Some)?;
+ }
+ Field::AuthenticatorAttachment => {
+ if attach.is_some() {
+ return Err(Error::duplicate_field(AUTHENTICATOR_ATTACHMENT));
+ }
+ attach = map.next_value().map(Some)?;
+ }
+ Field::ClientExtensionResults => {
+ if ext.is_some() {
+ return Err(Error::duplicate_field(CLIENT_EXTENSION_RESULTS));
+ }
+ ext = map.next_value::<Option<E>>().map(Some)?;
+ }
+ Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
+ }
+ }
+ resp.ok_or_else(|| Error::missing_field(RESPONSE))
+ .and_then(|response| {
+ opt_id.ok_or(false).and_then(|id| id.ok_or(true)).map_or_else(
+ |flag| {
+ if REL && REGI {
+ Ok(None)
+ } else if flag {
+ Err(Error::invalid_type(Unexpected::Other("null"), &format!("{ID} to be a base64url-encoded CredentialId").as_str()))
+ } else {
+ Err(Error::missing_field(ID))
+ }
+ },
+ |id| Ok(Some(id)),
+ )
+ .and_then(|id| {
+ raw.ok_or(false).and_then(|opt_raw_id| opt_raw_id.ok_or(true)).map_or_else(
+ |flag| {
+ if REL {
+ Ok(())
+ } else if flag {
+ Err(Error::invalid_type(Unexpected::Other("null"), &format!("{RAW_ID} to be a base64url-encoded CredentialId").as_str()))
+ } else {
+ Err(Error::missing_field(RAW_ID))
+ }
+ },
+ |raw_id| {
+ id.as_ref().map_or_else(
+ || Ok(()),
+ |i| {
+ if raw_id == i {
+ Ok(())
+ } else {
+ Err(Error::invalid_value(
+ Unexpected::Bytes(raw_id.as_ref()),
+ &format!("{ID} and {RAW_ID} to match: {i:?}").as_str(),
+ ))
+ }
+ },
+ )
+ }
+ )
+ .and_then(|()| {
+ ext.ok_or(false).and_then(|opt_ext| opt_ext.ok_or(true)).map_or_else(
+ |flag| {
+ if REL {
+ Ok(E::empty())
+ } else if flag {
+ Err(Error::invalid_type(Unexpected::Other("null"), &format!("{CLIENT_EXTENSION_RESULTS} to be a map of allowed client extensions").as_str()))
+ } else {
+ Err(Error::missing_field(CLIENT_EXTENSION_RESULTS))
+ }
+ },
+ Ok
+ )
+ .and_then(|client_extension_results| {
+ typ.ok_or(false).and_then(|opt_typ| opt_typ.ok_or(true)).map_or_else(
+ |flag| {
+ if REL {
+ Ok(())
+ } else if flag {
+ Err(Error::invalid_type(Unexpected::Other("null"), &format!("{TYPE} to be '{PUBLIC_KEY}'").as_str()))
+ } else {
+ Err(Error::missing_field(TYPE))
+ }
+ },
+ |_| Ok(()),
+ ).map(|()| PublicKeyCredential {
+ id,
+ response,
+ authenticator_attachment: attach.flatten().unwrap_or(AuthenticatorAttachment::None),
+ client_extension_results,
+ })
+ })
+ })
+ })
+ })
+ }
+}
+/// `"id"`.
+const ID: &str = "id";
+/// `"type"`.
+const TYPE: &str = "type";
+/// `"rawId"`.
+const RAW_ID: &str = "rawId";
+/// `"response"`.
+const RESPONSE: &str = "response";
+/// `"authenticatorAttachment"`.
+const AUTHENTICATOR_ATTACHMENT: &str = "authenticatorAttachment";
+/// `"clientExtensionResults"`.
+const CLIENT_EXTENSION_RESULTS: &str = "clientExtensionResults";
+/// `"public-key"`.
+const PUBLIC_KEY: &str = "public-key";
+/// Fields for `PublicKeyCredentialJSON`.
+const REG_FIELDS: &[&str; 6] = &[
+ ID,
+ TYPE,
+ RAW_ID,
+ RESPONSE,
+ AUTHENTICATOR_ATTACHMENT,
+ CLIENT_EXTENSION_RESULTS,
+];
+impl<'de, const REL: bool, const REGI: bool, R, E> Deserialize<'de>
+ for PublicKeyCredential<REL, REGI, R, E>
+where
+ R: for<'a> Deserialize<'a>,
+ E: for<'a> Deserialize<'a> + ClientExtensions,
+{
+ /// Deserializes a `struct` based on
+ /// [`PublicKeyCredentialJSON`](https://www.w3.org/TR/webauthn-3/#typedefdef-publickeycredentialjson).
+ ///
+ /// `REL` iff unknown fields should be ignored and not cause an error.
+ /// `REGI` iff `Self` is from a registration ceremony.
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_struct(
+ "PublicKeyCredential",
+ REG_FIELDS,
+ PublicKeyCredentialVisitor::<REL, REGI, _, _>(PhantomData),
+ )
+ }
+}
+impl Serialize for AllAcceptedCredentialsOptions<'_, '_> {
+ /// Serializes `self` to conform with
+ /// [`AllAcceptedCredentialsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use core::str::FromStr;
+ /// # #[cfg(feature = "bin")]
+ /// # use webauthn_rp::bin::Decode;
+ /// # use webauthn_rp::{
+ /// # request::{register::UserHandle, AsciiDomain, RpId},
+ /// # response::{error::CredentialIdErr, AllAcceptedCredentialsOptions, CredentialId},
+ /// # };
+ /// /// Retrieves the `CredentialId`s associated with `user_id` from the database.
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// fn get_credential_ids(user_id: UserHandle<&[u8]>) -> Result<Vec<CredentialId<Vec<u8>>>, CredentialIdErr> {
+ /// // ⋮
+ /// # CredentialId::decode(vec![0; 16]).map(|cred_id| vec![cred_id])
+ /// }
+ /// /// Retrieves the `UserHandle` from a session cookie.
+ /// # #[cfg(feature = "custom")]
+ /// fn get_user_handle() -> UserHandle<Vec<u8>> {
+ /// // ⋮
+ /// # UserHandle::try_from(vec![0]).unwrap()
+ /// }
+ /// # #[cfg(feature = "custom")]
+ /// let user_id = get_user_handle();
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(
+ /// serde_json::to_string(&AllAcceptedCredentialsOptions {
+ /// rp_id: &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
+ /// user_id: (&user_id).into(),
+ /// all_accepted_credential_ids: get_credential_ids((&user_id).into())?,
+ /// })
+ /// .unwrap(),
+ /// r#"{"rpId":"example.com","userId":"AA","allAcceptedCredentialIds":["AAAAAAAAAAAAAAAAAAAAAA"]}"#
+ /// );
+ /// # Ok::<_, webauthn_rp::AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("AllAcceptedCredentialsOptions", 3)
+ .and_then(|mut ser| {
+ ser.serialize_field("rpId", self.rp_id).and_then(|()| {
+ ser.serialize_field("userId", &self.user_id).and_then(|()| {
+ ser.serialize_field(
+ "allAcceptedCredentialIds",
+ &self.all_accepted_credential_ids,
+ )
+ .and_then(|()| ser.end())
+ })
+ })
+ })
+ }
+}
+impl Serialize for CurrentUserDetailsOptions<'_, '_, '_, '_> {
+ /// Serializes `self` to conform with
+ /// [`CurrentUserDetailsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use core::str::FromStr;
+ /// # #[cfg(feature = "bin")]
+ /// # use webauthn_rp::bin::Decode;
+ /// # use webauthn_rp::{
+ /// # request::{register::{Nickname, PublicKeyCredentialUserEntity, UserHandle, Username}, AsciiDomain, RpId},
+ /// # response::CurrentUserDetailsOptions,
+ /// # AggErr,
+ /// # };
+ /// /// Retrieves the `PublicKeyCredentialUserEntity` info associated with `user_id` from the database.
+ /// # #[cfg(feature = "bin")]
+ /// fn get_user_info(user_id: UserHandle<&[u8]>) -> Result<(Username, Option<Nickname>), AggErr> {
+ /// // ⋮
+ /// # Ok((Username::decode("foo".to_owned()).unwrap(), Some(Nickname::decode("foo".to_owned()).unwrap())))
+ /// }
+ /// /// Retrieves the `UserHandle` from a session cookie.
+ /// # #[cfg(feature = "custom")]
+ /// fn get_user_handle() -> UserHandle<Vec<u8>> {
+ /// // ⋮
+ /// # UserHandle::try_from(vec![0]).unwrap()
+ /// }
+ /// # #[cfg(feature = "custom")]
+ /// let user_handle = get_user_handle();
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let id = (&user_handle).into();
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// let (name, display_name) = get_user_info(id)?;
+ /// # #[cfg(all(feature = "bin", feature = "custom"))]
+ /// assert_eq!(
+ /// serde_json::to_string(&CurrentUserDetailsOptions {
+ /// rp_id: &RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?),
+ /// user: PublicKeyCredentialUserEntity { name, id, display_name, },
+ /// })
+ /// .unwrap(),
+ /// r#"{"rpId":"example.com","userId":"AA","name":"foo","displayName":"foo"}"#
+ /// );
+ /// # Ok::<_, AggErr>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer
+ .serialize_struct("CurrentUserDetailsOptions", 4)
+ .and_then(|mut ser| {
+ ser.serialize_field("rpId", self.rp_id).and_then(|()| {
+ ser.serialize_field("userId", &self.user.id).and_then(|()| {
+ ser.serialize_field("name", &self.user.name).and_then(|()| {
+ ser.serialize_field("displayName", &self.user.display_name)
+ .and_then(|()| ser.end())
+ })
+ })
+ })
+ })
+ }
+}
+/// JSON `null`.
+struct Null;
+impl<'de> Deserialize<'de> for Null {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ /// `Visitor` for `Null`.
+ struct NullVisitor;
+ impl Visitor<'_> for NullVisitor {
+ type Value = Null;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("null")
+ }
+ fn visit_none<E>(self) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Ok(Null)
+ }
+ }
+ deserializer.deserialize_option(NullVisitor)
+ }
+}
+/// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
+pub(super) struct AuthenticationExtensionsPrfValues;
+/// `Visitor` for `AuthenticationExtensionsPrfValues`.
+///
+/// Unknown fields are ignored iff `RELAXED`.`first` must always exist if `second` does.
+/// `first` and `second` must be `null` if they exist. `first` must exist iff `!RELAXED`.
+pub(super) struct AuthenticationExtensionsPrfValuesVisitor<const RELAXED: bool>;
+impl<'d, const R: bool> Visitor<'d> for AuthenticationExtensionsPrfValuesVisitor<R> {
+ type Value = AuthenticationExtensionsPrfValues;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("AuthenticationExtensionsPrfValues")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Fields.
+ enum Field<const IGNORE_UNKNOWN: bool> {
+ /// `first` field.
+ First,
+ /// `second` field.
+ Second,
+ /// Unknown field.
+ Other,
+ }
+ impl<'e, const I: bool> Deserialize<'e> for Field<I> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ ///
+ /// Unknown fields are ignored iff `IGNORE_UNKNOWN`.
+ struct FieldVisitor<const IGNORE_UNKNOWN: bool>;
+ impl<const IG: bool> Visitor<'_> for FieldVisitor<IG> {
+ type Value = Field<IG>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{FIRST}' or '{SECOND}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ FIRST => Ok(Field::First),
+ SECOND => Ok(Field::Second),
+ _ => {
+ if IG {
+ Ok(Field::Other)
+ } else {
+ Err(E::unknown_field(v, PRF_VALUES_FIELDS))
+ }
+ }
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut first = None;
+ let mut second = None;
+ while let Some(key) = map.next_key::<Field<R>>()? {
+ match key {
+ Field::First => {
+ if first.is_some() {
+ return Err(Error::duplicate_field(FIRST));
+ }
+ first = map.next_value::<Null>().map(Some)?;
+ }
+ Field::Second => {
+ if second.is_some() {
+ return Err(Error::duplicate_field(SECOND));
+ }
+ second = map.next_value::<Null>().map(Some)?;
+ }
+ Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
+ }
+ }
+ if first.is_some() || (R && second.is_none()) {
+ Ok(AuthenticationExtensionsPrfValues)
+ } else {
+ Err(Error::missing_field(FIRST))
+ }
+ }
+}
+/// `"first"`
+const FIRST: &str = "first";
+/// `"second"`
+const SECOND: &str = "second";
+/// `AuthenticationExtensionsPrfValues` fields.
+pub(super) const PRF_VALUES_FIELDS: &[&str; 2] = &[FIRST, SECOND];
+impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfValues {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_struct(
+ "AuthenticationExtensionsPrfValues",
+ PRF_VALUES_FIELDS,
+ AuthenticationExtensionsPrfValuesVisitor::<false>,
+ )
+ }
+}
+/// [`AuthenticationExtensionsPRFOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs).
+///
+/// `RELAXED` iff unknown fields are ignored.
+///
+/// `REGISTRATION` iff
+/// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled)
+/// is required (and must not be `null`); otherwise it's forbidden.
+///
+/// The contained `Option` is `Some` iff `REGISTRATION`.
+pub(super) struct AuthenticationExtensionsPrfOutputsHelper<
+ const RELAXED: bool,
+ const REGISTRATION: bool,
+ Prf,
+>(pub Option<bool>, pub PhantomData<fn() -> Prf>);
+/// `Visitor` for `AuthenticationExtensionsPrfOutputs`.
+///
+/// Unknown fields are ignored iff `RELAXED`.`enabled` must exist and not be `null` iff `REGISTRATION`.
+struct AuthenticationExtensionsPrfOutputsVisitor<const RELAXED: bool, const REGISTRATION: bool, Prf>(
+ PhantomData<fn() -> Prf>,
+);
+impl<'d, const REL: bool, const REG: bool, Prf> Visitor<'d>
+ for AuthenticationExtensionsPrfOutputsVisitor<REL, REG, Prf>
+where
+ Prf: for<'a> Deserialize<'a>,
+{
+ type Value = AuthenticationExtensionsPrfOutputsHelper<REL, REG, Prf>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("AuthenticationExtensionsPrfOutputs")
+ }
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'d>,
+ {
+ /// Fields.
+ enum Field<const IGNORE_UNKNOWN: bool, const REGI: bool> {
+ /// `enabled` field.
+ Enabled,
+ /// `results` field.
+ Results,
+ /// Unknown field.
+ Other,
+ }
+ impl<'e, const I: bool, const R: bool> Deserialize<'e> for Field<I, R> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'e>,
+ {
+ /// `Visitor` for `Field`.
+ ///
+ /// Unknown fields are ignored iff `IGNORE_UNKNOWN`.
+ /// `enabled` is allowed to exist iff `REGI`.
+ struct FieldVisitor<const IGNORE_UNKNOWN: bool, const REGI: bool>;
+ impl<const IG: bool, const RE: bool> Visitor<'_> for FieldVisitor<IG, RE> {
+ type Value = Field<IG, RE>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ if RE {
+ write!(formatter, "'{ENABLED}' or '{RESULTS}'")
+ } else {
+ write!(formatter, "'{RESULTS}'")
+ }
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ ENABLED => {
+ if RE {
+ Ok(Field::Enabled)
+ } else {
+ Err(E::unknown_field(v, PRF_AUTH_OUTPUTS_FIELDS))
+ }
+ }
+ RESULTS => Ok(Field::Results),
+ _ => {
+ if IG {
+ Ok(Field::Other)
+ } else if RE {
+ Err(E::unknown_field(v, PRF_REG_OUTPUTS_FIELDS))
+ } else {
+ Err(E::unknown_field(v, PRF_AUTH_OUTPUTS_FIELDS))
+ }
+ }
+ }
+ }
+ }
+ deserializer.deserialize_identifier(FieldVisitor)
+ }
+ }
+ let mut enabled = None;
+ let mut results = None;
+ while let Some(key) = map.next_key::<Field<REL, REG>>()? {
+ match key {
+ Field::Enabled => {
+ if enabled.is_some() {
+ return Err(Error::duplicate_field(ENABLED));
+ }
+ enabled = map.next_value().map(Some)?;
+ }
+ Field::Results => {
+ if results.is_some() {
+ return Err(Error::duplicate_field(RESULTS));
+ }
+ results = map.next_value::<Option<Prf>>().map(Some)?;
+ }
+ Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
+ }
+ }
+ if REG {
+ enabled.ok_or_else(|| Error::missing_field(ENABLED)).and_then(|e| {
+ if e || results.is_none() {
+ Ok(())
+ } else {
+ Err(Error::custom("prf must not have 'results', including a null 'results', if 'enabled' is false"))
+ }
+ })
+ } else {
+ Ok(())
+ }.map(|()| AuthenticationExtensionsPrfOutputsHelper(enabled, PhantomData))
+ }
+}
+/// `"enabled"`
+const ENABLED: &str = "enabled";
+/// `"results"`
+const RESULTS: &str = "results";
+/// `AuthenticationExtensionsPrfOutputs` field during registration.
+const PRF_REG_OUTPUTS_FIELDS: &[&str; 2] = &[ENABLED, RESULTS];
+/// `AuthenticationExtensionsPrfOutputs` field during authentication.
+const PRF_AUTH_OUTPUTS_FIELDS: &[&str; 1] = &[RESULTS];
+impl<'de, const REL: bool, const REG: bool, Prf> Deserialize<'de>
+ for AuthenticationExtensionsPrfOutputsHelper<REL, REG, Prf>
+where
+ for<'a> Prf: Deserialize<'a>,
+{
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_struct(
+ "AuthenticationExtensionsPrfOutputs",
+ if REG {
+ PRF_REG_OUTPUTS_FIELDS
+ } else {
+ PRF_AUTH_OUTPUTS_FIELDS
+ },
+ AuthenticationExtensionsPrfOutputsVisitor::<REL, REG, Prf>(PhantomData),
+ )
+ }
+}
diff --git a/src/response/ser_relaxed.rs b/src/response/ser_relaxed.rs
@@ -0,0 +1,674 @@
+#![expect(
+ clippy::pub_use,
+ clippy::question_mark_used,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
+)]
+extern crate alloc;
+use super::{
+ ser::{
+ AuthenticationExtensionsPrfValues, AuthenticationExtensionsPrfValuesVisitor,
+ PRF_VALUES_FIELDS,
+ },
+ ClientDataJsonParser, CollectedClientData, Origin,
+};
+#[cfg(doc)]
+use super::{Challenge, LimitedVerificationParser};
+use alloc::borrow::Cow;
+use core::{
+ fmt::{self, Formatter},
+ marker::PhantomData,
+};
+#[cfg(doc)]
+use data_encoding::BASE64URL_NOPAD;
+use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor};
+#[cfg(doc)]
+use serde_json::de;
+/// Category returned by [`SerdeJsonErr::classify`].
+#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+pub use serde_json::error::Category;
+/// Error returned by [`CollectedClientData::from_client_data_json_relaxed`] or any of the [`Deserialize`]
+/// implementations when relying on [`de::Deserializer`] or [`de::StreamDeserializer`].
+#[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))]
+pub use serde_json::error::Error as SerdeJsonErr;
+/// "Relaxed" [`ClientDataJsonParser`].
+///
+/// Unlike [`LimitedVerificationParser`] which requires
+/// [JSON-compatible serialization of client data](https://www.w3.org/TR/webauthn-3/#collectedclientdata-json-compatible-serialization-of-client-data)
+/// to be parsed _exactly_ as required by
+/// the [limited verification algorithm](https://www.w3.org/TR/webauthn-3/#clientdatajson-verification),
+/// this is a "relaxed" parser.
+///
+/// L1 clients predate the JSON-compatible serialization of client data; additionally there are L2 and L3 clients
+/// that don't adhere to the JSON-compatible serialization of client data despite being required to. These clients
+/// serialize `CollectedClientData` so that it's valid JSON and conforms to the Web IDL `dictionary` and nothing more.
+/// Furthermore the spec requires that data be decoded in a way equivalent
+/// to [UTF-8 decode](https://encoding.spec.whatwg.org/#utf-8-decode) which both interprets a leading zero
+/// width no-breaking space (i.e., U+FEFF) as a byte-order mark (BOM) as well as replaces any sequences of invalid
+/// UTF-8 code units with the replacement character (i.e., U+FFFD). That is precisely what this parser does.
+///
+/// In particular the parser errors iff any of the following is true:
+///
+/// * The payload is not valid JSON _after_ ignoring a leading U+FEFF and replacing any sequences of invalid
+/// UTF-8 code units with U+FFFD.
+/// * The JSON does not conform to the Web IDL `dictionary`.
+/// * [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type) is not `"webauthn.create"`
+/// or `"webauthn.get"` when `REGISTRATION` and `!REGISTRATION` respectively.
+/// * [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) is not a
+/// base64url-encoded [`Challenge`].
+/// * Existence of duplicate keys in the root object _including_ keys that otherwise would have been ignored.
+pub(super) struct RelaxedClientDataJsonParser<const REGISTRATION: bool>;
+impl<const R: bool> ClientDataJsonParser for RelaxedClientDataJsonParser<R> {
+ type Err = SerdeJsonErr;
+ fn parse(json: &[u8]) -> Result<CollectedClientData<'_>, Self::Err> {
+ /// U+FEFF encoded in UTF-8.
+ const BOM: [u8; 3] = [0xef, 0xbb, 0xbf];
+ // We avoid first calling `String::from_utf8_lossy` since `CDataJsonHelper` relies on
+ // [`Deserializer::deserialize_bytes`] instead of [`Deserializer::deserialize_str`] and
+ // [`Deserializer::deserialize_identifier`]. Additionally [`CollectedClientData::origin`] and
+ // [`CollectedClientData::top_origin`] are the only fields that need to actually replace invalid
+ // UTF-8 code units, and this is achieved via the inner `OriginWrapper` type.
+ serde_json::from_slice::<CDataJsonHelper<'_, R>>(json.split_at_checked(BOM.len()).map_or(
+ json,
+ |(bom, rem)| {
+ if bom == BOM {
+ rem
+ } else {
+ json
+ }
+ },
+ ))
+ .map(|val| val.0)
+ }
+}
+/// "type".
+const TYPE: &str = "type";
+/// "challenge".
+const CHALLENGE: &str = "challenge";
+/// "origin".
+const ORIGIN: &str = "origin";
+/// "crossOrigin".
+const CROSS_ORIGIN: &str = "crossOrigin";
+/// "topOrigin".
+const TOP_ORIGIN: &str = "topOrigin";
+/// Fields for `CollectedClientData`.
+const FIELDS: &[&str; 5] = &[TYPE, CHALLENGE, ORIGIN, CROSS_ORIGIN, TOP_ORIGIN];
+/// Helper for [`RelaxedClientDataJsonParser`].
+struct RelaxedHelper<'a, const REGISTRATION: bool>(PhantomData<fn() -> &'a ()>);
+impl<'de: 'a, 'a, const R: bool> Visitor<'de> for RelaxedHelper<'a, R> {
+ type Value = CollectedClientData<'a>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("CollectedClientData")
+ }
+ #[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<'de>,
+ {
+ /// "webauthn.create".
+ const CREATE: &str = "webauthn.create";
+ /// "webauthn.get".
+ const GET: &str = "webauthn.get";
+ /// `CollectedClientData` fields.
+ enum Field {
+ /// "type" field.
+ Type,
+ /// "challenge" field.
+ Challenge,
+ /// "origin" field.
+ Origin,
+ /// "crossOrigin" field.
+ CrossOrigin,
+ /// "topOrigin" field.
+ TopOrigin,
+ /// Unknown field.
+ Other,
+ }
+ impl<'d> Deserialize<'d> for Field {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'d>,
+ {
+ /// `Visitor` for `Field`.
+ struct FieldVisitor;
+ impl Visitor<'_> for FieldVisitor {
+ type Value = Field;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{TYPE}', '{CHALLENGE}', '{ORIGIN}', '{CROSS_ORIGIN}', or '{TOP_ORIGIN}'")
+ }
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ b"type" => Ok(Field::Type),
+ b"challenge" => Ok(Field::Challenge),
+ b"origin" => Ok(Field::Origin),
+ b"crossOrigin" => Ok(Field::CrossOrigin),
+ b"topOrigin" => Ok(Field::TopOrigin),
+ _ => Ok(Field::Other),
+ }
+ }
+ }
+ // MUST NOT call `Deserializer::deserialize_identifier` since that will call
+ // `FieldVisitor::visit_str` which obviously requires decoding the field as UTF-8 first.
+ deserializer.deserialize_bytes(FieldVisitor)
+ }
+ }
+ /// Deserializes the type value.
+ /// Contains `true` iff the type is `"webauthn.create"`; otherwise the
+ /// value is `"webauthn.get"`.
+ struct Type(bool);
+ impl<'d> Deserialize<'d> for Type {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'d>,
+ {
+ /// `Visitor` for `Type`.
+ struct TypeVisitor;
+ impl Visitor<'_> for TypeVisitor {
+ type Value = Type;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{CREATE}' or '{GET}'")
+ }
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ match v {
+ b"webauthn.create" => Ok(Type(true)),
+ b"webauthn.get" => Ok(Type(false)),
+ _ => Err(Error::invalid_value(
+ Unexpected::Bytes(v),
+ &format!("'{CREATE}' or '{GET}'").as_str(),
+ )),
+ }
+ }
+ }
+ deserializer.deserialize_bytes(TypeVisitor)
+ }
+ }
+ /// `newtype` around `Origin` that implements [`Deserialize`] such that invalid UTF-8 code units are first
+ /// replaced with the replacement character. We don't do this for `Origin` since we want its public API
+ /// to forbid invalid UTF-8.
+ struct OriginWrapper<'d>(Origin<'d>);
+ impl<'d: 'e, 'e> Deserialize<'d> for OriginWrapper<'e> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'d>,
+ {
+ /// `Visitor` for `OriginWrapper`.
+ struct OriginWrapperVisitor<'f>(PhantomData<fn() -> &'f ()>);
+ impl<'f: 'g, 'g> Visitor<'f> for OriginWrapperVisitor<'g> {
+ type Value = OriginWrapper<'g>;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str("OriginWrapper")
+ }
+ fn visit_borrowed_bytes<E>(self, v: &'f [u8]) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Ok(OriginWrapper(Origin(String::from_utf8_lossy(v))))
+ }
+ #[expect(unsafe_code, reason = "safety comment justifies its use")]
+ fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ Ok(OriginWrapper(Origin(
+ match String::from_utf8_lossy(v.as_slice()) {
+ Cow::Borrowed(_) => {
+ // SAFETY:
+ // `String::from_utf8_lossy` returns `Cow::Borrowed` iff the input was valid
+ // UTF-8.
+ let val = unsafe { String::from_utf8_unchecked(v) };
+ Cow::Owned(val)
+ }
+ Cow::Owned(val) => Cow::Owned(val),
+ },
+ )))
+ }
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ self.visit_byte_buf(v.to_owned())
+ }
+ }
+ deserializer.deserialize_bytes(OriginWrapperVisitor(PhantomData))
+ }
+ }
+ let mut typ = false;
+ let mut chall = None;
+ let mut orig = None;
+ let mut cross = None;
+ let mut top_orig = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::Type => {
+ if typ {
+ return Err(Error::duplicate_field(TYPE));
+ }
+ typ = map.next_value::<Type>().and_then(|v| {
+ if v.0 {
+ if R {
+ Ok(true)
+ } else {
+ Err(Error::invalid_value(Unexpected::Str(CREATE), &GET))
+ }
+ } else if R {
+ Err(Error::invalid_value(Unexpected::Str(GET), &CREATE))
+ } else {
+ Ok(true)
+ }
+ })?;
+ }
+ Field::Challenge => {
+ if chall.is_some() {
+ return Err(Error::duplicate_field(CHALLENGE));
+ }
+ chall = map.next_value().map(Some)?;
+ }
+ Field::Origin => {
+ if orig.is_some() {
+ return Err(Error::duplicate_field(ORIGIN));
+ }
+ orig = map.next_value::<OriginWrapper<'_>>().map(|o| Some(o.0))?;
+ }
+ Field::CrossOrigin => {
+ if cross.is_some() {
+ return Err(Error::duplicate_field(CROSS_ORIGIN));
+ }
+ cross = map.next_value().map(Some)?;
+ }
+ Field::TopOrigin => {
+ if top_orig.is_some() {
+ return Err(Error::duplicate_field(TOP_ORIGIN));
+ }
+ top_orig = map.next_value::<Option<OriginWrapper<'_>>>().map(Some)?;
+ }
+ // `IgnoredAny` ignores invalid UTF-8 in and only in JSON strings.
+ Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
+ }
+ }
+ if typ {
+ chall
+ .ok_or_else(|| Error::missing_field(CHALLENGE))
+ .and_then(|challenge| {
+ orig.ok_or_else(|| Error::missing_field(ORIGIN))
+ .map(|origin| CollectedClientData {
+ challenge,
+ origin,
+ cross_origin: cross.flatten().unwrap_or_default(),
+ top_origin: top_orig.flatten().map(|o| o.0),
+ })
+ })
+ } else {
+ Err(Error::missing_field(TYPE))
+ }
+ }
+}
+/// `newtype` around [`CollectedClientData`] to avoid implementing [`Deserialize`] for `CollectedClientData`.
+struct CDataJsonHelper<'a, const REGISTRATION: bool>(CollectedClientData<'a>);
+impl<'de: 'a, 'a, const R: bool> Deserialize<'de> for CDataJsonHelper<'a, R> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer
+ .deserialize_struct(
+ "CollectedClientData",
+ FIELDS,
+ RelaxedHelper::<R>(PhantomData),
+ )
+ .map(Self)
+ }
+}
+/// `newtype` around `AuthenticationExtensionsPrfValues` with a "relaxed" [`Self::deserialize`] implementation.
+pub(super) struct AuthenticationExtensionsPrfValuesRelaxed(pub AuthenticationExtensionsPrfValues);
+impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfValuesRelaxed {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer
+ .deserialize_struct(
+ "AuthenticationExtensionsPrfValuesRelaxed",
+ PRF_VALUES_FIELDS,
+ AuthenticationExtensionsPrfValuesVisitor::<true>,
+ )
+ .map(Self)
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::{ClientDataJsonParser, Cow, RelaxedClientDataJsonParser};
+ use serde::de::{Error as _, Unexpected};
+ use serde_json::Error;
+ #[test]
+ fn relaxed_client_data_json() {
+ // Base case is correct.
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.create",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).map_or(false, |c| {
+ c.cross_origin
+ && c.challenge.0
+ == u128::from_le_bytes([
+ 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0,
+ ])
+ && matches!(c.origin.0, Cow::Borrowed(o) if o == "https://example.com")
+ && c.top_origin.map_or(
+ false,
+ |t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org"),
+ )
+ })
+ );
+ // Base case is correct.
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.get",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert!(
+ RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).map_or(false, |c| {
+ c.cross_origin
+ && c.challenge.0
+ == u128::from_le_bytes([
+ 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0,
+ ])
+ && matches!(c.origin.0, Cow::Borrowed(o) if o == "https://example.com")
+ && c.top_origin.map_or(
+ false,
+ |t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org"),
+ )
+ })
+ );
+ // Unknown keys are allowed.
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.create",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org",
+ "foo": true
+ })
+ .to_string();
+ assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok());
+ // Duplicate keys are forbidden.
+ let input = "{
+ \"challenge\": \"ABABABABABABABABABABAA\",
+ \"type\": \"webauthn.create\",
+ \"origin\": \"https://example.com\",
+ \"crossOrigin\": true,
+ \"topOrigin\": \"https://example.org\",
+ \"crossOrigin\": true,
+ }";
+ let mut err = Error::duplicate_field("crossOrigin")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `crossOrigin`.
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.create",
+ "origin": "https://example.com",
+ "crossOrigin": null,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .map_or(false, |c| !c.cross_origin));
+ // Missing `crossOrigin`.
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.create",
+ "origin": "https://example.com",
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .map_or(false, |c| !c.cross_origin));
+ // `null` `topOrigin`.
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.create",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": null
+ })
+ .to_string();
+ assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .map_or(false, |c| c.top_origin.is_none()));
+ // Missing `topOrigin`.
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.create",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ })
+ .to_string();
+ assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .map_or(false, |c| c.top_origin.is_none()));
+ // `null` `challenge`.
+ err = Error::invalid_type(
+ Unexpected::Other("null"),
+ &"base64 encoding of the 16-byte challenge in a URL safe way without padding",
+ )
+ .to_string()
+ .into_bytes();
+ let input = serde_json::json!({
+ "challenge": null,
+ "type": "webauthn.create",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert_eq!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `challenge`.
+ err = Error::missing_field("challenge").to_string().into_bytes();
+ let input = serde_json::json!({
+ "type": "webauthn.create",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert_eq!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `type`.
+ err = Error::invalid_type(
+ Unexpected::Other("null"),
+ &"'webauthn.create' or 'webauthn.get'",
+ )
+ .to_string()
+ .into_bytes();
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": null,
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert_eq!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `type`.
+ err = Error::missing_field("type").to_string().into_bytes();
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert_eq!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `null` `origin`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"OriginWrapper")
+ .to_string()
+ .into_bytes();
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.get",
+ "origin": null,
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert_eq!(
+ RelaxedClientDataJsonParser::<false>::parse(input.as_bytes())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Missing `origin`.
+ err = Error::missing_field("origin").to_string().into_bytes();
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.get",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert_eq!(
+ RelaxedClientDataJsonParser::<false>::parse(input.as_bytes())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Mismatched `type`.
+ err = Error::invalid_value(Unexpected::Str("webauthn.create"), &"webauthn.get")
+ .to_string()
+ .into_bytes();
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.create",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert_eq!(
+ RelaxedClientDataJsonParser::<false>::parse(input.as_bytes())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // Mismatched `type`.
+ err = Error::invalid_value(Unexpected::Str("webauthn.get"), &"webauthn.create")
+ .to_string()
+ .into_bytes();
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.get",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert_eq!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_bytes())
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
+ );
+ // `crossOrigin` can be `false` even when `topOrigin` exists.
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.get",
+ "origin": "https://example.com",
+ "crossOrigin": false,
+ "topOrigin": "https://example.org"
+ })
+ .to_string();
+ assert!(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).is_ok());
+ // `crossOrigin` can be `true` even when `topOrigin` does not exist.
+ let input = serde_json::json!({
+ "challenge": "ABABABABABABABABABABAA",
+ "type": "webauthn.get",
+ "origin": "https://example.com",
+ "crossOrigin": true,
+ })
+ .to_string();
+ assert!(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).is_ok());
+ // BOM is removed.
+ let input = "\u{feff}{
+ \"challenge\": \"ABABABABABABABABABABAA\",
+ \"type\": \"webauthn.create\",
+ \"origin\": \"https://example.com\",
+ \"crossOrigin\": true,
+ \"topOrigin\": \"https://example.org\"
+ }";
+ assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok());
+ // Invalid Unicode is replaced.
+ let input = b"{
+ \"challenge\": \"ABABABABABABABABABABAA\",
+ \"type\": \"webauthn.create\",
+ \"origin\": \"https://\xffexample.com\",
+ \"crossOrigin\": true,
+ \"topOrigin\": \"https://example.org\"
+ }";
+ assert!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_slice()).map_or(false, |c| {
+ matches!(c.origin.0, Cow::Owned(o) if o == "https://\u{fffd}example.com")
+ })
+ );
+ // Escape characters are de-escaped.
+ let input = b"{
+ \"challenge\": \"ABABABABABABABABABABAA\",
+ \"type\": \"webauthn\\u002ecreate\",
+ \"origin\": \"https://examp\\\\le.com\",
+ \"crossOrigin\": true,
+ \"topOrigin\": \"https://example.org\"
+ }";
+ assert!(
+ RelaxedClientDataJsonParser::<true>::parse(input.as_slice()).map_or(false, |c| {
+ matches!(c.origin.0, Cow::Owned(o) if o == "https://examp\\le.com")
+ })
+ );
+ }
+}