Skip to main content

structured_pkix/algorithm/
signature.rs

1//! Original signature algorithm structure, without provider or policy selection.
2//!
3//! Recognizes SHA-family RSA PKCS#1 v1.5, RFC3279/5758 DSA, ECDSA,
4//! RSASSA-PSS, pure Ed25519/Ed448 and RFC9881 ML-DSA. It does not infer a scheme from a key,
5//! signature size or separately written CMS digest. Unknown OIDs, including
6//! legacy OIW aliases, MD2/MD5, additional DSA profiles and other PQ, remain unknown.
7
8use super::{AlgorithmIdentifier, digest, ml_dsa, pss};
9use crate::{Error, ErrorCode, Result};
10use structured_asn1::memory::Work;
11
12mod inspection;
13pub use inspection::{Inspection, with_inspection};
14
15/// The scheme named by an OID, independently of parameter conformance.
16/// Recognition does not establish provider support or usable signature inputs.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum Scheme {
20    /// RSA PKCS#1 v1.5 with an OID-implied digest.
21    RsaPkcs1v15(digest::Algorithm),
22    /// RSASSA-PSS; actual parameters must be read separately.
23    RsaPss,
24    /// DSA with an OID-implied digest.
25    Dsa(digest::Algorithm),
26    /// ECDSA with an OID-implied digest.
27    Ecdsa(digest::Algorithm),
28    /// Pure Ed25519.
29    Ed25519,
30    /// Pure Ed448.
31    Ed448,
32    /// Pure ML-DSA with an empty PKIX context.
33    MlDsa(ml_dsa::ParameterSet),
34}
35
36/// A recognized signature structure, not backend support or authorization.
37///
38/// Fixed digests are implied by the signature OID; no encoded digest identifier
39/// is fabricated. The caller keeps the original signature identifier for
40/// provenance, provider dispatch and unsupported-algorithm diagnostics.
41#[derive(Debug, Clone, Copy)]
42#[non_exhaustive]
43pub enum Algorithm<'a, 'parameters> {
44    /// RSA PKCS#1 v1.5 with the indicated digest.
45    RsaPkcs1v15(digest::Algorithm),
46    /// Original PSS parameters, without narrowing integers or choosing a backend.
47    RsaPss(&'parameters pss::Parameters<'a>),
48    /// DSA with SHA-1, SHA-224 or SHA-256.
49    Dsa(digest::Algorithm),
50    /// ECDSA with the indicated digest.
51    Ecdsa(digest::Algorithm),
52    /// Pure Ed25519: not Ed25519ph or Ed25519ctx.
53    Ed25519,
54    /// Pure Ed448: not Ed448ph.
55    Ed448,
56    /// Pure ML-DSA; certificate/CRL verification requires an empty context.
57    /// This does not authorize HashML-DSA or imply a finalized-message digest.
58    MlDsa(ml_dsa::ParameterSet),
59}
60
61#[derive(Clone, Copy)]
62enum Parameters {
63    Absent,
64    Null,
65    NullOrAbsent,
66    Pss,
67}
68
69/// Visits a retained signature structure under original and current work.
70///
71/// RSA SHA-224/256/384/512 accepts absent or primitive NULL on reception
72/// (RFC4055 §5). Recognized RSA SHA-1, SHA-512/224, SHA-512/256 and SHA3 require
73/// NULL (RFC3279 §2.2.1, RFC8017 A.2.4, RFC9688 §3.1). DSA SHA-1/224/256,
74/// ECDSA SHA-1/SHA-2/SHA3 and pure Ed25519/Ed448 require absent parameters
75/// (RFC3279 §2.2, RFC5758 §3, RFC9688 §3.2, RFC8410 §6). BER NULL spellings are
76/// validated by the original reader; bytes are never compared to fabricated DER.
77/// ML-DSA-44/65/87 also require absent parameters (RFC9881 §2); the PKIX
78/// provider must use pure mode and an empty context (RFC9881 §3).
79///
80/// PSS signature parameters must be present (RFC4055 §3.1), unlike optional
81/// key restrictions. The existing strict PSS kernel preserves independent
82/// message/MGF hashes, wide salt/trailer fields and original ranges/depth;
83/// DER written DEFAULTs remain errors. Use [`pss::inspect`] for readable PSS
84/// diagnostics. Backend support, PSS compatibility, key strength, policy and
85/// signature math remain separate. A known structure does not promise that
86/// its provider implements it or accepts its parameters.
87///
88/// Visits `None` for identifiers outside the module's documented recognized
89/// set. Both original parser and current operation ceilings apply before any
90/// OID scan; a fresh account cannot reset source limits. Fixed cases allocate
91/// nothing and need no cursor. PSS fields are borrowed only for the visit,
92/// keeping the enum small without allocating a wrapper. The returned `R` may
93/// borrow original document bytes, or explicitly copy PSS fields, but cannot
94/// keep a reference to the temporary PSS descriptor. The visitor's result,
95/// including a caller-specific `Result`, is returned unchanged inside the
96/// outer processing result. The visitor is not called on a processing error.
97///
98/// # Errors
99/// Located missing/invalid parameters or strict PSS errors; terminal resource
100/// errors remain distinct from unknown algorithms and malformed parameters.
101///
102/// # Examples
103/// ```
104/// use std::sync::Arc;
105/// use structured_asn1::{Parser, memory::{Memory, MemoryAccount}};
106/// use structured_pkix::algorithm::{self, digest, signature};
107/// let source = Parser::der(Memory::new(Arc::new(MemoryAccount::new(4096, 4096))));
108/// // ecdsa-with-SHA256, with parameters absent.
109/// let bytes = b"\x30\x0a\x06\x08\x2a\x86\x48\xce\x3d\x04\x03\x02";
110/// let mut reader = source.read(bytes, 400).unwrap();
111/// let element = reader.next_element().unwrap().unwrap();
112/// let identifier = algorithm::read(&reader, &element, "signatureAlgorithm")?;
113/// let current = Memory::new(Arc::new(MemoryAccount::new(0, 4096)));
114/// let recognized = signature::with_algorithm(identifier, &current, |algorithm|
115///     matches!(algorithm, Some(signature::Algorithm::Ecdsa(digest::Algorithm::Sha256))))?;
116/// assert!(recognized);
117/// # Ok::<(), structured_pkix::Error>(())
118/// ```
119/// A reference to the temporary PSS descriptor cannot escape:
120/// ```compile_fail
121/// use structured_pkix::algorithm::{AlgorithmIdentifier, pss, signature};
122/// use structured_asn1::memory::Memory;
123/// fn escape<'a>(id: AlgorithmIdentifier<'a>, work: &Memory)
124///     -> structured_pkix::Result<&'a pss::Parameters<'a>> {
125///     signature::with_algorithm(id, work, |algorithm| match algorithm {
126///         Some(signature::Algorithm::RsaPss(parameters)) => parameters,
127///         _ => panic!("PSS expected"),
128///     })
129/// }
130/// ```
131pub fn with_algorithm<'a, W: Work, R>(
132    identifier: AlgorithmIdentifier<'a>,
133    execution: &W,
134    visitor: impl for<'parameters> FnOnce(Option<Algorithm<'a, 'parameters>>) -> R,
135) -> Result<R> {
136    let Some((scheme, parameters)) = classify(identifier, execution)? else {
137        return Ok(visitor(None));
138    };
139    if scheme == Scheme::RsaPss {
140        let value = pss::read_known(identifier, execution)?.ok_or_else(|| {
141            Error::new(
142                ErrorCode::MissingField,
143                identifier.field,
144                identifier.offset(),
145            )
146        })?;
147        return Ok(visitor(Some(Algorithm::RsaPss(&value))));
148    }
149    check_parameters(identifier, parameters)?;
150    Ok(visitor(fixed_algorithm(scheme)))
151}
152
153fn fixed_algorithm(scheme: Scheme) -> Option<Algorithm<'static, 'static>> {
154    Some(match scheme {
155        Scheme::RsaPkcs1v15(hash) => Algorithm::RsaPkcs1v15(hash),
156        Scheme::Dsa(hash) => Algorithm::Dsa(hash),
157        Scheme::Ecdsa(hash) => Algorithm::Ecdsa(hash),
158        Scheme::Ed25519 => Algorithm::Ed25519,
159        Scheme::Ed448 => Algorithm::Ed448,
160        Scheme::MlDsa(set) => Algorithm::MlDsa(set),
161        Scheme::RsaPss => return None,
162    })
163}
164
165fn classify<W: Work>(
166    identifier: AlgorithmIdentifier<'_>,
167    execution: &W,
168) -> Result<Option<(Scheme, Parameters)>> {
169    use digest::Algorithm as Hash;
170    let bytes = identifier.algorithm().as_bytes();
171    identifier
172        .query_in(execution)
173        .charge_work(bytes.len() as u64 + 1)
174        .map_err(|reason| Error::resource(reason, identifier.field, identifier.offset()))?;
175    let (algorithm, parameters) = match bytes {
176        // RFC8017 A.2.4 assigns distinct signature OIDs, not digest OIDs.
177        // https://www.rfc-editor.org/rfc/rfc8017#appendix-A.2.4
178        [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, last] => {
179            let (hash, parameters) = match last {
180                5 => (Hash::Sha1, Parameters::Null),
181                11 => (Hash::Sha256, Parameters::NullOrAbsent),
182                12 => (Hash::Sha384, Parameters::NullOrAbsent),
183                13 => (Hash::Sha512, Parameters::NullOrAbsent),
184                14 => (Hash::Sha224, Parameters::NullOrAbsent),
185                15 => (Hash::Sha512_224, Parameters::Null),
186                16 => (Hash::Sha512_256, Parameters::Null),
187                10 => return Ok(Some((Scheme::RsaPss, Parameters::Pss))),
188                _ => return Ok(None),
189            };
190            (Scheme::RsaPkcs1v15(hash), parameters)
191        }
192        [0x2a, 0x86, 0x48, 0xce, 0x38, 4, 3] => (Scheme::Dsa(Hash::Sha1), Parameters::Absent),
193        [0x2a, 0x86, 0x48, 0xce, 0x3d, 4, 1] => (Scheme::Ecdsa(Hash::Sha1), Parameters::Absent),
194        [0x2a, 0x86, 0x48, 0xce, 0x3d, 4, 3, last] => {
195            let hash = match last {
196                1 => Hash::Sha224,
197                2 => Hash::Sha256,
198                3 => Hash::Sha384,
199                4 => Hash::Sha512,
200                _ => return Ok(None),
201            };
202            (Scheme::Ecdsa(hash), Parameters::Absent)
203        }
204        [0x60, 0x86, 0x48, 1, 0x65, 3, 4, 3, last] => {
205            // SHA3 digest and signature parameter rules differ by role.
206            // https://www.rfc-editor.org/rfc/rfc9688#section-3
207            match last {
208                1 => (Scheme::Dsa(Hash::Sha224), Parameters::Absent),
209                2 => (Scheme::Dsa(Hash::Sha256), Parameters::Absent),
210                9..=16 => {
211                    let hash = match last {
212                        9 | 13 => Hash::Sha3_224,
213                        10 | 14 => Hash::Sha3_256,
214                        11 | 15 => Hash::Sha3_384,
215                        _ => Hash::Sha3_512,
216                    };
217                    if *last <= 12 {
218                        (Scheme::Ecdsa(hash), Parameters::Absent)
219                    } else {
220                        (Scheme::RsaPkcs1v15(hash), Parameters::Null)
221                    }
222                }
223                _ => {
224                    // The prefix was already paid and matched. Share the final
225                    // arc and parameter kernel without another OID scan.
226                    if let Some(set) = ml_dsa::from_arc(*last) {
227                        return Ok(Some((Scheme::MlDsa(set), Parameters::Absent)));
228                    }
229                    return Ok(None);
230                }
231            }
232        }
233        [0x2b, 0x65, 112] => (Scheme::Ed25519, Parameters::Absent),
234        [0x2b, 0x65, 113] => (Scheme::Ed448, Parameters::Absent),
235        _ => return Ok(None),
236    };
237    Ok(Some((algorithm, parameters)))
238}
239
240fn check_parameters(identifier: AlgorithmIdentifier<'_>, parameters: Parameters) -> Result<()> {
241    let present = identifier.parameters_nested();
242    let valid = match parameters {
243        // NULL is present, not an absent DSA/ECDSA or pure EdDSA parameter.
244        // https://www.rfc-editor.org/rfc/rfc5758#section-3
245        // https://www.rfc-editor.org/rfc/rfc8410#section-6
246        Parameters::Absent => present.is_none(),
247        // Signature AlgorithmIdentifier, not SHA3 digest AlgorithmIdentifier.
248        // https://www.rfc-editor.org/rfc/rfc9688#section-3.1
249        Parameters::Null => identifier.has_null_parameter(),
250        // RFC4055 §5 explicitly requires both forms on reception.
251        // https://www.rfc-editor.org/rfc/rfc4055#section-5
252        Parameters::NullOrAbsent => present.is_none() || identifier.has_null_parameter(),
253        Parameters::Pss => unreachable!("PSS uses its schema kernel"),
254    };
255    if !valid {
256        let at = present.map_or(identifier.offset(), super::Nested::offset);
257        let code = if present.is_some() {
258            ErrorCode::MalformedValue
259        } else {
260            ErrorCode::MissingField
261        };
262        return Err(Error::new(code, identifier.field, at));
263    }
264    Ok(())
265}