Skip to main content

structured_pkix/algorithm/pss/
profile.rs

1//! Interpreted PSS fields and RFC4055 key restrictions, without cryptography.
2
3use super::{Mask, Number, Parameters};
4use crate::algorithm::{AlgorithmIdentifier, digest};
5use crate::{Error, ErrorCode, Result};
6use structured_asn1::digest::Resolution;
7use structured_asn1::memory::Work;
8use structured_asn1::{Element, Parser};
9
10const FIELD: &str = "algorithm.pss.profile";
11
12/// A recognized MGF1 profile, not provider capability or signature validity.
13///
14/// The two digests are independent. Salt remains an original arbitrary-width
15/// INTEGER (or the schema default), not a platform-sized backend parameter.
16/// Only trailer 1 is recognized; its original written/default form is retained.
17#[derive(Debug, Clone, Copy)]
18pub struct Profile<'a> {
19    parser: &'a Parser,
20    element: Element<'a>,
21    hash: digest::Algorithm,
22    mask_hash: digest::Algorithm,
23    salt: Number<'a>,
24    trailer: Number<'a>,
25}
26
27impl<'a> Profile<'a> {
28    /// Original parameter SEQUENCE; no canonical representation is fabricated.
29    #[must_use]
30    pub fn element(self) -> Element<'a> {
31        self.element
32    }
33
34    /// Message digest identity, including the omitted SHA-1 default.
35    #[must_use]
36    pub fn hash(self) -> digest::Algorithm {
37        self.hash
38    }
39
40    /// MGF1 digest identity, independently of the message digest.
41    #[must_use]
42    pub fn mask_hash(self) -> digest::Algorithm {
43        self.mask_hash
44    }
45
46    /// Original salt octet count; no modulus or provider maximum is imposed.
47    #[must_use]
48    pub fn salt(self) -> Number<'a> {
49        self.salt
50    }
51
52    /// Original trailer field (the recognized numeric value is 1).
53    #[must_use]
54    pub fn trailer(self) -> Number<'a> {
55        self.trailer
56    }
57
58    /// Whether `signature` satisfies this present key profile.
59    ///
60    /// RFC4055 §3.3 requires identical message/MGF digests and trailer, but
61    /// signature salt >= key minimum, not equality. Both original source
62    /// ceilings and current work apply, even for a cheap digest mismatch.
63    /// Arbitrary-width nonnegative integers compare without truncation.
64    /// This checks restrictions only, not key validity, provider support,
65    /// modulus feasibility, policy, or signature mathematics.
66    /// <https://www.rfc-editor.org/rfc/rfc4055#section-3.3>
67    ///
68    /// # Errors
69    /// A located terminal resource refusal before comparison.
70    pub fn allows<W: Work>(&self, signature: &Self, execution: &W) -> Result<bool> {
71        self.charge(execution, 1, self.element.span().start)?;
72        signature.charge(execution, 1, signature.element.span().start)?;
73        if self.hash != signature.hash || self.mask_hash != signature.mask_hash {
74            return Ok(false);
75        }
76        // The profile constructor accepts only trailer 1. Omitted and written
77        // defaults are the same algorithm, independently of their encoding.
78        // https://www.rfc-editor.org/rfc/rfc8017#appendix-A.2.3
79        let minimum = Salt::new(self.salt);
80        let used = Salt::new(signature.salt);
81        self.charge(
82            execution,
83            minimum.len() as u64 * 2 + 2,
84            number_at(self.salt, self.element),
85        )?;
86        signature.charge(
87            execution,
88            used.len() as u64 * 2 + 2,
89            number_at(signature.salt, signature.element),
90        )?;
91        let minimum = minimum.bytes();
92        let used = used.bytes();
93        if used.len() != minimum.len() {
94            return Ok(used.len() > minimum.len());
95        }
96        Ok(used >= minimum)
97    }
98
99    fn charge<W: Work>(&self, execution: &W, amount: u64, at: u64) -> Result<()> {
100        execution
101            .execution_for(self.parser)
102            .charge_work(amount)
103            .map_err(|reason| Error::resource(reason, FIELD, at))
104    }
105}
106
107impl<'a> Parameters<'a> {
108    /// Interprets already decoded fields without a second PSS schema pass.
109    ///
110    /// Written digests reuse the shared digest resolver (SHA-1/SHA-2/SHA3);
111    /// schema defaults impose SHA-1 without a fabricated identifier. MGF1
112    /// may use a different digest from the message. Unknown digests/mask
113    /// functions or nonnegative trailers other than 1 are `Unsupported`,
114    /// never a key mismatch. Recognition does not promise backend support.
115    /// Negative trailers and invalid known digest parameters are located
116    /// malformed errors. Original fields remain available for inspection.
117    /// Strict versus recoverable DER spelling is determined by the caller's
118    /// earlier `pss::read` or `pss::inspect`; projection does not erase faults
119    /// or turn an inspection result into conforming evidence.
120    ///
121    /// # Errors
122    /// Located malformed fields or terminal source/current resource refusal.
123    ///
124    /// # Examples
125    /// ```
126    /// use std::sync::Arc;
127    /// use structured_asn1::{Parser, digest::Resolution, memory::{Memory, MemoryAccount}};
128    /// use structured_pkix::algorithm::{self, digest, pss};
129    /// let parser = Parser::ber(Memory::new(Arc::new(MemoryAccount::new(4096, 4096))));
130    /// // id-RSASSA-PSS, empty parameters: SHA1, MGF1-SHA1, salt 20, trailer 1.
131    /// let bytes = b"\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0a\x30\x00";
132    /// let mut reader = parser.read(bytes, 400).unwrap();
133    /// let element = reader.next_element().unwrap().unwrap();
134    /// let identifier = algorithm::read(&reader, &element, "signatureAlgorithm")?;
135    /// let fields = pss::read(identifier, parser.memory())?.unwrap();
136    /// let current = Memory::new(Arc::new(MemoryAccount::new(0, 4096)));
137    /// let Resolution::Supported(profile) = fields.profile(&current)? else { panic!("PSS") };
138    /// assert_eq!(profile.hash(), digest::Algorithm::Sha1);
139    /// assert_eq!(profile.salt().as_u64(), Some(20));
140    /// assert!(profile.allows(&profile, &current)?);
141    /// # Ok::<(), structured_pkix::Error>(())
142    /// ```
143    pub fn profile<W: Work>(&self, execution: &W) -> Result<Resolution<Profile<'a>>> {
144        let query = execution.execution_for(self.parser);
145        query
146            .charge_work(1)
147            .map_err(|reason| Error::resource(reason, FIELD, self.element.span().start))?;
148        let hash = match self.hash {
149            None => digest::Algorithm::Sha1,
150            Some(written) => match read_hash(written.value(), execution)? {
151                Resolution::Supported(hash) => hash,
152                _ => return Ok(Resolution::Unsupported),
153            },
154        };
155        let mask_hash = match self.mask {
156            Mask::DefaultMgf1Sha1 => digest::Algorithm::Sha1,
157            Mask::Mgf1 { hash, .. } => match read_hash(hash, execution)? {
158                Resolution::Supported(hash) => hash,
159                _ => return Ok(Resolution::Unsupported),
160            },
161            Mask::Other(_) => return Ok(Resolution::Unsupported),
162        };
163        if let Number::Written(written) = self.trailer {
164            let at = written.element().span().start;
165            // Sign classification plus the bounded u64 projection, including
166            // its own sign check and at most eight magnitude octets.
167            query
168                .charge_work(10)
169                .map_err(|reason| Error::resource(reason, FIELD, at))?;
170            if written.value().is_negative() {
171                return Err(Error::new(ErrorCode::MalformedValue, FIELD, at));
172            }
173            if written.value().as_u64() != Some(1) {
174                return Ok(Resolution::Unsupported);
175            }
176        }
177        Ok(Resolution::Supported(Profile {
178            parser: self.parser,
179            element: self.element,
180            hash,
181            mask_hash,
182            salt: self.salt,
183            trailer: self.trailer,
184        }))
185    }
186}
187
188/// Checks an original key [`AlgorithmIdentifier`] against a PSS signature profile.
189///
190/// RFC4055 §3.3: rsaEncryption carries no PSS restrictions; an id-RSASSA-PSS
191/// key with absent parameters permits any PSS profile; a present PSS profile
192/// sets exact digests/trailer and a minimum salt. The key identifier must be
193/// the selected key's original identifier, not the certificate's signature
194/// algorithm. No restrictions are inherited from an issuer.
195///
196/// rsaEncryption requires primitive NULL (RFC4055 §1.2). Other key algorithms
197/// and unknown PSS profiles are `Unsupported`; `Supported(false)` means a
198/// recognized restriction mismatch. No provider or policy authorization follows
199/// from `Supported(true)`.
200///
201/// # Errors
202/// Original located malformed parameters or terminal resource refusal.
203/// <https://www.rfc-editor.org/rfc/rfc4055#section-3.3>
204pub fn key_allows<W: Work>(
205    key: AlgorithmIdentifier<'_>,
206    signature: &Profile<'_>,
207    execution: &W,
208) -> Result<Resolution<bool>> {
209    let written = key.algorithm().as_bytes();
210    key.query_in(execution)
211        .charge_work(written.len() as u64 + 1)
212        .map_err(|reason| Error::resource(reason, FIELD, key.offset()))?;
213    signature.charge(execution, 1, signature.element.span().start)?;
214    // One shared prefix dispatch: do not compare the whole RSA prefix once
215    // for each candidate after admitting only one scan.
216    match written {
217        [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, last] => match last {
218            1 => {
219                if !key.has_null_parameter() {
220                    return Err(Error::new(
221                        ErrorCode::MalformedValue,
222                        FIELD,
223                        key.parameters_nested()
224                            .map_or(key.offset(), super::super::Nested::offset),
225                    ));
226                }
227                return Ok(Resolution::Supported(true));
228            }
229            10 => {}
230            _ => return Ok(Resolution::Unsupported),
231        },
232        _ => return Ok(Resolution::Unsupported),
233    }
234    let Some(parameters) = super::read_known(key, execution)? else {
235        return Ok(Resolution::Supported(true));
236    };
237    match parameters.profile(execution)? {
238        Resolution::Supported(profile) => profile
239            .allows(signature, execution)
240            .map(Resolution::Supported),
241        _ => Ok(Resolution::Unsupported),
242    }
243}
244
245fn read_hash<W: Work>(
246    identifier: AlgorithmIdentifier<'_>,
247    execution: &W,
248) -> Result<Resolution<digest::Algorithm>> {
249    match digest::algorithm(&identifier, execution)? {
250        Resolution::Malformed => Err(Error::new(
251            ErrorCode::MalformedValue,
252            FIELD,
253            identifier.offset(),
254        )),
255        value => Ok(value),
256    }
257}
258
259fn number_at(number: Number<'_>, enclosing: Element<'_>) -> u64 {
260    match number {
261        Number::Default(_) => enclosing.span().start,
262        Number::Written(value) => value.element().span().start,
263    }
264}
265
266enum Salt<'a> {
267    Written(&'a [u8]),
268    Default,
269}
270
271impl<'a> Salt<'a> {
272    fn new(number: Number<'a>) -> Self {
273        match number {
274            Number::Written(value) => Self::Written(value.value().as_bytes()),
275            Number::Default(value) => {
276                // Parameters has a private constructor; only salt's schema
277                // default (20), never trailer's default, reaches this helper.
278                debug_assert_eq!(value, 20);
279                Self::Default
280            }
281        }
282    }
283
284    fn len(&self) -> usize {
285        match self {
286            Self::Written(bytes) => bytes.len(),
287            Self::Default => 1,
288        }
289    }
290
291    fn bytes(&self) -> &[u8] {
292        let bytes = match self {
293            Self::Written(bytes) => *bytes,
294            Self::Default => &[20],
295        };
296        // Strict INTEGER decoding already guarantees minimal two's complement;
297        // at most one sign zero exists. The schema default is borrowed too.
298        let mut start = 0;
299        while start + 1 < bytes.len() && bytes[start] == 0 {
300            start += 1;
301        }
302        &bytes[start..]
303    }
304}