Skip to main content

xml_sec/
policy.rs

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