Skip to main content

xml_sec/xmldsig/verify/session/
lookup.rs

1//! Operation-bound lookups for signature-extension formats.
2
3use super::*;
4
5/// Outcome of resolving a same-document bare-name URI (`#id`).
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum BareNameTarget {
8    /// The fragment names exactly this element under the operation's ID rules.
9    Element(NodeIdentity),
10    /// The URI has another form: a non-empty base, no fragment, or an XPointer.
11    NotBareName,
12    /// No element, or more than one, carries this ID.
13    Unresolved,
14}
15
16/// An original `DigestMethod` interpreted under the compiled digest policy.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DigestMethodResolution {
19    /// A digest this crate implements and the operation policy permits.
20    Supported(DigestAlgorithm),
21    /// The Algorithm URI names no digest implemented by this crate.
22    Unsupported,
23    /// The required Algorithm attribute is absent.
24    Malformed,
25}
26
27/// One incremental digest of a verified operation's provider.
28pub struct SessionDigest<'p> {
29    algorithm: DigestAlgorithm,
30    state: Box<dyn crate::provider::DigestState + 'p>,
31}
32
33impl SessionDigest<'_> {
34    /// Required digest output width, independent of provider output.
35    pub fn output_len(&self) -> usize {
36        self.algorithm.output_len()
37    }
38
39    /// Absorb `data`, charging its length to `work` first.
40    pub fn update<Q: Work>(&mut self, data: &[u8], work: &Q) -> Result<(), DsigError> {
41        work.charge_work(u64::try_from(data.len()).map_err(|_| MemoryError::WorkExceeded)?)?;
42        self.state.update(data).map_err(DsigError::from)
43    }
44
45    /// Write the digest into an exactly sized caller buffer. Admit finalization
46    /// work and the provider's temporary output before invoking it; the caller
47    /// can keep an inline output without another heap allocation.
48    pub fn finish<Q: Work>(self, output: &mut [u8], work: &Q) -> Result<(), DsigError> {
49        let width = self.algorithm.output_len();
50        if output.len() != width {
51            return Err(DsigError::InvalidRequest {
52                reason: "digest output buffer has the wrong width",
53            });
54        }
55        work.charge_work(width as u64)?;
56        let _temporary = work.memory().reserve(width)?;
57        let digest = super::super::super::digest::finish_digest(self.algorithm, self.state)
58            .map_err(DsigError::from)?;
59        output.copy_from_slice(&digest);
60        Ok(())
61    }
62}
63
64impl<W: Work> VerifiedSession<'_, '_, '_, W> {
65    /// Resolve `#id` through this operation's ID registrations, fragment
66    /// unescaping and same-document ID semantics, charging `work`. XMLDSig 1.1
67    /// §4.4.3.3 defines the bare-name form; XPointer and non-fragment forms are
68    /// reported, never interpreted.
69    /// <https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-Same-Document>
70    pub fn resolve_bare_name_in<Q: Work>(
71        &self,
72        uri: &str,
73        work: &Q,
74    ) -> Result<BareNameTarget, DsigError> {
75        work.charge_work(1)?;
76        let target = self
77            .resolver
78            .bare_name_node_in(
79                uri,
80                Some(self.budgets.transforms.node_set_materialization()),
81                &crate::c14n::scratch::Accounted(work),
82            )
83            .map_err(|error| {
84                DsigError::Reference(ReferenceProcessingError::UriDereference(error))
85            })?;
86        Ok(match target {
87            None => BareNameTarget::NotBareName,
88            Some(Some(node)) if node.is_element() => {
89                BareNameTarget::Element(self.view.node_identity(node))
90            }
91            Some(_) => BareNameTarget::Unresolved,
92        })
93    }
94
95    /// Interpret an original `DigestMethod` element. A recognized algorithm
96    /// that the operation policy forbids is a policy error, not `Unsupported`.
97    pub fn resolve_digest_method_in<Q: Work>(
98        &self,
99        method: NodeIdentity,
100        work: &Q,
101    ) -> Result<DigestMethodResolution, DsigError> {
102        work.charge_work(1)?;
103        let node = self.view.resolve_node(method)?;
104        if !node.has_tag_name((XMLDSIG_NS, "DigestMethod")) {
105            return Err(DsigError::InvalidRequest {
106                reason: "digest method identity is not a ds:DigestMethod",
107            });
108        }
109        let mut algorithm = None;
110        for attribute in node.attributes() {
111            work.charge_work(
112                u64::try_from(attribute.name().len() + 1).map_err(|_| MemoryError::WorkExceeded)?,
113            )?;
114            if attribute.namespace().is_none() && attribute.name() == "Algorithm" {
115                algorithm = Some(attribute.value());
116                break;
117            }
118        }
119        let Some(uri) = algorithm else {
120            return Ok(DigestMethodResolution::Malformed);
121        };
122        work.charge_work(u64::try_from(uri.len()).map_err(|_| MemoryError::WorkExceeded)?)?;
123        // XMLDSig 1.1 §4.4.3.5 types Algorithm as anyURI, whose whiteSpace is
124        // collapse (XSD 1.0 §4.3.6). No algorithm URI has interior whitespace,
125        // so trimming the four XML whitespace characters decides the same.
126        // https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#rf-whiteSpace
127        let uri = uri.trim_matches([' ', '\t', '\r', '\n']);
128        let Some(algorithm) = DigestAlgorithm::from_uri(uri) else {
129            return Ok(DigestMethodResolution::Unsupported);
130        };
131        self.context.policy.check_digest_algorithm(algorithm)?;
132        Ok(DigestMethodResolution::Supported(algorithm))
133    }
134
135    /// Start an incremental digest with the selected provider under the
136    /// compiled digest policy.
137    pub fn begin_digest(&self, algorithm: DigestAlgorithm) -> Result<SessionDigest<'_>, DsigError> {
138        self.context.policy.check_digest_algorithm(algorithm)?;
139        let state = super::super::super::digest::begin_digest(self.context.provider, algorithm)
140            .map_err(DsigError::from)?;
141        Ok(SessionDigest { algorithm, state })
142    }
143
144    /// This verified request's own execution. A continuation that funds its
145    /// work from the request, rather than from a separately supplied query
146    /// account, keeps the caller's local and aggregate ceilings.
147    pub fn execution(&self) -> &W {
148        self.work
149    }
150
151    /// A DER certificate parser bounded by this operation's X.509 resource
152    /// policy (`max_x509_work`, `max_x509_workspace_bytes`, under the hard
153    /// ceilings), on a fresh account of its own, as verification's
154    /// certificate processing is. Path validation and certificate storage
155    /// that use its account obey the same limits.
156    pub fn certificate_parser(&self) -> structured_asn1::Parser {
157        super::super::super::pkix::CertificateSession::new(&self.context.policy.resources)
158            .into_parser()
159    }
160
161    /// Shared path-processor bounds from this operation's key-trust policy,
162    /// the same bounds verification applied to its own paths.
163    pub fn certificate_path_bounds(&self) -> structured_pkix::path::ValidationBounds {
164        super::super::super::pkix::path_bounds(self.context.policy.key_trust.max_x509_chain_depth)
165    }
166
167    /// Certificate-path signature checks for `chain` and `anchor` through this
168    /// operation's provider and certificate trust policy.
169    pub fn certificate_path_verifier<'a, 'p>(
170        &'p self,
171        chain: &'p [&'a structured_pkix::Certificate<'a>],
172        anchor: &'a structured_pkix::Certificate<'a>,
173    ) -> super::super::super::pkix::CertificatePathVerifier<'a, 'p> {
174        super::super::super::pkix::CertificatePathVerifier::new(
175            self.context.provider,
176            &self.context.policy.key_trust,
177            chain,
178            anchor,
179        )
180    }
181}
182
183#[cfg(test)]
184mod digest_admission_tests {
185    use super::*;
186    use std::cell::Cell;
187
188    struct Observed<'a>(&'a Cell<bool>);
189    impl crate::provider::DigestState for Observed<'_> {
190        fn update(&mut self, _: &[u8]) -> Result<(), crate::provider::ProviderError> {
191            Ok(())
192        }
193        fn finalize(self: Box<Self>) -> Result<Vec<u8>, crate::provider::ProviderError> {
194            self.0.set(true);
195            Ok(vec![0; 32])
196        }
197    }
198
199    #[test]
200    fn digest_refusals_never_enter_provider_finalize() {
201        // Wrong output shape and both admission axes fail before the backend,
202        // and a temporary output reservation is released on every path.
203        for (capacity, work, width) in [(31, 100, 32), (32, 31, 32), (32, 100, 31), (32, 100, 32)] {
204            let memory = structured_asn1::memory::Memory::new(std::sync::Arc::new(
205                structured_asn1::memory::MemoryAccount::new(capacity, work),
206            ));
207            let called = Cell::new(false);
208            let state = SessionDigest {
209                algorithm: DigestAlgorithm::Sha256,
210                state: Box::new(Observed(&called)),
211            };
212            let mut output = [0; 32];
213            let result = state.finish(&mut output[..width], &memory);
214            let admitted = capacity == 32 && work == 100 && width == 32;
215            assert_eq!(result.is_ok(), admitted, "{result:?}");
216            assert_eq!(called.get(), admitted);
217            assert_eq!(memory.used(), 0);
218        }
219    }
220}