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
8use super::AlgorithmIdentifier;
9use crate::error::{Error, ErrorCode, Located, Result};
10use structured_asn1::memory::{OwnedVec, Work};
11use structured_asn1::{Element, Integer, Oid, ScopedReader as Reader, Tag, universal};
12
13const SHA1: Oid<'static> = Oid::from_bytes(&[0x2b, 0x0e, 3, 2, 0x1a]);
14const MGF1: Oid<'static> = Oid::from_bytes(&[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 8]);
15const FIELD: &str = "algorithm.pss";
16
17/// An explicitly written field with its original EXPLICIT wrapper.
18#[derive(Debug, Clone, Copy)]
19pub struct Written<'a, T> {
20    element: Element<'a>,
21    value: T,
22}
23
24impl<'a, T: Copy> Written<'a, T> {
25    /// Original context-specific wrapper, including its source span.
26    #[must_use]
27    pub fn element(self) -> Element<'a> {
28        self.element
29    }
30
31    /// Decoded borrowed value, without re-encoding the field.
32    #[must_use]
33    pub fn value(self) -> T {
34        self.value
35    }
36}
37
38/// A numeric field, distinguishing omission from an explicit value.
39#[derive(Debug, Clone, Copy)]
40pub enum Number<'a> {
41    /// The schema's omitted value (salt 20 or trailer 1).
42    Default(u64),
43    /// An arbitrary-width original INTEGER, including its sign.
44    Written(Written<'a, Integer<'a>>),
45}
46
47impl Number<'_> {
48    /// Bounded numeric projection; negative/wider values remain `None`.
49    #[must_use]
50    pub fn as_u64(self) -> Option<u64> {
51        match self {
52            Self::Default(value) => Some(value),
53            Self::Written(value) => value.value.as_u64(),
54        }
55    }
56}
57
58/// The mask function and, for the defined MGF1 schema, its original hash.
59#[derive(Debug, Clone, Copy)]
60pub enum Mask<'a> {
61    /// Omitted maskGenAlgorithm means MGF1 with SHA-1.
62    DefaultMgf1Sha1,
63    /// Written MGF1 requires a hash `AlgorithmIdentifier` parameter.
64    Mgf1 {
65        /// Original explicit mask field and function identifier.
66        function: Written<'a, AlgorithmIdentifier<'a>>,
67        /// Original nested hash identifier, not a manufactured default.
68        hash: AlgorithmIdentifier<'a>,
69    },
70    /// Another mask function, retaining opaque parameters for its provider.
71    Other(Written<'a, AlgorithmIdentifier<'a>>),
72}
73
74/// Original PSS schema values, not algorithm support or signature validity.
75#[derive(Debug, Clone, Copy)]
76pub struct Parameters<'a> {
77    element: Element<'a>,
78    hash: Option<Written<'a, AlgorithmIdentifier<'a>>>,
79    mask: Mask<'a>,
80    salt: Number<'a>,
81    trailer: Number<'a>,
82}
83
84impl<'a> Parameters<'a> {
85    /// Complete original parameter SEQUENCE, without copies or normalization.
86    #[must_use]
87    pub fn element(self) -> Element<'a> {
88        self.element
89    }
90
91    /// Written hash identifier; `None` is the schema's SHA-1 default.
92    #[must_use]
93    pub fn hash(self) -> Option<Written<'a, AlgorithmIdentifier<'a>>> {
94        self.hash
95    }
96
97    /// Mask identity and original MGF1 hash, including omitted default.
98    #[must_use]
99    pub fn mask(self) -> Mask<'a> {
100        self.mask
101    }
102
103    /// Salt octet length (default 20, independently of the selected hash).
104    /// Its applicability as an exact signature length or key minimum is a
105    /// caller role decision (RFC4055 §3.3); no backend maximum is imposed here.
106    #[must_use]
107    pub fn salt(self) -> Number<'a> {
108        self.salt
109    }
110
111    /// Trailer number (default 1). Other numbers are retained for inspection;
112    /// RFC8017 A.2.3 defines support only for trailer 1, not other schemes.
113    #[must_use]
114    pub fn trailer(self) -> Number<'a> {
115        self.trailer
116    }
117}
118
119/// Readable fields and retained encoding diagnostics, not conforming evidence.
120#[derive(Debug)]
121pub struct Reading<'a> {
122    parameters: Parameters<'a>,
123    faults: OwnedVec<Error>,
124}
125
126impl<'a> Reading<'a> {
127    /// Unmodified original fields, even when their spelling has a diagnostic.
128    #[must_use]
129    pub fn parameters(&self) -> Parameters<'a> {
130        self.parameters
131    }
132
133    /// Located DER DEFAULT spelling diagnostics.
134    /// Resource and foreign-reader failures are terminal errors, never faults.
135    #[must_use]
136    pub fn faults(&self) -> &[Error] {
137        &self.faults
138    }
139}
140
141/// Reads present PSS parameters in the producing session; preserves absence.
142///
143/// Callers reject absent signature parameters but may accept an unrestricted
144/// PSS key (RFC4055 §3.1). This function does neither role selection nor policy.
145/// Original encoding, offsets, depth and local/current work ceilings survive.
146///
147/// ```
148/// use std::sync::Arc;
149/// use structured_asn1::{Parser, memory::{Memory, MemoryAccount}};
150/// use structured_pkix::algorithm::{self, pss};
151/// let parser = Parser::der(Memory::new(Arc::new(MemoryAccount::new(4096, 4096))));
152/// let bytes = b"\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0a\x30\x00";
153/// let mut reader = parser.read(bytes, 400).unwrap();
154/// let element = reader.next_element().unwrap().unwrap();
155/// let algorithm = algorithm::read(&reader, &element, "signatureAlgorithm")?;
156/// let parameters = pss::read_in(algorithm, &reader)?.unwrap();
157/// assert!(parameters.hash().is_none());
158/// assert_eq!(parameters.salt().as_u64(), Some(20));
159/// assert_eq!(parameters.element().span().start, 413);
160/// # Ok::<(), structured_pkix::Error>(())
161/// ```
162pub fn read_in<'a>(
163    identifier: AlgorithmIdentifier<'a>,
164    parent: &Reader<'a, '_>,
165) -> Result<Option<Parameters<'a>>> {
166    let execution = parent.execution();
167    let Some((reader, element)) = open(identifier, parent, &execution)? else {
168        return Ok(None);
169    };
170    decode(&reader, element, Err).map(Some)
171}
172
173/// Inspection counterpart of [`read_in`], sharing its schema kernel.
174///
175/// RFC4055 §3.1 requires recognizing explicitly written defaults during
176/// verification. Inspection therefore retains those values plus a DER spelling
177/// fault (X.690 §11.5), rather than losing the algorithm's meaning. BER permits
178/// written defaults. Neither operation verifies a signature or accepts a key.
179/// <https://www.rfc-editor.org/rfc/rfc4055#section-3.1>
180pub fn inspect_in<'a>(
181    identifier: AlgorithmIdentifier<'a>,
182    parent: &Reader<'a, '_>,
183) -> Result<Option<Reading<'a>>> {
184    let execution = parent.execution();
185    let Some((reader, element)) = open(identifier, parent, &execution)? else {
186        return Ok(None);
187    };
188    let mut faults = execution.vector();
189    let parameters = decode(&reader, element, |fault| {
190        let at = fault.offset();
191        let field = fault.field();
192        faults
193            .try_push(fault)
194            .map_err(|reason| Error::resource(reason, field, at))
195    })?;
196    Ok(Some(Reading {
197        parameters,
198        faults: faults.into_owned(),
199    }))
200}
201
202fn open<'a: 'operation, 'operation, W: Work>(
203    identifier: AlgorithmIdentifier<'a>,
204    parent: &Reader<'a, '_>,
205    execution: &'operation W,
206) -> Result<Option<(Reader<'a, 'operation>, Element<'a>)>> {
207    if !identifier.same_session(parent) {
208        return Err(Error::new(
209            ErrorCode::ForeignReader,
210            FIELD,
211            identifier.offset(),
212        ));
213    }
214    if !oid_is(
215        parent,
216        identifier.algorithm(),
217        crate::oid::RSASSA_PSS,
218        identifier.offset(),
219    )? {
220        return Err(Error::new(
221            ErrorCode::UnexpectedField,
222            FIELD,
223            identifier.offset(),
224        ));
225    }
226    let value = open_parameter(identifier, parent, execution, FIELD)?;
227    if let Some((_, element)) = &value
228        && element.tag() != Tag::constructed(universal::SEQUENCE)
229    {
230        return Err(Error::new(
231            ErrorCode::Encoding(structured_asn1::ErrorCode::UnexpectedTag),
232            FIELD,
233            element.span().start,
234        ));
235    }
236    Ok(value)
237}
238
239fn open_parameter<'a: 'operation, 'operation, W: Work>(
240    identifier: AlgorithmIdentifier<'a>,
241    parent: &Reader<'a, '_>,
242    execution: &'operation W,
243    field: &'static str,
244) -> Result<Option<(Reader<'a, 'operation>, Element<'a>)>> {
245    charge(parent, 1, identifier.offset())?;
246    let Some(parameters) = identifier.parameters_nested() else {
247        return Ok(None);
248    };
249    parameters.open(execution, field).map(Some)
250}
251
252fn decode<'a>(
253    parent: &Reader<'a, '_>,
254    element: Element<'a>,
255    mut fault: impl FnMut(Error) -> Result<()>,
256) -> Result<Parameters<'a>> {
257    let mut fields = parent.enter(&element).field(FIELD)?;
258    let mut parameters = Parameters {
259        element,
260        hash: None,
261        mask: Mask::DefaultMgf1Sha1,
262        salt: Number::Default(20),
263        trailer: Number::Default(1),
264    };
265    let mut previous = None;
266    while let Some(element) = fields.next_element().field(FIELD)? {
267        fields.charge(1).field(FIELD)?;
268        let Some(number) = element.tag().number() else {
269            return Err(Error::new(
270                ErrorCode::UnexpectedField,
271                FIELD,
272                element.span().start,
273            ));
274        };
275        if number > 3
276            || element.tag() != Tag::context(true, number)
277            || previous.is_some_and(|last| number <= last)
278        {
279            return Err(Error::new(
280                ErrorCode::UnexpectedField,
281                FIELD,
282                element.span().start,
283            ));
284        }
285        previous = Some(number);
286        let mut inside = fields.enter(&element).field(FIELD)?;
287        let value = inside
288            .next_element()
289            .field(FIELD)?
290            .ok_or_else(|| Error::new(ErrorCode::MissingField, FIELD, inside.offset()))?;
291        if !inside.is_empty() {
292            return Err(Error::new(ErrorCode::TrailingData, FIELD, inside.offset()));
293        }
294        let defaulted = match number {
295            0 => {
296                let hash = super::read(&inside, &value, "algorithm.pss.hash")?;
297                parameters.hash = Some(Written {
298                    element,
299                    value: hash,
300                });
301                default_hash(&inside, hash)?
302            }
303            1 => {
304                let (mask, defaulted) = read_mask(&inside, element, value)?;
305                parameters.mask = mask;
306                defaulted
307            }
308            _ => {
309                if value.tag() != Tag::primitive(universal::INTEGER) {
310                    return Err(Error::new(
311                        ErrorCode::Encoding(structured_asn1::ErrorCode::UnexpectedTag),
312                        FIELD,
313                        value.span().start,
314                    ));
315                }
316                let integer = inside.integer(&value).field(FIELD)?;
317                if number == 2 && integer.is_negative() {
318                    return Err(Error::new(
319                        ErrorCode::MalformedValue,
320                        FIELD,
321                        value.span().start,
322                    ));
323                }
324                charge(&inside, 9, value.span().start)?;
325                let written = Number::Written(Written {
326                    element,
327                    value: integer,
328                });
329                if number == 2 {
330                    parameters.salt = written;
331                    integer.as_u64() == Some(20)
332                } else {
333                    parameters.trailer = written;
334                    integer.as_u64() == Some(1)
335                }
336            }
337        };
338        // X.690 §11.5 restricts DER spelling, independently of RFC4055's
339        // requirement that verification recognize explicit default values.
340        // https://www.itu.int/rec/T-REC-X.690-202102-I/en
341        if defaulted && fields.encoding().is_der() {
342            fault(Error::new(
343                ErrorCode::DefaultWritten,
344                FIELD,
345                element.span().start,
346            ))?;
347        }
348    }
349    Ok(parameters)
350}
351
352fn default_hash(parent: &Reader<'_, '_>, identifier: AlgorithmIdentifier<'_>) -> Result<bool> {
353    // RFC4055 §§2.1/6 defines the ASN.1 sha1Identifier DEFAULT with NULL.
354    // Absent parameters have equivalent algorithm semantics but are a
355    // distinct ASN.1 value; do not manufacture NULL for X.690 §11.5.
356    // https://www.rfc-editor.org/rfc/rfc4055#section-2.1
357    Ok(
358        oid_is(parent, identifier.algorithm(), SHA1, identifier.offset())?
359            && identifier.has_null_parameter(),
360    )
361}
362
363fn oid_is(parent: &Reader<'_, '_>, left: Oid<'_>, right: Oid<'_>, at: u64) -> Result<bool> {
364    charge(parent, 1, at)?;
365    if left.as_bytes().len() != right.as_bytes().len() {
366        return Ok(false);
367    }
368    charge(parent, left.as_bytes().len() as u64, at)?;
369    Ok(left == right)
370}
371
372fn charge(parent: &Reader<'_, '_>, amount: u64, at: u64) -> Result<()> {
373    parent
374        .charge(amount)
375        .map_err(|error| Error::new(ErrorCode::Encoding(error.code()), FIELD, at))
376}
377
378fn read_mask<'a>(
379    parent: &Reader<'a, '_>,
380    element: Element<'a>,
381    value: Element<'a>,
382) -> Result<(Mask<'a>, bool)> {
383    let algorithm = super::read(parent, &value, "algorithm.pss.mask")?;
384    let function = Written {
385        element,
386        value: algorithm,
387    };
388    if !oid_is(parent, algorithm.algorithm(), MGF1, algorithm.offset())? {
389        return Ok((Mask::Other(function), false));
390    }
391    let execution = parent.execution();
392    let (reader, element) =
393        open_parameter(algorithm, parent, &execution, "algorithm.pss.mask.hash")?.ok_or_else(
394            || {
395                Error::new(
396                    ErrorCode::MissingField,
397                    "algorithm.pss.mask.hash",
398                    algorithm.offset(),
399                )
400            },
401        )?;
402    let hash = super::read(&reader, &element, "algorithm.pss.mask.hash")?;
403    Ok((Mask::Mgf1 { function, hash }, default_hash(&reader, hash)?))
404}