Skip to main content

structured_pkix/
algorithm.rs

1//! Which algorithm, and the parameters that go with it.
2//!
3//! The identifier is kept as it was encoded and is not interpreted here. What
4//! may be verified with a key, and how, belongs to the algorithm registry; this
5//! layer's job is to hand that registry exactly what the certificate said,
6//! including parameters it does not understand.
7
8use crate::error::{Error, ErrorCode, Located, Result};
9use core::num::NonZeroUsize;
10use structured_asn1::memory::{Memory, Work};
11use structured_asn1::{Element, Encoding, Oid, Parser, Reader, Tag, universal};
12
13/// An algorithm and its parameters, as the certificate wrote them.
14#[derive(Debug, Clone, Copy)]
15pub struct AlgorithmIdentifier<'a> {
16    algorithm: Oid<'a>,
17    // A parameter follows the OID, so its relative start is nonzero. It is
18    // the last schema component; only an indefinite root's EOC follows it.
19    parameter_start: Option<NonZeroUsize>,
20    null_parameter: bool,
21    der: &'a [u8],
22    offset: u64,
23    field: &'static str,
24    parser: &'a Parser,
25    outer: u32,
26    encoding: Encoding,
27    indefinite: bool,
28}
29
30impl<'a> AlgorithmIdentifier<'a> {
31    /// Whether a written primitive NULL was validated by the producing reader.
32    /// Original parameter bytes remain unchanged; absence is not NULL here.
33    #[must_use]
34    pub fn has_null_parameter(self) -> bool {
35        self.null_parameter
36    }
37
38    /// Compares written values without assuming algorithm-specific defaults.
39    /// Two validated primitive NULLs share a value despite BER length spelling;
40    /// other parameters retain opaque encoded identity. Both identifiers must
41    /// come from this producing parser. This is not algorithm acceptance.
42    ///
43    /// # Errors
44    /// Returns a located resource refusal or a foreign-parser error before
45    /// comparing identifiers from another parsing session.
46    pub fn same_written_in(self, other: &Self, parent: &Reader<'_>) -> Result<bool> {
47        self.value_matches(other, parent, false)
48    }
49
50    /// Compares RFC5754 §2 SHA224/256/384/512 identifiers with absent/NULL defaults.
51    /// Unknown algorithms retain written-value semantics, not a guessed default.
52    /// Invalid known parameters and resource/caller refusals remain errors.
53    /// Selected-provider support and signature-profile acceptance are separate.
54    /// <https://www.rfc-editor.org/rfc/rfc5754#section-2>
55    ///
56    /// # Errors
57    /// Returns invalid known parameters, foreign-parser misuse, or resource
58    /// refusal separately from an ordinary identity mismatch.
59    pub fn digest_matches_in(self, other: &Self, parent: &Reader<'_>) -> Result<bool> {
60        self.value_matches(other, parent, true)
61    }
62
63    fn value_matches(self, other: &Self, parent: &Reader<'_>, digest: bool) -> Result<bool> {
64        for operand in [self, *other] {
65            if !core::ptr::eq(operand.parser, parent.parser()) {
66                return Err(Error::new(
67                    ErrorCode::ForeignReader,
68                    operand.field,
69                    operand.offset,
70                ));
71            }
72        }
73        let charge = |amount| {
74            parent.charge(amount).map_err(|error| {
75                Error::new(ErrorCode::Encoding(error.code()), other.field, other.offset)
76            })
77        };
78        let sha2 = if digest {
79            let known = self.validate_digest_params(parent)?;
80            other.validate_digest_params(parent)?;
81            known
82        } else {
83            false
84        };
85        charge(1)?;
86        let kind = self.algorithm.as_bytes();
87        if kind.len() != other.algorithm.as_bytes().len() {
88            return Ok(false);
89        }
90        charge(kind.len() as u64)?;
91        if self.algorithm != other.algorithm {
92            return Ok(false);
93        }
94        if sha2 {
95            return Ok(true);
96        }
97        if self.null_parameter && other.null_parameter {
98            return Ok(true);
99        }
100        let (Some(left), Some(right)) = (self.parameters(), other.parameters()) else {
101            return Ok(self.parameter_start.is_none() && other.parameter_start.is_none());
102        };
103        if left.len() != right.len() {
104            return Ok(false);
105        }
106        charge(left.len() as u64)?;
107        Ok(left == right)
108    }
109
110    fn validate_digest_params(self, parent: &Reader<'_>) -> Result<bool> {
111        let charge = |amount| {
112            parent.charge(amount).map_err(|error| {
113                Error::new(ErrorCode::Encoding(error.code()), self.field, self.offset)
114            })
115        };
116        charge(1)?;
117        let kind = self.algorithm.as_bytes();
118        if kind.len() != 9 {
119            return Ok(false);
120        }
121        charge(9)?;
122        if kind[..8] != [0x60, 0x86, 0x48, 1, 0x65, 3, 4, 2] || kind[8] < 1 || kind[8] > 4 {
123            return Ok(false);
124        }
125        if self.parameter_start.is_some() && !self.null_parameter {
126            // RFC5754 §2 permits only absent or NULL parameters for these four
127            // SHA2 identifiers. Unknown algorithms get no inferred default.
128            // https://www.rfc-editor.org/rfc/rfc5754#section-2
129            return Err(Error::new(
130                ErrorCode::MalformedValue,
131                self.field,
132                self.offset,
133            ));
134        }
135        Ok(true)
136    }
137    /// Which algorithm.
138    #[must_use]
139    pub fn algorithm(self) -> Oid<'a> {
140        self.algorithm
141    }
142
143    /// The parameters, as the whole encoded element, or `None` where the
144    /// certificate wrote none.
145    ///
146    /// Kept encoded because what they mean depends on the algorithm: the same
147    /// bytes are a curve for one and a hash function and salt length for
148    /// another.
149    #[must_use]
150    pub fn parameters(self) -> Option<&'a [u8]> {
151        let start = self.parameter_start?.get();
152        let end = self.der.len() - if self.indefinite { 2 } else { 0 };
153        Some(&self.der[start..end])
154    }
155
156    /// Original algorithm element offset, without scanning or deriving a new range.
157    #[must_use]
158    pub fn offset(self) -> u64 {
159        self.offset
160    }
161
162    /// Encoding selected by the original schema reader for this identifier.
163    #[must_use]
164    pub fn encoding(self) -> Encoding {
165        self.encoding
166    }
167
168    /// Opens the whole original parameter TLV at its original reader depth.
169    ///
170    /// `parent` must belong to the original parser session; rebinding its
171    /// current execution account is allowed. A foreign session is rejected
172    /// before lookup, work or allocation. Both current funding and the original
173    /// cumulative session ceiling govern all later decoding. Absence is only
174    /// returned after admission, never as a resource fallback.
175    ///
176    /// The returned depth equals an immediate `inside.reread(parameter, encoding)`
177    /// after this identifier was read, including an IMPLICIT context envelope.
178    /// It includes the parameter header, not just its contents; no replacement
179    /// parser, canonical serialization or extra parameter nesting is introduced.
180    pub fn parameters_reader_in<'query>(
181        self,
182        parent: &Reader<'query>,
183        field: &'static str,
184    ) -> Result<Option<Reader<'query>>>
185    where
186        'a: 'query,
187    {
188        if !self.same_session(parent) {
189            return Err(Error::new(ErrorCode::ForeignReader, field, self.offset));
190        }
191        let at = self
192            .parameter_start
193            .map_or(self.offset, |start| self.offset + start.get() as u64);
194        parent
195            .execution()
196            .charge_work(1)
197            .map_err(|reason| Error::resource(reason, field, at))?;
198        let Some(parameter) = self.parameters() else {
199            return Ok(None);
200        };
201        self.parser
202            .read_nested_query_in(parent, parameter, at, self.encoding, self.outer)
203            .field(field)
204            .map(Some)
205    }
206
207    /// Whether this original identifier was produced by this parser session.
208    /// Account equality does not establish source/session identity. This O(1)
209    /// metadata predicate reads no parameters and spends no data-dependent work.
210    #[must_use]
211    pub fn same_session(self, parent: &Reader<'_>) -> bool {
212        core::ptr::eq(self.parser, parent.parser())
213    }
214
215    /// The whole identifier as it was encoded.
216    ///
217    /// For [`read_implicit`], this retains the original context tag, not a
218    /// synthesized SEQUENCE suitable for reparsing as an untagged identifier.
219    #[must_use]
220    pub fn as_bytes(self) -> &'a [u8] {
221        self.der
222    }
223
224    /// Whether the parameters are absent, or the null that means the same
225    /// thing for the algorithms that spell it that way.
226    #[must_use]
227    pub fn has_no_parameters(self) -> bool {
228        // A null is two bytes: the tag and a zero length.
229        self.parameters()
230            .is_none_or(|parameters| parameters == [0x05, 0x00])
231    }
232
233    /// Whether two identifiers name the same algorithm with the same
234    /// parameters.
235    ///
236    /// An absent parameter and an explicit null are treated as the same, and
237    /// nothing else is. Issuers disagree about which of the two to write for
238    /// the algorithms that take no parameters, and both say the same thing;
239    /// any other difference changes what is being verified.
240    ///
241    /// The equivalence is deliberately not algorithm-specific. Which
242    /// parameter encodings an algorithm permits at all, including that the
243    /// Edwards-curve identifiers forbid the null form, is the algorithm
244    /// registry's decision, made against the identifier inside the signed
245    /// bytes. This comparison only asks whether the unsigned outer identifier
246    /// agrees with that signed one, and absent versus null cannot change
247    /// which algorithm that is: an outer null beside a signed absent is a
248    /// malformed spelling of the same identity, not a different algorithm
249    /// escaping the mismatch check.
250    ///
251    /// Deferred comparisons charge the supplied current operation. Their
252    /// resource diagnostics identify `self`'s original field and offset; they
253    /// do not assert that the two identifiers share a source. Parser-local
254    /// limits apply only when comparing through the producing reader.
255    ///
256    /// # Errors
257    /// Returns a resource refusal before an unfunded byte comparison.
258    ///
259    /// # Examples
260    /// ```
261    /// # extern crate alloc;
262    /// # use alloc::sync::Arc;
263    /// # use structured_asn1::{Parser, Tag, universal};
264    /// # use structured_asn1::memory::{Memory, MemoryAccount};
265    /// # let execution = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
266    /// # let parser = Parser::der(execution.clone());
267    /// # let mut reader = parser.read(b"\x30\x04\x06\x02\x2a\x03", 0).unwrap();
268    /// # let element = reader.expect(Tag::constructed(universal::SEQUENCE)).unwrap();
269    /// let identifier = structured_pkix::algorithm::read(&reader, &element, "algorithm")?;
270    /// assert!(identifier.matches(&identifier, &execution)?);
271    /// # Ok::<(), structured_pkix::Error>(())
272    /// ```
273    pub fn matches(self, other: &AlgorithmIdentifier<'_>, execution: &Memory) -> Result<bool> {
274        self.matches_with(other, |work| {
275            execution
276                .charge_work(work)
277                .map_err(|reason| Error::resource(reason, self.field, self.offset))
278        })
279    }
280
281    pub(crate) fn matches_in(
282        self,
283        other: &AlgorithmIdentifier<'_>,
284        parent: &Reader<'_>,
285    ) -> Result<bool> {
286        self.matches_with(other, |work| {
287            parent.charge(work).map_err(|error| {
288                Error::new(ErrorCode::Encoding(error.code()), self.field, self.offset)
289            })
290        })
291    }
292
293    fn matches_with(
294        self,
295        other: &AlgorithmIdentifier<'_>,
296        mut charge: impl FnMut(u64) -> Result<()>,
297    ) -> Result<bool> {
298        charge(1)?;
299        if self.algorithm.as_bytes().len() != other.algorithm.as_bytes().len() {
300            return Ok(false);
301        }
302        charge(self.algorithm.as_bytes().len() as u64)?;
303        if self.algorithm != other.algorithm {
304            return Ok(false);
305        }
306        if self.has_no_parameters() && other.has_no_parameters() {
307            return Ok(true);
308        }
309        let (Some(left), Some(right)) = (self.parameters(), other.parameters()) else {
310            return Ok(false);
311        };
312        if left.len() != right.len() {
313            return Ok(false);
314        }
315        charge(left.len() as u64)?;
316        Ok(left == right)
317    }
318}
319
320/// Reads an algorithm identifier from an element a schema said is one.
321///
322/// It is public because certificates are not the only place this structure
323/// appears: a signed message names a digest and a signature algorithm the same
324/// way, and reading them a second time somewhere else would be a second set of
325/// rules about what parameters are allowed to look like.
326///
327/// # Errors
328///
329/// Reports the encoding fault and the field it was in.
330pub fn read<'a>(
331    parent: &Reader<'a>,
332    element: &Element<'a>,
333    field: &'static str,
334) -> Result<AlgorithmIdentifier<'a>> {
335    read_tagged(
336        parent,
337        element,
338        Tag::constructed(universal::SEQUENCE),
339        field,
340    )
341}
342
343/// Reads an original `[number] IMPLICIT AlgorithmIdentifier` without retagging.
344///
345/// The caller supplies the tag number selected by its trusted schema. For
346/// example, `CMSAlgorithmProtection` uses `[1]` for a signature and `[2]` for a
347/// MAC (RFC6211 §2); that cardinality/choice rule belongs to the CMS schema.
348/// The same inner algorithm/parameter core as [`read`] preserves original
349/// bytes, encoding, inherited depth and current producing execution ceilings.
350/// <https://www.rfc-editor.org/rfc/rfc6211#section-2>
351pub fn read_implicit<'a>(
352    parent: &Reader<'a>,
353    element: &Element<'a>,
354    number: u32,
355    field: &'static str,
356) -> Result<AlgorithmIdentifier<'a>> {
357    read_tagged(parent, element, Tag::context(true, number), field)
358}
359
360fn read_tagged<'a>(
361    parent: &Reader<'a>,
362    element: &Element<'a>,
363    expected: Tag,
364    field: &'static str,
365) -> Result<AlgorithmIdentifier<'a>> {
366    let fail = |code| Error::new(code, field, element.span().start);
367
368    if element.tag() != expected {
369        return Err(fail(ErrorCode::Encoding(
370            structured_asn1::ErrorCode::UnexpectedTag,
371        )));
372    }
373
374    let mut inside = parent.enter(element).field(field)?;
375    let algorithm = inside.read_oid().field(field)?;
376    let parameter = inside.next_element().field(field)?;
377
378    // What the parameter says is the registry's business: a curve, a hash,
379    // the salt length of a padding scheme. It is handed the bytes on the
380    // promise that they are the encoding the document claims, so the whole
381    // value is held to that and not only its outer header.
382    let mut null_parameter = false;
383    if let Some(parameter) = &parameter {
384        crate::name::walk_der(&inside, parameter, field, crate::name::Strings::Checked)?;
385        if parameter.tag() == Tag::primitive(universal::NULL) {
386            inside.null(parameter).field(field)?;
387            null_parameter = true;
388        }
389    }
390    // The schema has room for one parameter and no more, and a second would
391    // leave which of them the algorithm takes with no answer.
392    if !inside.is_empty() {
393        return Err(fail(ErrorCode::TrailingData));
394    }
395
396    let parameter_start = match parameter {
397        None => None,
398        Some(parameter) => {
399            let start = usize::try_from(parameter.span().start - element.span().start)
400                .map_err(|_| fail(ErrorCode::ValueTooLarge))?;
401            Some(NonZeroUsize::new(start).expect("the algorithm OID precedes its parameters"))
402        }
403    };
404
405    Ok(AlgorithmIdentifier {
406        algorithm,
407        parameter_start,
408        null_parameter,
409        der: element.as_bytes(),
410        offset: element.span().start,
411        field,
412        parser: parent.parser(),
413        outer: parent.depth(),
414        encoding: inside.encoding(),
415        indefinite: element.is_indefinite(),
416    })
417}
418
419#[cfg(test)]
420mod tests;