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