structured_xades/binary.rs
1//! Scoped ASN.1 carried by original XML nodes, without retaining decoded buffers.
2
3use crate::xml::{Access, AccessError, AccessResult, Name, Snapshot, Xml};
4use structured_asn1::{
5 Encoding, Parser, Reader,
6 memory::{OwnedVec, Work},
7};
8use structured_cms::timestamp::Token;
9
10/// XML-owned lexical decoding of an encapsulated binary value.
11///
12/// Common processing selects the original `Encoding` attribute/default; the
13/// owner validates the XML lexical form,
14/// including children and whitespace, before invoking the visitor. It admits
15/// traversal, decoded capacity and copies before doing that work. Existing XML
16/// base64 processing must be reused rather than reparsing serialized XML.
17///
18/// The owner supplies the actual decoded buffer and its producing Parser inside
19/// the scope. Common processing constructs the Reader with the selected encoding
20/// through [`Parser::read_query_in`]. Both original and current operation
21/// ceilings are therefore preserved by construction, rather than trusting an
22/// adapter to return a Reader with the correct budget. ASN.1 depth is not XML depth.
23///
24/// Decoded offsets identify this binary source, never physical XML bytes. The
25/// original XML node and generation accompany every returned common error.
26pub trait BinaryXml: Xml {
27 /// Visits decoded octets that are not themselves an ASN.1 value, such as a
28 /// `CertDigest` `DigestValue`. No ASN.1 depth gate applies to these octets.
29 /// Both entry points reuse one owner lexical/decoding kernel and admit its
30 /// complete work/capacity before scanning or decoding. No implicit I/O.
31 fn with_octets<R, C, W: Work>(
32 &self,
33 snapshot: Snapshot<Self::Document, Self::Generation>,
34 node: Self::Id,
35 execution: &W,
36 visitor: impl for<'source> FnOnce(&'source [u8]) -> Result<R, C>,
37 ) -> Result<Result<R, C>, Self::Error>;
38
39 /// Visits the actual decoded source under the current operation.
40 ///
41 /// The result cannot borrow the temporary buffer or parser. Independent
42 /// retained results must retain their allocation charges after scope exit.
43 /// The owner must not swallow a visitor refusal or call the visitor twice.
44 fn with_binary<R, C>(
45 &self,
46 snapshot: Snapshot<Self::Document, Self::Generation>,
47 node: Self::Id,
48 operation: &Reader<'_>,
49 visitor: impl for<'source> FnOnce(Decoded<'source>) -> Result<R, C>,
50 ) -> Result<Result<R, C>, Self::Error>;
51}
52
53/// Independent decoded ownership for simultaneous certificate/status checking.
54///
55/// The same owner lexical/decode kernel serves scoped and owned entry points.
56/// Decode directly into admitted caller storage, then move its ownership; do
57/// not decode into an ordinary Vec and copy it a second time. Every allocation,
58/// initialization, lexical pass and decode is admitted before execution. A moved
59/// existing buffer retains its original storage account; new processing work is
60/// charged to the current operation. Returning owned data does not qualify an
61/// implementation's transitive accounting without testing its actual kernel.
62pub trait OwnedBinaryXml: BinaryXml {
63 /// Returns the original decoded octets with independent retained capacity.
64 fn decode_owned<W: Work>(
65 &self,
66 snapshot: Snapshot<Self::Document, Self::Generation>,
67 node: Self::Id,
68 execution: &W,
69 ) -> Result<OwnedVec<u8>, Self::Error>;
70
71 /// Moves an ASN.1 source together with its producing session. A cached
72 /// source keeps its existing cumulative ceilings; the adapter must not
73 /// replace that session merely to start a new query with a clean budget.
74 /// The current operation funds decoding and subsequent query execution.
75 fn decode_binary_owned(
76 &self,
77 snapshot: Snapshot<Self::Document, Self::Generation>,
78 node: Self::Id,
79 operation: &Reader<'_>,
80 ) -> Result<OwnedDecoded, Self::Error>;
81}
82
83/// Independently owned decoded bytes and their actual producing session.
84///
85/// This has no internal borrowed pointers. Move sources into their final
86/// accounted catalogue before borrowing parsed certificates or status values.
87/// A later borrow cannot outlive the source or coexist with moving it. Storage
88/// remains charged to the bytes' original account; a new query pays current work.
89///
90/// ```compile_fail
91/// use structured_asn1::{Encoding, Reader};
92/// use structured_xades::binary::OwnedDecoded;
93/// fn escape<'a>(source: OwnedDecoded, operation: &Reader<'a>) -> Reader<'a> {
94/// source.reader_in(operation, Encoding::Der).unwrap()
95/// }
96/// ```
97#[derive(Debug)]
98pub struct OwnedDecoded {
99 /// Actual producing session, including work already consumed on this source.
100 pub parser: Parser,
101 /// Original decoded allocation, transferred without re-encoding or copying.
102 pub bytes: OwnedVec<u8>,
103 /// Offset of this window in the decoded document, never a physical XML offset.
104 pub base: u64,
105 /// Actual source-reader ASN.1 depth, independent of XML depth; fresh roots use zero.
106 pub depth: u32,
107}
108
109impl OwnedDecoded {
110 /// Borrows the source without allocation, account cloning or a new Parser.
111 #[must_use]
112 pub fn view(&self) -> Decoded<'_> {
113 Decoded {
114 parser: &self.parser,
115 bytes: &self.bytes,
116 base: self.base,
117 depth: self.depth,
118 }
119 }
120
121 /// Opens the original window under both source and current-operation limits.
122 /// The material schema selects its encoding; this never infers a permissive
123 /// codec from malformed bytes. Existing source work is not reset or credited.
124 pub fn reader_in<'source>(
125 &'source self,
126 operation: &Reader<'source>,
127 encoding: Encoding,
128 ) -> structured_asn1::Result<Reader<'source>> {
129 self.parser
130 .read_query_in(operation, &self.bytes, self.base, encoding, self.depth)
131 }
132}
133
134/// The actual scoped decoded source and its original ASN.1 provenance.
135/// These borrows cannot escape the higher-ranked visitor. A fresh decoded
136/// document has base zero and reader depth zero; neither is a physical XML offset.
137#[derive(Debug, Clone, Copy)]
138pub struct Decoded<'source> {
139 /// Producing session for this actual source, with explicit owner limits.
140 pub parser: &'source Parser,
141 /// Original decoded source window, without re-encoding or an extra copy.
142 pub bytes: &'source [u8],
143 /// Source-window offset in this decoded document.
144 pub base: u64,
145 /// Actual source-reader ASN.1 depth, independent of XML depth; fresh roots use zero.
146 pub depth: u32,
147}
148
149// The original XML enclosure of a decoded binary source is a common source
150// location; `Located` and every binary error carry it.
151pub use structured_ltv::source::Location;
152
153/// A retained result alongside its original XML/binary-source enclosure.
154/// Any ASN.1 offsets inside the value remain decoded-source coordinates.
155#[derive(Debug)]
156pub struct Located<T, N, D, G> {
157 /// Original document generation and encapsulated-value node.
158 pub location: Location<N, D, G>,
159 /// Retained scoped result; it cannot borrow the temporary decoded source.
160 pub value: T,
161}
162
163/// A result carrying this XML owner's stable source identity types.
164pub type LocatedValue<T, X> =
165 Located<T, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
166
167/// Moves an owned decoded source out together with its exact XML enclosure.
168///
169/// This performs no ASN.1 parse or codec substitution. The caller's material
170/// schema must still select and validate its actual PKI format. No buffer copy,
171/// allocation or reference-count increment is introduced by this wrapper.
172pub fn owned<X: OwnedBinaryXml, W: Work>(
173 xml: &X,
174 location: Location<X::Id, X::Document, X::Generation>,
175 execution: &W,
176) -> AccessResult<LocatedValue<OwnedVec<u8>, X>, X> {
177 let value = Access::new(xml, location.snapshot, execution)
178 .query(location.node, |xml, snapshot, work| {
179 xml.decode_owned(snapshot, location.node, work)
180 })?;
181 Ok(Located { location, value })
182}
183
184/// Retains a source for simultaneous material checking without a second decode.
185/// Original XML identity accompanies every source in a multi-source catalogue.
186/// Like [`owned`], this does not itself establish the material's PKI schema,
187/// authenticity, completeness or trust.
188pub fn owned_source<X: OwnedBinaryXml>(
189 xml: &X,
190 location: Location<X::Id, X::Document, X::Generation>,
191 operation: &Reader<'_>,
192) -> AccessResult<LocatedValue<OwnedDecoded, X>, X> {
193 let execution = operation.execution();
194 let value = Access::new(xml, location.snapshot, &execution)
195 .query(location.node, |xml, snapshot, _| {
196 xml.decode_binary_owned(snapshot, location.node, operation)
197 })?;
198 Ok(Located { location, value })
199}
200
201/// Refusal preserving both the XML enclosure and the unchanged inner cause.
202#[derive(Debug)]
203pub enum Error<E, C, N, D, G> {
204 /// The original Encoding attribute does not identify a specified PKI codec.
205 UnknownEncoding {
206 /// Original encapsulated-value element.
207 location: Location<N, D, G>,
208 },
209 /// A specified codec has no decoder in this ASN.1 processing capability.
210 /// This is unsupported processing, not malformed XML or a digest mismatch.
211 UnsupportedEncoding {
212 /// Original encapsulated-value element.
213 location: Location<N, D, G>,
214 /// Original specified codec.
215 encoding: PkiEncoding,
216 },
217 /// Common Reader construction refused before the inspection visitor.
218 ReaderCreation {
219 /// Original encapsulated-value element.
220 location: Location<N, D, G>,
221 /// Original ASN.1 processing/resource error in decoded-source coordinates.
222 cause: structured_asn1::Error,
223 },
224 /// XML access, decoding, generation or resource refusal.
225 Access {
226 /// Original XML enclosure, not a fabricated binary byte range.
227 location: Location<N, D, G>,
228 /// Original access/owner cause, including pre-visitor resource errors.
229 cause: AccessError<E, N>,
230 },
231 /// Scoped ASN.1 inspection/check callback refused.
232 Callback {
233 /// Inner offsets refer to the decoded source at this XML node.
234 location: Location<N, D, G>,
235 /// Unchanged cause; retained maps keep their own inner source contexts.
236 cause: C,
237 },
238}
239
240impl<E, C, N, D, G> Error<E, C, N, D, G> {
241 // Flatten nested scoped operations without relabelling XML locations or
242 // allocating/cloning while already handling an opaque callback refusal.
243 pub(crate) fn map_callback<T>(self, map: impl FnOnce(C) -> T) -> Error<E, T, N, D, G> {
244 match self {
245 Self::UnknownEncoding { location } => Error::UnknownEncoding { location },
246 Self::UnsupportedEncoding { location, encoding } => {
247 Error::UnsupportedEncoding { location, encoding }
248 }
249 Self::ReaderCreation { location, cause } => Error::ReaderCreation { location, cause },
250 Self::Access { location, cause } => Error::Access { location, cause },
251 Self::Callback { location, cause } => Error::Callback {
252 location,
253 cause: map(cause),
254 },
255 }
256 }
257}
258
259/// Encoding identifiers defined by EN319132-1 §5.1.3; absence specifically means DER.
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum PkiEncoding {
262 /// ASN.1 distinguished encoding.
263 Der,
264 /// ASN.1 basic encoding.
265 Ber,
266 /// ASN.1 canonical encoding, distinct from merely accepting BER syntax.
267 Cer,
268 /// ASN.1 packed encoding.
269 Per,
270 /// ASN.1 XML encoding, distinct from the outer base64 XML carrier.
271 Xer,
272}
273
274fn pki_encoding<X: Xml, W: Work>(
275 access: &Access<'_, X, W>,
276 node: X::Id,
277) -> core::result::Result<Option<PkiEncoding>, AccessError<X::Error, X::Id>> {
278 access
279 .attribute(
280 node,
281 Name {
282 namespace: "",
283 local: "Encoding",
284 },
285 |value| {
286 // EN319132-1 §5.1.3 specifies DER for the absent attribute; this is
287 // a wire default, never an implicit permissive parser/policy fallback.
288 // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
289 let Some(value) = value else {
290 return Ok(Some(PkiEncoding::Der));
291 };
292 let value = access.trim_xml_whitespace(value)?;
293 for (uri, encoding) in [
294 ("http://uri.etsi.org/01903/v1.2.2#DER", PkiEncoding::Der),
295 ("http://uri.etsi.org/01903/v1.2.2#BER", PkiEncoding::Ber),
296 ("http://uri.etsi.org/01903/v1.2.2#CER", PkiEncoding::Cer),
297 ("http://uri.etsi.org/01903/v1.2.2#PER", PkiEncoding::Per),
298 ("http://uri.etsi.org/01903/v1.2.2#XER", PkiEncoding::Xer),
299 ] {
300 if access.equal(value, uri)? {
301 return Ok(Some(encoding));
302 }
303 }
304 Ok(None)
305 },
306 )?
307 .map_err(|cause| AccessError::Resource { node, cause })
308}
309
310/// Scoped binary result using this XML owner's source identity types.
311pub type BinaryResult<T, X, C> = core::result::Result<
312 T,
313 Error<<X as Xml>::Error, C, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
314>;
315
316/// Visits an original decoded octet value without inventing an ASN.1 enclosure.
317/// The result cannot borrow the temporary decoded buffer.
318///
319/// ```compile_fail
320/// use structured_asn1::memory::Work;
321/// use structured_xades::binary::{self, BinaryResult, BinaryXml, Location};
322/// fn escape<'a, X: BinaryXml, W: Work>(xml: &X,
323/// location: Location<X::Id, X::Document, X::Generation>, work: &W)
324/// -> BinaryResult<&'a [u8], X, ()>
325/// {
326/// binary::with_octets(xml, location, work, |bytes| Ok(bytes))
327/// }
328/// ```
329pub fn with_octets<X: BinaryXml, W: Work, R, C>(
330 xml: &X,
331 location: Location<X::Id, X::Document, X::Generation>,
332 execution: &W,
333 visitor: impl for<'source> FnOnce(&'source [u8]) -> Result<R, C>,
334) -> BinaryResult<R, X, C> {
335 Access::new(xml, location.snapshot, execution)
336 .query(location.node, |xml, snapshot, work| {
337 xml.with_octets(snapshot, location.node, work, visitor)
338 })
339 .map_err(|cause| Error::Access { location, cause })?
340 .map_err(|cause| Error::Callback { location, cause })
341}
342
343/// Visits one original decoded source with snapshot checks around the callback.
344pub fn with_reader<X: BinaryXml, R, C>(
345 xml: &X,
346 location: Location<X::Id, X::Document, X::Generation>,
347 operation: &Reader<'_>,
348 visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> core::result::Result<R, C>,
349) -> BinaryResult<R, X, C> {
350 let execution = operation.execution();
351 let access = Access::new(xml, location.snapshot, &execution);
352 let encoding = selected_encoding(&access, location)
353 .map_err(|error| error.map_callback(|never| match never {}))?;
354 with_reader_as(xml, location, operation, encoding, visitor)
355}
356
357pub(crate) fn selected_encoding<X: Xml, W: Work>(
358 access: &Access<'_, X, W>,
359 location: Location<X::Id, X::Document, X::Generation>,
360) -> BinaryResult<Encoding, X, core::convert::Infallible> {
361 let specified = pki_encoding(access, location.node)
362 .map_err(|cause| Error::Access { location, cause })?
363 .ok_or(Error::UnknownEncoding { location })?;
364 let encoding = match specified {
365 PkiEncoding::Der => Encoding::Der,
366 PkiEncoding::Ber => Encoding::Ber,
367 other => {
368 return Err(Error::UnsupportedEncoding {
369 location,
370 encoding: other,
371 });
372 }
373 };
374 Ok(encoding)
375}
376
377/// Visits a schema-fixed DER value, such as `IssuerSerialV2` (§5.2.2).
378/// An unrelated XML attribute cannot make this ASN.1 value permissive BER.
379/// Outer XML schema/attribute validation remains a separate obligation.
380pub fn with_der_reader<X: BinaryXml, R, C>(
381 xml: &X,
382 location: Location<X::Id, X::Document, X::Generation>,
383 operation: &Reader<'_>,
384 visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> Result<R, C>,
385) -> BinaryResult<R, X, C> {
386 with_reader_as(xml, location, operation, Encoding::Der, visitor)
387}
388
389fn with_reader_as<X: BinaryXml, R, C>(
390 xml: &X,
391 location: Location<X::Id, X::Document, X::Generation>,
392 operation: &Reader<'_>,
393 encoding: Encoding,
394 visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> Result<R, C>,
395) -> BinaryResult<R, X, C> {
396 let execution = operation.execution();
397 let result = Access::new(xml, location.snapshot, &execution)
398 .query(location.node, |xml, snapshot, _| {
399 xml.with_binary(snapshot, location.node, operation, |source| {
400 let mut reader = source
401 .parser
402 .read_query_in(operation, source.bytes, source.base, encoding, source.depth)
403 .map_err(ScopedError::Processing)?;
404 visitor(&mut reader).map_err(ScopedError::Callback)
405 })
406 })
407 .map_err(|cause| Error::Access { location, cause })?;
408 result.map_err(|cause| match cause {
409 ScopedError::Processing(cause) => Error::ReaderCreation { location, cause },
410 ScopedError::Callback(cause) => Error::Callback { location, cause },
411 })
412}
413
414enum ScopedError<C> {
415 Processing(structured_asn1::Error),
416 Callback(C),
417}
418
419/// Scoped token refusal, distinct from XML decoding and callback failures.
420#[derive(Debug)]
421pub enum TokenError<C> {
422 /// Original CMS envelope processing error in decoded-source coordinates.
423 Processing(structured_cms::Error),
424 /// Original inspection or complete timestamp-check refusal.
425 Callback(C),
426}
427
428/// Reads the carried RFC3161 CMS token through its existing common kernel.
429///
430/// The visitor can inspect retained faults or run [`Token::check`] against the
431/// selected TSA certificate and original covered input. Reading alone establishes
432/// neither timestamp mathematics nor authority/trust. Errors from a complete
433/// check, including its retained inner `SourceMap`s, keep the binary/XML enclosure.
434/// No new grammar, encoded copy or replacement default-budget parser is used.
435pub fn with_token<X: BinaryXml, R, C>(
436 xml: &X,
437 location: Location<X::Id, X::Document, X::Generation>,
438 operation: &Reader<'_>,
439 visitor: impl for<'source> FnOnce(Token<'source>, &Reader<'source>) -> core::result::Result<R, C>,
440) -> BinaryResult<R, X, TokenError<C>> {
441 with_reader(xml, location, operation, |reader| {
442 let token = Token::read_from(reader).map_err(TokenError::Processing)?;
443 visitor(token, reader).map_err(TokenError::Callback)
444 })
445}