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}