Skip to main content

structured_pkix/ocsp/
digest.rs

1//! Provider-injected digest matching without materializing a second output.
2
3use super::CertId;
4use super::ResponderId;
5use crate::AlgorithmIdentifier;
6use crate::Certificate;
7use crate::error::{CallbackError, CallbackResult, Error};
8use core::num::NonZeroUsize;
9use structured_asn1::memory::{Memory, MemoryError, Work};
10
11/// The digest's normative source, without reconstructing an identifier.
12#[derive(Debug, Clone, Copy)]
13pub enum DigestAlgorithm<'a> {
14    /// Hash algorithm encoded in the original `CertID`, including parameters.
15    Identifier(&'a AlgorithmIdentifier<'a>),
16    /// SHA-1 imposed by the `ResponderID` byKey syntax, not a signature choice.
17    Sha1,
18}
19
20/// Copyable resolved capability and fixed output width. No output is allocated
21/// while resolving, and an unsupported capability is represented by None.
22#[derive(Debug, Clone, Copy)]
23pub struct ResolvedDigest<H: Copy> {
24    /// Provider-owned selected algorithm handle, not a second engine.
25    pub handle: H,
26    /// Fixed output width used to check expected data before hashing.
27    pub width: NonZeroUsize,
28}
29
30/// The consumer's selected cryptographic provider and compiled algorithm policy.
31/// Every input-derived callback scan/allocation/hash/comparison is admitted
32/// through the supplied execution domain before work begins. A fixed output
33/// reservation does not qualify opaque custom-provider internal allocations.
34pub trait Digest<E: Work = Memory> {
35    /// Copyable resolved algorithm capability.
36    type Handle: Copy;
37    /// Original provider or resource error, never collapsed into mismatch.
38    type Error;
39    /// Resolve original identifier/parameters or protocol-imposed SHA-1.
40    fn resolve(
41        &self,
42        algorithm: DigestAlgorithm<'_>,
43        execution: &E,
44    ) -> core::result::Result<Option<ResolvedDigest<Self::Handle>>, Self::Error>;
45    /// Hash original bytes and compare with an already width-checked expected
46    /// value. No owned-output conversion or extra allocation is required by core.
47    fn matches(
48        &self,
49        handle: Self::Handle,
50        input: &[u8],
51        expected: &[u8],
52        execution: &E,
53    ) -> core::result::Result<bool, Self::Error>;
54}
55
56/// Identity comparison is not responder authentication or a certificate verdict.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum CertMatch {
59    /// Serial, issuer name and both original-byte hashes match.
60    Matches,
61    /// A completed semantic or digest comparison found different identity.
62    Differs,
63    /// The advertised fixed hash width contradicts a carried hash value.
64    MalformedHashWidth,
65    /// The consumer does not support the encoded hash algorithm/parameters.
66    UnsupportedHash,
67}
68
69/// Responder identity alone, without signature, authority or status acceptance.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum ResponderMatch {
72    /// Subject Name or imposed SHA-1 public-key hash identifies this certificate.
73    Matches,
74    /// Completed name or complete digest comparison differs.
75    Differs,
76    /// Written hash or provider-advertised SHA-1 width contradicts twenty octets.
77    MalformedHashWidth,
78    /// Protocol-imposed SHA-1 is unsupported by the selected provider/policy.
79    UnsupportedHash,
80}
81
82impl ResponderId<'_> {
83    /// Compare an original responder selector with the actual signer certificate.
84    /// RFC6960 §4.2.1 hashes subjectPublicKey contents with SHA-1, excluding
85    /// BIT STRING tag, length and unused-bits count. Name comparison reuses the
86    /// shared PKIX Name kernel. No signature recheck or standing-key fabrication.
87    /// <https://www.rfc-editor.org/rfc/rfc6960#section-4.2.1>
88    ///
89    /// `field` and `at` identify the real enclosing selector in the caller's
90    /// source; callbacks preserve their own original processing/resource cause.
91    pub fn matches_certificate<D: Digest<E>, E: Work>(
92        &self,
93        certificate: &Certificate<'_>,
94        digest: &D,
95        execution: &E,
96        field: &'static str,
97        at: u64,
98    ) -> CallbackResult<ResponderMatch, D::Error> {
99        let charge = |work| {
100            execution
101                .charge_work(work)
102                .map_err(|reason| Error::resource(reason, field, at))
103        };
104        charge(1)?;
105        let same = match self {
106            Self::Name(name) => name.matches(certificate.tbs().subject(), execution)?,
107            Self::KeyHash(expected) => {
108                if expected.len() != 20 {
109                    return Ok(ResponderMatch::MalformedHashWidth);
110                }
111                let Some(resolved) = digest
112                    .resolve(DigestAlgorithm::Sha1, execution)
113                    .map_err(CallbackError::Callback)?
114                else {
115                    return Ok(ResponderMatch::UnsupportedHash);
116                };
117                if resolved.width.get() != 20 {
118                    return Ok(ResponderMatch::MalformedHashWidth);
119                }
120                digest
121                    .matches(
122                        resolved.handle,
123                        certificate.tbs().public_key().key().as_bytes(),
124                        expected,
125                        execution,
126                    )
127                    .map_err(CallbackError::Callback)?
128            }
129        };
130        Ok(if same {
131            ResponderMatch::Matches
132        } else {
133            ResponderMatch::Differs
134        })
135    }
136}
137
138impl CertId<'_> {
139    /// Match the actual target and candidate issuer without re-encoding names.
140    /// RFC6960 §4.1.1 hashes original target issuer DER and issuer public-key
141    /// contents. The caller retains their authenticated issuer/path provenance;
142    /// matching hashes does not verify the target certificate's signature.
143    /// <https://www.rfc-editor.org/rfc/rfc6960#section-4.1.1>
144    pub fn matches<D: Digest<E>, E: Work>(
145        &self,
146        target: &Certificate<'_>,
147        issuer: &Certificate<'_>,
148        digest: &D,
149        execution: &E,
150    ) -> CallbackResult<CertMatch, D::Error> {
151        const FIELD: &str = "ocspResponse.certID";
152        let at = self.span.start;
153        let left = self.serial.as_bytes();
154        let right = target.tbs().serial().as_bytes();
155        let work = u64::try_from(left.len().max(right.len()))
156            .ok()
157            .and_then(|n| n.checked_mul(2))
158            .and_then(|n| n.checked_add(1))
159            .ok_or_else(|| Error::resource(MemoryError::WorkExceeded, FIELD, at))?;
160        execution
161            .charge_work(work)
162            .map_err(|reason| Error::resource(reason, FIELD, at))?;
163        if left != right {
164            return Ok(CertMatch::Differs);
165        }
166        if !target
167            .tbs()
168            .issuer()
169            .matches(issuer.tbs().subject(), execution)?
170        {
171            return Ok(CertMatch::Differs);
172        }
173        execution
174            .charge_work(1)
175            .map_err(|reason| Error::resource(reason, FIELD, at))?;
176        let Some(resolved) = digest
177            .resolve(DigestAlgorithm::Identifier(&self.algorithm), execution)
178            .map_err(CallbackError::Callback)?
179        else {
180            return Ok(CertMatch::UnsupportedHash);
181        };
182        // A malformed known width is classified before either hashing callback.
183        if self.issuer_name_hash.len() != resolved.width.get()
184            || self.issuer_key_hash.len() != resolved.width.get()
185        {
186            return Ok(CertMatch::MalformedHashWidth);
187        }
188        if !digest
189            .matches(
190                resolved.handle,
191                target.tbs().issuer().as_bytes(),
192                self.issuer_name_hash,
193                execution,
194            )
195            .map_err(CallbackError::Callback)?
196        {
197            return Ok(CertMatch::Differs);
198        }
199        if !digest
200            .matches(
201                resolved.handle,
202                issuer.tbs().public_key().key().as_bytes(),
203                self.issuer_key_hash,
204                execution,
205            )
206            .map_err(CallbackError::Callback)?
207        {
208            return Ok(CertMatch::Differs);
209        }
210        Ok(CertMatch::Matches)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::{CertMatch, Digest, DigestAlgorithm, ResolvedDigest};
217    use core::cell::Cell;
218    use core::num::NonZeroUsize;
219    use structured_asn1::memory::Memory;
220
221    struct Observed<'a> {
222        original_algorithm: &'a [u8],
223        name: &'a [u8],
224        key: &'a [u8],
225        hashes: Cell<usize>,
226        supported: bool,
227        failure: bool,
228    }
229    impl Digest for Observed<'_> {
230        type Handle = u8;
231        type Error = &'static str;
232        fn resolve(
233            &self,
234            algorithm: DigestAlgorithm<'_>,
235            _: &Memory,
236        ) -> Result<Option<ResolvedDigest<u8>>, Self::Error> {
237            let DigestAlgorithm::Identifier(algorithm) = algorithm else {
238                panic!("CertID does not impose SHA1")
239            };
240            assert_eq!(algorithm.as_bytes(), self.original_algorithm);
241            assert_eq!(
242                algorithm.as_bytes().as_ptr(),
243                self.original_algorithm.as_ptr()
244            );
245            if !self.supported {
246                return Ok(None);
247            }
248            Ok(Some(ResolvedDigest {
249                handle: 1,
250                width: NonZeroUsize::new(20).unwrap(),
251            }))
252        }
253        fn matches(
254            &self,
255            handle: u8,
256            input: &[u8],
257            expected: &[u8],
258            _: &Memory,
259        ) -> Result<bool, Self::Error> {
260            assert_eq!(handle, 1);
261            assert_eq!(expected.len(), 20);
262            if self.failure {
263                return Err("provider failure");
264            }
265            let calls = self.hashes.get();
266            self.hashes.set(calls + 1);
267            let original = if calls == 0 { self.name } else { self.key };
268            assert_eq!(input, original);
269            assert_eq!(input.as_ptr(), original.as_ptr());
270            Ok(true)
271        }
272    }
273
274    #[test]
275    fn cert_id_prechecks_width_and_forwards_original_bytes_without_output_copy() {
276        // A callback sees original target issuer DER, then raw public-key
277        // contents, never rendered Name/SPKI. Bad expected width does not hash.
278        let parts = crate::fixtures::CertificateParts::default();
279        let issuer_bytes = crate::fixtures::CertificateParts {
280            subject: parts.issuer.clone(),
281            ..crate::fixtures::CertificateParts::default()
282        }
283        .build();
284        let target_bytes = parts.build();
285        let execution = crate::fixtures::memory();
286        let parser = structured_asn1::Parser::der(execution.clone());
287        let target = crate::Certificate::read(&parser, &target_bytes, 100).unwrap();
288        let issuer = crate::Certificate::read(&parser, &issuer_bytes, 300).unwrap();
289        let id = super::CertId {
290            algorithm: target.signature_algorithm(),
291            serial: target.tbs().serial(),
292            issuer_name_hash: &[0x11; 20],
293            issuer_key_hash: &[0x22; 20],
294            span: structured_asn1::Span {
295                start: 700,
296                length: 50,
297            },
298        };
299        let provider = Observed {
300            original_algorithm: id.algorithm.as_bytes(),
301            name: target.tbs().issuer().as_bytes(),
302            key: issuer.tbs().public_key().key().as_bytes(),
303            hashes: Cell::new(0),
304            supported: true,
305            failure: false,
306        };
307        let storage = execution.used();
308        assert_eq!(
309            id.matches(&target, &issuer, &provider, &execution).unwrap(),
310            CertMatch::Matches
311        );
312        assert_eq!(provider.hashes.get(), 2);
313        assert_eq!(execution.used(), storage);
314        let malformed = super::CertId {
315            issuer_name_hash: &[0x11; 19],
316            ..id
317        };
318        provider.hashes.set(0);
319        assert_eq!(
320            malformed
321                .matches(&target, &issuer, &provider, &execution)
322                .unwrap(),
323            CertMatch::MalformedHashWidth
324        );
325        assert_eq!(provider.hashes.get(), 0);
326        let mut provider = provider;
327        provider.supported = false;
328        assert_eq!(
329            id.matches(&target, &issuer, &provider, &execution).unwrap(),
330            CertMatch::UnsupportedHash
331        );
332        provider.supported = true;
333        provider.failure = true;
334        assert!(matches!(
335            id.matches(&target, &issuer, &provider, &execution)
336                .unwrap_err(),
337            crate::error::CallbackError::Callback("provider failure")
338        ));
339        let stopped = structured_asn1::memory::Memory::new(alloc::sync::Arc::new(
340            structured_asn1::memory::MemoryAccount::new(0, 0),
341        ));
342        assert!(
343            matches!(id.matches(&target, &issuer, &provider, &stopped).unwrap_err(), crate::error::CallbackError::Processing(error) if error.is_resource() && error.offset() == 700)
344        );
345    }
346}