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