Skip to main content

structured_pkix/ocsp/request/
binding.rs

1//! Batch identity and nonce relations retain each actual sent request entry.
2
3use super::super::digest::CertDigest;
4use super::{FIELD, OcspRequest, RequestEntry};
5use crate::algorithm::digest::AsnDigest;
6use crate::error::{CallbackError, CallbackResult, Error, Result};
7use crate::ocsp::{
8    CertId, CertMatch, ExtensionProcessing, Lookup, NonceBinding, RequestNonce, SignedResponse,
9};
10use structured_asn1::memory::{OwnedVec, Work};
11
12pub(super) fn equal<L: Work, R: Work>(
13    left: &[u8],
14    l: &L,
15    right: &[u8],
16    r: &R,
17    field: &'static str,
18    left_at: u64,
19    right_at: u64,
20) -> Result<bool> {
21    l.charge_work(1)
22        .map_err(|e| Error::resource(e, field, left_at))?;
23    r.charge_work(1)
24        .map_err(|e| Error::resource(e, field, right_at))?;
25    if left.len() != right.len() {
26        return Ok(false);
27    }
28    l.charge_work(left.len() as u64)
29        .map_err(|e| Error::resource(e, field, left_at))?;
30    r.charge_work(right.len() as u64)
31        .map_err(|e| Error::resource(e, field, right_at))?;
32    Ok(left == right)
33}
34
35impl CertId<'_> {
36    /// Compare two actual wire `CertIDs` under their own source ceilings and the
37    /// current operation, resolving complete digest identities before hash-byte
38    /// comparison. This does not hash certificates or establish issuer trust.
39    /// Unknown/malformed algorithms and widths never become positive matches.
40    pub fn matches_id<'d, D: AsnDigest<'d> + ?Sized, W: Work>(
41        &self,
42        other: &CertId<'_>,
43        digest: &'d D,
44        execution: &W,
45    ) -> CallbackResult<CertMatch, D::Error> {
46        const FIELD: &str = "ocsp.certID";
47        let left_work = self.algorithm.query_in(execution);
48        let right_work = other.algorithm.query_in(execution);
49        if !equal(
50            self.serial.as_bytes(),
51            &left_work,
52            other.serial.as_bytes(),
53            &right_work,
54            FIELD,
55            self.span.start,
56            other.span.start,
57        )? {
58            return Ok(CertMatch::Differs);
59        }
60        let left = match self.digest_identity(digest, execution)? {
61            CertDigest::Ready(id) => id,
62            CertDigest::Rejected(reason) => return Ok(reason),
63        };
64        let right = match other.digest_identity(digest, execution)? {
65            CertDigest::Ready(id) => id,
66            CertDigest::Rejected(reason) => return Ok(reason),
67        };
68        execution
69            .charge_work(1)
70            .map_err(|e| Error::resource(e, FIELD, self.span.start))?;
71        if !digest
72            .compare(&left, &right, execution)
73            .map_err(CallbackError::Callback)?
74            .is_eq()
75        {
76            return Ok(CertMatch::Differs);
77        }
78        for (left, right) in [
79            (self.issuer_name_hash, other.issuer_name_hash),
80            (self.issuer_key_hash, other.issuer_key_hash),
81        ] {
82            if !equal(
83                left,
84                &left_work,
85                right,
86                &right_work,
87                FIELD,
88                self.span.start,
89                other.span.start,
90            )? {
91                return Ok(CertMatch::Differs);
92            }
93        }
94        Ok(CertMatch::Matches)
95    }
96}
97
98/// One original sent entry and its duplicate-preserving authenticated lookup.
99#[derive(Debug)]
100pub struct RequestLookup<'q, 'a, 'r, 'response> {
101    request: &'q RequestEntry<'a>,
102    lookup: Lookup<'r, 'response>,
103    extensions: ExtensionProcessing<'a>,
104}
105impl<'q, 'a, 'r, 'response> RequestLookup<'q, 'a, 'r, 'response> {
106    /// Original sent entry, not a reconstructed certificate identity.
107    #[must_use]
108    pub fn request(&self) -> &'q RequestEntry<'a> {
109        self.request
110    }
111    /// Original response match; ambiguity is never reduced to the first entry.
112    #[must_use]
113    pub fn lookup(&self) -> Lookup<'r, 'response> {
114        self.lookup
115    }
116    /// Unknown per-entry critical demands remain separate from identity.
117    #[must_use]
118    pub fn extensions(&self) -> ExtensionProcessing<'a> {
119        self.extensions
120    }
121}
122
123/// Complete request/response relation, not freshness, responder authority or
124/// request acceptance. Inspect every lookup, critical demand and nonce fact.
125#[derive(Debug)]
126pub struct BoundResponse<'q, 'a, 'r, 'response> {
127    request: &'q OcspRequest<'a>,
128    response: SignedResponse<'r, 'response>,
129    nonce: NonceBinding,
130    extensions: ExtensionProcessing<'a>,
131    lookups: OwnedVec<RequestLookup<'q, 'a, 'r, 'response>>,
132}
133impl<'q, 'a, 'r, 'response> BoundResponse<'q, 'a, 'r, 'response> {
134    /// Original request whose complete batch and nonce were examined.
135    #[must_use]
136    pub fn request(&self) -> &'q OcspRequest<'a> {
137        self.request
138    }
139    /// Actual authenticated response; issuer authority remains a separate check.
140    #[must_use]
141    pub fn response(&self) -> SignedResponse<'r, 'response> {
142        self.response
143    }
144    /// Actual sent/received nonce relation, preserving missing and unsolicited.
145    #[must_use]
146    pub fn nonce(&self) -> NonceBinding {
147        self.nonce
148    }
149    /// Request-wide critical processing, not application acceptance.
150    #[must_use]
151    pub fn extensions(&self) -> ExtensionProcessing<'a> {
152        self.extensions
153    }
154    /// Every original request in order, including repeated `CertIDs`.
155    #[must_use]
156    pub fn lookups(&self) -> &[RequestLookup<'q, 'a, 'r, 'response>] {
157        &self.lookups
158    }
159}
160
161impl<'a> OcspRequest<'a> {
162    /// Decode the original request-wide nonce with the same RFC9654 kernel as
163    /// responses. Received legacy sizes 1..128 are readable; generation of new
164    /// requests must still supply at least 32 strong random octets.
165    pub fn nonce<W: Work>(&self, execution: &W) -> Result<Option<RequestNonce<'a>>> {
166        let source = self.execution_in(execution);
167        let Some(extension) = self.extensions.get(crate::oid::OCSP_NONCE, &source)? else {
168            return Ok(None);
169        };
170        let bytes = super::super::nonce::read(extension, &source, "ocspRequest.nonce")?;
171        RequestNonce::from_sent(bytes).map(Some).map_err(|_| {
172            Error::new(
173                crate::ErrorCode::MalformedValue,
174                "ocspRequest.nonce",
175                extension.span().start,
176            )
177        })
178    }
179
180    /// Process known request-wide nonce syntax and preserve unknown critical
181    /// demands. RFC6960 §4.1.2 makes all specific extensions optional; unknown
182    /// noncritical values remain in the original model, never silently dropped.
183    pub fn check_extensions<W: Work>(&self, execution: &W) -> Result<ExtensionProcessing<'a>> {
184        process_extensions(&self.extensions, true, &self.execution_in(execution))
185            .map(|processed| processed.processing)
186    }
187
188    /// Bind the complete original batch to an already authenticated response.
189    /// RFC6960 §3.2 requires actual requested-certificate binding. This query
190    /// proves neither responder authority nor that a found status is Good.
191    /// On any execution/provider refusal all partial lookup storage is dropped.
192    /// <https://www.rfc-editor.org/rfc/rfc6960#section-3.2>
193    pub fn bind_response<'q, 'r, 'response, 'd, D: AsnDigest<'d> + ?Sized, W: Work>(
194        &'q self,
195        response: SignedResponse<'r, 'response>,
196        digest: &'d D,
197        execution: &W,
198    ) -> CallbackResult<BoundResponse<'q, 'a, 'r, 'response>, D::Error> {
199        let source = self.execution_in(execution);
200        let processed = process_extensions(&self.extensions, true, &source)?;
201        let response_nonce = response.response().nonce(execution)?;
202        let response_work = response
203            .response()
204            .signature_algorithm()
205            .query_in(execution);
206        let nonce = super::super::nonce::bind(
207            processed.nonce,
208            response_nonce,
209            (&source, "ocspRequest.nonce", self.span.start),
210            (
211                &response_work,
212                "ocspResponse.nonce",
213                response.response().span().start,
214            ),
215        )?;
216        let mut lookups = source.vector();
217        // The complete output must be retainable before selected-provider
218        // comparison callbacks run. Refusal returns no successful partial batch.
219        lookups
220            .try_reserve_exact(self.entries.len())
221            .map_err(|e| Error::resource(e, FIELD, self.span.start))?;
222        for request in &self.entries {
223            source
224                .charge_work(1)
225                .map_err(|e| Error::resource(e, FIELD, request.span.start))?;
226            let extensions = process_extensions(&request.extensions, false, &source)?.processing;
227            let lookup = response.find_id(&request.id, digest, execution)?;
228            lookups
229                .try_push(RequestLookup {
230                    request,
231                    lookup,
232                    extensions,
233                })
234                .map_err(|e| Error::resource(e, FIELD, request.span.start))?;
235        }
236        Ok(BoundResponse {
237            request: self,
238            response,
239            nonce,
240            extensions: processed.processing,
241            lookups: lookups.into_owned(),
242        })
243    }
244}
245
246struct Processed<'a> {
247    processing: ExtensionProcessing<'a>,
248    nonce: Option<RequestNonce<'a>>,
249}
250
251fn process_extensions<'a, W: Work>(
252    extensions: &crate::Extensions<'a>,
253    wide: bool,
254    execution: &W,
255) -> Result<Processed<'a>> {
256    let mut processing = ExtensionProcessing::Understood;
257    let mut nonce = None;
258    for extension in extensions.iter(execution) {
259        let extension = extension?;
260        execution
261            .charge_work(10)
262            .map_err(|e| Error::resource(e, FIELD, extension.span().start))?;
263        if wide && extension.kind() == crate::oid::OCSP_NONCE {
264            let bytes = super::super::nonce::read(extension, execution, "ocspRequest.nonce")?;
265            nonce = Some(RequestNonce::from_sent(bytes).map_err(|_| {
266                Error::new(
267                    crate::ErrorCode::MalformedValue,
268                    "ocspRequest.nonce",
269                    extension.span().start,
270                )
271            })?);
272        } else if extension.is_critical() && matches!(processing, ExtensionProcessing::Understood) {
273            processing = ExtensionProcessing::Unsupported {
274                kind: extension.kind(),
275                span: extension.span(),
276            };
277        }
278    }
279    Ok(Processed { processing, nonce })
280}