Skip to main content

xml_sec/
policy.rs

1//! Immutable security policy snapshots shared by XML Security operations.
2//!
3//! Policy contains trusted, reusable decisions. Caller-owned keys, selected
4//! document targets, tenant identity, and external resource bytes remain in
5//! operation request contexts and are deliberately not stored here.
6
7#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
8use std::collections::HashSet;
9#[cfg(feature = "xmldsig")]
10use std::time::SystemTime;
11
12#[cfg(feature = "xmldsig")]
13use crate::xmldsig::{DigestAlgorithm, SignatureAlgorithm, UriTypeSet, XPathHereSemantics};
14#[cfg(feature = "xmlenc")]
15use crate::xmlenc::{
16    DataEncryptionAlgorithm, KeyTransportAlgorithm, KeyWrapAlgorithm, OaepDigestAlgorithm,
17};
18
19/// Canonical diagnostics for limits represented by [`ResourcePolicy`].
20///
21/// Validation and every enforcement point use the same names so callers can
22/// match typed policy violations without operation-specific string drift.
23pub(crate) mod resource_name {
24    pub const XML_NODES: &str = "XML nodes";
25    pub const XML_DEPTH: &str = "XML element depth";
26    pub const SIGNATURE_REFERENCES: &str = "signature references";
27    pub const REFERENCE_TRANSFORMS: &str = "reference transforms";
28    pub const XML_BASE_COMPONENTS: &str = "XML Base components";
29    pub const XML_BASE_RESOLUTION_BYTES: &str = "XML Base resolution bytes";
30    pub const CANONICALIZED_BYTES: &str = "canonicalized bytes";
31    pub const EXTERNAL_RESOURCE_BYTES: &str = "external resource bytes";
32    pub const AGGREGATE_EXTERNAL_RESOURCE_BYTES: &str = "aggregate external resource bytes";
33    pub const ENCRYPTION_PLAINTEXT_BYTES: &str = "encryption plaintext bytes";
34    #[cfg(feature = "xmlenc")]
35    pub const AGGREGATE_ENCRYPTION_CIPHER_VALUE_BYTES: &str =
36        "aggregate encryption CipherValue bytes";
37    pub const XML_DOCUMENT: &str = "XML document";
38    pub const XML_PARSE_WORK_BYTES: &str = "cumulative XML parse-work bytes";
39    pub const ENCRYPTION_RECIPIENTS: &str = "encryption recipients";
40    pub const ENCRYPTION_METADATA_BYTES: &str = "encryption metadata bytes";
41    pub const KEY_CANDIDATES: &str = "key candidates";
42    pub const KEY_INFO_REFERENCE_DEPTH: &str = "KeyInfoReference depth";
43    pub const BASE64_TRANSFORM_INPUT_BYTES: &str = "Base64 transform input bytes";
44    pub const BASE64_TRANSFORM_OUTPUT_BYTES: &str = "Base64 transform output bytes";
45    pub const XPATH_EXPRESSIONS: &str = "XPath expressions";
46    pub const XPATH_EXPRESSION_BYTES: &str = "XPath expression bytes";
47    pub const XPATH_EXPRESSION_COMPLEXITY: &str = "XPath expression complexity";
48    pub const XPATH_CONTEXT_EVALUATIONS: &str = "XPath context evaluations";
49    pub const XPATH_EVALUATION_WORK: &str = "XPath evaluation work";
50    pub const XPATH_MIRROR_STRING_BYTES: &str = "XPath mirror string bytes";
51    pub const XPATH_STRING_WORK_BYTES: &str = "XPath string-processing work bytes";
52    pub const XPATH_NAMESPACE_BINDINGS: &str = "XPath namespace bindings";
53    pub const XPATH_NAMESPACE_BYTES: &str = "XPath namespace bytes";
54    pub const XPATH_FILTERS: &str = "XPath filters";
55    pub const NODE_SET_FILTER_WORK: &str = "node-set filter work";
56    pub const NODE_SET_ENTRIES: &str = "node-set entries";
57    pub const NODE_SET_OWNED_STRING_BYTES: &str = "node-set owned string bytes";
58    pub const NODE_SET_CUMULATIVE_OWNED_STRING_BYTES: &str =
59        "cumulative node-set owned string bytes";
60}
61
62/// A typed rejection produced by an operation policy.
63#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
64#[non_exhaustive]
65pub enum PolicyViolation {
66    /// An algorithm is outside the operation allowlist.
67    #[error("{operation} policy rejects algorithm {algorithm}")]
68    Algorithm {
69        /// Operation evaluating the algorithm.
70        operation: &'static str,
71        /// Stable algorithm URI or diagnostic name.
72        algorithm: String,
73    },
74    /// An HMAC output length is outside the operation's configured bounds.
75    #[cfg(feature = "xmldsig")]
76    #[error("HMAC output length must be between {minimum} and {maximum} bits: got {actual}")]
77    HmacOutputLength {
78        /// Minimum output length selected by policy.
79        minimum: usize,
80        /// Full output width of the selected HMAC algorithm.
81        maximum: usize,
82        /// Parsed output length.
83        actual: usize,
84    },
85    /// An input exceeds a configured resource ceiling.
86    #[error("{resource} exceeds policy maximum {maximum}: got {actual}")]
87    ResourceLimit {
88        /// Resource whose consumption was rejected.
89        resource: &'static str,
90        /// Effective policy ceiling.
91        maximum: usize,
92        /// Observed consumption.
93        actual: usize,
94    },
95    /// A configured resource limit violates a structural policy requirement.
96    #[error("{resource} has invalid policy limit {actual}: {requirement}")]
97    InvalidResourceLimit {
98        /// Resource whose configured limit was rejected.
99        resource: &'static str,
100        /// Required property of the configured limit.
101        requirement: &'static str,
102        /// Rejected configured value.
103        actual: usize,
104    },
105    /// The selected key source or trust mode is disallowed.
106    #[error("key/trust policy rejected the operation: {reason}")]
107    KeyTrust {
108        /// Non-secret reason suitable for diagnostics.
109        reason: &'static str,
110    },
111    /// XML parser behavior is disallowed.
112    #[error("XML input policy rejected the operation: {reason}")]
113    XmlInput {
114        /// Non-secret reason suitable for diagnostics.
115        reason: &'static str,
116    },
117    /// A URI class is outside the operation policy.
118    #[error("{operation} URI policy rejected the operation: {reason}")]
119    Uri {
120        /// Operation evaluating the URI.
121        operation: &'static str,
122        /// Non-sensitive reason suitable for diagnostics.
123        reason: &'static str,
124    },
125    /// An RSA key falls outside the operation's configured strength range.
126    #[error(
127        "{operation} policy requires {key_type} keys between {minimum_bits} and {maximum_bits} bits: got {actual_bits}"
128    )]
129    KeySize {
130        /// Operation evaluating the key.
131        operation: &'static str,
132        /// Stable key-family diagnostic.
133        key_type: &'static str,
134        /// Configured minimum modulus width.
135        minimum_bits: usize,
136        /// Non-configurable implementation ceiling.
137        maximum_bits: usize,
138        /// Observed normalized modulus width.
139        actual_bits: usize,
140    },
141    /// RSA key material is structurally invalid.
142    #[error("{operation} policy rejects invalid {key_type} key material: {reason}")]
143    InvalidKeyMaterial {
144        /// Operation evaluating the key.
145        operation: &'static str,
146        /// Stable key-family diagnostic.
147        key_type: &'static str,
148        /// Non-secret structural rejection reason.
149        reason: &'static str,
150    },
151}
152
153/// HMAC key-strength and truncation requirements shared by signing and verification.
154#[cfg(feature = "xmldsig")]
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct HmacPolicy {
157    /// Minimum caller-owned secret length in bits.
158    pub minimum_key_bits: usize,
159    /// Minimum emitted or accepted MAC length in bits.
160    pub minimum_output_bits: usize,
161}
162
163#[cfg(feature = "xmldsig")]
164impl Default for HmacPolicy {
165    fn default() -> Self {
166        Self {
167            minimum_key_bits: 128,
168            minimum_output_bits: 128,
169        }
170    }
171}
172
173#[cfg(feature = "xmldsig")]
174impl HmacPolicy {
175    pub(crate) fn validate(self) -> Result<(), PolicyViolation> {
176        if self.minimum_key_bits == 0 || self.minimum_output_bits == 0 {
177            return Err(PolicyViolation::KeyTrust {
178                reason: "HMAC minimum key and output lengths must be nonzero",
179            });
180        }
181        Ok(())
182    }
183
184    pub(crate) fn validate_key_bytes(self, key_bytes: usize) -> Result<(), PolicyViolation> {
185        self.validate_key_bits(key_bytes.saturating_mul(8))
186    }
187
188    pub(crate) fn validate_key_bits(self, key_bits: usize) -> Result<(), PolicyViolation> {
189        if key_bits < self.minimum_key_bits {
190            return Err(PolicyViolation::InvalidKeyMaterial {
191                operation: "HMAC",
192                key_type: "symmetric",
193                reason: "secret is shorter than the configured minimum",
194            });
195        }
196        Ok(())
197    }
198
199    pub(crate) fn validate_output(
200        self,
201        algorithm: SignatureAlgorithm,
202        selected_bits: usize,
203    ) -> Result<(), PolicyViolation> {
204        let maximum = algorithm
205            .hmac_output_bits()
206            .ok_or_else(|| PolicyViolation::Algorithm {
207                operation: "HMAC",
208                algorithm: algorithm.uri().to_owned(),
209            })?;
210        // XMLDSig 1.1 section 6.3.1 makes this a protocol floor, not a
211        // deployment preference: truncation is at least 80 bits and at least
212        // half the underlying digest width. Caller policy may only tighten it.
213        let minimum = self.minimum_output_bits.max(80).max(maximum / 2);
214        if selected_bits < minimum || selected_bits > maximum {
215            return Err(PolicyViolation::HmacOutputLength {
216                minimum,
217                maximum,
218                actual: selected_bits,
219            });
220        }
221        Ok(())
222    }
223}
224
225/// RSA strength and structural requirements for outbound cryptographic operations.
226#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub struct RsaKeyPolicy {
229    /// Minimum mathematical RSA modulus bit length accepted for new output.
230    pub minimum_modulus_bits: usize,
231}
232
233/// DSA strength requirements for legacy signature verification.
234#[cfg(feature = "xmldsig")]
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub struct DsaKeyPolicy {
237    /// Minimum prime-modulus width accepted for DSA verification.
238    pub minimum_modulus_bits: usize,
239}
240
241#[cfg(feature = "xmldsig")]
242impl Default for DsaKeyPolicy {
243    fn default() -> Self {
244        Self {
245            minimum_modulus_bits: 2048,
246        }
247    }
248}
249
250#[cfg(feature = "xmldsig")]
251impl DsaKeyPolicy {
252    /// Validate the configured minimum against the implementation ceiling.
253    pub fn validate(&self) -> Result<(), PolicyViolation> {
254        validate_modulus_minimum(
255            "minimum DSA modulus bits",
256            self.minimum_modulus_bits,
257            64,
258            "minimum must be a nonzero multiple of 64 bits",
259            crate::hard_limits::DSA_MODULUS_BIT_CEILING,
260        )
261    }
262
263    pub(crate) fn validate_modulus_bits(&self, actual_bits: usize) -> Result<(), PolicyViolation> {
264        self.validate()?;
265        if !(self.minimum_modulus_bits..=crate::hard_limits::DSA_MODULUS_BIT_CEILING)
266            .contains(&actual_bits)
267        {
268            return Err(PolicyViolation::KeySize {
269                operation: "verification",
270                key_type: "DSA",
271                minimum_bits: self.minimum_modulus_bits,
272                maximum_bits: crate::hard_limits::DSA_MODULUS_BIT_CEILING,
273                actual_bits,
274            });
275        }
276        Ok(())
277    }
278}
279
280#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
281impl Default for RsaKeyPolicy {
282    fn default() -> Self {
283        Self {
284            minimum_modulus_bits: 2048,
285        }
286    }
287}
288
289#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
290impl RsaKeyPolicy {
291    /// Validate the configured minimum against the implementation ceiling.
292    pub fn validate(&self) -> Result<(), PolicyViolation> {
293        validate_modulus_minimum(
294            "minimum RSA modulus bits",
295            self.minimum_modulus_bits,
296            8,
297            "minimum must be a nonzero whole-byte width",
298            crate::hard_limits::RSA_MODULUS_BIT_CEILING,
299        )
300    }
301
302    pub(crate) fn validate_components(
303        &self,
304        operation: &'static str,
305        modulus: &[u8],
306        exponent: &[u8],
307    ) -> Result<usize, PolicyViolation> {
308        self.validate()?;
309        let modulus = modulus
310            .iter()
311            .position(|byte| *byte != 0)
312            .map(|start| &modulus[start..])
313            .ok_or(PolicyViolation::InvalidKeyMaterial {
314                operation,
315                key_type: "RSA",
316                reason: "modulus is zero",
317            })?;
318        let modulus_bits = modulus
319            .len()
320            .checked_mul(8)
321            .and_then(|width| width.checked_sub(modulus[0].leading_zeros() as usize))
322            .ok_or(PolicyViolation::InvalidKeyMaterial {
323                operation,
324                key_type: "RSA",
325                reason: "modulus width overflows",
326            })?;
327        if !(self.minimum_modulus_bits..=crate::hard_limits::RSA_MODULUS_BIT_CEILING)
328            .contains(&modulus_bits)
329        {
330            return Err(PolicyViolation::KeySize {
331                operation,
332                key_type: "RSA",
333                minimum_bits: self.minimum_modulus_bits,
334                maximum_bits: crate::hard_limits::RSA_MODULUS_BIT_CEILING,
335                actual_bits: modulus_bits,
336            });
337        }
338        if exponent.is_empty() || exponent.len() > 8 {
339            return Err(PolicyViolation::InvalidKeyMaterial {
340                operation,
341                key_type: "RSA",
342                reason: "public exponent has invalid encoding",
343            });
344        }
345        let mut exponent_bytes = [0_u8; 8];
346        exponent_bytes[8 - exponent.len()..].copy_from_slice(exponent);
347        let exponent = u64::from_be_bytes(exponent_bytes);
348        if !(3..=((1_u64 << 33) - 1)).contains(&exponent) || exponent % 2 == 0 {
349            return Err(PolicyViolation::InvalidKeyMaterial {
350                operation,
351                key_type: "RSA",
352                reason: "public exponent is outside the supported odd range",
353            });
354        }
355        Ok(modulus.len())
356    }
357}
358
359#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
360fn validate_modulus_minimum(
361    resource: &'static str,
362    minimum_bits: usize,
363    alignment_bits: usize,
364    requirement: &'static str,
365    ceiling: usize,
366) -> Result<(), PolicyViolation> {
367    if minimum_bits == 0 || !minimum_bits.is_multiple_of(alignment_bits) {
368        return Err(PolicyViolation::InvalidResourceLimit {
369            resource,
370            requirement,
371            actual: minimum_bits,
372        });
373    }
374    ResourcePolicy::within(resource, minimum_bits, ceiling)
375}
376
377/// Resource ceilings shared by parsing, transforms, and cryptographic output.
378#[derive(Debug, Clone, PartialEq, Eq)]
379pub struct ResourcePolicy {
380    /// Maximum XML nodes in one parsed document.
381    pub max_xml_nodes: usize,
382    /// Maximum element nesting depth in one parsed document.
383    pub max_xml_depth: usize,
384    /// Maximum references in one signature or manifest.
385    pub max_references: usize,
386    /// Maximum transforms in one reference.
387    pub max_transforms_per_reference: usize,
388    /// Maximum inherited `xml:base` components in one URI resolution.
389    pub max_xml_base_components: usize,
390    /// Maximum cumulative bytes processed while resolving `xml:base` URIs.
391    pub max_xml_base_resolution_bytes: usize,
392    /// Maximum canonical bytes retained across one signature operation.
393    pub max_canonicalized_bytes: usize,
394    /// Maximum decoded external resource bytes.
395    pub max_external_resource_bytes: usize,
396    /// Maximum bytes in the complete external map and cumulatively dereferenced.
397    pub max_external_resource_total_bytes: usize,
398    /// Maximum XMLEnc plaintext bytes.
399    pub max_encryption_plaintext_bytes: usize,
400    /// Maximum caller-owned XML bytes accepted by any document operation.
401    pub max_xml_document_bytes: usize,
402    /// Maximum cumulative XML bytes parsed by one operation.
403    pub max_xml_parse_work_bytes: usize,
404    /// Maximum independently wrapped recipients.
405    pub max_encryption_recipients: usize,
406    /// Maximum caller-controlled XMLEnc metadata bytes per field.
407    pub max_encryption_metadata_bytes: usize,
408    /// Maximum key-source expansion work and concrete key or certificate
409    /// candidates inspected by one operation stage.
410    pub max_key_candidates: usize,
411    /// Maximum nested `KeyInfoReference` dereference depth.
412    pub max_key_info_reference_depth: usize,
413    /// Maximum bytes accepted by Base64 transforms before decoding.
414    pub max_base64_transform_input_bytes: usize,
415    /// Maximum cumulative bytes emitted by Base64 transforms in one operation.
416    pub max_base64_transform_output_bytes: usize,
417    /// Maximum XPath expressions evaluated by one signature operation.
418    pub max_xpath_expressions: usize,
419    /// Maximum UTF-8 bytes in one XPath expression.
420    pub max_xpath_expression_bytes: usize,
421    /// Maximum structural complexity accepted for one XPath expression.
422    pub max_xpath_expression_complexity: usize,
423    /// Maximum context-node evaluations for one ordinary XPath transform.
424    pub max_xpath_context_evaluations: usize,
425    /// Maximum conservative XPath node-evaluation work per operation.
426    pub max_xpath_evaluation_work: usize,
427    /// Maximum source strings copied into the XPath mirror.
428    pub max_xpath_mirror_string_bytes: usize,
429    /// Maximum conservative XPath string-processing work per operation.
430    pub max_xpath_string_work_bytes: usize,
431    /// Maximum namespace bindings captured by one XPath expression.
432    pub max_xpath_namespace_bindings: usize,
433    /// Maximum aggregate namespace prefix and URI bytes per XPath expression.
434    pub max_xpath_namespace_bytes: usize,
435    /// Maximum filters in one XPath Filter 2.0 transform.
436    pub max_xpath_filters: usize,
437    /// Maximum cumulative node-set entries visited by filtering transforms.
438    pub max_node_set_filter_work: usize,
439    /// Maximum entries materialized in one exact node set.
440    pub max_node_set_entries: usize,
441    /// Maximum owned string bytes in one materialized node set.
442    pub max_node_set_owned_string_bytes: usize,
443    /// Maximum cumulative owned node-set string bytes per operation.
444    pub max_node_set_cumulative_owned_string_bytes: usize,
445}
446
447impl Default for ResourcePolicy {
448    fn default() -> Self {
449        Self {
450            max_xml_nodes: crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize,
451            max_xml_depth: crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
452            max_references: crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
453            max_transforms_per_reference: crate::hard_limits::REFERENCE_TRANSFORM_CEILING,
454            max_xml_base_components: crate::hard_limits::XML_BASE_COMPONENT_CEILING,
455            max_xml_base_resolution_bytes: crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING,
456            max_canonicalized_bytes: crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
457            max_external_resource_bytes: crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
458            max_external_resource_total_bytes:
459                crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
460            max_encryption_plaintext_bytes: crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
461            max_xml_document_bytes: crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
462            max_xml_parse_work_bytes: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING,
463            max_encryption_recipients: crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING,
464            max_encryption_metadata_bytes: crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING,
465            max_key_candidates: crate::hard_limits::KEY_CANDIDATE_CEILING,
466            max_key_info_reference_depth: crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING,
467            max_base64_transform_input_bytes:
468                crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING,
469            max_base64_transform_output_bytes:
470                crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING,
471            max_xpath_expressions: crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING,
472            max_xpath_expression_bytes: crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING,
473            max_xpath_expression_complexity:
474                crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
475            max_xpath_context_evaluations: crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING,
476            max_xpath_evaluation_work: crate::hard_limits::XPATH_EVALUATION_WORK_CEILING,
477            max_xpath_mirror_string_bytes: crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING,
478            max_xpath_string_work_bytes: crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING,
479            max_xpath_namespace_bindings: crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING,
480            max_xpath_namespace_bytes: crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING,
481            max_xpath_filters: crate::hard_limits::XPATH_FILTER_COUNT_CEILING,
482            max_node_set_filter_work: crate::hard_limits::NODE_SET_FILTER_WORK_CEILING,
483            max_node_set_entries: crate::hard_limits::NODE_SET_ENTRY_CEILING,
484            max_node_set_owned_string_bytes: crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING,
485            max_node_set_cumulative_owned_string_bytes:
486                crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING,
487        }
488    }
489}
490
491impl ResourcePolicy {
492    /// Validate policy values against non-configurable implementation ceilings.
493    pub fn validate(&self) -> Result<(), PolicyViolation> {
494        Self::within(
495            resource_name::XML_NODES,
496            self.max_xml_nodes,
497            crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize,
498        )?;
499        Self::within(
500            resource_name::XML_DEPTH,
501            self.max_xml_depth,
502            crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
503        )?;
504        Self::within(
505            resource_name::CANONICALIZED_BYTES,
506            self.max_canonicalized_bytes,
507            crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
508        )?;
509        Self::within(
510            resource_name::SIGNATURE_REFERENCES,
511            self.max_references,
512            crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
513        )?;
514        Self::within(
515            resource_name::REFERENCE_TRANSFORMS,
516            self.max_transforms_per_reference,
517            crate::hard_limits::REFERENCE_TRANSFORM_CEILING,
518        )?;
519        Self::within(
520            resource_name::XML_BASE_COMPONENTS,
521            self.max_xml_base_components,
522            crate::hard_limits::XML_BASE_COMPONENT_CEILING,
523        )?;
524        Self::within(
525            resource_name::XML_BASE_RESOLUTION_BYTES,
526            self.max_xml_base_resolution_bytes,
527            crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING,
528        )?;
529        Self::within(
530            resource_name::XML_DOCUMENT,
531            self.max_xml_document_bytes,
532            crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
533        )?;
534        Self::within(
535            resource_name::XML_PARSE_WORK_BYTES,
536            self.max_xml_parse_work_bytes,
537            crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING,
538        )?;
539        Self::within(
540            resource_name::EXTERNAL_RESOURCE_BYTES,
541            self.max_external_resource_bytes,
542            crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
543        )?;
544        Self::within(
545            resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
546            self.max_external_resource_total_bytes,
547            crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
548        )?;
549        Self::within(
550            resource_name::ENCRYPTION_PLAINTEXT_BYTES,
551            self.max_encryption_plaintext_bytes,
552            crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
553        )?;
554        Self::within(
555            resource_name::ENCRYPTION_RECIPIENTS,
556            self.max_encryption_recipients,
557            crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING,
558        )?;
559        Self::within(
560            resource_name::ENCRYPTION_METADATA_BYTES,
561            self.max_encryption_metadata_bytes,
562            crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING,
563        )?;
564        for (resource, selected, ceiling) in [
565            (
566                resource_name::KEY_CANDIDATES,
567                self.max_key_candidates,
568                crate::hard_limits::KEY_CANDIDATE_CEILING,
569            ),
570            (
571                resource_name::KEY_INFO_REFERENCE_DEPTH,
572                self.max_key_info_reference_depth,
573                crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING,
574            ),
575            (
576                resource_name::BASE64_TRANSFORM_INPUT_BYTES,
577                self.max_base64_transform_input_bytes,
578                crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING,
579            ),
580            (
581                resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
582                self.max_base64_transform_output_bytes,
583                crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING,
584            ),
585            (
586                resource_name::XPATH_EXPRESSIONS,
587                self.max_xpath_expressions,
588                crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING,
589            ),
590            (
591                resource_name::XPATH_EXPRESSION_BYTES,
592                self.max_xpath_expression_bytes,
593                crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING,
594            ),
595            (
596                resource_name::XPATH_EXPRESSION_COMPLEXITY,
597                self.max_xpath_expression_complexity,
598                crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
599            ),
600            (
601                resource_name::XPATH_CONTEXT_EVALUATIONS,
602                self.max_xpath_context_evaluations,
603                crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING,
604            ),
605            (
606                resource_name::XPATH_EVALUATION_WORK,
607                self.max_xpath_evaluation_work,
608                crate::hard_limits::XPATH_EVALUATION_WORK_CEILING,
609            ),
610            (
611                resource_name::XPATH_MIRROR_STRING_BYTES,
612                self.max_xpath_mirror_string_bytes,
613                crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING,
614            ),
615            (
616                resource_name::XPATH_STRING_WORK_BYTES,
617                self.max_xpath_string_work_bytes,
618                crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING,
619            ),
620            (
621                resource_name::XPATH_NAMESPACE_BINDINGS,
622                self.max_xpath_namespace_bindings,
623                crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING,
624            ),
625            (
626                resource_name::XPATH_NAMESPACE_BYTES,
627                self.max_xpath_namespace_bytes,
628                crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING,
629            ),
630            (
631                resource_name::XPATH_FILTERS,
632                self.max_xpath_filters,
633                crate::hard_limits::XPATH_FILTER_COUNT_CEILING,
634            ),
635            (
636                resource_name::NODE_SET_FILTER_WORK,
637                self.max_node_set_filter_work,
638                crate::hard_limits::NODE_SET_FILTER_WORK_CEILING,
639            ),
640            (
641                resource_name::NODE_SET_ENTRIES,
642                self.max_node_set_entries,
643                crate::hard_limits::NODE_SET_ENTRY_CEILING,
644            ),
645            (
646                resource_name::NODE_SET_OWNED_STRING_BYTES,
647                self.max_node_set_owned_string_bytes,
648                crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING,
649            ),
650            (
651                resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
652                self.max_node_set_cumulative_owned_string_bytes,
653                crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING,
654            ),
655        ] {
656            Self::within(resource, selected, ceiling)?;
657        }
658        Ok(())
659    }
660
661    pub(crate) fn validate_xml_document_len(&self, actual: usize) -> Result<(), PolicyViolation> {
662        if actual > self.max_xml_document_bytes {
663            return Err(PolicyViolation::ResourceLimit {
664                resource: resource_name::XML_DOCUMENT,
665                maximum: self.max_xml_document_bytes,
666                actual,
667            });
668        }
669        Ok(())
670    }
671
672    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
673    /// Reject aggregate key-candidate work beyond this policy snapshot.
674    pub fn validate_key_candidates(&self, actual: usize) -> Result<(), PolicyViolation> {
675        if actual > self.max_key_candidates {
676            return Err(PolicyViolation::ResourceLimit {
677                resource: resource_name::KEY_CANDIDATES,
678                maximum: self.max_key_candidates,
679                actual,
680            });
681        }
682        Ok(())
683    }
684
685    /// Reject `KeyInfoReference` traversal beyond this policy snapshot.
686    #[cfg(feature = "xmldsig")]
687    pub fn validate_key_info_reference_depth(&self, actual: usize) -> Result<(), PolicyViolation> {
688        if actual > self.max_key_info_reference_depth {
689            return Err(PolicyViolation::ResourceLimit {
690                resource: resource_name::KEY_INFO_REFERENCE_DEPTH,
691                maximum: self.max_key_info_reference_depth,
692                actual,
693            });
694        }
695        Ok(())
696    }
697
698    pub(crate) fn effective_xml_nodes(&self) -> u32 {
699        u32::try_from(self.max_xml_nodes)
700            .unwrap_or(crate::hard_limits::XML_DOCUMENT_NODE_CEILING)
701            .min(crate::hard_limits::XML_DOCUMENT_NODE_CEILING)
702    }
703
704    #[cfg(feature = "xmldsig")]
705    pub(crate) fn effective_canonicalized_bytes(&self) -> usize {
706        self.max_canonicalized_bytes
707            .min(crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING)
708    }
709
710    #[cfg(feature = "xmldsig")]
711    pub(crate) fn effective_xml_base_components(&self) -> usize {
712        self.max_xml_base_components
713            .min(crate::hard_limits::XML_BASE_COMPONENT_CEILING)
714    }
715
716    #[cfg(feature = "xmldsig")]
717    pub(crate) fn effective_xml_base_resolution_bytes(&self) -> usize {
718        self.max_xml_base_resolution_bytes
719            .min(crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING)
720    }
721
722    fn within(
723        resource: &'static str,
724        selected: usize,
725        ceiling: usize,
726    ) -> Result<(), PolicyViolation> {
727        if selected > ceiling {
728            return Err(PolicyViolation::ResourceLimit {
729                resource,
730                maximum: ceiling,
731                actual: selected,
732            });
733        }
734        Ok(())
735    }
736
737    #[cfg(feature = "xmldsig")]
738    fn nonzero_within(
739        resource: &'static str,
740        selected: usize,
741        ceiling: usize,
742    ) -> Result<(), PolicyViolation> {
743        if selected == 0 {
744            return Err(PolicyViolation::InvalidResourceLimit {
745                resource,
746                requirement: "limit must be nonzero",
747                actual: selected,
748            });
749        }
750        Self::within(resource, selected, ceiling)
751    }
752}
753
754/// XML parsing decisions shared by all operation policies.
755#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
756pub struct XmlInputPolicy {
757    /// Permit bounded internal DTD declarations. External resolution stays off.
758    pub allow_internal_dtd: bool,
759}
760
761/// XMLDSig transform and canonicalization decisions shared by signing and verification.
762#[cfg(feature = "xmldsig")]
763#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
764pub enum SameDocumentIdSemantics {
765    /// Require bare fragment identifiers to satisfy the XML NCName grammar.
766    #[default]
767    Specification,
768    /// Apply libxmlsec1's default barename compatibility grammar.
769    ///
770    /// Registered non-NCName values are accepted unless a single quote makes
771    /// them unrepresentable in the donor's single-quoted XPointer expression.
772    /// The resulting node set retains barename semantics and excludes comments.
773    XmlSecBarename,
774    /// Resolve the fragment text directly as an ID, including non-NCName values.
775    ///
776    /// This reproduces libxmlsec1's explicit Visa3D compatibility flag.
777    XmlSecVisa3d,
778}
779
780/// Wire representation used for ECDSA `SignatureValue` bytes.
781#[cfg(feature = "xmldsig")]
782#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
783pub enum EcdsaSignatureValueEncoding {
784    /// XMLDSig fixed-width `r || s` representation.
785    #[default]
786    XmlDsig,
787    /// ASN.1 DER `SEQUENCE(INTEGER(r), INTEGER(s))` compatibility representation.
788    XmlSecAsn1Der,
789}
790
791/// XMLDSig transform and canonicalization decisions shared by signing and verification.
792#[cfg(feature = "xmldsig")]
793#[derive(Debug, Clone, Default, PartialEq, Eq)]
794pub struct TransformPolicy {
795    /// Allowed transform and canonicalization URIs; `None` accepts every implemented algorithm.
796    pub allowed_algorithms: Option<HashSet<String>>,
797    /// Node selected for the XPath `here()` extension function.
798    pub xpath_here_semantics: XPathHereSemantics,
799    /// Interpretation of bare same-document ID fragments.
800    pub same_document_id_semantics: SameDocumentIdSemantics,
801}
802
803/// URI-class decisions shared by XMLDSig reference and key retrieval processing.
804#[cfg(feature = "xmldsig")]
805#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806pub struct UriPolicy {
807    /// URI classes accepted by SignedInfo and Manifest references.
808    pub references: UriTypeSet,
809    /// URI classes accepted by RetrievalMethod processing.
810    pub retrieval_methods: UriTypeSet,
811    /// URI classes accepted by XMLDSig 1.1 `KeyInfoReference` processing.
812    pub key_info_references: UriTypeSet,
813}
814
815#[cfg(feature = "xmldsig")]
816impl Default for UriPolicy {
817    fn default() -> Self {
818        Self {
819            references: UriTypeSet::SAME_DOCUMENT,
820            retrieval_methods: UriTypeSet::SAME_DOCUMENT,
821            key_info_references: UriTypeSet::SAME_DOCUMENT,
822        }
823    }
824}
825
826/// KeyInfo sources an XMLDSig verification operation is permitted to trust.
827#[cfg(feature = "xmldsig")]
828#[derive(Debug, Clone, Copy, PartialEq, Eq)]
829pub struct KeySourcePolicy {
830    /// Permit a caller-supplied pre-resolved key.
831    pub preset_key: bool,
832    /// Permit keys selected by document `KeyName`.
833    pub key_name: bool,
834    /// Permit public keys embedded in `KeyValue`.
835    pub key_value: bool,
836    /// Permit public keys embedded in `DEREncodedKeyValue`.
837    pub der_encoded_key_value: bool,
838    /// Permit certificates and selectors embedded in `X509Data`.
839    pub x509_data: bool,
840    /// Permit `KeyInfoReference` indirection to another bounded `KeyInfo`.
841    pub key_info_reference: bool,
842}
843
844#[cfg(feature = "xmldsig")]
845impl Default for KeySourcePolicy {
846    fn default() -> Self {
847        Self {
848            preset_key: true,
849            key_name: true,
850            key_value: true,
851            der_encoded_key_value: true,
852            x509_data: true,
853            key_info_reference: true,
854        }
855    }
856}
857
858/// An RFC 5280 extended-key-purpose identifier accepted for XML signing.
859#[cfg(feature = "xmldsig")]
860#[derive(Debug, Clone, PartialEq, Eq, Hash)]
861#[non_exhaustive]
862pub enum ExtendedKeyPurpose {
863    /// TLS server authentication (`id-kp-serverAuth`).
864    ServerAuth,
865    /// TLS client authentication (`id-kp-clientAuth`).
866    ClientAuth,
867    /// Executable code signing (`id-kp-codeSigning`).
868    CodeSigning,
869    /// Email protection (`id-kp-emailProtection`).
870    EmailProtection,
871    /// Trusted timestamping (`id-kp-timeStamping`).
872    TimeStamping,
873    /// OCSP response signing (`id-kp-OCSPSigning`).
874    OcspSigning,
875    /// Application-defined purpose represented as OID arcs.
876    Other(Vec<u64>),
877}
878
879/// X.509 and key-resolution decisions for verification.
880#[cfg(feature = "xmldsig")]
881#[derive(Debug, Clone, PartialEq, Eq)]
882pub struct KeyTrustPolicy {
883    /// Require embedded or selected certificates to chain to a configured anchor.
884    pub verify_x509_chains: bool,
885    /// Maximum validated path depth.
886    pub max_x509_chain_depth: usize,
887    /// Maximum complete or partial signature-valid path states generated.
888    pub max_x509_candidate_paths: usize,
889    /// Legacy signature algorithms explicitly permitted for verification.
890    pub allowed_legacy_signature_algorithms: HashSet<SignatureAlgorithm>,
891    /// RSA requirements enforced for resolved verification keys and issuer keys.
892    pub rsa_keys: RsaKeyPolicy,
893    /// DSA requirements enforced for resolved verification keys.
894    pub dsa_keys: DsaKeyPolicy,
895    /// Purposes accepted when any certificate in a path carries ExtendedKeyUsage.
896    ///
897    /// An empty set accepts only paths whose certificates omit ExtendedKeyUsage
898    /// or use `anyExtendedKeyUsage`; it does not treat TLS/code-signing purposes
899    /// as a generic authorization for XML signatures.
900    pub allowed_extended_key_usages: HashSet<ExtendedKeyPurpose>,
901    /// Authenticate and enforce embedded CRLs during path validation.
902    /// Requires [`Self::verify_x509_chains`].
903    pub check_crls: bool,
904    /// Verification time override; `None` selects the system clock.
905    pub verification_time: Option<SystemTime>,
906}
907
908#[cfg(feature = "xmldsig")]
909impl Default for KeyTrustPolicy {
910    fn default() -> Self {
911        Self {
912            verify_x509_chains: false,
913            max_x509_chain_depth: crate::hard_limits::X509_CHAIN_DEPTH_CEILING,
914            max_x509_candidate_paths: crate::hard_limits::X509_CANDIDATE_PATH_CEILING,
915            allowed_legacy_signature_algorithms: HashSet::new(),
916            rsa_keys: RsaKeyPolicy::default(),
917            dsa_keys: DsaKeyPolicy::default(),
918            allowed_extended_key_usages: HashSet::new(),
919            check_crls: false,
920            verification_time: None,
921        }
922    }
923}
924
925#[cfg(feature = "xmldsig")]
926impl KeyTrustPolicy {
927    pub(crate) fn validate(&self) -> Result<(), PolicyViolation> {
928        if self.check_crls && !self.verify_x509_chains {
929            return Err(PolicyViolation::KeyTrust {
930                reason: "CRL checking requires X.509 chain validation",
931            });
932        }
933        if self
934            .allowed_extended_key_usages
935            .iter()
936            .any(|purpose| match purpose {
937                ExtendedKeyPurpose::Other(arcs) => {
938                    arcs.len() < 2 || arcs[0] > 2 || (arcs[0] < 2 && arcs[1] > 39)
939                }
940                _ => false,
941            })
942        {
943            return Err(PolicyViolation::KeyTrust {
944                reason: "custom extended key purposes must contain valid OID arcs",
945            });
946        }
947        self.rsa_keys.validate()?;
948        self.dsa_keys.validate()?;
949        ResourcePolicy::nonzero_within(
950            "X.509 chain depth",
951            self.max_x509_chain_depth,
952            crate::hard_limits::X509_CHAIN_DEPTH_CEILING,
953        )?;
954        ResourcePolicy::nonzero_within(
955            "X.509 candidate paths",
956            self.max_x509_candidate_paths,
957            crate::hard_limits::X509_CANDIDATE_PATH_CEILING,
958        )
959    }
960}
961
962/// Whether an XMLDSig operation evaluates direct `<Object>/<Manifest>` references.
963#[cfg(feature = "xmldsig")]
964#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
965pub enum ManifestProcessing {
966    /// Leave Manifest reference values untouched and perform no Manifest work.
967    #[default]
968    Ignore,
969    /// Evaluate Manifest references under the operation's shared resource policy.
970    Process,
971}
972
973/// Immutable policy snapshot for XMLDSig verification.
974#[cfg(feature = "xmldsig")]
975#[derive(Debug, Clone, Default)]
976pub struct VerificationPolicy {
977    /// Allowed signature methods; `None` accepts every implemented method subject to
978    /// independent gates such as [`KeyTrustPolicy::allowed_legacy_signature_algorithms`].
979    pub signature_algorithms: Option<HashSet<SignatureAlgorithm>>,
980    /// Allowed reference digest methods; `None` accepts every implemented method.
981    pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
982    /// HMAC secret and output-length requirements.
983    pub hmac: HmacPolicy,
984    /// Required ECDSA `SignatureValue` wire representation.
985    pub ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding,
986    /// Key and certificate trust rules.
987    pub key_trust: KeyTrustPolicy,
988    /// KeyInfo source permissions.
989    pub key_sources: KeySourcePolicy,
990    /// Reference and key-retrieval URI permissions.
991    pub uris: UriPolicy,
992    /// Transform and canonicalization permissions.
993    pub transforms: TransformPolicy,
994    /// Whether authenticated Manifest references are processed.
995    pub manifest_processing: ManifestProcessing,
996    /// XML parser rules.
997    pub xml: XmlInputPolicy,
998    /// Resource ceilings.
999    pub resources: ResourcePolicy,
1000}
1001
1002#[cfg(feature = "xmldsig")]
1003impl VerificationPolicy {
1004    /// Validate the complete snapshot against implementation hard ceilings.
1005    pub fn validate(&self) -> Result<(), PolicyViolation> {
1006        self.resources.validate()?;
1007        self.key_trust.validate()?;
1008        self.hmac.validate()
1009    }
1010
1011    /// Enforce the signature algorithm after key resolution.
1012    pub fn check_signature_algorithm(
1013        &self,
1014        algorithm: SignatureAlgorithm,
1015    ) -> Result<(), PolicyViolation> {
1016        if matches!(
1017            algorithm,
1018            SignatureAlgorithm::RsaSha1
1019                | SignatureAlgorithm::DsaSha1
1020                | SignatureAlgorithm::HmacSha1
1021                | SignatureAlgorithm::EcdsaSha1
1022        ) && !self
1023            .key_trust
1024            .allowed_legacy_signature_algorithms
1025            .contains(&algorithm)
1026        {
1027            return Err(PolicyViolation::Algorithm {
1028                operation: "verification",
1029                algorithm: algorithm.uri().to_string(),
1030            });
1031        }
1032        if self
1033            .signature_algorithms
1034            .as_ref()
1035            .is_some_and(|allowed| !allowed.contains(&algorithm))
1036        {
1037            return Err(PolicyViolation::Algorithm {
1038                operation: "verification",
1039                algorithm: algorithm.uri().to_string(),
1040            });
1041        }
1042        Ok(())
1043    }
1044}
1045
1046/// Immutable policy snapshot for XMLDSig signing.
1047#[cfg(feature = "xmldsig")]
1048#[derive(Debug, Clone, Default)]
1049pub struct SigningPolicy {
1050    /// Allowed signing methods; `None` uses the implemented secure defaults.
1051    pub signature_algorithms: Option<HashSet<SignatureAlgorithm>>,
1052    /// Allowed reference digest methods; `None` uses the implemented secure defaults.
1053    pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
1054    /// HMAC secret and output-length requirements.
1055    pub hmac: HmacPolicy,
1056    /// ECDSA `SignatureValue` wire representation emitted by signing.
1057    pub ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding,
1058    /// RSA requirements enforced before producing a signature.
1059    pub rsa_keys: RsaKeyPolicy,
1060    /// DSA requirements enforced before producing a signature.
1061    pub dsa_keys: DsaKeyPolicy,
1062    /// Reference URI permissions. External URIs remain unsupported until the
1063    /// caller supplies request-scoped external bytes through the signing API.
1064    pub uris: UriPolicy,
1065    /// Transform and canonicalization permissions.
1066    pub transforms: TransformPolicy,
1067    /// Whether direct `<Object>/<Manifest>` reference digests are populated.
1068    pub manifest_processing: ManifestProcessing,
1069    /// XML parser rules.
1070    pub xml: XmlInputPolicy,
1071    /// Resource ceilings.
1072    pub resources: ResourcePolicy,
1073}
1074
1075#[cfg(feature = "xmldsig")]
1076impl SigningPolicy {
1077    /// Validate the complete snapshot before signing work begins.
1078    pub fn validate(&self) -> Result<(), PolicyViolation> {
1079        self.resources.validate()?;
1080        self.rsa_keys.validate()?;
1081        self.dsa_keys.validate()?;
1082        self.hmac.validate()
1083    }
1084
1085    pub(crate) fn check_signature_algorithm(
1086        &self,
1087        algorithm: SignatureAlgorithm,
1088    ) -> Result<(), PolicyViolation> {
1089        check_signing_algorithm(
1090            self.signature_algorithms.as_ref(),
1091            algorithm,
1092            algorithm.signing_allowed(),
1093            algorithm.uri(),
1094        )
1095    }
1096
1097    pub(crate) fn check_digest_algorithm(
1098        &self,
1099        algorithm: DigestAlgorithm,
1100    ) -> Result<(), PolicyViolation> {
1101        check_signing_algorithm(
1102            self.digest_algorithms.as_ref(),
1103            algorithm,
1104            algorithm.signing_allowed(),
1105            algorithm.uri(),
1106        )
1107    }
1108}
1109
1110#[cfg(feature = "xmldsig")]
1111fn check_signing_algorithm<T: Eq + std::hash::Hash>(
1112    allowlist: Option<&HashSet<T>>,
1113    algorithm: T,
1114    default_allowed: bool,
1115    uri: &str,
1116) -> Result<(), PolicyViolation> {
1117    if allowlist.map_or(default_allowed, |allowed| allowed.contains(&algorithm)) {
1118        Ok(())
1119    } else {
1120        Err(PolicyViolation::Algorithm {
1121            operation: "signing",
1122            algorithm: uri.to_owned(),
1123        })
1124    }
1125}
1126
1127/// Immutable policy snapshot for XMLEnc encryption.
1128#[cfg(feature = "xmlenc")]
1129#[derive(Debug, Clone, Default)]
1130pub struct EncryptionPolicy {
1131    /// Allowed content-encryption algorithms.
1132    pub data_algorithms: Option<HashSet<DataEncryptionAlgorithm>>,
1133    /// Allowed RSA key-transport algorithms.
1134    pub key_transport_algorithms: Option<HashSet<KeyTransportAlgorithm>>,
1135    /// Allowed symmetric key-wrap algorithms.
1136    pub key_wrap_algorithms: Option<HashSet<KeyWrapAlgorithm>>,
1137    /// Allowed OAEP digest algorithms.
1138    pub oaep_digests: Option<HashSet<OaepDigestAlgorithm>>,
1139    /// RSA requirements enforced when producing OAEP key transport.
1140    pub rsa_keys: RsaKeyPolicy,
1141    /// XML parser rules.
1142    pub xml: XmlInputPolicy,
1143    /// Resource ceilings.
1144    pub resources: ResourcePolicy,
1145}
1146
1147#[cfg(feature = "xmlenc")]
1148impl EncryptionPolicy {
1149    /// Validate the complete snapshot before outbound encryption work begins.
1150    pub fn validate(&self) -> Result<(), PolicyViolation> {
1151        self.resources.validate()?;
1152        self.rsa_keys.validate()
1153    }
1154}
1155
1156/// Immutable policy snapshot for XMLEnc decryption.
1157#[cfg(feature = "xmlenc")]
1158#[derive(Debug, Clone, Default)]
1159pub struct DecryptionPolicy {
1160    /// Allowed content-decryption algorithms.
1161    pub data_algorithms: Option<HashSet<DataEncryptionAlgorithm>>,
1162    /// Allowed RSA key-transport algorithms accepted on input.
1163    pub key_transport_algorithms: Option<HashSet<KeyTransportAlgorithm>>,
1164    /// Allowed symmetric key-wrap algorithms accepted on input.
1165    pub key_wrap_algorithms: Option<HashSet<KeyWrapAlgorithm>>,
1166    /// Allowed OAEP digest algorithms accepted on input.
1167    pub oaep_digests: Option<HashSet<OaepDigestAlgorithm>>,
1168    /// XML parser rules.
1169    pub xml: XmlInputPolicy,
1170    /// Resource ceilings.
1171    pub resources: ResourcePolicy,
1172}
1173
1174#[cfg(feature = "xmlenc")]
1175impl DecryptionPolicy {
1176    /// Validate the complete snapshot before inbound decryption work begins.
1177    pub fn validate(&self) -> Result<(), PolicyViolation> {
1178        self.resources.validate()
1179    }
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184    use super::*;
1185
1186    #[test]
1187    fn resource_policy_cannot_exceed_implementation_ceiling() {
1188        let policy = ResourcePolicy {
1189            max_xml_nodes: 100_001,
1190            ..ResourcePolicy::default()
1191        };
1192        assert!(matches!(
1193            policy.validate(),
1194            Err(PolicyViolation::ResourceLimit {
1195                resource: resource_name::XML_NODES,
1196                maximum: 100_000,
1197                actual: 100_001,
1198            })
1199        ));
1200    }
1201
1202    #[test]
1203    fn xml_parse_work_policy_cannot_exceed_implementation_ceiling() {
1204        let policy = ResourcePolicy {
1205            max_xml_parse_work_bytes: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING
1206                .saturating_add(1),
1207            ..ResourcePolicy::default()
1208        };
1209
1210        assert_eq!(
1211            policy.validate(),
1212            Err(PolicyViolation::ResourceLimit {
1213                resource: resource_name::XML_PARSE_WORK_BYTES,
1214                maximum: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING,
1215                actual: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING.saturating_add(1),
1216            })
1217        );
1218    }
1219
1220    #[test]
1221    fn every_resource_policy_field_obeys_its_hard_ceiling() {
1222        // Each public tuning knob is only a stricter operational limit; none
1223        // may raise the implementation's allocation ceiling. Exact diagnostics
1224        // also catch a field accidentally paired with another field's ceiling.
1225        type Case = (&'static str, usize, fn(&mut ResourcePolicy) -> &mut usize);
1226        let cases: &[Case] = &[
1227            (
1228                resource_name::XML_NODES,
1229                crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize,
1230                |p| &mut p.max_xml_nodes,
1231            ),
1232            (
1233                resource_name::XML_DEPTH,
1234                crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
1235                |p| &mut p.max_xml_depth,
1236            ),
1237            (
1238                resource_name::SIGNATURE_REFERENCES,
1239                crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
1240                |p| &mut p.max_references,
1241            ),
1242            (
1243                resource_name::REFERENCE_TRANSFORMS,
1244                crate::hard_limits::REFERENCE_TRANSFORM_CEILING,
1245                |p| &mut p.max_transforms_per_reference,
1246            ),
1247            (
1248                resource_name::XML_BASE_COMPONENTS,
1249                crate::hard_limits::XML_BASE_COMPONENT_CEILING,
1250                |p| &mut p.max_xml_base_components,
1251            ),
1252            (
1253                resource_name::XML_BASE_RESOLUTION_BYTES,
1254                crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING,
1255                |p| &mut p.max_xml_base_resolution_bytes,
1256            ),
1257            (
1258                resource_name::CANONICALIZED_BYTES,
1259                crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
1260                |p| &mut p.max_canonicalized_bytes,
1261            ),
1262            (
1263                resource_name::EXTERNAL_RESOURCE_BYTES,
1264                crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
1265                |p| &mut p.max_external_resource_bytes,
1266            ),
1267            (
1268                resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1269                crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
1270                |p| &mut p.max_external_resource_total_bytes,
1271            ),
1272            (
1273                resource_name::ENCRYPTION_PLAINTEXT_BYTES,
1274                crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
1275                |p| &mut p.max_encryption_plaintext_bytes,
1276            ),
1277            (
1278                resource_name::XML_DOCUMENT,
1279                crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
1280                |p| &mut p.max_xml_document_bytes,
1281            ),
1282            (
1283                resource_name::XML_PARSE_WORK_BYTES,
1284                crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING,
1285                |p| &mut p.max_xml_parse_work_bytes,
1286            ),
1287            (
1288                resource_name::ENCRYPTION_RECIPIENTS,
1289                crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING,
1290                |p| &mut p.max_encryption_recipients,
1291            ),
1292            (
1293                resource_name::ENCRYPTION_METADATA_BYTES,
1294                crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING,
1295                |p| &mut p.max_encryption_metadata_bytes,
1296            ),
1297            (
1298                resource_name::KEY_CANDIDATES,
1299                crate::hard_limits::KEY_CANDIDATE_CEILING,
1300                |p| &mut p.max_key_candidates,
1301            ),
1302            (
1303                resource_name::KEY_INFO_REFERENCE_DEPTH,
1304                crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING,
1305                |p| &mut p.max_key_info_reference_depth,
1306            ),
1307            (
1308                resource_name::BASE64_TRANSFORM_INPUT_BYTES,
1309                crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING,
1310                |p| &mut p.max_base64_transform_input_bytes,
1311            ),
1312            (
1313                resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
1314                crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING,
1315                |p| &mut p.max_base64_transform_output_bytes,
1316            ),
1317            (
1318                resource_name::XPATH_EXPRESSIONS,
1319                crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING,
1320                |p| &mut p.max_xpath_expressions,
1321            ),
1322            (
1323                resource_name::XPATH_EXPRESSION_BYTES,
1324                crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING,
1325                |p| &mut p.max_xpath_expression_bytes,
1326            ),
1327            (
1328                resource_name::XPATH_EXPRESSION_COMPLEXITY,
1329                crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
1330                |p| &mut p.max_xpath_expression_complexity,
1331            ),
1332            (
1333                resource_name::XPATH_CONTEXT_EVALUATIONS,
1334                crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING,
1335                |p| &mut p.max_xpath_context_evaluations,
1336            ),
1337            (
1338                resource_name::XPATH_EVALUATION_WORK,
1339                crate::hard_limits::XPATH_EVALUATION_WORK_CEILING,
1340                |p| &mut p.max_xpath_evaluation_work,
1341            ),
1342            (
1343                resource_name::XPATH_MIRROR_STRING_BYTES,
1344                crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING,
1345                |p| &mut p.max_xpath_mirror_string_bytes,
1346            ),
1347            (
1348                resource_name::XPATH_STRING_WORK_BYTES,
1349                crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING,
1350                |p| &mut p.max_xpath_string_work_bytes,
1351            ),
1352            (
1353                resource_name::XPATH_NAMESPACE_BINDINGS,
1354                crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING,
1355                |p| &mut p.max_xpath_namespace_bindings,
1356            ),
1357            (
1358                resource_name::XPATH_NAMESPACE_BYTES,
1359                crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING,
1360                |p| &mut p.max_xpath_namespace_bytes,
1361            ),
1362            (
1363                resource_name::XPATH_FILTERS,
1364                crate::hard_limits::XPATH_FILTER_COUNT_CEILING,
1365                |p| &mut p.max_xpath_filters,
1366            ),
1367            (
1368                resource_name::NODE_SET_FILTER_WORK,
1369                crate::hard_limits::NODE_SET_FILTER_WORK_CEILING,
1370                |p| &mut p.max_node_set_filter_work,
1371            ),
1372            (
1373                resource_name::NODE_SET_ENTRIES,
1374                crate::hard_limits::NODE_SET_ENTRY_CEILING,
1375                |p| &mut p.max_node_set_entries,
1376            ),
1377            (
1378                resource_name::NODE_SET_OWNED_STRING_BYTES,
1379                crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING,
1380                |p| &mut p.max_node_set_owned_string_bytes,
1381            ),
1382            (
1383                resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
1384                crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING,
1385                |p| &mut p.max_node_set_cumulative_owned_string_bytes,
1386            ),
1387        ];
1388
1389        for &(resource, ceiling, field) in cases {
1390            let mut policy = ResourcePolicy::default();
1391            let actual = ceiling.saturating_add(1);
1392            *field(&mut policy) = actual;
1393            assert_eq!(
1394                policy.validate(),
1395                Err(PolicyViolation::ResourceLimit {
1396                    resource,
1397                    maximum: ceiling,
1398                    actual,
1399                }),
1400                "wrong hard-ceiling validation for {resource}",
1401            );
1402        }
1403    }
1404
1405    #[test]
1406    fn resource_policy_accepts_zero_as_a_deny_all_ceiling() {
1407        // Zero is a valid policy decision for resources that an operation can
1408        // avoid consuming; runtime checks must reject only actual non-zero use.
1409        let policy = ResourcePolicy {
1410            max_xml_nodes: 0,
1411            max_xml_depth: 0,
1412            max_references: 0,
1413            max_transforms_per_reference: 0,
1414            max_xml_base_components: 0,
1415            max_xml_base_resolution_bytes: 0,
1416            max_canonicalized_bytes: 0,
1417            max_external_resource_bytes: 0,
1418            max_external_resource_total_bytes: 0,
1419            max_encryption_plaintext_bytes: 0,
1420            max_xml_document_bytes: 0,
1421            max_xml_parse_work_bytes: 0,
1422            max_encryption_recipients: 0,
1423            max_encryption_metadata_bytes: 0,
1424            max_key_candidates: 0,
1425            max_key_info_reference_depth: 0,
1426            max_base64_transform_input_bytes: 0,
1427            max_base64_transform_output_bytes: 0,
1428            max_xpath_expressions: 0,
1429            max_xpath_expression_bytes: 0,
1430            max_xpath_expression_complexity: 0,
1431            max_xpath_context_evaluations: 0,
1432            max_xpath_evaluation_work: 0,
1433            max_xpath_mirror_string_bytes: 0,
1434            max_xpath_string_work_bytes: 0,
1435            max_xpath_namespace_bindings: 0,
1436            max_xpath_namespace_bytes: 0,
1437            max_xpath_filters: 0,
1438            max_node_set_filter_work: 0,
1439            max_node_set_entries: 0,
1440            max_node_set_owned_string_bytes: 0,
1441            max_node_set_cumulative_owned_string_bytes: 0,
1442        };
1443
1444        assert_eq!(policy.validate(), Ok(()));
1445    }
1446
1447    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
1448    #[test]
1449    fn rsa_key_policy_enforces_structure_range_and_explicit_relaxation() {
1450        let secure = RsaKeyPolicy::default();
1451        assert!(matches!(
1452            secure.validate_components("test", &[0x80; 128], &[1, 0, 1]),
1453            Err(PolicyViolation::KeySize {
1454                minimum_bits: 2048,
1455                maximum_bits: 8192,
1456                actual_bits: 1024,
1457                ..
1458            })
1459        ));
1460        let mut short_2048_width = [0_u8; 256];
1461        short_2048_width[0] = 1;
1462        assert!(matches!(
1463            secure.validate_components("test", &short_2048_width, &[1, 0, 1]),
1464            Err(PolicyViolation::KeySize {
1465                minimum_bits: 2048,
1466                actual_bits: 2041,
1467                ..
1468            })
1469        ));
1470        assert!(matches!(
1471            secure.validate_components("test", &[1; 1025], &[1, 0, 1]),
1472            Err(PolicyViolation::KeySize {
1473                actual_bits: 8193,
1474                ..
1475            })
1476        ));
1477        assert!(matches!(
1478            secure.validate_components("test", &[0x80; 256], &[2]),
1479            Err(PolicyViolation::InvalidKeyMaterial { .. })
1480        ));
1481        assert_eq!(
1482            secure.validate_components("test", &[0x80; 256], &[0x80, 0, 0, 1]),
1483            Ok(256),
1484            "normalized RSA components encode the exponent as unsigned bytes"
1485        );
1486
1487        let compatibility = RsaKeyPolicy {
1488            minimum_modulus_bits: 1024,
1489        };
1490        assert_eq!(
1491            compatibility.validate_components("test", &[0x80; 128], &[1, 0, 1]),
1492            Ok(128)
1493        );
1494        assert!(
1495            RsaKeyPolicy {
1496                minimum_modulus_bits: 2047,
1497            }
1498            .validate()
1499            .is_err()
1500        );
1501    }
1502
1503    #[cfg(feature = "xmldsig")]
1504    #[test]
1505    fn mandatory_x509_limits_report_the_nonzero_requirement() {
1506        // A lower-bound violation must not be reported as exceeding the upper
1507        // ceiling: that diagnostic points callers toward the wrong correction.
1508        let policy = KeyTrustPolicy {
1509            max_x509_chain_depth: 0,
1510            ..KeyTrustPolicy::default()
1511        };
1512
1513        let error = policy.validate().expect_err("zero depth must be rejected");
1514        assert!(
1515            error.to_string().contains("must be nonzero"),
1516            "unexpected lower-bound diagnostic: {error}"
1517        );
1518    }
1519
1520    #[cfg(feature = "xmldsig")]
1521    #[test]
1522    fn custom_extended_key_purposes_require_valid_oid_arcs() {
1523        // The typed policy rejects impossible OIDs when the immutable snapshot
1524        // is validated instead of silently making the purpose unmatchable.
1525        let mut policy = KeyTrustPolicy::default();
1526        policy
1527            .allowed_extended_key_usages
1528            .insert(ExtendedKeyPurpose::Other(vec![1, 40, 7]));
1529
1530        assert!(matches!(
1531            policy.validate(),
1532            Err(PolicyViolation::KeyTrust {
1533                reason: "custom extended key purposes must contain valid OID arcs",
1534            })
1535        ));
1536    }
1537
1538    #[cfg(feature = "xmldsig")]
1539    #[test]
1540    fn crl_checking_requires_x509_chain_validation() {
1541        // CRLs authenticate through the validated issuer path. Accepting this
1542        // combination would advertise a security control the resolver skips.
1543        let policy = KeyTrustPolicy {
1544            check_crls: true,
1545            ..KeyTrustPolicy::default()
1546        };
1547
1548        assert!(matches!(
1549            policy.validate(),
1550            Err(PolicyViolation::KeyTrust {
1551                reason: "CRL checking requires X.509 chain validation"
1552            })
1553        ));
1554    }
1555
1556    #[cfg(feature = "xmldsig")]
1557    #[test]
1558    fn legacy_signature_algorithms_require_independent_policy_opt_ins() {
1559        let legacy = [
1560            SignatureAlgorithm::RsaSha1,
1561            SignatureAlgorithm::DsaSha1,
1562            SignatureAlgorithm::HmacSha1,
1563            SignatureAlgorithm::EcdsaSha1,
1564        ];
1565        let mut policy = VerificationPolicy::default();
1566
1567        for algorithm in legacy {
1568            assert!(matches!(
1569                policy.check_signature_algorithm(algorithm),
1570                Err(PolicyViolation::Algorithm { .. })
1571            ));
1572            policy
1573                .key_trust
1574                .allowed_legacy_signature_algorithms
1575                .insert(algorithm);
1576            assert_eq!(policy.check_signature_algorithm(algorithm), Ok(()));
1577            policy
1578                .key_trust
1579                .allowed_legacy_signature_algorithms
1580                .remove(&algorithm);
1581        }
1582    }
1583
1584    #[cfg(feature = "xmldsig")]
1585    #[test]
1586    fn dsa_key_policy_enforces_configured_minimum_and_hard_ceiling() {
1587        let policy = DsaKeyPolicy::default();
1588
1589        assert!(matches!(
1590            policy.validate_modulus_bits(1024),
1591            Err(PolicyViolation::KeySize {
1592                key_type: "DSA",
1593                minimum_bits: 2048,
1594                maximum_bits: 3072,
1595                actual_bits: 1024,
1596                ..
1597            })
1598        ));
1599        assert_eq!(policy.validate_modulus_bits(2048), Ok(()));
1600        assert!(matches!(
1601            policy.validate_modulus_bits(4096),
1602            Err(PolicyViolation::KeySize {
1603                key_type: "DSA",
1604                minimum_bits: 2048,
1605                maximum_bits: 3072,
1606                actual_bits: 4096,
1607                ..
1608            })
1609        ));
1610    }
1611
1612    #[cfg(feature = "xmldsig")]
1613    #[test]
1614    fn hmac_output_policy_cannot_weaken_the_xmldsig_floor() {
1615        // Caller policy may tighten but cannot weaken XMLDSig section 6.3.1:
1616        // truncation is at least 80 bits and at least half the digest width.
1617        let compatibility = HmacPolicy {
1618            minimum_key_bits: 40,
1619            minimum_output_bits: 40,
1620        };
1621
1622        assert_eq!(
1623            compatibility.validate_output(SignatureAlgorithm::HmacSha1, 80),
1624            Ok(())
1625        );
1626        assert!(matches!(
1627            compatibility.validate_output(SignatureAlgorithm::HmacSha1, 72),
1628            Err(PolicyViolation::HmacOutputLength { minimum: 80, .. })
1629        ));
1630        assert!(matches!(
1631            compatibility.validate_output(SignatureAlgorithm::HmacSha256, 120),
1632            Err(PolicyViolation::HmacOutputLength { minimum: 128, .. })
1633        ));
1634    }
1635
1636    #[cfg(feature = "xmldsig")]
1637    #[test]
1638    fn documented_xmldsig_limits_match_hard_limits() {
1639        // Public deployment guidance must change in the same commit as the
1640        // implementation ceilings from which these values are derived.
1641        let docs = include_str!("../docs/xmldsig.md");
1642        let mib = 1024 * 1024;
1643        assert!(docs.contains(&format!(
1644            "Individual resources are limited to {} MiB",
1645            crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING / mib
1646        )));
1647        assert!(docs.contains(&format!(
1648            "complete map to {} MiB",
1649            crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING / mib
1650        )));
1651        assert!(docs.contains(&format!(
1652            "ceilings are {} components and {} MiB per operation",
1653            crate::hard_limits::XML_BASE_COMPONENT_CEILING,
1654            crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING / mib
1655        )));
1656    }
1657}