auth.rs (33014B)
1 #[cfg(feature = "serde_relaxed")] 2 use self::{ 3 super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}, 4 ser_relaxed::{AuthenticationRelaxed, CustomAuthentication}, 5 }; 6 #[cfg(doc)] 7 use super::super::{ 8 AuthenticatedCredential, RegisteredCredential, StaticState, 9 hash::hash_set::MaxLenHashSet, 10 request::{ 11 Challenge, 12 auth::{ 13 CredentialSpecificExtension, DiscoverableAuthenticationServerState, Extension, 14 NonDiscoverableAuthenticationServerState, PublicKeyCredentialRequestOptions, 15 }, 16 register::{self, UserHandle16, UserHandle64}, 17 }, 18 }; 19 use super::{ 20 super::{UserHandle, request::register::USER_HANDLE_MAX_LEN}, 21 AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment, 22 CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor, 23 HmacSecretGet, HmacSecretGetErr, LimitedVerificationParser, ParsedAuthData, Response, 24 SentChallenge, 25 auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr, MissingUserHandleErr}, 26 cbor, 27 error::CollectedClientDataErr, 28 register::CompressedPubKeyBorrowed, 29 }; 30 use core::convert::Infallible; 31 use ed25519_dalek::{Signature, Verifier as _}; 32 use ml_dsa::{MlDsa44, MlDsa65, MlDsa87, Signature as MlDsaSig}; 33 use p256::ecdsa::DerSignature as P256DerSig; 34 use p384::ecdsa::DerSignature as P384DerSig; 35 use rsa::{ 36 pkcs1v15, 37 sha2::{Sha256, digest::Digest as _}, 38 }; 39 #[cfg(feature = "serde_relaxed")] 40 use serde::Deserialize; 41 /// Contains error types. 42 pub mod error; 43 /// Contains functionality to deserialize data from a client. 44 #[cfg(feature = "serde")] 45 pub(super) mod ser; 46 /// Contains functionality to deserialize data from a client in a "relaxed" way. 47 #[cfg(feature = "serde_relaxed")] 48 pub mod ser_relaxed; 49 /// [`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). 50 /// 51 /// This is only relevant when [`Extension::prf`] or [`CredentialSpecificExtension::prf`] is `Some` and for 52 /// authenticators that implement [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) on top of 53 /// `hmac-secret`. 54 /// 55 /// Note while many authenticators that implement `prf` don't require `prf` to have been sent during registration 56 /// (i.e., [`register::Extension::prf`]), it is recommended to do so for those authenticators that do require it. 57 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 58 pub enum HmacSecret { 59 /// No `hmac-secret` response. 60 /// 61 /// Either [`Extension::prf`] was not sent, the credential is not PRF-capable, or the authenticator does not use 62 /// the 63 /// [`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) 64 /// extension. 65 None, 66 /// One encrypted `hmac-secret`. 67 /// 68 /// [`Extension::prf`] was sent with one PRF input for a PRF-capable credential whose authenticator implements 69 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) on top of the 70 /// [`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) 71 /// extension. 72 One, 73 /// Two encrypted `hmac-secret`s. 74 /// 75 /// [`Extension::prf`] was sent with two PRF inputs for a PRF-capable credential whose authenticator implements 76 /// [`prf`](https://www.w3.org/TR/webauthn-3/#prf-extension) on top of the 77 /// [`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) 78 /// extension. 79 Two, 80 } 81 /// [Authenticator extension output](https://www.w3.org/TR/webauthn-3/#authenticator-extension-output). 82 #[derive(Clone, Copy, Debug)] 83 pub struct AuthenticatorExtensionOutput { 84 /// [`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). 85 pub hmac_secret: HmacSecret, 86 } 87 impl AuthExtOutput for AuthenticatorExtensionOutput { 88 fn missing(self) -> bool { 89 matches!(self.hmac_secret, HmacSecret::None) 90 } 91 } 92 impl From<HmacSecretGetErr> for AuthenticatorExtensionOutputErr { 93 #[inline] 94 fn from(value: HmacSecretGetErr) -> Self { 95 match value { 96 HmacSecretGetErr::Len => Self::Len, 97 HmacSecretGetErr::Type => Self::HmacSecretType, 98 HmacSecretGetErr::Value => Self::HmacSecretValue, 99 } 100 } 101 } 102 impl From<HmacSecretGet<false>> for HmacSecret { 103 #[inline] 104 fn from(value: HmacSecretGet<false>) -> Self { 105 match value { 106 HmacSecretGet::None => Self::None, 107 HmacSecretGet::One => Self::One, 108 HmacSecretGet::Two => Self::Two, 109 } 110 } 111 } 112 impl FromCbor<'_> for AuthenticatorExtensionOutput { 113 type Err = AuthenticatorExtensionOutputErr; 114 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 115 // We don't allow unsupported extensions; thus the only possibilities is any ordered element of 116 // the power set of {"hmac-secret":<HmacSecret>}. 117 cbor.split_first().map_or_else( 118 || { 119 Ok(CborSuccess { 120 value: Self { 121 hmac_secret: HmacSecret::None, 122 }, 123 remaining: cbor, 124 }) 125 }, 126 |(map, map_rem)| { 127 if *map == cbor::MAP_1 { 128 HmacSecretGet::from_cbor(map_rem) 129 .map_err(AuthenticatorExtensionOutputErr::from) 130 .map(|success| CborSuccess { 131 value: Self { 132 hmac_secret: success.value.into(), 133 }, 134 remaining: success.remaining, 135 }) 136 } else { 137 Err(AuthenticatorExtensionOutputErr::CborHeader) 138 } 139 }, 140 ) 141 } 142 } 143 /// Unit type for `AuthData::CredData`. 144 pub(crate) struct NoCred; 145 impl FromCbor<'_> for NoCred { 146 type Err = Infallible; 147 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 148 Ok(CborSuccess { 149 value: Self, 150 remaining: cbor, 151 }) 152 } 153 } 154 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). 155 #[derive(Clone, Copy, Debug)] 156 pub struct AuthenticatorData<'a> { 157 /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash). 158 rp_id_hash: &'a [u8], 159 /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags). 160 flags: Flag, 161 /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount). 162 sign_count: u32, 163 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions). 164 extensions: AuthenticatorExtensionOutput, 165 } 166 impl<'a> AuthenticatorData<'a> { 167 /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash). 168 #[inline] 169 #[must_use] 170 pub const fn rp_id_hash(&self) -> &'a [u8] { 171 self.rp_id_hash 172 } 173 /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags). 174 #[inline] 175 #[must_use] 176 pub const fn flags(&self) -> Flag { 177 self.flags 178 } 179 /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount). 180 #[inline] 181 #[must_use] 182 pub const fn sign_count(&self) -> u32 { 183 self.sign_count 184 } 185 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions). 186 #[inline] 187 #[must_use] 188 pub const fn extensions(&self) -> AuthenticatorExtensionOutput { 189 self.extensions 190 } 191 } 192 impl<'a> AuthData<'a> for AuthenticatorData<'a> { 193 type UpBitErr = (); 194 type CredData = NoCred; 195 type Ext = AuthenticatorExtensionOutput; 196 fn contains_at_bit() -> bool { 197 false 198 } 199 fn user_is_not_present() -> Result<(), Self::UpBitErr> { 200 Err(()) 201 } 202 fn new( 203 rp_id_hash: &'a [u8], 204 flags: Flag, 205 sign_count: u32, 206 _: Self::CredData, 207 extensions: Self::Ext, 208 ) -> Self { 209 Self { 210 rp_id_hash, 211 flags, 212 sign_count, 213 extensions, 214 } 215 } 216 fn rp_hash(&self) -> &'a [u8] { 217 self.rp_id_hash 218 } 219 fn flag(&self) -> Flag { 220 self.flags 221 } 222 } 223 impl<'a> AuthDataContainer<'a> for AuthenticatorData<'a> { 224 type Auth = Self; 225 type Err = AuthenticatorDataErr; 226 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 227 #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")] 228 fn from_data(data: &'a [u8]) -> Result<ParsedAuthData<'a, Self>, Self::Err> { 229 // `data.len().checked_sub(Sha256::output_size()).unwrap()` is less than `data.len()`, 230 // so indexing is fine. 231 Self::try_from(&data[..data.len().checked_sub(Sha256::output_size()).unwrap_or_else(|| unreachable!("AuthenticatorData::from_data must be passed a slice with 32 bytes of trailing data"))]).map(|auth_data| ParsedAuthData { data: auth_data, auth_data_and_32_trailing_bytes: data, }) 232 } 233 fn authenticator_data(&self) -> &Self::Auth { 234 self 235 } 236 } 237 impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> { 238 type Error = AuthenticatorDataErr; 239 /// Deserializes `value` based on the 240 /// [authenticator data structure](https://www.w3.org/TR/webauthn-3/#table-authData). 241 #[expect( 242 clippy::panic_in_result_fn, 243 reason = "we want to crash when there is a bug" 244 )] 245 #[inline] 246 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { 247 Self::from_cbor(value) 248 .map_err(AuthenticatorDataErr::from) 249 .map(|auth_data| { 250 assert!( 251 auth_data.remaining.is_empty(), 252 "there is a bug in AuthenticatorData::from_cbor" 253 ); 254 auth_data.value 255 }) 256 } 257 } 258 /// [`AuthenticatorAssertionResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse). 259 #[derive(Debug)] 260 pub struct AuthenticatorAssertion<const USER_LEN: usize, const DISCOVERABLE: bool> { 261 /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). 262 client_data_json: Vec<u8>, 263 /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata) 264 /// followed by the SHA-256 hash of [`Self::client_data_json`]. 265 authenticator_data_and_c_data_hash: Vec<u8>, 266 /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature). 267 signature: Vec<u8>, 268 /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle). 269 user_handle: Option<UserHandle<USER_LEN>>, 270 } 271 impl<const USER_LEN: usize, const DISCOVERABLE: bool> 272 AuthenticatorAssertion<USER_LEN, DISCOVERABLE> 273 { 274 /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). 275 #[inline] 276 #[must_use] 277 pub const fn client_data_json(&self) -> &[u8] { 278 self.client_data_json.as_slice() 279 } 280 /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata). 281 #[expect( 282 clippy::arithmetic_side_effects, 283 clippy::indexing_slicing, 284 reason = "comment justifies their correctness" 285 )] 286 #[inline] 287 #[must_use] 288 pub fn authenticator_data(&self) -> &[u8] { 289 // We only allow creation via [`Self::new`] which creates [`Self::authenticator_data_and_c_data_hash`] 290 // by appending the SHA-256 hash of [`Self::client_data_json`] to the authenticator data that was passed; 291 // thus indexing is fine and subtraction won't cause underflow. 292 &self.authenticator_data_and_c_data_hash 293 [..self.authenticator_data_and_c_data_hash.len() - Sha256::output_size()] 294 } 295 /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature). 296 #[inline] 297 #[must_use] 298 pub const fn signature(&self) -> &[u8] { 299 self.signature.as_slice() 300 } 301 /// Constructs an instance of `Self` with the contained data. 302 /// 303 /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes 304 /// of available capacity; if not, a reallocation will occur. 305 fn new_inner( 306 client_data_json: Vec<u8>, 307 mut authenticator_data: Vec<u8>, 308 signature: Vec<u8>, 309 user_handle: Option<UserHandle<USER_LEN>>, 310 ) -> Self { 311 authenticator_data.extend_from_slice(&Sha256::digest(client_data_json.as_slice())); 312 Self { 313 client_data_json, 314 authenticator_data_and_c_data_hash: authenticator_data, 315 signature, 316 user_handle, 317 } 318 } 319 } 320 impl<const USER_LEN: usize> AuthenticatorAssertion<USER_LEN, false> { 321 /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle). 322 #[inline] 323 #[must_use] 324 pub const fn user_handle(&self) -> Option<&UserHandle<USER_LEN>> { 325 self.user_handle.as_ref() 326 } 327 /// Constructs an instance of `Self` with the contained data. 328 /// 329 /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes 330 /// of available capacity; if not, a reallocation will occur. 331 #[inline] 332 #[must_use] 333 pub fn with_optional_user( 334 client_data_json: Vec<u8>, 335 authenticator_data: Vec<u8>, 336 signature: Vec<u8>, 337 user_handle: Option<UserHandle<USER_LEN>>, 338 ) -> Self { 339 Self::new_inner(client_data_json, authenticator_data, signature, user_handle) 340 } 341 /// Same as [`Self::with_optional_user`] with `None` used for `user_handle`. 342 #[inline] 343 #[must_use] 344 pub fn without_user( 345 client_data_json: Vec<u8>, 346 authenticator_data: Vec<u8>, 347 signature: Vec<u8>, 348 ) -> Self { 349 Self::with_optional_user(client_data_json, authenticator_data, signature, None) 350 } 351 /// Same as [`Self::with_optional_user`] with `Some(user_handle)` used for `user_handle`. 352 #[inline] 353 #[must_use] 354 pub fn with_user( 355 client_data_json: Vec<u8>, 356 authenticator_data: Vec<u8>, 357 signature: Vec<u8>, 358 user_handle: UserHandle<USER_LEN>, 359 ) -> Self { 360 Self::with_optional_user( 361 client_data_json, 362 authenticator_data, 363 signature, 364 Some(user_handle), 365 ) 366 } 367 } 368 impl<const USER_LEN: usize> AuthenticatorAssertion<USER_LEN, true> { 369 /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle). 370 #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] 371 #[inline] 372 #[must_use] 373 pub fn user_handle(&self) -> &UserHandle<USER_LEN> { 374 self.user_handle 375 .as_ref() 376 .unwrap_or_else(|| unreachable!("bug in AuthenticatorAssertion<USER_LEN, true>")) 377 } 378 /// Constructs an instance of `Self` with the contained data. 379 /// 380 /// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes 381 /// of available capacity; if not, a reallocation will occur. 382 #[inline] 383 #[must_use] 384 pub fn new( 385 client_data_json: Vec<u8>, 386 authenticator_data: Vec<u8>, 387 signature: Vec<u8>, 388 user_handle: UserHandle<USER_LEN>, 389 ) -> Self { 390 Self::new_inner( 391 client_data_json, 392 authenticator_data, 393 signature, 394 Some(user_handle), 395 ) 396 } 397 } 398 impl<const USER_LEN: usize, const DISCOVERABLE: bool> AuthResponse 399 for AuthenticatorAssertion<USER_LEN, DISCOVERABLE> 400 { 401 type Auth<'a> 402 = AuthenticatorData<'a> 403 where 404 Self: 'a; 405 type CredKey<'a> = CompressedPubKeyBorrowed<'a>; 406 #[expect(clippy::too_many_lines, reason = "134 lines is OK")] 407 fn parse_data_and_verify_sig( 408 &self, 409 key: Self::CredKey<'_>, 410 relaxed: bool, 411 ) -> Result< 412 (CollectedClientData<'_>, Self::Auth<'_>), 413 AuthRespErr<<Self::Auth<'_> as AuthDataContainer<'_>>::Err>, 414 > { 415 /// Always `panic`s. 416 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 417 #[cfg(not(feature = "serde_relaxed"))] 418 fn get_client_collected_data<const LEN: usize, const DISC: bool>(_: &[u8]) -> ! { 419 unreachable!( 420 "AuthenticatorAssertion::parse_data_and_verify_sig must be passed false when serde_relaxed is not enabled" 421 ); 422 } 423 /// Parses `data` using `CollectedClientData::from_client_data_json_relaxed::<false>`. 424 #[cfg(feature = "serde_relaxed")] 425 fn get_client_collected_data<const LEN: usize, const DISC: bool>( 426 data: &[u8], 427 ) -> Result< 428 CollectedClientData<'_>, 429 AuthRespErr< 430 <<AuthenticatorAssertion<LEN, DISC> as AuthResponse>::Auth<'_> as AuthDataContainer<'_>>::Err, 431 >, 432 >{ 433 CollectedClientData::from_client_data_json_relaxed::<false>(data) 434 .map_err(AuthRespErr::CollectedClientDataRelaxed) 435 } 436 if relaxed { 437 get_client_collected_data::<USER_LEN, DISCOVERABLE>(self.client_data_json.as_slice()) 438 } else { 439 CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice()) 440 .map_err(AuthRespErr::CollectedClientData) 441 } 442 .and_then(|client_data_json| { 443 Self::Auth::from_data(self.authenticator_data_and_c_data_hash.as_slice()) 444 .map_err(AuthRespErr::Auth) 445 .and_then(|val| { 446 match key { 447 CompressedPubKeyBorrowed::MlDsa87(k) => self 448 .signature 449 .as_slice() 450 .try_into() 451 .map_err(|_e| AuthRespErr::Signature) 452 .and_then(|s| { 453 MlDsaSig::<MlDsa87>::decode(s) 454 .ok_or(AuthRespErr::Signature) 455 .and_then(|sig| { 456 k.into_ver_key() 457 .verify( 458 self.authenticator_data_and_c_data_hash.as_slice(), 459 &sig, 460 ) 461 .map_err(|_e| AuthRespErr::Signature) 462 }) 463 }), 464 CompressedPubKeyBorrowed::MlDsa65(k) => self 465 .signature 466 .as_slice() 467 .try_into() 468 .map_err(|_e| AuthRespErr::Signature) 469 .and_then(|s| { 470 MlDsaSig::<MlDsa65>::decode(s) 471 .ok_or(AuthRespErr::Signature) 472 .and_then(|sig| { 473 k.into_ver_key() 474 .verify( 475 self.authenticator_data_and_c_data_hash.as_slice(), 476 &sig, 477 ) 478 .map_err(|_e| AuthRespErr::Signature) 479 }) 480 }), 481 CompressedPubKeyBorrowed::MlDsa44(k) => self 482 .signature 483 .as_slice() 484 .try_into() 485 .map_err(|_e| AuthRespErr::Signature) 486 .and_then(|s| { 487 MlDsaSig::<MlDsa44>::decode(s) 488 .ok_or(AuthRespErr::Signature) 489 .and_then(|sig| { 490 k.into_ver_key() 491 .verify( 492 self.authenticator_data_and_c_data_hash.as_slice(), 493 &sig, 494 ) 495 .map_err(|_e| AuthRespErr::Signature) 496 }) 497 }), 498 CompressedPubKeyBorrowed::Ed25519(k) => k 499 .into_ver_key() 500 .map_err(AuthRespErr::PubKey) 501 .and_then(|ver_key| { 502 Signature::from_slice(self.signature.as_slice()) 503 .and_then(|sig| { 504 // We don't need to use `VerifyingKey::verify_strict` since 505 // `Ed25519PubKey::into_ver_key` verifies the public key is not 506 // in the small-order subgroup. `VerifyingKey::verify_strict` additionally 507 // ensures _R_ of the signature is not in the small-order subgroup, but this 508 // doesn't provide additional benefits and is still not enough to comply 509 // with standards like RFC 8032 or NIST SP 800-186. 510 ver_key.verify( 511 self.authenticator_data_and_c_data_hash.as_slice(), 512 &sig, 513 ) 514 }) 515 .map_err(|_e| AuthRespErr::Signature) 516 }), 517 CompressedPubKeyBorrowed::P256(k) => k 518 .into_ver_key() 519 .map_err(AuthRespErr::PubKey) 520 .and_then(|ver_key| { 521 P256DerSig::from_bytes(self.signature.as_slice()) 522 .and_then(|sig| { 523 ver_key.verify( 524 self.authenticator_data_and_c_data_hash.as_slice(), 525 &sig, 526 ) 527 }) 528 .map_err(|_e| AuthRespErr::Signature) 529 }), 530 CompressedPubKeyBorrowed::P384(k) => k 531 .into_ver_key() 532 .map_err(AuthRespErr::PubKey) 533 .and_then(|ver_key| { 534 P384DerSig::from_bytes(self.signature.as_slice()) 535 .and_then(|sig| { 536 ver_key.verify( 537 self.authenticator_data_and_c_data_hash.as_slice(), 538 &sig, 539 ) 540 }) 541 .map_err(|_e| AuthRespErr::Signature) 542 }), 543 CompressedPubKeyBorrowed::Rsa(k) => { 544 pkcs1v15::Signature::try_from(self.signature.as_slice()) 545 .and_then(|sig| { 546 k.as_ver_key().verify( 547 self.authenticator_data_and_c_data_hash.as_slice(), 548 &sig, 549 ) 550 }) 551 .map_err(|_e| AuthRespErr::Signature) 552 } 553 } 554 .map(|()| (client_data_json, val.data)) 555 }) 556 }) 557 } 558 } 559 /// `AuthenticatorAssertion` with a required `UserHandle`. 560 pub type DiscoverableAuthenticatorAssertion<const USER_LEN: usize> = 561 AuthenticatorAssertion<USER_LEN, true>; 562 /// `AuthenticatorAssertion` with an optional `UserHandle`. 563 pub type NonDiscoverableAuthenticatorAssertion<const USER_LEN: usize> = 564 AuthenticatorAssertion<USER_LEN, false>; 565 impl<const USER_LEN: usize> From<DiscoverableAuthenticatorAssertion<USER_LEN>> 566 for NonDiscoverableAuthenticatorAssertion<USER_LEN> 567 { 568 #[inline] 569 fn from(value: DiscoverableAuthenticatorAssertion<USER_LEN>) -> Self { 570 Self { 571 client_data_json: value.client_data_json, 572 authenticator_data_and_c_data_hash: value.authenticator_data_and_c_data_hash, 573 signature: value.signature, 574 user_handle: value.user_handle, 575 } 576 } 577 } 578 impl<const USER_LEN: usize> TryFrom<NonDiscoverableAuthenticatorAssertion<USER_LEN>> 579 for DiscoverableAuthenticatorAssertion<USER_LEN> 580 { 581 type Error = MissingUserHandleErr; 582 #[inline] 583 fn try_from( 584 value: NonDiscoverableAuthenticatorAssertion<USER_LEN>, 585 ) -> Result<Self, MissingUserHandleErr> { 586 if value.user_handle.is_some() { 587 Ok(Self { 588 client_data_json: value.client_data_json, 589 authenticator_data_and_c_data_hash: value.authenticator_data_and_c_data_hash, 590 signature: value.signature, 591 user_handle: value.user_handle, 592 }) 593 } else { 594 Err(MissingUserHandleErr) 595 } 596 } 597 } 598 /// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for authentication ceremonies. 599 #[expect( 600 clippy::field_scoped_visibility_modifiers, 601 reason = "no invariants to uphold" 602 )] 603 #[derive(Debug)] 604 pub struct Authentication<const USER_LEN: usize, const DISCOVERABLE: bool> { 605 /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid). 606 pub(crate) raw_id: CredentialId<Box<[u8]>>, 607 /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response) 608 pub(crate) response: AuthenticatorAssertion<USER_LEN, DISCOVERABLE>, 609 /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). 610 pub(crate) authenticator_attachment: AuthenticatorAttachment, 611 } 612 impl<const USER_LEN: usize, const DISCOVERABLE: bool> Authentication<USER_LEN, DISCOVERABLE> { 613 /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid). 614 #[inline] 615 #[must_use] 616 pub fn raw_id(&self) -> CredentialId<&[u8]> { 617 (&self.raw_id).into() 618 } 619 /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response). 620 #[inline] 621 #[must_use] 622 pub const fn response(&self) -> &AuthenticatorAssertion<USER_LEN, DISCOVERABLE> { 623 &self.response 624 } 625 /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). 626 #[inline] 627 #[must_use] 628 pub const fn authenticator_attachment(&self) -> AuthenticatorAttachment { 629 self.authenticator_attachment 630 } 631 /// Constructs an `Authentication`. 632 #[cfg(feature = "custom")] 633 #[inline] 634 #[must_use] 635 pub const fn new( 636 raw_id: CredentialId<Box<[u8]>>, 637 response: AuthenticatorAssertion<USER_LEN, DISCOVERABLE>, 638 authenticator_attachment: AuthenticatorAttachment, 639 ) -> Self { 640 Self { 641 raw_id, 642 response, 643 authenticator_attachment, 644 } 645 } 646 /// Returns the associated `SentChallenge`. 647 /// 648 /// This is useful when wanting to extract the corresponding [`DiscoverableAuthenticationServerState`] 649 /// or [`NonDiscoverableAuthenticationServerState`] from an in-memory collection (e.g., [`MaxLenHashSet`]) or 650 /// storage. 651 /// 652 /// Note if [`CollectedClientData::from_client_data_json`] returns `Ok`, then this will return `Ok` 653 /// containing the same value as [`CollectedClientData::challenge`]; however the converse is _not_ true. 654 /// This is because this function parses the minimal amount of data possible. 655 /// 656 /// # Errors 657 /// 658 /// Errors iff [`AuthenticatorAssertion::client_data_json`] does not contain a base64url-encoded 659 /// [`Challenge`] in the required position. 660 #[inline] 661 pub fn challenge(&self) -> Result<SentChallenge, CollectedClientDataErr> { 662 LimitedVerificationParser::<false>::get_sent_challenge( 663 self.response.client_data_json.as_slice(), 664 ) 665 } 666 /// Returns the associated `SentChallenge`. 667 /// 668 /// This is useful when wanting to extract the corresponding [`DiscoverableAuthenticationServerState`] 669 /// or [`NonDiscoverableAuthenticationServerState`] from an in-memory collection (e.g., 670 /// [`MaxLenHashSet`]) or storage. 671 /// 672 /// Note if [`CollectedClientData::from_client_data_json_relaxed`] returns `Ok`, then this will return 673 /// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse 674 /// is _not_ true. This is because this function attempts to reduce the amount of data parsed. 675 /// 676 /// # Errors 677 /// 678 /// Errors iff [`AuthenticatorAssertion::client_data_json`] is invalid JSON _after_ ignoring 679 /// a leading U+FEFF and replacing any sequences of invalid UTF-8 code units with U+FFFD or 680 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) does not exist 681 /// or is not a base64url-encoded [`Challenge`]. 682 #[cfg(feature = "serde_relaxed")] 683 #[inline] 684 pub fn challenge_relaxed(&self) -> Result<SentChallenge, SerdeJsonErr> { 685 RelaxedClientDataJsonParser::<false>::get_sent_challenge( 686 self.response.client_data_json.as_slice(), 687 ) 688 } 689 /// Convenience function for [`AuthenticationRelaxed::deserialize`]. 690 /// 691 /// # Errors 692 /// 693 /// Errors iff [`AuthenticationRelaxed::deserialize`] does. 694 #[cfg(feature = "serde_relaxed")] 695 #[inline] 696 pub fn from_json_relaxed<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr> 697 where 698 UserHandle<USER_LEN>: Deserialize<'a>, 699 { 700 serde_json::from_slice::<AuthenticationRelaxed<USER_LEN, DISCOVERABLE>>(json) 701 .map(|val| val.0) 702 } 703 /// Convenience function for [`CustomAuthentication::deserialize`]. 704 /// 705 /// # Errors 706 /// 707 /// Errors iff [`CustomAuthentication::deserialize`] does. 708 #[cfg(feature = "serde_relaxed")] 709 #[inline] 710 pub fn from_json_custom<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr> 711 where 712 UserHandle<USER_LEN>: Deserialize<'a>, 713 { 714 serde_json::from_slice::<CustomAuthentication<USER_LEN, DISCOVERABLE>>(json) 715 .map(|val| val.0) 716 } 717 } 718 impl<const USER_LEN: usize, const DISCOVERABLE: bool> Response 719 for Authentication<USER_LEN, DISCOVERABLE> 720 { 721 type Auth = AuthenticatorAssertion<USER_LEN, DISCOVERABLE>; 722 fn auth(&self) -> &Self::Auth { 723 &self.response 724 } 725 } 726 /// `Authentication` with a required [`UserHandle`]. 727 pub type DiscoverableAuthentication<const USER_LEN: usize> = Authentication<USER_LEN, true>; 728 /// `Authentication` with a required [`UserHandle64`]. 729 pub type DiscoverableAuthentication64 = Authentication<USER_HANDLE_MAX_LEN, true>; 730 /// `Authentication` with a required [`UserHandle16`]. 731 pub type DiscoverableAuthentication16 = Authentication<16, true>; 732 /// `Authentication` with an optional [`UserHandle`]. 733 pub type NonDiscoverableAuthentication<const USER_LEN: usize> = Authentication<USER_LEN, false>; 734 impl<const USER_LEN: usize> From<DiscoverableAuthentication<USER_LEN>> 735 for NonDiscoverableAuthentication<USER_LEN> 736 { 737 #[inline] 738 fn from(value: DiscoverableAuthentication<USER_LEN>) -> Self { 739 Self { 740 raw_id: value.raw_id, 741 response: value.response.into(), 742 authenticator_attachment: value.authenticator_attachment, 743 } 744 } 745 } 746 impl<const USER_LEN: usize> TryFrom<NonDiscoverableAuthentication<USER_LEN>> 747 for DiscoverableAuthentication<USER_LEN> 748 { 749 type Error = MissingUserHandleErr; 750 #[inline] 751 fn try_from( 752 value: NonDiscoverableAuthentication<USER_LEN>, 753 ) -> Result<Self, MissingUserHandleErr> { 754 value.response.try_into().map(|response| Self { 755 raw_id: value.raw_id, 756 response, 757 authenticator_attachment: value.authenticator_attachment, 758 }) 759 } 760 } 761 /// `Authentication` with an optional [`UserHandle64`]. 762 pub type NonDiscoverableAuthentication64 = Authentication<USER_HANDLE_MAX_LEN, false>; 763 /// `Authentication` with an optional [`UserHandle16`]. 764 pub type NonDiscoverableAuthentication16 = Authentication<16, false>;