Skip to main content

structured_xades/renewed/
create.rs

1//! Recomputed Manifest digests before an archive is requested.
2
3use super::check::{self, Input, Mode};
4use crate::{
5    binding::{Bound, Status},
6    certificate::{self, Agreement, DigestProvider},
7    container::DS,
8    digest,
9    produce::{Emit, Error, Transaction},
10    reference::ObjectLocation,
11    timestamp::{TimestampXml, XADES_141},
12    xml::{Access, Snapshot},
13};
14use structured_asn1::{
15    digest::Resolution,
16    memory::{OwnedVec, Work},
17};
18
19/// One original authenticated Manifest instruction offered to the caller's
20/// algorithm-longevity decision. The format does not invent a weak-hash policy.
21#[derive(Debug, Clone, Copy)]
22pub struct Candidate<N> {
23    /// Original Reference instruction, not its detached target.
24    pub reference: N,
25    /// Actual resolver-observed origin, not inferred from URI spelling.
26    pub location: ObjectLocation,
27    /// Completed original reference outcome.
28    pub status: Status,
29}
30
31/// Failure before a complete renewal can be returned or written.
32#[derive(Debug)]
33pub enum CreationError<E, PE, N, D, G> {
34    /// Original append-position traversal/source/resource refusal.
35    Input(crate::timestamp::InputError<E, N>),
36    /// Original source, method, provider, traversal or streaming failure.
37    Processing(super::CheckError<E, PE, N, D, G>),
38    /// Selected old/new algorithm is unsupported, malformed or prohibited.
39    Algorithm {
40        /// Actual offending method.
41        method: N,
42        /// Completed algorithm classification.
43        answer: Agreement,
44    },
45    /// Classification of the supplied new parameter-free method, not a source node.
46    NewAlgorithm(Agreement),
47    /// Original construction resolver cause, including deployment/resource refusal.
48    NewProvider(PE),
49    /// Selected instruction is not an actual successfully checked detached reference.
50    Ineligible {
51        /// Original instruction.
52        reference: N,
53    },
54    /// New and old semantic digest identities are equal.
55    Unchanged {
56        /// Original instruction whose digest was not strengthened.
57        reference: N,
58    },
59}
60
61struct Pair<N, O> {
62    reference: N,
63    instruction: O,
64    object: O,
65}
66
67/// Complete recomputed pairs from the selected original Manifest instructions.
68/// Output ownership is that of the existing selected digest provider. No whole
69/// detached input is buffered, and no URI-based algorithm dispatcher is added.
70pub struct Renewal<'method, N, D, G, O> {
71    snapshot: Snapshot<D, G>,
72    signature: N,
73    parent: Option<N>,
74    canonicalization: N,
75    digest: &'method str,
76    pairs: OwnedVec<Pair<N, O>>,
77}
78
79impl<N: Copy, D: Copy + Eq, G: Copy + Eq, O: AsRef<[u8]>> Renewal<'_, N, D, G, O> {
80    /// Number of actual selected original instructions, not a success subset.
81    #[must_use]
82    pub fn len(&self) -> usize {
83        self.pairs.len()
84    }
85    /// No selected expiring digest means no property should be written.
86    #[must_use]
87    pub fn is_empty(&self) -> bool {
88        self.pairs.is_empty()
89    }
90
91    /// Append the original computed pairs in selection order. The transaction
92    /// copies the original canonicalization method with its parameters and
93    /// emits the exact new parameter-free digest URI resolved during creation.
94    /// EN319132-1 ยง5.5.3 requires ordinary `ReferenceOutput` for `NewSDODigestValue`
95    /// and canonicalized Reference XML for `OriginalRefDigest`, in that order.
96    /// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
97    pub fn write<B: Transaction<Id = N, Document = D, Generation = G>, W: Work>(
98        &self,
99        writer: &mut B,
100        work: &W,
101    ) -> Result<(), Error<B::Error>>
102    where
103        N: structured_asn1::memory::HashWork,
104    {
105        work.charge_work(1).map_err(Error::Resource)?;
106        if writer.base() != self.snapshot
107            || !structured_ltv::source::equal_id(writer.signature(), self.signature, work)
108                .map_err(Error::Resource)?
109            || !crate::produce::same_parent(writer.unsigned_parent(), self.parent, work)
110                .map_err(Error::Resource)?
111        {
112            return Err(Error::Foreign);
113        }
114        if self.pairs.is_empty() {
115            return Ok(());
116        }
117        let mut emit = Emit { writer, work };
118        emit.begin(XADES_141, "RenewedDigestsV2")?;
119        work.charge_work(1).map_err(Error::Resource)?;
120        emit.writer
121            .method(self.canonicalization, work)
122            .map_err(Error::Writer)?;
123        emit.begin(DS, "DigestMethod")?;
124        emit.attribute("Algorithm", self.digest)?;
125        emit.end()?;
126        for pair in &self.pairs {
127            emit.begin(XADES_141, "RecomputedDigestValue")?;
128            emit.binary(XADES_141, "NewSDODigestValue", pair.object.as_ref())?;
129            emit.binary(XADES_141, "OriginalRefDigest", pair.instruction.as_ref())?;
130            emit.end()?;
131        }
132        emit.end()?;
133        work.charge_work(1).map_err(Error::Resource)?;
134        if emit.writer.base() != self.snapshot
135            || !structured_ltv::source::equal_id(emit.writer.signature(), self.signature, work)
136                .map_err(Error::Resource)?
137            || !crate::produce::same_parent(emit.writer.unsigned_parent(), self.parent, work)
138                .map_err(Error::Resource)?
139        {
140            return Err(Error::Foreign);
141        }
142        Ok(())
143    }
144    /// Actual original instruction ordinal-to-identity mapping, without copying.
145    pub fn references(&self) -> impl ExactSizeIterator<Item = N> + '_ {
146        self.pairs.iter().map(|p| p.reference)
147    }
148}
149
150/// Creation result in the XML owner's source and error domain.
151pub type CreationResult<T, X, PE> = Result<
152    T,
153    CreationError<
154        <X as crate::xml::Xml>::Error,
155        PE,
156        <X as crate::xml::Xml>::Id,
157        <X as crate::xml::Xml>::Document,
158        <X as crate::xml::Xml>::Generation,
159    >,
160>;
161/// Complete new renewal with the selected provider's actual output storage.
162pub type Created<'method, 'p, X, P> = Renewal<
163    'method,
164    <X as crate::xml::Xml>::Id,
165    <X as crate::xml::Xml>::Document,
166    <X as crate::xml::Xml>::Generation,
167    <P as structured_asn1::digest::Digest<'p>>::Output,
168>;
169
170/// New parameter-free XML digest methods through the SAME selected provider.
171/// This capability does not add a dispatcher or change existing-node resolution.
172pub trait MethodProvider<'p, X: crate::xml::Xml>: DigestProvider<'p, X> {
173    /// Resolve exactly this URI under the current operation's compiled policy.
174    /// It is emitted unchanged as the new `DigestMethod` Algorithm attribute.
175    /// Admit lexical scans, policy evaluation and any retained identity storage
176    /// through `work` before execution. Reject noncanonical lexical input as
177    /// `Malformed`; do not normalize into a different emitted value. Methods
178    /// requiring parameters are `Unsupported` or `Malformed`, never silently
179    /// assigned absent/default parameters. Original terminal errors stay errors.
180    fn resolve_new<W: Work>(
181        &self,
182        algorithm: &str,
183        work: &W,
184    ) -> Result<Resolution<Self::Identity>, Self::Error>;
185}
186
187/// Resolve every selected method before starting any digest/source callback.
188/// Selection examines the complete authenticated Manifest census under Work;
189/// unknown origin/unchecked/invalid selected instructions refuse, never skip.
190/// Both hashes reuse validation's source and digest kernels. Strength beyond
191/// semantic inequality is the caller's explicit algorithm-longevity policy.
192pub fn create<'method, 'p, X, P, W>(
193    xml: &X,
194    bound: &Bound<X::Id, X::Document, X::Generation>,
195    canonicalization: X::Id,
196    new_method: &'method str,
197    provider: &'p P,
198    work: &W,
199    selected: impl FnMut(Candidate<X::Id>, &W) -> Result<bool, P::Error>,
200) -> CreationResult<Created<'method, 'p, X, P>, X, P::Error>
201where
202    X: TimestampXml,
203    P: MethodProvider<'p, X>,
204    W: Work,
205{
206    let layout = bound.containers();
207    let access = Access::new(xml, layout.snapshot(), work);
208    let processing = CreationError::Processing;
209    let resource = |cause| {
210        processing(super::CheckError::Resource {
211            node: layout.signature(),
212            cause,
213        })
214    };
215    let parent = match layout.unsigned() {
216        Some(unsigned) => crate::timestamp::optional(
217            &access,
218            unsigned,
219            crate::container::XADES,
220            "UnsignedSignatureProperties",
221        )
222        .map_err(CreationError::Input)?,
223        None => None,
224    };
225    access
226        .query(layout.signature(), |_, _, _| Ok(()))
227        .map_err(|e| processing(super::CheckError::Access(e)))?;
228    if !access
229        .has_name(
230            canonicalization,
231            crate::xml::Name {
232                namespace: crate::container::DS,
233                local: "CanonicalizationMethod",
234            },
235        )
236        .map_err(|e| processing(super::CheckError::Access(e)))?
237    {
238        return Err(CreationError::Ineligible {
239            reference: canonicalization,
240        });
241    }
242    let identity = resolve_new(&access, layout.signature(), new_method, provider)?;
243    let choices = choose(&access, layout.signature(), &identity, provider, selected)?;
244    let mut pairs = work.vector();
245    pairs.try_reserve_exact(choices.len()).map_err(resource)?;
246    for reference in choices {
247        let mut source = Input {
248            xml,
249            snapshot: layout.snapshot(),
250            mode: Mode::Canonical {
251                node: reference,
252                method: canonicalization,
253            },
254        };
255        let instruction = computed(&identity, &mut source, provider, work, reference)?;
256        source.mode = Mode::Reference {
257            signature: layout.signature(),
258            node: reference,
259        };
260        let object = computed(&identity, &mut source, provider, work, reference)?;
261        pairs
262            .try_push(Pair {
263                reference,
264                instruction,
265                object,
266            })
267            .map_err(resource)?;
268    }
269    access
270        .query(layout.signature(), |_, _, _| Ok(()))
271        .map_err(|e| processing(super::CheckError::Access(e)))?;
272    Ok(Renewal {
273        snapshot: layout.snapshot(),
274        signature: layout.signature(),
275        parent,
276        canonicalization,
277        digest: new_method,
278        pairs: pairs.into_owned(),
279    })
280}
281
282fn resolve_new<'p, X: TimestampXml, P: MethodProvider<'p, X>, W: Work>(
283    access: &Access<'_, X, W>,
284    signature: X::Id,
285    algorithm: &str,
286    provider: &P,
287) -> CreationResult<certificate::FormatDigest<P::Identity>, X, P::Error> {
288    if let Some(answer) =
289        certificate::preflight_algorithm(algorithm, access.execution()).map_err(|cause| {
290            CreationError::Processing(super::CheckError::Resource {
291                node: signature,
292                cause,
293            })
294        })?
295    {
296        return Err(CreationError::NewAlgorithm(answer));
297    }
298    let resolved = access
299        .query(signature, |_, _, work| {
300            Ok(provider.resolve_new(algorithm, work))
301        })
302        .map_err(|e| CreationError::Processing(super::CheckError::Access(e)))?
303        .map_err(CreationError::NewProvider)?;
304    let resolved = match resolved {
305        Resolution::Supported(identity) => identity,
306        Resolution::Unsupported => return Err(CreationError::NewAlgorithm(Agreement::Unsupported)),
307        Resolution::Malformed => {
308            return Err(CreationError::NewAlgorithm(Agreement::MalformedMethod));
309        }
310    };
311    match certificate::baseline_digest::<X, P>(resolved, provider) {
312        certificate::Expected::Ready(identity) => Ok(identity),
313        certificate::Expected::Answer(answer) => Err(CreationError::NewAlgorithm(answer)),
314    }
315}
316
317fn choose<'p, X: TimestampXml, P: DigestProvider<'p, X>, W: Work>(
318    access: &Access<'_, X, W>,
319    signature: X::Id,
320    identity: &certificate::FormatDigest<P::Identity>,
321    provider: &P,
322    mut selected: impl FnMut(Candidate<X::Id>, &W) -> Result<bool, P::Error>,
323) -> CreationResult<OwnedVec<X::Id>, X, P::Error> {
324    let work = access.execution();
325    let processing = CreationError::Processing;
326    let resource = |cause| {
327        processing(super::CheckError::Resource {
328            node: signature,
329            cause,
330        })
331    };
332    let instructions = check::inventory(access, signature, signature).map_err(processing)?;
333    let mut choices = work.vector();
334    for instruction in instructions {
335        work.charge_work(1).map_err(resource)?;
336        let choose = selected(
337            Candidate {
338                reference: instruction.node,
339                location: instruction.location,
340                status: instruction.status,
341            },
342            work,
343        )
344        .map_err(|cause| {
345            processing(super::CheckError::Provider {
346                node: instruction.node,
347                cause,
348            })
349        })?;
350        if !choose {
351            continue;
352        }
353        if instruction.location != ObjectLocation::Detached || instruction.status != Status::Valid {
354            return Err(CreationError::Ineligible {
355                reference: instruction.node,
356            });
357        }
358        let method = check::original_method(access, instruction.node)
359            .map_err(|e| processing(super::CheckError::Property(e)))?;
360        let old = match check::resolve(access, method, provider).map_err(processing)? {
361            certificate::Expected::Ready(identity) => identity,
362            certificate::Expected::Answer(answer) => {
363                return Err(CreationError::Algorithm { method, answer });
364            }
365        };
366        work.charge_work(1).map_err(resource)?;
367        if provider
368            .compare(old.identity(), identity.identity(), work)
369            .map_err(|cause| {
370                processing(super::CheckError::Provider {
371                    node: method,
372                    cause,
373                })
374            })?
375            == core::cmp::Ordering::Equal
376        {
377            return Err(CreationError::Unchanged {
378                reference: instruction.node,
379            });
380        }
381        choices.try_push(instruction.node).map_err(resource)?;
382    }
383    Ok(choices.into_owned())
384}
385
386fn computed<'p, X: TimestampXml, P: DigestProvider<'p, X>, W: Work>(
387    identity: &certificate::FormatDigest<P::Identity>,
388    source: &mut Input<'_, X>,
389    provider: &'p P,
390    work: &W,
391    node: X::Id,
392) -> CreationResult<P::Output, X, P::Error> {
393    match digest::compute_resolved::<P, _, _>(identity, source, provider, work)
394        .map_err(|cause| CreationError::Processing(super::CheckError::Digest { node, cause }))?
395    {
396        Resolution::Supported(output) => Ok(output),
397        Resolution::Unsupported => Err(CreationError::Algorithm {
398            method: node,
399            answer: Agreement::Unsupported,
400        }),
401        Resolution::Malformed => Err(CreationError::Algorithm {
402            method: node,
403            answer: Agreement::MalformedMethod,
404        }),
405    }
406}