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