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