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