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