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