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