Skip to main content

structured_xades/produce/
augmentation.rs

1//! Source-bound append operations over a caller-owned unpublished XML draft.
2
3use super::{Error, Materials, Timestamp, TimestampKind, Writer};
4use crate::{
5    baseline::Assessment,
6    prepare::Prepared,
7    timestamp::{self, AppendKind, Input, TimestampXml},
8    unsigned::Requirements,
9    xml::Snapshot,
10};
11use structured_asn1::{
12    Reader,
13    memory::{Execution, Work},
14};
15use structured_cms::{
16    provider::SignatureProvider,
17    timestamp::{CheckContext, ProfileCheckError, TokenExtension},
18};
19
20/// Append-only transaction opened by the XML owner at the exact original tail.
21/// Base identities remain fixed while new draft generations are unpublished.
22/// Commit checks preserve original `SignedInfo`, `SignatureValue`, signed target
23/// bytes, namespace context, earlier unsigned subtrees and all archive inputs.
24/// Rollback/discard on any error belongs to the owner of that transaction.
25pub trait Transaction: Writer {
26    /// Opaque source node identity.
27    type Id: Copy + Eq;
28    /// Original document identity.
29    type Document: Copy + Eq;
30    /// Original generation identity.
31    type Generation: Copy + Eq;
32    /// Immutable base used to open this transaction, never the evolving draft.
33    fn base(&self) -> Snapshot<Self::Document, Self::Generation>;
34    /// Exact selected signature, not any signature in the same document.
35    fn signature(&self) -> Self::Id;
36    /// Original unsigned parent, absent when the owner must create it.
37    fn unsigned_parent(&self) -> Option<Self::Id>;
38    /// Copy the complete actual `CanonicalizationMethod` or `DigestMethod` with
39    /// its parameters from the immutable base/staging source into the new property.
40    /// This admits every scan/copy/allocation and verifies source identity.
41    fn method<W: Work>(&mut self, method: Self::Id, work: &W) -> Result<(), Self::Error>;
42}
43
44/// Source-bound plan. Every operation checks this original source and selected
45/// tail before callbacks, and again before returning a completed draft step.
46/// A changed generation needs a new plan and a new TSA request.
47pub struct Augmentation<'a, X: TimestampXml> {
48    xml: &'a X,
49    prepared: &'a Prepared<X>,
50}
51
52/// Original response, selected provider and explicit request/time context.
53/// These borrows neither allocate nor select another crypto engine.
54pub struct ResponseCheck<'a, 'context, P> {
55    /// Exact live original response being checked and incorporated.
56    pub response: &'a structured_cms::timestamp::Response<'a>,
57    /// Actual selected provider.
58    pub provider: &'a P,
59    /// Current operation reader, independent of the response source.
60    pub query: &'context Reader<'a>,
61    /// Existing RFC3161 request, certificate and clock facts.
62    pub context: &'context CheckContext<'a, 'context>,
63}
64
65/// Original whole-signature missing-material report for this XML identity domain.
66pub type Presence<'view, 'source, X> = structured_ltv::material::Incorporation<
67    'view,
68    'source,
69    <X as crate::xml::Xml>::Id,
70    <X as crate::xml::Xml>::Document,
71    <X as crate::xml::Xml>::Generation,
72    crate::material::Placement<<X as crate::xml::Xml>::Id>,
73>;
74/// Source-domain timestamp response result retaining original causes.
75pub type ResponseResult<T, X, PE, RE> =
76    Result<T, ResponseError<<X as crate::xml::Xml>::Error, <X as crate::xml::Xml>::Id, PE, RE>>;
77/// Checked response in the selected XML owner's identity domain.
78pub type CheckedFor<'a, X> = Checked<
79    'a,
80    <X as crate::xml::Xml>::Document,
81    <X as crate::xml::Xml>::Generation,
82    <X as crate::xml::Xml>::Id,
83>;
84/// Future input result, without impossible response/provider error variants.
85pub type InputResult<'a, X> = Result<
86    Input<'a, X>,
87    PreparationError<<X as crate::xml::Xml>::Error, <X as crate::xml::Xml>::Id>,
88>;
89impl<'a, X: TimestampXml> Augmentation<'a, X> {
90    /// Borrow an actual prepared signature, without cloning any source/receipt.
91    #[must_use]
92    pub const fn new(xml: &'a X, prepared: &'a Prepared<X>) -> Self {
93        Self { xml, prepared }
94    }
95
96    /// Prepare the exact future timestamp input after all required material
97    /// and optional `RenewedDigestsV2` have been staged. No placeholder timestamp
98    /// or token is inserted and no canonical bytes are buffered.
99    pub fn input<A, W: Work>(
100        &self,
101        assessment: &Assessment<'_, A, X::Id, X::Document, X::Generation>,
102        kind: TimestampKind,
103        method: X::Id,
104        work: &W,
105    ) -> InputResult<'a, X> {
106        self.gate(assessment, kind, work)
107            .map_err(PreparationError::Admission)?;
108        Input::prepare_append(
109            self.xml,
110            self.prepared.bound(),
111            append_kind(kind),
112            method,
113            work,
114        )
115        .map_err(PreparationError::Input)
116    }
117
118    /// Check the exact original response through the existing RFC3161 and
119    /// `XAdES` profile kernels, including the actual proposed input stream.
120    /// Request context is mandatory. A granted response or matching nonce alone
121    /// cannot construct a timestamp: privately issued mathematical evidence is
122    /// required. Negative facts are returned intact when evidence is absent.
123    #[expect(
124        clippy::result_large_err,
125        reason = "preserve original common profile/source/resource causes without allocating an error while handling exhaustion"
126    )]
127    pub fn check_response<'q, A, P, PE, RE>(
128        &'q self,
129        assessment: &Assessment<'_, A, X::Id, X::Document, X::Generation>,
130        kind: TimestampKind,
131        method: X::Id,
132        check: &ResponseCheck<'q, '_, P>,
133        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
134    ) -> ResponseResult<CheckedFor<'q, X>, X, PE, RE>
135    where
136        'a: 'q,
137        P: for<'scope> SignatureProvider<'scope, Error = PE>,
138    {
139        let work = check.query.execution();
140        self.gate(assessment, kind, &work)
141            .map_err(ResponseError::Admission)?;
142        let mut input = Input::prepare_append(
143            self.xml,
144            self.prepared.bound(),
145            append_kind(kind),
146            method,
147            &work,
148        )
149        .map_err(ResponseError::Input)?;
150        let facts = check
151            .response
152            .check_with_profile(
153                check.provider,
154                check.query,
155                check.context,
156                &mut input,
157                recognizes,
158                |input| timestamp::profile::check(&input),
159            )
160            .map_err(ResponseError::Check)?;
161        Ok(Checked {
162            snapshot: self.prepared.bound().containers().snapshot(),
163            signature: self.prepared.bound().containers().signature(),
164            method,
165            kind,
166            response: check.response,
167            facts,
168        })
169    }
170
171    /// Append the accepted actual token using its actual original method.
172    /// The transaction must be opened against this exact source/tail. Any
173    /// caller-side mutation while awaiting the TSA refuses before emission.
174    pub fn write_timestamp<B, W: Work>(
175        &self,
176        timestamp: &Timestamp<'_, X::Document, X::Generation, X::Id>,
177        writer: &mut B,
178        work: &W,
179    ) -> Result<(), Error<B::Error>>
180    where
181        B: Transaction<Id = X::Id, Document = X::Document, Generation = X::Generation>,
182    {
183        self.require(writer, work)?;
184        let layout = self.prepared.bound().containers();
185        work.charge_work(1).map_err(Error::Resource)?;
186        if timestamp.snapshot != layout.snapshot()
187            || !structured_ltv::source::equal_id(timestamp.signature, layout.signature(), work)
188                .map_err(Error::Resource)?
189        {
190            return Err(Error::Foreign);
191        }
192        timestamp.write(writer, work, |writer, work| {
193            writer.method(timestamp.method, work)
194        })?;
195        self.require(writer, work)
196    }
197
198    /// Append every missing actual artifact from the same source's complete
199    /// incorporation report. Existing copies and earlier protected properties
200    /// are preserved. Re-prepare and re-assess the draft before timestamping it.
201    pub fn write_materials<'view, 'source, B, W: Work>(
202        &self,
203        required: &Presence<'view, 'source, X>,
204        writer: &mut B,
205        work: &W,
206        envelope: impl FnMut(
207            &'view structured_pkix::ocsp::BasicResponse<'source>,
208            &Execution<'_>,
209        )
210            -> Result<&'view structured_pkix::ocsp::ResponseBytes<'source>, B::Error>,
211    ) -> Result<(), Error<B::Error>>
212    where
213        B: Transaction<Id = X::Id, Document = X::Document, Generation = X::Generation>,
214    {
215        self.require(writer, work)?;
216        let layout = self.prepared.bound().containers();
217        work.charge_work(1).map_err(Error::Resource)?;
218        if required.snapshot() != layout.snapshot()
219            || !structured_ltv::source::equal_id(required.signature(), layout.signature(), work)
220                .map_err(Error::Resource)?
221        {
222            return Err(Error::Foreign);
223        }
224        let materials = Materials::missing(required, work, envelope)?;
225        materials.write(writer, work)?;
226        self.require(writer, work)
227    }
228
229    fn gate<A, W: Work>(
230        &self,
231        assessment: &Assessment<'_, A, X::Id, X::Document, X::Generation>,
232        kind: TimestampKind,
233        work: &W,
234    ) -> Result<(), Error<core::convert::Infallible>> {
235        work.charge_work(1).map_err(Error::Resource)?;
236        if self.xml.snapshot() != self.prepared.bound().containers().snapshot()
237            || !core::ptr::eq(assessment.evidence().prepared(), self.prepared)
238        {
239            return Err(Error::Foreign);
240        }
241        if !assessment.all_requirements_met() {
242            return Err(Error::Prerequisites);
243        }
244        if kind == TimestampKind::Archive
245            && !matches!(
246                assessment.requested(),
247                Requirements::LongTerm | Requirements::Archival
248            )
249        {
250            return Err(Error::Prerequisites);
251        }
252        Ok(())
253    }
254
255    fn require<B, W: Work>(&self, writer: &B, work: &W) -> Result<(), Error<B::Error>>
256    where
257        B: Transaction<Id = X::Id, Document = X::Document, Generation = X::Generation>,
258    {
259        let layout = self.prepared.bound().containers();
260        work.charge_work(2).map_err(Error::Resource)?;
261        if self.xml.snapshot() != layout.snapshot()
262            || writer.base() != layout.snapshot()
263            || !structured_ltv::source::equal_id(writer.signature(), layout.signature(), work)
264                .map_err(Error::Resource)?
265            || !super::same_parent(
266                writer.unsigned_parent(),
267                self.prepared.unsigned().parent(),
268                work,
269            )
270            .map_err(Error::Resource)?
271        {
272            return Err(Error::Foreign);
273        }
274        Ok(())
275    }
276}
277
278fn append_kind(kind: TimestampKind) -> AppendKind {
279    match kind {
280        TimestampKind::Signature => AppendKind::Signature,
281        TimestampKind::Archive => AppendKind::Archive,
282    }
283}
284
285/// Complete common response facts, with the exact proposed source and input.
286/// Negative facts and positive evidence remain in one owner, without copying
287/// retained proof storage or allocating a variant box.
288pub struct Checked<'a, D, G, N> {
289    snapshot: Snapshot<D, G>,
290    signature: N,
291    method: N,
292    kind: TimestampKind,
293    response: &'a structured_cms::timestamp::Response<'a>,
294    facts: structured_cms::timestamp::response::CheckedResponse<'a>,
295}
296impl<D: Copy, G: Copy, N: Copy> Checked<'_, D, G, N> {
297    /// Complete original check results, including negative/request/time facts.
298    #[must_use]
299    pub fn facts(&self) -> &structured_cms::timestamp::response::CheckedResponse<'_> {
300        &self.facts
301    }
302    /// Borrow the actual same-response token only when the common checker
303    /// issued evidence for this exact proposed input. No new query or crypto.
304    #[must_use]
305    pub fn timestamp(&self) -> Option<Timestamp<'_, D, G, N>> {
306        let evidence = self.facts.timestamp()?.evidence()?;
307        let token = self.response.token_element()?;
308        let producer = self
309            .response
310            .token()?
311            .signer()
312            .digest_algorithm()
313            .identifier()?
314            .execution_origin();
315        Some(Timestamp {
316            snapshot: self.snapshot,
317            signature: self.signature,
318            method: self.method,
319            kind: self.kind,
320            token,
321            producer,
322            evidence,
323        })
324    }
325}
326/// Source/stage failures during future-input preparation.
327#[derive(Debug)]
328pub enum PreparationError<E, N> {
329    /// Actual shared stage or original/current resource admission.
330    Admission(Error<core::convert::Infallible>),
331    /// Original XML input selection/refusal.
332    Input(timestamp::InputError<E, N>),
333}
334/// Input/source errors and original RFC3161/profile causes remain distinct.
335#[derive(Debug)]
336pub enum ResponseError<E, N, PE, RE> {
337    /// Original stage/source/work admission before provider or input replay.
338    Admission(Error<core::convert::Infallible>),
339    /// Actual original XML input selection or replay refusal.
340    Input(timestamp::InputError<E, N>),
341    /// Existing common timestamp check, including request mismatch/resource cause.
342    Check(ProfileCheckError<timestamp::InputError<E, N>, PE, RE, timestamp::DigestError>),
343}