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