response.rs (107399B)
1 extern crate alloc; 2 use crate::{ 3 request::{register::{PublicKeyCredentialUserEntity, UserHandle}, Challenge, RpId, Url}, 4 response::{ 5 auth::error::{ 6 AuthCeremonyErr, AuthenticatorDataErr as AuthAuthDataErr, 7 AuthenticatorExtensionOutputErr as AuthAuthExtErr, 8 }, 9 error::{CollectedClientDataErr, CredentialIdErr}, 10 register::error::{AttestationObjectErr, AttestedCredentialDataErr, AuthenticatorDataErr as RegAuthDataErr, AuthenticatorExtensionOutputErr as RegAuthExtErr, PubKeyErr, RegCeremonyErr}, 11 }, 12 }; 13 use alloc::borrow::Cow; 14 use core::{ 15 borrow::Borrow, 16 cmp::Ordering, 17 convert::Infallible, 18 fmt::{self, Display, Formatter}, 19 hash::{Hash, Hasher}, 20 str, 21 }; 22 use data_encoding::BASE64URL_NOPAD; 23 use rsa::sha2::{digest::OutputSizeUser as _, Sha256}; 24 #[cfg(feature = "serde_relaxed")] 25 use ser_relaxed::SerdeJsonErr; 26 /// Contains functionality for completing the 27 /// [authentication ceremony](https://www.w3.org/TR/webauthn-3/#authentication-ceremony). 28 /// 29 /// # Examples 30 /// 31 /// ```no_run 32 /// # use core::convert; 33 /// # use data_encoding::BASE64URL_NOPAD; 34 /// # use webauthn_rp::{ 35 /// # hash::hash_set::FixedCapHashSet, 36 /// # request::{auth::{error::InvalidTimeout, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, AsciiDomainStatic, BackupReq, RpId}, 37 /// # response::{auth::{error::AuthCeremonyErr, DiscoverableAuthentication64}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKeyOwned, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId}, 38 /// # AuthenticatedCredential, CredentialErr 39 /// # }; 40 /// # #[derive(Debug)] 41 /// # enum E { 42 /// # CollectedClientData(CollectedClientDataErr), 43 /// # RpId(AsciiDomainErr), 44 /// # InvalidTimeout(InvalidTimeout), 45 /// # SerdeJson(serde_json::Error), 46 /// # MissingUserHandle, 47 /// # MissingCeremony, 48 /// # UnknownCredential, 49 /// # Credential(CredentialErr), 50 /// # AuthCeremony(AuthCeremonyErr), 51 /// # } 52 /// # impl From<AsciiDomainErr> for E { 53 /// # fn from(value: AsciiDomainErr) -> Self { 54 /// # Self::RpId(value) 55 /// # } 56 /// # } 57 /// # impl From<CollectedClientDataErr> for E { 58 /// # fn from(value: CollectedClientDataErr) -> Self { 59 /// # Self::CollectedClientData(value) 60 /// # } 61 /// # } 62 /// # impl From<InvalidTimeout> for E { 63 /// # fn from(value: InvalidTimeout) -> Self { 64 /// # Self::InvalidTimeout(value) 65 /// # } 66 /// # } 67 /// # impl From<serde_json::Error> for E { 68 /// # fn from(value: serde_json::Error) -> Self { 69 /// # Self::SerdeJson(value) 70 /// # } 71 /// # } 72 /// # impl From<CredentialErr> for E { 73 /// # fn from(value: CredentialErr) -> Self { 74 /// # Self::Credential(value) 75 /// # } 76 /// # } 77 /// # impl From<AuthCeremonyErr> for E { 78 /// # fn from(value: AuthCeremonyErr) -> Self { 79 /// # Self::AuthCeremony(value) 80 /// # } 81 /// # } 82 /// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap()); 83 /// let mut ceremonies = FixedCapHashSet::new(128); 84 /// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?; 85 /// assert!( 86 /// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) 87 /// ); 88 /// # #[cfg(feature = "serde")] 89 /// let authentication = serde_json::from_str::<DiscoverableAuthentication64>(get_authentication_json(client).as_str())?; 90 /// # #[cfg(feature = "serde")] 91 /// let user_handle = authentication.response().user_handle(); 92 /// # #[cfg(feature = "serde")] 93 /// let (static_state, dynamic_state) = get_credential(authentication.raw_id(), &user_handle).ok_or(E::UnknownCredential)?; 94 /// # #[cfg(all(feature = "custom", feature = "serde"))] 95 /// let mut cred = AuthenticatedCredential::new(authentication.raw_id(), &user_handle, static_state, dynamic_state)?; 96 /// # #[cfg(all(feature = "custom", feature = "serde"))] 97 /// if ceremonies.take(&authentication.challenge()?).ok_or(E::MissingCeremony)?.verify(RP_ID, &authentication, &mut cred, &AuthenticationVerificationOptions::<&str, &str>::default())? { 98 /// update_cred(authentication.raw_id(), cred.dynamic_state()); 99 /// } 100 /// /// Send `DiscoverableAuthenticationClientState` and receive `DiscoverableAuthentication64` JSON from client. 101 /// # #[cfg(feature = "serde")] 102 /// fn get_authentication_json(client: DiscoverableAuthenticationClientState<'_, '_, '_>) -> String { 103 /// // ⋮ 104 /// # let client_data_json = BASE64URL_NOPAD.encode(serde_json::json!({ 105 /// # "type": "webauthn.get", 106 /// # "challenge": client.options().public_key.challenge, 107 /// # "origin": format!("https://{}", client.options().public_key.rp_id.as_ref()), 108 /// # "crossOrigin": false 109 /// # }).to_string().as_bytes()); 110 /// # serde_json::json!({ 111 /// # "id": "AAAAAAAAAAAAAAAAAAAAAA", 112 /// # "rawId": "AAAAAAAAAAAAAAAAAAAAAA", 113 /// # "response": { 114 /// # "clientDataJSON": client_data_json, 115 /// # "authenticatorData": "", 116 /// # "signature": "", 117 /// # "userHandle": "AA" 118 /// # }, 119 /// # "clientExtensionResults": {}, 120 /// # "type": "public-key" 121 /// # }).to_string() 122 /// } 123 /// /// Gets the `AuthenticatedCredential` parts associated with `id` and `user_handle` from the database. 124 /// fn get_credential(id: CredentialId<&[u8]>, user_handle: &UserHandle64) -> Option<(StaticState<CompressedPubKeyOwned>, DynamicState)> { 125 /// // ⋮ 126 /// # Some((StaticState { credential_public_key: CompressedPubKeyOwned::Ed25519(Ed25519PubKey::from([0; 32])), extensions: AuthenticatorExtensionOutputStaticState { cred_protect: CredentialProtectionPolicy::UserVerificationRequired, hmac_secret: None, }, client_extension_results: ClientExtensionsOutputsStaticState { prf: None, }, }, DynamicState { user_verified: true, backup: Backup::NotEligible, sign_count: 1, authenticator_attachment: AuthenticatorAttachment::None })) 127 /// } 128 /// /// Updates the current `DynamicState` associated with `id` in the database to 129 /// /// `dyn_state`. 130 /// fn update_cred(id: CredentialId<&[u8]>, dyn_state: DynamicState) { 131 /// // ⋮ 132 /// } 133 /// # Ok::<_, E>(()) 134 /// ``` 135 pub mod auth; 136 /// Contains functionality to (de)serialize data to a data store. 137 #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] 138 #[cfg(feature = "bin")] 139 pub mod bin; 140 /// Contains constants useful for 141 /// [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). 142 mod cbor; 143 /// Contains functionality that needs to be accessible when `bin` or `serde` are not enabled. 144 #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] 145 #[cfg(feature = "custom")] 146 pub mod custom; 147 /// Contains error types. 148 pub mod error; 149 /// Contains functionality for completing the 150 /// [registration ceremony](https://www.w3.org/TR/webauthn-3/#registration-ceremony). 151 /// 152 /// # Examples 153 /// 154 /// ```no_run 155 /// # use core::convert; 156 /// # use data_encoding::BASE64URL_NOPAD; 157 /// # use webauthn_rp::{ 158 /// # hash::hash_set::FixedCapHashSet, 159 /// # request::{register::{error::CreationOptionsErr, CredentialCreationOptions, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, error::AsciiDomainErr, AsciiDomainStatic, PublicKeyCredentialDescriptor, RpId}, 160 /// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData}, 161 /// # RegisteredCredential 162 /// # }; 163 /// # #[derive(Debug)] 164 /// # enum E { 165 /// # CollectedClientData(CollectedClientDataErr), 166 /// # RpId(AsciiDomainErr), 167 /// # CreationOptions(CreationOptionsErr), 168 /// # SerdeJson(serde_json::Error), 169 /// # MissingCeremony, 170 /// # RegCeremony(RegCeremonyErr), 171 /// # } 172 /// # impl From<AsciiDomainErr> for E { 173 /// # fn from(value: AsciiDomainErr) -> Self { 174 /// # Self::RpId(value) 175 /// # } 176 /// # } 177 /// # impl From<CollectedClientDataErr> for E { 178 /// # fn from(value: CollectedClientDataErr) -> Self { 179 /// # Self::CollectedClientData(value) 180 /// # } 181 /// # } 182 /// # impl From<CreationOptionsErr> for E { 183 /// # fn from(value: CreationOptionsErr) -> Self { 184 /// # Self::CreationOptions(value) 185 /// # } 186 /// # } 187 /// # impl From<serde_json::Error> for E { 188 /// # fn from(value: serde_json::Error) -> Self { 189 /// # Self::SerdeJson(value) 190 /// # } 191 /// # } 192 /// # impl From<RegCeremonyErr> for E { 193 /// # fn from(value: RegCeremonyErr) -> Self { 194 /// # Self::RegCeremony(value) 195 /// # } 196 /// # } 197 /// const RP_ID: &RpId = &RpId::StaticDomain(AsciiDomainStatic::new("example.com").unwrap()); 198 /// # #[cfg(feature = "custom")] 199 /// let mut ceremonies = FixedCapHashSet::new(128); 200 /// # #[cfg(feature = "custom")] 201 /// let user_handle = get_user_handle(); 202 /// # #[cfg(feature = "custom")] 203 /// let user = get_user_entity(&user_handle); 204 /// # #[cfg(feature = "custom")] 205 /// let creds = get_registered_credentials(user_handle); 206 /// # #[cfg(feature = "custom")] 207 /// let (server, client) = CredentialCreationOptions::passkey(RP_ID, user, creds).start_ceremony()?; 208 /// # #[cfg(feature = "custom")] 209 /// assert!( 210 /// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) 211 /// ); 212 /// # #[cfg(all(feature = "serde_relaxed", feature = "custom"))] 213 /// let registration = serde_json::from_str::<Registration>(get_registration_json(client).as_str())?; 214 /// let ver_opts = RegistrationVerificationOptions::<&str, &str>::default(); 215 /// # #[cfg(all(feature = "custom", feature = "serde_relaxed"))] 216 /// insert_cred(ceremonies.take(®istration.challenge()?).ok_or(E::MissingCeremony)?.verify(RP_ID, ®istration, &ver_opts)?); 217 /// /// Extract `UserHandle` from session cookie if this is not the first credential registered. 218 /// # #[cfg(feature = "custom")] 219 /// fn get_user_handle() -> UserHandle64 { 220 /// // ⋮ 221 /// # [0; USER_HANDLE_MAX_LEN].into() 222 /// } 223 /// /// Fetch `PublicKeyCredentialUserEntity` info associated with `user`. 224 /// /// 225 /// /// If this is the first time a credential is being registered, then `PublicKeyCredentialUserEntity` 226 /// /// will need to be constructed with `name` and `display_name` passed from the client and `UserHandle::new` 227 /// /// used for `id`. Once created, this info can be stored such that the entity information 228 /// /// does not need to be requested for subsequent registrations. 229 /// # #[cfg(feature = "custom")] 230 /// fn get_user_entity(user: &UserHandle<USER_HANDLE_MAX_LEN>) -> PublicKeyCredentialUserEntity<'_, '_, '_, USER_HANDLE_MAX_LEN> { 231 /// // ⋮ 232 /// # PublicKeyCredentialUserEntity { 233 /// # name: "foo".try_into().unwrap(), 234 /// # id: user, 235 /// # display_name: None, 236 /// # } 237 /// } 238 /// /// Send `RegistrationClientState` and receive `Registration` JSON from client. 239 /// # #[cfg(feature = "serde")] 240 /// fn get_registration_json(client: RegistrationClientState<'_, '_, '_, '_, '_, '_, USER_HANDLE_MAX_LEN>) -> String { 241 /// // ⋮ 242 /// # let client_data_json = BASE64URL_NOPAD.encode(serde_json::json!({ 243 /// # "type": "webauthn.create", 244 /// # "challenge": client.options().public_key.challenge, 245 /// # "origin": format!("https://{}", client.options().public_key.rp_id.as_ref()), 246 /// # "crossOrigin": false 247 /// # }).to_string().as_bytes()); 248 /// # serde_json::json!({ 249 /// # "response": { 250 /// # "clientDataJSON": client_data_json, 251 /// # "attestationObject": "" 252 /// # } 253 /// # }).to_string() 254 /// } 255 /// /// Fetch the `PublicKeyCredentialDescriptor`s associated with `user`. 256 /// /// 257 /// /// This doesn't need to be called when this is the first credential registered for `user`; instead 258 /// /// an empty `Vec` should be passed. 259 /// fn get_registered_credentials( 260 /// user: UserHandle<USER_HANDLE_MAX_LEN>, 261 /// ) -> Vec<PublicKeyCredentialDescriptor<Vec<u8>>> { 262 /// // ⋮ 263 /// # Vec::new() 264 /// } 265 /// /// Inserts `RegisteredCredential::into_parts` into the database. 266 /// fn insert_cred(cred: RegisteredCredential<'_, USER_HANDLE_MAX_LEN>) { 267 /// // ⋮ 268 /// } 269 /// # Ok::<_, E>(()) 270 /// ``` 271 pub mod register; 272 /// Contains functionality to (de)serialize data to/from a client. 273 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 274 #[cfg(feature = "serde")] 275 mod ser; 276 /// Contains functionality to deserialize data from a client in a "relaxed" way. 277 #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] 278 #[cfg(feature = "serde_relaxed")] 279 pub mod ser_relaxed; 280 /// [Backup eligibility](https://www.w3.org/TR/webauthn-3/#backup-eligibility) and 281 /// [backup state](https://www.w3.org/TR/webauthn-3/#backup-state). 282 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 283 pub enum Backup { 284 /// [BE and BS](https://www.w3.org/TR/webauthn-3/#authdata-flags) flags are `0`. 285 NotEligible, 286 /// [BE and BS](https://www.w3.org/TR/webauthn-3/#authdata-flags) flags are `1` and `0` respectively. 287 Eligible, 288 /// [BE and BS](https://www.w3.org/TR/webauthn-3/#authdata-flags) flags are `1`. 289 Exists, 290 } 291 impl PartialEq<&Self> for Backup { 292 #[inline] 293 fn eq(&self, other: &&Self) -> bool { 294 *self == **other 295 } 296 } 297 impl PartialEq<Backup> for &Backup { 298 #[inline] 299 fn eq(&self, other: &Backup) -> bool { 300 **self == *other 301 } 302 } 303 /// [`AuthenticatorTransport`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport). 304 #[derive(Clone, Copy, Debug)] 305 pub enum AuthenticatorTransport { 306 /// [`ble`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-ble). 307 Ble, 308 /// [`hybrid`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-hybrid). 309 Hybrid, 310 /// [`internal`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-internal). 311 Internal, 312 /// [`nfc`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-nfc). 313 Nfc, 314 /// [`smart-card`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-smart-card). 315 SmartCard, 316 /// [`usb`](https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-usb). 317 Usb, 318 } 319 impl AuthenticatorTransport { 320 /// Returns the encoded [`u8`] that `self` represents. 321 const fn to_u8(self) -> u8 { 322 match self { 323 Self::Ble => 0x1, 324 Self::Hybrid => 0x2, 325 Self::Internal => 0x4, 326 Self::Nfc => 0x8, 327 Self::SmartCard => 0x10, 328 Self::Usb => 0x20, 329 } 330 } 331 } 332 /// Set of [`AuthenticatorTransport`]s. 333 #[derive(Clone, Copy, Debug)] 334 pub struct AuthTransports(u8); 335 impl AuthTransports { 336 /// An empty `AuthTransports`. 337 #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] 338 #[cfg(feature = "custom")] 339 pub const NONE: Self = Self::new(); 340 /// An `AuthTransports` containing all possible [`AuthenticatorTransport`]s. 341 #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] 342 #[cfg(feature = "custom")] 343 pub const ALL: Self = Self::all(); 344 /// Construct an empty `AuthTransports`. 345 #[cfg(any(feature = "bin", feature = "custom", feature = "serde"))] 346 pub(super) const fn new() -> Self { 347 Self(0) 348 } 349 #[cfg(any(feature = "bin", feature = "custom"))] 350 /// Construct an `AuthTransports` containing all `AuthenticatorTransport`s. 351 const fn all() -> Self { 352 Self::new() 353 .add_transport(AuthenticatorTransport::Ble) 354 .add_transport(AuthenticatorTransport::Hybrid) 355 .add_transport(AuthenticatorTransport::Internal) 356 .add_transport(AuthenticatorTransport::Nfc) 357 .add_transport(AuthenticatorTransport::SmartCard) 358 .add_transport(AuthenticatorTransport::Usb) 359 } 360 /// Returns the number of [`AuthenticatorTransport`]s in `self`. 361 /// 362 /// # Examples 363 /// 364 /// ``` 365 /// # use webauthn_rp::response::AuthTransports; 366 /// # #[cfg(feature = "custom")] 367 /// assert_eq!(AuthTransports::ALL.count(), 6); 368 /// ``` 369 #[inline] 370 #[must_use] 371 pub const fn count(self) -> u32 { 372 self.0.count_ones() 373 } 374 /// Returns `true` iff there are no [`AuthenticatorTransport`]s in `self`. 375 /// 376 /// # Examples 377 /// 378 /// ``` 379 /// # use webauthn_rp::response::AuthTransports; 380 /// # #[cfg(feature = "custom")] 381 /// assert!(AuthTransports::NONE.is_empty()); 382 /// ``` 383 #[inline] 384 #[must_use] 385 pub const fn is_empty(self) -> bool { 386 self.0 == 0 387 } 388 /// Returns `true` iff `self` contains `transport`. 389 /// 390 /// # Examples 391 /// 392 /// ``` 393 /// # use webauthn_rp::response::{AuthTransports, AuthenticatorTransport}; 394 /// # #[cfg(feature = "custom")] 395 /// assert!(AuthTransports::ALL.contains(AuthenticatorTransport::Ble)); 396 /// ``` 397 #[inline] 398 #[must_use] 399 pub const fn contains(self, transport: AuthenticatorTransport) -> bool { 400 let val = transport.to_u8(); 401 self.0 & val == val 402 } 403 /// Returns a copy of `self` with `transport` added. 404 /// 405 /// `self` is returned iff `transport` already exists. 406 #[cfg(any(feature = "bin", feature = "custom", feature = "serde"))] 407 const fn add_transport(self, transport: AuthenticatorTransport) -> Self { 408 Self(self.0 | transport.to_u8()) 409 } 410 /// Returns a copy of `self` with `transport` added. 411 /// 412 /// `self` is returned iff `transport` already exists. 413 /// 414 /// # Examples 415 /// 416 /// ``` 417 /// # use webauthn_rp::response::{AuthTransports, AuthenticatorTransport}; 418 /// assert_eq!( 419 /// AuthTransports::NONE 420 /// .add(AuthenticatorTransport::Usb) 421 /// .count(), 422 /// 1 423 /// ); 424 /// assert_eq!( 425 /// AuthTransports::ALL.add(AuthenticatorTransport::Usb).count(), 426 /// 6 427 /// ); 428 /// ``` 429 #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] 430 #[cfg(feature = "custom")] 431 #[inline] 432 #[must_use] 433 pub const fn add(self, transport: AuthenticatorTransport) -> Self { 434 self.add_transport(transport) 435 } 436 /// Returns a copy of `self` with `transport` removed. 437 /// 438 /// `self` is returned iff `transport` did not exist. 439 /// 440 /// # Examples 441 /// 442 /// ``` 443 /// # use webauthn_rp::response::{AuthTransports, AuthenticatorTransport}; 444 /// assert_eq!( 445 /// AuthTransports::ALL 446 /// .remove(AuthenticatorTransport::Internal) 447 /// .count(), 448 /// 5 449 /// ); 450 /// assert_eq!( 451 /// AuthTransports::NONE.remove(AuthenticatorTransport::Usb).count(), 452 /// 0 453 /// ); 454 /// ``` 455 #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] 456 #[cfg(feature = "custom")] 457 #[inline] 458 #[must_use] 459 pub const fn remove(self, transport: AuthenticatorTransport) -> Self { 460 Self(self.0 & !transport.to_u8()) 461 } 462 } 463 /// [`AuthenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment). 464 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 465 pub enum AuthenticatorAttachment { 466 /// No attachment information. 467 None, 468 /// [`platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-platform). 469 Platform, 470 /// [`cross-platform`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattachment-cross-platform). 471 CrossPlatform, 472 } 473 impl PartialEq<&Self> for AuthenticatorAttachment { 474 #[inline] 475 fn eq(&self, other: &&Self) -> bool { 476 *self == **other 477 } 478 } 479 impl PartialEq<AuthenticatorAttachment> for &AuthenticatorAttachment { 480 #[inline] 481 fn eq(&self, other: &AuthenticatorAttachment) -> bool { 482 **self == *other 483 } 484 } 485 /// The maximum number of bytes that can make up a Credential ID 486 /// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#credential-id). 487 pub const CRED_ID_MAX_LEN: usize = 1023; 488 /// The minimum number of bytes that can make up a Credential ID 489 /// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#credential-id). 490 /// 491 /// The spec does not call out this value directly instead it states the following: 492 /// 493 /// > Credential IDs are generated by authenticators in two forms: 494 /// > 495 /// > * At least 16 bytes that include at least 100 bits of entropy, or 496 /// > * The [public key credential source](https://www.w3.org/TR/webauthn-3/#public-key-credential-source), 497 /// > without its Credential ID or mutable items, encrypted so only its managing 498 /// > authenticator can decrypt it. This form allows the authenticator to be nearly 499 /// > stateless, by having the Relying Party store any necessary state. 500 /// 501 /// One of the immutable items of the public key credential source is the private key 502 /// which for any real-world signature algorithm will always be at least 16 bytes. 503 pub const CRED_ID_MIN_LEN: usize = 16; 504 /// A [Credential ID](https://www.w3.org/TR/webauthn-3/#credential-id) that is made up of 505 /// [`CRED_ID_MIN_LEN`]–[`CRED_ID_MAX_LEN`] bytes. 506 #[derive(Clone, Copy, Debug)] 507 pub struct CredentialId<T>(T); 508 impl<T> CredentialId<T> { 509 /// Returns the contained data consuming `self`. 510 #[inline] 511 pub fn into_inner(self) -> T { 512 self.0 513 } 514 /// Returns the contained data. 515 #[inline] 516 pub const fn inner(&self) -> &T { 517 &self.0 518 } 519 } 520 impl<'a> CredentialId<&'a [u8]> { 521 /// Creates a `CredentialId` from a `slice`. 522 #[expect(single_use_lifetimes, reason = "false positive")] 523 fn from_slice<'b: 'a>(value: &'b [u8]) -> Result<Self, CredentialIdErr> { 524 if (CRED_ID_MIN_LEN..=CRED_ID_MAX_LEN).contains(&value.len()) { 525 Ok(Self(value)) 526 } else { 527 Err(CredentialIdErr) 528 } 529 } 530 } 531 impl<T: AsRef<[u8]>> AsRef<[u8]> for CredentialId<T> { 532 #[inline] 533 fn as_ref(&self) -> &[u8] { 534 self.0.as_ref() 535 } 536 } 537 impl<T: Borrow<[u8]>> Borrow<[u8]> for CredentialId<T> { 538 #[inline] 539 fn borrow(&self) -> &[u8] { 540 self.0.borrow() 541 } 542 } 543 impl<'a: 'b, 'b> From<&'a CredentialId<Vec<u8>>> for CredentialId<&'b Vec<u8>> { 544 #[inline] 545 fn from(value: &'a CredentialId<Vec<u8>>) -> Self { 546 Self(&value.0) 547 } 548 } 549 impl<'a: 'b, 'b> From<CredentialId<&'a Vec<u8>>> for CredentialId<&'b [u8]> { 550 #[inline] 551 fn from(value: CredentialId<&'a Vec<u8>>) -> Self { 552 Self(value.0.as_slice()) 553 } 554 } 555 impl<'a: 'b, 'b> From<&'a CredentialId<Vec<u8>>> for CredentialId<&'b [u8]> { 556 #[inline] 557 fn from(value: &'a CredentialId<Vec<u8>>) -> Self { 558 Self(value.0.as_slice()) 559 } 560 } 561 impl From<CredentialId<&[u8]>> for CredentialId<Vec<u8>> { 562 #[inline] 563 fn from(value: CredentialId<&[u8]>) -> Self { 564 Self(value.0.to_owned()) 565 } 566 } 567 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CredentialId<T>> for CredentialId<T2> { 568 #[inline] 569 fn eq(&self, other: &CredentialId<T>) -> bool { 570 self.0 == other.0 571 } 572 } 573 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CredentialId<T>> for &CredentialId<T2> { 574 #[inline] 575 fn eq(&self, other: &CredentialId<T>) -> bool { 576 **self == *other 577 } 578 } 579 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&CredentialId<T>> for CredentialId<T2> { 580 #[inline] 581 fn eq(&self, other: &&CredentialId<T>) -> bool { 582 *self == **other 583 } 584 } 585 impl<T: Eq> Eq for CredentialId<T> {} 586 impl<T: Hash> Hash for CredentialId<T> { 587 #[inline] 588 fn hash<H: Hasher>(&self, state: &mut H) { 589 self.0.hash(state); 590 } 591 } 592 impl<T: PartialOrd<T2>, T2: PartialOrd<T>> PartialOrd<CredentialId<T>> for CredentialId<T2> { 593 #[inline] 594 fn partial_cmp(&self, other: &CredentialId<T>) -> Option<Ordering> { 595 self.0.partial_cmp(&other.0) 596 } 597 } 598 impl<T: Ord> Ord for CredentialId<T> { 599 #[inline] 600 fn cmp(&self, other: &Self) -> Ordering { 601 self.0.cmp(&other.0) 602 } 603 } 604 // We define a separate type to ensure challenges sent to the client are always randomly generated; 605 // otherwise one could deserialize arbitrary data into a `Challenge`. 606 /// Copy of [`Challenge`] sent back from the client. 607 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 608 pub struct SentChallenge(pub u128); 609 impl SentChallenge { 610 /// Transforms `value` into a `SentChallenge` by interpreting `value` as a 611 /// little-endian `u128`. 612 #[expect(clippy::little_endian_bytes, reason = "Challenge and SentChallenge need to be compatible, and we need to ensure the data is sent and received in the same order")] 613 #[inline] 614 #[must_use] 615 pub const fn from_array(value: [u8; 16]) -> Self { 616 Self(u128::from_le_bytes(value)) 617 } 618 /// Transforms `value` into a `SentChallenge`. 619 #[inline] 620 #[must_use] 621 pub const fn from_challenge(value: Challenge) -> Self { 622 Self(value.into_data()) 623 } 624 } 625 impl From<Challenge> for SentChallenge { 626 #[inline] 627 fn from(value: Challenge) -> Self { 628 Self::from_challenge(value) 629 } 630 } 631 impl From<[u8; 16]> for SentChallenge { 632 #[inline] 633 fn from(value: [u8; 16]) -> Self { 634 Self::from_array(value) 635 } 636 } 637 impl PartialEq<&Self> for SentChallenge { 638 #[inline] 639 fn eq(&self, other: &&Self) -> bool { 640 *self == **other 641 } 642 } 643 impl PartialEq<SentChallenge> for &SentChallenge { 644 #[inline] 645 fn eq(&self, other: &SentChallenge) -> bool { 646 **self == *other 647 } 648 } 649 impl PartialOrd for SentChallenge { 650 #[inline] 651 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 652 Some(self.cmp(other)) 653 } 654 } 655 impl Ord for SentChallenge { 656 #[inline] 657 fn cmp(&self, other: &Self) -> Ordering { 658 self.0.cmp(&other.0) 659 } 660 } 661 impl Hash for SentChallenge { 662 #[inline] 663 fn hash<H: Hasher>(&self, state: &mut H) { 664 state.write_u128(self.0); 665 } 666 } 667 /// An [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin) or 668 /// [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin). 669 #[derive(Debug, Eq)] 670 pub struct Origin<'a>(pub Cow<'a, str>); 671 impl PartialEq<Origin<'_>> for Origin<'_> { 672 #[inline] 673 fn eq(&self, other: &Origin<'_>) -> bool { 674 self.0 == other.0 675 } 676 } 677 impl PartialEq<&Origin<'_>> for Origin<'_> { 678 #[inline] 679 fn eq(&self, other: &&Origin<'_>) -> bool { 680 *self == **other 681 } 682 } 683 impl PartialEq<Origin<'_>> for &Origin<'_> { 684 #[inline] 685 fn eq(&self, other: &Origin<'_>) -> bool { 686 **self == *other 687 } 688 } 689 impl PartialEq<str> for Origin<'_> { 690 #[inline] 691 fn eq(&self, other: &str) -> bool { 692 self.0.as_ref() == other 693 } 694 } 695 impl PartialEq<Origin<'_>> for str { 696 #[inline] 697 fn eq(&self, other: &Origin<'_>) -> bool { 698 *other == *self 699 } 700 } 701 impl PartialEq<&str> for Origin<'_> { 702 #[inline] 703 fn eq(&self, other: &&str) -> bool { 704 *self == **other 705 } 706 } 707 impl PartialEq<Origin<'_>> for &str { 708 #[inline] 709 fn eq(&self, other: &Origin<'_>) -> bool { 710 **self == *other 711 } 712 } 713 impl PartialEq<String> for Origin<'_> { 714 #[inline] 715 fn eq(&self, other: &String) -> bool { 716 self.0 == *other 717 } 718 } 719 impl PartialEq<Origin<'_>> for String { 720 #[inline] 721 fn eq(&self, other: &Origin<'_>) -> bool { 722 *other == *self 723 } 724 } 725 impl PartialEq<Url> for Origin<'_> { 726 #[inline] 727 fn eq(&self, other: &Url) -> bool { 728 self.0.as_ref() == other.as_ref() 729 } 730 } 731 impl PartialEq<Origin<'_>> for Url { 732 #[inline] 733 fn eq(&self, other: &Origin<'_>) -> bool { 734 *other == *self 735 } 736 } 737 impl PartialEq<&Url> for Origin<'_> { 738 #[inline] 739 fn eq(&self, other: &&Url) -> bool { 740 *self == **other 741 } 742 } 743 impl PartialEq<Origin<'_>> for &Url { 744 #[inline] 745 fn eq(&self, other: &Origin<'_>) -> bool { 746 **self == *other 747 } 748 } 749 /// [Authenticator data flags](https://www.w3.org/TR/webauthn-3/#authdata-flags). 750 #[derive(Clone, Copy, Debug)] 751 pub struct Flag { 752 /// [`UP` flag](https://www.w3.org/TR/webauthn-3/#authdata-flags-up). 753 /// 754 /// Note this is always `true` when part of [`auth::AuthenticatorData::flags`]. 755 pub user_present: bool, 756 /// [`UV` flag](https://www.w3.org/TR/webauthn-3/#concept-user-verified). 757 pub user_verified: bool, 758 /// [`BE`](https://www.w3.org/TR/webauthn-3/#backup-eligibility) and 759 /// [`BS`](https://www.w3.org/TR/webauthn-3/#backup-state) flags. 760 pub backup: Backup, 761 } 762 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). 763 pub(super) trait AuthData<'a>: Sized { 764 /// Error returned by [`Self::user_is_not_present`]. 765 /// 766 /// This should be [`Infallible`] in the event user must not always be present. 767 type UpBitErr; 768 /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata). 769 type CredData; 770 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions). 771 type Ext: AuthExtOutput + Copy; 772 /// Errors iff the user must always be present. 773 fn user_is_not_present() -> Result<(), Self::UpBitErr>; 774 /// `true` iff `AT` bit (i.e., bit 6) in [`Self::flag_data`] can and must be set to 1. 775 fn contains_at_bit() -> bool; 776 /// Constructor. 777 fn new(rp_id_hash: &'a [u8], flags: Flag, sign_count: u32, attested_credential_data: Self::CredData, extensions: Self::Ext) -> Self; 778 /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash). 779 fn rp_hash(&self) -> &'a [u8]; 780 /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags). 781 fn flag(&self) -> Flag; 782 } 783 /// [`CollectedClientData`](https://www.w3.org/TR/webauthn-3/#dictdef-collectedclientdata). 784 #[derive(Debug)] 785 pub struct CollectedClientData<'a> { 786 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge). 787 pub challenge: SentChallenge, 788 /// [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin). 789 pub origin: Origin<'a>, 790 /// [`crossOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-crossorigin). 791 pub cross_origin: bool, 792 /// [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin). 793 /// 794 /// When `CollectedClientData` is constructed via [`Self::from_client_data_json`], this can only be 795 /// `Some` if [`Self::cross_origin`]; and if `Some`, it will be different than [`Self::origin`]. 796 pub top_origin: Option<Origin<'a>>, 797 } 798 impl<'a> CollectedClientData<'a> { 799 /// Parses `json` based on the 800 /// [limited verification algorithm](https://www.w3.org/TR/webauthn-3/#clientdatajson-verification). 801 /// 802 /// Additionally, [`topOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin) is only 803 /// allowed to exist if it has a different value than 804 /// [`origin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-origin) and 805 /// [`crossOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-crossorigin) is `true`. 806 /// 807 /// `REGISTRATION` iff [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type) must be 808 /// `"webauthn.create"`; otherwise it must be `"webauthn.get"`. 809 /// 810 /// # Errors 811 /// 812 /// Errors iff `json` cannot be parsed based on the aforementioned requirements. 813 /// 814 /// # Examples 815 /// 816 /// ``` 817 /// # use webauthn_rp::response::{error::CollectedClientDataErr, CollectedClientData}; 818 /// assert!(!CollectedClientData::from_client_data_json::<true>(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice())?.cross_origin); 819 /// assert!(!CollectedClientData::from_client_data_json::<false>(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice())?.cross_origin); 820 /// # Ok::<_, CollectedClientDataErr>(()) 821 /// ``` 822 #[expect(single_use_lifetimes, reason = "false positive")] 823 #[inline] 824 pub fn from_client_data_json<'b: 'a, const REGISTRATION: bool>(json: &'b [u8]) -> Result<Self, CollectedClientDataErr> { 825 LimitedVerificationParser::<REGISTRATION>::parse(json) 826 } 827 /// Parses `json` in a "relaxed" way. 828 /// 829 /// Unlike [`Self::from_client_data_json`] which requires `json` to be an output from the 830 /// [JSON-compatible serialization of client data](https://www.w3.org/TR/webauthn-3/#clientdatajson-serialization), 831 /// this parses `json` based entirely on the 832 /// [`CollectedClientData`](https://www.w3.org/TR/webauthn-3/#dictdef-collectedclientdata) Web IDL `dictionary`. 833 /// 834 /// L1 clients predate the JSON-compatible serialization of client data; additionally there are L2 and L3 835 /// clients that don't adhere to the JSON-compatible serialization of client data despite being required to. 836 /// These clients serialize `CollectedClientData` so that it's valid JSON and conforms to the Web IDL `dictionary` 837 /// and nothing more. Furthermore, when not relying on the 838 /// [limited verification algorithm](https://www.w3.org/TR/webauthn-3/#clientdatajson-verification), the spec 839 /// requires the data to be decoded in a way equivalent to 840 /// [UTF-8 decode](https://encoding.spec.whatwg.org/#utf-8-decode) which both interprets a leading zero 841 /// width no-breaking space (i.e., U+FEFF) as a byte-order mark (BOM) as well as replaces any sequences of 842 /// invalid UTF-8 code units with the replacement character (i.e., U+FFFD). That is precisely what this 843 /// function does. 844 /// 845 /// # Errors 846 /// 847 /// Errors iff any of the following is true: 848 /// * The payload is not valid JSON _after_ ignoring a leading U+FEFF and replacing any sequences of invalid 849 /// UTF-8 code units with U+FFFD. 850 /// * The JSON does not conform to the Web IDL `dictionary`. 851 /// * [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type) is not `"webauthn.create"` 852 /// or `"webauthn.get"` when `REGISTRATION` and `!REGISTRATION` respectively. 853 /// * [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) is not a 854 /// base64url-encoded [`Challenge`]. 855 /// * Existence of duplicate keys for the keys that are expected. 856 /// 857 /// # Examples 858 /// 859 /// ``` 860 /// # use webauthn_rp::response::{ser_relaxed::SerdeJsonErr, CollectedClientData}; 861 /// assert!(!CollectedClientData::from_client_data_json_relaxed::<true>(b"\xef\xbb\xbf{ 862 /// \"type\": \"webauthn.create\", 863 /// \"origin\": \"https://example.com\", 864 /// \"f\xffo\": 123, 865 /// \"topOrigin\": \"https://example.com\", 866 /// \"challenge\": \"AAAAAAAAAAAAAAAAAAAAAA\" 867 /// }")?.cross_origin); 868 /// # Ok::<_, SerdeJsonErr>(()) 869 /// ``` 870 #[expect(single_use_lifetimes, reason = "false positive")] 871 #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] 872 #[cfg(feature = "serde_relaxed")] 873 #[inline] 874 pub fn from_client_data_json_relaxed<'b: 'a, const REGISTRATION: bool>(json: &'b [u8]) -> Result<Self, SerdeJsonErr> { 875 ser_relaxed::RelaxedClientDataJsonParser::<REGISTRATION>::parse(json) 876 } 877 } 878 /// Parser of 879 /// [`JSON-compatible serialization of client data`](https://www.w3.org/TR/webauthn-3/#collectedclientdata-json-compatible-serialization-of-client-data). 880 trait ClientDataJsonParser { 881 /// Error returned by [`Self::parse`]. 882 type Err; 883 /// Parses `json` into `CollectedClientData` based on the value of 884 /// [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type). 885 /// 886 /// # Errors 887 /// 888 /// Errors iff `json` cannot be parsed into a `CollectedClientData`. 889 fn parse(json: &[u8]) -> Result<CollectedClientData<'_>, Self::Err>; 890 /// Extracts [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) 891 /// from `json`. 892 /// 893 /// Note `json` should be minimally parsed such that only `challenge` is extracted; thus 894 /// `Ok` being returned does _not_ mean `json` is in fact valid. 895 fn get_sent_challenge(json: &[u8]) -> Result<SentChallenge, Self::Err>; 896 } 897 /// [`ClientDataJsonParser`] based on the 898 /// [limited verification algorithm](https://www.w3.org/TR/webauthn-3/#clientdatajson-verification) 899 /// with the following additional requirements: 900 /// * Unknown keys are not allowed. 901 /// * The entire payload is parsed; thus the payload is guaranteed to be valid UTF-8 and JSON. 902 /// * [`CollectedClientData::top_origin`] can only be `Some` if 903 /// [`crossOrigin`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-crossorigin). 904 /// * If `CollectedClientData::top_origin` is `Some`, then it does not equal [`CollectedClientData::origin`]. 905 /// 906 /// `REGISTRATION` iff [`ClientDataJsonParser::parse`] requires 907 /// [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type) to be `"webauthn.create"`; 908 /// otherwise it must be `"webauthn.get"`. 909 struct LimitedVerificationParser<const REGISTRATION: bool>; 910 impl<const R: bool> LimitedVerificationParser<R> { 911 /// Parses `val` as a JSON string with possibly trailing data. `val` MUST NOT begin with an opening quote. Upon 912 /// encountering the first non-escaped quote, the parsed value is returned in addition to the remaining 913 /// portion of `val` _after_ the closing quote. The limited verification algorithm is adhered to; thus the 914 /// _only_ Unicode scalar values that are allowed (and must) be hex-escaped are U+0000 to U+001F inclusively. 915 /// Similarly only `b'\\'` and `b'"'` are allowed (and must) be escaped with `b'\\'`. 916 #[expect(unsafe_code, reason = "comment justifies its correctness")] 917 #[expect(clippy::arithmetic_side_effects, clippy::indexing_slicing, reason = "comments justify their correctness")] 918 fn parse_string(val: &[u8]) -> Result<(Cow<'_, str>, &'_ [u8]), CollectedClientDataErr> { 919 /// Tracks the state of the current Unicode scalar value that is being parsed. 920 enum State { 921 /// We are not parsing `'"'`, `'\\'`, or U+0000 to U+001F. 922 Normal, 923 /// We just encountered the escape character. 924 Escape, 925 /// We just encountered `b"\\u"`. 926 UnicodeEscape, 927 /// We just encountered `b"\\u0"`. 928 UnicodeHex1, 929 /// We just encountered `b"\\u00"`. 930 UnicodeHex2, 931 /// We just encountered `b"\\u000"` or `b"\\u001"`. The contained `u8` is `0` iff the former; otherwise 932 /// `0x10`. 933 UnicodeHex3(u8), 934 } 935 // We parse this as UTF-8 only at the end iff it is not empty. This contains all the potential Unicode scalar 936 // values after de-escaping. 937 let mut utf8 = Vec::new(); 938 // We check for all `u8`s already; thus we might as well check if we encounter a non-ASCII `u8`. 939 // If we don't, then we can rely on `str::from_utf8_unchecked`. 940 let mut all_ascii = true; 941 // This tracks the start index of the next slice to add. We add slices iff we encounter the escape character or 942 // we return the parsed `Cow` (i.e., encounter an unescaped `b'"'`). 943 let mut cur_idx = 0; 944 // The state of the yet-to-be-parsed Unicode scalar value. 945 let mut state = State::Normal; 946 for (counter, &b) in val.iter().enumerate() { 947 match state { 948 State::Normal => { 949 match b { 950 b'"' => { 951 if utf8.is_empty() { 952 if all_ascii { 953 // `cur_idx` is 0 or 1. The latter is true iff `val` starts with a 954 // `b'\\'` or `b'"'` but contains no other escaped characters. 955 let s = &val[cur_idx..counter]; 956 // SAFETY: 957 // `all_ascii` is `false` iff we encountered any `u8` that was not 958 // an ASCII `u8`; thus we know `s` is valid ASCII which in turn means 959 // it's valid UTF-8. 960 let v = unsafe { str::from_utf8_unchecked(s) }; 961 // `val.len() > counter`, so indexing is fine and overflow cannot happen. 962 return Ok((Cow::Borrowed(v), &val[counter + 1..])); 963 } 964 // `cur_idx` is 0 or 1. The latter is true iff `val` starts with a 965 // `b'\\'` or `b'"'` but contains no other escaped characters. 966 return str::from_utf8(&val[cur_idx..counter]) 967 .map_err(CollectedClientDataErr::Utf8) 968 // `val.len() > counter`, so indexing is fine and overflow cannot happen. 969 .map(|v| (Cow::Borrowed(v), &val[counter + 1..])); 970 } 971 // `val.len() > counter && counter >= cur_idx`, so indexing is fine and overflow 972 // cannot happen. 973 utf8.extend_from_slice(&val[cur_idx..counter]); 974 if all_ascii { 975 // SAFETY: 976 // `all_ascii` is `false` iff we encountered any `u8` that was not 977 // an ASCII `u8`; thus we know `utf8` is valid ASCII which in turn means 978 // it's valid UTF-8. 979 let v = unsafe { String::from_utf8_unchecked(utf8) }; 980 // `val.len() > counter`, so indexing is fine and overflow cannot happen. 981 return Ok((Cow::Owned(v), &val[counter + 1..])); 982 } 983 return String::from_utf8(utf8) 984 .map_err(CollectedClientDataErr::Utf8Owned) 985 // `val.len() > counter`, so indexing is fine and overflow cannot happen. 986 .map(|v| (Cow::Owned(v), &val[counter + 1..])); 987 } 988 b'\\' => { 989 // Write the current slice of data. 990 utf8.extend_from_slice(&val[cur_idx..counter]); 991 state = State::Escape; 992 } 993 // ASCII is a subset of UTF-8 and this is a subset of ASCII. The code unit that is used for an 994 // ASCII Unicode scalar value _never_ appears in multi-code-unit Unicode scalar values; thus we 995 // error immediately. 996 ..=0x1f => return Err(CollectedClientDataErr::InvalidEscapedString), 997 128.. => all_ascii = false, 998 _ => (), 999 } 1000 } 1001 State::Escape => { 1002 match b { 1003 b'"' | b'\\' => { 1004 // We start the next slice here since we need to add it. 1005 cur_idx = counter; 1006 state = State::Normal; 1007 } 1008 b'u' => { 1009 state = State::UnicodeEscape; 1010 } 1011 _ => { 1012 return Err(CollectedClientDataErr::InvalidEscapedString); 1013 } 1014 } 1015 } 1016 State::UnicodeEscape => { 1017 if b != b'0' { 1018 return Err(CollectedClientDataErr::InvalidEscapedString); 1019 } 1020 state = State::UnicodeHex1; 1021 } 1022 State::UnicodeHex1 => { 1023 if b != b'0' { 1024 return Err(CollectedClientDataErr::InvalidEscapedString); 1025 } 1026 state = State::UnicodeHex2; 1027 } 1028 State::UnicodeHex2 => { 1029 state = State::UnicodeHex3(match b { 1030 b'0' => 0, 1031 b'1' => 0x10, 1032 _ => return Err(CollectedClientDataErr::InvalidEscapedString), 1033 }); 1034 } 1035 State::UnicodeHex3(v) => { 1036 match b { 1037 // Only and all _lowercase_ hex is allowed. 1038 b'0'..=b'9' | b'a'..=b'f' => { 1039 // When `b < b'a'`, then `b >= b'0'`; and `b'a' > 87`; thus underflow cannot happen. 1040 // Note `b'a' - 10 == 87`. 1041 utf8.push(v | (b - if b < b'a' { b'0' } else { 87 })); 1042 // `counter < val.len()`, so overflow cannot happen. 1043 cur_idx = counter + 1; 1044 state = State::Normal; 1045 } 1046 _ => return Err(CollectedClientDataErr::InvalidEscapedString), 1047 } 1048 } 1049 } 1050 } 1051 // We never encountered an unescaped `b'"'`; thus we could not parse a string. 1052 Err(CollectedClientDataErr::InvalidObject) 1053 } 1054 } 1055 impl<const R: bool> ClientDataJsonParser for LimitedVerificationParser<R> { 1056 type Err = CollectedClientDataErr; 1057 #[expect(clippy::panic_in_result_fn, reason = "want to crash when there is a bug")] 1058 #[expect(clippy::little_endian_bytes, reason = "Challenge::serialize and this need to be consistent across architectures")] 1059 #[expect(clippy::too_many_lines, reason = "110 lines is fine")] 1060 fn parse(json: &[u8]) -> Result<CollectedClientData<'_>, Self::Err> { 1061 // `{"type":"webauthn.<create|get>","challenge":"<22 bytes>","origin":"<bytes>","crossOrigin":<true|false>[,"topOrigin":"<bytes>"][,<anything>]}`. 1062 /// First portion of `value`. 1063 const HEADER: &[u8; 18] = br#"{"type":"webauthn."#; 1064 /// `get`. 1065 const GET: &[u8; 3] = b"get"; 1066 /// `create`. 1067 const CREATE: &[u8; 6] = b"create"; 1068 /// Value after type before the start of the base64url-encoded challenge. 1069 const AFTER_TYPE: &[u8; 15] = br#"","challenge":""#; 1070 /// Value after challenge before the start of the origin value. 1071 const AFTER_CHALLENGE: &[u8; 12] = br#"","origin":""#; 1072 /// Value after origin before the start of the crossOrigin value. 1073 const AFTER_ORIGIN: &[u8; 15] = br#","crossOrigin":"#; 1074 /// `true`. 1075 const TRUE: &[u8; 4] = b"true"; 1076 /// `false`. 1077 const FALSE: &[u8; 5] = b"false"; 1078 /// Value after crossOrigin before the start of the topOrigin value. 1079 const AFTER_CROSS: &[u8; 13] = br#""topOrigin":""#; 1080 json.split_last().ok_or(CollectedClientDataErr::Len).and_then(|(last, last_rem)| { 1081 if *last == b'}' { 1082 last_rem.split_at_checked(HEADER.len()).ok_or(CollectedClientDataErr::Len).and_then(|(header, header_rem)| { 1083 if header == HEADER { 1084 if R { 1085 header_rem.split_at_checked(CREATE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(create, create_rem)| { 1086 if create == CREATE { 1087 Ok(create_rem) 1088 } else { 1089 Err(CollectedClientDataErr::Type) 1090 } 1091 }) 1092 } else { 1093 header_rem.split_at_checked(GET.len()).ok_or(CollectedClientDataErr::Len).and_then(|(get, get_rem)| { 1094 if get == GET { 1095 Ok(get_rem) 1096 } else { 1097 Err(CollectedClientDataErr::Type) 1098 } 1099 }) 1100 }.and_then(|type_rem| { 1101 type_rem.split_at_checked(AFTER_TYPE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(chall_key, chall_key_rem)| { 1102 if chall_key == AFTER_TYPE { 1103 chall_key_rem.split_at_checked(Challenge::BASE64_LEN).ok_or(CollectedClientDataErr::Len).and_then(|(base64_chall, base64_chall_rem)| { 1104 let mut chall = [0; 16]; 1105 BASE64URL_NOPAD.decode_mut(base64_chall, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).and_then(|chall_len| { 1106 assert_eq!(chall_len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut"); 1107 base64_chall_rem.split_at_checked(AFTER_CHALLENGE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(origin_key, origin_key_rem)| { 1108 if origin_key == AFTER_CHALLENGE { 1109 Self::parse_string(origin_key_rem).and_then(|(origin, origin_rem)| { 1110 origin_rem.split_at_checked(AFTER_ORIGIN.len()).ok_or(CollectedClientDataErr::Len).and_then(|(cross_key, cross_key_rem)| { 1111 if cross_key == AFTER_ORIGIN { 1112 // `FALSE.len() > TRUE.len()`, so we check for `FALSE` in `and_then`. 1113 cross_key_rem.split_at_checked(TRUE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(cross_true, cross_true_rem)| { 1114 if cross_true == TRUE { 1115 Ok((true, cross_true_rem)) 1116 } else { 1117 cross_key_rem.split_at_checked(FALSE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(cross_false, cross_false_rem)| { 1118 if cross_false == FALSE { 1119 Ok((false, cross_false_rem)) 1120 } else { 1121 Err(CollectedClientDataErr::CrossOrigin) 1122 } 1123 }) 1124 }.and_then(|(cross, cross_rem)| { 1125 cross_rem.split_first().map_or(Ok((cross, None)), |(comma, comma_rem)| { 1126 if *comma == b',' { 1127 comma_rem.split_at_checked(AFTER_CROSS.len()).map_or(Ok((cross, None)), |(top, top_rem)| { 1128 if top == AFTER_CROSS { 1129 if cross { 1130 Self::parse_string(top_rem).and_then(|(top_origin, top_origin_rem)| { 1131 top_origin_rem.first().map_or(Ok(()), |v| { 1132 if *v == b',' { 1133 Ok(()) 1134 } else { 1135 Err(CollectedClientDataErr::InvalidObject) 1136 } 1137 }).and_then(|()| { 1138 if origin == top_origin { 1139 Err(CollectedClientDataErr::TopOriginSameAsOrigin) 1140 } else { 1141 Ok((true, Some(Origin(top_origin)))) 1142 } 1143 }) 1144 }) 1145 } else { 1146 Err(CollectedClientDataErr::TopOriginWithoutCrossOrigin) 1147 } 1148 } else { 1149 Ok((cross, None)) 1150 } 1151 }) 1152 } else { 1153 Err(CollectedClientDataErr::InvalidObject) 1154 } 1155 }).map(|(cross_origin, top_origin)| CollectedClientData { challenge: SentChallenge(u128::from_le_bytes(chall)), origin: Origin(origin), cross_origin, top_origin, }) 1156 }) 1157 }) 1158 } else { 1159 Err(CollectedClientDataErr::CrossOriginKey) 1160 } 1161 }) 1162 }) 1163 } else { 1164 Err(CollectedClientDataErr::OriginKey) 1165 } 1166 }) 1167 }) 1168 }) 1169 } else { 1170 Err(CollectedClientDataErr::ChallengeKey) 1171 } 1172 }) 1173 }) 1174 } else { 1175 Err(CollectedClientDataErr::InvalidStart) 1176 } 1177 }) 1178 } else { 1179 Err(CollectedClientDataErr::InvalidObject) 1180 } 1181 }) 1182 } 1183 #[expect(clippy::panic_in_result_fn, reason = "want to crash when there is a bug")] 1184 #[expect(clippy::arithmetic_side_effects, reason = "comment justifies correctness")] 1185 #[expect(clippy::little_endian_bytes, reason = "Challenge::serialize and this need to be consistent across architectures")] 1186 fn get_sent_challenge(json: &[u8]) -> Result<SentChallenge, Self::Err> { 1187 // Index 39. 1188 // `{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA"...`. 1189 // Index 36. 1190 // `{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA"...`. 1191 let idx = if R { 39 } else { 36 }; 1192 // This maxes at 39 + 22 = 61; thus overflow is not an issue. 1193 json.get(idx..idx + Challenge::BASE64_LEN).ok_or(CollectedClientDataErr::Len).and_then(|chall_slice| { 1194 let mut chall = [0; 16]; 1195 BASE64URL_NOPAD.decode_mut(chall_slice, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).map(|len| { 1196 assert_eq!(len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut"); 1197 SentChallenge(u128::from_le_bytes(chall)) 1198 }) 1199 }) 1200 } 1201 } 1202 /// Authenticator extension outputs; 1203 pub(super) trait AuthExtOutput { 1204 /// MUST return `true` iff there is no data. 1205 fn missing(self) -> bool; 1206 } 1207 /// Successful return type from [`FromCbor::from_cbor`]. 1208 struct CborSuccess<'a, T> { 1209 /// Value parsed from the slice. 1210 value: T, 1211 /// Remaining unprocessed data. 1212 remaining: &'a [u8], 1213 } 1214 /// Types that parse 1215 /// [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) 1216 /// data without necessarily consuming all the data. 1217 /// 1218 /// The purpose of this `trait` is to allow chains of types to progressively consume `cbor` by passing 1219 /// [`CborSuccess::remaining`] into the next `FromCbor` type. 1220 trait FromCbor<'a>: Sized { 1221 /// Error when conversion fails. 1222 type Err; 1223 /// Parses `cbor` into `Self`. 1224 /// 1225 /// # Errors 1226 /// 1227 /// Errors if `cbor` cannot be parsed into `Self:`. 1228 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err>; 1229 } 1230 /// Error returned from [`A::from_cbor`] `where A: AuthData`. 1231 enum AuthenticatorDataErr<UpErr, CredData, AuthExt> { 1232 /// The `slice` had an invalid length. 1233 Len, 1234 /// [UP](https://www.w3.org/TR/webauthn-3/#authdata-flags-at) bit was 0. 1235 UserNotPresent(UpErr), 1236 /// Bit 1 in [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags) is not 0. 1237 FlagsBit1Not0, 1238 /// Bit 5 in [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags) is not 0. 1239 FlagsBit5Not0, 1240 /// [AT](https://www.w3.org/TR/webauthn-3/#authdata-flags-at) bit was 0 during registration or was 1 1241 /// during authentication. 1242 AttestedCredentialData, 1243 /// [BE](https://www.w3.org/TR/webauthn-3/#authdata-flags-be) and 1244 /// [BS](https://www.w3.org/TR/webauthn-3/#authdata-flags-bs) bits were 0 and 1 respectively. 1245 BackupWithoutEligibility, 1246 /// Error returned from [`AttestedCredentialData::from_cbor`]. 1247 AttestedCredential(CredData), 1248 /// Error returned from [`register::AuthenticatorExtensionOutput::from_cbor`] and 1249 /// [`auth::AuthenticatorExtensionOutput::from_cbor`]. 1250 AuthenticatorExtension(AuthExt), 1251 /// [ED](https://www.w3.org/TR/webauthn-3/#authdata-flags-ed) bit was 0, but 1252 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) existed. 1253 NoExtensionBitWithData, 1254 /// [ED](https://www.w3.org/TR/webauthn-3/#authdata-flags-ed) bit was 1, but 1255 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) did not exist. 1256 ExtensionBitWithoutData, 1257 /// There was trailing data that could not be deserialized. 1258 TrailingData, 1259 } 1260 impl<U, C: Display, A: Display> Display for AuthenticatorDataErr<U, C, A> { 1261 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 1262 match *self { 1263 Self::Len => f.write_str("authenticator data had an invalid length"), 1264 Self::UserNotPresent(_) => f.write_str("user was not present"), 1265 Self::FlagsBit1Not0 => f.write_str("flags 1-bit was 1"), 1266 Self::FlagsBit5Not0 => f.write_str("flags 5-bit was 1"), 1267 Self::AttestedCredentialData => f.write_str("attested credential data was included during authentication or was not included during registration"), 1268 Self::BackupWithoutEligibility => { 1269 f.write_str("backup state bit was 1 despite backup eligibility being 0") 1270 } 1271 Self::AttestedCredential(ref err) => err.fmt(f), 1272 Self::AuthenticatorExtension(ref err) => err.fmt(f), 1273 Self::NoExtensionBitWithData => { 1274 f.write_str("extension bit was 0 despite extensions existing") 1275 } 1276 Self::ExtensionBitWithoutData => { 1277 f.write_str("extension bit was 1 despite no extensions existing") 1278 } 1279 Self::TrailingData => { 1280 f.write_str("slice had trailing data that could not be deserialized") 1281 } 1282 } 1283 } 1284 } 1285 impl From<AuthenticatorDataErr<Infallible, AttestedCredentialDataErr, RegAuthExtErr>> for RegAuthDataErr { 1286 #[inline] 1287 fn from(value: AuthenticatorDataErr<Infallible, AttestedCredentialDataErr, RegAuthExtErr>) -> Self { 1288 match value { 1289 AuthenticatorDataErr::Len => Self::Len, 1290 AuthenticatorDataErr::UserNotPresent(v) => match v {}, 1291 AuthenticatorDataErr::FlagsBit1Not0 => Self::FlagsBit1Not0, 1292 AuthenticatorDataErr::FlagsBit5Not0 => Self::FlagsBit5Not0, 1293 AuthenticatorDataErr::AttestedCredentialData => Self::AttestedCredentialDataNotIncluded, 1294 AuthenticatorDataErr::BackupWithoutEligibility => Self::BackupWithoutEligibility, 1295 AuthenticatorDataErr::AttestedCredential(err) => Self::AttestedCredential(err), 1296 AuthenticatorDataErr::AuthenticatorExtension(err) => Self::AuthenticatorExtension(err), 1297 AuthenticatorDataErr::NoExtensionBitWithData => Self::NoExtensionBitWithData, 1298 AuthenticatorDataErr::ExtensionBitWithoutData => Self::ExtensionBitWithoutData, 1299 AuthenticatorDataErr::TrailingData => Self::TrailingData, 1300 } 1301 } 1302 } 1303 impl From<AuthenticatorDataErr<(), Infallible, AuthAuthExtErr>> for AuthAuthDataErr { 1304 #[inline] 1305 fn from(value: AuthenticatorDataErr<(), Infallible, AuthAuthExtErr>) -> Self { 1306 match value { 1307 AuthenticatorDataErr::Len => Self::Len, 1308 AuthenticatorDataErr::UserNotPresent(()) => Self::UserNotPresent, 1309 AuthenticatorDataErr::FlagsBit1Not0 => Self::FlagsBit1Not0, 1310 AuthenticatorDataErr::FlagsBit5Not0 => Self::FlagsBit5Not0, 1311 AuthenticatorDataErr::AttestedCredentialData => Self::AttestedCredentialDataIncluded, 1312 AuthenticatorDataErr::AttestedCredential(val) => match val {}, 1313 AuthenticatorDataErr::BackupWithoutEligibility => Self::BackupWithoutEligibility, 1314 AuthenticatorDataErr::AuthenticatorExtension(err) => Self::AuthenticatorExtension(err), 1315 AuthenticatorDataErr::NoExtensionBitWithData => Self::NoExtensionBitWithData, 1316 AuthenticatorDataErr::ExtensionBitWithoutData => Self::ExtensionBitWithoutData, 1317 AuthenticatorDataErr::TrailingData => Self::TrailingData, 1318 } 1319 } 1320 } 1321 impl<'a, A> FromCbor<'a> for A 1322 where 1323 A: AuthData<'a>, 1324 A::CredData: FromCbor<'a>, 1325 A::Ext: FromCbor<'a>, 1326 { 1327 type Err = AuthenticatorDataErr<A::UpBitErr, <A::CredData as FromCbor<'a>>::Err, <A::Ext as FromCbor<'a>>::Err>; 1328 #[expect(clippy::big_endian_bytes, reason = "CBOR integers are in big-endian")] 1329 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 1330 /// Length of `signCount`. 1331 const SIGN_COUNT_LEN: usize = 4; 1332 /// `UP` bit (i.e., bit 0) set to 1. 1333 const UP: u8 = 0b0000_0001; 1334 /// `RFU1` bit (i.e., bit 1) set to 1. 1335 const RFU1: u8 = UP << 1; 1336 /// `UV` bit (i.e., bit 2) set to 1. 1337 const UV: u8 = RFU1 << 1; 1338 /// `BE` bit (i.e., bit 3) set to 1. 1339 const BE: u8 = UV << 1; 1340 /// `BS` bit (i.e., bit 4) set to 1. 1341 const BS: u8 = BE << 1; 1342 /// `RFU2` bit (i.e., bit 5) set to 1. 1343 const RFU2: u8 = BS << 1; 1344 /// `AT` bit (i.e., bit 6) set to 1. 1345 const AT: u8 = RFU2 << 1; 1346 /// `ED` bit (i.e., bit 7) set to 1. 1347 const ED: u8 = AT << 1; 1348 cbor.split_at_checked(Sha256::output_size()).ok_or_else(|| AuthenticatorDataErr::Len).and_then(|(rp_id_slice, rp_id_rem)| { 1349 rp_id_rem.split_first().ok_or_else(|| AuthenticatorDataErr::Len).and_then(|(&flag, flag_rem)| { 1350 let user_present = flag & UP == UP; 1351 if user_present { 1352 Ok(()) 1353 } else { 1354 A::user_is_not_present().map_err(AuthenticatorDataErr::UserNotPresent) 1355 } 1356 .and_then(|()| { 1357 if flag & RFU1 == 0 { 1358 if flag & RFU2 == 0 { 1359 let at_bit = A::contains_at_bit(); 1360 if flag & AT == AT { 1361 if at_bit { 1362 Ok(()) 1363 } else { 1364 Err(AuthenticatorDataErr::AttestedCredentialData) 1365 } 1366 } else if at_bit { 1367 Err(AuthenticatorDataErr::AttestedCredentialData) 1368 } else { 1369 Ok(()) 1370 }.and_then(|()| { 1371 let bs = flag & BS == BS; 1372 if flag & BE == BE { 1373 if bs { 1374 Ok(Backup::Exists) 1375 } else { 1376 Ok(Backup::Eligible) 1377 } 1378 } else if bs { 1379 Err(AuthenticatorDataErr::BackupWithoutEligibility) 1380 } else { 1381 Ok(Backup::NotEligible) 1382 } 1383 .and_then(|backup| { 1384 flag_rem.split_at_checked(SIGN_COUNT_LEN).ok_or_else(|| AuthenticatorDataErr::Len).and_then(|(count_slice, count_rem)| { 1385 A::CredData::from_cbor(count_rem).map_err(AuthenticatorDataErr::AttestedCredential).and_then(|att_data| { 1386 A::Ext::from_cbor(att_data.remaining).map_err(AuthenticatorDataErr::AuthenticatorExtension).and_then(|ext| { 1387 if ext.remaining.is_empty() { 1388 let ed = flag & ED == ED; 1389 if ext.value.missing() { 1390 if ed { 1391 Err(AuthenticatorDataErr::ExtensionBitWithoutData) 1392 } else { 1393 Ok(()) 1394 } 1395 } else if ed { 1396 Ok(()) 1397 } else { 1398 Err(AuthenticatorDataErr::NoExtensionBitWithData) 1399 }.map(|()| { 1400 let mut sign_count = [0; SIGN_COUNT_LEN]; 1401 sign_count.copy_from_slice(count_slice); 1402 // `signCount` is in big-endian. 1403 CborSuccess { value: A::new(rp_id_slice, Flag { user_present, user_verified: flag & UV == UV, backup, }, u32::from_be_bytes(sign_count), att_data.value, ext.value), remaining: ext.remaining, } 1404 }) 1405 } else { 1406 Err(AuthenticatorDataErr::TrailingData) 1407 } 1408 }) 1409 }) 1410 }) 1411 }) 1412 }) 1413 } else { 1414 Err(AuthenticatorDataErr::FlagsBit5Not0) 1415 } 1416 } else { 1417 Err(AuthenticatorDataErr::FlagsBit1Not0) 1418 } 1419 }) 1420 }) 1421 }) 1422 } 1423 } 1424 /// Data returned by [`AuthDataContainer::from_data`]. 1425 pub(super) struct ParsedAuthData<'a, A> { 1426 /// The data the CBOR is parsed into. 1427 data: A, 1428 /// The raw authenticator data and 32-bytes of trailing data. 1429 auth_data_and_32_trailing_bytes: &'a [u8], 1430 } 1431 /// Error returned by [`AuthResponse::parse_data_and_verify_sig`]. 1432 pub(super) enum AuthRespErr<AuthDataErr> { 1433 /// Variant returned when parsing 1434 /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson) 1435 /// into [`CollectedClientData`] fails. 1436 CollectedClientData(CollectedClientDataErr), 1437 /// Variant returned when parsing 1438 /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson) 1439 /// in a "relaxed" way into [`CollectedClientData`] fails. 1440 #[cfg(feature = "serde_relaxed")] 1441 CollectedClientDataRelaxed(SerdeJsonErr), 1442 /// Variant returned when parsing [`AuthResponse::Auth`] fails. 1443 Auth(AuthDataErr), 1444 /// Variant when the [`CompressedPubKey`] or [`UncompressePubKey`] is not valid. 1445 PubKey(PubKeyErr), 1446 /// Variant returned when the signature, if one exists, associated with 1447 /// [`Self::AuthResponse`] is invalid. 1448 Signature, 1449 } 1450 impl<A: Display> Display for AuthRespErr<A> { 1451 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 1452 match *self { 1453 Self::CollectedClientData(ref err) => write!(f, "CollectedClientData could not be parsed: {err}"), 1454 #[cfg(feature = "serde_relaxed")] 1455 Self::CollectedClientDataRelaxed(ref err) => write!(f, "CollectedClientData could not be parsed: {err}"), 1456 Self::Auth(ref err) => write!(f, "auth data could not be parsed: {err}"), 1457 Self::PubKey(err) => err.fmt(f), 1458 Self::Signature => f.write_str("the signature over the authenticator data and CollectedClientData could not be verified"), 1459 } 1460 } 1461 } 1462 impl From<AuthRespErr<AttestationObjectErr>> for RegCeremonyErr { 1463 #[inline] 1464 fn from(value: AuthRespErr<AttestationObjectErr>) -> Self { 1465 match value { 1466 AuthRespErr::CollectedClientData(err) => Self::CollectedClientData(err), 1467 #[cfg(feature = "serde_relaxed")] 1468 AuthRespErr::CollectedClientDataRelaxed(err) => Self::CollectedClientDataRelaxed(err), 1469 AuthRespErr::Auth(err) => Self::AttestationObject(err), 1470 AuthRespErr::PubKey(err) => Self::PubKey(err), 1471 AuthRespErr::Signature => Self::AttestationSignature, 1472 } 1473 } 1474 } 1475 impl From<AuthRespErr<AuthAuthDataErr>> for AuthCeremonyErr { 1476 #[inline] 1477 fn from(value: AuthRespErr<AuthAuthDataErr>) -> Self { 1478 match value { 1479 AuthRespErr::CollectedClientData(err) => Self::CollectedClientData(err), 1480 #[cfg(feature = "serde_relaxed")] 1481 AuthRespErr::CollectedClientDataRelaxed(err) => Self::CollectedClientDataRelaxed(err), 1482 AuthRespErr::Auth(err) => Self::AuthenticatorData(err), 1483 AuthRespErr::PubKey(err) => Self::PubKey(err), 1484 AuthRespErr::Signature => Self::AssertionSignature, 1485 } 1486 } 1487 } 1488 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data) 1489 /// container. 1490 /// 1491 /// Note [`Self::Auth`] may be `Self`. 1492 pub(super) trait AuthDataContainer<'a>: Sized { 1493 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). 1494 type Auth: AuthData<'a>; 1495 /// Error returned from [`Self::from_data`]. 1496 type Err; 1497 /// Converts `data` into [`ParsedAuthData`]. 1498 /// 1499 /// # Errors 1500 /// 1501 /// Errors iff `data` cannot be converted into `ParsedAuthData`. 1502 fn from_data(data: &'a [u8]) -> Result<ParsedAuthData<'a, Self>, Self::Err>; 1503 /// Returns the contained 1504 /// [authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). 1505 fn authenticator_data(&self) -> &Self::Auth; 1506 } 1507 /// [`AuthenticatorResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorresponse). 1508 pub(super) trait AuthResponse { 1509 /// [Attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object) or 1510 /// [authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). 1511 type Auth<'a>: AuthDataContainer<'a> where Self: 'a; 1512 /// Public key to use to verify the contained signature. 1513 type CredKey<'a>; 1514 /// Parses 1515 /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson) 1516 /// based on `RELAXED` and [`Self::Auth`] via [`AuthDataContainer::from_data`] in addition to 1517 /// verifying any possible signature over the concatenation of the raw 1518 /// [`AuthDataContainer::Auth`] and `clientDataJSON` using `key` or the contained 1519 /// public key if one exists. If `Self` contains a public key and should not be passed one, then it should set 1520 /// [`Self::CredKey`] to `()`. 1521 /// 1522 /// # Errors 1523 /// 1524 /// Errors iff parsing `clientDataJSON` errors, [`AuthDataContainer::from_data`] does, or the signature 1525 /// is invalid. 1526 /// 1527 /// # Panics 1528 /// 1529 /// `panic`s iff `relaxed` and `serde_relaxed` is not enabled. 1530 #[expect( 1531 clippy::type_complexity, 1532 reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable" 1533 )] 1534 fn parse_data_and_verify_sig(&self, key: Self::CredKey<'_>, relaxed: bool) -> Result<(CollectedClientData<'_>, Self::Auth<'_>), AuthRespErr<<Self::Auth<'_> as AuthDataContainer<'_>>::Err>>; 1535 } 1536 /// Ceremony response (i.e., [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#publickeycredential)). 1537 pub(super) trait Response { 1538 /// [`AuthenticatorResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorresponse). 1539 type Auth: AuthResponse; 1540 /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response). 1541 fn auth(&self) -> &Self::Auth; 1542 } 1543 /// Error returned from [`Ceremony::partial_validate`]. 1544 pub(super) enum CeremonyErr<AuthDataErr> { 1545 /// Timeout occurred. 1546 Timeout, 1547 /// Read [`AuthRespErr`] for information. 1548 AuthResp(AuthRespErr<AuthDataErr>), 1549 /// Origin did not validate. 1550 OriginMismatch, 1551 /// Cross origin was `true` but was not allowed to be. 1552 CrossOrigin, 1553 /// Top origin did not validate. 1554 TopOriginMismatch, 1555 /// Challenges don't match. 1556 ChallengeMismatch, 1557 /// `rpIdHash` does not match the SHA-256 hash of the [`RpId`]. 1558 RpIdHashMismatch, 1559 /// User was not verified despite being required to. 1560 UserNotVerified, 1561 /// [`Backup::NotEligible`] was not sent back despite [`BackupReq::NotEligible`]. 1562 BackupEligible, 1563 /// [`Backup::NotEligible`] was sent back despite [`BackupReq::Eligible`]. 1564 BackupNotEligible, 1565 /// [`Backup::Eligible`] was not sent back despite [`BackupReq::EligibleNoteExists`]. 1566 BackupExists, 1567 /// [`Backup::Exists`] was not sent back despite [`BackupReq::Exists`]. 1568 BackupDoesNotExist, 1569 } 1570 impl<A: Display> Display for CeremonyErr<A> { 1571 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 1572 match *self { 1573 Self::Timeout => f.write_str("ceremony timed out"), 1574 Self::AuthResp(ref err) => err.fmt(f), 1575 Self::OriginMismatch => { 1576 f.write_str("the origin sent from the client is not an allowed origin") 1577 } 1578 Self::CrossOrigin => { 1579 f.write_str("cross origin was from the client, but it is not allowed") 1580 } 1581 Self::TopOriginMismatch => { 1582 f.write_str("the top origin sent from the client is not an allowed top origin") 1583 } 1584 Self::ChallengeMismatch => f.write_str( 1585 "the challenge sent to the client does not match the challenge sent back", 1586 ), 1587 Self::RpIdHashMismatch => f.write_str( 1588 "the SHA-256 hash of the RP ID doesn't match the hash sent from the client", 1589 ), 1590 Self::UserNotVerified => f.write_str("user was not verified despite being required to"), 1591 Self::BackupEligible => f.write_str("credential is eligible to be backed up despite requiring that it not be"), 1592 Self::BackupNotEligible => f.write_str("credential is not eligible to be backed up despite requiring that it be"), 1593 Self::BackupExists => f.write_str("credential backup exists despite requiring that a backup not exist"), 1594 Self::BackupDoesNotExist => f.write_str("credential backup does not exist despite requiring that a backup exist"), 1595 } 1596 } 1597 } 1598 impl From<CeremonyErr<AttestationObjectErr>> for RegCeremonyErr { 1599 #[inline] 1600 fn from(value: CeremonyErr<AttestationObjectErr>) -> Self { 1601 match value { 1602 CeremonyErr::Timeout => Self::Timeout, 1603 CeremonyErr::AuthResp(err) => err.into(), 1604 CeremonyErr::OriginMismatch => Self::OriginMismatch, 1605 CeremonyErr::CrossOrigin => Self::CrossOrigin, 1606 CeremonyErr::TopOriginMismatch => Self::TopOriginMismatch, 1607 CeremonyErr::ChallengeMismatch => Self::ChallengeMismatch, 1608 CeremonyErr::RpIdHashMismatch => Self::RpIdHashMismatch, 1609 CeremonyErr::UserNotVerified => Self::UserNotVerified, 1610 CeremonyErr::BackupEligible => Self::BackupEligible, 1611 CeremonyErr::BackupNotEligible => Self::BackupNotEligible, 1612 CeremonyErr::BackupExists => Self::BackupExists, 1613 CeremonyErr::BackupDoesNotExist => Self::BackupDoesNotExist, 1614 } 1615 } 1616 } 1617 impl From<CeremonyErr<AuthAuthDataErr>> for AuthCeremonyErr { 1618 #[inline] 1619 fn from(value: CeremonyErr<AuthAuthDataErr>) -> Self { 1620 match value { 1621 CeremonyErr::Timeout => Self::Timeout, 1622 CeremonyErr::AuthResp(err) => err.into(), 1623 CeremonyErr::OriginMismatch => Self::OriginMismatch, 1624 CeremonyErr::CrossOrigin => Self::CrossOrigin, 1625 CeremonyErr::TopOriginMismatch => Self::TopOriginMismatch, 1626 CeremonyErr::ChallengeMismatch => Self::ChallengeMismatch, 1627 CeremonyErr::RpIdHashMismatch => Self::RpIdHashMismatch, 1628 CeremonyErr::UserNotVerified => Self::UserNotVerified, 1629 CeremonyErr::BackupEligible => Self::BackupEligible, 1630 CeremonyErr::BackupNotEligible => Self::BackupNotEligible, 1631 CeremonyErr::BackupExists => Self::BackupExists, 1632 CeremonyErr::BackupDoesNotExist => Self::BackupDoesNotExist, 1633 } 1634 } 1635 } 1636 /// [`AllAcceptedCredentialsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions). 1637 /// 1638 /// This can be sent to _an already authenticated user_ to inform what credentials are currently registered. 1639 /// This can be useful when a user deletes credentials on the RP's side but does not do so on the authenticator. 1640 /// When the client forwards this response to the authenticator, it can remove all credentials that don't have 1641 /// a [`CredentialId`] in [`Self::all_accepted_credential_ids`]. 1642 #[derive(Debug)] 1643 pub struct AllAcceptedCredentialsOptions<'rp, 'user, const USER_LEN: usize> { 1644 /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-rpid). 1645 pub rp_id: &'rp RpId, 1646 /// [`userId`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-userid). 1647 pub user_id: &'user UserHandle<USER_LEN>, 1648 /// [`allAcceptedCredentialIds`](https://www.w3.org/TR/webauthn-3/#dictdef-allacceptedcredentialsoptions-allacceptedcredentialids). 1649 pub all_accepted_credential_ids: Vec<CredentialId<Vec<u8>>>, 1650 } 1651 /// [`CurrentUserDetailsOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions). 1652 /// 1653 /// This can be sent to _an already authenticated user_ to inform the user information. 1654 /// This can be useful when a user updates their user information on the RP's side but does not do so on the authenticator. 1655 /// When the client forwards this response to the authenticator, it can update the user info for the associated credential. 1656 #[derive(Debug)] 1657 pub struct CurrentUserDetailsOptions<'rp_id, 'name, 'display_name, 'id, const LEN: usize> { 1658 /// [`rpId`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-rpid). 1659 pub rp_id: &'rp_id RpId, 1660 /// [`userId`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-userid), 1661 /// [`name`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-name), and 1662 /// [`displayName`](https://www.w3.org/TR/webauthn-3/#dictdef-currentuserdetailsoptions-displayname). 1663 pub user: PublicKeyCredentialUserEntity<'name, 'display_name, 'id, LEN>, 1664 } 1665 /// [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hmac-secret-extension) 1666 /// during authentication and 1667 /// [`hmac-secret-mc`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-make-cred-extension) 1668 /// during registration. 1669 /// 1670 /// `REG` iff `hmac-secret-mc`. 1671 enum HmacSecretGet<const REG: bool> { 1672 /// No `hmac-secret` response. 1673 None, 1674 /// One encrypted `hmac-secret`. 1675 One, 1676 /// Two encrypted `hmac-secret`s. 1677 Two, 1678 } 1679 /// Error returned by [`HmacSecretGet::from_cbor`] 1680 enum HmacSecretGetErr { 1681 /// Error related to the length of the CBOR input. 1682 Len, 1683 /// Error related to the type of the CBOR key. 1684 Type, 1685 /// Error related to the value of the CBOR value. 1686 Value, 1687 } 1688 impl<const REG: bool> FromCbor<'_> for HmacSecretGet<REG> { 1689 type Err = HmacSecretGetErr; 1690 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 1691 /// AES block size. 1692 const AES_BLOCK_SIZE: usize = 16; 1693 /// HMAC-SHA-256 output length. 1694 const HMAC_SHA_256_LEN: usize = 32; 1695 /// Length of two HMAC-SHA-256 outputs concatenated together. 1696 const TWO_HMAC_SHA_256_LEN: usize = HMAC_SHA_256_LEN << 1; 1697 // We need the smallest multiple of `AES_BLOCK_SIZE` that 1698 // is strictly greater than `HMAC_SHA_256_LEN`. 1699 /// AES-256 output length on a 32-byte input. 1700 #[expect( 1701 clippy::integer_division_remainder_used, 1702 reason = "doesn't need to be constant time" 1703 )] 1704 const ONE_SECRET_LEN: usize = 1705 HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (HMAC_SHA_256_LEN % AES_BLOCK_SIZE)); 1706 // We need the smallest multiple of `AES_BLOCK_SIZE` that 1707 // is strictly greater than `TWO_HMAC_SHA_256_LEN`. 1708 /// AES-256 output length on a 64-byte input. 1709 #[expect( 1710 clippy::integer_division_remainder_used, 1711 reason = "doesn't need to be constant time" 1712 )] 1713 const TWO_SECRET_LEN: usize = 1714 TWO_HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (TWO_HMAC_SHA_256_LEN % AES_BLOCK_SIZE)); 1715 /// `hmac-secret-mc`. 1716 /// 1717 /// This is the key iff `REG`. 1718 const KEY: [u8; 15] = [ 1719 cbor::TEXT_14, 1720 b'h', 1721 b'm', 1722 b'a', 1723 b'c', 1724 b'-', 1725 b's', 1726 b'e', 1727 b'c', 1728 b'r', 1729 b'e', 1730 b't', 1731 b'-', 1732 b'm', 1733 b'c', 1734 ]; 1735 /// Helper that unifies `HmacSecretGet`. 1736 enum CborVal<'a> { 1737 /// Extension does not exist with remaining payload 1738 Success, 1739 /// Extension exists with remaining payload. 1740 Continue(&'a [u8]), 1741 } 1742 if REG { 1743 cbor.split_at_checked(KEY.len()).map_or( 1744 Ok(CborVal::Success), 1745 |(key, key_rem)| { 1746 if key == KEY { 1747 Ok(CborVal::Continue(key_rem)) 1748 } else { 1749 Ok(CborVal::Success) 1750 } 1751 } 1752 ) 1753 } else { 1754 cbor.split_at_checked(cbor::HMAC_SECRET.len()).map_or( 1755 Ok(CborVal::Success), 1756 |(key, key_rem)| { 1757 if key == cbor::HMAC_SECRET { 1758 Ok(CborVal::Continue(key_rem)) 1759 } else { 1760 Ok(CborVal::Success) 1761 } 1762 } 1763 ) 1764 }.and_then(|cbor_val| { 1765 match cbor_val { 1766 CborVal::Success => Ok(CborSuccess { value: Self::None, remaining: cbor, }), 1767 CborVal::Continue(key_rem) => { 1768 key_rem 1769 .split_first() 1770 .ok_or(HmacSecretGetErr::Len) 1771 .and_then(|(bytes, bytes_rem)| { 1772 if *bytes == cbor::BYTES_INFO_24 { 1773 bytes_rem 1774 .split_first() 1775 .ok_or(HmacSecretGetErr::Len) 1776 .and_then(|(&len, len_rem)| { 1777 len_rem.split_at_checked(usize::from(len)).ok_or(HmacSecretGetErr::Len).and_then(|(_, remaining)| { 1778 match usize::from(len) { 1779 ONE_SECRET_LEN => { 1780 Ok(CborSuccess { 1781 value: Self::One, 1782 remaining, 1783 }) 1784 } 1785 TWO_SECRET_LEN => { 1786 Ok(CborSuccess { 1787 value: Self::Two, 1788 remaining, 1789 }) 1790 } 1791 _ => Err(HmacSecretGetErr::Value), 1792 } 1793 }) 1794 }) 1795 } else { 1796 Err(HmacSecretGetErr::Type) 1797 } 1798 }) 1799 } 1800 } 1801 }) 1802 } 1803 } 1804 #[cfg(test)] 1805 mod tests { 1806 use super::{CollectedClientDataErr, ClientDataJsonParser, LimitedVerificationParser}; 1807 #[test] 1808 fn parse_string() { 1809 assert!(LimitedVerificationParser::<true>::parse_string(br#"abc""#) 1810 .map_or(false, |tup| { tup.0 == "abc" && tup.1 == br#""# })); 1811 assert!(LimitedVerificationParser::<false>::parse_string(br#"abc"23"#) 1812 .map_or(false, |tup| { tup.0 == "abc" && tup.1 == br#"23"# })); 1813 assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\"c"23"#) 1814 .map_or(false, |tup| { tup.0 == r#"ab"c"# && tup.1 == br#"23"# })); 1815 assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\\c"23"#) 1816 .map_or(false, |tup| { tup.0 == r#"ab\c"# && tup.1 == br#"23"# })); 1817 assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\u001fc"23"#) 1818 .map_or(false, |tup| { tup.0 == "ab\u{001f}c" && tup.1 == br#"23"# })); 1819 assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\u000dc"23"#) 1820 .map_or(false, |tup| { tup.0 == "ab\u{000d}c" && tup.1 == br#"23"# })); 1821 assert!( 1822 LimitedVerificationParser::<true>::parse_string(b"\\\\\\\\\\\\a\\\\\\\\a\\\\\"").map_or(false, |tup| { 1823 tup.0 == "\\\\\\a\\\\a\\" && tup.1.is_empty() 1824 }) 1825 ); 1826 assert!( 1827 LimitedVerificationParser::<false>::parse_string(b"\\\\\\\\\\a\\\\\\\\a\\\\\"").map_or_else( 1828 |e| matches!(e, CollectedClientDataErr::InvalidEscapedString), 1829 |_| false 1830 ) 1831 ); 1832 assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\u0020c"23"#).map_or_else( 1833 |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), 1834 |_| false 1835 )); 1836 assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\ac"23"#).map_or_else( 1837 |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), 1838 |_| false 1839 )); 1840 assert!(LimitedVerificationParser::<true>::parse_string(br#"ab\""#).map_or_else( 1841 |err| matches!(err, CollectedClientDataErr::InvalidObject), 1842 |_| false 1843 )); 1844 assert!(LimitedVerificationParser::<false>::parse_string(br#"ab\u001Fc"23"#).map_or_else( 1845 |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), 1846 |_| false 1847 )); 1848 assert!(LimitedVerificationParser::<true>::parse_string([0, b'"'].as_slice()).map_or_else( 1849 |err| matches!(err, CollectedClientDataErr::InvalidEscapedString), 1850 |_| false 1851 )); 1852 assert!(LimitedVerificationParser::<false>::parse_string([b'a', 255, b'"'].as_slice()) 1853 .map_or_else(|err| matches!(err, CollectedClientDataErr::Utf8(_)), |_| false)); 1854 assert!(LimitedVerificationParser::<true>::parse_string([b'a', b'"', 255].as_slice()).is_ok()); 1855 assert!( 1856 LimitedVerificationParser::<false>::parse_string(br#"""#).map_or(false, |tup| tup.0.is_empty() && tup.1.is_empty()) 1857 ); 1858 } 1859 #[test] 1860 fn c_data_json() { 1861 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); 1862 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,{}}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); 1863 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_none())); 1864 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.map_or(false, |v| v == "bob"))); 1865 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob",a}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.map_or(false, |v| v == "bob"))); 1866 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); 1867 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin), |_| false)); 1868 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false)); 1869 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0.is_empty() && !val.cross_origin && val.top_origin.is_none())); 1870 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Type), |_| false)); 1871 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false)); 1872 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false)); 1873 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\\e.com" && !val.cross_origin && val.top_origin.is_none())); 1874 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\"e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\"e.com" && !val.cross_origin && val.top_origin.is_none())); 1875 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0013e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\u{0013}e.com" && !val.cross_origin && val.top_origin.is_none())); 1876 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\3e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); 1877 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); 1878 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); 1879 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); 1880 assert!(LimitedVerificationParser::<true>::parse([].as_slice()) 1881 .map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false)); 1882 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidStart), |_| false)); 1883 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); 1884 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false)); 1885 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOriginKey), |_| false)); 1886 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOrigin), |_| false)); 1887 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); 1888 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true","topOrigin":"https://abc.com"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); 1889 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); 1890 assert!(LimitedVerificationParser::<false>::parse(b"{\"type\":\"webauthn.get\",\"challenge\":\"AAAAAAAAAAAAAAAAAAAAAA\",\"origin\":\"https://example.com\",\"crossOrigin\":false,\xff}".as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); 1891 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.is_none())); 1892 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"bob"}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && val.cross_origin && val.top_origin.map_or(false, |v| v == "bob"))); 1893 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::TopOriginWithoutCrossOrigin), |_| false)); 1894 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"topOrigin":""}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false)); 1895 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0.is_empty() && !val.cross_origin && val.top_origin.is_none())); 1896 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Type), |_| false)); 1897 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get", "challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false)); 1898 assert!(LimitedVerificationParser::<false>::parse( 1899 br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false}"# 1900 .as_slice() 1901 ) 1902 .map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false)); 1903 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\\e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\\e.com" && !val.cross_origin && val.top_origin.is_none())); 1904 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\"e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\"e.com" && !val.cross_origin && val.top_origin.is_none())); 1905 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0013e.com","crossOrigin":false}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://exampl\u{0013}e.com" && !val.cross_origin && val.top_origin.is_none())); 1906 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\3e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); 1907 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\e.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); 1908 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u0020.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); 1909 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://exampl\u000A.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidEscapedString), |_| false)); 1910 assert!(LimitedVerificationParser::<false>::parse([].as_slice()) 1911 .map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false)); 1912 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"abc","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidStart), |_| false)); 1913 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); 1914 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","crossOrigin":false,"origin":"example.com"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::OriginKey), |_| false)); 1915 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","topOrigin":"bob","crossOrigin":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOriginKey), |_| false)); 1916 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":"abc"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::CrossOrigin), |_| false)); 1917 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true"a}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::InvalidObject), |_| false)); 1918 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":true,"topOrigin":"https://example.com"}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::TopOriginSameAsOrigin), |_| false)); 1919 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"foo":true}"#.as_slice()).map_or(false, |val| val.challenge.0 == 0 && val.origin.0 == "https://example.com" && !val.cross_origin && val.top_origin.is_none())); 1920 assert!(LimitedVerificationParser::<false>::parse(br#"{"type":"webauthn.get","challengE":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false,"foo":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false)); 1921 assert!(LimitedVerificationParser::<true>::parse(br#"{"type":"webauthn.create"challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossorigin":false,"foo":true}"#.as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::ChallengeKey), |_| false)); 1922 } 1923 #[test] 1924 fn c_data_challenge() { 1925 assert!(LimitedVerificationParser::<false>::get_sent_challenge([].as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false)); 1926 assert!(LimitedVerificationParser::<true>::get_sent_challenge([].as_slice()).map_or_else(|e| matches!(e, CollectedClientDataErr::Len), |_| false)); 1927 assert!(LimitedVerificationParser::<true>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBB").map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false)); 1928 assert!(LimitedVerificationParser::<false>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBB").map_or_else(|e| matches!(e, CollectedClientDataErr::Challenge), |_| false)); 1929 assert!(LimitedVerificationParser::<true>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".as_slice()).map_or(false, |c| c.0 == 0)); 1930 assert!(LimitedVerificationParser::<false>::get_sent_challenge(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".as_slice()).map_or(false, |c| c.0 == 0)); 1931 } 1932 }