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, 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, &current, |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}