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