1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//! Baseline digest rules over the common checker's already parsed fields.
use structured_asn1::{
Reader,
memory::{MemoryError, Work},
};
use structured_cms::{
ess::Version,
provider::DigestAlgorithm,
timestamp::{HashAlgorithmReading, ProfileFailure, ProfileInput, ProfileSource},
};
use structured_pkix::AlgorithmIdentifier;
/// Actual timestamp digest use, not an unused advertised algorithm.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigestUse {
/// The CMS signer's declared digest.
Signer,
/// An actual signed ESS certificate restriction, in original order.
Certificate {
/// Original ESS attribute schema.
version: Version,
/// Original readable identifier ordinal in that attribute.
index: usize,
},
/// The carried `TSTInfo` message imprint.
Imprint,
}
/// Baseline refusal before provider or covered-input side effects.
#[derive(Debug)]
pub enum DigestError {
/// EN319132-1 §6.2.1 unconditionally prohibits this MD5 digest use.
Prohibited {
/// Actual field that selected the digest.
usage: DigestUse,
/// Original algorithm offset in its explicit profile source domain.
offset: u64,
},
/// Current producing work refused before metadata or OID comparison.
Resource {
/// Actual field whose query was refused.
usage: DigestUse,
/// Original enclosing field offset, without fabricated XML coordinates.
offset: u64,
/// Original admission cause, never interpreted as unsupported/mismatch.
cause: MemoryError,
},
}
pub(super) fn check(
input: &ProfileInput<'_, '_, '_, '_>,
) -> Result<(), ProfileFailure<DigestError>> {
// The baseline's SHALL NOT applies to actual digest use, including a
// timestamp's imprint and signed ESS restrictions. Provider support cannot
// relax it; algorithm-suite SHOULD recommendations are not extra bans.
// EN319132-1 V1.3.1 §6.2.1:
// https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
written(
input
.token
.signer()
.digest_algorithm()
.identifier()
.expect("timestamp profile runs only after signer faults were refused"),
input.parent,
ProfileSource::Envelope,
DigestUse::Signer,
)?;
for attribute in [input.ess.v1(), input.ess.v2()].into_iter().flatten() {
let mut index = 0;
while index < attribute.identifiers().len() {
let usage = DigestUse::Certificate {
version: attribute.version(),
index,
};
admit(
input.parent,
ProfileSource::Envelope,
usage,
attribute.element().span().start,
1,
)?;
let identifier = &attribute.identifiers()[index];
if let DigestAlgorithm::Written(algorithm) = identifier.digest_algorithm() {
written(algorithm, input.parent, ProfileSource::Envelope, usage)?;
}
// ESS's implied SHA1/SHA256 are fixed schema identities, not MD5.
index += 1;
}
}
if let HashAlgorithmReading::Identifier(algorithm) = input.info.imprint().algorithm() {
written(
*algorithm,
input.reader,
ProfileSource::Information,
DigestUse::Imprint,
)?;
}
Ok(())
}
fn written(
algorithm: AlgorithmIdentifier<'_>,
parent: &Reader<'_>,
source: ProfileSource,
usage: DigestUse,
) -> Result<(), ProfileFailure<DigestError>> {
// id-md5: RFC1321 §3. The comparison reads at most this fixed OID width;
// mismatched widths need only metadata admission, never a full OID scan.
const MD5: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 2, 5];
let offset = algorithm.offset();
admit(parent, source, usage, offset, 1)?;
let bytes = algorithm.algorithm().as_bytes();
if bytes.len() != MD5.len() {
return Ok(());
}
admit(parent, source, usage, offset, MD5.len() as u64)?;
if bytes == MD5 {
return Err(ProfileFailure {
source,
cause: DigestError::Prohibited { usage, offset },
});
}
Ok(())
}
fn admit(
parent: &Reader<'_>,
source: ProfileSource,
usage: DigestUse,
offset: u64,
cost: u64,
) -> Result<(), ProfileFailure<DigestError>> {
parent
.execution()
.charge_work(cost)
.map_err(|cause| ProfileFailure {
source,
cause: DigestError::Resource {
usage,
offset,
cause,
},
})
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::sync::Arc;
use structured_asn1::{
Encoding, Limits, Parser,
memory::{Memory, MemoryAccount},
};
#[test]
fn prohibited_digest_scan_keeps_current_local_and_aggregate_refusals_distinct() {
// A current query over retained original identifiers cannot borrow
// credit from their earlier parse. Insufficient work is terminal before
// the OID scan, rather than a successful prohibition/allowance decision.
let bytes = [0x30, 10, 6, 8, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 2, 5];
let original = Memory::new(Arc::new(MemoryAccount::new(65536, 65536)));
let parser = Parser::der(original.clone());
let mut parent = parser.read(&bytes, 400).unwrap();
let element = parent.next_element().unwrap().unwrap();
let algorithm = structured_pkix::algorithm::read(&parent, &element, "digest").unwrap();
let parsed_work = original.work_used();
// Each source bridge admits one metadata step before this query.
for (local, aggregate) in [(1, 100), (9, 100), (100, 1), (100, 9), (10, 10)] {
let memory = Memory::new(Arc::new(MemoryAccount::new(0, aggregate)));
let operation = Parser::new(
Encoding::Der,
Limits {
max_work: local,
..Limits::default()
},
memory.clone(),
);
let query = operation.read(&[], 900).unwrap();
let scoped = parser
.read_query_in(&query, &[], 400, Encoding::Der, 0)
.unwrap();
let refusal = written(
algorithm,
&scoped,
ProfileSource::Envelope,
DigestUse::Signer,
)
.unwrap_err();
assert_eq!(refusal.source, ProfileSource::Envelope);
if local == 10 && aggregate == 10 {
assert!(matches!(
refusal.cause,
DigestError::Prohibited {
usage: DigestUse::Signer,
offset: 400
}
));
assert_eq!(memory.work_used(), 10);
} else {
assert!(matches!(
refusal.cause,
DigestError::Resource {
usage: DigestUse::Signer,
offset: 400,
..
}
));
assert!(memory.work_used() <= 2);
}
assert_eq!(memory.used(), 0);
}
assert_eq!(original.work_used(), parsed_work);
}
}