Skip to main content

structured_xades/timestamp/
profile.rs

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