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