From 1056c78b06fddc1010afebf549434ac097a6db42 Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Sat, 9 May 2026 07:30:47 +0200 Subject: [PATCH] ctap2.3: ML-DSA-44 (alg -50) + feature-gated size bumps for FIPS 204 --- Cargo.toml | 5 +++++ src/ctap2.rs | 8 ++++---- src/ctap2/get_assertion.rs | 4 ++-- src/sizes.rs | 25 +++++++++++++++++++++++++ src/webauthn.rs | 6 ++++-- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ddae0a6..0038712 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,11 @@ arbitrary = ["dep:arbitrary", "std"] get-info-full = [] # enables support for implementing the large-blobs extension, see src/sizes.rs large-blobs = [] +# Bumps `MAX_PACKED_SIG_LENGTH`, `MAX_X5C_CERT_LENGTH`, and +# `AUTHENTICATOR_DATA_LENGTH` so packed attestation can carry an +# ML-DSA-44 (CTAP 2.3 alg -50) signature (2420 B) alongside the larger +# COSE_Key (1322 B) inside authData. See `src/sizes.rs`. +mldsa44 = [] third-party-payment = [] log-all = [] diff --git a/src/ctap2.rs b/src/ctap2.rs index 9944987..1d56bfe 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -292,8 +292,8 @@ impl<'de> Deserialize<'de> for AttestationStatement { use serde::de::Error as _; let mut alg: Option = None; - let mut sig: Option> = None; - let mut x5c: Option, 1>> = None; + let mut sig: Option> = None; + let mut x5c: Option, 1>> = None; let mut has_values = false; while let Some(key) = map.next_key::<&str>()? { has_values = true; @@ -366,9 +366,9 @@ pub struct NoneAttestationStatement {} #[cfg_attr(feature = "platform-serde", derive(Deserialize))] pub struct PackedAttestationStatement { pub alg: i32, - pub sig: Bytes, + pub sig: Bytes, #[serde(skip_serializing_if = "Option::is_none")] - pub x5c: Option, 1>>, + pub x5c: Option, 1>>, } #[derive(Clone, Debug, Default, Eq, PartialEq)] diff --git a/src/ctap2/get_assertion.rs b/src/ctap2/get_assertion.rs index 9e139c0..f9befbf 100644 --- a/src/ctap2/get_assertion.rs +++ b/src/ctap2/get_assertion.rs @@ -135,7 +135,7 @@ pub struct Request<'a> { pub struct Response { pub credential: PublicKeyCredentialDescriptor, pub auth_data: Bytes, - pub signature: Bytes, + pub signature: Bytes, #[serde(skip_serializing_if = "Option::is_none")] pub user: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -158,7 +158,7 @@ pub struct Response { pub struct ResponseBuilder { pub credential: PublicKeyCredentialDescriptor, pub auth_data: Bytes, - pub signature: Bytes, + pub signature: Bytes, } impl ResponseBuilder { diff --git a/src/sizes.rs b/src/sizes.rs index eb2c321..4837b45 100644 --- a/src/sizes.rs +++ b/src/sizes.rs @@ -1,9 +1,34 @@ +// Sized to hold ML-DSA-44 authData (header + 1312-byte raw public key +// wrapped in a ~10-byte COSE_Key map + AAGUID + credId + extensions). +// Pre-mldsa44 builds used 676 bytes which overflowed silently on alg=-50, +// surfacing as `extend_from_slice` Err → CTAP `Error::Other` (0x7F). +// Gated so non-mldsa44 builds keep the historical footprint. +#[cfg(feature = "mldsa44")] +pub const AUTHENTICATOR_DATA_LENGTH: usize = 2048; +#[cfg(not(feature = "mldsa44"))] pub const AUTHENTICATOR_DATA_LENGTH: usize = 676; // pub const AUTHENTICATOR_DATA_LENGTH_BYTES: usize = 512; pub const ASN1_SIGNATURE_LENGTH: usize = 77; // pub const ASN1_SIGNATURE_LENGTH_BYTES: usize = 72; +/// Max length of a packed-attestation signature. ECDSA over P-256 fits in +/// `ASN1_SIGNATURE_LENGTH` (77 B). With `mldsa44`, the authenticator may +/// sign with ML-DSA-44 whose signature is 2420 bytes, so we bump. +#[cfg(feature = "mldsa44")] +pub const MAX_PACKED_SIG_LENGTH: usize = 2432; +#[cfg(not(feature = "mldsa44"))] +pub const MAX_PACKED_SIG_LENGTH: usize = ASN1_SIGNATURE_LENGTH; + +/// Max length of one x5c entry (the attestation certificate carried in +/// `PackedAttestationStatement.x5c`). Matches what trussed's +/// `read_certificate` Reply.der fits in (`Message`); 1024 historically, +/// 2048 with `mldsa44` so larger Message buffers don't truncate. +#[cfg(feature = "mldsa44")] +pub const MAX_X5C_CERT_LENGTH: usize = 2048; +#[cfg(not(feature = "mldsa44"))] +pub const MAX_X5C_CERT_LENGTH: usize = 1024; + pub const COSE_KEY_LENGTH: usize = 256; // pub const COSE_KEY_LENGTH_BYTES: usize = 256; diff --git a/src/webauthn.rs b/src/webauthn.rs index 7a9b80e..c2d7cc4 100644 --- a/src/webauthn.rs +++ b/src/webauthn.rs @@ -158,9 +158,11 @@ pub enum UnknownPKCredentialParam { pub const ES256: i32 = -7; /// EdDSA pub const ED_DSA: i32 = -8; +/// ML-DSA-44 (FIPS 204, NIST level 2) +pub const ML_DSA_44: i32 = -50; -pub const COUNT_KNOWN_ALGS: usize = 2; -pub const KNOWN_ALGS: [i32; COUNT_KNOWN_ALGS] = [ES256, ED_DSA]; +pub const COUNT_KNOWN_ALGS: usize = 3; +pub const KNOWN_ALGS: [i32; COUNT_KNOWN_ALGS] = [ES256, ED_DSA, ML_DSA_44]; impl TryFrom for KnownPublicKeyCredentialParameters { type Error = UnknownPKCredentialParam;