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(¤t)? 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, ¤t)?);
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}