webauthn_rp

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

README.md (25189B)


      1 # `webauthn_rp`
      2 
      3 [<img alt="git" src="https://git.philomathiclife.com/badges/webauthn_rp.svg" height="20">](https://git.philomathiclife.com/webauthn_rp/log.html)
      4 [<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)
      5 [<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/)
      6 
      7 `webauthn_rp` is a library for _server-side_
      8 [Web Authentication (WebAuthn)](https://www.w3.org/TR/webauthn-3/#sctn-rp-operations) Relying Party
      9 (RP) operations.
     10 
     11 The purpose of a server-side RP library is to be modular so that any client can be used with it as a backend
     12 _including_ native applications—WebAuthn technically only covers web applications; however it's relatively easy
     13 to adapt to native applications as well. It achieves this by not assuming how data is sent to/from the client;
     14 having said that, there are pre-defined serialization formats for "common" deployments which can be used when
     15 [`serde`](#serde) is enabled.
     16 
     17 ## `webauthn_rp` in action
     18 
     19 ```rust
     20 use core::convert;
     21 use webauthn_rp::{
     22     AuthenticatedCredential64, DiscoverableAuthentication64, DiscoverableAuthenticationServerState,
     23     DiscoverableCredentialRequestOptions, CredentialCreationOptions64, RegisteredCredential64,
     24     Registration, RegistrationServerState64,
     25     hash::hash_set::FixedCapHashSet,
     26     request::{
     27         AsciiDomainStatic, PublicKeyCredentialDescriptor, RpId,
     28         auth::AuthenticationVerificationOptions,
     29         register::{
     30             Nickname, PublicKeyCredentialUserEntity64, RegistrationVerificationOptions,
     31             UserHandle64, Username,
     32         },
     33     },
     34     response::{
     35         CredentialId,
     36         auth::error::AuthCeremonyErr,
     37         register::{CompressedPubKeyOwned, DynamicState, error::RegCeremonyErr},
     38     },
     39 };
     40 use serde::de::{Deserialize, Deserializer};
     41 use serde_json::Error as JsonErr;
     42 /// The RP ID our application uses.
     43 const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap());
     44 /// The registration verification options.
     45 const REG_OPTS: &RegistrationVerificationOptions::<'static, 'static, &'static str, &'static str> = &RegistrationVerificationOptions::new();
     46 /// The authentication verification options.
     47 const AUTH_OPTS: &AuthenticationVerificationOptions::<'static, 'static, &'static str, &'static str> = &AuthenticationVerificationOptions::new();
     48 /// Error we return in our application when a function fails.
     49 enum AppErr {
     50     /// WebAuthn registration ceremony failed.
     51     RegCeremony(RegCeremonyErr),
     52     /// WebAuthn authentication ceremony failed.
     53     AuthCeremony(AuthCeremonyErr),
     54     /// Unable to insert a WebAuthn ceremony.
     55     WebAuthnCeremonyCreation,
     56     /// WebAuthn ceremony does not exist; thus the ceremony could not be completed.
     57     MissingWebAuthnCeremony,
     58     /// General error related to JSON deserialization.
     59     Json(JsonErr),
     60     /// No account exists associated with a particular `UserHandle64`.
     61     NoAccount,
     62     /// No credential exists associated with a particular `CredentialId`.
     63     NoCredential,
     64     /// `CredentialId` exists but the associated `UserHandle64` does not match.
     65     CredentialUserIdMismatch,
     66 }
     67 impl From<JsonErr> for AppErr {
     68     fn from(value: JsonErr) -> Self {
     69         Self::Json(value)
     70     }
     71 }
     72 impl From<RegCeremonyErr> for AppErr {
     73     fn from(value: RegCeremonyErr) -> Self {
     74         Self::RegCeremony(value)
     75     }
     76 }
     77 impl From<AuthCeremonyErr> for AppErr {
     78     fn from(value: AuthCeremonyErr) -> Self {
     79         Self::AuthCeremony(value)
     80     }
     81 }
     82 /// First-time account creation.
     83 ///
     84 /// This gets sent from the user after an account is created on their side. The registration ceremony
     85 /// still has to be successfully completed for the account to be created server side. In the event of an error,
     86 /// the user should delete the created passkey since it won't be usable.
     87 struct AccountReg<'a, 'b> {
     88     registration: Registration,
     89     user_name: Username<'a>,
     90     user_display_name: Nickname<'b>,
     91 }
     92 impl<'de: 'a + 'b, 'a, 'b> Deserialize<'de> for AccountReg<'a, 'b> {
     93     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     94     where
     95         D: Deserializer<'de>,
     96     {
     97         // ⋮
     98     }
     99 }
    100 /// Starts account creation.
    101 ///
    102 /// This only makes sense for greenfield deployments since account information (e.g., user name) would likely
    103 /// already exist otherwise. This is similar to credential creation except a random `UserHandle64` is generated and
    104 /// will be used for subsequent credential registrations.
    105 fn start_account_creation(
    106     reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
    107 ) -> Result<Vec<u8>, AppErr> {
    108     let user_id = UserHandle64::new();
    109     let (server, client) =
    110         CredentialCreationOptions64::first_passkey_with_blank_user_info(
    111             RP_ID, &user_id,
    112         )
    113         .start_ceremony()
    114         .unwrap_or_else(|_e| {
    115             unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
    116         });
    117     if reg_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity)
    118     {
    119         Ok(serde_json::to_vec(&client)
    120             .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState64::serialize")))
    121     } else {
    122         Err(AppErr::WebAuthnCeremonyCreation)
    123     }
    124 }
    125 /// Finishes account creation.
    126 ///
    127 /// Pending a successful registration ceremony, a new account associated with the randomly generated
    128 /// `UserHandle64` will be created with a corresponding passkey entry. This passkey will be used to
    129 /// log into the application.
    130 ///
    131 /// Note if this errors, then the user should be notified to delete the passkey created on their
    132 /// authenticator.
    133 fn finish_account_creation(
    134     reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
    135     client_data: &[u8],
    136 ) -> Result<(), AppErr> {
    137     let account = serde_json::from_slice::<AccountReg<'_, '_>>(client_data)?;
    138     insert_account(
    139         &account,
    140         reg_ceremonies
    141             // `Registration::challenge_relaxed` is available iff `serde_relaxed` is enabled.
    142             .take(&account.registration.challenge_relaxed()?)
    143             .ok_or(AppErr::MissingWebAuthnCeremony)?
    144             .verify(
    145                 RP_ID,
    146                 &account.registration,
    147                 REG_OPTS,
    148             )?,
    149     )
    150 }
    151 /// Starts passkey registration.
    152 ///
    153 /// This is used for _existing_ accounts where the user is already logged in and wants to register another
    154 /// passkey. This is similar to account creation except we already have the user entity info and we need to
    155 /// fetch the registered `PublicKeyCredentialDescriptor`s to avoid accidentally overwriting a passkey on
    156 /// the authenticator.
    157 fn start_cred_registration(
    158     user_id: &UserHandle64,
    159     reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
    160 ) -> Result<Vec<u8>, AppErr> {
    161     let (entity, creds) = select_user_info(user_id)?.ok_or(AppErr::NoAccount)?;
    162     let (server, client) = CredentialCreationOptions64::passkey(RP_ID, entity, creds)
    163         .start_ceremony()
    164         .unwrap_or_else(|_e| {
    165             unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
    166         });
    167     if reg_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity)
    168     {
    169         Ok(serde_json::to_vec(&client)
    170             .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState64::serialize")))
    171     } else {
    172         Err(AppErr::WebAuthnCeremonyCreation)
    173     }
    174 }
    175 /// Finishes passkey registration.
    176 ///
    177 /// Pending a successful registration ceremony, a new credential associated with the `UserHandle64`
    178 /// will be created. This passkey can then be used to log into the application just like any other registered
    179 /// passkey.
    180 ///
    181 /// Note if this errors, then the user should be notified to delete the passkey created on their
    182 /// authenticator.
    183 fn finish_cred_registration(
    184     reg_ceremonies: &mut FixedCapHashSet<RegistrationServerState64>,
    185     client_data: &[u8],
    186 ) -> Result<(), AppErr> {
    187     // `Registration::from_json_custom` is available iff `serde_relaxed` is enabled.
    188     let registration = Registration::from_json_custom(client_data)?;
    189     insert_credential(
    190         reg_ceremonies
    191             // `Registration::challenge_relaxed` is available iff `serde_relaxed` is enabled.
    192             .take(&registration.challenge_relaxed()?)
    193             .ok_or(AppErr::MissingWebAuthnCeremony)?
    194             .verify(
    195                 RP_ID,
    196                 &registration,
    197                 REG_OPTS,
    198             )?,
    199     )
    200 }
    201 /// Starts the passkey authentication ceremony.
    202 fn start_auth(
    203     auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
    204 ) -> Result<Vec<u8>, AppErr> {
    205     let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID)
    206         .start_ceremony()
    207         .unwrap_or_else(|_e| {
    208             unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error")
    209         });
    210     if auth_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity)
    211     {
    212         Ok(serde_json::to_vec(&client).unwrap_or_else(|_e| {
    213             unreachable!("bug in DiscoverableAuthenticationClientState::serialize")
    214         }))
    215     } else {
    216         Err(AppErr::WebAuthnCeremonyCreation)
    217     }
    218 }
    219 /// Finishes the passkey authentication ceremony.
    220 fn finish_auth(
    221     auth_ceremonies: &mut FixedCapHashSet<DiscoverableAuthenticationServerState>,
    222     client_data: &[u8],
    223 ) -> Result<(), AppErr> {
    224     // `DiscoverableAuthentication64::from_json_custom` is available iff `serde_relaxed` is enabled.
    225     let authentication =
    226         DiscoverableAuthentication64::from_json_custom(client_data)?;
    227     let mut cred = select_credential(
    228         authentication.raw_id(),
    229         authentication.response().user_handle(),
    230     )?
    231     .ok_or(AppErr::NoCredential)?;
    232     if auth_ceremonies
    233         // `DiscoverableAuthentication64::challenge_relaxed` is available iff `serde_relaxed` is enabled.
    234         .take(&authentication.challenge_relaxed()?)
    235         .ok_or(AppErr::MissingWebAuthnCeremony)?
    236         .verify(
    237             RP_ID,
    238             &authentication,
    239             &mut cred,
    240             AUTH_OPTS,
    241         )?
    242     {
    243         update_credential(cred.id(), cred.dynamic_state())
    244     } else {
    245         Ok(())
    246     }
    247 }
    248 /// Writes `account` and `cred` to storage.
    249 ///
    250 /// # Errors
    251 ///
    252 /// Errors iff writing `account` or `cred` errors,  there already exists a credential using the same
    253 /// `CredentialId`, or there already exists an account using the same `UserHandle64`.
    254 fn insert_account(
    255     account: &AccountReg<'_, '_>,
    256     cred: RegisteredCredential64<'_>,
    257 ) -> Result<(), AppErr> {
    258     // ⋮
    259 }
    260 /// Fetches the user info and registered credentials associated with `user_id`.
    261 ///
    262 /// # Errors
    263 ///
    264 /// Errors iff fetching the data errors.
    265 fn select_user_info(
    266     user_id: &UserHandle64,
    267 ) -> Result<
    268     Option<(
    269         PublicKeyCredentialUserEntity64<'static, 'static, '_>,
    270         Vec<PublicKeyCredentialDescriptor<Vec<u8>>>,
    271     )>,
    272     AppErr,
    273 > {
    274     // ⋮
    275 }
    276 /// Writes `cred` to storage.
    277 ///
    278 /// # Errors
    279 ///
    280 /// Errors iff writing `cred` errors, there already exists a credential using the same `CredentialId`,
    281 /// or there does not exist an account under the `UserHandle64`.
    282 fn insert_credential(
    283     cred: RegisteredCredential64<'_>,
    284 ) -> Result<(), AppErr> {
    285     // ⋮
    286 }
    287 /// Fetches the `AuthenticatedCredential` associated with `cred_id` ensuring `user_id` matches the
    288 /// `UserHandle64` associated with the account.
    289 ///
    290 /// # Errors
    291 ///
    292 /// Errors iff fetching the data errors or the `user_id` does not match the stored `UserHandle64`.
    293 fn select_credential<'cred, 'user>(
    294     cred_id: CredentialId<&'cred [u8]>,
    295     user_id: &'user UserHandle64,
    296 ) -> Result<
    297     Option<
    298         AuthenticatedCredential64<
    299             'cred,
    300             'user,
    301             CompressedPubKeyOwned,
    302         >,
    303     >,
    304     AppErr,
    305 > {
    306     // ⋮
    307 }
    308 /// Overwrites the current `DynamicState` associated with `cred_id` with `dynamic_state`.
    309 ///
    310 /// # Errors
    311 ///
    312 /// Errors iff writing errors or `cred_id` does not exist.
    313 fn update_credential(
    314     cred_id: CredentialId<&[u8]>,
    315     dynamic_state: DynamicState,
    316 ) -> Result<(), AppErr> {
    317     // ⋮
    318 }
    319 ```
    320 
    321 ## Cargo "features"
    322 
    323 [`custom`](#custom) or both [`bin`](#bin) and [`serde`](#serde) must be enabled; otherwise a `compile_error`
    324  will occur.
    325 
    326 ### `bin`
    327 Enables binary (de)serialization via `Encode` and `Decode`. Since registered credentials will almost always
    328 have to be saved to persistent storage, _some_ form of (de)serialization is necessary. In the event `bin` is
    329 unsuitable or only partially suitable (e.g., human-readable output is desired), one will need to enable
    330 [`custom`](#custom) to allow construction of certain types (e.g., `AuthenticatedCredential`).
    331 
    332 If possible and desired, one may wish to save the data "directly" to avoid any potential temporary allocations.
    333 For example `StaticState::encode` will return a `Vec` containing hundreds (and possibly thousands in the
    334 extreme case) of bytes if the underlying public key is an RSA key. This additional allocation and copy of data
    335 is obviously avoided if `StaticState` is stored as a
    336 [composite type](https://www.postgresql.org/docs/current/rowtypes.html) or its fields are stored in separate
    337 columns when written to a relational database (RDB).
    338 
    339 ### `custom`
    340 
    341 Exposes functions (e.g., `AuthenticatedCredential::new`) that allows one to construct instances of types that
    342 cannot be constructed when [`bin`](#bin) or [`serde`](#serde) is not enabled.
    343 
    344 ### `serde`
    345 
    346 This feature _strictly_ adheres to the JSON-motivated definitions. You _will_ encounter clients that send data that
    347 cannot be deserialized using this feature. For many [`serde_relaxed`](#serde_relaxed) should be used instead.
    348 
    349 Enables (de)serialization of data sent to/from the client via [`serde`](https://docs.rs/serde/latest/serde/)
    350 based on the JSON-motivated definitions (e.g.,
    351 [`RegistrationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson)). Since
    352 data has to be sent to/from the client, _some_ form of (de)serialization is necessary. In the event `serde`
    353 is unsuitable or only partially suitable, one will need to enable [`custom`](#custom) to allow construction
    354 of certain types (e.g., `Registration`).
    355 
    356 Code is _strongly_ encouraged to rely on the `Deserialize` implementations as much as possible to reduce the
    357 chances of improperly deserializing the client data.
    358 
    359 Note that clients are free to send data in whatever form works best, so there is no requirement the
    360 JSON-motivated definitions are used even when JSON is sent. This is especially relevant since the JSON-motivated
    361 definitions were only added in [WebAuthn Level 3](https://www.w3.org/TR/webauthn-3/); thus many deployments only
    362 partially conform. Some specific deviations that may require partial customization of deserialization are the
    363 following:
    364 
    365 * [`ArrayBuffer`](https://webidl.spec.whatwg.org/#idl-ArrayBuffer)s encoded using something other than
    366   base64url.
    367 * `ArrayBuffer`s that are encoded multiple times (including the use of different encodings each time).
    368 * Missing fields (e.g.,
    369   [`transports`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-transports)).
    370 * Different field names (e.g., `extensions` instead of
    371   [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-clientextensionresults)).
    372 
    373 ### `serde_relaxed`
    374 
    375 Automatically enables [`serde`](#serde) in addition to "relaxed" `Deserialize` implementations
    376 (e.g., `RegistrationRelaxed`). Roughly "relaxed" translates to unknown fields being ignored and only
    377 the fields necessary for construction of the type are required. Case still matters, duplicate fields are still
    378 forbidden, and interrelated data validation is still performed when applicable. This can be useful when one
    379 wants to accommodate non-conforming clients or clients that implement older versions of the spec.
    380 
    381 ### `serializable_server_state`
    382 
    383 Automatically enables [`bin`](#bin) in addition to `Encode` and `Decode` implementations for
    384 `RegistrationServerState`, `DiscoverableAuthenticationServerState`, and
    385 `NonDiscoverableAuthenticationServerState`. Less accurate `SystemTime` is used instead of `Instant` for
    386 timeout enforcement. This should be enabled if you don't desire to use in-memory collections to store the instances
    387 of those types.
    388 
    389 Note even when written to persistent storage, an application should still periodically remove expired ceremonies.
    390 If one is using a relational database (RDB); then one can achieve this by storing `SentChallenge`,
    391 the `Vec` returned from `Encode::encode`, and `TimedCeremony::expiration` and periodically remove all rows
    392 whose expiration exceeds the current date and time.
    393 
    394 ## Registration and authentication
    395 
    396 Both [registration](https://www.w3.org/TR/webauthn-3/#registration-ceremony) and
    397 [authentication](https://www.w3.org/TR/webauthn-3/#authentication-ceremony) ceremonies rely on "challenges", and
    398 these challenges are inherently temporary. For this reason the data associated with challenge completion can
    399 often be stored in memory without concern for out-of-memory (OOM) conditions. There are several benefits to
    400 storing such data in memory:
    401 
    402 * No data manipulation
    403     * By leveraging move semantics, the data sent to the client cannot be mutated once the ceremony begins.
    404 * Improved timeout enforcement
    405     * By ensuring the same machine that started the ceremony is also used to finish the ceremony, deviation of
    406       system clocks is not a concern. Additionally, allowing serialization requires the use of some form of
    407       cross-platform "timestamp" (e.g., [Unix time](https://en.wikipedia.org/wiki/Unix_time)) which differ in
    408       implementation (e.g., platforms implement leap seconds in different ways) and are often not monotonically
    409       increasing. If data resides in memory, a monotonic `Instant` can be used instead.
    410 
    411 It is for those reasons data like `RegistrationServerState` are not serializable by default and require the
    412 use of in-memory collections (e.g., `FixedCapHashSet`). To better ensure OOM is not a concern, RPs should set
    413 reasonable timeouts. Since ceremonies can only be completed by moving data (e.g.,
    414 `RegistrationServerState::verify`), ceremony completion is guaranteed to free up the memory used—
    415 `RegistrationServerState` instances are as small as 48 bytes on `x86_64-unknown-linux-gnu` platforms. To avoid
    416 issues related to incomplete ceremonies, RPs can periodically iterate the collection for expired ceremonies and
    417 remove such data. Other techniques can be employed as well to mitigate OOM, but they are application specific
    418 and out-of-scope. If this is undesirable, one can enable [`serializable_server_state`](#serializable_server_state)
    419 so that `RegistrationServerState`, `DiscoverableAuthenticationServerState`, and
    420 `NonDiscoverableAuthenticationServerState` implement `Encode` and `Decode`. Another reason one may need to
    421 store this information persistently is for load-balancing purposes where the server that started the ceremony is
    422 not guaranteed to be the server that finishes the ceremony.
    423 
    424 ## Supported signature algorithms
    425 
    426 The only supported signature algorithms are the following:
    427 
    428 * Ed25519 as defined in [RFC 8032 § 5.1](https://www.rfc-editor.org/rfc/rfc8032#section-5.1). This corresponds
    429   to `CoseAlgorithmIdentifier::Eddsa`.
    430 * ECDSA as defined in [SEC 1 Version 2.0 § 4.1](https://www.secg.org/sec1-v2.pdf#subsection.4.1) using SHA-256
    431   as the hash function and NIST P-256 as defined in
    432   [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)
    433   for the underlying elliptic curve. This corresponds to `CoseAlgorithmIdentifier::Es256`.
    434 * 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
    435   [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)
    436   for the underlying elliptic curve. This corresponds to `CoseAlgorithmIdentifier::Es384`.
    437 * RSASSA-PKCS1-v1_5 as defined in [RFC 8017 § 8.2](https://www.rfc-editor.org/rfc/rfc8017#section-8.2) using
    438   SHA-256 as the hash function. This corresponds to `CoseAlgorithmIdentifier::Rs256`.
    439 
    440 ## Correctness of code
    441 
    442 This library more strictly adheres to the spec than many other similar libraries including but not limited to
    443 the following ways:
    444 
    445 * [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).
    446 * `Deserialize` implementations requiring _exact_ conformance (e.g., not allowing unknown data).
    447 * More thorough interrelated data validation (e.g., all places a Credential ID exists must match).
    448 * Implement a lot of recommended (i.e., SHOULD) criteria (e.g.,
    449   [User display names conforming to the Nickname Profile as defined in RFC 8266](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name)).
    450 
    451 Unfortunately like almost all software, this library has not been formally verified; however great care is
    452 employed in the following ways:
    453 
    454 * Leverage move semantics to prevent mutation of data once in a static state.
    455 * Ensure a great many invariants via types.
    456 * Reduce code duplication.
    457 * Reduce variable mutation allowing for simpler algebraic reasoning.
    458 * `panic`-free code[^note] (i.e., define true/total functions).
    459 * Ensure arithmetic "side effects" don't occur (e.g., overflow).
    460 * Aggressive use of compiler and [Clippy](https://doc.rust-lang.org/stable/clippy/lints.html) lints.
    461 * Unit tests for common cases, edge cases, and error cases.
    462 
    463 ## Cryptographic libraries
    464 
    465 This library does not rely on _any_ sensitive data (e.g., private keys) as only signature verification is
    466 ever performed. This means that the only thing that matters with the libraries used is their algorithmic
    467 correctness and not other normally essential aspects like susceptibility to side-channel attacks. While I
    468 personally believe the libraries that are used are at least as "secure" as alternatives even when dealing with
    469 sensitive data, one only needs to audit the correctness of the libraries to be confident in their use. In fact
    470 [`curve25519_dalek`](https://docs.rs/curve25519-dalek/latest/curve25519_dalek/#backends) has been formally
    471 verified when the [`fiat`](https://github.com/mit-plv/fiat-crypto) backend is used making it _objectively_
    472 better than many other libraries whose correctness has not been proven. Two additional benefits of the library
    473 choices are simpler APIs making it more likely their use is correct and better cross-platform compatibility.
    474 
    475 ## Minimum Supported Rust Version (MSRV)
    476 
    477 This will frequently be updated to be the same as stable. Specifically, any time stable is updated and that
    478 update has "useful" features or compilation no longer succeeds (e.g., due to new compiler lints), then MSRV
    479 will be updated.
    480 
    481 MSRV changes will correspond to a SemVer patch version bump pre-`1.0.0`; otherwise a minor version bump.
    482 
    483 ## SemVer Policy
    484 
    485 * All on-by-default features of this library are covered by SemVer
    486 * MSRV is considered exempt from SemVer as noted above
    487 
    488 ## License
    489 
    490 Licensed under either of
    491 
    492 * Apache License, Version 2.0 ([LICENSE-APACHE](https://www.apache.org/licenses/LICENSE-2.0))
    493 * MIT license ([LICENSE-MIT](https://opensource.org/licenses/MIT))
    494 
    495 at your option.
    496 
    497 ## Contribution
    498 
    499 Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you,
    500 as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
    501 
    502 Before any PR is sent, `cargo clippy` and `cargo t` should be run _for each possible combination of "features"_
    503 using stable Rust. One easy way to achieve this is by building `ci` and invoking it with no commands in the
    504 `webauthn_rp` directory or sub-directories. You can fetch `ci` via `git clone https://git.philomathiclife.com/repos/ci`,
    505 and it can be built with `cargo build --release`. Additionally,
    506 `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be built.
    507 
    508 ### Status
    509 
    510 This package is actively maintained and will conform to the
    511 [latest WebAuthn API version](https://www.w3.org/TR/webauthn-3/). Previous versions will not be supported—excluding
    512 bug fixes of course—however functionality will exist to facilitate the migration process from the previous version.
    513 
    514 The crate is only tested on `x86_64-unknown-linux-gnu` and `x86_64-unknown-openbsd` targets, but it should work
    515 on most platforms.
    516 
    517 [^note]: `panic`s related to memory allocations or stack overflow are possible since such issues are not
    518          formally guarded against.