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