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