Skip to main content

structured_xades/validation/
ref_envelope.rs

1//! Bind one OCSP reference's digest and identifier to the same signed response.
2
3use super::{
4    OcspReferenceError, OcspReferenceIdentity, ReferenceDigest, ReferenceDigestError,
5    ReferenceIdentifier, ReferenceProperty, ReferenceTarget, ReferenceTime, check_ocsp_reference,
6    compare_reference_target,
7};
8use crate::{
9    binary::{self, BinaryXml},
10    certificate::{Agreement, DigestProvider},
11    properties::CalendarXml,
12    scalar::TextXml,
13    xml::{Access, AccessError, Xml},
14};
15use structured_asn1::{
16    Reader,
17    memory::{MemoryError, Work},
18};
19use structured_pkix::{
20    algorithm::digest::AsnDigest,
21    ocsp::{ResponderMatch, ResponseBytes, SignedResponse},
22    oid,
23};
24
25/// Original whole envelope and an actual signature receipt. This is an INPUT,
26/// not a trusted binding: [`check_ocsp_envelope_reference`] checks the relation.
27#[derive(Clone, Copy)]
28pub struct OcspReferenceInput<'envelope, 'signed, 'document> {
29    /// Whole original successful `OCSPResponse`; a basic-only value is insufficient.
30    pub envelope: &'envelope ResponseBytes<'document>,
31    /// Already authenticated original basic response and its actual signer.
32    pub signed: SignedResponse<'signed, 'document>,
33}
34
35/// Identifier and optional digest checked against the SAME signed envelope.
36/// This does not prove authorization, target status, actual use or trust.
37pub struct OcspEnvelopeReference<'r, 'e, 's, 'd, N, D, G> {
38    references: &'r ReferenceProperty<N, D, G>,
39    entry: usize,
40    actual: OcspReferenceInput<'e, 's, 'd>,
41    identity: binary::Located<OcspReferenceIdentity, N, D, G>,
42    digest: Option<ReferenceDigest>,
43}
44impl<'r, 'e, 's, 'd, N, D, G> OcspEnvelopeReference<'r, 'e, 's, 'd, N, D, G> {
45    /// Original property and its immutable source/generation.
46    #[must_use]
47    pub fn references(&self) -> &'r ReferenceProperty<N, D, G> {
48        self.references
49    }
50    /// Original reference ordinal within that property.
51    #[must_use]
52    pub fn entry(&self) -> usize {
53        self.entry
54    }
55    /// Same whole envelope, authenticated body and signer used by both checks.
56    #[must_use]
57    pub fn actual(&self) -> OcspReferenceInput<'e, 's, 'd> {
58        self.actual
59    }
60    /// Original identifier location and all readable responder/time facts.
61    #[must_use]
62    pub fn identity(&self) -> &binary::Located<OcspReferenceIdentity, N, D, G> {
63        &self.identity
64    }
65    /// None means mandatory identifier syntax was malformed/unsupported, so
66    /// digest processing was not attempted. `IdentifierOnly` means no digest was
67    /// written; neither is a successful whole-envelope digest agreement.
68    #[must_use]
69    pub fn digest(&self) -> Option<ReferenceDigest> {
70        self.digest
71    }
72
73    /// Mandatory responder/time agree with this response, and its digest
74    /// agrees if written. Annex A.1.2 makes the digest optional (SHOULD): an
75    /// identifier-only reference can identify this response but does not bind
76    /// its whole contents against substitution. Inspect [`Self::digest`] to
77    /// retain that distinction; this proves no authority, status, trust or grade.
78    #[must_use]
79    pub fn identifies_response(&self) -> bool {
80        if self.identity.value.responder() != Some(ResponderMatch::Matches)
81            || self.identity.value.produced_at() != ReferenceTime::Matches
82        {
83            return false;
84        }
85        matches!(
86            self.digest,
87            Some(ReferenceDigest::Compared(Agreement::Matches) | ReferenceDigest::IdentifierOnly)
88        )
89    }
90}
91
92/// Input/source and execution failures never become an OCSP identity mismatch.
93#[derive(Debug)]
94pub enum OcspEnvelopeError<E, KP, DP, N, D, G> {
95    /// Current original XML/source query refused.
96    Access(AccessError<E, N>),
97    /// No original reference exists at this ordinal.
98    Index {
99        /// Requested ordinal, not a physical document offset.
100        index: usize,
101    },
102    /// The supplied envelope does not contain this receipt's exact basic
103    /// response under the known basic-response syntax. Caller input association
104    /// failed; the original document is not thereby declared malformed.
105    InputMismatch {
106        /// Original reference node whose input pair was supplied incorrectly.
107        node: N,
108    },
109    /// Current relation comparison was not admitted before scanning.
110    Resource {
111        /// Original reference node; no invented binary offset.
112        node: N,
113        /// Exact admission cause.
114        cause: MemoryError,
115    },
116    /// Existing original identifier/source/provider cause.
117    Identity(OcspReferenceError<E, KP, N, D, G>),
118    /// Existing original whole-response digest/source/provider cause.
119    Digest(binary::Error<E, ReferenceDigestError<DP>, N, D, G>),
120}
121
122/// Complete paired facts or the unchanged stage-specific terminal cause.
123pub type OcspEnvelopeResult<'r, 'e, 's, 'd, X, KP, DP> = Result<
124    Option<
125        OcspEnvelopeReference<
126            'r,
127            'e,
128            's,
129            'd,
130            <X as Xml>::Id,
131            <X as Xml>::Document,
132            <X as Xml>::Generation,
133        >,
134    >,
135    OcspEnvelopeError<
136        <X as Xml>::Error,
137        KP,
138        DP,
139        <X as Xml>::Id,
140        <X as Xml>::Document,
141        <X as Xml>::Generation,
142    >,
143>;
144
145pub(super) fn basic_payload<'a, W: Work>(
146    envelope: &ResponseBytes<'a>,
147    work: &W,
148) -> Result<Option<&'a [u8]>, MemoryError> {
149    work.charge_work(1)?;
150    let kind = envelope.kind().as_bytes();
151    let expected_kind = oid::OCSP_BASIC.as_bytes();
152    if kind.len() != expected_kind.len() {
153        return Ok(None);
154    }
155    work.charge_work(u64::try_from(kind.len()).map_err(|_| MemoryError::WorkExceeded)?)?;
156    if kind != expected_kind {
157        return Ok(None);
158    }
159    Ok(Some(envelope.payload()))
160}
161
162pub(super) fn envelope_contains<W: Work>(
163    envelope: &ResponseBytes<'_>,
164    authenticated: &[u8],
165    work: &W,
166) -> Result<bool, MemoryError> {
167    let Some(payload) = basic_payload(envelope, work)? else {
168        return Ok(false);
169    };
170    if payload.len() != authenticated.len() {
171        return Ok(false);
172    }
173    work.charge_work(u64::try_from(payload.len()).map_err(|_| MemoryError::WorkExceeded)?)?;
174    Ok(payload == authenticated)
175}
176
177/// Compose Annex A.1.2's mandatory responder/time fields and optional whole
178/// `OCSPResponse` digest WITHOUT allowing a valid identifier from response A to
179/// be joined to a digest of envelope B. The known syntax and exact original
180/// payload relation are paid and checked before any provider callback; neither
181/// signature verification nor parsing repeats. Two different encodings of the
182/// same signed body are not normalized or silently substituted.
183/// Malformed/unavailable mandatory time syntax prevents both key and envelope
184/// digest callbacks. A readable time mismatch retains both independent facts.
185/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
186/// <https://www.rfc-editor.org/rfc/rfc6960#section-4.2.1>
187///
188/// This is not issuer authorization, freshness, status applicability, presence,
189/// actor membership, substitution-resistant acquisition policy or a grade.
190/// None means the original reference has a different kind, never agreement.
191pub fn check_ocsp_envelope_reference<'r, 'e, 's, 'd, 'k, 'q, X, K, P, KP>(
192    xml: &X,
193    references: &'r ReferenceProperty<X::Id, X::Document, X::Generation>,
194    index: usize,
195    actual: OcspReferenceInput<'e, 's, 'd>,
196    key_digest: &'k K,
197    provider: &'q P,
198    operation: &Reader<'_>,
199) -> OcspEnvelopeResult<'r, 'e, 's, 'd, X, KP, P::Error>
200where
201    X: BinaryXml + CalendarXml + TextXml,
202    K: AsnDigest<'k, Error = KP>,
203    P: DigestProvider<'q, X>,
204{
205    let work = operation.execution();
206    let access = Access::new(xml, references.snapshot(), &work);
207    access
208        .query(references.node(), |_, _, _| Ok(()))
209        .map_err(OcspEnvelopeError::Access)?;
210    let Some(entry) = references.entries().get(index) else {
211        return Err(OcspEnvelopeError::Index { index });
212    };
213    let ReferenceIdentifier::Ocsp { node, .. } = entry.identifier() else {
214        return Ok(None);
215    };
216    let resource = |cause| OcspEnvelopeError::Resource { node, cause };
217    if !envelope_contains(actual.envelope, actual.signed.response().as_bytes(), &work)
218        .map_err(resource)?
219    {
220        return Err(OcspEnvelopeError::InputMismatch { node });
221    }
222    let identity =
223        check_ocsp_reference(xml, references, index, actual.signed, key_digest, operation)
224            .map_err(OcspEnvelopeError::Identity)?;
225    let Some(identity) = identity else {
226        return Err(OcspEnvelopeError::InputMismatch { node });
227    };
228    let digest = if identity.value.responder().is_some() {
229        Some(
230            compare_reference_target(
231                xml,
232                references,
233                index,
234                ReferenceTarget::Ocsp(actual.envelope),
235                provider,
236                &work,
237            )
238            .map_err(OcspEnvelopeError::Digest)?,
239        )
240    } else {
241        None
242    };
243    access
244        .query(node, |_, _, _| Ok(()))
245        .map_err(OcspEnvelopeError::Access)?;
246    Ok(Some(OcspEnvelopeReference {
247        references,
248        entry: index,
249        actual,
250        identity,
251        digest,
252    }))
253}