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