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(®istration.challenge_relaxed()?) 193 .ok_or(AppErr::MissingWebAuthnCeremony)? 194 .verify( 195 RP_ID, 196 ®istration, 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.