Skip to main content

structured_pkix/ocsp/
evidence.rs

1//! Compose signature/issuer scope with requested target, extension and time facts.
2
3use super::{
4    ExtensionProcessing, Lookup, NonceBinding, RequestNonce, ScopedResponse, SingleResponse,
5    UpdateWindow,
6};
7use crate::algorithm::digest::AsnDigest;
8use crate::error::{CallbackResult, Result};
9use crate::revocation::{RevocationScheme, RevocationTarget, TargetKind};
10use crate::temporal::{LeapTable, Reading};
11use crate::{Certificate, Error};
12use core::marker::PhantomData;
13use structured_asn1::memory::{Memory, MemoryError, Work};
14use structured_asn1::{Oid, Span};
15
16/// Actual caller request facts, never inferred from the received response.
17pub struct Request<'p, 'a, T = Certificate<'a>> {
18    /// Original target identified by the actual request and its selected issuer.
19    pub target: &'p T,
20    /// Actual sent nonce, not a newly generated replacement.
21    pub nonce: Option<RequestNonce<'p>>,
22    /// Optional expected exact signer certificate chosen by the caller. Absent
23    /// binding is reported explicitly rather than counted as a match.
24    pub intended_signer: Option<&'p Certificate<'a>>,
25}
26
27/// Current temporal/resource facts; maximum age and clock-skew policy stay outside.
28pub struct Evaluation<'p, E: Work = Memory> {
29    /// Actual caller-supplied current label, retaining subnanosecond precision.
30    pub now: Reading,
31    /// Caller-authenticated UTC correction evidence and explicit coverage.
32    pub utc: &'p LeapTable<'p>,
33    /// Current operation's shared work and storage domain.
34    pub execution: &'p E,
35}
36
37/// Whether this evidence is bound to the caller's exact intended signer input.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum RecipientBinding {
40    /// Caller did not supply an exact signer; no such claim is made.
41    NotSpecified,
42    /// Authenticated actual signer matches the supplied exact certificate.
43    Matches,
44}
45
46/// Composed original evidence, still not final application acceptance. Read its
47/// authority source for pending delegate revocation, and apply nonce, maximum
48/// age, recipient and time-uncertainty policy before interpreting status.
49#[derive(Debug, Clone, Copy)]
50pub struct Evidence<'r, 'a> {
51    source: ScopedResponse<'r, 'a>,
52    assertion: &'r SingleResponse<'a>,
53    window: UpdateWindow,
54    nonce: NonceBinding,
55    recipient: RecipientBinding,
56    revocation_time: Option<crate::time::Relation>,
57    status: super::Status,
58    archive_cutoff: Option<Reading>,
59}
60
61impl<'r, 'a> Evidence<'r, 'a> {
62    /// Signature and explicit issuer-scope provenance.
63    #[must_use]
64    pub fn source(self) -> ScopedResponse<'r, 'a> {
65        self.source
66    }
67    /// Selected original assertion, with every uninterpreted noncritical value.
68    #[must_use]
69    pub fn assertion(self) -> &'r SingleResponse<'a> {
70        self.assertion
71    }
72    /// Update-window relation, not a maximum-age decision.
73    #[must_use]
74    pub fn window(self) -> UpdateWindow {
75        self.window
76    }
77    /// Actual request/response nonce relation.
78    #[must_use]
79    pub fn nonce(self) -> NonceBinding {
80        self.nonce
81    }
82    /// Exact intended signer binding, when requested.
83    #[must_use]
84    pub fn recipient(self) -> RecipientBinding {
85        self.recipient
86    }
87
88    /// UTC-qualified recorded revocation time when this assertion is revoked.
89    #[must_use]
90    pub fn revocation_time(self) -> Option<crate::time::Relation> {
91        self.revocation_time
92    }
93
94    /// Assertion status under the response's extended revoked definition,
95    /// including [`super::Status::NotIssued`].
96    #[must_use]
97    pub fn status(self) -> super::Status {
98        self.status
99    }
100
101    /// The assertion's archive cutoff, retaining decimal subnanosecond precision.
102    #[must_use]
103    pub fn archive_cutoff(self) -> Option<Reading> {
104        self.archive_cutoff
105    }
106}
107
108/// Target processing outcome, without turning missing/unsupported into Good.
109#[derive(Debug, Clone, Copy)]
110#[expect(
111    clippy::large_enum_variant,
112    reason = "retain source/key/time facts by value rather than allocate a box per target merely to discriminate the result"
113)]
114pub enum TargetEvidence<'r, 'a> {
115    /// The AC explicitly uses RFC5755 §6 `NoRevAvail`; no status is minted.
116    NoRevAvail,
117    /// Ordinary direct PKC delegation cannot confer AC status authority:
118    /// RFC5755 §4.5 forbids the AA from also issuing public-key certificates.
119    /// Use the actual AA or independently configured responder trust instead.
120    AttributeDelegation,
121    /// Identity and supported extension semantics completed; policy facts remain.
122    Evidence(Evidence<'r, 'a>),
123    /// Actual signer differs from the caller's intended exact signer.
124    UnexpectedSigner,
125    /// Identity lookup did not yield one usable assertion.
126    Lookup(Lookup<'r, 'a>),
127    /// Response-wide or selected-assertion critical demand cannot be processed.
128    UnsupportedCritical {
129        /// Original unsupported demand identifier.
130        kind: Oid<'a>,
131        /// Original location.
132        span: Span,
133    },
134}
135
136/// Complete target processing together with the original requested certificate.
137/// Target and response lifetimes are independent; no temporary request,
138/// evaluation context, provider or execution wrapper is retained.
139#[derive(Debug)]
140pub struct TargetReading<'target, 'request, 'response, 'source, T = Certificate<'request>> {
141    target: &'target T,
142    // The default PKC source lifetime remains explicit in the public type;
143    // a different target carries its own borrowed source in T, at zero cost.
144    request_source: PhantomData<&'request ()>,
145    evaluated_at: Reading,
146    outcome: TargetEvidence<'response, 'source>,
147}
148
149impl<'target, 'request, 'response, 'source, T>
150    TargetReading<'target, 'request, 'response, 'source, T>
151{
152    /// Original actual target, not another certificate with a matching hint.
153    #[must_use]
154    pub fn target(&self) -> &'target T {
155        self.target
156    }
157    /// Exact label used by the same update-window/revocation-time processing.
158    #[must_use]
159    pub fn evaluated_at(&self) -> Reading {
160        self.evaluated_at
161    }
162    /// Original complete result, including lookup and critical-demand failures.
163    #[must_use]
164    pub fn outcome(&self) -> &TargetEvidence<'response, 'source> {
165        &self.outcome
166    }
167    /// Bind established mechanism facts to the target used by this invocation.
168    /// Status, temporal uncertainty, nonce and delegated-responder requirements
169    /// remain exactly as reported; this does not mean the status is Good.
170    #[must_use]
171    pub fn evidence(&self) -> Option<BoundEvidence<'target, 'request, 'response, 'source, T>> {
172        match self.outcome {
173            TargetEvidence::Evidence(evidence) => Some(BoundEvidence {
174                target: self.target,
175                request_source: PhantomData,
176                evaluated_at: self.evaluated_at,
177                evidence,
178            }),
179            _ => None,
180        }
181    }
182    /// Move the original outcome when the caller does not retain target binding.
183    #[must_use]
184    pub fn into_outcome(self) -> TargetEvidence<'response, 'source> {
185        self.outcome
186    }
187}
188
189/// Privately bound target and facts from the same completed common evaluation.
190/// This association cannot be created from an unrelated certificate and result.
191#[derive(Debug)]
192pub struct BoundEvidence<'target, 'request, 'response, 'source, T = Certificate<'request>> {
193    target: &'target T,
194    request_source: PhantomData<&'request ()>,
195    evaluated_at: Reading,
196    evidence: Evidence<'response, 'source>,
197}
198
199impl<T> Copy for BoundEvidence<'_, '_, '_, '_, T> {}
200impl<T> Clone for BoundEvidence<'_, '_, '_, '_, T> {
201    fn clone(&self) -> Self {
202        *self
203    }
204}
205
206impl<'target, 'response, 'source, T> BoundEvidence<'target, '_, 'response, 'source, T> {
207    /// Original requested certificate whose identity the common kernel matched.
208    #[must_use]
209    pub fn target(self) -> &'target T {
210        self.target
211    }
212    /// Original evaluation label; the caller cannot relabel status in time.
213    #[must_use]
214    pub fn evaluated_at(self) -> Reading {
215        self.evaluated_at
216    }
217    /// Same original mechanism facts, without repeating identity hashes.
218    #[must_use]
219    pub fn evidence(self) -> Evidence<'response, 'source> {
220        self.evidence
221    }
222}
223
224fn exact_signer<E: Work>(
225    left: &Certificate<'_>,
226    right: &Certificate<'_>,
227    execution: &E,
228    at: u64,
229) -> Result<bool> {
230    let left = left.as_bytes();
231    let right = right.as_bytes();
232    let work = u64::try_from(left.len().max(right.len()))
233        .ok()
234        .and_then(|n| n.checked_mul(2))
235        .and_then(|n| n.checked_add(1))
236        .ok_or_else(|| Error::resource(MemoryError::WorkExceeded, "ocspResponse.recipient", at))?;
237    execution
238        .charge_work(work)
239        .map_err(|reason| Error::resource(reason, "ocspResponse.recipient", at))?;
240    Ok(left == right)
241}
242
243impl<'r, 'a> ScopedResponse<'r, 'a> {
244    /// Run the same complete target-processing kernel once while retaining its
245    /// original requested certificate. No hashing, parsing or allocation is
246    /// repeated to recover provenance later. Failures retain their original
247    /// result/cause; no positive target binding is returned for a lookup failure.
248    pub fn evaluate_with_target<
249        'target,
250        'request,
251        't,
252        'd,
253        T: RevocationTarget<'t>,
254        D: AsnDigest<'d> + ?Sized,
255        E: Work,
256    >(
257        self,
258        request: &Request<'target, 'request, T>,
259        time: &Evaluation<'_, E>,
260        digest: &'d D,
261    ) -> CallbackResult<TargetReading<'target, 'request, 'r, 'a, T>, D::Error> {
262        let outcome = self.evaluate(request, time, digest)?;
263        Ok(TargetReading {
264            target: request.target,
265            request_source: PhantomData,
266            evaluated_at: time.now,
267            outcome,
268        })
269    }
270
271    /// Evaluate the actual request against this issuer-scoped signed response.
272    /// RFC6960 §3.2 requires identity, signer, authorization and temporal checks;
273    /// this operation composes mechanism facts without choosing caller policy.
274    /// <https://www.rfc-editor.org/rfc/rfc6960#section-3.2>
275    pub fn evaluate<'t, 'd, T: RevocationTarget<'t>, D: AsnDigest<'d> + ?Sized, E: Work>(
276        self,
277        request: &Request<'_, '_, T>,
278        time: &Evaluation<'_, E>,
279        digest: &'d D,
280    ) -> CallbackResult<TargetEvidence<'r, 'a>, D::Error> {
281        let response = self.signed().response();
282        let execution = &response.signature_algorithm().query_in(time.execution);
283        execution.charge_work(1).map_err(|reason| {
284            Error::resource(reason, "ocspResponse.target", response.span().start)
285        })?;
286        let target_work = request.target.execution_in(time.execution);
287        target_work.charge_work(1).map_err(|reason| {
288            Error::resource(reason, "ocspResponse.target", request.target.span().start)
289        })?;
290        if request.target.revocation_scheme() == RevocationScheme::NoRevAvail {
291            return Ok(TargetEvidence::NoRevAvail);
292        }
293        // RFC5755 §4.5: an AA cannot also issue the PKC required by
294        // RFC6960 §4.2.2.2's direct certificate-delegation branch. This does
295        // not infer configured trust from an included certificate or OCSP EKU.
296        // https://www.rfc-editor.org/rfc/rfc5755#section-4.5
297        if request.target.kind() == TargetKind::Attribute
298            && matches!(self.source(), super::AuthoritySource::Delegate { .. })
299        {
300            return Ok(TargetEvidence::AttributeDelegation);
301        }
302        let recipient = match request.intended_signer {
303            None => RecipientBinding::NotSpecified,
304            Some(expected) => {
305                if !exact_signer(
306                    self.signed().signer(),
307                    expected,
308                    execution,
309                    response.span().start,
310                )? {
311                    return Ok(TargetEvidence::UnexpectedSigner);
312                }
313                RecipientBinding::Matches
314            }
315        };
316        let processed = response.process_extensions(execution)?;
317        if let ExtensionProcessing::Unsupported { kind, span } = processed.processing {
318            return Ok(TargetEvidence::UnsupportedCritical { kind, span });
319        }
320        let assertion = match self.find(request.target, digest, execution)? {
321            Lookup::Found(assertion) => assertion,
322            outcome => return Ok(TargetEvidence::Lookup(outcome)),
323        };
324        let single = assertion.process_extensions(execution)?;
325        if let ExtensionProcessing::Unsupported { kind, span } = single.processing {
326            return Ok(TargetEvidence::UnsupportedCritical { kind, span });
327        }
328        let status = super::retention::status(assertion, processed.extended_revoke, execution)?;
329        let nonce = super::nonce::bind(
330            request.nonce,
331            processed.nonce,
332            (execution, "ocspResponse.nonce", response.span().start),
333            (
334                &response.signature_algorithm().query_in(execution),
335                "ocspResponse.nonce",
336                response.span().start,
337            ),
338        )?;
339        let window = assertion.update_window(time.now, time.utc, execution)?;
340        let revocation_time = assertion.revocation_relation(time.now, time.utc, execution)?;
341        Ok(TargetEvidence::Evidence(Evidence {
342            source: self,
343            assertion,
344            nonce,
345            window,
346            recipient,
347            revocation_time,
348            status,
349            archive_cutoff: single.archive_cutoff,
350        }))
351    }
352}
353
354#[cfg(test)]
355mod tests;