webauthn_rp

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

README.md (25594B)


      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::{InsertRemoveExpired, MaxLenHashSet},
     26     request::{
     27         PublicKeyCredentialDescriptor, RpId,
     28         auth::AuthenticationVerificationOptions,
     29         register::{
     30             PublicKeyCredentialUserEntity64, RegistrationVerificationOptions,
     31             UserHandle64,
     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::from_static_domain("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 {
     88     registration: Registration,
     89     user_name: String,
     90     user_display_name: String,
     91 }
     92 impl<'de> Deserialize<'de> for AccountReg {
     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 MaxLenHashSet<RegistrationServerState64>,
    107 ) -> Result<Vec<u8>, AppErr> {
    108     let user_id = UserHandle64::new();
    109     let (server, client) =
    110         CredentialCreationOptions64::passkey(
    111             RP_ID, PublicKeyCredentialUserEntity64 { id: &user_id, name: "", display_name: "", }, Vec::new()
    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 matches!(reg_ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success)
    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 MaxLenHashSet<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 MaxLenHashSet<RegistrationServerState64>,
    160 ) -> Result<Vec<u8>, AppErr> {
    161     let (username, user_display_name, creds) = select_user_info(user_id)?.ok_or(AppErr::NoAccount)?;
    162     let (server, client) = CredentialCreationOptions64::passkey(RP_ID, PublicKeyCredentialUserEntity64 { name: &username, id: user_id, display_name: &user_display_name, }, 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 matches!(reg_ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success)
    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 MaxLenHashSet<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 MaxLenHashSet<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 matches!(auth_ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success)
    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 MaxLenHashSet<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         String,
    270         String,
    271         Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>,
    272     )>,
    273     AppErr,
    274 > {
    275     // ⋮
    276 }
    277 /// Writes `cred` to storage.
    278 ///
    279 /// # Errors
    280 ///
    281 /// Errors iff writing `cred` errors, there already exists a credential using the same `CredentialId`,
    282 /// or there does not exist an account under the `UserHandle64`.
    283 fn insert_credential(
    284     cred: RegisteredCredential64<'_>,
    285 ) -> Result<(), AppErr> {
    286     // ⋮
    287 }
    288 /// Fetches the `AuthenticatedCredential` associated with `cred_id` ensuring `user_id` matches the
    289 /// `UserHandle64` associated with the account.
    290 ///
    291 /// # Errors
    292 ///
    293 /// Errors iff fetching the data errors or the `user_id` does not match the stored `UserHandle64`.
    294 fn select_credential<'cred, 'user>(
    295     cred_id: CredentialId<&'cred [u8]>,
    296     user_id: &'user UserHandle64,
    297 ) -> Result<
    298     Option<
    299         AuthenticatedCredential64<
    300             'cred,
    301             'user,
    302             CompressedPubKeyOwned,
    303         >,
    304     >,
    305     AppErr,
    306 > {
    307     // ⋮
    308 }
    309 /// Overwrites the current `DynamicState` associated with `cred_id` with `dynamic_state`.
    310 ///
    311 /// # Errors
    312 ///
    313 /// Errors iff writing errors or `cred_id` does not exist.
    314 fn update_credential(
    315     cred_id: CredentialId<&[u8]>,
    316     dynamic_state: DynamicState,
    317 ) -> Result<(), AppErr> {
    318     // ⋮
    319 }
    320 ```
    321 
    322 ## Cargo "features"
    323 
    324 [`custom`](#custom) or both [`bin`](#bin) and [`serde`](#serde) must be enabled; otherwise a `compile_error`
    325  will occur.
    326 
    327 ### `bin`
    328 Enables binary (de)serialization via `Encode` and `Decode`. Since registered credentials will almost always
    329 have to be saved to persistent storage, _some_ form of (de)serialization is necessary. In the event `bin` is
    330 unsuitable or only partially suitable (e.g., human-readable output is desired), one will need to enable
    331 [`custom`](#custom) to allow construction of certain types (e.g., `AuthenticatedCredential`).
    332 
    333 If possible and desired, one may wish to save the data "directly" to avoid any potential temporary allocations.
    334 For example `StaticState::encode` will return a `Vec` containing thousands of bytes if the underlying public key
    335 is an ML-DSA key. This additional allocation and copy of data 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., `MaxLenHashSet`). 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 * ML-DSA-87 as defined in [NIST FIPS 204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf). This
    429   corresponds to `CoseAlgorithmIdentifier::Mldsa87`.
    430 * ML-DSA-65 as defined in [NIST FIPS 204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf). This
    431   corresponds to `CoseAlgorithmIdentifier::Mldsa65`.
    432 * ML-DSA-44 as defined in [NIST FIPS 204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf). This
    433   corresponds to `CoseAlgorithmIdentifier::Mldsa44`.
    434 * Ed25519 as defined in [RFC 8032 § 5.1](https://www.rfc-editor.org/rfc/rfc8032#section-5.1). This corresponds
    435   to `CoseAlgorithmIdentifier::Eddsa`.
    436 * ECDSA as defined in [SEC 1 Version 2.0 § 4.1](https://www.secg.org/sec1-v2.pdf#subsection.4.1) using SHA-256
    437   as the hash function and NIST P-256 as defined in
    438   [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)
    439   for the underlying elliptic curve. This corresponds to `CoseAlgorithmIdentifier::Es256`.
    440 * 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
    441   [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)
    442   for the underlying elliptic curve. This corresponds to `CoseAlgorithmIdentifier::Es384`.
    443 * RSASSA-PKCS1-v1_5 as defined in [RFC 8017 § 8.2](https://www.rfc-editor.org/rfc/rfc8017#section-8.2) using
    444   SHA-256 as the hash function. This corresponds to `CoseAlgorithmIdentifier::Rs256`.
    445 
    446 ## Correctness of code
    447 
    448 This library more strictly adheres to the spec than many other similar libraries including but not limited to
    449 the following ways:
    450 
    451 * [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).
    452 * `Deserialize` implementations requiring _exact_ conformance (e.g., not allowing unknown data).
    453 * More thorough interrelated data validation (e.g., all places a Credential ID exists must match).
    454 * Implement a lot of recommended (i.e., SHOULD) criteria.
    455 
    456 Unfortunately like almost all software, this library has not been formally verified; however great care is
    457 employed in the following ways:
    458 
    459 * Leverage move semantics to prevent mutation of data once in a static state.
    460 * Ensure a great many invariants via types.
    461 * Reduce code duplication.
    462 * Reduce variable mutation allowing for simpler algebraic reasoning.
    463 * `panic`-free code[^note] (i.e., define true/total functions).
    464 * Ensure arithmetic "side effects" don't occur (e.g., overflow).
    465 * Aggressive use of compiler and [Clippy](https://doc.rust-lang.org/stable/clippy/lints.html) lints.
    466 * Unit tests for common cases, edge cases, and error cases.
    467 
    468 ## Cryptographic libraries
    469 
    470 This library does not rely on _any_ sensitive data (e.g., private keys) as only signature verification is
    471 ever performed. This means that the only thing that matters with the libraries used is their algorithmic
    472 correctness and not other normally essential aspects like susceptibility to side-channel attacks. While I
    473 personally believe the libraries that are used are at least as "secure" as alternatives even when dealing with
    474 sensitive data, one only needs to audit the correctness of the libraries to be confident in their use. In fact
    475 [`curve25519_dalek`](https://docs.rs/curve25519-dalek/latest/curve25519_dalek/#backends) has been formally
    476 verified when the [`fiat`](https://github.com/mit-plv/fiat-crypto) backend is used making it _objectively_
    477 better than many other libraries whose correctness has not been proven. Two additional benefits of the library
    478 choices are simpler APIs making it more likely their use is correct and better cross-platform compatibility.
    479 
    480 ## Minimum Supported Rust Version (MSRV)
    481 
    482 This will frequently be updated to be the same as stable. Specifically, any time stable is updated and that
    483 update has "useful" features or compilation no longer succeeds (e.g., due to new compiler lints), then MSRV
    484 will be updated.
    485 
    486 MSRV changes will correspond to a SemVer patch version bump pre-`1.0.0`; otherwise a minor version bump.
    487 
    488 ## SemVer Policy
    489 
    490 * All on-by-default features of this library are covered by SemVer
    491 * MSRV is considered exempt from SemVer as noted above
    492 
    493 ## License
    494 
    495 Licensed under either of
    496 
    497 * Apache License, Version 2.0 ([LICENSE-APACHE](https://www.apache.org/licenses/LICENSE-2.0))
    498 * MIT license ([LICENSE-MIT](https://opensource.org/licenses/MIT))
    499 
    500 at your option.
    501 
    502 ## Contribution
    503 
    504 Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you,
    505 as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
    506 
    507 Before any PR is sent, `cargo clippy --all-targets`, `cargo test --all-targets -- --include-ignored`, and
    508 `cargo test --doc` should be run _for each possible combination of "features"_ using the stable and MSRV toolchains.
    509 One easy way to achieve this is by invoking [`ci-cargo`](https://crates.io/crates/ci-cargo) as
    510 `ci-cargo clippy --all-targets test --all-targets --include-ignored --ignore-compile-errors` in the `webauthn_rp`
    511 directory.
    512 
    513 Last, `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be
    514 built.
    515 
    516 ### Status
    517 
    518 This package is actively maintained and will conform to the
    519 [latest WebAuthn API version](https://www.w3.org/TR/webauthn-3/). Previous versions will not be supported—excluding
    520 bug fixes of course—however functionality will exist to facilitate the migration process from the previous version.
    521 
    522 The crate is only tested on the `x86_64-unknown-linux-gnu`, `x86_64-unknown-openbsd`, and `aarch64-apple-darwin`
    523 targets; but it should work on most platforms.
    524 
    525 [^note]: `panic`s related to memory allocations or stack overflow are possible since such issues are not
    526          formally guarded against.