Skip to main content

structured_pkix/algorithm/
pss.rs

1//! Borrowed RSASSA-PSS parameters, without selecting a cryptographic provider.
2//!
3//! RFC4055 §3.1 distinguishes absent key restrictions from absent signature
4//! parameters. [`read_in`] preserves absence; its caller applies that role rule.
5//! Hash/MGF support, key sanity, salt bounds and signature verification remain
6//! with the selected provider. No replacement parser or encoded default is made.
7//! Retained identifiers use [`read`] or [`inspect`] under current execution;
8//! [`read_in`] and [`inspect_in`] additionally check a producing reader session.
9
10use super::AlgorithmIdentifier;
11use crate::error::{Error, ErrorCode, Located, Result};
12use structured_asn1::memory::{OwnedVec, Work};
13use structured_asn1::{Element, Integer, Oid, ScopedReader as Reader, Tag, universal};
14
15use crate::oid::SHA1;
16pub mod profile;
17const MGF1: Oid<'static> = Oid::from_bytes(&[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 8]);
18const FIELD: &str = "algorithm.pss";
19
20/// An explicitly written field with its original EXPLICIT wrapper.
21#[derive(Debug, Clone, Copy)]
22pub struct Written<'a, T> {
23    element: Element<'a>,
24    value: T,
25}
26
27impl<'a, T: Copy> Written<'a, T> {
28    /// Original context-specific wrapper, including its source span.
29    #[must_use]
30    pub fn element(self) -> Element<'a> {
31        self.element
32    }
33
34    /// Decoded borrowed value, without re-encoding the field.
35    #[must_use]
36    pub fn value(self) -> T {
37        self.value
38    }
39}
40
41/// A numeric field, distinguishing omission from an explicit value.
42#[derive(Debug, Clone, Copy)]
43pub enum Number<'a> {
44    /// The schema's omitted value (salt 20 or trailer 1).
45    Default(u64),
46    /// An arbitrary-width original INTEGER, including its sign.
47    Written(Written<'a, Integer<'a>>),
48}
49
50impl Number<'_> {
51    /// Bounded numeric projection; negative/wider values remain `None`.
52    #[must_use]
53    pub fn as_u64(self) -> Option<u64> {
54        match self {
55            Self::Default(value) => Some(value),
56            Self::Written(value) => value.value.as_u64(),
57        }
58    }
59}
60
61/// The mask function and, for the defined MGF1 schema, its original hash.
62#[derive(Debug, Clone, Copy)]
63pub enum Mask<'a> {
64    /// Omitted maskGenAlgorithm means MGF1 with SHA-1.
65    DefaultMgf1Sha1,
66    /// Written MGF1 requires a hash `AlgorithmIdentifier` parameter.
67    Mgf1 {
68        /// Original explicit mask field and function identifier.
69        function: Written<'a, AlgorithmIdentifier<'a>>,
70        /// Original nested hash identifier, not a manufactured default.
71        hash: AlgorithmIdentifier<'a>,
72    },
73    /// Another mask function, retaining opaque parameters for its provider.
74    Other(Written<'a, AlgorithmIdentifier<'a>>),
75}
76
77/// Original PSS schema values, not algorithm support or signature validity.
78#[derive(Debug, Clone, Copy)]
79pub struct Parameters<'a> {
80    parser: &'a structured_asn1::Parser,
81    element: Element<'a>,
82    hash: Option<Written<'a, AlgorithmIdentifier<'a>>>,
83    mask: Mask<'a>,
84    salt: Number<'a>,
85    trailer: Number<'a>,
86}
87
88impl<'a> Parameters<'a> {
89    /// Complete original parameter SEQUENCE, without copies or normalization.
90    #[must_use]
91    pub fn element(self) -> Element<'a> {
92        self.element
93    }
94
95    /// Written hash identifier; `None` is the schema's SHA-1 default.
96    #[must_use]
97    pub fn hash(self) -> Option<Written<'a, AlgorithmIdentifier<'a>>> {
98        self.hash
99    }
100
101    /// Mask identity and original MGF1 hash, including omitted default.
102    #[must_use]
103    pub fn mask(self) -> Mask<'a> {
104        self.mask
105    }
106
107    /// Salt octet length (default 20, independently of the selected hash).
108    /// Its applicability as an exact signature length or key minimum is a
109    /// caller role decision (RFC4055 §3.3); no backend maximum is imposed here.
110    #[must_use]
111    pub fn salt(self) -> Number<'a> {
112        self.salt
113    }
114
115    /// Trailer number (default 1). Other numbers are retained for inspection;
116    /// RFC8017 A.2.3 defines support only for trailer 1, not other schemes.
117    #[must_use]
118    pub fn trailer(self) -> Number<'a> {
119        self.trailer
120    }
121}
122
123/// Readable fields and retained encoding diagnostics, not conforming evidence.
124#[derive(Debug)]
125pub struct Reading<'a> {
126    parameters: Parameters<'a>,
127    faults: OwnedVec<Error>,
128}
129
130impl<'a> Reading<'a> {
131    /// Unmodified original fields, even when their spelling has a diagnostic.
132    #[must_use]
133    pub fn parameters(&self) -> Parameters<'a> {
134        self.parameters
135    }
136
137    /// Located DER DEFAULT spelling diagnostics.
138    /// Resource and foreign-reader failures are terminal errors, never faults.
139    #[must_use]
140    pub fn faults(&self) -> &[Error] {
141        &self.faults
142    }
143}
144
145/// Reads present PSS parameters in the producing session; preserves absence.
146///
147/// Callers reject absent signature parameters but may accept an unrestricted
148/// PSS key (RFC4055 §3.1). This function does neither role selection nor policy.
149/// Original encoding, offsets, depth and local/current work ceilings survive.
150///
151/// ```
152/// use std::sync::Arc;
153/// use structured_asn1::{Parser, memory::{Memory, MemoryAccount}};
154/// use structured_pkix::algorithm::{self, pss};
155/// let parser = Parser::der(Memory::new(Arc::new(MemoryAccount::new(4096, 4096))));
156/// let bytes = b"\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0a\x30\x00";
157/// let mut reader = parser.read(bytes, 400).unwrap();
158/// let element = reader.next_element().unwrap().unwrap();
159/// let algorithm = algorithm::read(&reader, &element, "signatureAlgorithm")?;
160/// let parameters = pss::read_in(algorithm, &reader)?.unwrap();
161/// assert!(parameters.hash().is_none());
162/// assert_eq!(parameters.salt().as_u64(), Some(20));
163/// assert_eq!(parameters.element().span().start, 413);
164/// # Ok::<(), structured_pkix::Error>(())
165/// ```
166pub fn read_in<'a>(
167    identifier: AlgorithmIdentifier<'a>,
168    parent: &Reader<'a, '_>,
169) -> Result<Option<Parameters<'a>>> {
170    require_session(identifier, parent)?;
171    read(identifier, &parent.execution())
172}
173
174/// Inspection counterpart of [`read_in`], sharing its schema kernel.
175///
176/// RFC4055 §3.1 requires recognizing explicitly written defaults during
177/// verification. Inspection therefore retains those values plus a DER spelling
178/// fault (X.690 §11.5), rather than losing the algorithm's meaning. BER permits
179/// written defaults. Neither operation verifies a signature or accepts a key.
180/// <https://www.rfc-editor.org/rfc/rfc4055#section-3.1>
181pub fn inspect_in<'a>(
182    identifier: AlgorithmIdentifier<'a>,
183    parent: &Reader<'a, '_>,
184) -> Result<Option<Reading<'a>>> {
185    require_session(identifier, parent)?;
186    inspect(identifier, &parent.execution())
187}
188
189/// Reads a retained identifier's original PSS parameters under current work.
190///
191/// For verification callbacks that retain an [`AlgorithmIdentifier`] without
192/// its former reader. Source parser, encoding, parameter depth and cumulative
193/// ceiling come from that identifier; `execution` supplies current funding and
194/// its root-operation ceiling. No reader is fabricated and no parser, resource
195/// account, parameter copy or default encoding is created.
196///
197/// Like [`read_in`], returns `None` for absent parameters. RFC4055 §3.1 and
198/// RFC5756 §2 require signature parameters, while absent PSS key parameters
199/// mean unrestricted PSS usage; that role is the caller's decision. Use the
200/// key's original algorithm identifier: absent PSS restrictions do not inherit
201/// from another certificate (RFC4055 §3.3). No backend acceptance is implied.
202/// <https://www.rfc-editor.org/rfc/rfc5756#section-2>
203///
204/// # Errors
205/// Reports located schema/encoding errors, including written DER defaults,
206/// separately from terminal source/current work, depth or storage refusal.
207/// Returned scalar views borrow the original source, not current execution.
208///
209/// # Examples
210/// ```
211/// use std::sync::Arc;
212/// use structured_asn1::{Parser, memory::{Memory, MemoryAccount}};
213/// use structured_pkix::algorithm::{self, pss};
214/// let source = Parser::der(Memory::new(Arc::new(MemoryAccount::new(4096, 4096))));
215/// let bytes = b"\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0a\x30\x00";
216/// let identifier = {
217///     let mut reader = source.read(bytes, 400).unwrap();
218///     let element = reader.next_element().unwrap().unwrap();
219///     algorithm::read(&reader, &element, "signatureAlgorithm")?
220/// };
221/// let current = Memory::new(Arc::new(MemoryAccount::new(4096, 4096)));
222/// let parameters = pss::read(identifier, &current)?.unwrap();
223/// assert_eq!(parameters.element().span().start, 413);
224/// assert_eq!(parameters.salt().as_u64(), Some(20));
225/// # Ok::<(), structured_pkix::Error>(())
226/// ```
227pub fn read<'a, W: Work>(
228    identifier: AlgorithmIdentifier<'a>,
229    execution: &W,
230) -> Result<Option<Parameters<'a>>> {
231    require_algorithm(identifier, execution)?;
232    read_known(identifier, execution)
233}
234
235// Only a caller that has already paid for and classified the PSS OID may skip
236// this check (signature classification or key-restriction interpretation).
237// Parameter traversal still uses the same source/current schema kernel.
238pub(super) fn read_known<'a, W: Work>(
239    identifier: AlgorithmIdentifier<'a>,
240    execution: &W,
241) -> Result<Option<Parameters<'a>>> {
242    let Some((reader, element)) = open_parameters(identifier, execution)? else {
243        return Ok(None);
244    };
245    decode(&reader, element, Err).map(Some)
246}
247
248/// Inspects retained PSS parameters through the same kernel as [`read`].
249///
250/// Readable DER DEFAULT spelling faults are retained on the current storage
251/// account. All diagnostic admission also obeys the original and current work
252/// ceilings. The result can outlive the temporary operation; its fields still
253/// borrow the original source and its diagnostic capacity stays charged until
254/// dropped. Resource failures never become missing faults or successful absence.
255///
256/// # Examples
257/// ```
258/// use std::sync::Arc;
259/// use structured_asn1::{Parser, memory::{Memory, MemoryAccount}};
260/// use structured_pkix::{ErrorCode, algorithm::{self, pss}};
261/// let source = Parser::der(Memory::new(Arc::new(MemoryAccount::new(4096, 4096))));
262/// let bytes = b"\x30\x12\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0a\x30\x05\xa2\x03\x02\x01\x14";
263/// let mut reader = source.read(bytes, 400).unwrap();
264/// let element = reader.next_element().unwrap().unwrap();
265/// let identifier = algorithm::read(&reader, &element, "signatureAlgorithm")?;
266/// let current = Memory::new(Arc::new(MemoryAccount::new(4096, 4096)));
267/// let reading = pss::inspect(identifier, &current)?.unwrap();
268/// assert_eq!(reading.parameters().salt().as_u64(), Some(20));
269/// assert_eq!(reading.faults()[0].code(), ErrorCode::DefaultWritten);
270/// # Ok::<(), structured_pkix::Error>(())
271/// ```
272pub fn inspect<'a, W: Work>(
273    identifier: AlgorithmIdentifier<'a>,
274    execution: &W,
275) -> Result<Option<Reading<'a>>> {
276    require_algorithm(identifier, execution)?;
277    let Some((reader, element)) = open_parameters(identifier, execution)? else {
278        return Ok(None);
279    };
280    let query = identifier.query_in(execution);
281    let mut faults = query.vector();
282    let parameters = decode(&reader, element, |fault| {
283        let at = fault.offset();
284        let field = fault.field();
285        faults
286            .try_push(fault)
287            .map_err(|reason| Error::resource(reason, field, at))
288    })?;
289    Ok(Some(Reading {
290        parameters,
291        faults: faults.into_owned(),
292    }))
293}
294
295fn require_algorithm<W: Work>(identifier: AlgorithmIdentifier<'_>, execution: &W) -> Result<()> {
296    let query = identifier.query_in(execution);
297    if !crate::key::same_oid(
298        identifier.algorithm(),
299        crate::oid::RSASSA_PSS,
300        &query,
301        FIELD,
302        identifier.offset(),
303    )? {
304        return Err(Error::new(
305            ErrorCode::UnexpectedField,
306            FIELD,
307            identifier.offset(),
308        ));
309    }
310    Ok(())
311}
312
313fn open_parameters<'a: 'operation, 'operation, W: Work>(
314    identifier: AlgorithmIdentifier<'a>,
315    execution: &'operation W,
316) -> Result<Option<(Reader<'a, 'operation>, Element<'a>)>> {
317    let query = identifier.query_in(execution);
318    query
319        .charge_work(1)
320        .map_err(|reason| Error::resource(reason, FIELD, identifier.offset()))?;
321    let value = identifier
322        .parameters_nested()
323        .map(|parameters| parameters.open(execution, FIELD))
324        .transpose()?;
325    if let Some((_, element)) = &value
326        && element.tag() != Tag::constructed(universal::SEQUENCE)
327    {
328        return Err(Error::new(
329            ErrorCode::Encoding(structured_asn1::ErrorCode::UnexpectedTag),
330            FIELD,
331            element.span().start,
332        ));
333    }
334    Ok(value)
335}
336
337fn require_session(identifier: AlgorithmIdentifier<'_>, parent: &Reader<'_, '_>) -> Result<()> {
338    if !identifier.same_session(parent) {
339        return Err(Error::new(
340            ErrorCode::ForeignReader,
341            FIELD,
342            identifier.offset(),
343        ));
344    }
345    Ok(())
346}
347
348fn open_parameter<'a: 'operation, 'operation, W: Work>(
349    identifier: AlgorithmIdentifier<'a>,
350    parent: &Reader<'a, '_>,
351    execution: &'operation W,
352    field: &'static str,
353) -> Result<Option<(Reader<'a, 'operation>, Element<'a>)>> {
354    charge(parent, 1, identifier.offset())?;
355    let Some(parameters) = identifier.parameters_nested() else {
356        return Ok(None);
357    };
358    parameters.open(execution, field).map(Some)
359}
360
361fn decode<'a>(
362    parent: &Reader<'a, '_>,
363    element: Element<'a>,
364    mut fault: impl FnMut(Error) -> Result<()>,
365) -> Result<Parameters<'a>> {
366    let mut fields = parent.enter(&element).field(FIELD)?;
367    let mut parameters = Parameters {
368        parser: parent.parser(),
369        element,
370        hash: None,
371        mask: Mask::DefaultMgf1Sha1,
372        salt: Number::Default(20),
373        trailer: Number::Default(1),
374    };
375    let mut previous = None;
376    while let Some(element) = fields.next_element().field(FIELD)? {
377        fields.charge(1).field(FIELD)?;
378        let Some(number) = element.tag().number() else {
379            return Err(Error::new(
380                ErrorCode::UnexpectedField,
381                FIELD,
382                element.span().start,
383            ));
384        };
385        if number > 3
386            || element.tag() != Tag::context(true, number)
387            || previous.is_some_and(|last| number <= last)
388        {
389            return Err(Error::new(
390                ErrorCode::UnexpectedField,
391                FIELD,
392                element.span().start,
393            ));
394        }
395        previous = Some(number);
396        let mut inside = fields.enter(&element).field(FIELD)?;
397        let value = inside
398            .next_element()
399            .field(FIELD)?
400            .ok_or_else(|| Error::new(ErrorCode::MissingField, FIELD, inside.offset()))?;
401        if !inside.is_empty() {
402            return Err(Error::new(ErrorCode::TrailingData, FIELD, inside.offset()));
403        }
404        let defaulted = match number {
405            0 => {
406                let hash = super::read(&inside, &value, "algorithm.pss.hash")?;
407                parameters.hash = Some(Written {
408                    element,
409                    value: hash,
410                });
411                default_hash(&inside, hash)?
412            }
413            1 => {
414                let (mask, defaulted) = read_mask(&inside, element, value)?;
415                parameters.mask = mask;
416                defaulted
417            }
418            _ => {
419                if value.tag() != Tag::primitive(universal::INTEGER) {
420                    return Err(Error::new(
421                        ErrorCode::Encoding(structured_asn1::ErrorCode::UnexpectedTag),
422                        FIELD,
423                        value.span().start,
424                    ));
425                }
426                let integer = inside.integer(&value).field(FIELD)?;
427                if number == 2 && integer.is_negative() {
428                    return Err(Error::new(
429                        ErrorCode::MalformedValue,
430                        FIELD,
431                        value.span().start,
432                    ));
433                }
434                charge(&inside, 9, value.span().start)?;
435                let written = Number::Written(Written {
436                    element,
437                    value: integer,
438                });
439                if number == 2 {
440                    parameters.salt = written;
441                    integer.as_u64() == Some(20)
442                } else {
443                    parameters.trailer = written;
444                    integer.as_u64() == Some(1)
445                }
446            }
447        };
448        // X.690 §11.5 restricts DER spelling, independently of RFC4055's
449        // requirement that verification recognize explicit default values.
450        // https://www.itu.int/rec/T-REC-X.690-202102-I/en
451        if defaulted && fields.encoding().is_der() {
452            fault(Error::new(
453                ErrorCode::DefaultWritten,
454                FIELD,
455                element.span().start,
456            ))?;
457        }
458    }
459    Ok(parameters)
460}
461
462fn default_hash(parent: &Reader<'_, '_>, identifier: AlgorithmIdentifier<'_>) -> Result<bool> {
463    // RFC4055 §§2.1/6 defines the ASN.1 sha1Identifier DEFAULT with NULL.
464    // Absent parameters have equivalent algorithm semantics but are a
465    // distinct ASN.1 value; do not manufacture NULL for X.690 §11.5.
466    // https://www.rfc-editor.org/rfc/rfc4055#section-2.1
467    Ok(
468        oid_is(parent, identifier.algorithm(), SHA1, identifier.offset())?
469            && identifier.has_null_parameter(),
470    )
471}
472
473fn oid_is(parent: &Reader<'_, '_>, left: Oid<'_>, right: Oid<'_>, at: u64) -> Result<bool> {
474    charge(parent, 1, at)?;
475    if left.as_bytes().len() != right.as_bytes().len() {
476        return Ok(false);
477    }
478    charge(parent, left.as_bytes().len() as u64, at)?;
479    Ok(left == right)
480}
481
482fn charge(parent: &Reader<'_, '_>, amount: u64, at: u64) -> Result<()> {
483    parent
484        .charge(amount)
485        .map_err(|error| Error::new(ErrorCode::Encoding(error.code()), FIELD, at))
486}
487
488fn read_mask<'a>(
489    parent: &Reader<'a, '_>,
490    element: Element<'a>,
491    value: Element<'a>,
492) -> Result<(Mask<'a>, bool)> {
493    let algorithm = super::read(parent, &value, "algorithm.pss.mask")?;
494    let function = Written {
495        element,
496        value: algorithm,
497    };
498    if !oid_is(parent, algorithm.algorithm(), MGF1, algorithm.offset())? {
499        return Ok((Mask::Other(function), false));
500    }
501    let execution = parent.execution();
502    let (reader, element) =
503        open_parameter(algorithm, parent, &execution, "algorithm.pss.mask.hash")?.ok_or_else(
504            || {
505                Error::new(
506                    ErrorCode::MissingField,
507                    "algorithm.pss.mask.hash",
508                    algorithm.offset(),
509                )
510            },
511        )?;
512    let hash = super::read(&reader, &element, "algorithm.pss.mask.hash")?;
513    Ok((Mask::Mgf1 { function, hash }, default_hash(&reader, hash)?))
514}