webauthn_rp

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

commit 09f505df6d54ab95baeb64256e3203d412598266
parent 8d4bd507df89f0c896ff8b4cd795aa936a8738ca
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sat,  7 Dec 2024 10:14:04 -0700

level 3 api

Diffstat:
MCargo.toml | 63++++++++++++++++++++++++++++++++++++++++++++++++++-------------
MLICENSE-MIT | 2+-
MREADME.md | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Asrc/bin.rs | 412+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 1169++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/request.rs | 3855+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/auth.rs | 1174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/auth/error.rs | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/auth/ser.rs | 399+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/auth/ser_server_state.rs | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/error.rs | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/register.rs | 1674+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/register/bin.rs | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/register/custom.rs | 18++++++++++++++++++
Asrc/request/register/error.rs | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/register/ser.rs | 1094+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/register/ser_server_state.rs | 262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/ser.rs | 319+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/request/ser_server_state.rs | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response.rs | 1684+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/auth.rs | 531+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/auth/error.rs | 297+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/auth/ser.rs | 1666+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/auth/ser_relaxed.rs | 1282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/bin.rs | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/cbor.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/custom.rs | 18++++++++++++++++++
Asrc/response/error.rs | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/register.rs | 3202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/register/bin.rs | 598+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/register/error.rs | 624+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/register/ser.rs | 5502+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/register/ser_relaxed.rs | 3954+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/ser.rs | 1207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/response/ser_relaxed.rs | 674+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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)&ensp;[![crates-io]](https://crates.io/crates/webauthn_rp)&ensp;[![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(&registration.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, handle, &registration, &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") + }) + ); + } +}