register.rs (182231B)
1 #[cfg(feature = "serde_relaxed")] 2 use self::{ 3 super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr}, 4 ser_relaxed::{CustomRegistration, RegistrationRelaxed}, 5 }; 6 #[cfg(all(doc, feature = "bin"))] 7 use super::super::bin::{Decode, Encode}; 8 #[cfg(feature = "bin")] 9 use super::register::bin::{AaguidOwned, MetadataOwned}; 10 use super::{ 11 super::request::register::{FourToSixtyThree, ResidentKeyRequirement}, 12 AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthTransports, 13 AuthenticatorAttachment, Backup, CborSuccess, ClientDataJsonParser as _, CollectedClientData, 14 CredentialId, Flag, FromCbor, HmacSecretGet, HmacSecretGetErr, LimitedVerificationParser, 15 ParsedAuthData, Response, SentChallenge, cbor, 16 error::CollectedClientDataErr, 17 register::error::{ 18 AaguidErr, AttestationErr, AttestationObjectErr, AttestedCredentialDataErr, 19 AuthenticatorDataErr, AuthenticatorExtensionOutputErr, CompressedP256PubKeyErr, 20 CompressedP384PubKeyErr, CoseKeyErr, Ed25519PubKeyErr, Ed25519SignatureErr, PubKeyErr, 21 RsaPubKeyErr, UncompressedP256PubKeyErr, UncompressedP384PubKeyErr, 22 }, 23 }; 24 #[cfg(doc)] 25 use super::{ 26 super::{ 27 AuthenticatedCredential, RegisteredCredential, 28 hash::hash_set::MaxLenHashSet, 29 request::{ 30 BackupReq, Challenge, UserVerificationRequirement, 31 auth::{AuthenticationVerificationOptions, PublicKeyCredentialRequestOptions}, 32 register::{CoseAlgorithmIdentifier, Extension, RegistrationServerState}, 33 }, 34 }, 35 AuthenticatorTransport, 36 }; 37 use core::{ 38 cmp::Ordering, 39 convert::Infallible, 40 fmt::{self, Display, Formatter}, 41 }; 42 use ed25519_dalek::{Signature, Verifier as _, VerifyingKey}; 43 use p256::{ 44 AffinePoint as P256Affine, EncodedPoint as P256Pt, NistP256, 45 ecdsa::{DerSignature as P256Sig, VerifyingKey as P256VerKey}, 46 elliptic_curve::{Curve, generic_array::typenum::ToInt as _, point::DecompressPoint as _}, 47 }; 48 use p384::{ 49 AffinePoint as P384Affine, EncodedPoint as P384Pt, NistP384, 50 ecdsa::{DerSignature as P384Sig, VerifyingKey as P384VerKey}, 51 }; 52 use rsa::{ 53 BigUint, RsaPublicKey, 54 pkcs1v15::{self, VerifyingKey as RsaVerKey}, 55 sha2::{Sha256, digest::Digest as _}, 56 }; 57 #[cfg(all(doc, feature = "serde_relaxed"))] 58 use serde::Deserialize; 59 /// Contains functionality to (de)serialize data to a data store. 60 #[cfg(feature = "bin")] 61 pub mod bin; 62 /// Contains error types. 63 pub mod error; 64 /// Contains functionality to deserialize data from a client. 65 #[cfg(feature = "serde")] 66 mod ser; 67 /// Contains functionality to deserialize data from a client in a "relaxed" way. 68 #[cfg(feature = "serde_relaxed")] 69 pub mod ser_relaxed; 70 /// [`credentialProtectionPolicy`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#dom-authenticationextensionsclientinputs-credentialprotectionpolicy). 71 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 72 pub enum CredentialProtectionPolicy { 73 /// `credProtect` was not sent. 74 None, 75 /// [`userVerificationOptional`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptional). 76 UserVerificationOptional, 77 /// [`userVerificationOptionalWithCredentialIDList`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptionalwithcredentialidlist). 78 UserVerificationOptionalWithCredentialIdList, 79 /// [`userVerificationRequired`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationrequired). 80 UserVerificationRequired, 81 } 82 impl Display for CredentialProtectionPolicy { 83 #[inline] 84 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 85 f.write_str(match *self { 86 Self::None => "no credential protection policy sent", 87 Self::UserVerificationOptional => "user verification optional", 88 Self::UserVerificationOptionalWithCredentialIdList => { 89 "user verification optional with credential ID list" 90 } 91 Self::UserVerificationRequired => "user verification required", 92 }) 93 } 94 } 95 #[expect(clippy::too_long_first_doc_paragraph, reason = "false positive")] 96 /// [`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) 97 /// and 98 /// [`hmac-secret-mc`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-make-cred-extension). 99 /// 100 /// Note `hmac-secret-mc` can only exist if `hmac-secret` exists with a value of `true`. 101 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 102 pub enum HmacSecret { 103 /// No `hmac-secret` extension. 104 None, 105 /// `hmac-secret` extension with a value of `false`. 106 NotEnabled, 107 /// `hmac-secret` extension with a value of `true`. 108 Enabled, 109 /// `hmac-secret` extension with a value of `true` and `hmac-secret-mc` contained one encrypted PRF output. 110 One, 111 /// `hmac-secret` extension with a value of `true` and `hmac-secret-mc` contained two encrypted PRF outputs. 112 Two, 113 } 114 /// [Authenticator extension output](https://www.w3.org/TR/webauthn-3/#authenticator-extension-output). 115 #[derive(Clone, Copy, Debug)] 116 pub struct AuthenticatorExtensionOutput { 117 /// [`credProtect`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension). 118 pub cred_protect: CredentialProtectionPolicy, 119 /// [`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) 120 /// and 121 /// [`hmac-secret-mc`](https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-make-cred-extension). 122 pub hmac_secret: HmacSecret, 123 /// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension). 124 pub min_pin_length: Option<FourToSixtyThree>, 125 } 126 impl AuthExtOutput for AuthenticatorExtensionOutput { 127 fn missing(self) -> bool { 128 matches!(self.cred_protect, CredentialProtectionPolicy::None) 129 && matches!(self.hmac_secret, HmacSecret::None) 130 && self.min_pin_length.is_none() 131 } 132 } 133 /// [`AuthenticatorExtensionOutput`] extensions that are saved in [`StaticState`] because they are used during 134 /// authentication ceremonies. 135 #[derive(Clone, Copy, Debug)] 136 pub struct AuthenticatorExtensionOutputStaticState { 137 /// [`AuthenticatorExtensionOutput::cred_protect`]. 138 pub cred_protect: CredentialProtectionPolicy, 139 /// [`AuthenticatorExtensionOutput::hmac_secret`]. 140 /// 141 /// Note we only care about whether or not it has been enabled. Specifcally this is `None` iff 142 /// [`HmacSecret::None`], `Some(false)` iff [`HmacSecret::NotEnabled`]; otherwise `Some(true)`. 143 pub hmac_secret: Option<bool>, 144 } 145 /// [`AuthenticatorExtensionOutput`] extensions that are saved in [`Metadata`] because they are purely informative 146 /// and not used during authentication ceremonies. 147 #[derive(Clone, Copy, Debug)] 148 pub struct AuthenticatorExtensionOutputMetadata { 149 /// [`AuthenticatorExtensionOutput::min_pin_length`]. 150 pub min_pin_length: Option<FourToSixtyThree>, 151 } 152 impl From<AuthenticatorExtensionOutput> for AuthenticatorExtensionOutputMetadata { 153 #[inline] 154 fn from(value: AuthenticatorExtensionOutput) -> Self { 155 Self { 156 min_pin_length: value.min_pin_length, 157 } 158 } 159 } 160 impl From<AuthenticatorExtensionOutput> for AuthenticatorExtensionOutputStaticState { 161 #[inline] 162 fn from(value: AuthenticatorExtensionOutput) -> Self { 163 Self { 164 cred_protect: value.cred_protect, 165 hmac_secret: match value.hmac_secret { 166 HmacSecret::None => None, 167 HmacSecret::NotEnabled => Some(false), 168 HmacSecret::Enabled | HmacSecret::One | HmacSecret::Two => Some(true), 169 }, 170 } 171 } 172 } 173 impl FromCbor<'_> for CredentialProtectionPolicy { 174 type Err = AuthenticatorExtensionOutputErr; 175 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 176 /// [`userVerificationOptional`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptional). 177 const USER_VERIFICATION_OPTIONAL: u8 = cbor::ONE; 178 /// [`userVerificationOptionalWithCredentialIDList`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationoptionalwithcredentialidlist). 179 const USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST: u8 = cbor::TWO; 180 /// [`userVerificationRequired`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#userverificationrequired). 181 const USER_VERIFICATION_REQUIRED: u8 = cbor::THREE; 182 /// `credProtect` key. 183 const KEY: [u8; 12] = [ 184 cbor::TEXT_11, 185 b'c', 186 b'r', 187 b'e', 188 b'd', 189 b'P', 190 b'r', 191 b'o', 192 b't', 193 b'e', 194 b'c', 195 b't', 196 ]; 197 cbor.split_at_checked(KEY.len()).map_or( 198 Ok(CborSuccess { 199 value: Self::None, 200 remaining: cbor, 201 }), 202 |(key, key_rem)| { 203 if key == KEY { 204 key_rem 205 .split_first() 206 .ok_or(AuthenticatorExtensionOutputErr::Len) 207 .and_then(|(uv, remaining)| { 208 match *uv { 209 USER_VERIFICATION_OPTIONAL => Ok(Self::UserVerificationOptional), 210 USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST => { 211 Ok(Self::UserVerificationOptionalWithCredentialIdList) 212 } 213 USER_VERIFICATION_REQUIRED => Ok(Self::UserVerificationRequired), 214 _ => Err(AuthenticatorExtensionOutputErr::CredProtectValue), 215 } 216 .map(|value| CborSuccess { value, remaining }) 217 }) 218 } else { 219 Ok(CborSuccess { 220 value: Self::None, 221 remaining: cbor, 222 }) 223 } 224 }, 225 ) 226 } 227 } 228 /// [`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). 229 enum HmacSecretEnabled { 230 /// No `hmac-secret` extension. 231 None, 232 /// `hmac-secret` set to the contained `bool`. 233 Val(bool), 234 } 235 impl FromCbor<'_> for HmacSecretEnabled { 236 type Err = AuthenticatorExtensionOutputErr; 237 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 238 cbor.split_at_checked(cbor::HMAC_SECRET.len()).map_or( 239 Ok(CborSuccess { 240 value: Self::None, 241 remaining: cbor, 242 }), 243 |(key, key_rem)| { 244 if key == cbor::HMAC_SECRET { 245 key_rem 246 .split_first() 247 .ok_or(AuthenticatorExtensionOutputErr::Len) 248 .and_then(|(hmac, remaining)| { 249 match *hmac { 250 cbor::SIMPLE_FALSE => Ok(Self::Val(false)), 251 cbor::SIMPLE_TRUE => Ok(Self::Val(true)), 252 _ => Err(AuthenticatorExtensionOutputErr::HmacSecretValue), 253 } 254 .map(|value| CborSuccess { value, remaining }) 255 }) 256 } else { 257 Ok(CborSuccess { 258 value: Self::None, 259 remaining: cbor, 260 }) 261 } 262 }, 263 ) 264 } 265 } 266 /// [`minPinLength`](https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-minpinlength-extension). 267 enum MinPinLength { 268 /// No `minPinLength` extension. 269 None, 270 /// `minPinLength` with the value of the contained `FourToSixtyThree`. 271 Val(FourToSixtyThree), 272 } 273 impl FromCbor<'_> for MinPinLength { 274 type Err = AuthenticatorExtensionOutputErr; 275 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 276 /// `minPinLength` key. 277 const KEY: [u8; 13] = [ 278 cbor::TEXT_12, 279 b'm', 280 b'i', 281 b'n', 282 b'P', 283 b'i', 284 b'n', 285 b'L', 286 b'e', 287 b'n', 288 b'g', 289 b't', 290 b'h', 291 ]; 292 cbor.split_at_checked(KEY.len()).map_or( 293 Ok(CborSuccess { 294 value: Self::None, 295 remaining: cbor, 296 }), 297 |(key, key_rem)| { 298 if key == KEY { 299 key_rem 300 .split_first() 301 .ok_or(AuthenticatorExtensionOutputErr::Len) 302 .and_then(|(&key_len, remaining)| match key_len.cmp(&24) { 303 Ordering::Less => FourToSixtyThree::from_u8(key_len) 304 .ok_or(AuthenticatorExtensionOutputErr::MinPinLengthValue) 305 .map(|val| CborSuccess { 306 value: Self::Val(val), 307 remaining, 308 }), 309 Ordering::Equal => remaining 310 .split_first() 311 .ok_or(AuthenticatorExtensionOutputErr::Len) 312 .and_then(|(&key_24, rem)| { 313 if key_24 > 23 { 314 FourToSixtyThree::from_u8(key_24) 315 .ok_or( 316 AuthenticatorExtensionOutputErr::MinPinLengthValue, 317 ) 318 .map(|val| CborSuccess { 319 value: Self::Val(val), 320 remaining: rem, 321 }) 322 } else { 323 Err(AuthenticatorExtensionOutputErr::MinPinLengthValue) 324 } 325 }), 326 Ordering::Greater => { 327 Err(AuthenticatorExtensionOutputErr::MinPinLengthValue) 328 } 329 }) 330 } else { 331 Ok(CborSuccess { 332 value: Self::None, 333 remaining: cbor, 334 }) 335 } 336 }, 337 ) 338 } 339 } 340 impl From<HmacSecretGetErr> for AuthenticatorExtensionOutputErr { 341 #[inline] 342 fn from(value: HmacSecretGetErr) -> Self { 343 match value { 344 HmacSecretGetErr::Len => Self::Len, 345 HmacSecretGetErr::Type => Self::HmacSecretMcType, 346 HmacSecretGetErr::Value => Self::HmacSecretMcValue, 347 } 348 } 349 } 350 impl FromCbor<'_> for AuthenticatorExtensionOutput { 351 type Err = AuthenticatorExtensionOutputErr; 352 #[expect( 353 clippy::too_many_lines, 354 reason = "don't want to move logic into outer scope" 355 )] 356 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 357 // We don't allow unsupported extensions; thus the only possibilities is any ordered element of 358 // the power set of {"credProtect":<1, 2, or 3>, "hmac-secret":<true or false>, "minPinLength":<0-255>, "hmac-secret-mc":<48|80 bytes>}. 359 // Since the keys are the same type (text), order is first done based on length; and then 360 // byte-wise lexical order is followed; thus `credProtect` must come before `hmac-secret` which 361 // must come before `minPinLength` which comes before `hmac-secret-mc`. 362 // 363 // Note `hmac-secret-mc` can only exist if `hmac-secret` exists with a value of `true`. 364 cbor.split_first().map_or_else( 365 || { 366 Ok(CborSuccess { 367 value: Self { 368 cred_protect: CredentialProtectionPolicy::None, 369 hmac_secret: HmacSecret::None, 370 min_pin_length: None, 371 }, 372 remaining: cbor, 373 }) 374 }, 375 |(map, map_rem)| match *map { 376 cbor::MAP_1 => { 377 CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| { 378 if matches!(cred_success.value, CredentialProtectionPolicy::None) { 379 HmacSecretEnabled::from_cbor(cred_success.remaining).and_then( 380 |hmac_success| match hmac_success.value { 381 HmacSecretEnabled::None => MinPinLength::from_cbor( 382 hmac_success.remaining, 383 ) 384 .and_then(|pin_success| match pin_success.value { 385 MinPinLength::None => { 386 // We don't even bother checking for `HmacSecretGet` since 387 // it's only valid when `HmacSecretEnabled` exists with a value 388 // of `true`. 389 Err(AuthenticatorExtensionOutputErr::Missing) 390 } 391 MinPinLength::Val(min_pin_len) => Ok(CborSuccess { 392 value: Self { 393 cred_protect: cred_success.value, 394 hmac_secret: HmacSecret::None, 395 min_pin_length: Some(min_pin_len), 396 }, 397 remaining: pin_success.remaining, 398 }), 399 }), 400 HmacSecretEnabled::Val(hmac) => Ok(CborSuccess { 401 value: Self { 402 cred_protect: cred_success.value, 403 hmac_secret: if hmac { 404 HmacSecret::Enabled 405 } else { 406 HmacSecret::NotEnabled 407 }, 408 min_pin_length: None, 409 }, 410 remaining: hmac_success.remaining, 411 }), 412 }, 413 ) 414 } else { 415 Ok(CborSuccess { 416 value: Self { 417 cred_protect: cred_success.value, 418 hmac_secret: HmacSecret::None, 419 min_pin_length: None, 420 }, 421 remaining: cred_success.remaining, 422 }) 423 } 424 }) 425 } 426 cbor::MAP_2 => { 427 CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| { 428 if matches!(cred_success.value, CredentialProtectionPolicy::None) { 429 HmacSecretEnabled::from_cbor(cred_success.remaining).and_then( 430 |hmac_success| match hmac_success.value { 431 HmacSecretEnabled::None => { 432 // We don't even bother checking for `HmacSecretGet` since 433 // it's only valid when `HmacSecretEnabled` exists with a value 434 // of `true`. 435 Err(AuthenticatorExtensionOutputErr::Missing) 436 } 437 HmacSecretEnabled::Val(hmac) => MinPinLength::from_cbor( 438 hmac_success.remaining, 439 ) 440 .and_then(|pin_success| match pin_success.value { 441 MinPinLength::None => { 442 if hmac { 443 HmacSecretGet::<true>::from_cbor( 444 pin_success.remaining, 445 ) 446 .map_err(AuthenticatorExtensionOutputErr::from) 447 .and_then(|hmac_get| match hmac_get.value { 448 HmacSecretGet::None => Err( 449 AuthenticatorExtensionOutputErr::Missing, 450 ), 451 HmacSecretGet::One => Ok(CborSuccess { 452 value: Self { 453 cred_protect: cred_success.value, 454 hmac_secret: HmacSecret::One, 455 min_pin_length: None, 456 }, 457 remaining: hmac_get.remaining, 458 }), 459 HmacSecretGet::Two => Ok(CborSuccess { 460 value: Self { 461 cred_protect: cred_success.value, 462 hmac_secret: HmacSecret::Two, 463 min_pin_length: None, 464 }, 465 remaining: hmac_get.remaining, 466 }), 467 }) 468 } else { 469 // We don't even bother checking for `HmacSecretGet` since 470 // it's only valid when `HmacSecretEnabled` exists with a value 471 // of `true`. 472 Err(AuthenticatorExtensionOutputErr::Missing) 473 } 474 } 475 MinPinLength::Val(min_pin_len) => Ok(CborSuccess { 476 value: Self { 477 cred_protect: cred_success.value, 478 hmac_secret: if hmac { 479 HmacSecret::Enabled 480 } else { 481 HmacSecret::NotEnabled 482 }, 483 min_pin_length: Some(min_pin_len), 484 }, 485 remaining: pin_success.remaining, 486 }), 487 }), 488 }, 489 ) 490 } else { 491 HmacSecretEnabled::from_cbor(cred_success.remaining).and_then( 492 |hmac_success| match hmac_success.value { 493 HmacSecretEnabled::None => MinPinLength::from_cbor( 494 hmac_success.remaining, 495 ) 496 .and_then(|pin_success| match pin_success.value { 497 MinPinLength::None => { 498 // We don't even bother checking for `HmacSecretGet` since 499 // it's only valid when `HmacSecretEnabled` exists with a value 500 // of `true`. 501 Err(AuthenticatorExtensionOutputErr::Missing) 502 } 503 MinPinLength::Val(min_pin_len) => Ok(CborSuccess { 504 value: Self { 505 cred_protect: cred_success.value, 506 hmac_secret: HmacSecret::None, 507 min_pin_length: Some(min_pin_len), 508 }, 509 remaining: pin_success.remaining, 510 }), 511 }), 512 // We don't even bother checking for `HmacSecretGet` since 513 // it's only valid when `HmacSecretEnabled` exists with a value 514 // of `true`. 515 HmacSecretEnabled::Val(hmac) => Ok(CborSuccess { 516 value: Self { 517 cred_protect: cred_success.value, 518 hmac_secret: if hmac { 519 HmacSecret::Enabled 520 } else { 521 HmacSecret::NotEnabled 522 }, 523 min_pin_length: None, 524 }, 525 remaining: hmac_success.remaining, 526 }), 527 }, 528 ) 529 } 530 }) 531 } 532 cbor::MAP_3 => { 533 CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| { 534 if matches!(cred_success.value, CredentialProtectionPolicy::None) { 535 HmacSecretEnabled::from_cbor(cred_success.remaining).and_then( 536 |hmac_success| match hmac_success.value { 537 HmacSecretEnabled::None => { 538 Err(AuthenticatorExtensionOutputErr::Missing) 539 } 540 HmacSecretEnabled::Val(hmac) => { 541 if hmac { 542 MinPinLength::from_cbor( 543 hmac_success.remaining, 544 ) 545 .and_then(|pin_success| match pin_success.value { 546 MinPinLength::None => Err(AuthenticatorExtensionOutputErr::Missing), 547 MinPinLength::Val(min_pin_len) => HmacSecretGet::<true>::from_cbor(pin_success.remaining).map_err(AuthenticatorExtensionOutputErr::from).and_then(|hmac_get| { 548 match hmac_get.value { 549 HmacSecretGet::None => Err(AuthenticatorExtensionOutputErr::Missing), 550 HmacSecretGet::One => { 551 Ok(CborSuccess { 552 value: Self { 553 cred_protect: cred_success.value, 554 hmac_secret: HmacSecret::One, 555 min_pin_length: Some(min_pin_len), 556 }, 557 remaining: hmac_get.remaining, 558 }) 559 } 560 HmacSecretGet::Two => { 561 Ok(CborSuccess { 562 value: Self { 563 cred_protect: cred_success.value, 564 hmac_secret: HmacSecret::Two, 565 min_pin_length: Some(min_pin_len), 566 }, 567 remaining: hmac_get.remaining, 568 }) 569 } 570 } 571 }) 572 }) 573 } else { 574 // We don't even bother checking for `HmacSecretGet` since 575 // it's only valid when `HmacSecretEnabled` exists with a value 576 // of `true`. 577 Err(AuthenticatorExtensionOutputErr::Missing) 578 } 579 } 580 }, 581 ) 582 } else { 583 HmacSecretEnabled::from_cbor(cred_success.remaining).and_then( 584 |hmac_success| match hmac_success.value { 585 // We don't even bother checking for `HmacSecretGet` since 586 // it's only valid when `HmacSecretEnabled` exists with a value 587 // of `true`. 588 HmacSecretEnabled::None => Err(AuthenticatorExtensionOutputErr::Missing), 589 HmacSecretEnabled::Val(hmac) => { 590 MinPinLength::from_cbor(hmac_success.remaining).and_then(|pin_success| { 591 match pin_success.value { 592 MinPinLength::None => { 593 if hmac { 594 HmacSecretGet::<true>::from_cbor(pin_success.remaining).map_err(AuthenticatorExtensionOutputErr::from).and_then(|hmac_get| { 595 match hmac_get.value { 596 HmacSecretGet::None => Err(AuthenticatorExtensionOutputErr::Missing), 597 HmacSecretGet::One => { 598 Ok(CborSuccess { 599 value: Self { 600 cred_protect: cred_success.value, 601 hmac_secret: HmacSecret::One, 602 min_pin_length: None, 603 }, 604 remaining: hmac_get.remaining, 605 }) 606 } 607 HmacSecretGet::Two => { 608 Ok(CborSuccess { 609 value: Self { 610 cred_protect: cred_success.value, 611 hmac_secret: HmacSecret::Two, 612 min_pin_length: None, 613 }, 614 remaining: hmac_get.remaining, 615 }) 616 } 617 } 618 }) 619 } else { 620 // We don't even bother checking for `HmacSecretGet` since 621 // it's only valid when `HmacSecretEnabled` exists with a value 622 // of `true`. 623 Err(AuthenticatorExtensionOutputErr::Missing) 624 } 625 } 626 MinPinLength::Val(min_pin_len) => { 627 Ok(CborSuccess { 628 value: Self { 629 cred_protect: cred_success.value, 630 hmac_secret: if hmac { HmacSecret::Enabled } else { HmacSecret::NotEnabled }, 631 min_pin_length: Some(min_pin_len), 632 }, 633 remaining: pin_success.remaining, 634 }) 635 } 636 } 637 }) 638 } 639 } 640 ) 641 } 642 }) 643 } 644 cbor::MAP_4 => { 645 CredentialProtectionPolicy::from_cbor(map_rem).and_then(|cred_success| { 646 if matches!(cred_success.value, CredentialProtectionPolicy::None) { 647 Err(AuthenticatorExtensionOutputErr::Missing) 648 } else { 649 HmacSecretEnabled::from_cbor(cred_success.remaining).and_then( 650 |hmac_success| match hmac_success.value { 651 HmacSecretEnabled::None => { 652 Err(AuthenticatorExtensionOutputErr::Missing) 653 } 654 HmacSecretEnabled::Val(hmac) => { 655 if hmac { 656 MinPinLength::from_cbor( 657 hmac_success.remaining, 658 ) 659 .and_then(|pin_success| match pin_success.value { 660 MinPinLength::None => { 661 Err(AuthenticatorExtensionOutputErr::Missing) 662 } 663 MinPinLength::Val(min_pin_len) => HmacSecretGet::<true>::from_cbor(pin_success.remaining).map_err(AuthenticatorExtensionOutputErr::from).and_then(|hmac_get| { 664 match hmac_get.value { 665 HmacSecretGet::None => Err(AuthenticatorExtensionOutputErr::Missing), 666 HmacSecretGet::One => { 667 Ok(CborSuccess { 668 value: Self { 669 cred_protect: cred_success.value, 670 hmac_secret: HmacSecret::One, 671 min_pin_length: Some(min_pin_len), 672 }, 673 remaining: hmac_get.remaining, 674 }) 675 } 676 HmacSecretGet::Two => { 677 Ok(CborSuccess { 678 value: Self { 679 cred_protect: cred_success.value, 680 hmac_secret: HmacSecret::Two, 681 min_pin_length: Some(min_pin_len), 682 }, 683 remaining: hmac_get.remaining, 684 }) 685 } 686 } 687 }) 688 }) 689 } else { 690 // We don't even bother checking for `HmacSecretGet` since 691 // it's only valid when `HmacSecretEnabled` exists with a value 692 // of `true`. 693 Err(AuthenticatorExtensionOutputErr::Missing) 694 } 695 } 696 }, 697 ) 698 } 699 }) 700 } 701 _ => Err(AuthenticatorExtensionOutputErr::CborHeader), 702 }, 703 ) 704 } 705 } 706 /// 32-bytes representing an alleged Ed25519 public key (i.e., compressed y-coordinate). 707 #[derive(Clone, Copy, Debug)] 708 pub struct Ed25519PubKey<T>(T); 709 impl<T> Ed25519PubKey<T> { 710 /// Returns the contained data consuming `self`. 711 #[inline] 712 pub fn into_inner(self) -> T { 713 self.0 714 } 715 /// Returns the contained data. 716 #[inline] 717 pub const fn inner(&self) -> &T { 718 &self.0 719 } 720 } 721 impl<T: AsRef<[u8]>> Ed25519PubKey<T> { 722 /// Returns the compressed y-coordinate. 723 #[inline] 724 #[must_use] 725 pub fn compressed_y_coordinate(&self) -> &[u8] { 726 self.0.as_ref() 727 } 728 } 729 impl Ed25519PubKey<&[u8]> { 730 /// Validates `self` is in fact a valid Ed25519 public key. 731 /// 732 /// # Errors 733 /// 734 /// Errors iff `self` is not a valid Ed25519 public key. 735 #[inline] 736 pub fn validate(self) -> Result<(), PubKeyErr> { 737 self.into_ver_key().map(|_| ()) 738 } 739 /// Converts `self` into [`VerifyingKey`]. 740 pub(super) fn into_ver_key(self) -> Result<VerifyingKey, PubKeyErr> { 741 self.into_owned().into_ver_key() 742 } 743 /// Transforms `self` into an "owned" version. 744 #[inline] 745 #[must_use] 746 pub fn into_owned(self) -> Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> { 747 Ed25519PubKey(*Ed25519PubKey::<&[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>::from(self).0) 748 } 749 } 750 impl Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> { 751 /// Validates `self` is in fact a valid Ed25519 public key. 752 /// 753 /// # Errors 754 /// 755 /// Errors iff `self` is not a valid Ed25519 public key. 756 #[inline] 757 pub fn validate(self) -> Result<(), PubKeyErr> { 758 self.into_ver_key().map(|_| ()) 759 } 760 /// Converts `self` into [`VerifyingKey`]. 761 fn into_ver_key(self) -> Result<VerifyingKey, PubKeyErr> { 762 // ["Taming the many EdDSAs"](https://eprint.iacr.org/2020/1244.pdf) goes over 763 // and proves varying levels of signature security. The only property that is 764 // important for WebAuthn is existential unforgeability under chosen message 765 // attacks (EUF-CMA). No matter how `ed25519-dalek` is used this is met. 766 // Additional properties that may be of importance are strong unforgeability 767 // under chosen message attacks (SUF-CMA), binding signature (BS), and 768 // strongly binding signature (SBS). 769 // No matter how `ed25519-dalek` is used, SUF-CMA is achieved. Because 770 // we always achieve SUF-CMA, we elect—despite no benefit in WebAuthn—to also 771 // achieve SBS. One can achieve SBS-secure by simply rejecting small-order 772 // keys which is precisely what `VerifyingKey::is_weak` does. 773 // Note this means we _don't_ conform to [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) 774 // nor [NIST SP 800-186](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf). 775 // As stated, there is no additional security by conforming to the above specs though; 776 // furthermore, RFC 8032 does not require rejecting small-order points despite requiring 777 // canonical encoding of points; thus it does not achieve SBS-security. 778 // NIST SP 800-186 does achieve SUF-CMA and SBS-security but requires additional properties 779 // that have no cryptographic importance. Specifically it mandates canonicity of encodings 780 // for both public keys and signatures and requires not only that points not be small-order 781 // but more strictly that points are in the prime-order subgroup (excluding the identity). 782 // This is more work for no benefit, so we elect to stay within the confines of the exposed API. 783 VerifyingKey::from_bytes(&self.0) 784 .map_err(|_e| PubKeyErr::Ed25519) 785 .and_then(|key| { 786 if key.is_weak() { 787 Err(PubKeyErr::Ed25519) 788 } else { 789 Ok(key) 790 } 791 }) 792 } 793 } 794 impl<'a: 'b, 'b> TryFrom<&'a [u8]> for Ed25519PubKey<&'b [u8]> { 795 type Error = Ed25519PubKeyErr; 796 /// Interprets `value` as the compressed y-coordinate of an Ed25519 public key. 797 #[inline] 798 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { 799 if value.len() == ed25519_dalek::PUBLIC_KEY_LENGTH { 800 Ok(Self(value)) 801 } else { 802 Err(Ed25519PubKeyErr) 803 } 804 } 805 } 806 impl From<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> 807 for Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> 808 { 809 #[inline] 810 fn from(value: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]) -> Self { 811 Self(value) 812 } 813 } 814 impl<'a: 'b, 'b> From<&'a Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>> 815 for Ed25519PubKey<&'b [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> 816 { 817 #[inline] 818 fn from(value: &'a Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>) -> Self { 819 Self(&value.0) 820 } 821 } 822 impl<'a: 'b, 'b> From<Ed25519PubKey<&'a [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>> 823 for Ed25519PubKey<&'b [u8]> 824 { 825 #[inline] 826 fn from(value: Ed25519PubKey<&'a [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>) -> Self { 827 Self(value.0.as_slice()) 828 } 829 } 830 impl<'a: 'b, 'b> From<&'a Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>> 831 for Ed25519PubKey<&'b [u8]> 832 { 833 #[inline] 834 fn from(value: &'a Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>) -> Self { 835 Self(value.0.as_slice()) 836 } 837 } 838 impl<'a: 'b, 'b> From<Ed25519PubKey<&'a [u8]>> 839 for Ed25519PubKey<&'b [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> 840 { 841 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 842 #[inline] 843 fn from(value: Ed25519PubKey<&'a [u8]>) -> Self { 844 Self( 845 value 846 .0 847 .try_into() 848 .unwrap_or_else(|_e| unreachable!("there is a bug in &Array::try_from")), 849 ) 850 } 851 } 852 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<Ed25519PubKey<T>> for Ed25519PubKey<T2> { 853 #[inline] 854 fn eq(&self, other: &Ed25519PubKey<T>) -> bool { 855 self.0 == other.0 856 } 857 } 858 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<Ed25519PubKey<T>> for &Ed25519PubKey<T2> { 859 #[inline] 860 fn eq(&self, other: &Ed25519PubKey<T>) -> bool { 861 **self == *other 862 } 863 } 864 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&Ed25519PubKey<T>> for Ed25519PubKey<T2> { 865 #[inline] 866 fn eq(&self, other: &&Ed25519PubKey<T>) -> bool { 867 *self == **other 868 } 869 } 870 impl<T: Eq> Eq for Ed25519PubKey<T> {} 871 /// Two 32-byte regions representing the big-endian x and y coordinates of an alleged P-256 public key. 872 #[derive(Clone, Copy, Debug)] 873 pub struct UncompressedP256PubKey<'a>(&'a [u8], &'a [u8]); 874 impl<'a> UncompressedP256PubKey<'a> { 875 /// Returns the big-endian x-coordinate. 876 #[inline] 877 #[must_use] 878 pub const fn x(self) -> &'a [u8] { 879 self.0 880 } 881 /// Returns the big-endian y-coordinate. 882 #[inline] 883 #[must_use] 884 pub const fn y(self) -> &'a [u8] { 885 self.1 886 } 887 /// Validates `self` is in fact a valid P-256 public key. 888 /// 889 /// # Errors 890 /// 891 /// Errors iff `self` is not a valid P-256 public key. 892 #[inline] 893 pub fn validate(self) -> Result<(), PubKeyErr> { 894 self.into_ver_key().map(|_| ()) 895 } 896 /// Converts `self` into [`P256VerKey`]. 897 fn into_ver_key(self) -> Result<P256VerKey, PubKeyErr> { 898 P256VerKey::from_encoded_point(&P256Pt::from_affine_coordinates( 899 self.0.into(), 900 self.1.into(), 901 false, 902 )) 903 .map_err(|_e| PubKeyErr::P256) 904 } 905 /// Returns `true` iff [`Self::y`] is odd. 906 #[expect(clippy::indexing_slicing, reason = "comment justifies correctness")] 907 #[inline] 908 #[must_use] 909 pub const fn y_is_odd(self) -> bool { 910 // `self.1.len() == 32`, so this won't `panic`. 911 self.1[31] & 1 == 1 912 } 913 /// Transforms `self` into the compressed version that owns the data. 914 #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] 915 #[inline] 916 #[must_use] 917 pub fn into_compressed( 918 self, 919 ) -> CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]> { 920 CompressedP256PubKey { 921 x: self.0.try_into().unwrap_or_else(|_e| unreachable!("there is a bug in UncompressedP256PubKey that allows for the x-coordinate to not be 32 bytes in length")), 922 y_is_odd: self.y_is_odd(), 923 } 924 } 925 } 926 impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP256PubKey<'b> { 927 type Error = UncompressedP256PubKeyErr; 928 /// The first item is the big-endian x-coordinate, and the second item is the big-endian y-coordinate. 929 #[inline] 930 fn try_from((x, y): (&'a [u8], &'a [u8])) -> Result<Self, Self::Error> { 931 /// Number of bytes each coordinate is made of. 932 const COORD_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT; 933 if x.len() == COORD_LEN { 934 if y.len() == COORD_LEN { 935 Ok(Self(x, y)) 936 } else { 937 Err(UncompressedP256PubKeyErr::Y) 938 } 939 } else { 940 Err(UncompressedP256PubKeyErr::X) 941 } 942 } 943 } 944 impl PartialEq<UncompressedP256PubKey<'_>> for UncompressedP256PubKey<'_> { 945 #[inline] 946 fn eq(&self, other: &UncompressedP256PubKey<'_>) -> bool { 947 self.0 == other.0 && self.1 == other.1 948 } 949 } 950 impl PartialEq<UncompressedP256PubKey<'_>> for &UncompressedP256PubKey<'_> { 951 #[inline] 952 fn eq(&self, other: &UncompressedP256PubKey<'_>) -> bool { 953 **self == *other 954 } 955 } 956 impl PartialEq<&UncompressedP256PubKey<'_>> for UncompressedP256PubKey<'_> { 957 #[inline] 958 fn eq(&self, other: &&UncompressedP256PubKey<'_>) -> bool { 959 *self == **other 960 } 961 } 962 impl Eq for UncompressedP256PubKey<'_> {} 963 /// 32-bytes representing the big-endian x-coordinate and a `bool` representing whether the y-coordinate 964 /// is odd of an alleged P-256 public key. 965 #[derive(Clone, Copy, Debug)] 966 pub struct CompressedP256PubKey<T> { 967 /// 32-byte x-coordinate. 968 x: T, 969 /// `true` iff the y-coordinate is odd. 970 y_is_odd: bool, 971 } 972 impl<T> CompressedP256PubKey<T> { 973 /// Returns [`Self::x`] and [`Self::y_is_odd`] consuming `self`. 974 #[inline] 975 pub fn into_parts(self) -> (T, bool) { 976 (self.x, self.y_is_odd) 977 } 978 /// Returns [`Self::x`] and [`Self::y_is_odd`]. 979 #[inline] 980 pub const fn as_parts(&self) -> (&T, bool) { 981 (&self.x, self.y_is_odd) 982 } 983 /// Returns the 32-byte big-endian x-coordinate. 984 #[inline] 985 pub const fn x(&self) -> &T { 986 &self.x 987 } 988 /// `true` iff the y-coordinate is odd. 989 #[inline] 990 pub const fn y_is_odd(&self) -> bool { 991 self.y_is_odd 992 } 993 } 994 impl CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]> { 995 /// Validates `self` is in fact a valid P-256 public key. 996 /// 997 /// # Errors 998 /// 999 /// Errors iff `self` is not a valid P-256 public key. 1000 #[inline] 1001 pub fn validate(self) -> Result<(), PubKeyErr> { 1002 self.into_ver_key().map(|_| ()) 1003 } 1004 /// Converts `self` into [`P256VerKey`]. 1005 pub(super) fn into_ver_key(self) -> Result<P256VerKey, PubKeyErr> { 1006 P256Affine::decompress(&self.x.into(), u8::from(self.y_is_odd).into()) 1007 .into_option() 1008 .ok_or(PubKeyErr::P256) 1009 .and_then(|pt| P256VerKey::from_affine(pt).map_err(|_e| PubKeyErr::P256)) 1010 } 1011 } 1012 impl CompressedP256PubKey<&[u8]> { 1013 /// Validates `self` is in fact a valid P-256 public key. 1014 /// 1015 /// # Errors 1016 /// 1017 /// Errors iff `self` is not a valid P-256 public key. 1018 #[inline] 1019 pub fn validate(self) -> Result<(), PubKeyErr> { 1020 self.into_ver_key().map(|_| ()) 1021 } 1022 /// Converts `self` into [`P256VerKey`]. 1023 pub(super) fn into_ver_key(self) -> Result<P256VerKey, PubKeyErr> { 1024 P256Affine::decompress(self.x.into(), u8::from(self.y_is_odd).into()) 1025 .into_option() 1026 .ok_or(PubKeyErr::P256) 1027 .and_then(|pt| P256VerKey::from_affine(pt).map_err(|_e| PubKeyErr::P256)) 1028 } 1029 } 1030 impl<'a: 'b, 'b> TryFrom<(&'a [u8], bool)> for CompressedP256PubKey<&'b [u8]> { 1031 type Error = CompressedP256PubKeyErr; 1032 #[inline] 1033 fn try_from((x, y_is_odd): (&'a [u8], bool)) -> Result<Self, Self::Error> { 1034 /// The number of bytes the x-coordinate is. 1035 const X_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT; 1036 if x.len() == X_LEN { 1037 Ok(Self { x, y_is_odd }) 1038 } else { 1039 Err(CompressedP256PubKeyErr) 1040 } 1041 } 1042 } 1043 impl From<([u8; <NistP256 as Curve>::FieldBytesSize::INT], bool)> 1044 for CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]> 1045 { 1046 #[inline] 1047 fn from((x, y_is_odd): ([u8; <NistP256 as Curve>::FieldBytesSize::INT], bool)) -> Self { 1048 Self { x, y_is_odd } 1049 } 1050 } 1051 impl<'a: 'b, 'b> From<&'a CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]>> 1052 for CompressedP256PubKey<&'b [u8; <NistP256 as Curve>::FieldBytesSize::INT]> 1053 { 1054 #[inline] 1055 fn from( 1056 value: &'a CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]>, 1057 ) -> Self { 1058 Self { 1059 x: &value.x, 1060 y_is_odd: value.y_is_odd, 1061 } 1062 } 1063 } 1064 impl<'a: 'b, 'b> From<CompressedP256PubKey<&'a [u8; <NistP256 as Curve>::FieldBytesSize::INT]>> 1065 for CompressedP256PubKey<&'b [u8]> 1066 { 1067 #[inline] 1068 fn from( 1069 value: CompressedP256PubKey<&'a [u8; <NistP256 as Curve>::FieldBytesSize::INT]>, 1070 ) -> Self { 1071 Self { 1072 x: value.x.as_slice(), 1073 y_is_odd: value.y_is_odd, 1074 } 1075 } 1076 } 1077 impl<'a: 'b, 'b> From<&'a CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]>> 1078 for CompressedP256PubKey<&'b [u8]> 1079 { 1080 #[inline] 1081 fn from( 1082 value: &'a CompressedP256PubKey<[u8; <NistP256 as Curve>::FieldBytesSize::INT]>, 1083 ) -> Self { 1084 Self { 1085 x: value.x.as_slice(), 1086 y_is_odd: value.y_is_odd, 1087 } 1088 } 1089 } 1090 impl<'a: 'b, 'b> From<CompressedP256PubKey<&'a [u8]>> 1091 for CompressedP256PubKey<&'b [u8; <NistP256 as Curve>::FieldBytesSize::INT]> 1092 { 1093 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 1094 #[inline] 1095 fn from(value: CompressedP256PubKey<&'a [u8]>) -> Self { 1096 Self { 1097 x: value 1098 .x 1099 .try_into() 1100 .unwrap_or_else(|_e| unreachable!("&Array::try_from has a bug")), 1101 y_is_odd: value.y_is_odd, 1102 } 1103 } 1104 } 1105 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CompressedP256PubKey<T>> 1106 for CompressedP256PubKey<T2> 1107 { 1108 #[inline] 1109 fn eq(&self, other: &CompressedP256PubKey<T>) -> bool { 1110 self.x == other.x && self.y_is_odd == other.y_is_odd 1111 } 1112 } 1113 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CompressedP256PubKey<T>> 1114 for &CompressedP256PubKey<T2> 1115 { 1116 #[inline] 1117 fn eq(&self, other: &CompressedP256PubKey<T>) -> bool { 1118 **self == *other 1119 } 1120 } 1121 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&CompressedP256PubKey<T>> 1122 for CompressedP256PubKey<T2> 1123 { 1124 #[inline] 1125 fn eq(&self, other: &&CompressedP256PubKey<T>) -> bool { 1126 *self == **other 1127 } 1128 } 1129 impl<T: Eq> Eq for CompressedP256PubKey<T> {} 1130 /// Two 48-byte regions representing the big-endian x and y coordinates of an alleged P-384 public key. 1131 #[derive(Clone, Copy, Debug)] 1132 pub struct UncompressedP384PubKey<'a>(&'a [u8], &'a [u8]); 1133 impl<'a> UncompressedP384PubKey<'a> { 1134 /// Returns the big-endian x-coordinate. 1135 #[inline] 1136 #[must_use] 1137 pub const fn x(self) -> &'a [u8] { 1138 self.0 1139 } 1140 /// Returns the big-endian y-coordinate. 1141 #[inline] 1142 #[must_use] 1143 pub const fn y(self) -> &'a [u8] { 1144 self.1 1145 } 1146 /// Validates `self` is in fact a valid P-384 public key. 1147 /// 1148 /// # Errors 1149 /// 1150 /// Errors iff `self` is not a valid P-384 public key. 1151 #[inline] 1152 pub fn validate(self) -> Result<(), PubKeyErr> { 1153 self.into_ver_key().map(|_| ()) 1154 } 1155 /// Converts `self` into [`P384VerKey`]. 1156 fn into_ver_key(self) -> Result<P384VerKey, PubKeyErr> { 1157 P384VerKey::from_encoded_point(&P384Pt::from_affine_coordinates( 1158 self.0.into(), 1159 self.1.into(), 1160 false, 1161 )) 1162 .map_err(|_e| PubKeyErr::P384) 1163 } 1164 /// Returns `true` iff [`Self::y`] is odd. 1165 #[expect(clippy::indexing_slicing, reason = "comment justifies correctness")] 1166 #[inline] 1167 #[must_use] 1168 pub const fn y_is_odd(self) -> bool { 1169 // `self.1.len() == 48`, so this won't `panic`. 1170 self.1[47] & 1 == 1 1171 } 1172 /// Transforms `self` into the compressed version that owns the data. 1173 #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] 1174 #[inline] 1175 #[must_use] 1176 pub fn into_compressed( 1177 self, 1178 ) -> CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]> { 1179 CompressedP384PubKey { 1180 x: self.0.try_into().unwrap_or_else(|_e| unreachable!("there is a bug in UncompressedP384PubKey that allows for the x-coordinate to not be 48 bytes in length")), 1181 y_is_odd: self.y_is_odd(), 1182 } 1183 } 1184 } 1185 impl<'a: 'b, 'b> TryFrom<(&'a [u8], &'a [u8])> for UncompressedP384PubKey<'b> { 1186 type Error = UncompressedP384PubKeyErr; 1187 /// The first item is the big-endian x-coordinate, and the second item is the big-endian y-coordinate. 1188 #[inline] 1189 fn try_from((x, y): (&'a [u8], &'a [u8])) -> Result<Self, Self::Error> { 1190 /// Number of bytes each coordinate is made of. 1191 const COORD_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT; 1192 if x.len() == COORD_LEN { 1193 if y.len() == COORD_LEN { 1194 Ok(Self(x, y)) 1195 } else { 1196 Err(UncompressedP384PubKeyErr::Y) 1197 } 1198 } else { 1199 Err(UncompressedP384PubKeyErr::X) 1200 } 1201 } 1202 } 1203 impl PartialEq<UncompressedP384PubKey<'_>> for UncompressedP384PubKey<'_> { 1204 #[inline] 1205 fn eq(&self, other: &UncompressedP384PubKey<'_>) -> bool { 1206 self.0 == other.0 && self.1 == other.1 1207 } 1208 } 1209 impl PartialEq<UncompressedP384PubKey<'_>> for &UncompressedP384PubKey<'_> { 1210 #[inline] 1211 fn eq(&self, other: &UncompressedP384PubKey<'_>) -> bool { 1212 **self == *other 1213 } 1214 } 1215 impl PartialEq<&UncompressedP384PubKey<'_>> for UncompressedP384PubKey<'_> { 1216 #[inline] 1217 fn eq(&self, other: &&UncompressedP384PubKey<'_>) -> bool { 1218 *self == **other 1219 } 1220 } 1221 impl Eq for UncompressedP384PubKey<'_> {} 1222 /// 48-bytes representing the big-endian x-coordinate and a `bool` representing whether the y-coordinate 1223 /// is odd of an alleged P-384 public key. 1224 #[derive(Clone, Copy, Debug)] 1225 pub struct CompressedP384PubKey<T> { 1226 /// 48-byte x-coordinate. 1227 x: T, 1228 /// `true` iff the y-coordinate is odd. 1229 y_is_odd: bool, 1230 } 1231 impl<T> CompressedP384PubKey<T> { 1232 /// Returns [`Self::x`] and [`Self::y_is_odd`] consuming `self`. 1233 #[inline] 1234 pub fn into_parts(self) -> (T, bool) { 1235 (self.x, self.y_is_odd) 1236 } 1237 /// Returns [`Self::x`] and [`Self::y_is_odd`]. 1238 #[inline] 1239 pub const fn as_parts(&self) -> (&T, bool) { 1240 (&self.x, self.y_is_odd) 1241 } 1242 /// Returns the 48-byte big-endian x-coordinate. 1243 #[inline] 1244 pub const fn x(&self) -> &T { 1245 &self.x 1246 } 1247 /// `true` iff the y-coordinate is odd. 1248 #[inline] 1249 #[must_use] 1250 pub const fn y_is_odd(&self) -> bool { 1251 self.y_is_odd 1252 } 1253 } 1254 impl CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]> { 1255 /// Validates `self` is in fact a valid P-384 public key. 1256 /// 1257 /// # Errors 1258 /// 1259 /// Errors iff `self` is not a valid P-384 public key. 1260 #[inline] 1261 pub fn validate(self) -> Result<(), PubKeyErr> { 1262 self.into_ver_key().map(|_| ()) 1263 } 1264 /// Converts `self` into [`P384VerKey`]. 1265 pub(super) fn into_ver_key(self) -> Result<P384VerKey, PubKeyErr> { 1266 P384Affine::decompress(&self.x.into(), u8::from(self.y_is_odd).into()) 1267 .into_option() 1268 .ok_or(PubKeyErr::P384) 1269 .and_then(|pt| P384VerKey::from_affine(pt).map_err(|_e| PubKeyErr::P384)) 1270 } 1271 } 1272 impl CompressedP384PubKey<&[u8]> { 1273 /// Validates `self` is in fact a valid P-384 public key. 1274 /// 1275 /// # Errors 1276 /// 1277 /// Errors iff `self` is not a valid P-384 public key. 1278 #[inline] 1279 pub fn validate(self) -> Result<(), PubKeyErr> { 1280 self.into_ver_key().map(|_| ()) 1281 } 1282 /// Converts `self` into [`P384VerKey`]. 1283 pub(super) fn into_ver_key(self) -> Result<P384VerKey, PubKeyErr> { 1284 P384Affine::decompress(self.x.into(), u8::from(self.y_is_odd).into()) 1285 .into_option() 1286 .ok_or(PubKeyErr::P384) 1287 .and_then(|pt| P384VerKey::from_affine(pt).map_err(|_e| PubKeyErr::P384)) 1288 } 1289 } 1290 impl<'a: 'b, 'b> TryFrom<(&'a [u8], bool)> for CompressedP384PubKey<&'b [u8]> { 1291 type Error = CompressedP384PubKeyErr; 1292 #[inline] 1293 fn try_from((x, y_is_odd): (&'a [u8], bool)) -> Result<Self, Self::Error> { 1294 /// Number of bytes of the x-coordinate is. 1295 const X_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT; 1296 if x.len() == X_LEN { 1297 Ok(Self { x, y_is_odd }) 1298 } else { 1299 Err(CompressedP384PubKeyErr) 1300 } 1301 } 1302 } 1303 impl From<([u8; <NistP384 as Curve>::FieldBytesSize::INT], bool)> 1304 for CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]> 1305 { 1306 #[inline] 1307 fn from((x, y_is_odd): ([u8; <NistP384 as Curve>::FieldBytesSize::INT], bool)) -> Self { 1308 Self { x, y_is_odd } 1309 } 1310 } 1311 impl<'a: 'b, 'b> From<&'a CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]>> 1312 for CompressedP384PubKey<&'b [u8; <NistP384 as Curve>::FieldBytesSize::INT]> 1313 { 1314 #[inline] 1315 fn from( 1316 value: &'a CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]>, 1317 ) -> Self { 1318 Self { 1319 x: &value.x, 1320 y_is_odd: value.y_is_odd, 1321 } 1322 } 1323 } 1324 impl<'a: 'b, 'b> From<CompressedP384PubKey<&'a [u8; <NistP384 as Curve>::FieldBytesSize::INT]>> 1325 for CompressedP384PubKey<&'b [u8]> 1326 { 1327 #[inline] 1328 fn from( 1329 value: CompressedP384PubKey<&'a [u8; <NistP384 as Curve>::FieldBytesSize::INT]>, 1330 ) -> Self { 1331 Self { 1332 x: value.x.as_slice(), 1333 y_is_odd: value.y_is_odd, 1334 } 1335 } 1336 } 1337 impl<'a: 'b, 'b> From<&'a CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]>> 1338 for CompressedP384PubKey<&'b [u8]> 1339 { 1340 #[inline] 1341 fn from( 1342 value: &'a CompressedP384PubKey<[u8; <NistP384 as Curve>::FieldBytesSize::INT]>, 1343 ) -> Self { 1344 Self { 1345 x: value.x.as_slice(), 1346 y_is_odd: value.y_is_odd, 1347 } 1348 } 1349 } 1350 impl<'a: 'b, 'b> From<CompressedP384PubKey<&'a [u8]>> 1351 for CompressedP384PubKey<&'b [u8; <NistP384 as Curve>::FieldBytesSize::INT]> 1352 { 1353 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 1354 #[inline] 1355 fn from(value: CompressedP384PubKey<&'a [u8]>) -> Self { 1356 Self { 1357 x: value 1358 .x 1359 .try_into() 1360 .unwrap_or_else(|_e| unreachable!("&Array::try_from has a bug")), 1361 y_is_odd: value.y_is_odd, 1362 } 1363 } 1364 } 1365 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CompressedP384PubKey<T>> 1366 for CompressedP384PubKey<T2> 1367 { 1368 #[inline] 1369 fn eq(&self, other: &CompressedP384PubKey<T>) -> bool { 1370 self.x == other.x && self.y_is_odd == other.y_is_odd 1371 } 1372 } 1373 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<CompressedP384PubKey<T>> 1374 for &CompressedP384PubKey<T2> 1375 { 1376 #[inline] 1377 fn eq(&self, other: &CompressedP384PubKey<T>) -> bool { 1378 **self == *other 1379 } 1380 } 1381 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&CompressedP384PubKey<T>> 1382 for CompressedP384PubKey<T2> 1383 { 1384 #[inline] 1385 fn eq(&self, other: &&CompressedP384PubKey<T>) -> bool { 1386 *self == **other 1387 } 1388 } 1389 impl<T: Eq> Eq for CompressedP384PubKey<T> {} 1390 /// The minimum RSA public key exponent allowed by [`RsaPubKey`]. 1391 /// 1392 /// [RFC 8017 § 3.1](https://www.rfc-editor.org/rfc/rfc8017#section-3.1) states the smallest valid RSA public 1393 /// exponent is 3. 1394 pub const MIN_RSA_E: u32 = 3; 1395 /// The most bits an RSA public key modulus is allowed to consist of per [`RsaPubKey`]. 1396 /// 1397 /// [RFC 8230 § 6.1](https://www.rfc-editor.org/rfc/rfc8230#section-6.1) recommends allowing moduli up to 16K bits. 1398 pub const MAX_RSA_N_BITS: usize = 0x4000; 1399 /// The fewest bits an RSA public key modulus is allowed to consist of per [`RsaPubKey`]. 1400 /// 1401 /// [RFC 8230 § 6.1](https://www.rfc-editor.org/rfc/rfc8230#section-6.1) requires the modulus to be at least 2048 1402 /// bits. 1403 pub const MIN_RSA_N_BITS: usize = 0x800; 1404 /// [`MIN_RSA_N_BITS`]–[`MAX_RSA_N_BITS`] bits representing the big-endian modulus and a `u32` `>=` 1405 /// [`MIN_RSA_E`] representing the exponent of an alleged RSA public key. 1406 /// 1407 /// Note the modulus and exponent are always odd. 1408 #[derive(Clone, Copy, Debug)] 1409 pub struct RsaPubKey<T>(T, u32); 1410 impl<T> RsaPubKey<T> { 1411 /// Returns [`Self::n`] and [`Self::e`] consuming `self`. 1412 #[inline] 1413 pub fn into_parts(self) -> (T, u32) { 1414 (self.0, self.1) 1415 } 1416 /// Returns [`Self::n`] and [`Self::e`]. 1417 #[inline] 1418 pub const fn as_parts(&self) -> (&T, u32) { 1419 (&self.0, self.1) 1420 } 1421 /// Returns the big-endian modulus. 1422 #[inline] 1423 pub const fn n(&self) -> &T { 1424 &self.0 1425 } 1426 /// Returns the exponent. 1427 #[inline] 1428 pub const fn e(&self) -> u32 { 1429 self.1 1430 } 1431 } 1432 impl<T: AsRef<[u8]>> RsaPubKey<T> { 1433 /// Converts `self` into [`RsaVerKey`]. 1434 pub(super) fn as_ver_key(&self) -> RsaVerKey<Sha256> { 1435 RsaVerKey::new(RsaPublicKey::new_unchecked( 1436 BigUint::from_bytes_be(self.0.as_ref()), 1437 self.1.into(), 1438 )) 1439 } 1440 } 1441 impl RsaPubKey<&[u8]> { 1442 /// Transforms `self` into an "owned" version. 1443 #[inline] 1444 #[must_use] 1445 pub fn into_owned(self) -> RsaPubKey<Vec<u8>> { 1446 RsaPubKey(self.0.to_owned(), self.1) 1447 } 1448 } 1449 impl<'a: 'b, 'b> TryFrom<(&'a [u8], u32)> for RsaPubKey<&'b [u8]> { 1450 type Error = RsaPubKeyErr; 1451 /// The first item is the big-endian modulus, and the second item is the exponent. 1452 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 1453 #[expect( 1454 clippy::arithmetic_side_effects, 1455 clippy::as_conversions, 1456 reason = "comment justifies correctness" 1457 )] 1458 #[inline] 1459 fn try_from((n, e): (&'a [u8], u32)) -> Result<Self, Self::Error> { 1460 n.first().map_or(Err(RsaPubKeyErr::NSize), |fst| { 1461 // `fst.leading_zeros()` is inclusively between `0` and `8`, so it's safe to convert to a 1462 // `usize`. 1463 let zeros = fst.leading_zeros() as usize; 1464 if zeros == 8 { 1465 Err(RsaPubKeyErr::NLeading0) 1466 // `bits` is at least 8 since `n.len()` is at least 1; thus underflow cannot occur. 1467 } else if let Some(bits) = n.len().checked_mul(8) 1468 && (MIN_RSA_N_BITS..=MAX_RSA_N_BITS).contains(&(bits - zeros)) 1469 { 1470 // We know `n` is not empty, so this won't `panic`. 1471 if n.last() 1472 .unwrap_or_else(|| unreachable!("there is a bug in RsaPubKey::try_from")) 1473 & 1 1474 == 0 1475 { 1476 Err(RsaPubKeyErr::NEven) 1477 } else if e < MIN_RSA_E { 1478 Err(RsaPubKeyErr::ESize) 1479 } else if e & 1 == 0 { 1480 Err(RsaPubKeyErr::EEven) 1481 } else { 1482 Ok(Self(n, e)) 1483 } 1484 } else { 1485 Err(RsaPubKeyErr::NSize) 1486 } 1487 }) 1488 } 1489 } 1490 impl TryFrom<(Vec<u8>, u32)> for RsaPubKey<Vec<u8>> { 1491 type Error = RsaPubKeyErr; 1492 /// Similar to [`RsaPubKey::try_from`] except `n` is a `Vec`. 1493 #[inline] 1494 fn try_from((n, e): (Vec<u8>, u32)) -> Result<Self, Self::Error> { 1495 match RsaPubKey::<&[u8]>::try_from((n.as_slice(), e)) { 1496 Ok(_) => Ok(Self(n, e)), 1497 Err(err) => Err(err), 1498 } 1499 } 1500 } 1501 impl<'a: 'b, 'b> From<&'a RsaPubKey<Vec<u8>>> for RsaPubKey<&'b Vec<u8>> { 1502 #[inline] 1503 fn from(value: &'a RsaPubKey<Vec<u8>>) -> Self { 1504 Self(&value.0, value.1) 1505 } 1506 } 1507 impl<'a: 'b, 'b> From<RsaPubKey<&'a Vec<u8>>> for RsaPubKey<&'b [u8]> { 1508 #[inline] 1509 fn from(value: RsaPubKey<&'a Vec<u8>>) -> Self { 1510 Self(value.0.as_slice(), value.1) 1511 } 1512 } 1513 impl<'a: 'b, 'b> From<&'a RsaPubKey<Vec<u8>>> for RsaPubKey<&'b [u8]> { 1514 #[inline] 1515 fn from(value: &'a RsaPubKey<Vec<u8>>) -> Self { 1516 Self(value.0.as_slice(), value.1) 1517 } 1518 } 1519 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<RsaPubKey<T>> for RsaPubKey<T2> { 1520 #[inline] 1521 fn eq(&self, other: &RsaPubKey<T>) -> bool { 1522 self.0 == other.0 && self.1 == other.1 1523 } 1524 } 1525 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<RsaPubKey<T>> for &RsaPubKey<T2> { 1526 #[inline] 1527 fn eq(&self, other: &RsaPubKey<T>) -> bool { 1528 **self == *other 1529 } 1530 } 1531 impl<T: PartialEq<T2>, T2: PartialEq<T>> PartialEq<&RsaPubKey<T>> for RsaPubKey<T2> { 1532 #[inline] 1533 fn eq(&self, other: &&RsaPubKey<T>) -> bool { 1534 *self == **other 1535 } 1536 } 1537 impl<T: Eq> Eq for RsaPubKey<T> {} 1538 /// `kty` COSE key common parameter as defined by 1539 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters). 1540 const KTY: u8 = cbor::ONE; 1541 /// `OKP` COSE key type as defined by 1542 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type). 1543 const OKP: u8 = cbor::ONE; 1544 /// `EC2` COSE key type as defined by 1545 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type). 1546 const EC2: u8 = cbor::TWO; 1547 /// `RSA` COSE key type as defined by 1548 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type). 1549 const RSA: u8 = cbor::THREE; 1550 /// `alg` COSE key common parameter as defined by 1551 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters). 1552 const ALG: u8 = cbor::THREE; 1553 /// `EdDSA` COSE algorithm as defined by 1554 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms). 1555 const EDDSA: u8 = cbor::NEG_EIGHT; 1556 /// `ES256` COSE algorithm as defined by 1557 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms). 1558 const ES256: u8 = cbor::NEG_SEVEN; 1559 /// `ES384` COSE algorithm as defined by 1560 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms). 1561 /// 1562 /// This is -35 encoded in cbor which is encoded as |-35| - 1 = 35 - 1 = 34. Note 1563 /// this must be preceded with `cbor::NEG_INFO_24`. 1564 const ES384: u8 = 34; 1565 /// `RS256` COSE algorithm as defined by 1566 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms). 1567 /// 1568 /// This is -257 encoded in cbor which is encoded as |-257| - 1 = 257 - 1 = 256 = [1, 0] in big endian. 1569 /// Note this must be preceded with `cbor::NEG_INFO_25`. 1570 const RS256: [u8; 2] = [1, 0]; 1571 impl<'a> FromCbor<'a> for Ed25519PubKey<&'a [u8]> { 1572 type Err = CoseKeyErr; 1573 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 1574 /// `crv` COSE key type parameter for [`OKP`] as defined by 1575 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1576 const CRV: u8 = cbor::NEG_ONE; 1577 /// `Ed25519` COSE elliptic curve as defined by 1578 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves). 1579 const ED25519: u8 = cbor::SIX; 1580 /// `x` COSE key type parameter for [`OKP`] as defined by 1581 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1582 const X: u8 = cbor::NEG_TWO; 1583 // `32 as u8` is OK. 1584 /// `ed25519_dalek::PUBLIC_KEY_LENGTH` as a `u8`. 1585 #[expect( 1586 clippy::as_conversions, 1587 clippy::cast_possible_truncation, 1588 reason = "explained above and want a const" 1589 )] 1590 const KEY_LEN_U8: u8 = ed25519_dalek::PUBLIC_KEY_LENGTH as u8; 1591 /// COSE header. 1592 /// {kty:OKP,alg:EdDSA,crv:Ed25519,x:<CompressedEdwardsYPoint>}. 1593 /// `kty` and `alg` come before `crv` and `x` since map order first 1594 /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s. 1595 /// `kty` comes before `alg` since order is done byte-wise and 1596 /// 1 is before 3. `crv` is before `x` since `0b001_00000` comes before 1597 /// `0b001_00001` byte-wise. 1598 const HEADER: [u8; 10] = [ 1599 cbor::MAP_4, 1600 KTY, 1601 OKP, 1602 ALG, 1603 EDDSA, 1604 CRV, 1605 ED25519, 1606 X, 1607 cbor::BYTES_INFO_24, 1608 KEY_LEN_U8, 1609 ]; 1610 cbor.split_at_checked(HEADER.len()) 1611 .ok_or(CoseKeyErr::Len) 1612 .and_then(|(header, header_rem)| { 1613 if header == HEADER { 1614 header_rem 1615 .split_at_checked(ed25519_dalek::PUBLIC_KEY_LENGTH) 1616 .ok_or(CoseKeyErr::Len) 1617 .map(|(key, remaining)| CborSuccess { 1618 value: Self(key), 1619 remaining, 1620 }) 1621 } else { 1622 Err(CoseKeyErr::Ed25519CoseEncoding) 1623 } 1624 }) 1625 } 1626 } 1627 impl<'a> FromCbor<'a> for UncompressedP256PubKey<'a> { 1628 type Err = CoseKeyErr; 1629 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 1630 /// `crv` COSE key type parameter for [`EC2`] as defined by 1631 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1632 const CRV: u8 = cbor::NEG_ONE; 1633 /// `P-256` COSE elliptic curve as defined by 1634 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves). 1635 const P256: u8 = cbor::ONE; 1636 /// `x` COSE key type parameter for [`EC2`] as defined by 1637 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1638 const X: u8 = cbor::NEG_TWO; 1639 /// `y` COSE key type parameter for [`EC2`] as defined by 1640 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1641 const Y: u8 = cbor::NEG_THREE; 1642 /// Number of bytes the x-coordinate takes. 1643 const X_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT; 1644 // `32 as u8` is OK. 1645 /// `X_LEN` as a `u8`. 1646 #[expect( 1647 clippy::as_conversions, 1648 clippy::cast_possible_truncation, 1649 reason = "explained above and want a const" 1650 )] 1651 const X_LEN_U8: u8 = X_LEN as u8; 1652 /// Number of bytes the y-coordinate takes. 1653 const Y_LEN: usize = <NistP256 as Curve>::FieldBytesSize::INT; 1654 // `32 as u8` is OK. 1655 /// `Y_LEN` as a `u8`. 1656 #[expect( 1657 clippy::as_conversions, 1658 clippy::cast_possible_truncation, 1659 reason = "explained above and want a const" 1660 )] 1661 const Y_LEN_U8: u8 = Y_LEN as u8; 1662 /// COSE header. 1663 // {kty:EC2,alg:ES256,crv:P-256,x:<affine x-coordinate>,...}. 1664 /// `kty` and `alg` come before `crv`, `x`, and `y` since map order first 1665 /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s. 1666 /// `kty` comes before `alg` since order is done byte-wise and 1667 /// 1 is before 3. `crv` is before `x` which is before `y` since 1668 /// `0b001_00000` comes before `0b001_00001` which comes before 1669 /// `0b001_00010` byte-wise. 1670 const HEADER: [u8; 10] = [ 1671 cbor::MAP_5, 1672 KTY, 1673 EC2, 1674 ALG, 1675 ES256, 1676 CRV, 1677 P256, 1678 X, 1679 cbor::BYTES_INFO_24, 1680 X_LEN_U8, 1681 ]; 1682 /// {...y:<affine y-coordinate>}. 1683 const Y_META: [u8; 3] = [Y, cbor::BYTES_INFO_24, Y_LEN_U8]; 1684 cbor.split_at_checked(HEADER.len()) 1685 .ok_or(CoseKeyErr::Len) 1686 .and_then(|(header, header_rem)| { 1687 if header == HEADER { 1688 header_rem 1689 .split_at_checked(X_LEN) 1690 .ok_or(CoseKeyErr::Len) 1691 .and_then(|(x, x_rem)| { 1692 x_rem 1693 .split_at_checked(Y_META.len()) 1694 .ok_or(CoseKeyErr::Len) 1695 .and_then(|(y_meta, y_meta_rem)| { 1696 if y_meta == Y_META { 1697 y_meta_rem 1698 .split_at_checked(Y_LEN) 1699 .ok_or(CoseKeyErr::Len) 1700 .map(|(y, remaining)| CborSuccess { 1701 value: Self(x, y), 1702 remaining, 1703 }) 1704 } else { 1705 Err(CoseKeyErr::P256CoseEncoding) 1706 } 1707 }) 1708 }) 1709 } else { 1710 Err(CoseKeyErr::P256CoseEncoding) 1711 } 1712 }) 1713 } 1714 } 1715 impl<'a> FromCbor<'a> for UncompressedP384PubKey<'a> { 1716 type Err = CoseKeyErr; 1717 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 1718 /// `crv` COSE key type parameter for [`EC2`] as defined by 1719 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1720 const CRV: u8 = cbor::NEG_ONE; 1721 /// `P-384` COSE elliptic curve as defined by 1722 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves). 1723 const P384: u8 = cbor::TWO; 1724 /// `x` COSE key type parameter for [`EC2`] as defined by 1725 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1726 const X: u8 = cbor::NEG_TWO; 1727 /// `y` COSE key type parameter for [`EC2`] as defined by 1728 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1729 const Y: u8 = cbor::NEG_THREE; 1730 /// Number of bytes the x-coordinate takes. 1731 const X_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT; 1732 // `48 as u8` is OK. 1733 /// `X_LEN` as a `u8`. 1734 #[expect( 1735 clippy::as_conversions, 1736 clippy::cast_possible_truncation, 1737 reason = "explained above and want a const" 1738 )] 1739 const X_LEN_U8: u8 = X_LEN as u8; 1740 /// Number of bytes the y-coordinate takes. 1741 const Y_LEN: usize = <NistP384 as Curve>::FieldBytesSize::INT; 1742 // `48 as u8` is OK. 1743 /// `Y_LEN` as a `u8`. 1744 #[expect( 1745 clippy::as_conversions, 1746 clippy::cast_possible_truncation, 1747 reason = "explained above and want a const" 1748 )] 1749 const Y_LEN_U8: u8 = Y_LEN as u8; 1750 /// COSE header. 1751 // {kty:EC2,alg:ES384,crv:P-384,x:<affine x-coordinate>,...}. 1752 /// `kty` and `alg` come before `crv`, `x`, and `y` since map order first 1753 /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s. 1754 /// `kty` comes before `alg` since order is done byte-wise and 1755 /// 1 is before 3. `crv` is before `x` which is before `y` since 1756 /// `0b001_00000` comes before `0b001_00001` which comes before 1757 /// `0b001_00010` byte-wise. 1758 const HEADER: [u8; 11] = [ 1759 cbor::MAP_5, 1760 KTY, 1761 EC2, 1762 ALG, 1763 cbor::NEG_INFO_24, 1764 ES384, 1765 CRV, 1766 P384, 1767 X, 1768 cbor::BYTES_INFO_24, 1769 X_LEN_U8, 1770 ]; 1771 /// {...y:<affine y-coordinate>}. 1772 const Y_META: [u8; 3] = [Y, cbor::BYTES_INFO_24, Y_LEN_U8]; 1773 cbor.split_at_checked(HEADER.len()) 1774 .ok_or(CoseKeyErr::Len) 1775 .and_then(|(header, header_rem)| { 1776 if header == HEADER { 1777 header_rem 1778 .split_at_checked(X_LEN) 1779 .ok_or(CoseKeyErr::Len) 1780 .and_then(|(x, x_rem)| { 1781 x_rem 1782 .split_at_checked(Y_META.len()) 1783 .ok_or(CoseKeyErr::Len) 1784 .and_then(|(y_meta, y_meta_rem)| { 1785 if y_meta == Y_META { 1786 y_meta_rem 1787 .split_at_checked(Y_LEN) 1788 .ok_or(CoseKeyErr::Len) 1789 .map(|(y, remaining)| CborSuccess { 1790 value: Self(x, y), 1791 remaining, 1792 }) 1793 } else { 1794 Err(CoseKeyErr::P384CoseEncoding) 1795 } 1796 }) 1797 }) 1798 } else { 1799 Err(CoseKeyErr::P384CoseEncoding) 1800 } 1801 }) 1802 } 1803 } 1804 impl<'a> FromCbor<'a> for RsaPubKey<&'a [u8]> { 1805 type Err = CoseKeyErr; 1806 #[expect( 1807 clippy::arithmetic_side_effects, 1808 clippy::big_endian_bytes, 1809 clippy::indexing_slicing, 1810 clippy::missing_asserts_for_indexing, 1811 reason = "comments justify their correctness" 1812 )] 1813 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 1814 /// `n` COSE key type parameter for [`RSA`] as defined by 1815 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1816 const N: u8 = cbor::NEG_ONE; 1817 /// `e` COSE key type parameter for [`RSA`] as defined by 1818 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters). 1819 const E: u8 = cbor::NEG_TWO; 1820 /// COSE header. 1821 /// {kty:RSA,alg:RS256,n:<RSA modulus>,...}. 1822 /// `kty` and `alg` come before `n` and `e` since map order first 1823 /// is done by data type and `cbor::UINT`s come before `cbor::NEG`s. 1824 /// `kty` comes before `alg` since order is done byte-wise and 1825 /// 1 is before 3. `n` is before `e` since `0b001_00000` comes before 1826 /// `0b001_00001` byte-wise. 1827 /// 1828 /// Note `RS256` COSE algorithm as defined by 1829 /// [IANA](https://www.iana.org/assignments/cose/cose.xhtml#algorithms) 1830 /// is encoded as -257 which is encoded in CBOR as `[cbor::NEG_INFO_25, 1, 0]` since 1831 /// |-257| - 1 = 256 which takes two bytes to encode as 1, 0 in big-endian. 1832 /// Ditto for a byte string of length 0x100 to 0xFFFF inclusively replacing `cbor::NEG_INFO_25` with 1833 /// `cbor::BYTES_INFO_25`. 1834 /// 1835 /// Recall that [`RsaPubKey`] requires the modulus to be at least 256 bytes in length but no greater than 1836 /// 2048 bytes in length; thus we know a valid and allowed `n` will have length whose metadata 1837 /// takes exactly two bytes. 1838 const HEADER: [u8; 9] = [ 1839 cbor::MAP_4, 1840 KTY, 1841 RSA, 1842 ALG, 1843 cbor::NEG_INFO_25, 1844 1, 1845 0, 1846 N, 1847 cbor::BYTES_INFO_25, 1848 ]; 1849 cbor.split_at_checked(HEADER.len()).ok_or(CoseKeyErr::Len).and_then(|(header, header_rem)| { 1850 if header == HEADER { 1851 header_rem.split_at_checked(2).ok_or(CoseKeyErr::Len).and_then(|(n_len_slice, n_len_rem)| { 1852 let mut len = [0; 2]; 1853 len.copy_from_slice(n_len_slice); 1854 // cbor uints are in big-endian. 1855 let n_len = usize::from(u16::from_be_bytes(len)); 1856 if n_len > 255 { 1857 n_len_rem.split_at_checked(n_len).ok_or(CoseKeyErr::Len).and_then(|(n, n_rem)| { 1858 n_rem.split_at_checked(2).ok_or(CoseKeyErr::RsaCoseEncoding).and_then(|(e_meta, e_meta_rem)| { 1859 // `e_meta.len() == 2`, so this is fine. 1860 if e_meta[0] == E { 1861 // `e_meta.len() == 2`, so this is fine. 1862 let e_meta_len = e_meta[1]; 1863 if e_meta_len & cbor::BYTES == cbor::BYTES { 1864 let e_len = usize::from(e_meta_len ^ cbor::BYTES); 1865 if e_len < 5 { 1866 e_meta_rem.split_at_checked(e_len).ok_or(CoseKeyErr::Len).and_then(|(e_slice, remaining)| { 1867 e_slice.first().ok_or(CoseKeyErr::Len).and_then(|e_first| { 1868 // We ensure the leading byte is not 0; otherwise the 1869 // exponent is not properly encoded. Note the exponent 1870 // can never be 0. 1871 if *e_first > 0 { 1872 let mut e = [0; 4]; 1873 // `e_slice.len()` is `e_len` which is less than 5. 1874 // We also know it is greater than 0 since `e_slice.first()` did not err. 1875 // Thus this won't `panic`. 1876 e[4 - e_len..].copy_from_slice(e_slice); 1877 Self::try_from((n, u32::from_be_bytes(e))).map_err(CoseKeyErr::RsaPubKey).map(|value| CborSuccess { value, remaining, } ) 1878 } else { 1879 Err(CoseKeyErr::RsaCoseEncoding) 1880 } 1881 }) 1882 }) 1883 } else { 1884 Err(CoseKeyErr::RsaExponentTooLarge) 1885 } 1886 } else { 1887 Err(CoseKeyErr::RsaCoseEncoding) 1888 } 1889 } else { 1890 Err(CoseKeyErr::RsaCoseEncoding) 1891 } 1892 }) 1893 }) 1894 } else { 1895 Err(CoseKeyErr::RsaCoseEncoding) 1896 } 1897 }) 1898 } else { 1899 Err(CoseKeyErr::RsaCoseEncoding) 1900 } 1901 }) 1902 } 1903 } 1904 /// An alleged uncompressed public key that borrows the key data. 1905 /// 1906 /// Note [`Self::Ed25519`] is compressed. 1907 #[derive(Clone, Copy, Debug)] 1908 pub enum UncompressedPubKey<'a> { 1909 /// An alleged Ed25519 public key. 1910 Ed25519(Ed25519PubKey<&'a [u8]>), 1911 /// An alleged uncompressed P-256 public key. 1912 P256(UncompressedP256PubKey<'a>), 1913 /// An alleged uncompressed P-384 public key. 1914 P384(UncompressedP384PubKey<'a>), 1915 /// An alleged RSA public key. 1916 Rsa(RsaPubKey<&'a [u8]>), 1917 } 1918 impl UncompressedPubKey<'_> { 1919 /// Validates `self` is in fact a valid public key. 1920 /// 1921 /// # Errors 1922 /// 1923 /// Errors iff `self` is not a valid public key. 1924 #[inline] 1925 pub fn validate(self) -> Result<(), PubKeyErr> { 1926 match self { 1927 Self::Ed25519(k) => k.validate(), 1928 Self::P256(k) => k.validate(), 1929 Self::P384(k) => k.validate(), 1930 Self::Rsa(_) => Ok(()), 1931 } 1932 } 1933 /// Transforms `self` into the compressed version that owns the data. 1934 #[inline] 1935 #[must_use] 1936 pub fn into_compressed( 1937 self, 1938 ) -> CompressedPubKey< 1939 [u8; ed25519_dalek::PUBLIC_KEY_LENGTH], 1940 [u8; <NistP256 as Curve>::FieldBytesSize::INT], 1941 [u8; <NistP384 as Curve>::FieldBytesSize::INT], 1942 Vec<u8>, 1943 > { 1944 match self { 1945 Self::Ed25519(key) => CompressedPubKey::Ed25519(key.into_owned()), 1946 Self::P256(key) => CompressedPubKey::P256(key.into_compressed()), 1947 Self::P384(key) => CompressedPubKey::P384(key.into_compressed()), 1948 Self::Rsa(key) => CompressedPubKey::Rsa(key.into_owned()), 1949 } 1950 } 1951 } 1952 impl PartialEq<UncompressedPubKey<'_>> for UncompressedPubKey<'_> { 1953 #[inline] 1954 fn eq(&self, other: &UncompressedPubKey<'_>) -> bool { 1955 match *self { 1956 Self::Ed25519(k) => matches!(*other, UncompressedPubKey::Ed25519(k2) if k == k2), 1957 Self::P256(k) => matches!(*other, UncompressedPubKey::P256(k2) if k == k2), 1958 Self::P384(k) => matches!(*other, UncompressedPubKey::P384(k2) if k == k2), 1959 Self::Rsa(k) => matches!(*other, UncompressedPubKey::Rsa(k2) if k == k2), 1960 } 1961 } 1962 } 1963 impl PartialEq<&UncompressedPubKey<'_>> for UncompressedPubKey<'_> { 1964 #[inline] 1965 fn eq(&self, other: &&UncompressedPubKey<'_>) -> bool { 1966 *self == **other 1967 } 1968 } 1969 impl PartialEq<UncompressedPubKey<'_>> for &UncompressedPubKey<'_> { 1970 #[inline] 1971 fn eq(&self, other: &UncompressedPubKey<'_>) -> bool { 1972 **self == *other 1973 } 1974 } 1975 impl Eq for UncompressedPubKey<'_> {} 1976 /// An alleged compressed public key. 1977 /// 1978 /// Note [`Self::Rsa`] is uncompressed. 1979 #[derive(Clone, Copy, Debug)] 1980 pub enum CompressedPubKey<T, T2, T3, T4> { 1981 /// An alleged Ed25519 public key. 1982 Ed25519(Ed25519PubKey<T>), 1983 /// An alleged compressed P-256 public key. 1984 P256(CompressedP256PubKey<T2>), 1985 /// An alleged compressed P-384 public key. 1986 P384(CompressedP384PubKey<T3>), 1987 /// An alleged RSA public key. 1988 Rsa(RsaPubKey<T4>), 1989 } 1990 /// `CompressedPubKey` that owns the key data. 1991 pub type CompressedPubKeyOwned = CompressedPubKey< 1992 [u8; ed25519_dalek::PUBLIC_KEY_LENGTH], 1993 [u8; <NistP256 as Curve>::FieldBytesSize::INT], 1994 [u8; <NistP384 as Curve>::FieldBytesSize::INT], 1995 Vec<u8>, 1996 >; 1997 /// `CompressedPubKey` that borrows the key data. 1998 pub type CompressedPubKeyBorrowed<'a> = CompressedPubKey<&'a [u8], &'a [u8], &'a [u8], &'a [u8]>; 1999 impl CompressedPubKey<&[u8], &[u8], &[u8], &[u8]> { 2000 /// Validates `self` is in fact a valid public key. 2001 /// 2002 /// # Errors 2003 /// 2004 /// Errors iff `self` is not a valid public key. 2005 #[inline] 2006 pub fn validate(self) -> Result<(), PubKeyErr> { 2007 match self { 2008 Self::Ed25519(k) => k.validate(), 2009 Self::P256(k) => k.validate(), 2010 Self::P384(k) => k.validate(), 2011 Self::Rsa(_) => Ok(()), 2012 } 2013 } 2014 } 2015 impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8]>> 2016 From<&'a CompressedPubKey<T, T2, T3, T4>> 2017 for CompressedPubKey<&'b [u8], &'b [u8], &'b [u8], &'b [u8]> 2018 { 2019 #[inline] 2020 fn from(value: &'a CompressedPubKey<T, T2, T3, T4>) -> Self { 2021 match *value { 2022 CompressedPubKey::Ed25519(ref val) => Self::Ed25519(Ed25519PubKey(val.0.as_ref())), 2023 CompressedPubKey::P256(ref val) => Self::P256(CompressedP256PubKey { 2024 x: val.x.as_ref(), 2025 y_is_odd: val.y_is_odd, 2026 }), 2027 CompressedPubKey::P384(ref val) => Self::P384(CompressedP384PubKey { 2028 x: val.x.as_ref(), 2029 y_is_odd: val.y_is_odd, 2030 }), 2031 CompressedPubKey::Rsa(ref val) => Self::Rsa(RsaPubKey(val.0.as_ref(), val.1)), 2032 } 2033 } 2034 } 2035 impl< 2036 T: PartialEq<T5>, 2037 T5: PartialEq<T>, 2038 T2: PartialEq<T6>, 2039 T6: PartialEq<T2>, 2040 T3: PartialEq<T7>, 2041 T7: PartialEq<T3>, 2042 T4: PartialEq<T8>, 2043 T8: PartialEq<T4>, 2044 > PartialEq<CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8> 2045 { 2046 #[inline] 2047 fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool { 2048 match *self { 2049 Self::Ed25519(ref val) => { 2050 matches!(*other, CompressedPubKey::Ed25519(ref val2) if val == val2) 2051 } 2052 Self::P256(ref val) => { 2053 matches!(*other, CompressedPubKey::P256(ref val2) if val == val2) 2054 } 2055 Self::P384(ref val) => { 2056 matches!(*other, CompressedPubKey::P384(ref val2) if val == val2) 2057 } 2058 Self::Rsa(ref val) => matches!(*other, CompressedPubKey::Rsa(ref val2) if val == val2), 2059 } 2060 } 2061 } 2062 impl< 2063 T: PartialEq<T5>, 2064 T5: PartialEq<T>, 2065 T2: PartialEq<T6>, 2066 T6: PartialEq<T2>, 2067 T3: PartialEq<T7>, 2068 T7: PartialEq<T3>, 2069 T4: PartialEq<T8>, 2070 T8: PartialEq<T4>, 2071 > PartialEq<CompressedPubKey<T, T2, T3, T4>> for &CompressedPubKey<T5, T6, T7, T8> 2072 { 2073 #[inline] 2074 fn eq(&self, other: &CompressedPubKey<T, T2, T3, T4>) -> bool { 2075 **self == *other 2076 } 2077 } 2078 impl< 2079 T: PartialEq<T5>, 2080 T5: PartialEq<T>, 2081 T2: PartialEq<T6>, 2082 T6: PartialEq<T2>, 2083 T3: PartialEq<T7>, 2084 T7: PartialEq<T3>, 2085 T4: PartialEq<T8>, 2086 T8: PartialEq<T4>, 2087 > PartialEq<&CompressedPubKey<T, T2, T3, T4>> for CompressedPubKey<T5, T6, T7, T8> 2088 { 2089 #[inline] 2090 fn eq(&self, other: &&CompressedPubKey<T, T2, T3, T4>) -> bool { 2091 *self == **other 2092 } 2093 } 2094 impl<T: Eq, T2: Eq, T3: Eq, T4: Eq> Eq for CompressedPubKey<T, T2, T3, T4> {} 2095 impl<'a> FromCbor<'a> for UncompressedPubKey<'a> { 2096 type Err = CoseKeyErr; 2097 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 2098 // {kty:<type>...}. 2099 cbor.get(2) 2100 .ok_or(CoseKeyErr::Len) 2101 .and_then(|kty| match *kty { 2102 OKP => Ed25519PubKey::from_cbor(cbor).map(|key| CborSuccess { 2103 value: Self::Ed25519(key.value), 2104 remaining: key.remaining, 2105 }), 2106 // {kty:EC2,alg:ES256|ES384,...} 2107 EC2 => cbor.get(4).ok_or(CoseKeyErr::Len).and_then(|alg| { 2108 if *alg == ES256 { 2109 UncompressedP256PubKey::from_cbor(cbor).map(|key| CborSuccess { 2110 value: Self::P256(key.value), 2111 remaining: key.remaining, 2112 }) 2113 } else { 2114 UncompressedP384PubKey::from_cbor(cbor).map(|key| CborSuccess { 2115 value: Self::P384(key.value), 2116 remaining: key.remaining, 2117 }) 2118 } 2119 }), 2120 RSA => RsaPubKey::from_cbor(cbor).map(|key| CborSuccess { 2121 value: Self::Rsa(key.value), 2122 remaining: key.remaining, 2123 }), 2124 _ => Err(CoseKeyErr::CoseKeyType), 2125 }) 2126 } 2127 } 2128 /// Length of AAGUID. 2129 const AAGUID_LEN: usize = 16; 2130 /// 16 bytes representing an 2131 /// [Authenticator Attestation Globally Unique Identifier (AAGUID)](https://www.w3.org/TR/webauthn-3/#aaguid). 2132 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2133 pub struct Aaguid<'a>(&'a [u8]); 2134 impl<'a> Aaguid<'a> { 2135 /// Returns the contained data. 2136 #[inline] 2137 #[must_use] 2138 pub const fn data(self) -> &'a [u8] { 2139 self.0 2140 } 2141 } 2142 impl<'a: 'b, 'b> TryFrom<&'a [u8]> for Aaguid<'b> { 2143 type Error = AaguidErr; 2144 #[inline] 2145 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { 2146 if value.len() == AAGUID_LEN { 2147 Ok(Self(value)) 2148 } else { 2149 Err(AaguidErr) 2150 } 2151 } 2152 } 2153 /// [Attested credential data](https://www.w3.org/TR/webauthn-3/#attested-credential-data). 2154 #[derive(Debug)] 2155 pub struct AttestedCredentialData<'a> { 2156 /// [`aaguid`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-aaguid). 2157 pub aaguid: Aaguid<'a>, 2158 /// [`credentialId`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialid). 2159 pub credential_id: CredentialId<&'a [u8]>, 2160 /// [`credentialPublicKey`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialpublickey). 2161 pub credential_public_key: UncompressedPubKey<'a>, 2162 } 2163 impl<'a> FromCbor<'a> for AttestedCredentialData<'a> { 2164 type Err = AttestedCredentialDataErr; 2165 #[expect(clippy::big_endian_bytes, reason = "CBOR integers are big-endian")] 2166 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 2167 /// Number of bytes the `CredentialId` length is encoded into. 2168 const CRED_LEN_LEN: usize = 2; 2169 cbor.split_at_checked(AAGUID_LEN) 2170 .ok_or(AttestedCredentialDataErr::Len) 2171 .and_then(|(aaguid, aaguid_rem)| { 2172 aaguid_rem 2173 .split_at_checked(CRED_LEN_LEN) 2174 .ok_or(AttestedCredentialDataErr::Len) 2175 .and_then(|(cred_len_slice, cred_len_rem)| { 2176 let mut cred_len = [0; CRED_LEN_LEN]; 2177 cred_len.copy_from_slice(cred_len_slice); 2178 // `credentialIdLength` is in big-endian. 2179 cred_len_rem 2180 .split_at_checked(usize::from(u16::from_be_bytes(cred_len))) 2181 .ok_or(AttestedCredentialDataErr::Len) 2182 .and_then(|(cred_id, cred_id_rem)| { 2183 CredentialId::from_slice(cred_id) 2184 .map_err(AttestedCredentialDataErr::CredentialId) 2185 .and_then(|credential_id| { 2186 UncompressedPubKey::from_cbor(cred_id_rem) 2187 .map_err(AttestedCredentialDataErr::CoseKey) 2188 .map(|cose| CborSuccess { 2189 value: Self { 2190 aaguid: Aaguid(aaguid), 2191 credential_id, 2192 credential_public_key: cose.value, 2193 }, 2194 remaining: cose.remaining, 2195 }) 2196 }) 2197 }) 2198 }) 2199 }) 2200 } 2201 } 2202 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). 2203 #[derive(Debug)] 2204 pub struct AuthenticatorData<'a> { 2205 /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash). 2206 rp_id_hash: &'a [u8], 2207 /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags). 2208 flags: Flag, 2209 /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount). 2210 sign_count: u32, 2211 /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata). 2212 attested_credential_data: AttestedCredentialData<'a>, 2213 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions). 2214 extensions: AuthenticatorExtensionOutput, 2215 } 2216 impl<'a> AuthenticatorData<'a> { 2217 /// [`rpIdHash`](https://www.w3.org/TR/webauthn-3/#authdata-rpidhash). 2218 #[inline] 2219 #[must_use] 2220 pub const fn rp_id_hash(&self) -> &'a [u8] { 2221 self.rp_id_hash 2222 } 2223 /// [`flags`](https://www.w3.org/TR/webauthn-3/#authdata-flags). 2224 #[inline] 2225 #[must_use] 2226 pub const fn flags(&self) -> Flag { 2227 self.flags 2228 } 2229 /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount). 2230 #[inline] 2231 #[must_use] 2232 pub const fn sign_count(&self) -> u32 { 2233 self.sign_count 2234 } 2235 /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata). 2236 #[inline] 2237 #[must_use] 2238 pub const fn attested_credential_data(&self) -> &AttestedCredentialData<'a> { 2239 &self.attested_credential_data 2240 } 2241 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions). 2242 #[inline] 2243 #[must_use] 2244 pub const fn extensions(&self) -> AuthenticatorExtensionOutput { 2245 self.extensions 2246 } 2247 } 2248 impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> { 2249 type Error = AuthenticatorDataErr; 2250 /// Deserializes `value` based on the 2251 /// [authenticator data structure](https://www.w3.org/TR/webauthn-3/#table-authData). 2252 #[expect( 2253 clippy::panic_in_result_fn, 2254 reason = "we want to crash when there is a bug" 2255 )] 2256 #[inline] 2257 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { 2258 Self::from_cbor(value) 2259 .map_err(AuthenticatorDataErr::from) 2260 .map(|success| { 2261 assert!( 2262 success.remaining.is_empty(), 2263 "there is a bug in AuthenticatorData::from_cbor" 2264 ); 2265 success.value 2266 }) 2267 } 2268 } 2269 impl<'a> AuthData<'a> for AuthenticatorData<'a> { 2270 type UpBitErr = Infallible; 2271 type CredData = AttestedCredentialData<'a>; 2272 type Ext = AuthenticatorExtensionOutput; 2273 fn contains_at_bit() -> bool { 2274 true 2275 } 2276 fn user_is_not_present() -> Result<(), Self::UpBitErr> { 2277 Ok(()) 2278 } 2279 fn new( 2280 rp_id_hash: &'a [u8], 2281 flags: Flag, 2282 sign_count: u32, 2283 attested_credential_data: Self::CredData, 2284 extensions: Self::Ext, 2285 ) -> Self { 2286 Self { 2287 rp_id_hash, 2288 flags, 2289 sign_count, 2290 attested_credential_data, 2291 extensions, 2292 } 2293 } 2294 fn rp_hash(&self) -> &'a [u8] { 2295 self.rp_id_hash 2296 } 2297 fn flag(&self) -> Flag { 2298 self.flags 2299 } 2300 } 2301 /// [None](https://www.w3.org/TR/webauthn-3/#sctn-none-attestation). 2302 struct NoneAttestation; 2303 impl FromCbor<'_> for NoneAttestation { 2304 type Err = AttestationErr; 2305 fn from_cbor(cbor: &[u8]) -> Result<CborSuccess<'_, Self>, Self::Err> { 2306 cbor.split_first() 2307 .ok_or(AttestationErr::Len) 2308 .and_then(|(map, remaining)| { 2309 if *map == cbor::MAP_0 { 2310 Ok(CborSuccess { 2311 value: Self, 2312 remaining, 2313 }) 2314 } else { 2315 Err(AttestationErr::NoneFormat) 2316 } 2317 }) 2318 } 2319 } 2320 /// A 64-byte slice that allegedly represents an Ed25519 signature. 2321 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2322 pub struct Ed25519Signature<'a>(&'a [u8]); 2323 impl<'a> Ed25519Signature<'a> { 2324 /// Returns signature. 2325 #[inline] 2326 #[must_use] 2327 pub const fn data(self) -> &'a [u8] { 2328 self.0 2329 } 2330 /// Transforms `self` into `Signature`. 2331 fn into_sig(self) -> Signature { 2332 let mut sig = [0; ed25519_dalek::SIGNATURE_LENGTH]; 2333 sig.copy_from_slice(self.0); 2334 Signature::from_bytes(&sig) 2335 } 2336 } 2337 impl<'a: 'b, 'b> TryFrom<&'a [u8]> for Ed25519Signature<'b> { 2338 type Error = Ed25519SignatureErr; 2339 /// Interprets `value` as an Ed25519 signature. 2340 /// 2341 /// # Errors 2342 /// 2343 /// Errors iff `value.len() != 64`. 2344 #[inline] 2345 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { 2346 if value.len() == ed25519_dalek::SIGNATURE_LENGTH { 2347 Ok(Self(value)) 2348 } else { 2349 Err(Ed25519SignatureErr) 2350 } 2351 } 2352 } 2353 impl<'a> FromCbor<'a> for Ed25519Signature<'a> { 2354 type Err = AttestationErr; 2355 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 2356 // `64 as u8` is OK. 2357 /// CBOR metadata describing the signature. 2358 #[expect( 2359 clippy::as_conversions, 2360 clippy::cast_possible_truncation, 2361 reason = "comments justify their correctness" 2362 )] 2363 const HEADER: [u8; 2] = [cbor::BYTES_INFO_24, ed25519_dalek::SIGNATURE_LENGTH as u8]; 2364 cbor.split_at_checked(HEADER.len()) 2365 .ok_or(AttestationErr::Len) 2366 .and_then(|(header, header_rem)| { 2367 if header == HEADER { 2368 header_rem 2369 .split_at_checked(ed25519_dalek::SIGNATURE_LENGTH) 2370 .ok_or(AttestationErr::Len) 2371 .map(|(sig, remaining)| CborSuccess { 2372 value: Self(sig), 2373 remaining, 2374 }) 2375 } else { 2376 Err(AttestationErr::PackedFormatCborEd25519Signature) 2377 } 2378 }) 2379 } 2380 } 2381 /// An alleged DER-encoded P-256 signature using SHA-256. 2382 struct P256DerSig<'a>(&'a [u8]); 2383 impl<'a> FromCbor<'a> for P256DerSig<'a> { 2384 type Err = AttestationErr; 2385 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 2386 // ```asn 2387 // Signature ::= SEQUENCE { 2388 // r INTEGER, 2389 // s INTEGER 2390 // } 2391 // ``` 2392 // We assume `r` and `s` can be _any_ 32-byte unsigned integer; thus when DER-encoded without metadata, 2393 // the number of bytes is inclusively between 1 and 33—INTEGER is a signed integer; thus a 0 byte must 2394 // be prepended iff the high bit is 1 (i.e., 32-byte unsigned integers with the high bit set take 33 bytes). 2395 // With metadata this makes the lengths inclusively between 3 and 35—we add 1 for the INTEGER tag and 1 2396 // for the number of bytes it takes to encode the integer. The total encoded length is thus inclusively 2397 // between 8 = 1 + 1 + 2(3) and 72 = 1 + 1 + 2(35)—we add 1 for the CONSTRUCTED SEQUENCE tag and 1 for 2398 // the number of bytes to encode the sequence. Instead of handling that specific range of lengths, 2399 // we handle all lengths inclusively between 0 and 255. 2400 cbor.split_first() 2401 .ok_or(AttestationErr::Len) 2402 .and_then(|(bytes, bytes_rem)| { 2403 if bytes & cbor::BYTES == cbor::BYTES { 2404 let len_info = bytes ^ cbor::BYTES; 2405 match len_info { 2406 ..=23 => Ok((bytes_rem, len_info)), 2407 24 => bytes_rem.split_first().ok_or(AttestationErr::Len).and_then( 2408 |(&len, len_rem)| { 2409 if len > 23 { 2410 Ok((len_rem, len)) 2411 } else { 2412 Err(AttestationErr::PackedFormatCborP256Signature) 2413 } 2414 }, 2415 ), 2416 _ => Err(AttestationErr::PackedFormatCborP256Signature), 2417 } 2418 .and_then(|(rem, len)| { 2419 rem.split_at_checked(usize::from(len)) 2420 .ok_or(AttestationErr::Len) 2421 .map(|(sig, remaining)| CborSuccess { 2422 value: Self(sig), 2423 remaining, 2424 }) 2425 }) 2426 } else { 2427 Err(AttestationErr::PackedFormatCborP256Signature) 2428 } 2429 }) 2430 } 2431 } 2432 /// An alleged DER-encoded P-384 signature using SHA-384. 2433 struct P384DerSig<'a>(&'a [u8]); 2434 impl<'a> FromCbor<'a> for P384DerSig<'a> { 2435 type Err = AttestationErr; 2436 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 2437 // ```asn 2438 // Signature ::= SEQUENCE { 2439 // r INTEGER, 2440 // s INTEGER 2441 // } 2442 // ``` 2443 // We assume `r` and `s` can be _any_ 48-byte unsigned integer; thus when DER-encoded without metadata, 2444 // the number of bytes is inclusively between 1 and 49—INTEGER is a signed integer; thus a 0 byte must 2445 // be prepended iff the high bit is 1 (i.e., 48-byte unsigned integers with the high bit set take 49 bytes). 2446 // With metadata this makes the lengths inclusively between 3 and 51—we add 1 for the INTEGER tag and 1 2447 // for the number of bytes it takes to encode the integer. The total encoded length is thus inclusively 2448 // between 8 = 1 + 1 + 2(3) and 104 = 1 + 1 + 2(51)—we add 1 for the CONSTRUCTED SEQUENCE tag and 1 for 2449 // the number of bytes to encode the sequence. Instead of handling that specific range of lengths, 2450 // we handle all lengths inclusively between 0 and 255. 2451 cbor.split_first() 2452 .ok_or(AttestationErr::Len) 2453 .and_then(|(bytes, bytes_rem)| { 2454 if bytes & cbor::BYTES == cbor::BYTES { 2455 let len_info = bytes ^ cbor::BYTES; 2456 match len_info { 2457 ..=23 => Ok((bytes_rem, len_info)), 2458 24 => bytes_rem.split_first().ok_or(AttestationErr::Len).and_then( 2459 |(&len, len_rem)| { 2460 if len > 23 { 2461 Ok((len_rem, len)) 2462 } else { 2463 Err(AttestationErr::PackedFormatCborP384Signature) 2464 } 2465 }, 2466 ), 2467 _ => Err(AttestationErr::PackedFormatCborP384Signature), 2468 } 2469 .and_then(|(rem, len)| { 2470 rem.split_at_checked(usize::from(len)) 2471 .ok_or(AttestationErr::Len) 2472 .map(|(sig, remaining)| CborSuccess { 2473 value: Self(sig), 2474 remaining, 2475 }) 2476 }) 2477 } else { 2478 Err(AttestationErr::PackedFormatCborP384Signature) 2479 } 2480 }) 2481 } 2482 } 2483 /// An alleged RSASSA-PKCS1-v1_5 signature using SHA-256. 2484 struct RsaPkcs1v15Sig<'a>(&'a [u8]); 2485 impl<'a> FromCbor<'a> for RsaPkcs1v15Sig<'a> { 2486 type Err = AttestationErr; 2487 #[expect(clippy::big_endian_bytes, reason = "CBOR integers are big-endian")] 2488 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 2489 // RSASSA-PKCS1-v1_5 signatures are the same length as the modulus. We only allow moduli consisting of 2490 // [`MIN_RSA_N_BYTES`] to [`MAX_RSA_N_BYTES`] bytes inclusively. This means 2491 // all signatures will use the same CBOR metadata tag (i.e., `cbor::BYTES_INFO_25`). 2492 cbor.split_first() 2493 .ok_or(AttestationErr::Len) 2494 .and_then(|(bytes, bytes_rem)| { 2495 if *bytes == cbor::BYTES_INFO_25 { 2496 bytes_rem 2497 .split_at_checked(2) 2498 .ok_or(AttestationErr::Len) 2499 .and_then(|(len_slice, len_rem)| { 2500 let mut cbor_len = [0; 2]; 2501 cbor_len.copy_from_slice(len_slice); 2502 // CBOR uints are big-endian. 2503 let len = usize::from(u16::from_be_bytes(cbor_len)); 2504 if len > 255 { 2505 len_rem 2506 .split_at_checked(len) 2507 .ok_or(AttestationErr::Len) 2508 .map(|(sig, remaining)| CborSuccess { 2509 value: Self(sig), 2510 remaining, 2511 }) 2512 } else { 2513 Err(AttestationErr::PackedFormatCborRs256Signature) 2514 } 2515 }) 2516 } else { 2517 Err(AttestationErr::PackedFormatCborRs256Signature) 2518 } 2519 }) 2520 } 2521 } 2522 /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation) signature. 2523 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2524 pub enum Sig<'a> { 2525 /// Alleged Ed25519 signature. 2526 Ed25519(Ed25519Signature<'a>), 2527 /// Alleged DER-encoded P-256 signature using SHA-256. 2528 P256(&'a [u8]), 2529 /// Alleged DER-encoded P-384 signature using SHA-384. 2530 P384(&'a [u8]), 2531 /// Alleged RSASSA-PKCS1-v1_5 signature using SHA-256. 2532 Rs256(&'a [u8]), 2533 } 2534 /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation). 2535 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2536 pub struct PackedAttestation<'a> { 2537 /// [Attestation signature](https://www.w3.org/TR/webauthn-3/#attestation-signature). 2538 pub signature: Sig<'a>, 2539 } 2540 impl<'a> FromCbor<'a> for PackedAttestation<'a> { 2541 type Err = AttestationErr; 2542 #[expect( 2543 clippy::too_many_lines, 2544 reason = "don't want to move code to an outer scope" 2545 )] 2546 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 2547 /// Parses `data` as packed attestation up until `x5c` _without_ the `cbor::MAP_2` or `cbor::MAP_3` header. 2548 fn parse_to_cert_chain(data: &[u8]) -> Result<CborSuccess<'_, Sig<'_>>, AttestationErr> { 2549 // {"alg":CoseAlgorithmIdentifier,"sig":sig_bytes...} 2550 /// "alg" key. 2551 const ALG: [u8; 4] = [cbor::TEXT_3, b'a', b'l', b'g']; 2552 /// "sig" key. 2553 const SIG: [u8; 4] = [cbor::TEXT_3, b's', b'i', b'g']; 2554 data.split_at_checked(ALG.len()) 2555 .ok_or(AttestationErr::Len) 2556 .and_then(|(alg, alg_rem)| { 2557 if alg == ALG { 2558 alg_rem.split_first().ok_or(AttestationErr::Len).and_then( 2559 |(cose, cose_rem)| match *cose { 2560 EDDSA => cose_rem 2561 .split_at_checked(SIG.len()) 2562 .ok_or(AttestationErr::Len) 2563 .and_then(|(sig, sig_rem)| { 2564 if sig == SIG { 2565 Ed25519Signature::from_cbor(sig_rem).map(|success| { 2566 CborSuccess { 2567 value: Sig::Ed25519(success.value), 2568 remaining: success.remaining, 2569 } 2570 }) 2571 } else { 2572 Err(AttestationErr::PackedFormatMissingSig) 2573 } 2574 }), 2575 ES256 => cose_rem 2576 .split_at_checked(SIG.len()) 2577 .ok_or(AttestationErr::Len) 2578 .and_then(|(sig, sig_rem)| { 2579 if sig == SIG { 2580 P256DerSig::from_cbor(sig_rem).map(|success| { 2581 CborSuccess { 2582 value: Sig::P256(success.value.0), 2583 remaining: success.remaining, 2584 } 2585 }) 2586 } else { 2587 Err(AttestationErr::PackedFormatMissingSig) 2588 } 2589 }), 2590 cbor::NEG_INFO_24 => cose_rem 2591 .split_first() 2592 .ok_or(AttestationErr::Len) 2593 .and_then(|(len, len_rem)| { 2594 if *len == ES384 { 2595 len_rem 2596 .split_at_checked(SIG.len()) 2597 .ok_or(AttestationErr::Len) 2598 .and_then(|(sig, sig_rem)| { 2599 if sig == SIG { 2600 P384DerSig::from_cbor(sig_rem).map( 2601 |success| CborSuccess { 2602 value: Sig::P384(success.value.0), 2603 remaining: success.remaining, 2604 }, 2605 ) 2606 } else { 2607 Err(AttestationErr::PackedFormatMissingSig) 2608 } 2609 }) 2610 } else { 2611 Err(AttestationErr::PackedFormatUnsupportedAlg) 2612 } 2613 }), 2614 cbor::NEG_INFO_25 => cose_rem 2615 .split_at_checked(2) 2616 .ok_or(AttestationErr::Len) 2617 .and_then(|(len, len_rem)| { 2618 if len == RS256 { 2619 len_rem 2620 .split_at_checked(SIG.len()) 2621 .ok_or(AttestationErr::Len) 2622 .and_then(|(sig, sig_rem)| { 2623 if sig == SIG { 2624 RsaPkcs1v15Sig::from_cbor(sig_rem).map( 2625 |success| CborSuccess { 2626 value: Sig::Rs256(success.value.0), 2627 remaining: success.remaining, 2628 }, 2629 ) 2630 } else { 2631 Err(AttestationErr::PackedFormatMissingSig) 2632 } 2633 }) 2634 } else { 2635 Err(AttestationErr::PackedFormatUnsupportedAlg) 2636 } 2637 }), 2638 _ => Err(AttestationErr::PackedFormatUnsupportedAlg), 2639 }, 2640 ) 2641 } else { 2642 Err(AttestationErr::PackedFormatMissingAlg) 2643 } 2644 }) 2645 } 2646 cbor.split_first() 2647 .ok_or(AttestationErr::Len) 2648 .and_then(|(map, map_rem)| match *map { 2649 cbor::MAP_2 => parse_to_cert_chain(map_rem).map(|success| CborSuccess { 2650 value: Self { 2651 signature: success.value, 2652 }, 2653 remaining: success.remaining, 2654 }), 2655 _ => Err(AttestationErr::PackedFormat), 2656 }) 2657 } 2658 } 2659 /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids). 2660 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2661 pub enum AttestationFormat<'a> { 2662 /// [None](https://www.w3.org/TR/webauthn-3/#sctn-none-attestation). 2663 None, 2664 /// [Packed](https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation). 2665 Packed(PackedAttestation<'a>), 2666 } 2667 impl<'a> FromCbor<'a> for AttestationFormat<'a> { 2668 type Err = AttestationErr; 2669 fn from_cbor(cbor: &'a [u8]) -> Result<CborSuccess<'a, Self>, Self::Err> { 2670 // Note we assume that cbor starts _after_ `cbor::MAP_3`. 2671 // {"fmt":"none"|"packed", "attStmt": NoneAttestation|CborPacked, ...}. 2672 /// "fmt" key. 2673 const FMT: [u8; 4] = [cbor::TEXT_3, b'f', b'm', b't']; 2674 /// "none" value. 2675 const NONE: [u8; 5] = [cbor::TEXT_4, b'n', b'o', b'n', b'e']; 2676 /// "packed" value. 2677 const PACKED: [u8; 7] = [cbor::TEXT_6, b'p', b'a', b'c', b'k', b'e', b'd']; 2678 /// "attStmt" key. 2679 const ATT_STMT: [u8; 8] = [cbor::TEXT_7, b'a', b't', b't', b'S', b't', b'm', b't']; 2680 cbor.split_at_checked(FMT.len()) 2681 .ok_or(AttestationErr::Len) 2682 .and_then(|(fmt, fmt_rem)| { 2683 if fmt == FMT { 2684 fmt_rem 2685 .split_at_checked(NONE.len()) 2686 .ok_or(AttestationErr::Len) 2687 // `PACKED.len() > NONE.len()`, so we check for `PACKED` in `and_then`. 2688 .and_then(|(none, none_rem)| { 2689 if none == NONE { 2690 none_rem 2691 .split_at_checked(ATT_STMT.len()) 2692 .ok_or(AttestationErr::Len) 2693 .and_then(|(att, att_rem)| { 2694 if att == ATT_STMT { 2695 NoneAttestation::from_cbor(att_rem).map(|success| { 2696 CborSuccess { 2697 value: Self::None, 2698 remaining: success.remaining, 2699 } 2700 }) 2701 } else { 2702 Err(AttestationErr::MissingStatement) 2703 } 2704 }) 2705 } else { 2706 fmt_rem 2707 .split_at_checked(PACKED.len()) 2708 .ok_or(AttestationErr::Len) 2709 .and_then(|(packed, packed_rem)| { 2710 if packed == PACKED { 2711 packed_rem 2712 .split_at_checked(ATT_STMT.len()) 2713 .ok_or(AttestationErr::Len) 2714 .and_then(|(att, att_rem)| { 2715 if att == ATT_STMT { 2716 PackedAttestation::from_cbor(att_rem).map( 2717 |success| CborSuccess { 2718 value: Self::Packed(success.value), 2719 remaining: success.remaining, 2720 }, 2721 ) 2722 } else { 2723 Err(AttestationErr::MissingStatement) 2724 } 2725 }) 2726 } else { 2727 Err(AttestationErr::UnsupportedFormat) 2728 } 2729 }) 2730 } 2731 }) 2732 } else { 2733 Err(AttestationErr::MissingFormat) 2734 } 2735 }) 2736 } 2737 } 2738 impl<'a> AttestationObject<'a> { 2739 /// Deserializes `data` based on the 2740 /// [attestation object layout](https://www.w3.org/TR/webauthn-3/#attestation-object) 2741 /// returning [`Self`] and the index within `data` that the authenticator data portion 2742 /// begins. 2743 #[expect(single_use_lifetimes, reason = "false positive")] 2744 #[expect( 2745 clippy::panic_in_result_fn, 2746 reason = "we want to crash when there is a bug" 2747 )] 2748 #[expect( 2749 clippy::arithmetic_side_effects, 2750 reason = "comment justifies its correctness" 2751 )] 2752 #[expect(clippy::big_endian_bytes, reason = "cbor lengths are in big-endian")] 2753 fn parse_data<'b: 'a>(data: &'b [u8]) -> Result<(Self, usize), AttestationObjectErr> { 2754 /// `authData` key. 2755 const AUTH_DATA_KEY: [u8; 9] = 2756 [cbor::TEXT_8, b'a', b'u', b't', b'h', b'D', b'a', b't', b'a']; 2757 // {"fmt":<AttestationFormat>, "attStmt":<AttestationObject>, "authData":<AuthentcatorData>}. 2758 data.split_first().ok_or(AttestationObjectErr::Len).and_then(|(map, map_rem)| { 2759 if *map == cbor::MAP_3 { 2760 AttestationFormat::from_cbor(map_rem).map_err(AttestationObjectErr::Attestation).and_then(|att| { 2761 att.remaining.split_at_checked(AUTH_DATA_KEY.len()).ok_or(AttestationObjectErr::Len).and_then(|(key, key_rem)| { 2762 if key == AUTH_DATA_KEY { 2763 key_rem.split_first().ok_or(AttestationObjectErr::Len).and_then(|(bytes, bytes_rem)| { 2764 if bytes & cbor::BYTES == cbor::BYTES { 2765 match bytes ^ cbor::BYTES { 2766 24 => bytes_rem.split_first().ok_or(AttestationObjectErr::Len).and_then(|(&len, auth_data)| { 2767 if len > 23 { 2768 Ok((usize::from(len), auth_data)) 2769 } else { 2770 Err(AttestationObjectErr::AuthDataLenInfo) 2771 } 2772 }), 2773 25 => bytes_rem.split_at_checked(2).ok_or(AttestationObjectErr::Len).and_then(|(len_slice, auth_data)| { 2774 let mut auth_len = [0; 2]; 2775 auth_len.copy_from_slice(len_slice); 2776 // Length is encoded as big-endian. 2777 let len = usize::from(u16::from_be_bytes(auth_len)); 2778 if len > 255 { 2779 Ok((len, auth_data)) 2780 } else { 2781 Err(AttestationObjectErr::AuthDataLenInfo) 2782 } 2783 }), 2784 _ => Err(AttestationObjectErr::AuthDataLenInfo), 2785 }.and_then(|(cbor_auth_data_len, auth_slice)| { 2786 if cbor_auth_data_len == auth_slice.len() { 2787 AuthenticatorData::from_cbor(auth_slice).map_err(|e| AttestationObjectErr::AuthData(e.into())).and_then(|auth_data| { 2788 assert!(auth_data.remaining.is_empty(), "there is a bug in AuthenticatorData::from_cbor"); 2789 match att.value { 2790 AttestationFormat::None => Ok(()), 2791 AttestationFormat::Packed(ref val) => match val.signature { 2792 Sig::Ed25519(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::Ed25519(_)) { 2793 Ok(()) 2794 } else { 2795 Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch) 2796 }, 2797 Sig::P256(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::P256(_)) { 2798 Ok(()) 2799 } else { 2800 Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch) 2801 }, 2802 Sig::P384(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::P384(_)) { 2803 Ok(()) 2804 } else { 2805 Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch) 2806 }, 2807 Sig::Rs256(_) => if matches!(auth_data.value.attested_credential_data.credential_public_key, UncompressedPubKey::Rsa(_)) { 2808 Ok(()) 2809 } else { 2810 Err(AttestationObjectErr::SelfAttestationAlgorithmMismatch) 2811 }, 2812 }, 2813 // `cbor_auth_data_len == `auth_slice.len()` and `auth_slice.len() < data.len()`, so underflow won't happen. 2814 }.map(|()| (Self { attestation: att.value, auth_data: auth_data.value }, data.len() - cbor_auth_data_len)) 2815 }) 2816 } else { 2817 Err(AttestationObjectErr::CborAuthDataLenMismatch) 2818 } 2819 }) 2820 } else { 2821 Err(AttestationObjectErr::AuthDataType) 2822 } 2823 }) 2824 } else { 2825 Err(AttestationObjectErr::MissingAuthData) 2826 } 2827 }) 2828 }) 2829 } else { 2830 Err(AttestationObjectErr::NotAMapOf3) 2831 } 2832 }) 2833 } 2834 } 2835 /// [Attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object). 2836 #[derive(Debug)] 2837 pub struct AttestationObject<'a> { 2838 /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids). 2839 attestation: AttestationFormat<'a>, 2840 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). 2841 auth_data: AuthenticatorData<'a>, 2842 } 2843 impl<'a> AttestationObject<'a> { 2844 /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids). 2845 #[inline] 2846 #[must_use] 2847 pub const fn attestation(&self) -> AttestationFormat<'a> { 2848 self.attestation 2849 } 2850 /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). 2851 #[inline] 2852 #[must_use] 2853 pub const fn auth_data(&self) -> &AuthenticatorData<'a> { 2854 &self.auth_data 2855 } 2856 } 2857 impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AttestationObject<'b> { 2858 type Error = AttestationObjectErr; 2859 /// Deserializes `value` based on the 2860 /// [attestation object layout](https://www.w3.org/TR/webauthn-3/#attestation-object). 2861 #[inline] 2862 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { 2863 Self::parse_data(value).map(|(val, _)| val) 2864 } 2865 } 2866 impl<'a> AuthDataContainer<'a> for AttestationObject<'a> { 2867 type Auth = AuthenticatorData<'a>; 2868 type Err = AttestationObjectErr; 2869 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 2870 #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")] 2871 fn from_data(data: &'a [u8]) -> Result<ParsedAuthData<'a, Self>, Self::Err> { 2872 // `data.len().checked_sub(Sha256::output_size())` is clearly less than `data.len()`; 2873 // thus indexing wont `panic`. 2874 Self::parse_data(&data[..data.len().checked_sub(Sha256::output_size()).unwrap_or_else(|| unreachable!("AttestationObject::from_data must be passed a slice with 32 bytes of trailing data"))]).map(|(attest, auth_idx)| ParsedAuthData { data: attest, auth_data_and_32_trailing_bytes: &data[auth_idx..], }) 2875 } 2876 fn authenticator_data(&self) -> &Self::Auth { 2877 &self.auth_data 2878 } 2879 } 2880 /// [`AuthenticatorAttestationResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse). 2881 #[derive(Debug)] 2882 pub struct AuthenticatorAttestation { 2883 /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). 2884 client_data_json: Vec<u8>, 2885 /// [attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object) followed by the SHA-256 hash 2886 /// of [`Self::client_data_json`]. 2887 attestation_object_and_c_data_hash: Vec<u8>, 2888 /// [`getTransports`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-gettransports). 2889 transports: AuthTransports, 2890 } 2891 impl AuthenticatorAttestation { 2892 /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson). 2893 #[inline] 2894 #[must_use] 2895 pub const fn client_data_json(&self) -> &[u8] { 2896 self.client_data_json.as_slice() 2897 } 2898 /// [attestation object](https://www.w3.org/TR/webauthn-3/#attestation-object). 2899 #[expect( 2900 clippy::arithmetic_side_effects, 2901 clippy::indexing_slicing, 2902 reason = "comment justifies their correctness" 2903 )] 2904 #[inline] 2905 #[must_use] 2906 pub fn attestation_object(&self) -> &[u8] { 2907 // We only allow creation via [`Self::new`] which creates [`Self::attestation_object_and_c_data_hash`] 2908 // by appending the SHA-256 hash of [`Self::client_data_json`] to the attestation object that was passed; 2909 // thus indexing is fine and subtraction won't cause underflow. 2910 &self.attestation_object_and_c_data_hash 2911 [..self.attestation_object_and_c_data_hash.len() - Sha256::output_size()] 2912 } 2913 /// [`getTransports`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-gettransports). 2914 #[inline] 2915 #[must_use] 2916 pub const fn transports(&self) -> AuthTransports { 2917 self.transports 2918 } 2919 /// Constructs an instance of `Self` with the contained data. 2920 /// 2921 /// Note calling code is encouraged to ensure `attestation_object` has at least 32 bytes 2922 /// of available capacity; if not, a reallocation will occur which may hinder performance 2923 /// depending on its size. 2924 #[inline] 2925 #[must_use] 2926 pub fn new( 2927 client_data_json: Vec<u8>, 2928 mut attestation_object: Vec<u8>, 2929 transports: AuthTransports, 2930 ) -> Self { 2931 attestation_object.extend_from_slice(&Sha256::digest(client_data_json.as_slice())); 2932 Self { 2933 client_data_json, 2934 attestation_object_and_c_data_hash: attestation_object, 2935 transports, 2936 } 2937 } 2938 } 2939 impl AuthResponse for AuthenticatorAttestation { 2940 type Auth<'a> 2941 = AttestationObject<'a> 2942 where 2943 Self: 'a; 2944 type CredKey<'a> = (); 2945 #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")] 2946 fn parse_data_and_verify_sig( 2947 &self, 2948 (): Self::CredKey<'_>, 2949 relaxed: bool, 2950 ) -> Result< 2951 (CollectedClientData<'_>, Self::Auth<'_>), 2952 AuthRespErr<<Self::Auth<'_> as AuthDataContainer<'_>>::Err>, 2953 > { 2954 if relaxed { 2955 #[cfg(not(feature = "serde_relaxed"))] 2956 unreachable!("AuthenticatorAttestation::parse_data_and_verify_sig: must be passed false when serde_relaxed is not enabled"); 2957 #[cfg(feature = "serde_relaxed")] 2958 CollectedClientData::from_client_data_json_relaxed::<true>( 2959 self.client_data_json.as_slice(), 2960 ).map_err(AuthRespErr::CollectedClientDataRelaxed) 2961 } else { 2962 CollectedClientData::from_client_data_json::<true>(self.client_data_json.as_slice()).map_err(AuthRespErr::CollectedClientData) 2963 } 2964 .and_then(|client_data_json| { 2965 Self::Auth::from_data(self.attestation_object_and_c_data_hash.as_slice()) 2966 .map_err(AuthRespErr::Auth) 2967 .and_then(|val| { 2968 match val.data.auth_data.attested_credential_data.credential_public_key { 2969 UncompressedPubKey::Ed25519(key) => key.into_ver_key().map_err(AuthRespErr::PubKey).and_then(|ver_key| { 2970 match val.data.attestation { 2971 AttestationFormat::None => Ok(()), 2972 AttestationFormat::Packed(packed) => match packed.signature { 2973 // We don't need to use `VerifyingKey::verify_strict` since 2974 // `Ed25519PubKey::into_ver_key` verifies the public key is not 2975 // in the small-order subgroup. `VerifyingKey::verify_strict` additionally 2976 // ensures _R_ of the signature is not in the small-order subgroup, but this 2977 // doesn't provide additional benefits and is still not enough to comply 2978 // with standards like RFC 8032 or NIST SP 800-186. 2979 Sig::Ed25519(sig) => ver_key.verify(val.auth_data_and_32_trailing_bytes, &sig.into_sig()).map_err(|_e| AuthRespErr::Signature), 2980 Sig::P256(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"), 2981 } 2982 } 2983 }), 2984 UncompressedPubKey::P256(key) => key.into_ver_key().map_err(AuthRespErr::PubKey).and_then(|ver_key| { 2985 match val.data.attestation { 2986 AttestationFormat::None => Ok(()), 2987 AttestationFormat::Packed(packed) => match packed.signature { 2988 Sig::P256(sig) => P256Sig::from_bytes(sig).map_err(|_e| AuthRespErr::Signature).and_then(|s| ver_key.verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)), 2989 Sig::Ed25519(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"), 2990 } 2991 } 2992 }), 2993 UncompressedPubKey::P384(key) => key.into_ver_key().map_err(AuthRespErr::PubKey).and_then(|ver_key| { 2994 match val.data.attestation { 2995 AttestationFormat::None => Ok(()), 2996 AttestationFormat::Packed(packed) => match packed.signature { 2997 Sig::P384(sig) => P384Sig::from_bytes(sig).map_err(|_e| AuthRespErr::Signature).and_then(|s| ver_key.verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)), 2998 Sig::Ed25519(_) | Sig::P256(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"), 2999 } 3000 } 3001 }), 3002 UncompressedPubKey::Rsa(key) => match val.data.attestation { 3003 AttestationFormat::None => Ok(()), 3004 AttestationFormat::Packed(packed) => match packed.signature { 3005 Sig::Rs256(sig) => pkcs1v15::Signature::try_from(sig).map_err(|_e| AuthRespErr::Signature).and_then(|s| key.as_ver_key().verify(val.auth_data_and_32_trailing_bytes, &s).map_err(|_e| AuthRespErr::Signature)), 3006 Sig::Ed25519(_) | Sig::P256(_) | Sig::P384(_) => unreachable!("there is a bug in AttestationObject::from_data"), 3007 } 3008 }, 3009 }.map(|()| (client_data_json, val.data)) 3010 }) 3011 }) 3012 } 3013 } 3014 /// [`CredentialPropertiesOutput`](https://www.w3.org/TR/webauthn-3/#dictdef-credentialpropertiesoutput). 3015 /// 3016 /// Note [`Self::rk`] is frequently unreliable. For example there are times it is `Some(false)` despite the 3017 /// credential being stored client-side. One may have better luck checking if [`AuthTransports::contains`] 3018 /// [`AuthenticatorTransport::Internal`] and using that as an indicator if a client-side credential was created. 3019 #[derive(Clone, Copy, Debug)] 3020 pub struct CredentialPropertiesOutput { 3021 /// [`rk`](https://www.w3.org/TR/webauthn-3/#dom-credentialpropertiesoutput-rk). 3022 pub rk: Option<bool>, 3023 } 3024 /// [`AuthenticationExtensionsPRFOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputs). 3025 /// 3026 /// Note since this is a server-side library, we don't store 3027 /// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) 3028 /// since it contains sensitive data that should remain client-side. 3029 #[derive(Clone, Copy, Debug)] 3030 pub struct AuthenticationExtensionsPrfOutputs { 3031 /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled). 3032 pub enabled: bool, 3033 } 3034 /// [`AuthenticationExtensionsClientOutputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientoutputs). 3035 #[derive(Clone, Copy, Debug)] 3036 pub struct ClientExtensionsOutputs { 3037 /// [`credProps`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-credprops). 3038 pub cred_props: Option<CredentialPropertiesOutput>, 3039 /// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf). 3040 pub prf: Option<AuthenticationExtensionsPrfOutputs>, 3041 } 3042 /// [`ClientExtensionsOutputs`] extensions that are saved in [`Metadata`] because they are purely informative 3043 /// and not used during authentication ceremonies. 3044 #[derive(Clone, Copy, Debug)] 3045 pub struct ClientExtensionsOutputsMetadata { 3046 /// [`ClientExtensionsOutputs::cred_props`]. 3047 pub cred_props: Option<CredentialPropertiesOutput>, 3048 } 3049 /// [`ClientExtensionsOutputs`] extensions that are saved in [`StaticState`] because they are used during 3050 /// authentication ceremonies. 3051 #[derive(Clone, Copy, Debug)] 3052 pub struct ClientExtensionsOutputsStaticState { 3053 /// [`ClientExtensionsOutputs::prf`]. 3054 pub prf: Option<AuthenticationExtensionsPrfOutputs>, 3055 } 3056 impl From<ClientExtensionsOutputs> for ClientExtensionsOutputsMetadata { 3057 #[inline] 3058 fn from(value: ClientExtensionsOutputs) -> Self { 3059 Self { 3060 cred_props: value.cred_props, 3061 } 3062 } 3063 } 3064 impl From<ClientExtensionsOutputs> for ClientExtensionsOutputsStaticState { 3065 #[inline] 3066 fn from(value: ClientExtensionsOutputs) -> Self { 3067 Self { prf: value.prf } 3068 } 3069 } 3070 /// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for registration ceremonies. 3071 #[expect( 3072 clippy::field_scoped_visibility_modifiers, 3073 reason = "no invariants to uphold" 3074 )] 3075 #[derive(Debug)] 3076 pub struct Registration { 3077 /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response). 3078 pub(crate) response: AuthenticatorAttestation, 3079 /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). 3080 pub(crate) authenticator_attachment: AuthenticatorAttachment, 3081 /// [`getClientExtensionResults()`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults). 3082 pub(crate) client_extension_results: ClientExtensionsOutputs, 3083 } 3084 impl Registration { 3085 /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response). 3086 #[inline] 3087 #[must_use] 3088 pub const fn response(&self) -> &AuthenticatorAttestation { 3089 &self.response 3090 } 3091 /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). 3092 #[inline] 3093 #[must_use] 3094 pub const fn authenticator_attachment(&self) -> AuthenticatorAttachment { 3095 self.authenticator_attachment 3096 } 3097 /// [`getClientExtensionResults()`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults). 3098 #[inline] 3099 #[must_use] 3100 pub const fn client_extension_results(&self) -> ClientExtensionsOutputs { 3101 self.client_extension_results 3102 } 3103 /// Constructs a `Registration` based on the passed arguments. 3104 #[cfg(feature = "custom")] 3105 #[inline] 3106 #[must_use] 3107 pub const fn new( 3108 response: AuthenticatorAttestation, 3109 authenticator_attachment: AuthenticatorAttachment, 3110 client_extension_results: ClientExtensionsOutputs, 3111 ) -> Self { 3112 Self { 3113 response, 3114 authenticator_attachment, 3115 client_extension_results, 3116 } 3117 } 3118 /// Returns the associated `SentChallenge`. 3119 /// 3120 /// This is useful when wanting to extract the corresponding [`RegistrationServerState`] from 3121 /// an in-memory collection (e.g., [`MaxLenHashSet`]) or storage. 3122 /// 3123 /// Note if [`CollectedClientData::from_client_data_json`] returns `Ok`, then this will return 3124 /// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse 3125 /// is _not_ true. This is because this function parses the minimal amount of data possible. 3126 /// 3127 /// # Errors 3128 /// 3129 /// Errors iff [`AuthenticatorAttestation::client_data_json`] does not contain a base64url-encoded 3130 /// [`Challenge`] in the required position. 3131 #[inline] 3132 pub fn challenge(&self) -> Result<SentChallenge, CollectedClientDataErr> { 3133 LimitedVerificationParser::<true>::get_sent_challenge( 3134 self.response.client_data_json.as_slice(), 3135 ) 3136 } 3137 /// Returns the associated `SentChallenge`. 3138 /// 3139 /// This is useful when wanting to extract the corresponding [`RegistrationServerState`] from 3140 /// an in-memory collection (e.g., [`MaxLenHashSet`]) or storage. 3141 /// 3142 /// Note if [`CollectedClientData::from_client_data_json_relaxed`] returns `Ok`, then this will return 3143 /// `Ok` containing the same value as [`CollectedClientData::challenge`]; however the converse 3144 /// is _not_ true. This is because this function attempts to reduce the amount of data parsed. 3145 /// 3146 /// # Errors 3147 /// 3148 /// Errors iff [`AuthenticatorAttestation::client_data_json`] is invalid JSON _after_ ignoring 3149 /// a leading U+FEFF and replacing any sequences of invalid UTF-8 code units with U+FFFD or 3150 /// [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) does not exist 3151 /// or is not a base64url-encoded [`Challenge`]. 3152 #[cfg(feature = "serde_relaxed")] 3153 #[inline] 3154 pub fn challenge_relaxed(&self) -> Result<SentChallenge, SerdeJsonErr> { 3155 RelaxedClientDataJsonParser::<true>::get_sent_challenge( 3156 self.response.client_data_json.as_slice(), 3157 ) 3158 } 3159 /// Convenience function for [`RegistrationRelaxed::deserialize`]. 3160 /// 3161 /// # Errors 3162 /// 3163 /// Errors iff [`RegistrationRelaxed::deserialize`] does. 3164 #[cfg(feature = "serde_relaxed")] 3165 #[inline] 3166 pub fn from_json_relaxed(json: &[u8]) -> Result<Self, SerdeJsonErr> { 3167 serde_json::from_slice::<RegistrationRelaxed>(json).map(|val| val.0) 3168 } 3169 /// Convenience function for [`CustomRegistration::deserialize`]. 3170 /// 3171 /// # Errors 3172 /// 3173 /// Errors iff [`CustomRegistration::deserialize`] does. 3174 #[cfg(feature = "serde_relaxed")] 3175 #[inline] 3176 pub fn from_json_custom(json: &[u8]) -> Result<Self, SerdeJsonErr> { 3177 serde_json::from_slice::<CustomRegistration>(json).map(|val| val.0) 3178 } 3179 } 3180 impl Response for Registration { 3181 type Auth = AuthenticatorAttestation; 3182 fn auth(&self) -> &Self::Auth { 3183 &self.response 3184 } 3185 } 3186 /// [Attestation statement](https://www.w3.org/TR/webauthn-3/#attestation-statement). 3187 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 3188 pub enum Attestation { 3189 /// [None](https://www.w3.org/TR/webauthn-3/#none). 3190 None, 3191 /// [Self](https://www.w3.org/TR/webauthn-3/#self). 3192 Surrogate, 3193 } 3194 /// Metadata associated with a [`RegisteredCredential`]. 3195 /// 3196 /// This information exists purely for informative reasons as it is not used in any way during authentication 3197 /// ceremonies; consequently, one may not want to store this information. 3198 #[derive(Clone, Copy, Debug)] 3199 pub struct Metadata<'a> { 3200 /// [Attestation statement](https://www.w3.org/TR/webauthn-3/#attestation-statement). 3201 pub attestation: Attestation, 3202 /// [`aaguid`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-aaguid). 3203 pub aaguid: Aaguid<'a>, 3204 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) output during registration that is 3205 /// never used during authentication ceremonies. 3206 pub extensions: AuthenticatorExtensionOutputMetadata, 3207 /// [`getClientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults) 3208 /// output during registration that is never used during authentication ceremonies. 3209 pub client_extension_results: ClientExtensionsOutputsMetadata, 3210 /// `ResidentKeyRequirement` sent during registration. 3211 pub resident_key: ResidentKeyRequirement, 3212 } 3213 impl Metadata<'_> { 3214 /// Transforms `self` into a JSON object conforming to the following pseudo-schema: 3215 /// 3216 /// ```json 3217 /// // MetadataJSON: 3218 /// { 3219 /// "attestation": "none" | "self", 3220 /// "aaguid": "<32-uppercase-hexadecimal digits>", 3221 /// "extensions": { 3222 /// "min_pin_length": null | <8-bit unsigned integer without leading 0s> 3223 /// }, 3224 /// "client_extension_results": { 3225 /// "cred_props": null | CredPropsJSON 3226 /// }, 3227 /// "resident_key": "required" | "discouraged" | "preferred" 3228 /// } 3229 /// // CredPropsJSON: 3230 /// { 3231 /// "rk": null | false | true 3232 /// } 3233 /// // PrfJSON: 3234 /// { 3235 /// "enabled": false | true 3236 /// } 3237 /// ``` 3238 /// where unnecessary whitespace does not exist. 3239 /// 3240 /// This primarily exists so that one can store the data in a human-readable way without the need of 3241 /// bringing the data back into the application. This allows one to read the data using some external 3242 /// means purely for informative reasons. If one wants the ability to "act" on the data; then one should 3243 /// [`Metadata::encode`] the data instead, or in addition to, that way it can be [`MetadataOwned::decode`]d. 3244 /// 3245 /// # Examples 3246 /// 3247 /// ``` 3248 /// # use core::str::FromStr; 3249 /// # use webauthn_rp::{ 3250 /// # request::register::{FourToSixtyThree, ResidentKeyRequirement}, 3251 /// # response::register::{ 3252 /// # Aaguid, Attestation, 3253 /// # AuthenticatorExtensionOutputMetadata, ClientExtensionsOutputsMetadata, CredentialPropertiesOutput, 3254 /// # Metadata, 3255 /// # }, 3256 /// # }; 3257 /// let metadata = Metadata { 3258 /// attestation: Attestation::None, 3259 /// aaguid: Aaguid::try_from([15; 16].as_slice())?, 3260 /// extensions: AuthenticatorExtensionOutputMetadata { 3261 /// min_pin_length: Some(FourToSixtyThree::Sixteen), 3262 /// }, 3263 /// client_extension_results: ClientExtensionsOutputsMetadata { 3264 /// cred_props: Some(CredentialPropertiesOutput { 3265 /// rk: Some(true), 3266 /// }), 3267 /// }, 3268 /// resident_key: ResidentKeyRequirement::Required 3269 /// }; 3270 /// let json = serde_json::json!({ 3271 /// "attestation": "none", 3272 /// "aaguid": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", 3273 /// "extensions": { 3274 /// "min_pin_length": 16 3275 /// }, 3276 /// "client_extension_results": { 3277 /// "cred_props": { 3278 /// "rk": true 3279 /// } 3280 /// }, 3281 /// "resident_key": "required" 3282 /// }); 3283 /// assert_eq!(metadata.into_json(), json.to_string()); 3284 /// # Ok::<_, webauthn_rp::AggErr>(()) 3285 /// ``` 3286 #[expect(unsafe_code, reason = "comment justifies its correctness and reason")] 3287 #[expect( 3288 clippy::arithmetic_side_effects, 3289 clippy::integer_division, 3290 clippy::integer_division_remainder_used, 3291 reason = "comments justify their correctness" 3292 )] 3293 #[inline] 3294 #[must_use] 3295 pub fn into_json(self) -> String { 3296 // Maximum capacity needed is not _that_ much larger than the minimum, 173. An example is the 3297 // following: 3298 // `{"attestation":"none","aaguid":"00000000000000000000000000000000","extensions":{"min_pin_length":null},"client_extension_results":{"cred_props":{"rk":false},"prf":{"enabled":false}},"resident_key":"discouraged"}`. 3299 // We use a raw `Vec` instead of a `String` since we need to transform some binary values into ASCII which 3300 // is easier to do as bytes. 3301 let mut buffer = Vec::with_capacity(187); 3302 buffer.extend_from_slice(br#"{"attestation":"#); 3303 buffer.extend_from_slice(match self.attestation { 3304 Attestation::None => br#""none","aaguid":""#, 3305 Attestation::Surrogate => br#""self","aaguid":""#, 3306 }); 3307 self.aaguid.0.iter().fold((), |(), byt| { 3308 // Get the first nibble. 3309 let nib_fst = byt & 0xf0; 3310 // We simply add the appropriate offset. For decimal digits this means simply adding `b'0'`; but 3311 // for uppercase hexadecimal, this means adding 55 since `b'A'` is 65. 3312 // Overflow cannot occur since this maxes at `b'F'`. 3313 buffer.push(nib_fst + if nib_fst < 0xa { b'0' } else { 55 }); 3314 // Get the second nibble. 3315 let nib_snd = byt & 0xf; 3316 // We simply add the appropriate offset. For decimal digits this means simply adding `b'0'`; but 3317 // for uppercase hexadecimal, this means adding 55 since `b'A'` is 65. 3318 // Overflow cannot occur since this maxes at `b'F'`. 3319 buffer.push(nib_snd + if nib_snd < 0xa { b'0' } else { 55 }); 3320 }); 3321 buffer.extend_from_slice(br#"","extensions":{"min_pin_length":"#); 3322 match self.extensions.min_pin_length { 3323 None => buffer.extend_from_slice(b"null"), 3324 Some(pin) => { 3325 // Clearly correct. 3326 let dig_1 = pin.into_u8() / 10; 3327 // Clearly correct. 3328 let dig_2 = pin.into_u8() % 10; 3329 if dig_1 > 0 { 3330 // We simply add the appropriate offset which is `b'0` for decimal digits. 3331 // Overflow cannot occur since this maxes at `b'9'`. 3332 buffer.push(dig_1 + b'0'); 3333 } 3334 // We simply add the appropriate offset which is `b'0` for decimal digits. 3335 // Overflow cannot occur since this maxes at `b'9'`. 3336 buffer.push(dig_2 + b'0'); 3337 } 3338 } 3339 buffer.extend_from_slice(br#"},"client_extension_results":{"cred_props":"#); 3340 match self.client_extension_results.cred_props { 3341 None => buffer.extend_from_slice(b"null"), 3342 Some(props) => { 3343 buffer.extend_from_slice(br#"{"rk":"#); 3344 match props.rk { 3345 None => buffer.extend_from_slice(b"null}"), 3346 Some(rk) => buffer.extend_from_slice(if rk { b"true}" } else { b"false}" }), 3347 } 3348 } 3349 } 3350 buffer.extend_from_slice(br#"},"resident_key":"#); 3351 buffer.extend_from_slice(match self.resident_key { 3352 ResidentKeyRequirement::Required => br#""required"}"#, 3353 ResidentKeyRequirement::Discouraged => br#""discouraged"}"#, 3354 ResidentKeyRequirement::Preferred => br#""preferred"}"#, 3355 }); 3356 // SAFETY: 3357 // Clearly above only appends ASCII, a subset of UTF-8, to `buffer`; thus `buffer` 3358 // is valid UTF-8. 3359 unsafe { String::from_utf8_unchecked(buffer) } 3360 } 3361 /// Transforms `self` into an "owned" version. 3362 #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] 3363 #[cfg(feature = "bin")] 3364 #[inline] 3365 #[must_use] 3366 pub fn into_owned(self) -> MetadataOwned { 3367 MetadataOwned { 3368 attestation: self.attestation, 3369 aaguid: AaguidOwned(self.aaguid.0.try_into().unwrap_or_else(|_e| { 3370 unreachable!( 3371 "there is a bug in Metadata that allows AAGUID to not have length of 16" 3372 ) 3373 })), 3374 extensions: self.extensions, 3375 client_extension_results: self.client_extension_results, 3376 resident_key: self.resident_key, 3377 } 3378 } 3379 } 3380 /// [`RegisteredCredential`] and [`AuthenticatedCredential`] static state. 3381 /// 3382 /// `PublicKey` needs to be [`UncompressedPubKey`] or [`CompressedPubKey`] for this type to be of any use. 3383 #[derive(Clone, Copy, Debug)] 3384 pub struct StaticState<PublicKey> { 3385 /// [`credentialPublicKey`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialpublickey). 3386 pub credential_public_key: PublicKey, 3387 /// [`extensions`](https://www.w3.org/TR/webauthn-3/#authdata-extensions) output during registration that are 3388 /// used during authentication ceremonies. 3389 pub extensions: AuthenticatorExtensionOutputStaticState, 3390 /// [`getClientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults) 3391 /// output during registration that are used during authentication ceremonies. 3392 pub client_extension_results: ClientExtensionsOutputsStaticState, 3393 } 3394 impl<'a: 'b, 'b, T: AsRef<[u8]>, T2: AsRef<[u8]>, T3: AsRef<[u8]>, T4: AsRef<[u8]>> 3395 From<&'a StaticState<CompressedPubKey<T, T2, T3, T4>>> 3396 for StaticState<CompressedPubKey<&'b [u8], &'b [u8], &'b [u8], &'b [u8]>> 3397 { 3398 #[inline] 3399 fn from(value: &'a StaticState<CompressedPubKey<T, T2, T3, T4>>) -> Self { 3400 Self { 3401 credential_public_key: (&value.credential_public_key).into(), 3402 extensions: value.extensions, 3403 client_extension_results: value.client_extension_results, 3404 } 3405 } 3406 } 3407 /// `StaticState` with an uncompressed [`Self::credential_public_key`]. 3408 pub type StaticStateUncompressed<'a> = StaticState<UncompressedPubKey<'a>>; 3409 /// `StaticState` with a compressed [`Self::credential_public_key`] that owns the key data. 3410 pub type StaticStateCompressed = StaticState<CompressedPubKeyOwned>; 3411 impl StaticStateUncompressed<'_> { 3412 /// Transforms `self` into `StaticState` that contains the compressed version of the public key. 3413 #[inline] 3414 #[must_use] 3415 pub fn into_compressed(self) -> StaticStateCompressed { 3416 StaticStateCompressed { 3417 credential_public_key: self.credential_public_key.into_compressed(), 3418 extensions: self.extensions, 3419 client_extension_results: self.client_extension_results, 3420 } 3421 } 3422 } 3423 /// [`RegisteredCredential`] and [`AuthenticatedCredential`] dynamic state. 3424 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 3425 pub struct DynamicState { 3426 /// [`UV`](https://www.w3.org/TR/webauthn-3/#authdata-flags-uv). 3427 /// 3428 /// Once this is `true`, it will remain `true`. It will be set to `true` when `false` iff 3429 /// [`Flag::user_verified`] and [`AuthenticationVerificationOptions::update_uv`]. In other words, this only 3430 /// means that the user has been verified _at some point in the past_; but it does _not_ mean the user has 3431 /// most-recently been verified this is because user verification is a _ceremony-specific_ property. To 3432 /// enforce user verification for all ceremonies, [`UserVerificationRequirement::Required`] must always be 3433 /// sent. 3434 pub user_verified: bool, 3435 /// This can only be updated if [`BackupReq`] allows for it. 3436 pub backup: Backup, 3437 /// [`signCount`](https://www.w3.org/TR/webauthn-3/#authdata-signcount). 3438 /// 3439 /// This is only updated if the authenticator supports 3440 /// [signature counters](https://www.w3.org/TR/webauthn-3/#signature-counter), and the behavior of how it is 3441 /// updated is controlled by [`AuthenticationVerificationOptions::sig_counter_enforcement`]. 3442 pub sign_count: u32, 3443 /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment). 3444 /// 3445 /// [`AuthenticationVerificationOptions::auth_attachment_enforcement`] controls if/how this updated. 3446 pub authenticator_attachment: AuthenticatorAttachment, 3447 } 3448 impl PartialEq<&Self> for DynamicState { 3449 #[inline] 3450 fn eq(&self, other: &&Self) -> bool { 3451 *self == **other 3452 } 3453 } 3454 impl PartialEq<DynamicState> for &DynamicState { 3455 #[inline] 3456 fn eq(&self, other: &DynamicState) -> bool { 3457 **self == *other 3458 } 3459 } 3460 #[cfg(test)] 3461 mod tests { 3462 use super::{ 3463 super::{ 3464 super::{ 3465 AggErr, 3466 request::{AsciiDomain, RpId}, 3467 }, 3468 auth::{AuthenticatorData, NonDiscoverableAuthenticatorAssertion}, 3469 }, 3470 AttestationFormat, AttestationObject, AuthDataContainer as _, AuthExtOutput as _, 3471 AuthTransports, AuthenticatorAttestation, AuthenticatorExtensionOutput, 3472 AuthenticatorExtensionOutputErr, Backup, CborSuccess, CredentialProtectionPolicy, 3473 FourToSixtyThree, FromCbor as _, HmacSecret, Sig, UncompressedPubKey, 3474 cbor::{ 3475 BYTES, BYTES_INFO_24, MAP_1, MAP_2, MAP_3, MAP_4, SIMPLE_FALSE, SIMPLE_TRUE, TEXT_11, 3476 TEXT_12, TEXT_14, 3477 }, 3478 }; 3479 use ed25519_dalek::Verifier as _; 3480 use p256::ecdsa::{DerSignature as P256Sig, SigningKey as P256Key}; 3481 use rsa::sha2::{Digest as _, Sha256}; 3482 #[expect(clippy::panic, reason = "OK in tests")] 3483 #[expect( 3484 clippy::arithmetic_side_effects, 3485 clippy::indexing_slicing, 3486 clippy::missing_asserts_for_indexing, 3487 reason = "comments justifies correctness" 3488 )] 3489 fn hex_decode<const N: usize>(input: &[u8; N]) -> Vec<u8> { 3490 /// Value to subtract from a lowercase hex digit. 3491 const LOWER_OFFSET: u8 = b'a' - 10; 3492 assert_eq!( 3493 N & 1, 3494 0, 3495 "hex_decode must be passed a reference to an array of even length" 3496 ); 3497 let mut data = Vec::with_capacity(N >> 1); 3498 input.chunks_exact(2).fold((), |(), byte| { 3499 // `byte.len() == 2`. 3500 let mut hex = byte[0]; 3501 let val = match hex { 3502 // `Won't underflow`. 3503 b'0'..=b'9' => hex - b'0', 3504 // `Won't underflow`. 3505 b'a'..=b'f' => hex - LOWER_OFFSET, 3506 _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"), 3507 } << 4u8; 3508 // `byte.len() == 2`. 3509 hex = byte[1]; 3510 data.push( 3511 val | match hex { 3512 // `Won't underflow`. 3513 b'0'..=b'9' => hex - b'0', 3514 // `Won't underflow`. 3515 b'a'..=b'f' => hex - LOWER_OFFSET, 3516 _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"), 3517 }, 3518 ); 3519 }); 3520 data 3521 } 3522 /// <https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-none-es256> 3523 #[expect( 3524 clippy::panic_in_result_fn, 3525 clippy::unwrap_in_result, 3526 clippy::unwrap_used, 3527 reason = "OK in tests" 3528 )] 3529 #[test] 3530 fn es256_test_vector() -> Result<(), AggErr> { 3531 let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?); 3532 let credential_private_key = 3533 hex_decode(b"6e68e7a58484a3264f66b77f5d6dc5bc36a47085b615c9727ab334e8c369c2ee"); 3534 let aaguid = hex_decode(b"8446ccb9ab1db374750b2367ff6f3a1f"); 3535 let credential_id = 3536 hex_decode(b"f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4"); 3537 let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c4535513d3d227d"); 3538 let attestation_object = hex_decode(b"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220"); 3539 let key = *P256Key::from_slice(credential_private_key.as_slice()) 3540 .unwrap() 3541 .verifying_key(); 3542 let enc_key = key.to_encoded_point(false); 3543 let auth_attest = 3544 AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0)); 3545 let att_obj = AttestationObject::from_data( 3546 auth_attest.attestation_object_and_c_data_hash.as_slice(), 3547 )?; 3548 assert_eq!( 3549 aaguid, 3550 att_obj.data.auth_data.attested_credential_data.aaguid.0 3551 ); 3552 assert_eq!( 3553 credential_id, 3554 att_obj 3555 .data 3556 .auth_data 3557 .attested_credential_data 3558 .credential_id 3559 .0 3560 ); 3561 assert!( 3562 matches!(att_obj.data.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if **enc_key.x().unwrap() == *pub_key.0 && **enc_key.y().unwrap() == *pub_key.1) 3563 ); 3564 assert_eq!( 3565 *att_obj.data.auth_data.rp_id_hash, 3566 *Sha256::digest(rp_id.as_ref()) 3567 ); 3568 assert!(att_obj.data.auth_data.flags.user_present); 3569 assert!(matches!(att_obj.data.attestation, AttestationFormat::None)); 3570 let authenticator_data = hex_decode( 3571 b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000", 3572 ); 3573 let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d"); 3574 let signature = hex_decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87"); 3575 let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user( 3576 client_data_json_2, 3577 authenticator_data, 3578 signature, 3579 ); 3580 let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; 3581 assert_eq!(*auth_data.rp_id_hash(), *Sha256::digest(rp_id.as_ref())); 3582 assert!(auth_data.flags().user_present); 3583 assert!(match att_obj.data.auth_data.flags.backup { 3584 Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible), 3585 Backup::Eligible => !matches!(auth_data.flags().backup, Backup::NotEligible), 3586 Backup::Exists => matches!(auth_data.flags().backup, Backup::Exists), 3587 }); 3588 let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap(); 3589 let mut msg = auth_assertion.authenticator_data().to_owned(); 3590 msg.extend_from_slice(&Sha256::digest(auth_assertion.client_data_json())); 3591 key.verify(msg.as_slice(), &sig).unwrap(); 3592 Ok(()) 3593 } 3594 /// <https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-packed-self-es256> 3595 #[expect( 3596 clippy::panic_in_result_fn, 3597 clippy::unwrap_in_result, 3598 clippy::unwrap_used, 3599 reason = "OK in tests" 3600 )] 3601 #[expect(clippy::indexing_slicing, reason = "comment justifies correctness")] 3602 #[test] 3603 fn es256_self_attest_test_vector() -> Result<(), AggErr> { 3604 let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?); 3605 let credential_private_key = 3606 hex_decode(b"b4bbfa5d68e1693b6ef5a19a0e60ef7ee2cbcac81f7fec7006ac3a21e0c5116a"); 3607 let aaguid = hex_decode(b"df850e09db6afbdfab51697791506cfc"); 3608 let credential_id = 3609 hex_decode(b"455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c"); 3610 let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f3078594856673d3d227d"); 3611 let attestation_object = hex_decode(b"a363666d74667061636b65646761747453746d74a263616c67266373696758483046022100ae045923ded832b844cae4d5fc864277c0dc114ad713e271af0f0d371bd3ac540221009077a088ed51a673951ad3ba2673d5029bab65b64f4ea67b234321f86fcfac5d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2"); 3612 let key = *P256Key::from_slice(credential_private_key.as_slice()) 3613 .unwrap() 3614 .verifying_key(); 3615 let enc_key = key.to_encoded_point(false); 3616 let auth_attest = 3617 AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0)); 3618 let (att_obj, auth_idx) = AttestationObject::parse_data(auth_attest.attestation_object())?; 3619 assert_eq!(aaguid, att_obj.auth_data.attested_credential_data.aaguid.0); 3620 assert_eq!( 3621 credential_id, 3622 att_obj.auth_data.attested_credential_data.credential_id.0 3623 ); 3624 assert!( 3625 matches!(att_obj.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if **enc_key.x().unwrap() == *pub_key.0 && **enc_key.y().unwrap() == *pub_key.1) 3626 ); 3627 assert_eq!( 3628 *att_obj.auth_data.rp_id_hash, 3629 *Sha256::digest(rp_id.as_ref()) 3630 ); 3631 assert!(att_obj.auth_data.flags.user_present); 3632 assert!(match att_obj.attestation { 3633 AttestationFormat::None => false, 3634 AttestationFormat::Packed(attest) => { 3635 match attest.signature { 3636 Sig::Ed25519(_) | Sig::P384(_) | Sig::Rs256(_) => false, 3637 Sig::P256(sig) => { 3638 let s = P256Sig::from_bytes(sig).unwrap(); 3639 key.verify( 3640 // Won't `panic` since `auth_idx` is returned from `AttestationObject::parse_data`. 3641 &auth_attest.attestation_object_and_c_data_hash[auth_idx..], 3642 &s, 3643 ) 3644 .is_ok() 3645 } 3646 } 3647 } 3648 }); 3649 let authenticator_data = hex_decode( 3650 b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000", 3651 ); 3652 let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d"); 3653 let signature = hex_decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744"); 3654 let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user( 3655 client_data_json_2, 3656 authenticator_data, 3657 signature, 3658 ); 3659 let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; 3660 assert_eq!(*auth_data.rp_id_hash(), *Sha256::digest(rp_id.as_ref())); 3661 assert!(auth_data.flags().user_present); 3662 assert!(match att_obj.auth_data.flags.backup { 3663 Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible), 3664 Backup::Eligible | Backup::Exists => 3665 !matches!(auth_data.flags().backup, Backup::NotEligible), 3666 }); 3667 let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap(); 3668 let mut msg = auth_assertion.authenticator_data().to_owned(); 3669 msg.extend_from_slice(&Sha256::digest(auth_assertion.client_data_json())); 3670 key.verify(msg.as_slice(), &sig).unwrap(); 3671 Ok(()) 3672 } 3673 struct AuthExtOptions<'a> { 3674 cred_protect: Option<u8>, 3675 hmac_secret: Option<bool>, 3676 min_pin_length: Option<u8>, 3677 hmac_secret_mc: Option<&'a [u8]>, 3678 } 3679 #[expect( 3680 clippy::panic, 3681 clippy::unreachable, 3682 reason = "want to crash when there is a bug" 3683 )] 3684 #[expect( 3685 clippy::arithmetic_side_effects, 3686 clippy::as_conversions, 3687 clippy::cast_possible_truncation, 3688 reason = "comments justify correctness" 3689 )] 3690 fn generate_auth_extensions(opts: &AuthExtOptions<'_>) -> Vec<u8> { 3691 // Maxes at 4, so addition is clearly free from overflow. 3692 let map_len = u8::from(opts.cred_protect.is_some()) 3693 + u8::from(opts.hmac_secret.is_some()) 3694 + u8::from(opts.min_pin_length.is_some()) 3695 + u8::from(opts.hmac_secret_mc.is_some()); 3696 let header = match map_len { 3697 0 => return Vec::new(), 3698 1 => MAP_1, 3699 2 => MAP_2, 3700 3 => MAP_3, 3701 4 => MAP_4, 3702 _ => unreachable!("bug"), 3703 }; 3704 let mut cbor = Vec::with_capacity(128); 3705 cbor.push(header); 3706 if let Some(protect) = opts.cred_protect { 3707 cbor.push(TEXT_11); 3708 cbor.extend_from_slice(b"credProtect".as_slice()); 3709 if protect >= 24 { 3710 cbor.push(24); 3711 } 3712 cbor.push(protect); 3713 } 3714 if let Some(hmac) = opts.hmac_secret { 3715 cbor.push(TEXT_11); 3716 cbor.extend_from_slice(b"hmac-secret".as_slice()); 3717 cbor.push(if hmac { SIMPLE_TRUE } else { SIMPLE_FALSE }); 3718 } 3719 if let Some(pin) = opts.min_pin_length { 3720 cbor.push(TEXT_12); 3721 cbor.extend_from_slice(b"minPinLength".as_slice()); 3722 if pin >= 24 { 3723 cbor.push(24); 3724 } 3725 cbor.push(pin); 3726 } 3727 if let Some(mc) = opts.hmac_secret_mc { 3728 cbor.push(TEXT_14); 3729 cbor.extend_from_slice(b"hmac-secret-mc".as_slice()); 3730 match mc.len() { 3731 len @ ..=23 => { 3732 // `as` is clearly OK. 3733 cbor.push(BYTES | len as u8); 3734 } 3735 len @ 24..=255 => { 3736 cbor.push(BYTES_INFO_24); 3737 // `as` is clearly OK. 3738 cbor.push(len as u8); 3739 } 3740 _ => panic!( 3741 "AuthExtOptions does not allow hmac_secret_mc to have length greater than 255" 3742 ), 3743 } 3744 cbor.extend_from_slice(mc); 3745 } 3746 cbor 3747 } 3748 #[expect(clippy::panic_in_result_fn, reason = "not a problem for a test")] 3749 #[expect(clippy::shadow_unrelated, reason = "struct destructuring is prefered")] 3750 #[expect(clippy::too_many_lines, reason = "a lot to test")] 3751 #[test] 3752 fn auth_ext() -> Result<(), AuthenticatorExtensionOutputErr> { 3753 let mut opts = generate_auth_extensions(&AuthExtOptions { 3754 cred_protect: None, 3755 hmac_secret: None, 3756 min_pin_length: None, 3757 hmac_secret_mc: None, 3758 }); 3759 let CborSuccess { value, remaining } = 3760 AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; 3761 assert!(remaining.is_empty()); 3762 assert!(value.missing()); 3763 opts = generate_auth_extensions(&AuthExtOptions { 3764 cred_protect: None, 3765 hmac_secret: None, 3766 min_pin_length: None, 3767 hmac_secret_mc: Some([0; 48].as_slice()), 3768 }); 3769 assert!( 3770 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 3771 |e| matches!(e, AuthenticatorExtensionOutputErr::Missing), 3772 |_| false, 3773 ) 3774 ); 3775 opts = generate_auth_extensions(&AuthExtOptions { 3776 cred_protect: None, 3777 hmac_secret: Some(true), 3778 min_pin_length: None, 3779 hmac_secret_mc: Some([0; 48].as_slice()), 3780 }); 3781 let CborSuccess { value, remaining } = 3782 AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; 3783 assert!(remaining.is_empty()); 3784 assert!( 3785 matches!(value.cred_protect, CredentialProtectionPolicy::None) 3786 && matches!(value.hmac_secret, HmacSecret::One) 3787 && value.min_pin_length.is_none() 3788 ); 3789 opts = generate_auth_extensions(&AuthExtOptions { 3790 cred_protect: None, 3791 hmac_secret: Some(false), 3792 min_pin_length: None, 3793 hmac_secret_mc: Some([0; 48].as_slice()), 3794 }); 3795 assert!( 3796 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 3797 |e| matches!(e, AuthenticatorExtensionOutputErr::Missing), 3798 |_| false, 3799 ) 3800 ); 3801 opts = generate_auth_extensions(&AuthExtOptions { 3802 cred_protect: None, 3803 hmac_secret: Some(true), 3804 min_pin_length: None, 3805 hmac_secret_mc: Some([0; 49].as_slice()), 3806 }); 3807 assert!( 3808 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 3809 |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcValue), 3810 |_| false, 3811 ) 3812 ); 3813 opts = generate_auth_extensions(&AuthExtOptions { 3814 cred_protect: None, 3815 hmac_secret: Some(true), 3816 min_pin_length: None, 3817 hmac_secret_mc: Some([0; 23].as_slice()), 3818 }); 3819 assert!( 3820 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 3821 |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcType), 3822 |_| false, 3823 ) 3824 ); 3825 opts = generate_auth_extensions(&AuthExtOptions { 3826 cred_protect: Some(1), 3827 hmac_secret: Some(true), 3828 min_pin_length: Some(5), 3829 hmac_secret_mc: Some([0; 48].as_slice()), 3830 }); 3831 let CborSuccess { value, remaining } = 3832 AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; 3833 assert!(remaining.is_empty()); 3834 assert!( 3835 matches!( 3836 value.cred_protect, 3837 CredentialProtectionPolicy::UserVerificationOptional 3838 ) && matches!(value.hmac_secret, HmacSecret::One) 3839 && value 3840 .min_pin_length 3841 .is_some_and(|pin| pin == FourToSixtyThree::Five) 3842 ); 3843 opts = generate_auth_extensions(&AuthExtOptions { 3844 cred_protect: Some(0), 3845 hmac_secret: None, 3846 min_pin_length: None, 3847 hmac_secret_mc: None, 3848 }); 3849 assert!( 3850 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 3851 |e| matches!(e, AuthenticatorExtensionOutputErr::CredProtectValue), 3852 |_| false, 3853 ) 3854 ); 3855 opts = generate_auth_extensions(&AuthExtOptions { 3856 cred_protect: None, 3857 hmac_secret: None, 3858 min_pin_length: Some(3), 3859 hmac_secret_mc: None, 3860 }); 3861 assert!( 3862 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 3863 |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue), 3864 |_| false, 3865 ) 3866 ); 3867 opts = generate_auth_extensions(&AuthExtOptions { 3868 cred_protect: None, 3869 hmac_secret: None, 3870 min_pin_length: Some(64), 3871 hmac_secret_mc: None, 3872 }); 3873 assert!( 3874 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 3875 |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue), 3876 |_| false, 3877 ) 3878 ); 3879 Ok(()) 3880 } 3881 }