Skip to main content

structured_xades/
hashes.rs

1//! Baseline MD5 guard over actual XML-owner hash-use receipts.
2
3use crate::binding::{Bound, SignatureXml};
4use crate::reference::{Parent, VisitError};
5use crate::xml::{Access, AccessError, Snapshot, Xml};
6use structured_asn1::memory::{MemoryError, Work};
7
8mod participants;
9pub use participants::{
10    ParticipantBinding, ParticipantHashes, ParticipantProblem, check_participants,
11};
12
13/// Actual selected hash classification, NOT a URI/width-based resolver.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum HashClass {
16    /// The selected native digest is MD5, whatever its original spelling.
17    Md5,
18    /// A known selected native digest other than MD5. This does not declare
19    /// deployment acceptance; algorithm-suite SHOULDs are not new format bans.
20    OtherKnown,
21    /// Pure signature suite with no external digest; no prehash downgrade.
22    Intrinsic,
23}
24
25/// Original component selecting an actual digest use, including PSS defaults.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum HashOrigin<N> {
28    /// Original selected signature method.
29    SignatureMessage {
30        /// Original instruction/enclosure, even when its method is malformed.
31        instruction: N,
32        /// Actual written method when available; no fake node for a default.
33        method: Option<N>,
34    },
35    /// Original PSS/MGF source; implicit defaults keep their real enclosure.
36    MaskGeneration {
37        /// Original selecting instruction/enclosure.
38        instruction: N,
39        /// Actual written method, independently of its implicit default.
40        method: Option<N>,
41    },
42    /// Actual direct or authenticated Manifest reference and its digest method.
43    Reference {
44        /// Original authenticated instruction.
45        instruction: N,
46        /// Original method, never a fabricated node for a default.
47        method: Option<N>,
48        /// Exact selected instruction parent.
49        parent: Parent<N>,
50    },
51}
52
53impl<N: Copy> HashOrigin<N> {
54    /// Real selecting instruction/enclosure. Optional exact method identity
55    /// remains separately available in the origin; None is not a fake node.
56    #[must_use]
57    pub fn node(self) -> N {
58        match self {
59            Self::SignatureMessage { instruction, .. }
60            | Self::MaskGeneration { instruction, .. }
61            | Self::Reference { instruction, .. } => instruction,
62        }
63    }
64}
65
66/// Actual selected/processed state; an unused advertisement is not Complete.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum HashUseState {
69    /// Completed digest computation or selected signature-suite fact.
70    Complete(HashClass),
71    /// Original algorithm is unsupported; it is not `OtherKnown`.
72    Unsupported,
73    /// Original instruction remains unprocessed; no digest fact is inferred.
74    Unprocessed,
75}
76
77/// One original selected-operation fact; contains no callback-local strings.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct HashUse<N> {
80    /// Exact selected signature whose operation produced this fact.
81    pub signature: N,
82    /// Actual original selection location/role.
83    pub origin: HashOrigin<N>,
84    /// Actual native state, without reconstructing a provider/hash operation.
85    pub state: HashUseState,
86}
87
88/// Discovery completeness is distinct from each instruction's processed state.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum HashCompleteness<N> {
91    /// Entire original message/MGF/reference catalogue was visited.
92    Complete,
93    /// Original frontier could not be completed with the selected capability.
94    Unsupported(N),
95    /// Required original instructions have not been processed.
96    Unprocessed(N),
97}
98
99/// XML-owned projection from ONE existing selected verification operation.
100pub trait HashXml: SignatureXml {
101    /// Preserve original message/PSS-MGF/direct/signed-Manifest uses and full
102    /// identity/default semantics from the actual parsed method/receipts. Never
103    /// reparse SignedInfo/PSS, resolve another provider, rehash/reverify payload,
104    /// classify by digest width, or map an unknown URI into `OtherKnown`. Intrinsic
105    /// belongs only to a pure `SignatureMessage`; PH suites name their real digest.
106    /// Current Work admits all lookup/traversal and any retention. Visitor and
107    /// processing refusals remain sticky separate original causes. Partial or
108    /// empty unprocessed catalogues must not be relabelled Complete.
109    fn visit_hash_uses<VE, W: Work>(
110        &self,
111        snapshot: Snapshot<Self::Document, Self::Generation>,
112        signature: Self::Id,
113        execution: &W,
114        visitor: impl FnMut(HashUse<Self::Id>) -> Result<(), VE>,
115    ) -> Result<HashCompleteness<Self::Id>, VisitError<Self::Error, VE>>;
116}
117
118/// Mandatory format failure/incompleteness, separate from application policy.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum HashFailure {
121    /// EN319132-1 6.2.1 prohibits actual MD5 digest use.
122    ProhibitedMd5,
123    /// An actual method is not supported.
124    Unsupported,
125    /// An actual instruction has not been processed.
126    Unprocessed,
127}
128
129/// An original failed field or incomplete discovery boundary.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum HashProblem<N> {
132    /// Actual field and its completed failure/incomplete state.
133    Use {
134        /// Original selecting role.
135        origin: HashOrigin<N>,
136        /// Original semantic outcome.
137        cause: HashFailure,
138    },
139    /// Original discovery frontier, without inventing a method.
140    Catalogue {
141        /// Actual original frontier/enclosure.
142        node: N,
143        /// Unsupported and unprocessed discovery remain distinct.
144        cause: HashFailure,
145    },
146}
147
148/// No-MD5/complete-use fact for one selected original XML operation. It asserts
149/// no application policy, key trust, certificate status or timestamp outcome.
150pub struct Hashes<N, D, G> {
151    snapshot: Snapshot<D, G>,
152    signature: N,
153}
154
155impl<N: Copy, D: Copy, G: Copy> Hashes<N, D, G> {
156    /// Original immutable source generation.
157    #[must_use]
158    pub fn snapshot(&self) -> Snapshot<D, G> {
159        self.snapshot
160    }
161    /// Same selected signature, never a global any-signature lookup.
162    #[must_use]
163    pub fn signature(&self) -> N {
164        self.signature
165    }
166}
167
168/// Complete guard or explicit nonconforming/incomplete original operation.
169pub enum HashBinding<N, D, G> {
170    /// Every actual original use was qualified and no prohibited MD5 was used.
171    Bound(Hashes<N, D, G>),
172    /// No positive guard receipt; not a processing/resource failure.
173    Unproven(HashProblem<N>),
174}
175
176/// Terminal source/current-operation/adapter errors, not semantic allowances.
177#[derive(Debug)]
178pub enum HashError<E, N> {
179    /// Original XML generation/processing/owner resource cause.
180    Access(AccessError<E, N>),
181    /// Actual receipt was substituted from another selected signature/source.
182    Foreign {
183        /// Original selected/enclosing node.
184        node: N,
185    },
186    /// Current metadata/identity comparison was not admitted.
187    Resource {
188        /// Actual selecting method/enclosure.
189        node: N,
190        /// Original admission refusal.
191        cause: MemoryError,
192    },
193    /// Claimed-complete projection omitted/duplicated the message suite or
194    /// assigned Intrinsic to an external digest use. Not malformed document data.
195    Projection {
196        /// Actual original enclosing method/signature.
197        node: N,
198    },
199}
200
201/// Complete result with the owner's exact original identity/error types.
202pub type HashResult<X> = Result<
203    HashBinding<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
204    HashError<<X as Xml>::Error, <X as Xml>::Id>,
205>;
206
207enum Stop<N> {
208    Problem(HashProblem<N>),
209    Resource(N, MemoryError),
210    Foreign(N),
211    Projection(N),
212}
213
214/// Check EN319132-1 6.2.1 against ACTUAL original hash uses, including PSS/MGF
215/// and authenticated Manifest references. This performs no new hash/provider/
216/// replay operation; it consumes the same selected XML owner's typed receipts.
217/// SHA1/other suite recommendations do not become extra unconditional bans.
218/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
219pub fn check<X: HashXml, W: Work>(
220    bound: &Bound<X::Id, X::Document, X::Generation>,
221    access: &Access<'_, X, W>,
222) -> HashResult<X> {
223    let layout = bound.containers();
224    let signature = layout.signature();
225    if layout.snapshot() != access.snapshot() {
226        return Err(HashError::Foreign { node: signature });
227    }
228    check_selected(signature, access)
229}
230
231// Plain XMLDSig counters have no root SignedProperties/baseline containers.
232// Their privately checked original counter edge supplies selection provenance;
233// the same actual-use kernel applies without manufacturing an XAdES binding.
234fn check_selected<X: HashXml, W: Work>(
235    signature: X::Id,
236    access: &Access<'_, X, W>,
237) -> HashResult<X> {
238    let mut message = false;
239    let visited = access.query_fallible(signature, |xml, snapshot, work| {
240        xml.visit_hash_uses(snapshot, signature, work, |actual| {
241            let node = actual.origin.node();
242            work.charge_work(1)
243                .map_err(|cause| Stop::Resource(node, cause))?;
244            if !access
245                .equal_id(actual.signature, signature)
246                .map_err(|cause| Stop::Resource(node, cause))?
247            {
248                return Err(Stop::Foreign(node));
249            }
250            if matches!(actual.origin, HashOrigin::SignatureMessage { .. }) {
251                if message {
252                    return Err(Stop::Projection(node));
253                }
254                message = true;
255            }
256            let cause = match actual.state {
257                HashUseState::Complete(HashClass::Intrinsic) => {
258                    if !matches!(actual.origin, HashOrigin::SignatureMessage { .. }) {
259                        return Err(Stop::Projection(node));
260                    }
261                    return Ok(());
262                }
263                HashUseState::Complete(HashClass::OtherKnown) => return Ok(()),
264                HashUseState::Complete(HashClass::Md5) => HashFailure::ProhibitedMd5,
265                HashUseState::Unsupported => HashFailure::Unsupported,
266                HashUseState::Unprocessed => HashFailure::Unprocessed,
267            };
268            Err(Stop::Problem(HashProblem::Use {
269                origin: actual.origin,
270                cause,
271            }))
272        })
273    });
274    let completeness = match visited {
275        Ok(complete) => complete,
276        Err(AccessError::Changed { node }) => {
277            return Err(HashError::Access(AccessError::Changed { node }));
278        }
279        Err(AccessError::Resource { node, cause }) => {
280            return Err(HashError::Access(AccessError::Resource { node, cause }));
281        }
282        Err(AccessError::Xml(VisitError::Processing(cause))) => {
283            return Err(HashError::Access(AccessError::Xml(cause)));
284        }
285        Err(AccessError::Xml(VisitError::Visitor(stop))) => {
286            return match stop {
287                Stop::Problem(problem) => Ok(HashBinding::Unproven(problem)),
288                Stop::Resource(node, cause) => Err(HashError::Resource { node, cause }),
289                Stop::Foreign(node) => Err(HashError::Foreign { node }),
290                Stop::Projection(node) => Err(HashError::Projection { node }),
291            };
292        }
293    };
294    match completeness {
295        HashCompleteness::Complete => {}
296        HashCompleteness::Unsupported(node) => {
297            return Ok(HashBinding::Unproven(HashProblem::Catalogue {
298                node,
299                cause: HashFailure::Unsupported,
300            }));
301        }
302        HashCompleteness::Unprocessed(node) => {
303            return Ok(HashBinding::Unproven(HashProblem::Catalogue {
304                node,
305                cause: HashFailure::Unprocessed,
306            }));
307        }
308    }
309    if !message {
310        return Err(HashError::Projection { node: signature });
311    }
312    Ok(HashBinding::Bound(Hashes {
313        snapshot: access.snapshot(),
314        signature,
315    }))
316}