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