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