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}