Skip to main content

structured_xades/validation/
ref_key.rs

1//! Original DER OCSP byKey hint over the shared responder field kernel.
2
3use super::{ReferenceIdentifier, ReferenceProperty, ResponderIdentifier};
4use crate::{
5    binary::{self, BinaryXml, Location},
6    xml::Access,
7};
8use structured_asn1::{Reader, Tag};
9use structured_pkix::{
10    Certificate,
11    algorithm::digest::AsnDigest,
12    ocsp::{ResponderId, ResponderMatch},
13};
14
15/// Source/schema and visitor causes remain distinct from key disagreement.
16#[derive(Debug)]
17pub enum ResponderKeyError<E> {
18    /// Caller selected no original reference at this ordinal.
19    Index {
20        /// Original caller-selected catalogue ordinal.
21        index: usize,
22    },
23    /// Original DER reader failure, preserving decoded-source coordinates.
24    Encoding(structured_asn1::Error),
25    /// Original shared RFC6960 field failure, including resource causes.
26    Responder(structured_pkix::Error),
27    /// Unconsumed bytes after the one byKey field.
28    Trailing {
29        /// First unconsumed decoded-source offset.
30        offset: u64,
31    },
32    /// Exact callback failure, without converting resource causes to mismatch.
33    Callback(E),
34}
35
36/// Original XML enclosure and shared/provider failure for a responder-key query.
37pub type ReferenceKeyResult<X, E> = binary::BinaryResult<
38    Option<binary::LocatedValue<ResponderMatch, X>>,
39    X,
40    ResponderKeyError<structured_pkix::error::CallbackError<E>>,
41>;
42
43/// Visit the complete original OCSP byKey hash. Annex A.1.2 requires base64
44/// of the DER-encoded RFC6960 byKey FIELD, not a naked twenty-byte digest.
45/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
46///
47/// The existing binary source bridge enforces DER and both budgets. The shared
48/// OCSP kernel owns tag/cardinality/hash width; no ASN.1 grammar is duplicated.
49/// None means this reference uses another identifier branch, not a matching
50/// responder. Even that absence checks the current original source first.
51pub fn with_responder_key<X: BinaryXml, R, E>(
52    xml: &X,
53    references: &ReferenceProperty<X::Id, X::Document, X::Generation>,
54    index: usize,
55    operation: &Reader<'_>,
56    visitor: impl for<'source> FnOnce(&'source [u8]) -> Result<R, E>,
57) -> binary::BinaryResult<Option<binary::LocatedValue<R, X>>, X, ResponderKeyError<E>> {
58    with_key_at(xml, references, index, operation, |hash, _, _| {
59        visitor(hash)
60    })
61}
62
63/// Compare the original DER byKey selector with an actual responder certificate.
64/// Reuses the same shared RFC6960 identity kernel as OCSP authentication;
65/// original source/depth and current query ceilings govern the hash callback.
66/// The certificate's signature/path/authorization and response producedAt
67/// remain independent. None identifies a different original selector branch.
68pub fn matches_reference_key<'k, X: BinaryXml, D, DE>(
69    xml: &X,
70    references: &ReferenceProperty<X::Id, X::Document, X::Generation>,
71    index: usize,
72    certificate: &Certificate<'_>,
73    digest: &'k D,
74    operation: &Reader<'_>,
75) -> ReferenceKeyResult<X, DE>
76where
77    D: AsnDigest<'k, Error = DE>,
78{
79    with_key_at(xml, references, index, operation, |hash, reader, at| {
80        ResponderId::KeyHash(hash).matches_certificate(
81            certificate,
82            digest,
83            &reader.execution(),
84            "xades.ocspIdentifier.byKey",
85            at,
86        )
87    })
88}
89
90fn with_key_at<X: BinaryXml, R, E>(
91    xml: &X,
92    references: &ReferenceProperty<X::Id, X::Document, X::Generation>,
93    index: usize,
94    operation: &Reader<'_>,
95    visitor: impl for<'source> FnOnce(&'source [u8], &Reader<'source>, u64) -> Result<R, E>,
96) -> binary::BinaryResult<Option<binary::LocatedValue<R, X>>, X, ResponderKeyError<E>> {
97    let location = Location {
98        snapshot: references.snapshot(),
99        node: references.node(),
100    };
101    let work = operation.execution();
102    Access::new(xml, references.snapshot(), &work)
103        .query(references.node(), |_, _, _| Ok(()))
104        .map_err(|cause| binary::Error::Access { location, cause })?;
105    let Some(entry) = references.entries().get(index) else {
106        return Err(binary::Error::Callback {
107            location,
108            cause: ResponderKeyError::Index { index },
109        });
110    };
111    let ReferenceIdentifier::Ocsp {
112        responder: ResponderIdentifier::Key(node),
113        ..
114    } = entry.identifier()
115    else {
116        return Ok(None);
117    };
118    let location = Location {
119        snapshot: references.snapshot(),
120        node,
121    };
122    binary::with_der_reader(xml, location, operation, |reader| {
123        let element = reader
124            .expect(Tag::context(true, 2))
125            .map_err(ResponderKeyError::Encoding)?;
126        let responder =
127            ResponderId::read_element(reader, &element).map_err(ResponderKeyError::Responder)?;
128        if !reader.is_empty() {
129            return Err(ResponderKeyError::Trailing {
130                offset: reader.offset(),
131            });
132        }
133        let ResponderId::KeyHash(hash) = responder else {
134            unreachable!("checked byKey tag produced a name");
135        };
136        visitor(hash, reader, element.span().start)
137            .map(|value| Some(binary::Located { location, value }))
138            .map_err(ResponderKeyError::Callback)
139    })
140}