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) = ¶meter {
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;