auth.rs (23202B)
1 #[cfg(all(doc, feature = "serde_relaxed"))] 2 use super::super::request::FixedCapHashSet; 3 #[cfg(doc)] 4 use super::super::{ 5 request::{ 6 auth::{AuthenticationServerState, PublicKeyCredentialRequestOptions}, 7 Challenge, 8 }, 9 AuthenticatedCredential, RegisteredCredential, StaticState, 10 }; 11 #[cfg(feature = "serde_relaxed")] 12 use super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}; 13 use super::{ 14 super::UserHandle, 15 auth::error::{AuthenticatorDataErr, AuthenticatorExtensionOutputErr}, 16 cbor, 17 error::CollectedClientDataErr, 18 register::CompressedPubKey, 19 AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment, 20 CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor, 21 LimitedVerificationParser, ParsedAuthData, Response, SentChallenge, 22 }; 23 use core::convert::Infallible; 24 use ed25519_dalek::{Signature, Verifier as _}; 25 use p256::ecdsa::DerSignature as P256DerSig; 26 use p384::ecdsa::DerSignature as P384DerSig; 27 use rsa::{ 28 pkcs1v15, 29 sha2::{digest::Digest as _, Sha256}, 30 }; 31 /// Contains error types. 32 pub mod error; 33 /// Contains functionality to deserialize data from a client. 34 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 35 #[cfg(feature = "serde")] 36 pub(super) mod ser; 37 /// Contains functionality to deserialize data from a client in a "relaxed" way. 38 #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] 39 #[cfg(feature = "serde_relaxed")] 40 pub mod ser_relaxed; 41 /// [`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). 42 #[derive(Clone, Copy, Debug)] 43 pub enum HmacSecret { 44 /// No `hmac-secret` response. 45 None, 46 /// One encrypted `hmac-secret`. 47 One, 48 /// Two encrypted `hmac-secret`s. 49 Two, 50 } 51 impl FromCbor<'_> for HmacSecret { 52 type Err = AuthenticatorExtensionOutputErr; 53 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 54 /// AES block size. 55 const AES_BLOCK_SIZE: usize = 16; 56 /// HMAC-SHA-256 output length. 57 const HMAC_SHA_256_LEN: usize = 32; 58 /// Length of two HMAC-SHA-256 outputs concatenated together. 59 const TWO_HMAC_SHA_256_LEN: usize = HMAC_SHA_256_LEN << 1; 60 // We need the smallest multiple of `AES_BLOCK_SIZE` that 61 // is strictly greater than `HMAC_SHA_256_LEN`. 62 /// AES-256 output length on a 32-byte input. 63 #[expect( 64 clippy::integer_division_remainder_used, 65 reason = "doesn't need to be constant time" 66 )] 67 const ONE_SECRET_LEN: usize = 68 HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (HMAC_SHA_256_LEN % AES_BLOCK_SIZE)); 69 // We need the smallest multiple of `AES_BLOCK_SIZE` that 70 // is strictly greater than `TWO_HMAC_SHA_256_LEN`. 71 /// AES-256 output length on a 64-byte input. 72 #[expect( 73 clippy::integer_division_remainder_used, 74 reason = "doesn't need to be constant time" 75 )] 76 const TWO_SECRET_LEN: usize = 77 TWO_HMAC_SHA_256_LEN + (AES_BLOCK_SIZE - (TWO_HMAC_SHA_256_LEN % AES_BLOCK_SIZE)); 78 cbor.split_at_checked(cbor::HMAC_SECRET.len()) 79 .ok_or(AuthenticatorExtensionOutputErr::Len) 80 .and_then(|(key, key_rem)| { 81 if key == cbor::HMAC_SECRET { 82 key_rem 83 .split_first() 84 .ok_or(AuthenticatorExtensionOutputErr::Len) 85 .and_then(|(bytes, bytes_rem)| { 86 if *bytes == cbor::BYTES_INFO_24 { 87 bytes_rem 88 .split_first() 89 .ok_or(AuthenticatorExtensionOutputErr::Len) 90 .and_then(|(&len, len_rem)| { 91 len_rem.split_at_checked(usize::from(len)).ok_or(AuthenticatorExtensionOutputErr::Len).and_then(|(_, remaining)| { 92 match usize::from(len) { 93 ONE_SECRET_LEN => { 94 Ok(CborSuccess { 95 value: Self::One, 96 remaining, 97 }) 98 } 99 TWO_SECRET_LEN => { 100 Ok(CborSuccess { 101 value: Self::Two, 102 remaining, 103 }) 104 } 105 _ => Err(AuthenticatorExtensionOutputErr::HmacSecretValue), 106 } 107 }) 108 }) 109 } else { 110 Err(AuthenticatorExtensionOutputErr::HmacSecretType) 111 } 112 }) 113 } else { 114 Err(AuthenticatorExtensionOutputErr::Unsupported) 115 } 116 }) 117 } 118 } 119 /// [Authenticator extension output](https://www.w3.org/TR/webauthn-3/#authenticator-extension-output). 120 #[derive(Clone, Copy, Debug)] 121 pub struct AuthenticatorExtensionOutput { 122 /// [`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). 123 pub hmac_secret: HmacSecret, 124 } 125 impl AuthExtOutput for AuthenticatorExtensionOutput { 126 fn missing(self) -> bool { 127 matches!(self.hmac_secret, HmacSecret::None) 128 } 129 } 130 impl FromCbor<'_> for AuthenticatorExtensionOutput { 131 type Err = AuthenticatorExtensionOutputErr; 132 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 133 // We don't allow unsupported extensions; thus the only possibilities is any ordered element of 134 // the power set of {"hmac-secret":<HmacSecret>}. 135 cbor.split_first().map_or_else( 136 || { 137 Ok(CborSuccess { 138 value: Self { 139 hmac_secret: HmacSecret::None, 140 }, 141 remaining: cbor, 142 }) 143 }, 144 |(map, map_rem)| { 145 if *map == cbor::MAP_1 { 146 HmacSecret::from_cbor(map_rem).map(|success| CborSuccess { 147 value: Self { 148 hmac_secret: success.value, 149 }, 150 remaining: success.remaining, 151 }) 152 } else { 153 Err(AuthenticatorExtensionOutputErr::CborHeader) 154 } 155 }, 156 ) 157 } 158 } 159 /// Unit type for `AuthData::CredData`. 160 pub(crate) struct NoCred; 161 impl FromCbor<'_> for NoCred { 162 type Err = Infallible; 163 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 164 Ok(CborSuccess { 165 value: Self, 166 remaining: cbor, 167 }) 168 } 169 } 170 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). 171 #[derive(Clone, Copy, Debug)] 172 pub struct AuthenticatorData<'a> { 173 /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash). 174 rp_id_hash: &'a [u8], 175 /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags). 176 flags: Flag, 177 /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount). 178 sign_count: u32, 179 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions). 180 extensions: AuthenticatorExtensionOutput, 181 } 182 impl<'a> AuthenticatorData<'a> { 183 /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash). 184 #[inline] 185 #[must_use] 186 pub const fn rp_id_hash(&self) -> &'a [u8] { 187 self.rp_id_hash 188 } 189 /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags). 190 #[inline] 191 #[must_use] 192 pub const fn flags(&self) -> Flag { 193 self.flags 194 } 195 /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount). 196 #[inline] 197 #[must_use] 198 pub const fn sign_count(&self) -> u32 { 199 self.sign_count 200 } 201 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions). 202 #[inline] 203 #[must_use] 204 pub const fn extensions(&self) -> AuthenticatorExtensionOutput { 205 self.extensions 206 } 207 } 208 impl<'a> AuthData<'a> for AuthenticatorData<'a> { 209 type UpBitErr = (); 210 type CredData = NoCred; 211 type Ext = AuthenticatorExtensionOutput; 212 fn contains_at_bit() -> bool { 213 false 214 } 215 fn user_is_not_present() -> Result<(), Self::UpBitErr> { 216 Err(()) 217 } 218 fn new( 219 rp_id_hash: &'a [u8], 220 flags: Flag, 221 sign_count: u32, 222 _: Self::CredData, 223 extensions: Self::Ext, 224 ) -> Self { 225 Self { 226 rp_id_hash, 227 flags, 228 sign_count, 229 extensions, 230 } 231 } 232 fn rp_hash(&self) -> &'a [u8] { 233 self.rp_id_hash 234 } 235 fn flag(&self) -> Flag { 236 self.flags 237 } 238 } 239 impl<'a> AuthDataContainer<'a> for AuthenticatorData<'a> { 240 type Auth = Self; 241 type Err = AuthenticatorDataErr; 242 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 243 #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")] 244 fn from_data(data: &'a [u8]) -> Result<ParsedAuthData<'a, Self>, Self::Err> { 245 // `data.len().checked_sub(Sha256::output_size()).unwrap()` is less than `data.len()`, 246 // so indexing is fine. 247 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, }) 248 } 249 fn authenticator_data(&self) -> &Self::Auth { 250 self 251 } 252 } 253 impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> { 254 type Error = AuthenticatorDataErr; 255 /// Deserializes `value` based on the 256 /// [authenticator data structure](https://www.w3.org/TR/webauthn-3/#table-authData). 257 #[expect( 258 clippy::panic_in_result_fn, 259 reason = "we want to crash when there is a bug" 260 )] 261 #[inline] 262 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { 263 Self::from_cbor(value) 264 .map_err(AuthenticatorDataErr::from) 265 .map(|auth_data| { 266 assert!( 267 auth_data.remaining.is_empty(), 268 "there is a bug in AuthenticatorData::from_cbor" 269 ); 270 auth_data.value 271 }) 272 } 273 } 274 /// [`AuthenticatorAssertionResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse). 275 #[derive(Debug)] 276 pub struct AuthenticatorAssertion { 277 /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). 278 client_data_json: Vec<u8>, 279 /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata) 280 /// followed by the SHA-256 hash of [`Self::client_data_json`]. 281 authenticator_data_and_c_data_hash: Vec<u8>, 282 /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature). 283 signature: Vec<u8>, 284 /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle). 285 user_handle: Option<UserHandle<Vec<u8>>>, 286 } 287 impl AuthenticatorAssertion { 288 /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). 289 #[inline] 290 #[must_use] 291 pub fn client_data_json(&self) -> &[u8] { 292 self.client_data_json.as_slice() 293 } 294 /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata). 295 #[expect( 296 clippy::arithmetic_side_effects, 297 clippy::indexing_slicing, 298 reason = "comment justifies their correctness" 299 )] 300 #[inline] 301 #[must_use] 302 pub fn authenticator_data(&self) -> &[u8] { 303 // We only allow creation via [`Self::new`] which creates [`Self::authenticator_data_and_c_data_hash`] 304 // by appending the SHA-256 hash of [`Self::client_data_json`] to the authenticator data that was passed; 305 // thus indexing is fine and subtraction won't cause underflow. 306 &self.authenticator_data_and_c_data_hash 307 [..self.authenticator_data_and_c_data_hash.len() - Sha256::output_size()] 308 } 309 /// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature). 310 #[inline] 311 #[must_use] 312 pub fn signature(&self) -> &[u8] { 313 self.signature.as_slice() 314 } 315 /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle). 316 #[inline] 317 #[must_use] 318 pub fn user_handle(&self) -> Option<UserHandle<&[u8]>> { 319 self.user_handle.as_ref().map(UserHandle::from) 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 new( 328 client_data_json: Vec<u8>, 329 mut authenticator_data: Vec<u8>, 330 signature: Vec<u8>, 331 user_handle: Option<UserHandle<Vec<u8>>>, 332 ) -> Self { 333 authenticator_data 334 .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); 335 Self { 336 client_data_json, 337 authenticator_data_and_c_data_hash: authenticator_data, 338 signature, 339 user_handle, 340 } 341 } 342 } 343 impl AuthResponse for AuthenticatorAssertion { 344 type Auth<'a> 345 = AuthenticatorData<'a> 346 where 347 Self: 'a; 348 type CredKey<'a> = CompressedPubKey<&'a [u8], &'a [u8], &'a [u8], &'a [u8]>; 349 fn parse_data_and_verify_sig( 350 &self, 351 key: Self::CredKey<'_>, 352 relaxed: bool, 353 ) -> Result< 354 (CollectedClientData<'_>, Self::Auth<'_>), 355 AuthRespErr<<Self::Auth<'_> as AuthDataContainer<'_>>::Err>, 356 > { 357 /// Always `panic`s. 358 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 359 #[cfg(not(feature = "serde_relaxed"))] 360 fn get_client_collected_data(_: &[u8]) -> ! { 361 unreachable!("AuthenticatorAssertion::parse_data_and_verify_sig must be passed false when serde_relaxed is not enabled"); 362 } 363 /// Parses `data` using `CollectedClientData::from_client_data_json_relaxed::<false>`. 364 #[cfg(feature = "serde_relaxed")] 365 fn get_client_collected_data( 366 data: &[u8], 367 ) -> Result< 368 CollectedClientData<'_>, 369 AuthRespErr< 370 <<AuthenticatorAssertion as AuthResponse>::Auth<'_> as AuthDataContainer<'_>>::Err, 371 >, 372 > { 373 CollectedClientData::from_client_data_json_relaxed::<false>(data) 374 .map_err(AuthRespErr::CollectedClientDataRelaxed) 375 } 376 if relaxed { 377 get_client_collected_data(self.client_data_json.as_slice()) 378 } else { 379 CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice()) 380 .map_err(AuthRespErr::CollectedClientData) 381 } 382 .and_then(|client_data_json| { 383 Self::Auth::from_data(self.authenticator_data_and_c_data_hash.as_slice()) 384 .map_err(AuthRespErr::Auth) 385 .and_then(|val| { 386 match key { 387 CompressedPubKey::Ed25519(k) => k 388 .into_ver_key() 389 .map_err(AuthRespErr::PubKey) 390 .and_then(|ver_key| { 391 Signature::from_slice(self.signature.as_slice()) 392 .and_then(|sig| { 393 ver_key.verify( 394 self.authenticator_data_and_c_data_hash.as_slice(), 395 &sig, 396 ) 397 }) 398 .map_err(|_e| AuthRespErr::Signature) 399 }), 400 CompressedPubKey::P256(k) => k 401 .into_ver_key() 402 .map_err(AuthRespErr::PubKey) 403 .and_then(|ver_key| { 404 P256DerSig::from_bytes(self.signature.as_slice()) 405 .and_then(|sig| { 406 ver_key.verify( 407 self.authenticator_data_and_c_data_hash.as_slice(), 408 &sig, 409 ) 410 }) 411 .map_err(|_e| AuthRespErr::Signature) 412 }), 413 CompressedPubKey::P384(k) => k 414 .into_ver_key() 415 .map_err(AuthRespErr::PubKey) 416 .and_then(|ver_key| { 417 P384DerSig::from_bytes(self.signature.as_slice()) 418 .and_then(|sig| { 419 ver_key.verify( 420 self.authenticator_data_and_c_data_hash.as_slice(), 421 &sig, 422 ) 423 }) 424 .map_err(|_e| AuthRespErr::Signature) 425 }), 426 CompressedPubKey::Rsa(k) => k 427 .as_ver_key() 428 .map_err(AuthRespErr::PubKey) 429 .and_then(|ver_key| { 430 pkcs1v15::Signature::try_from(self.signature.as_slice()) 431 .and_then(|sig| { 432 ver_key.verify( 433 self.authenticator_data_and_c_data_hash.as_slice(), 434 &sig, 435 ) 436 }) 437 .map_err(|_e| AuthRespErr::Signature) 438 }), 439 } 440 .map(|()| (client_data_json, val.data)) 441 }) 442 }) 443 } 444 } 445 /// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for authentication ceremonies. 446 #[expect( 447 clippy::field_scoped_visibility_modifiers, 448 reason = "no invariants to uphold" 449 )] 450 #[derive(Debug)] 451 pub struct Authentication { 452 /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid). 453 pub(crate) raw_id: CredentialId<Vec<u8>>, 454 /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response) 455 pub(crate) response: AuthenticatorAssertion, 456 /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). 457 pub(crate) authenticator_attachment: AuthenticatorAttachment, 458 } 459 impl Authentication { 460 /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid). 461 #[inline] 462 #[must_use] 463 pub fn raw_id(&self) -> CredentialId<&[u8]> { 464 (&self.raw_id).into() 465 } 466 /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response). 467 #[inline] 468 #[must_use] 469 pub const fn response(&self) -> &AuthenticatorAssertion { 470 &self.response 471 } 472 /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). 473 #[inline] 474 #[must_use] 475 pub const fn authenticator_attachment(&self) -> AuthenticatorAttachment { 476 self.authenticator_attachment 477 } 478 /// Constructs an `Authentication`. 479 #[cfg_attr(docsrs, doc(cfg(feature = "custom")))] 480 #[cfg(feature = "custom")] 481 #[inline] 482 #[must_use] 483 pub const fn new( 484 raw_id: CredentialId<Vec<u8>>, 485 response: AuthenticatorAssertion, 486 authenticator_attachment: AuthenticatorAttachment, 487 ) -> Self { 488 Self { 489 raw_id, 490 response, 491 authenticator_attachment, 492 } 493 } 494 /// Returns the associated `SentChallenge`. 495 /// 496 /// This is useful when wanting to extract the corresponding [`AuthenticationServerState`] from 497 /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage. 498 /// 499 /// Note if [`CollectedClientData::from_client_data_json`] returns `Ok`, then this will return `Ok` 500 /// containing the same value as [`CollectedClientData::challenge`]; however the converse is _not_ true. 501 /// This is because this function parses the minimal amount of data possible. 502 /// 503 /// # Errors 504 /// 505 /// Errors iff [`AuthenticatorAssertion::client_data_json`] does not contain a base64url-encoded 506 /// [`Challenge`] in the required position. 507 #[inline] 508 pub fn challenge(&self) -> Result<SentChallenge, CollectedClientDataErr> { 509 LimitedVerificationParser::<false>::get_sent_challenge( 510 self.response.client_data_json.as_slice(), 511 ) 512 } 513 /// Returns the associated `SentChallenge`. 514 /// 515 /// This is useful when wanting to extract the corresponding [`AuthenticationServerState`] from 516 /// an in-memory collection (e.g., [`FixedCapHashSet`]) or storage. 517 /// 518 /// Note if [`CollectedClientData::from_client_data_json_relaxed`] returns `Ok`, then this will return 519 /// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse 520 /// is _not_ true. This is because this function attempts to reduce the amount of data parsed. 521 /// 522 /// # Errors 523 /// 524 /// Errors iff [`AuthenticatorAssertion::client_data_json`] is invalid JSON _after_ ignoring 525 /// a leading U+FEFF and replacing any sequences of invalid UTF-8 code units with U+FFFD or 526 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) does not exist 527 /// or is not a base64url-encoded [`Challenge`]. 528 #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] 529 #[cfg(feature = "serde_relaxed")] 530 #[inline] 531 pub fn challenge_relaxed(&self) -> Result<SentChallenge, SerdeJsonErr> { 532 RelaxedClientDataJsonParser::<false>::get_sent_challenge( 533 self.response.client_data_json.as_slice(), 534 ) 535 } 536 } 537 impl Response for Authentication { 538 type Auth = AuthenticatorAssertion; 539 fn auth(&self) -> &Self::Auth { 540 &self.response 541 } 542 }