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