Skip to main content

structured_pkix/algorithm/
ml_dsa.rs

1//! Pure ML-DSA identities and wire sizes, without a cryptographic engine.
2//!
3//! RFC 9881 §§2–4 defines three distinct PKIX identifiers, all with absent
4//! parameters. HashML-DSA and pre-standard Dilithium are different algorithms.
5//! For certificate/CRL verification the provider must use pure ML-DSA with an
6//! empty context (RFC 9881 §3), not a conventional finalized-message prehash.
7//! <https://www.rfc-editor.org/rfc/rfc9881#section-3>
8
9use super::AlgorithmIdentifier;
10use crate::{Error, ErrorCode, Result};
11use structured_asn1::memory::Work;
12
13/// A pure ML-DSA parameter set, not provider capability or policy permission.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ParameterSet {
16    /// ML-DSA-44, OID `2.16.840.1.101.3.4.3.17`.
17    MlDsa44,
18    /// ML-DSA-65, OID `2.16.840.1.101.3.4.3.18`.
19    MlDsa65,
20    /// ML-DSA-87, OID `2.16.840.1.101.3.4.3.19`.
21    MlDsa87,
22}
23
24impl ParameterSet {
25    /// Raw public-key width (FIPS 204 table 2; RFC 9881 §4 and appendix B).
26    #[must_use]
27    pub const fn public_key_len(self) -> usize {
28        // RFC9881 appendix A has a 2602 typo for ML-DSA-87; §4, appendix B
29        // and the defining FIPS204 table2 all specify 2592.
30        // https://doi.org/10.6028/NIST.FIPS.204
31        match self {
32            Self::MlDsa44 => 1312,
33            Self::MlDsa65 => 1952,
34            Self::MlDsa87 => 2592,
35        }
36    }
37
38    /// Raw signature width, not an assertion of valid packed coefficients.
39    /// FIPS 204 §3.6.2 and table 2 require exact key/signature lengths.
40    #[must_use]
41    pub const fn signature_len(self) -> usize {
42        match self {
43            Self::MlDsa44 => 2420,
44            Self::MlDsa65 => 3309,
45            Self::MlDsa87 => 4627,
46        }
47    }
48}
49
50/// Resolves a retained identifier under original and current work ceilings.
51///
52/// Returns `None` for another algorithm, including HashML-DSA. A recognized
53/// identifier with any parameters, even primitive NULL, is malformed. No
54/// cursor, allocation, account clone or byte copy is needed. Provider support,
55/// packed-value validation, key usage, context selection and trust are separate.
56///
57/// # Errors
58/// Original located parameter/resource failures; resource refusal precedes
59/// classification and is never an unknown algorithm or malformed parameter.
60///
61/// # Examples
62/// ```
63/// use std::sync::Arc;
64/// use structured_asn1::{Parser, memory::{Memory, MemoryAccount}};
65/// use structured_pkix::algorithm::{self, ml_dsa::{self, ParameterSet}};
66/// let work = Memory::new(Arc::new(MemoryAccount::new(1024, 4096)));
67/// let source = Parser::der(work.clone());
68/// let bytes = b"\x30\x0b\x06\x09\x60\x86\x48\x01\x65\x03\x04\x03\x11";
69/// let mut reader = source.read(bytes, 400).unwrap();
70/// let element = reader.next_element().unwrap().unwrap();
71/// let id = algorithm::read(&reader, &element, "algorithm")?;
72/// assert_eq!(ml_dsa::resolve(id, &work)?, Some(ParameterSet::MlDsa44));
73/// # Ok::<(), structured_pkix::Error>(())
74/// ```
75pub fn resolve<W: Work>(
76    identifier: AlgorithmIdentifier<'_>,
77    execution: &W,
78) -> Result<Option<ParameterSet>> {
79    let bytes = identifier.algorithm().as_bytes();
80    identifier
81        .query_in(execution)
82        .charge_work(bytes.len() as u64 + 1)
83        .map_err(|reason| Error::resource(reason, identifier.field, identifier.offset()))?;
84    let value = from_octets(bytes);
85    if value.is_some() {
86        require_absent(identifier)?;
87    }
88    Ok(value)
89}
90
91/// The caller has already paid for inspecting the original OID.
92pub(super) fn from_octets(bytes: &[u8]) -> Option<ParameterSet> {
93    match bytes {
94        [0x60, 0x86, 0x48, 1, 0x65, 3, 4, 3, last] => from_arc(*last),
95        _ => None,
96    }
97}
98
99/// Shared final arc mapping after a paid, exact sigAlgs prefix match.
100pub(super) fn from_arc(last: u8) -> Option<ParameterSet> {
101    match last {
102        17 => Some(ParameterSet::MlDsa44),
103        18 => Some(ParameterSet::MlDsa65),
104        19 => Some(ParameterSet::MlDsa87),
105        _ => None,
106    }
107}
108
109pub(super) fn require_absent(identifier: AlgorithmIdentifier<'_>) -> Result<()> {
110    // RFC9881§2 forbids even NULL in both SPKI and signature identifiers.
111    // https://www.rfc-editor.org/rfc/rfc9881#section-2
112    if let Some(parameters) = identifier.parameters_nested() {
113        return Err(Error::new(
114            ErrorCode::MalformedValue,
115            identifier.field,
116            parameters.offset(),
117        ));
118    }
119    Ok(())
120}