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, ¤t, |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}