Skip to main content

structured_pkix/ocsp/
digest.rs

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