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.oid().to_owned(),
1052            });
1053        }
1054        Ok(())
1055    }
1056
1057    pub(crate) fn validate(&self) -> Result<(), PolicyViolation> {
1058        if self.check_crls && !self.verify_x509_chains {
1059            return Err(PolicyViolation::KeyTrust {
1060                reason: "CRL checking requires X.509 chain validation",
1061            });
1062        }
1063        if self
1064            .allowed_extended_key_usages
1065            .iter()
1066            .any(|purpose| match purpose {
1067                ExtendedKeyPurpose::Other(arcs) => {
1068                    arcs.len() < 2 || arcs[0] > 2 || (arcs[0] < 2 && arcs[1] > 39)
1069                }
1070                _ => false,
1071            })
1072        {
1073            return Err(PolicyViolation::KeyTrust {
1074                reason: "custom extended key purposes must contain valid OID arcs",
1075            });
1076        }
1077        self.rsa_keys.validate()?;
1078        self.dsa_keys.validate()?;
1079        ResourcePolicy::nonzero_within(
1080            "X.509 chain depth",
1081            self.max_x509_chain_depth,
1082            crate::hard_limits::X509_CHAIN_DEPTH_CEILING,
1083        )?;
1084        ResourcePolicy::nonzero_within(
1085            "X.509 candidate paths",
1086            self.max_x509_candidate_paths,
1087            crate::hard_limits::X509_CANDIDATE_PATH_CEILING,
1088        )
1089    }
1090}
1091
1092/// Whether an XMLDSig operation evaluates direct `<Object>/<Manifest>` references.
1093#[cfg(feature = "xmldsig")]
1094#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1095pub enum ManifestProcessing {
1096    /// Leave Manifest reference values untouched and perform no Manifest work.
1097    #[default]
1098    Ignore,
1099    /// Evaluate Manifest references under the operation's shared resource policy.
1100    Process,
1101}
1102
1103/// Immutable policy snapshot for XMLDSig verification.
1104#[cfg(feature = "xmldsig")]
1105#[derive(Debug, Clone, Default)]
1106pub struct VerificationPolicy {
1107    /// Allowed signature methods; `None` accepts every implemented method subject to
1108    /// independent gates such as [`KeyTrustPolicy::allowed_legacy_signature_algorithms`].
1109    pub signature_algorithms: Option<HashSet<SignatureAlgorithm>>,
1110    /// Allowed reference digest methods; `None` accepts every implemented method.
1111    pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
1112    /// HMAC secret and output-length requirements.
1113    pub hmac: HmacPolicy,
1114    /// Required ECDSA `SignatureValue` wire representation.
1115    pub ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding,
1116    /// Key and certificate trust rules.
1117    pub key_trust: KeyTrustPolicy,
1118    /// KeyInfo source permissions.
1119    pub key_sources: KeySourcePolicy,
1120    /// Reference and key-retrieval URI permissions.
1121    pub uris: UriPolicy,
1122    /// Transform and canonicalization permissions.
1123    pub transforms: TransformPolicy,
1124    /// Whether authenticated Manifest references are processed.
1125    pub manifest_processing: ManifestProcessing,
1126    /// XML parser rules.
1127    pub xml: XmlInputPolicy,
1128    /// Resource ceilings.
1129    pub resources: ResourcePolicy,
1130}
1131
1132#[cfg(feature = "xmldsig")]
1133impl VerificationPolicy {
1134    /// Validate the complete snapshot against implementation hard ceilings.
1135    pub fn validate(&self) -> Result<(), PolicyViolation> {
1136        self.resources.validate()?;
1137        self.key_trust.validate()?;
1138        self.hmac.validate()
1139    }
1140
1141    /// Enforce the signature algorithm after key resolution.
1142    pub fn check_signature_algorithm(
1143        &self,
1144        algorithm: SignatureAlgorithm,
1145    ) -> Result<(), PolicyViolation> {
1146        if matches!(algorithm, SignatureAlgorithm::PostQuantum(_))
1147            && !self
1148                .signature_algorithms
1149                .as_ref()
1150                .is_some_and(|allowed| allowed.contains(&algorithm))
1151        {
1152            return Err(PolicyViolation::Algorithm {
1153                operation: "verification",
1154                algorithm: algorithm.uri().to_owned(),
1155            });
1156        }
1157        if matches!(
1158            algorithm,
1159            SignatureAlgorithm::RsaSha1
1160                | SignatureAlgorithm::DsaSha1
1161                | SignatureAlgorithm::HmacSha1
1162                | SignatureAlgorithm::EcdsaSha1
1163        ) && !self
1164            .key_trust
1165            .allowed_legacy_signature_algorithms
1166            .contains(&algorithm)
1167        {
1168            return Err(PolicyViolation::Algorithm {
1169                operation: "verification",
1170                algorithm: algorithm.uri().to_string(),
1171            });
1172        }
1173        if self
1174            .signature_algorithms
1175            .as_ref()
1176            .is_some_and(|allowed| !allowed.contains(&algorithm))
1177        {
1178            return Err(PolicyViolation::Algorithm {
1179                operation: "verification",
1180                algorithm: algorithm.uri().to_string(),
1181            });
1182        }
1183        Ok(())
1184    }
1185}
1186
1187/// Immutable policy snapshot for XMLDSig signing.
1188#[cfg(feature = "xmldsig")]
1189#[derive(Debug, Clone, Default)]
1190pub struct SigningPolicy {
1191    /// Allowed signing methods; `None` uses the implemented secure defaults.
1192    pub signature_algorithms: Option<HashSet<SignatureAlgorithm>>,
1193    /// Allowed reference digest methods; `None` uses the implemented secure defaults.
1194    pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
1195    /// HMAC secret and output-length requirements.
1196    pub hmac: HmacPolicy,
1197    /// ECDSA `SignatureValue` wire representation emitted by signing.
1198    pub ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding,
1199    /// RSA requirements enforced before producing a signature.
1200    pub rsa_keys: RsaKeyPolicy,
1201    /// DSA requirements enforced before producing a signature.
1202    pub dsa_keys: DsaKeyPolicy,
1203    /// Reference URI permissions. External URIs resolve only from caller-supplied
1204    /// request bytes and never enable filesystem or network access.
1205    pub uris: UriPolicy,
1206    /// Transform and canonicalization permissions.
1207    pub transforms: TransformPolicy,
1208    /// Whether direct `<Object>/<Manifest>` reference digests are populated.
1209    pub manifest_processing: ManifestProcessing,
1210    /// XML parser rules.
1211    pub xml: XmlInputPolicy,
1212    /// Resource ceilings.
1213    pub resources: ResourcePolicy,
1214}
1215
1216#[cfg(feature = "xmldsig")]
1217impl SigningPolicy {
1218    /// Validate the complete snapshot before signing work begins.
1219    pub fn validate(&self) -> Result<(), PolicyViolation> {
1220        self.resources.validate()?;
1221        self.rsa_keys.validate()?;
1222        self.dsa_keys.validate()?;
1223        self.hmac.validate()
1224    }
1225
1226    pub(crate) fn check_signature_algorithm(
1227        &self,
1228        algorithm: SignatureAlgorithm,
1229    ) -> Result<(), PolicyViolation> {
1230        check_signing_algorithm(
1231            self.signature_algorithms.as_ref(),
1232            algorithm,
1233            algorithm.signing_allowed(),
1234            algorithm.uri(),
1235        )
1236    }
1237
1238    pub(crate) fn check_digest_algorithm(
1239        &self,
1240        algorithm: DigestAlgorithm,
1241    ) -> Result<(), PolicyViolation> {
1242        check_signing_algorithm(
1243            self.digest_algorithms.as_ref(),
1244            algorithm,
1245            algorithm.signing_allowed(),
1246            algorithm.uri(),
1247        )
1248    }
1249}
1250
1251#[cfg(feature = "xmldsig")]
1252fn check_signing_algorithm<T: Eq + std::hash::Hash>(
1253    allowlist: Option<&HashSet<T>>,
1254    algorithm: T,
1255    default_allowed: bool,
1256    uri: &str,
1257) -> Result<(), PolicyViolation> {
1258    if allowlist.map_or(default_allowed, |allowed| allowed.contains(&algorithm)) {
1259        Ok(())
1260    } else {
1261        Err(PolicyViolation::Algorithm {
1262            operation: "signing",
1263            algorithm: uri.to_owned(),
1264        })
1265    }
1266}
1267
1268/// Immutable policy snapshot for XMLEnc encryption.
1269#[cfg(feature = "xmlenc")]
1270#[derive(Debug, Clone, Default)]
1271pub struct EncryptionPolicy {
1272    /// Allowed content-encryption algorithms.
1273    pub data_algorithms: Option<HashSet<DataEncryptionAlgorithm>>,
1274    /// Allowed RSA key-transport algorithms.
1275    pub key_transport_algorithms: Option<HashSet<KeyTransportAlgorithm>>,
1276    /// Allowed symmetric key-wrap algorithms.
1277    pub key_wrap_algorithms: Option<HashSet<KeyWrapAlgorithm>>,
1278    /// Allowed OAEP digest algorithms.
1279    pub oaep_digests: Option<HashSet<OaepDigestAlgorithm>>,
1280    /// RSA requirements enforced when producing OAEP key transport.
1281    pub rsa_keys: RsaKeyPolicy,
1282    /// XML parser rules.
1283    pub xml: XmlInputPolicy,
1284    /// Resource ceilings.
1285    pub resources: ResourcePolicy,
1286}
1287
1288#[cfg(feature = "xmlenc")]
1289impl EncryptionPolicy {
1290    /// Validate the complete snapshot before outbound encryption work begins.
1291    pub fn validate(&self) -> Result<(), PolicyViolation> {
1292        self.resources.validate()?;
1293        self.rsa_keys.validate()
1294    }
1295}
1296
1297/// Immutable policy snapshot for XMLEnc decryption.
1298#[cfg(feature = "xmlenc")]
1299#[derive(Debug, Clone, Default)]
1300pub struct DecryptionPolicy {
1301    /// Allowed content-decryption algorithms.
1302    pub data_algorithms: Option<HashSet<DataEncryptionAlgorithm>>,
1303    /// Allowed RSA key-transport algorithms accepted on input.
1304    pub key_transport_algorithms: Option<HashSet<KeyTransportAlgorithm>>,
1305    /// Allowed symmetric key-wrap algorithms accepted on input.
1306    pub key_wrap_algorithms: Option<HashSet<KeyWrapAlgorithm>>,
1307    /// Allowed OAEP digest algorithms accepted on input.
1308    pub oaep_digests: Option<HashSet<OaepDigestAlgorithm>>,
1309    /// RSA requirements enforced before OAEP recovery; defaults to 2048 bits.
1310    /// Legacy input requires an explicit caller-selected lower minimum.
1311    pub rsa_keys: RsaKeyPolicy,
1312    /// XML parser rules.
1313    pub xml: XmlInputPolicy,
1314    /// Resource ceilings.
1315    pub resources: ResourcePolicy,
1316}
1317
1318#[cfg(feature = "xmlenc")]
1319impl DecryptionPolicy {
1320    /// Validate the complete snapshot before inbound decryption work begins.
1321    pub fn validate(&self) -> Result<(), PolicyViolation> {
1322        self.resources.validate()?;
1323        self.rsa_keys.validate()
1324    }
1325}
1326
1327#[cfg(test)]
1328mod tests {
1329    use super::*;
1330
1331    #[cfg(feature = "xmldsig")]
1332    #[test]
1333    fn certificate_permissions_keep_exact_pss_and_compatibility_distinct() {
1334        use crate::provider::X509SignatureAlgorithm;
1335        use crate::xmldsig::{DigestAlgorithm, PqAlgorithm};
1336        // Compatibility permission cannot impose an arbitrary PSS salt list;
1337        // exact allowlists must still distinguish every parameter tuple.
1338        let exact = X509SignatureAlgorithm::RsaPss {
1339            digest: DigestAlgorithm::Sha256,
1340            mgf_digest: DigestAlgorithm::Sha256,
1341            salt_len: 32,
1342        };
1343        let allowed = CertificateSignatureAlgorithms::Allowlist(HashSet::from([exact]));
1344        assert!(
1345            KeyTrustPolicy::check_certificate_signature_algorithm(exact, Some(&allowed)).is_ok()
1346        );
1347        for salt_len in [0, 17, 64, 255] {
1348            let method = X509SignatureAlgorithm::RsaPss {
1349                digest: DigestAlgorithm::Sha256,
1350                mgf_digest: DigestAlgorithm::Sha256,
1351                salt_len,
1352            };
1353            assert!(
1354                KeyTrustPolicy::check_certificate_signature_algorithm(method, Some(&allowed))
1355                    .is_err()
1356            );
1357            assert!(
1358                KeyTrustPolicy::check_certificate_signature_algorithm(
1359                    method,
1360                    Some(&CertificateSignatureAlgorithms::AllSupported)
1361                )
1362                .is_ok()
1363            );
1364        }
1365        let pq = X509SignatureAlgorithm::PostQuantum(PqAlgorithm::MlDsa44);
1366        assert!(KeyTrustPolicy::check_certificate_signature_algorithm(pq, None).is_err());
1367        assert!(
1368            KeyTrustPolicy::check_certificate_signature_algorithm(
1369                pq,
1370                Some(&CertificateSignatureAlgorithms::AllSupported)
1371            )
1372            .is_ok()
1373        );
1374    }
1375
1376    #[test]
1377    fn resource_policy_cannot_exceed_implementation_ceiling() {
1378        let policy = ResourcePolicy {
1379            max_xml_nodes: 100_001,
1380            ..ResourcePolicy::default()
1381        };
1382        assert!(matches!(
1383            policy.validate(),
1384            Err(PolicyViolation::ResourceLimit {
1385                resource: resource_name::XML_NODES,
1386                maximum: 100_000,
1387                actual: 100_001,
1388            })
1389        ));
1390    }
1391
1392    #[test]
1393    fn xml_parse_work_policy_cannot_exceed_implementation_ceiling() {
1394        let policy = ResourcePolicy {
1395            max_xml_parse_work_bytes: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING
1396                .saturating_add(1),
1397            ..ResourcePolicy::default()
1398        };
1399
1400        assert_eq!(
1401            policy.validate(),
1402            Err(PolicyViolation::ResourceLimit {
1403                resource: resource_name::XML_PARSE_WORK_BYTES,
1404                maximum: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING,
1405                actual: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING.saturating_add(1),
1406            })
1407        );
1408    }
1409
1410    #[test]
1411    fn every_resource_policy_field_obeys_its_hard_ceiling() {
1412        // Each public tuning knob is only a stricter operational limit; none
1413        // may raise the implementation's allocation ceiling. Exact diagnostics
1414        // also catch a field accidentally paired with another field's ceiling.
1415        type Case = (&'static str, usize, fn(&mut ResourcePolicy) -> &mut usize);
1416        let cases: &[Case] = &[
1417            (
1418                resource_name::XML_NODES,
1419                crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize,
1420                |p| &mut p.max_xml_nodes,
1421            ),
1422            (
1423                resource_name::XML_DEPTH,
1424                crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
1425                |p| &mut p.max_xml_depth,
1426            ),
1427            (
1428                resource_name::XML_NAMESPACE_BINDINGS,
1429                crate::hard_limits::XML_NAMESPACE_BINDING_CEILING,
1430                |p| &mut p.max_xml_namespace_bindings,
1431            ),
1432            (
1433                resource_name::SIGNATURE_REFERENCES,
1434                crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
1435                |p| &mut p.max_references,
1436            ),
1437            (
1438                resource_name::VERIFICATION_SIGNATURES,
1439                crate::hard_limits::VERIFICATION_SIGNATURE_CEILING,
1440                |p| &mut p.max_signatures,
1441            ),
1442            (
1443                resource_name::REFERENCE_TRANSFORMS,
1444                crate::hard_limits::REFERENCE_TRANSFORM_CEILING,
1445                |p| &mut p.max_transforms_per_reference,
1446            ),
1447            (
1448                resource_name::XML_BASE_COMPONENTS,
1449                crate::hard_limits::XML_BASE_COMPONENT_CEILING,
1450                |p| &mut p.max_xml_base_components,
1451            ),
1452            (
1453                resource_name::XML_BASE_RESOLUTION_BYTES,
1454                crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING,
1455                |p| &mut p.max_xml_base_resolution_bytes,
1456            ),
1457            (
1458                resource_name::CANONICALIZED_BYTES,
1459                crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
1460                |p| &mut p.max_canonicalized_bytes,
1461            ),
1462            (
1463                resource_name::EXTERNAL_RESOURCE_BYTES,
1464                crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
1465                |p| &mut p.max_external_resource_bytes,
1466            ),
1467            (
1468                resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1469                crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
1470                |p| &mut p.max_external_resource_total_bytes,
1471            ),
1472            (
1473                resource_name::ENCRYPTION_PLAINTEXT_BYTES,
1474                crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
1475                |p| &mut p.max_encryption_plaintext_bytes,
1476            ),
1477            (
1478                resource_name::XML_DOCUMENT,
1479                crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
1480                |p| &mut p.max_xml_document_bytes,
1481            ),
1482            (
1483                resource_name::XML_PARSE_WORK_BYTES,
1484                crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING,
1485                |p| &mut p.max_xml_parse_work_bytes,
1486            ),
1487            (
1488                resource_name::ENCRYPTION_RECIPIENTS,
1489                crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING,
1490                |p| &mut p.max_encryption_recipients,
1491            ),
1492            (
1493                resource_name::ENCRYPTION_METADATA_BYTES,
1494                crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING,
1495                |p| &mut p.max_encryption_metadata_bytes,
1496            ),
1497            (
1498                resource_name::KEY_CANDIDATES,
1499                crate::hard_limits::KEY_CANDIDATE_CEILING,
1500                |p| &mut p.max_key_candidates,
1501            ),
1502            (
1503                resource_name::KEY_IMPORT_KDF_WORK,
1504                crate::hard_limits::KEY_IMPORT_KDF_WORK_CEILING as usize,
1505                |p| &mut p.max_key_import_kdf_work,
1506            ),
1507            (
1508                resource_name::KEY_IMPORT_KDF_MEMORY,
1509                crate::hard_limits::KEY_IMPORT_KDF_MEMORY_CEILING,
1510                |p| &mut p.max_key_import_kdf_memory_bytes,
1511            ),
1512            (
1513                resource_name::KEY_INFO_REFERENCE_DEPTH,
1514                crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING,
1515                |p| &mut p.max_key_info_reference_depth,
1516            ),
1517            (
1518                resource_name::BASE64_TRANSFORM_INPUT_BYTES,
1519                crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING,
1520                |p| &mut p.max_base64_transform_input_bytes,
1521            ),
1522            (
1523                resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
1524                crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING,
1525                |p| &mut p.max_base64_transform_output_bytes,
1526            ),
1527            (
1528                resource_name::XPATH_EXPRESSIONS,
1529                crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING,
1530                |p| &mut p.max_xpath_expressions,
1531            ),
1532            (
1533                resource_name::XPATH_EXPRESSION_BYTES,
1534                crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING,
1535                |p| &mut p.max_xpath_expression_bytes,
1536            ),
1537            (
1538                resource_name::XPATH_EXPRESSION_COMPLEXITY,
1539                crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
1540                |p| &mut p.max_xpath_expression_complexity,
1541            ),
1542            (
1543                resource_name::XPATH_CONTEXT_EVALUATIONS,
1544                crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING,
1545                |p| &mut p.max_xpath_context_evaluations,
1546            ),
1547            (
1548                resource_name::XPATH_EVALUATION_WORK,
1549                crate::hard_limits::XPATH_EVALUATION_WORK_CEILING,
1550                |p| &mut p.max_xpath_evaluation_work,
1551            ),
1552            (
1553                resource_name::XPATH_MIRROR_STRING_BYTES,
1554                crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING,
1555                |p| &mut p.max_xpath_mirror_string_bytes,
1556            ),
1557            (
1558                resource_name::XPATH_STRING_WORK_BYTES,
1559                crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING,
1560                |p| &mut p.max_xpath_string_work_bytes,
1561            ),
1562            (
1563                resource_name::XPATH_NAMESPACE_BINDINGS,
1564                crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING,
1565                |p| &mut p.max_xpath_namespace_bindings,
1566            ),
1567            (
1568                resource_name::XPATH_NAMESPACE_BYTES,
1569                crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING,
1570                |p| &mut p.max_xpath_namespace_bytes,
1571            ),
1572            (
1573                resource_name::XPATH_FILTERS,
1574                crate::hard_limits::XPATH_FILTER_COUNT_CEILING,
1575                |p| &mut p.max_xpath_filters,
1576            ),
1577            (
1578                resource_name::NODE_SET_FILTER_WORK,
1579                crate::hard_limits::NODE_SET_FILTER_WORK_CEILING,
1580                |p| &mut p.max_node_set_filter_work,
1581            ),
1582            (
1583                resource_name::NODE_SET_ENTRIES,
1584                crate::hard_limits::NODE_SET_ENTRY_CEILING,
1585                |p| &mut p.max_node_set_entries,
1586            ),
1587            (
1588                resource_name::NODE_SET_OWNED_STRING_BYTES,
1589                crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING,
1590                |p| &mut p.max_node_set_owned_string_bytes,
1591            ),
1592            (
1593                resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
1594                crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING,
1595                |p| &mut p.max_node_set_cumulative_owned_string_bytes,
1596            ),
1597        ];
1598
1599        for &(resource, ceiling, field) in cases {
1600            let mut policy = ResourcePolicy::default();
1601            let actual = ceiling.saturating_add(1);
1602            *field(&mut policy) = actual;
1603            assert_eq!(
1604                policy.validate(),
1605                Err(PolicyViolation::ResourceLimit {
1606                    resource,
1607                    maximum: ceiling,
1608                    actual,
1609                }),
1610                "wrong hard-ceiling validation for {resource}",
1611            );
1612        }
1613    }
1614
1615    #[test]
1616    fn resource_policy_accepts_zero_as_a_deny_all_ceiling() {
1617        // Zero is a valid policy decision for resources that an operation can
1618        // avoid consuming; runtime checks must reject only actual non-zero use.
1619        let policy = ResourcePolicy {
1620            max_xml_nodes: 0,
1621            max_xml_depth: 0,
1622            max_xml_namespace_bindings: 0,
1623            max_references: 0,
1624            max_signatures: 0,
1625            max_transforms_per_reference: 0,
1626            max_xml_base_components: 0,
1627            max_xml_base_resolution_bytes: 0,
1628            max_canonicalized_bytes: 0,
1629            max_external_resource_bytes: 0,
1630            max_external_resource_total_bytes: 0,
1631            max_encryption_plaintext_bytes: 0,
1632            max_xml_document_bytes: 0,
1633            max_xml_parse_work_bytes: 0,
1634            max_encryption_recipients: 0,
1635            max_encryption_metadata_bytes: 0,
1636            max_key_candidates: 0,
1637            max_key_import_kdf_work: 0,
1638            max_key_import_kdf_memory_bytes: 0,
1639            max_key_info_reference_depth: 0,
1640            max_base64_transform_input_bytes: 0,
1641            max_base64_transform_output_bytes: 0,
1642            max_xpath_expressions: 0,
1643            max_xpath_expression_bytes: 0,
1644            max_xpath_expression_complexity: 0,
1645            max_xpath_context_evaluations: 0,
1646            max_xpath_evaluation_work: 0,
1647            max_xpath_mirror_string_bytes: 0,
1648            max_xpath_string_work_bytes: 0,
1649            max_xpath_namespace_bindings: 0,
1650            max_xpath_namespace_bytes: 0,
1651            max_xpath_filters: 0,
1652            max_node_set_filter_work: 0,
1653            max_node_set_entries: 0,
1654            max_node_set_owned_string_bytes: 0,
1655            max_node_set_cumulative_owned_string_bytes: 0,
1656        };
1657
1658        assert_eq!(policy.validate(), Ok(()));
1659    }
1660
1661    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
1662    #[test]
1663    fn node_set_policy_has_headroom_above_its_default() {
1664        let mut policy = ResourcePolicy::default();
1665        assert_eq!(policy.max_node_set_entries, 262_144);
1666        assert_eq!(policy.validate(), Ok(()));
1667
1668        policy.max_node_set_entries = 524_288;
1669        assert_eq!(policy.validate(), Ok(()));
1670
1671        policy.max_node_set_entries = 524_289;
1672        assert_eq!(
1673            policy.validate(),
1674            Err(PolicyViolation::ResourceLimit {
1675                resource: resource_name::NODE_SET_ENTRIES,
1676                maximum: 524_288,
1677                actual: 524_289,
1678            })
1679        );
1680    }
1681
1682    #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
1683    #[test]
1684    fn rsa_key_policy_enforces_structure_range_and_explicit_relaxation() {
1685        let secure = RsaKeyPolicy::default();
1686        assert!(matches!(
1687            secure.validate_components("test", &[0x80; 128], &[1, 0, 1]),
1688            Err(PolicyViolation::KeySize {
1689                minimum_bits: 2048,
1690                maximum_bits: 8192,
1691                actual_bits: 1024,
1692                ..
1693            })
1694        ));
1695        let mut short_2048_width = [0_u8; 256];
1696        short_2048_width[0] = 1;
1697        assert!(matches!(
1698            secure.validate_components("test", &short_2048_width, &[1, 0, 1]),
1699            Err(PolicyViolation::KeySize {
1700                minimum_bits: 2048,
1701                actual_bits: 2041,
1702                ..
1703            })
1704        ));
1705        assert!(matches!(
1706            secure.validate_components("test", &[1; 1025], &[1, 0, 1]),
1707            Err(PolicyViolation::KeySize {
1708                actual_bits: 8193,
1709                ..
1710            })
1711        ));
1712        assert!(matches!(
1713            secure.validate_components("test", &[0x80; 256], &[2]),
1714            Err(PolicyViolation::InvalidKeyMaterial { .. })
1715        ));
1716        assert_eq!(
1717            secure.validate_components("test", &[0x80; 256], &[0x80, 0, 0, 1]),
1718            Ok(256),
1719            "normalized RSA components encode the exponent as unsigned bytes"
1720        );
1721
1722        let compatibility = RsaKeyPolicy {
1723            minimum_modulus_bits: 1024,
1724        };
1725        assert_eq!(
1726            compatibility.validate_components("test", &[0x80; 128], &[1, 0, 1]),
1727            Ok(128)
1728        );
1729        assert!(
1730            RsaKeyPolicy {
1731                minimum_modulus_bits: 2047,
1732            }
1733            .validate()
1734            .is_err()
1735        );
1736    }
1737
1738    #[cfg(feature = "xmldsig")]
1739    #[test]
1740    fn mandatory_x509_limits_report_the_nonzero_requirement() {
1741        // A lower-bound violation must not be reported as exceeding the upper
1742        // ceiling: that diagnostic points callers toward the wrong correction.
1743        let policy = KeyTrustPolicy {
1744            max_x509_chain_depth: 0,
1745            ..KeyTrustPolicy::default()
1746        };
1747
1748        let error = policy.validate().expect_err("zero depth must be rejected");
1749        assert!(
1750            error.to_string().contains("must be nonzero"),
1751            "unexpected lower-bound diagnostic: {error}"
1752        );
1753    }
1754
1755    #[cfg(feature = "xmldsig")]
1756    #[test]
1757    fn custom_extended_key_purposes_require_valid_oid_arcs() {
1758        // The typed policy rejects impossible OIDs when the immutable snapshot
1759        // is validated instead of silently making the purpose unmatchable.
1760        let mut policy = KeyTrustPolicy::default();
1761        policy
1762            .allowed_extended_key_usages
1763            .insert(ExtendedKeyPurpose::Other(vec![1, 40, 7]));
1764
1765        assert!(matches!(
1766            policy.validate(),
1767            Err(PolicyViolation::KeyTrust {
1768                reason: "custom extended key purposes must contain valid OID arcs",
1769            })
1770        ));
1771    }
1772
1773    #[cfg(feature = "xmldsig")]
1774    #[test]
1775    fn crl_checking_requires_x509_chain_validation() {
1776        // CRLs authenticate through the validated issuer path. Accepting this
1777        // combination would advertise a security control the resolver skips.
1778        let policy = KeyTrustPolicy {
1779            check_crls: true,
1780            ..KeyTrustPolicy::default()
1781        };
1782
1783        assert!(matches!(
1784            policy.validate(),
1785            Err(PolicyViolation::KeyTrust {
1786                reason: "CRL checking requires X.509 chain validation"
1787            })
1788        ));
1789    }
1790
1791    #[cfg(feature = "xmldsig")]
1792    #[test]
1793    fn legacy_signature_algorithms_require_independent_policy_opt_ins() {
1794        let legacy = [
1795            SignatureAlgorithm::RsaSha1,
1796            SignatureAlgorithm::DsaSha1,
1797            SignatureAlgorithm::HmacSha1,
1798            SignatureAlgorithm::EcdsaSha1,
1799        ];
1800        let mut policy = VerificationPolicy::default();
1801
1802        for algorithm in legacy {
1803            assert!(matches!(
1804                policy.check_signature_algorithm(algorithm),
1805                Err(PolicyViolation::Algorithm { .. })
1806            ));
1807            policy
1808                .key_trust
1809                .allowed_legacy_signature_algorithms
1810                .insert(algorithm);
1811            assert_eq!(policy.check_signature_algorithm(algorithm), Ok(()));
1812            policy
1813                .key_trust
1814                .allowed_legacy_signature_algorithms
1815                .remove(&algorithm);
1816        }
1817    }
1818
1819    #[cfg(feature = "xmldsig")]
1820    #[test]
1821    fn dsa_key_policy_enforces_configured_minimum_and_hard_ceiling() {
1822        let policy = DsaKeyPolicy::default();
1823
1824        assert!(matches!(
1825            policy.validate_modulus_bits(1024),
1826            Err(PolicyViolation::KeySize {
1827                key_type: "DSA",
1828                minimum_bits: 2048,
1829                maximum_bits: 3072,
1830                actual_bits: 1024,
1831                ..
1832            })
1833        ));
1834        assert_eq!(policy.validate_modulus_bits(2048), Ok(()));
1835        assert!(matches!(
1836            policy.validate_modulus_bits(4096),
1837            Err(PolicyViolation::KeySize {
1838                key_type: "DSA",
1839                minimum_bits: 2048,
1840                maximum_bits: 3072,
1841                actual_bits: 4096,
1842                ..
1843            })
1844        ));
1845    }
1846
1847    #[cfg(feature = "xmldsig")]
1848    #[test]
1849    fn hmac_output_policy_cannot_weaken_the_xmldsig_floor() {
1850        // Caller policy may tighten but cannot weaken XMLDSig section 4.4.2:
1851        // truncation is at least 80 bits and at least half the digest width.
1852        let compatibility = HmacPolicy {
1853            minimum_key_bits: 40,
1854            minimum_output_bits: 40,
1855        };
1856
1857        assert_eq!(
1858            compatibility.validate_output(SignatureAlgorithm::HmacSha1, 80),
1859            Ok(())
1860        );
1861        assert!(matches!(
1862            compatibility.validate_output(SignatureAlgorithm::HmacSha1, 72),
1863            Err(PolicyViolation::HmacOutputLength { minimum: 80, .. })
1864        ));
1865        assert!(matches!(
1866            compatibility.validate_output(SignatureAlgorithm::HmacSha256, 120),
1867            Err(PolicyViolation::HmacOutputLength { minimum: 128, .. })
1868        ));
1869    }
1870
1871    #[cfg(feature = "xmldsig")]
1872    #[test]
1873    fn documented_xmldsig_limits_match_hard_limits() {
1874        // Public deployment guidance must change in the same commit as the
1875        // implementation ceilings from which these values are derived.
1876        let docs = include_str!("../docs/xmldsig.md");
1877        let mib = 1024 * 1024;
1878        assert!(docs.contains(&format!(
1879            "Individual resources are limited to {} MiB",
1880            crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING / mib
1881        )));
1882        assert!(docs.contains(&format!(
1883            "complete map to {} MiB",
1884            crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING / mib
1885        )));
1886        assert!(docs.contains(&format!(
1887            "ceilings are {} components and {} MiB per operation",
1888            crate::hard_limits::XML_BASE_COMPONENT_CEILING,
1889            crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING / mib
1890        )));
1891    }
1892}