//! Immutable security policy snapshots shared by XML Security operations. //! //! Policy contains trusted, reusable decisions. Caller-owned keys, selected //! document targets, tenant identity, and external resource bytes remain in //! operation request contexts and are deliberately not stored here. use alloc::{ borrow::ToOwned, collections::BTreeSet, format, string::{String, ToString}, vec::Vec, }; #[cfg(feature = "xmldsig")] use crate::xmldsig::{DigestAlgorithm, SignatureAlgorithm, UriTypeSet, XPathHereSemantics}; #[cfg(feature = "xmlenc")] use crate::xmlenc::{ DataEncryptionAlgorithm, KeyTransportAlgorithm, KeyWrapAlgorithm, OaepDigestAlgorithm, }; #[cfg(feature = "xmldsig")] pub(crate) mod clock; #[cfg(feature = "xmldsig")] pub use clock::Instant; #[cfg(all(feature = "xmldsig", any(feature = "std", test)))] pub use clock::instant_from_system_time; #[cfg(feature = "xmldsig")] mod key_establishment; #[cfg(feature = "xmldsig")] pub use key_establishment::{ KemContentAuthentication, KeyAgreementAlgorithm, KeyDerivationAlgorithm, KeyEstablishmentPolicy, }; #[cfg(feature = "xmldsig")] mod key_sources; #[cfg(feature = "xmldsig")] pub use key_sources::{KeySourceKind, KeySourcePolicy}; mod identity; pub use identity::PolicyIdentity; #[cfg(feature = "xmldsig")] mod audit; #[cfg(feature = "xmldsig")] pub use audit::{ AlgorithmRole, Audited, PolicyAudit, PolicyDecision, UNRECOGNIZED_ALGORITHM, UriRole, }; #[cfg(feature = "xmldsig")] mod profiles; #[cfg(feature = "xmldsig")] pub use profiles::{ CompiledPolicies, PolicyCompileContext, PolicyCompiler, PolicyProfile, PolicySet, PolicyValidity, SignatureConstraints, }; /// Permission for the unbounded parameterized RSA-PSS family. /// Exact permissions continue to use the signature algorithm allowlist. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum RsaPssPermission { /// No additional family grant; existing allowlists and secure defaults apply. #[default] ExactOnly, /// Permit supported parameterized tuples using non-legacy hashes only. SecureSupported, /// Permit every supported tuple; capability and key restrictions still apply. AllSupported, } #[cfg(feature = "xmldsig")] impl RsaPssPermission { fn permits(self, algorithm: SignatureAlgorithm) -> bool { matches!(algorithm, SignatureAlgorithm::RsaPss(_)) && (self == Self::AllSupported || (self == Self::SecureSupported && algorithm.signing_allowed())) } } /// Canonical diagnostics for limits represented by [`ResourcePolicy`]. /// /// Validation and every enforcement point use the same names so callers can /// match typed policy violations without operation-specific string drift. pub(crate) mod resource_name { pub const XML_NODES: &str = "XML nodes"; pub const XML_DEPTH: &str = "XML element depth"; pub const XML_NAMESPACE_BINDINGS: &str = "XML namespace bindings"; pub const SIGNATURE_REFERENCES: &str = "signature references"; #[cfg(feature = "xmlenc")] pub const ENCRYPTION_REFERENCES: &str = "encryption key references"; pub const VERIFICATION_SIGNATURES: &str = "verification signatures"; pub const REFERENCE_TRANSFORMS: &str = "reference transforms"; pub const XML_BASE_COMPONENTS: &str = "XML Base components"; pub const XML_BASE_RESOLUTION_BYTES: &str = "XML Base resolution bytes"; pub const CANONICALIZED_BYTES: &str = "canonicalized bytes"; pub const EXTERNAL_RESOURCE_BYTES: &str = "external resource bytes"; pub const AGGREGATE_EXTERNAL_RESOURCE_BYTES: &str = "aggregate external resource bytes"; pub const DETACHED_PAYLOAD_BYTES: &str = "streamed detached payload bytes"; pub const AGGREGATE_DETACHED_PAYLOAD_BYTES: &str = "aggregate streamed detached payload bytes"; pub const ENCRYPTION_PLAINTEXT_BYTES: &str = "encryption plaintext bytes"; #[cfg(feature = "xmlenc")] pub const AGGREGATE_ENCRYPTION_CIPHER_VALUE_BYTES: &str = "aggregate encryption CipherValue bytes"; pub const XML_DOCUMENT: &str = "XML document"; pub const XML_PARSE_WORK_BYTES: &str = "cumulative XML parse-work bytes"; pub const ENCRYPTION_RECIPIENTS: &str = "encryption recipients"; pub const ENCRYPTION_METADATA_BYTES: &str = "encryption metadata bytes"; pub const KEY_CANDIDATES: &str = "key candidates"; pub const KEY_IMPORT_KDF_WORK: &str = "key import KDF work"; pub const KEY_IMPORT_KDF_MEMORY: &str = "key import KDF memory bytes"; pub const X509_WORK: &str = "X.509 work"; pub const X509_WORKSPACE_BYTES: &str = "X.509 workspace bytes"; #[cfg(feature = "xmldsig")] pub const KEY_ESTABLISHMENT_HASH_BLOCKS: &str = "key establishment hash blocks"; #[cfg(feature = "xmldsig")] pub const KEY_ESTABLISHMENT_OWNED_BYTES: &str = "key establishment owned bytes"; #[cfg(feature = "xmldsig")] pub const DH_MODULUS_BITS: &str = "DH modulus bits"; #[cfg(feature = "xmldsig")] pub const DH_SUBGROUP_BITS: &str = "DH subgroup bits"; #[cfg(feature = "xmldsig")] pub const KEY_ESTABLISHMENT_MODULAR_WORK: &str = "key establishment modular work"; pub const KEY_INFO_REFERENCE_DEPTH: &str = "KeyInfoReference depth"; pub const BASE64_TRANSFORM_INPUT_BYTES: &str = "Base64 transform input bytes"; pub const BASE64_TRANSFORM_OUTPUT_BYTES: &str = "Base64 transform output bytes"; pub const XPATH_EXPRESSIONS: &str = "XPath expressions"; pub const XPATH_EXPRESSION_BYTES: &str = "XPath expression bytes"; pub const XPATH_EXPRESSION_COMPLEXITY: &str = "XPath expression complexity"; pub const XPATH_CONTEXT_EVALUATIONS: &str = "XPath context evaluations"; pub const XPATH_EVALUATION_WORK: &str = "XPath evaluation work"; pub const XPATH_MIRROR_STRING_BYTES: &str = "XPath mirror string bytes"; pub const XPATH_STRING_WORK_BYTES: &str = "XPath string-processing work bytes"; pub const XPATH_NAMESPACE_BINDINGS: &str = "XPath namespace bindings"; pub const XPATH_NAMESPACE_BYTES: &str = "XPath namespace bytes"; pub const XPATH_FILTERS: &str = "XPath filters"; pub const NODE_SET_FILTER_WORK: &str = "node-set filter work"; pub const OPC_WORKSPACE_BYTES: &str = "OPC workspace bytes"; pub const OPC_PARAMETER_BYTES: &str = "OPC parameter bytes"; pub const XSLT_WORK: &str = "XSLT work"; pub const XSLT_WORKSPACE_BYTES: &str = "XSLT workspace bytes"; pub const XSLT_OUTPUT_BYTES: &str = "XSLT output bytes"; pub const XSLT_RECURSION_DEPTH: &str = "XSLT recursion depth"; pub const NODE_SET_ENTRIES: &str = "node-set entries"; pub const NODE_SET_OWNED_STRING_BYTES: &str = "node-set owned string bytes"; pub const NODE_SET_CUMULATIVE_OWNED_STRING_BYTES: &str = "cumulative node-set owned string bytes"; } /// A typed rejection produced by an operation policy. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[non_exhaustive] pub enum PolicyViolation { /// Contradictory policy layers or an unavailable compiled requirement. #[error("policy compilation failed: {reason}")] Configuration { /// Stable, non-secret diagnostic. reason: &'static str, }, /// The immutable operation policy forbids executing Manifest references. #[cfg(feature = "xmldsig")] #[error("Manifest reference processing is disabled by policy")] ManifestProcessingDisabled, /// The policy forbids completing a detached Reference with a digest the /// caller attests instead of bytes this operation digests. #[cfg(feature = "xmldsig")] #[error("verification policy forbids caller-attested detached digests")] AttestedDigestForbidden, /// Untrusted XML does not satisfy the selected signature profile. #[cfg(feature = "xmldsig")] #[error("{profile:?} signature profile rejects document: {reason}")] SignatureProfile { /// Compiled protocol constraint being enforced. profile: SignatureConstraints, /// Stable, non-secret explanation of the document defect. reason: &'static str, }, /// An explicitly authorized exception is not valid at operation time. #[error("policy authorization is outside its validity interval")] AuthorizationExpired, /// An algorithm is outside the operation allowlist. #[error("{operation} policy rejects algorithm {algorithm}")] Algorithm { /// Operation evaluating the algorithm. operation: &'static str, /// Stable algorithm URI or diagnostic name. algorithm: String, }, /// An HMAC output length is outside the operation's configured bounds. #[cfg(feature = "xmldsig")] #[error("HMAC output length must be between {minimum} and {maximum} bits: got {actual}")] HmacOutputLength { /// Minimum output length selected by policy. minimum: usize, /// Full output width of the selected HMAC algorithm. maximum: usize, /// Parsed output length. actual: usize, }, /// An input exceeds a configured resource ceiling. #[error("{resource} exceeds policy maximum {maximum}: got {actual}")] ResourceLimit { /// Resource whose consumption was rejected. resource: &'static str, /// Effective policy ceiling. maximum: usize, /// Observed consumption. actual: usize, }, /// An import exceeded a resource ceiling, but its parser did not expose the measured value. #[error("{resource} exceeds policy maximum {maximum}")] ResourceLimitExceeded { /// Resource whose consumption was rejected. resource: &'static str, /// Effective policy ceiling. maximum: usize, }, /// A protected import supplied zero or too many KDF iterations; the parser did not expose the count. #[error("key import KDF iterations must be between 1 and {maximum}")] KdfIterationsOutsideLimit { /// Effective iteration ceiling. maximum: usize, }, /// A configured resource limit violates a structural policy requirement. #[error("{resource} has invalid policy limit {actual}: {requirement}")] InvalidResourceLimit { /// Resource whose configured limit was rejected. resource: &'static str, /// Required property of the configured limit. requirement: &'static str, /// Rejected configured value. actual: usize, }, /// The selected key source or trust mode is disallowed. #[error("key/trust policy rejected the operation: {reason}")] KeyTrust { /// Non-secret reason suitable for diagnostics. reason: &'static str, }, /// XML parser behavior is disallowed. #[error("XML input policy rejected the operation: {reason}")] XmlInput { /// Non-secret reason suitable for diagnostics. reason: &'static str, }, /// A URI class is outside the operation policy. #[error("{operation} URI policy rejected the operation: {reason}")] Uri { /// Operation evaluating the URI. operation: &'static str, /// Non-sensitive reason suitable for diagnostics. reason: &'static str, }, /// An asymmetric key/domain falls outside the operation's strength range. #[error( "{operation} policy requires {key_type} keys between {minimum_bits} and {maximum_bits} bits: got {actual_bits}" )] KeySize { /// Operation evaluating the key. operation: &'static str, /// Stable key-family diagnostic. key_type: &'static str, /// Configured minimum width of the named key/domain component. minimum_bits: usize, /// Effective maximum, bounded by the implementation ceiling. maximum_bits: usize, /// Observed normalized width of the named key/domain component. actual_bits: usize, }, /// RSA key material is structurally invalid. #[error("{operation} policy rejects invalid {key_type} key material: {reason}")] InvalidKeyMaterial { /// Operation evaluating the key. operation: &'static str, /// Stable key-family diagnostic. key_type: &'static str, /// Non-secret structural rejection reason. reason: &'static str, }, } /// HMAC key-strength and truncation requirements shared by signing and verification. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct HmacPolicy { /// Minimum caller-owned secret length in bits. pub minimum_key_bits: usize, /// Minimum emitted or accepted MAC length in bits. pub minimum_output_bits: usize, } #[cfg(feature = "xmldsig")] impl Default for HmacPolicy { fn default() -> Self { Self { minimum_key_bits: 128, minimum_output_bits: 128, } } } #[cfg(feature = "xmldsig")] impl HmacPolicy { pub(crate) fn validate(self) -> Result<(), PolicyViolation> { if self.minimum_key_bits == 0 || self.minimum_output_bits == 0 { return Err(PolicyViolation::KeyTrust { reason: "HMAC minimum key and output lengths must be nonzero", }); } Ok(()) } /// Check an HMAC secret of `key_bytes` bytes against the configured /// minimum key length. /// /// # Errors /// [`PolicyViolation::InvalidKeyMaterial`] for a shorter secret. pub fn validate_key_bytes(self, key_bytes: usize) -> Result<(), PolicyViolation> { self.validate_key_bits(key_bytes.saturating_mul(8)) } pub(crate) fn validate_key_bits(self, key_bits: usize) -> Result<(), PolicyViolation> { if key_bits < self.minimum_key_bits { return Err(PolicyViolation::InvalidKeyMaterial { operation: "HMAC", key_type: "symmetric", reason: "secret is shorter than the configured minimum", }); } Ok(()) } /// Check a selected HMAC output length of `algorithm` against this policy /// and the `XMLDSig` floor: at least the configured minimum, 80 bits and /// half the digest width, and at most the digest width. /// /// # Errors /// [`PolicyViolation::Algorithm`] for a method that is no HMAC, /// [`PolicyViolation::HmacOutputLength`] for a length out of range. pub fn validate_output( self, algorithm: SignatureAlgorithm, selected_bits: usize, ) -> Result<(), PolicyViolation> { let maximum = algorithm .hmac_output_bits() .ok_or_else(|| PolicyViolation::Algorithm { operation: "HMAC", algorithm: algorithm.uri().to_owned(), })?; // XMLDSig 1.1 section 4.4.2 makes this a protocol floor, not a // deployment preference: truncation is at least 80 bits and at least // half the underlying digest width. Caller policy may only tighten it. // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-SignatureMethod let minimum = self.minimum_output_bits.max(80).max(maximum / 2); if selected_bits < minimum || selected_bits > maximum { return Err(PolicyViolation::HmacOutputLength { minimum, maximum, actual: selected_bits, }); } Ok(()) } } /// RSA strength and structural requirements for cryptographic operations. #[cfg(any(feature = "xmldsig", feature = "xmlenc"))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RsaKeyPolicy { /// Minimum mathematical RSA modulus bit length accepted by the operation. pub minimum_modulus_bits: usize, } /// DSA strength requirements for legacy signature verification. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DsaKeyPolicy { /// Minimum prime-modulus width accepted for DSA verification. pub minimum_modulus_bits: usize, } #[cfg(feature = "xmldsig")] impl Default for DsaKeyPolicy { fn default() -> Self { Self { minimum_modulus_bits: 2048, } } } #[cfg(feature = "xmldsig")] impl DsaKeyPolicy { /// Validate the configured minimum against the implementation ceiling. /// /// # Errors /// Returns `InvalidResourceLimit` for a zero or non-64-bit-aligned minimum, /// and `ResourceLimit` for an above-ceiling minimum. pub fn validate(&self) -> Result<(), PolicyViolation> { validate_modulus_minimum( "minimum DSA modulus bits", self.minimum_modulus_bits, 64, "minimum must be a nonzero multiple of 64 bits", crate::hard_limits::DSA_MODULUS_BIT_CEILING, ) } pub(crate) fn validate_modulus_bits(self, actual_bits: usize) -> Result<(), PolicyViolation> { self.validate()?; if !(self.minimum_modulus_bits..=crate::hard_limits::DSA_MODULUS_BIT_CEILING) .contains(&actual_bits) { return Err(PolicyViolation::KeySize { operation: "verification", key_type: "DSA", minimum_bits: self.minimum_modulus_bits, maximum_bits: crate::hard_limits::DSA_MODULUS_BIT_CEILING, actual_bits, }); } Ok(()) } } #[cfg(any(feature = "xmldsig", feature = "xmlenc"))] impl Default for RsaKeyPolicy { fn default() -> Self { Self { minimum_modulus_bits: 2048, } } } #[cfg(any(feature = "xmldsig", feature = "xmlenc"))] impl RsaKeyPolicy { /// Validate the configured minimum against the implementation ceiling. /// /// # Errors /// Returns `InvalidResourceLimit` for a zero or non-byte-aligned minimum, /// and `ResourceLimit` for an above-ceiling minimum. pub fn validate(&self) -> Result<(), PolicyViolation> { validate_modulus_minimum( "minimum RSA modulus bits", self.minimum_modulus_bits, 8, "minimum must be a nonzero whole-byte width", crate::hard_limits::RSA_MODULUS_BIT_CEILING, ) } pub(crate) fn validate_components( self, operation: &'static str, modulus: &[u8], exponent: &[u8], ) -> Result { self.validate()?; let modulus = modulus .iter() .position(|byte| *byte != 0) .map(|start| &modulus[start..]) .ok_or(PolicyViolation::InvalidKeyMaterial { operation, key_type: "RSA", reason: "modulus is zero", })?; let modulus_bits = modulus .len() .checked_mul(8) .and_then(|width| width.checked_sub(modulus[0].leading_zeros() as usize)) .ok_or(PolicyViolation::InvalidKeyMaterial { operation, key_type: "RSA", reason: "modulus width overflows", })?; let exponent = if exponent.is_empty() || exponent.len() > 8 { None } else { let mut bytes = [0_u8; 8]; bytes[8 - exponent.len()..].copy_from_slice(exponent); Some(u64::from_be_bytes(bytes)) }; self.validate_public_metadata(operation, modulus_bits, exponent) } pub(crate) fn validate_public_metadata( self, operation: &'static str, modulus_bits: usize, exponent: Option, ) -> Result { self.validate()?; if modulus_bits == 0 { return Err(PolicyViolation::InvalidKeyMaterial { operation, key_type: "RSA", reason: "modulus is zero", }); } if !(self.minimum_modulus_bits..=crate::hard_limits::RSA_MODULUS_BIT_CEILING) .contains(&modulus_bits) { return Err(PolicyViolation::KeySize { operation, key_type: "RSA", minimum_bits: self.minimum_modulus_bits, maximum_bits: crate::hard_limits::RSA_MODULUS_BIT_CEILING, actual_bits: modulus_bits, }); } let exponent = exponent.ok_or(PolicyViolation::InvalidKeyMaterial { operation, key_type: "RSA", reason: "public exponent has invalid encoding", })?; if !(3..=((1_u64 << 33) - 1)).contains(&exponent) || exponent % 2 == 0 { return Err(PolicyViolation::InvalidKeyMaterial { operation, key_type: "RSA", reason: "public exponent is outside the supported odd range", }); } Ok(modulus_bits.div_ceil(8)) } } #[cfg(any(feature = "xmldsig", feature = "xmlenc"))] fn validate_modulus_minimum( resource: &'static str, minimum_bits: usize, alignment_bits: usize, requirement: &'static str, ceiling: usize, ) -> Result<(), PolicyViolation> { if minimum_bits == 0 || !minimum_bits.is_multiple_of(alignment_bits) { return Err(PolicyViolation::InvalidResourceLimit { resource, requirement, actual: minimum_bits, }); } ResourcePolicy::within(resource, minimum_bits, ceiling) } /// Resource ceilings shared by parsing, transforms, and cryptographic output. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResourcePolicy { /// Maximum XML nodes in one parsed document. pub max_xml_nodes: usize, /// Maximum element nesting depth in one parsed document. pub max_xml_depth: usize, /// Maximum namespace prefixes simultaneously bound while parsing XML. /// Node and depth limits alone do not bound declarations on one element. pub max_xml_namespace_bindings: usize, /// Maximum references in one signature or manifest. pub max_references: usize, /// Maximum signatures examined and retained in one verification request. pub max_signatures: usize, /// Maximum transforms in one reference. pub max_transforms_per_reference: usize, /// Maximum inherited `xml:base` components in one URI resolution. pub max_xml_base_components: usize, /// Maximum cumulative bytes processed while resolving `xml:base` URIs. pub max_xml_base_resolution_bytes: usize, /// Maximum canonical bytes retained across one signature operation. pub max_canonicalized_bytes: usize, /// Maximum decoded external resource bytes. pub max_external_resource_bytes: usize, /// Maximum bytes in the complete external map and cumulatively dereferenced. pub max_external_resource_total_bytes: usize, /// Maximum caller-streamed bytes of one detached payload. Streaming keeps /// no input resident, so this bounds hashing work, not memory. pub max_detached_payload_bytes: usize, /// Maximum caller-streamed detached payload bytes in one verified session. pub max_detached_payload_total_bytes: usize, /// Maximum `XMLEnc` plaintext bytes. pub max_encryption_plaintext_bytes: usize, /// Maximum caller-owned XML bytes accepted by any document operation. pub max_xml_document_bytes: usize, /// Maximum cumulative XML bytes parsed by one operation. pub max_xml_parse_work_bytes: usize, /// Maximum independently wrapped recipients. pub max_encryption_recipients: usize, /// Maximum caller-controlled `XMLEnc` metadata bytes per field. pub max_encryption_metadata_bytes: usize, /// Maximum key-source expansion work and concrete key or certificate /// candidates inspected by one operation stage. pub max_key_candidates: usize, /// Maximum cumulative work charged by the shared certificate mechanism /// in a processing session, across its candidates and nested decodes. /// A PKCS#12 import charges its shared container reading here too. pub max_x509_work: usize, /// Maximum live allocation capacity retained by the shared certificate /// mechanism, including path search, Name processing and CRL evaluation. pub max_x509_workspace_bytes: usize, /// Maximum aggregate PBKDF2/PKCS#12 hash rounds or conservative scrypt work /// during key import, including PKCS#8 password-hashing work units. pub max_key_import_kdf_work: usize, /// Maximum estimated scrypt or PKCS#12 KDF workspace bytes during key import. pub max_key_import_kdf_memory_bytes: usize, /// Maximum nested `KeyInfoReference` dereference depth. pub max_key_info_reference_depth: usize, /// Maximum bytes accepted by Base64 transforms before decoding. pub max_base64_transform_input_bytes: usize, /// Maximum cumulative bytes emitted by Base64 transforms in one operation. pub max_base64_transform_output_bytes: usize, /// Maximum `XPath` expressions evaluated by one signature operation. pub max_xpath_expressions: usize, /// Maximum UTF-8 bytes in one `XPath` expression. pub max_xpath_expression_bytes: usize, /// Maximum structural complexity accepted for one `XPath` expression. pub max_xpath_expression_complexity: usize, /// Maximum context-node evaluations for one ordinary `XPath` transform. pub max_xpath_context_evaluations: usize, /// Maximum conservative `XPath` node-evaluation work per operation. pub max_xpath_evaluation_work: usize, /// Maximum source strings copied into the `XPath` mirror. pub max_xpath_mirror_string_bytes: usize, /// Maximum conservative `XPath` string-processing work per operation. pub max_xpath_string_work_bytes: usize, /// Maximum namespace bindings captured by one `XPath` expression. pub max_xpath_namespace_bindings: usize, /// Maximum aggregate namespace prefix and URI bytes per `XPath` expression. pub max_xpath_namespace_bytes: usize, /// Maximum filters in one `XPath` Filter 2.0 transform. pub max_xpath_filters: usize, /// Maximum cumulative node-set entries visited by filtering transforms. pub max_node_set_filter_work: usize, /// Maximum tree, attribute and namespace nodes in one exact node set, /// whether it is stored as a document range or explicitly. Defaults to /// 262,144; callers may raise it to the absolute ceiling of 524,288. pub max_node_set_entries: usize, /// Maximum attribute and namespace name bytes covered by one node set. pub max_node_set_owned_string_bytes: usize, /// Maximum cumulative node-set name bytes per operation. pub max_node_set_cumulative_owned_string_bytes: usize, /// Maximum cumulative OPC parameter allocations retained during signature parsing. pub max_opc_parameter_bytes: usize, /// Maximum cumulative OPC normalization workspace allocations during execution. pub max_opc_workspace_bytes: usize, /// Maximum XSLT work of each kind per operation: stylesheet serialization, /// `XPath` evaluations and steps, /// extension calls, pattern tests, template applications, sort comparisons, /// key entries, result nodes and messages. pub max_xslt_work: usize, /// Maximum memory owned at once by one stylesheet compilation or execution. pub max_xslt_workspace_bytes: usize, /// Maximum cumulative bytes serialized by XSLT transforms in one operation. pub max_xslt_output_bytes: usize, /// Maximum XSLT compilation and template recursion depth. pub max_xslt_recursion_depth: usize, } impl Default for ResourcePolicy { fn default() -> Self { Self { max_xml_nodes: crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize, max_xml_depth: crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING, max_xml_namespace_bindings: crate::hard_limits::XML_NAMESPACE_BINDING_CEILING, max_references: crate::hard_limits::SIGNATURE_REFERENCE_CEILING, max_signatures: crate::hard_limits::VERIFICATION_SIGNATURE_CEILING, max_transforms_per_reference: crate::hard_limits::REFERENCE_TRANSFORM_CEILING, max_xml_base_components: crate::hard_limits::XML_BASE_COMPONENT_CEILING, max_xml_base_resolution_bytes: crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING, max_canonicalized_bytes: crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING, max_external_resource_bytes: crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING, max_external_resource_total_bytes: crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING, max_detached_payload_bytes: crate::hard_limits::DETACHED_PAYLOAD_BYTE_CEILING, max_detached_payload_total_bytes: crate::hard_limits::DETACHED_PAYLOAD_TOTAL_BYTE_CEILING, max_encryption_plaintext_bytes: crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING, max_xml_document_bytes: crate::hard_limits::XML_DOCUMENT_BYTE_CEILING, max_xml_parse_work_bytes: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING, max_encryption_recipients: crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING, max_encryption_metadata_bytes: crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING, max_key_candidates: crate::hard_limits::KEY_CANDIDATE_CEILING, max_x509_work: crate::hard_limits::X509_WORK_CEILING, max_x509_workspace_bytes: crate::hard_limits::X509_WORKSPACE_BYTE_CEILING, max_key_import_kdf_work: usize::try_from( crate::hard_limits::KEY_IMPORT_KDF_WORK_CEILING, ) .expect("key import work ceiling fits supported 32-bit and 64-bit targets"), max_key_import_kdf_memory_bytes: crate::hard_limits::KEY_IMPORT_KDF_MEMORY_CEILING, max_key_info_reference_depth: crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING, max_base64_transform_input_bytes: crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING, max_base64_transform_output_bytes: crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING, max_xpath_expressions: crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING, max_xpath_expression_bytes: crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING, max_xpath_expression_complexity: crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING, max_xpath_context_evaluations: crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING, max_xpath_evaluation_work: crate::hard_limits::XPATH_EVALUATION_WORK_CEILING, max_xpath_mirror_string_bytes: crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING, max_xpath_string_work_bytes: crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING, max_xpath_namespace_bindings: crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING, max_xpath_namespace_bytes: crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING, max_xpath_filters: crate::hard_limits::XPATH_FILTER_COUNT_CEILING, max_node_set_filter_work: crate::hard_limits::NODE_SET_FILTER_WORK_CEILING, max_node_set_entries: 262_144, max_node_set_owned_string_bytes: crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING, max_node_set_cumulative_owned_string_bytes: crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING, max_opc_workspace_bytes: crate::hard_limits::OPC_WORKSPACE_BYTE_CEILING, max_opc_parameter_bytes: crate::hard_limits::OPC_PARAMETER_BYTE_CEILING, max_xslt_work: crate::hard_limits::XSLT_WORK_CEILING, max_xslt_workspace_bytes: crate::hard_limits::XSLT_WORKSPACE_BYTE_CEILING, max_xslt_output_bytes: crate::hard_limits::XSLT_OUTPUT_BYTE_CEILING, max_xslt_recursion_depth: crate::hard_limits::XSLT_RECURSION_DEPTH_CEILING, } } } impl ResourcePolicy { /// Validate policy values against non-configurable implementation ceilings. /// /// # Errors /// Returns `ResourceLimit` for the first configured value exceeding its /// implementation ceiling, in XML, material, `XPath`, then transform order. pub fn validate(&self) -> Result<(), PolicyViolation> { self.validate_xml_limits()?; self.validate_material_limits()?; self.validate_xpath_limits()?; self.validate_transform_limits() } fn validate_xml_limits(&self) -> Result<(), PolicyViolation> { Self::within( resource_name::XML_NODES, self.max_xml_nodes, crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize, )?; Self::within( resource_name::XML_DEPTH, self.max_xml_depth, crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING, )?; Self::within( resource_name::XML_NAMESPACE_BINDINGS, self.max_xml_namespace_bindings, crate::hard_limits::XML_NAMESPACE_BINDING_CEILING, )?; Self::within( resource_name::CANONICALIZED_BYTES, self.max_canonicalized_bytes, crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING, )?; Self::within( resource_name::SIGNATURE_REFERENCES, self.max_references, crate::hard_limits::SIGNATURE_REFERENCE_CEILING, )?; Self::within( resource_name::VERIFICATION_SIGNATURES, self.max_signatures, crate::hard_limits::VERIFICATION_SIGNATURE_CEILING, )?; Self::within( resource_name::REFERENCE_TRANSFORMS, self.max_transforms_per_reference, crate::hard_limits::REFERENCE_TRANSFORM_CEILING, )?; Self::within( resource_name::XML_BASE_COMPONENTS, self.max_xml_base_components, crate::hard_limits::XML_BASE_COMPONENT_CEILING, )?; Self::within( resource_name::XML_BASE_RESOLUTION_BYTES, self.max_xml_base_resolution_bytes, crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING, )?; Self::within( resource_name::XML_DOCUMENT, self.max_xml_document_bytes, crate::hard_limits::XML_DOCUMENT_BYTE_CEILING, )?; Self::within( resource_name::XML_PARSE_WORK_BYTES, self.max_xml_parse_work_bytes, crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING, )?; Self::within( resource_name::EXTERNAL_RESOURCE_BYTES, self.max_external_resource_bytes, crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING, )?; Self::within( resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES, self.max_external_resource_total_bytes, crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING, )?; Self::within( resource_name::DETACHED_PAYLOAD_BYTES, self.max_detached_payload_bytes, crate::hard_limits::DETACHED_PAYLOAD_BYTE_CEILING, )?; Self::within( resource_name::AGGREGATE_DETACHED_PAYLOAD_BYTES, self.max_detached_payload_total_bytes, crate::hard_limits::DETACHED_PAYLOAD_TOTAL_BYTE_CEILING, )?; Self::within( resource_name::ENCRYPTION_PLAINTEXT_BYTES, self.max_encryption_plaintext_bytes, crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING, )?; Self::within( resource_name::ENCRYPTION_RECIPIENTS, self.max_encryption_recipients, crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING, )?; Self::within( resource_name::ENCRYPTION_METADATA_BYTES, self.max_encryption_metadata_bytes, crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING, )?; Ok(()) } fn validate_material_limits(&self) -> Result<(), PolicyViolation> { for (resource, selected, ceiling) in [ ( resource_name::X509_WORK, self.max_x509_work, crate::hard_limits::X509_WORK_CEILING, ), ( resource_name::X509_WORKSPACE_BYTES, self.max_x509_workspace_bytes, crate::hard_limits::X509_WORKSPACE_BYTE_CEILING, ), ( resource_name::KEY_CANDIDATES, self.max_key_candidates, crate::hard_limits::KEY_CANDIDATE_CEILING, ), ( resource_name::KEY_IMPORT_KDF_WORK, self.max_key_import_kdf_work, usize::try_from(crate::hard_limits::KEY_IMPORT_KDF_WORK_CEILING) .expect("key import work ceiling fits supported 32-bit and 64-bit targets"), ), ( resource_name::KEY_IMPORT_KDF_MEMORY, self.max_key_import_kdf_memory_bytes, crate::hard_limits::KEY_IMPORT_KDF_MEMORY_CEILING, ), ( resource_name::KEY_INFO_REFERENCE_DEPTH, self.max_key_info_reference_depth, crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING, ), ( resource_name::BASE64_TRANSFORM_INPUT_BYTES, self.max_base64_transform_input_bytes, crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING, ), ( resource_name::BASE64_TRANSFORM_OUTPUT_BYTES, self.max_base64_transform_output_bytes, crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING, ), ] { Self::within(resource, selected, ceiling)?; } Ok(()) } fn validate_xpath_limits(&self) -> Result<(), PolicyViolation> { for (resource, selected, ceiling) in [ ( resource_name::XPATH_EXPRESSIONS, self.max_xpath_expressions, crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING, ), ( resource_name::XPATH_EXPRESSION_BYTES, self.max_xpath_expression_bytes, crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING, ), ( resource_name::XPATH_EXPRESSION_COMPLEXITY, self.max_xpath_expression_complexity, crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING, ), ( resource_name::XPATH_CONTEXT_EVALUATIONS, self.max_xpath_context_evaluations, crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING, ), ( resource_name::XPATH_EVALUATION_WORK, self.max_xpath_evaluation_work, crate::hard_limits::XPATH_EVALUATION_WORK_CEILING, ), ( resource_name::XPATH_MIRROR_STRING_BYTES, self.max_xpath_mirror_string_bytes, crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING, ), ( resource_name::XPATH_STRING_WORK_BYTES, self.max_xpath_string_work_bytes, crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING, ), ( resource_name::XPATH_NAMESPACE_BINDINGS, self.max_xpath_namespace_bindings, crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING, ), ( resource_name::XPATH_NAMESPACE_BYTES, self.max_xpath_namespace_bytes, crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING, ), ( resource_name::XPATH_FILTERS, self.max_xpath_filters, crate::hard_limits::XPATH_FILTER_COUNT_CEILING, ), ( resource_name::NODE_SET_FILTER_WORK, self.max_node_set_filter_work, crate::hard_limits::NODE_SET_FILTER_WORK_CEILING, ), ( resource_name::NODE_SET_ENTRIES, self.max_node_set_entries, crate::hard_limits::NODE_SET_ENTRY_CEILING, ), ( resource_name::NODE_SET_OWNED_STRING_BYTES, self.max_node_set_owned_string_bytes, crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING, ), ( resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES, self.max_node_set_cumulative_owned_string_bytes, crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING, ), ] { Self::within(resource, selected, ceiling)?; } Ok(()) } fn validate_transform_limits(&self) -> Result<(), PolicyViolation> { Self::within( resource_name::OPC_WORKSPACE_BYTES, self.max_opc_workspace_bytes, crate::hard_limits::OPC_WORKSPACE_BYTE_CEILING, )?; Self::within( resource_name::OPC_PARAMETER_BYTES, self.max_opc_parameter_bytes, crate::hard_limits::OPC_PARAMETER_BYTE_CEILING, )?; for (resource, selected, ceiling) in [ ( resource_name::XSLT_WORK, self.max_xslt_work, crate::hard_limits::XSLT_WORK_CEILING, ), ( resource_name::XSLT_WORKSPACE_BYTES, self.max_xslt_workspace_bytes, crate::hard_limits::XSLT_WORKSPACE_BYTE_CEILING, ), ( resource_name::XSLT_OUTPUT_BYTES, self.max_xslt_output_bytes, crate::hard_limits::XSLT_OUTPUT_BYTE_CEILING, ), ( resource_name::XSLT_RECURSION_DEPTH, self.max_xslt_recursion_depth, crate::hard_limits::XSLT_RECURSION_DEPTH_CEILING, ), ] { Self::within(resource, selected, ceiling)?; } Ok(()) } pub(crate) fn validate_xml_document_len(&self, actual: usize) -> Result<(), PolicyViolation> { if actual > self.max_xml_document_bytes { return Err(PolicyViolation::ResourceLimit { resource: resource_name::XML_DOCUMENT, maximum: self.max_xml_document_bytes, actual, }); } Ok(()) } #[cfg(any(feature = "xmldsig", feature = "xmlenc"))] /// Reject aggregate key-candidate work beyond this policy snapshot. /// /// # Errors /// Returns a `ResourceLimit` for candidate counts above `max_key_candidates`. pub fn validate_key_candidates(&self, actual: usize) -> Result<(), PolicyViolation> { if actual > self.max_key_candidates { return Err(PolicyViolation::ResourceLimit { resource: resource_name::KEY_CANDIDATES, maximum: self.max_key_candidates, actual, }); } Ok(()) } /// Reject `KeyInfoReference` traversal beyond this policy snapshot. /// /// # Errors /// Returns a `ResourceLimit` when traversal exceeds `max_key_info_reference_depth`. #[cfg(feature = "xmldsig")] pub fn validate_key_info_reference_depth(&self, actual: usize) -> Result<(), PolicyViolation> { if actual > self.max_key_info_reference_depth { return Err(PolicyViolation::ResourceLimit { resource: resource_name::KEY_INFO_REFERENCE_DEPTH, maximum: self.max_key_info_reference_depth, actual, }); } Ok(()) } pub(crate) fn effective_xml_nodes(&self) -> u32 { u32::try_from(self.max_xml_nodes) .unwrap_or(crate::hard_limits::XML_DOCUMENT_NODE_CEILING) .min(crate::hard_limits::XML_DOCUMENT_NODE_CEILING) } #[cfg(feature = "xmldsig")] pub(crate) fn effective_canonicalized_bytes(&self) -> usize { self.max_canonicalized_bytes .min(crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING) } #[cfg(feature = "xmldsig")] pub(crate) fn effective_xml_base_components(&self) -> usize { self.max_xml_base_components .min(crate::hard_limits::XML_BASE_COMPONENT_CEILING) } #[cfg(feature = "xmldsig")] pub(crate) fn effective_xml_base_resolution_bytes(&self) -> usize { self.max_xml_base_resolution_bytes .min(crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING) } fn within( resource: &'static str, selected: usize, ceiling: usize, ) -> Result<(), PolicyViolation> { if selected > ceiling { return Err(PolicyViolation::ResourceLimit { resource, maximum: ceiling, actual: selected, }); } Ok(()) } #[cfg(feature = "xmldsig")] fn nonzero_within( resource: &'static str, selected: usize, ceiling: usize, ) -> Result<(), PolicyViolation> { if selected == 0 { return Err(PolicyViolation::InvalidResourceLimit { resource, requirement: "limit must be nonzero", actual: selected, }); } Self::within(resource, selected, ceiling) } } /// XML parsing decisions shared by all operation policies. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct XmlInputPolicy { /// Permit bounded internal DTD declarations. External resolution stays off. pub allow_internal_dtd: bool, } /// Trusted edition of relationship selection and normalization. /// This is not an OPC package-conformance profile: signature placement and /// package-wide part identity constraints require a package validator. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum OpcRelationshipEdition { /// ECMA-376 Part 2 (2012), including libxmlsec1's case-sensitive selection. Ecma2012, /// ECMA-376 Part 2 (2021) section 10.6, ASCII-insensitive selection. #[default] Ecma2021, } /// `XMLDSig` transform and canonicalization decisions shared by signing and verification. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum SameDocumentIdSemantics { /// Require bare fragment identifiers to satisfy the XML `NCName` grammar. #[default] Specification, /// Apply libxmlsec1's default barename compatibility grammar. /// /// Registered non-NCName values are accepted unless a single quote makes /// them unrepresentable in the donor's single-quoted `XPointer` expression. /// The resulting node set retains barename semantics and excludes comments. XmlSecBarename, /// Resolve the fragment text directly as an ID, including non-NCName values. /// /// This reproduces libxmlsec1's explicit `Visa3D` compatibility flag. XmlSecVisa3d, } /// How the stylesheet of an XSLT transform is read from its ``. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum XsltStylesheetSemantics { /// The stylesheet is an element of the signed document: every namespace in /// scope at it, including those declared on its ancestors (Namespaces in XML /// section 6.1), applies and is copied into literal result elements, and a /// prefix bound nowhere in `exclude-result-prefixes` or /// `extension-element-prefixes` is a stylesheet error (XSLT 1.0 section 7.1.1). #[default] Specification, /// libxmlsec1: the stylesheet is the serialized `` content, so /// namespaces declared on its ancestors are lost, and libxslt skips an /// unbound prefix in a prefix list after a warning. Libxmlsec1, } /// Wire representation used for ECDSA `SignatureValue` bytes. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum EcdsaSignatureValueEncoding { /// `XMLDSig` fixed-width `r || s` representation. #[default] XmlDsig, /// ASN.1 DER `SEQUENCE(INTEGER(r), INTEGER(s))` compatibility representation. XmlSecAsn1Der, } /// `XMLDSig` transform and canonicalization decisions shared by signing and verification. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct TransformPolicy { /// Allowed transform and canonicalization URIs; `None` accepts every implemented algorithm. pub allowed_algorithms: Option>, /// Node selected for the `XPath` `here()` extension function. pub xpath_here_semantics: XPathHereSemantics, /// Interpretation of bare same-document ID fragments. pub same_document_id_semantics: SameDocumentIdSemantics, /// Trusted normalization edition, not package validation; never inferred from XML. pub opc_relationship_edition: OpcRelationshipEdition, /// How XSLT stylesheets are read; applies to builds with `xslt`. pub xslt_stylesheet_semantics: XsltStylesheetSemantics, } /// URI-class decisions shared by `XMLDSig` reference and key retrieval processing. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct UriPolicy { /// URI classes accepted by `SignedInfo` and Manifest references. pub references: UriTypeSet, /// URI classes accepted by `RetrievalMethod` processing. pub retrieval_methods: UriTypeSet, /// URI classes accepted by `XMLDSig` 1.1 `KeyInfoReference` processing. pub key_info_references: UriTypeSet, } #[cfg(feature = "xmldsig")] impl Default for UriPolicy { fn default() -> Self { Self { references: UriTypeSet::SAME_DOCUMENT, retrieval_methods: UriTypeSet::SAME_DOCUMENT, key_info_references: UriTypeSet::SAME_DOCUMENT, } } } /// An RFC 5280 extended-key-purpose identifier accepted for XML signing. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum ExtendedKeyPurpose { /// TLS server authentication (`id-kp-serverAuth`). ServerAuth, /// TLS client authentication (`id-kp-clientAuth`). ClientAuth, /// Executable code signing (`id-kp-codeSigning`). CodeSigning, /// Email protection (`id-kp-emailProtection`). EmailProtection, /// Trusted timestamping (`id-kp-timeStamping`). TimeStamping, /// OCSP response signing (`id-kp-OCSPSigning`). OcspSigning, /// Application-defined purpose represented as OID arcs. Other(Vec), } /// Whether verification must establish signer authorization in addition to mathematics. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum VerificationTrustMode { /// Require path validation or an explicit caller trust declaration. #[default] RequireTrustedKey, /// Check mathematical validity without authenticating the signer. CryptographicOnly, } /// X.509 and key-resolution decisions for verification. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, PartialEq, Eq)] pub struct KeyTrustPolicy { /// Whether mathematical validity alone is sufficient for the operation. pub mode: VerificationTrustMode, /// Require embedded or selected certificates to chain to a configured anchor. pub verify_x509_chains: bool, /// Maximum validated path depth. pub max_x509_chain_depth: usize, /// Maximum complete or partial signature-valid path states generated. pub max_x509_candidate_paths: usize, /// Legacy signature algorithms explicitly permitted for verification. pub allowed_legacy_signature_algorithms: BTreeSet, /// Independent legacy-hash permission for parameterized RSA-PSS verification. pub parameterized_rsa_pss: RsaPssPermission, /// Certificate and CRL signature permissions, independent of XML signatures. /// `None` permits supported classical algorithms but rejects PQ algorithms; /// Explicit permissions select either an exact allowlist (including PSS /// parameters) or all provider-supported algorithms for compatibility. pub certificate_signature_algorithms: Option, /// RSA requirements enforced for resolved verification keys and issuer keys. pub rsa_keys: RsaKeyPolicy, /// DSA requirements enforced for resolved verification keys. pub dsa_keys: DsaKeyPolicy, /// Purposes accepted when any certificate in a path carries `ExtendedKeyUsage`. /// /// An empty set accepts only paths whose certificates omit `ExtendedKeyUsage` /// or use `anyExtendedKeyUsage`; it does not treat TLS/code-signing purposes /// as a generic authorization for XML signatures. pub allowed_extended_key_usages: BTreeSet, /// Authenticate and enforce embedded CRLs during path validation. /// Requires [`Self::verify_x509_chains`]. pub check_crls: bool, /// Verification time override; `None` selects the host clock. Builds /// without `std` have no clock and require this time. pub verification_time: Option, } #[cfg(feature = "xmldsig")] impl Default for KeyTrustPolicy { fn default() -> Self { Self { mode: VerificationTrustMode::RequireTrustedKey, verify_x509_chains: false, max_x509_chain_depth: crate::hard_limits::X509_CHAIN_DEPTH_CEILING, max_x509_candidate_paths: crate::hard_limits::X509_CANDIDATE_PATH_CEILING, allowed_legacy_signature_algorithms: BTreeSet::new(), parameterized_rsa_pss: RsaPssPermission::ExactOnly, certificate_signature_algorithms: None, rsa_keys: RsaKeyPolicy::default(), dsa_keys: DsaKeyPolicy::default(), allowed_extended_key_usages: BTreeSet::new(), check_crls: false, verification_time: None, } } } #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, PartialEq, Eq)] /// Explicit certificate/CRL permissions, independent of XML signature methods. pub enum CertificateSignatureAlgorithms { /// Permit any algorithm the selected provider implements, including PQ. /// This is an explicit compatibility opt-in, not a provider fallback. AllSupported, /// Permit only exact algorithms, including every RSA-PSS parameter. Allowlist(BTreeSet), } #[cfg(feature = "xmldsig")] impl KeyTrustPolicy { pub(crate) fn check_certificate_signature_algorithm( algorithm: crate::provider::X509SignatureAlgorithm, allowed: Option<&CertificateSignatureAlgorithms>, ) -> Result<(), PolicyViolation> { // This is deployment permission, not a PKIX syntax requirement. // Algorithm parameters are validated before this gate; compiled // provider capability is checked only after permission is established. let permitted = match allowed { Some(CertificateSignatureAlgorithms::AllSupported) => true, Some(CertificateSignatureAlgorithms::Allowlist(allowed)) => { allowed.contains(&algorithm) } None => match algorithm { crate::provider::X509SignatureAlgorithm::PostQuantum(_) => false, #[cfg(feature = "dstu4145")] crate::provider::X509SignatureAlgorithm::Dstu4145 { .. } => false, _ => true, }, }; if !permitted { return Err(PolicyViolation::Algorithm { operation: "certificate/CRL verification", algorithm: algorithm .oid() .map_or_else(|| format!("{algorithm:?}"), str::to_owned), }); } Ok(()) } pub(crate) fn validate(&self) -> Result<(), PolicyViolation> { if self.check_crls && !self.verify_x509_chains { return Err(PolicyViolation::KeyTrust { reason: "CRL checking requires X.509 chain validation", }); } if self .allowed_extended_key_usages .iter() .any(|purpose| match purpose { ExtendedKeyPurpose::Other(arcs) => { arcs.len() < 2 || arcs[0] > 2 || (arcs[0] < 2 && arcs[1] > 39) } _ => false, }) { return Err(PolicyViolation::KeyTrust { reason: "custom extended key purposes must contain valid OID arcs", }); } self.rsa_keys.validate()?; self.dsa_keys.validate()?; ResourcePolicy::nonzero_within( "X.509 chain depth", self.max_x509_chain_depth, crate::hard_limits::X509_CHAIN_DEPTH_CEILING, )?; ResourcePolicy::nonzero_within( "X.509 candidate paths", self.max_x509_candidate_paths, crate::hard_limits::X509_CANDIDATE_PATH_CEILING, ) } } /// Whether an `XMLDSig` operation evaluates Manifest references. Verification /// requires original authenticated coverage; signing populates direct Object/Manifest digests. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum ManifestProcessing { /// Leave Manifest reference values untouched and perform no Manifest work. #[default] Ignore, /// Evaluate Manifest references under the operation's shared resource policy. Process, } /// Whether a detached Reference may be completed with a digest the caller /// computed over the payload it paired, instead of bytes streamed through the /// selected provider. The result records which happened. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum DetachedDigestAttestation { /// Only bytes digested by this operation complete a detached Reference. #[default] Forbidden, /// A caller-attested digest completes a Reference with no transforms. Accepted, } /// How the RFC 3161 tokens of a signature are read. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum TimestampTokenEncoding { /// Each token holds to its declared encoding, DER when it declares none /// (EN 319 132-1 §5.1.3). #[default] Declared, /// RFC 5652 BER. A level then asserts nothing about EN 319 132-1 §5.1.3. Ber, } /// Immutable policy snapshot for `XMLDSig` verification. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Default)] pub struct VerificationPolicy { /// Explicitly authorized lifetime; checked again at operation entry. pub validity: Option, /// XML Security requirements, not application protocol validation. pub signature_constraints: SignatureConstraints, /// Shared key-establishment permissions and cumulative operation limits. pub key_establishment: KeyEstablishmentPolicy, /// Family permission selected by the caller, never derived from document input. /// Legacy hashes additionally require the independent key-trust permission. pub parameterized_rsa_pss: RsaPssPermission, /// Exact signature methods, supplemented by `parameterized_rsa_pss`. /// `None` accepts every implemented method subject to /// independent gates such as [`KeyTrustPolicy::allowed_legacy_signature_algorithms`]. pub signature_algorithms: Option>, /// Allowed reference digest methods; `None` accepts every implemented method. pub digest_algorithms: Option>, /// HMAC secret and output-length requirements. pub hmac: HmacPolicy, /// Required ECDSA `SignatureValue` wire representation. pub ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding, /// Key and certificate trust rules. pub key_trust: KeyTrustPolicy, /// `KeyInfo` source permissions. pub key_sources: KeySourcePolicy, /// Reference and key-retrieval URI permissions. pub uris: UriPolicy, /// Transform and canonicalization permissions. pub transforms: TransformPolicy, /// Whether authenticated Manifest references are processed. pub manifest_processing: ManifestProcessing, /// Whether a caller-attested digest may complete a detached Reference. pub detached_digest_attestation: DetachedDigestAttestation, /// How the signature's timestamp tokens are read. pub timestamp_token_encoding: TimestampTokenEncoding, /// XML parser rules. pub xml: XmlInputPolicy, /// Resource ceilings. pub resources: ResourcePolicy, } #[cfg(feature = "xmldsig")] impl VerificationPolicy { pub(crate) fn check_digest_algorithm( &self, algorithm: DigestAlgorithm, ) -> Result<(), PolicyViolation> { if self .digest_algorithms .as_ref() .map_or(!algorithm.requires_explicit_permission(), |allowed| { allowed.contains(&algorithm) }) { Ok(()) } else { Err(PolicyViolation::Algorithm { operation: "verification", algorithm: algorithm.uri().to_owned(), }) } } /// Validate the complete snapshot against implementation hard ceilings. /// /// # Errors /// Returns expired/invalid authorization, contradictory target trust, or /// invalid resource, key-establishment, trust and HMAC configuration. pub fn validate(&self) -> Result<(), PolicyViolation> { if let Some(validity) = &self.validity { validity.validate_at(clock::authorization_time(self.key_trust.verification_time)?)?; } if self.signature_constraints != SignatureConstraints::General && self.key_trust.mode == VerificationTrustMode::CryptographicOnly { return Err(PolicyViolation::Configuration { reason: "target coverage requires trusted-key verification", }); } self.resources.validate()?; self.key_establishment.validate()?; self.key_trust.validate()?; self.hmac.validate() } /// Enforce the signature algorithm after key resolution. /// /// # Errors /// Returns an `Algorithm` violation when exact, legacy or parameterized /// method permissions do not admit the requested signature method. pub fn check_signature_algorithm( &self, algorithm: SignatureAlgorithm, ) -> Result<(), PolicyViolation> { if (matches!(algorithm, SignatureAlgorithm::PostQuantum(_)) || algorithm.requires_explicit_permission()) && !self .signature_algorithms .as_ref() .is_some_and(|allowed| allowed.contains(&algorithm)) { return Err(PolicyViolation::Algorithm { operation: "verification", algorithm: algorithm.uri().to_owned(), }); } if (algorithm.uses_legacy_pss_hash() || matches!( algorithm, SignatureAlgorithm::RsaSha1 | SignatureAlgorithm::DsaSha1 | SignatureAlgorithm::HmacSha1 | SignatureAlgorithm::EcdsaSha1 )) && !self .key_trust .allowed_legacy_signature_algorithms .contains(&algorithm) && !self.key_trust.parameterized_rsa_pss.permits(algorithm) { return Err(PolicyViolation::Algorithm { operation: "verification", algorithm: algorithm.uri().to_string(), }); } if self .signature_algorithms .as_ref() .is_some_and(|allowed| !allowed.contains(&algorithm)) && !self.parameterized_rsa_pss.permits(algorithm) { return Err(PolicyViolation::Algorithm { operation: "verification", algorithm: algorithm.uri().to_string(), }); } Ok(()) } } /// Immutable policy snapshot for `XMLDSig` signing. #[cfg(feature = "xmldsig")] #[derive(Debug, Clone, Default)] pub struct SigningPolicy { /// Explicitly authorized lifetime; checked again at operation entry. pub validity: Option, /// XML Security requirements, not application protocol validation. pub signature_constraints: SignatureConstraints, /// Shared key-establishment permissions and cumulative operation limits. pub key_establishment: KeyEstablishmentPolicy, /// Explicit parameterized RSA-PSS family permission, including legacy hashes. pub parameterized_rsa_pss: RsaPssPermission, /// Exact signing methods, supplemented by `parameterized_rsa_pss`. /// `None` uses the implemented secure defaults. pub signature_algorithms: Option>, /// Allowed reference digest methods; `None` uses the implemented secure defaults. pub digest_algorithms: Option>, /// HMAC secret and output-length requirements. pub hmac: HmacPolicy, /// ECDSA `SignatureValue` wire representation emitted by signing. pub ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding, /// RSA requirements enforced before producing a signature. pub rsa_keys: RsaKeyPolicy, /// DSA requirements enforced before producing a signature. pub dsa_keys: DsaKeyPolicy, /// Reference URI permissions. External URIs resolve only from caller-supplied /// request bytes and never enable filesystem or network access. pub uris: UriPolicy, /// Transform and canonicalization permissions. pub transforms: TransformPolicy, /// Whether direct `/` reference digests are populated. pub manifest_processing: ManifestProcessing, /// XML parser rules. pub xml: XmlInputPolicy, /// Resource ceilings. pub resources: ResourcePolicy, } #[cfg(feature = "xmldsig")] impl SigningPolicy { /// Validate the complete snapshot before signing work begins. /// /// # Errors /// Returns expired/invalid authorization or invalid resource, /// key-establishment, RSA, DSA and HMAC configuration. pub fn validate(&self) -> Result<(), PolicyViolation> { if let Some(validity) = &self.validity { validity.validate_at(clock::authorization_time(None)?)?; } self.resources.validate()?; self.key_establishment.validate()?; self.rsa_keys.validate()?; self.dsa_keys.validate()?; self.hmac.validate() } pub(crate) fn check_signature_algorithm( &self, algorithm: SignatureAlgorithm, ) -> Result<(), PolicyViolation> { if self.parameterized_rsa_pss.permits(algorithm) { return Ok(()); } check_signing_algorithm( self.signature_algorithms.as_ref(), algorithm, algorithm.signing_allowed(), algorithm.uri(), ) } pub(crate) fn check_digest_algorithm( &self, algorithm: DigestAlgorithm, ) -> Result<(), PolicyViolation> { check_signing_algorithm( self.digest_algorithms.as_ref(), algorithm, algorithm.signing_allowed(), algorithm.uri(), ) } } #[cfg(feature = "xmldsig")] fn check_signing_algorithm( allowlist: Option<&BTreeSet>, algorithm: T, default_allowed: bool, uri: &str, ) -> Result<(), PolicyViolation> { if allowlist.map_or(default_allowed, |allowed| allowed.contains(&algorithm)) { Ok(()) } else { Err(PolicyViolation::Algorithm { operation: "signing", algorithm: uri.to_owned(), }) } } /// Enforce the shared content-cipher permission contract before dispatch. #[cfg(feature = "xmlenc")] pub(crate) fn check_content_algorithm( allowlist: Option<&BTreeSet>, algorithm: DataEncryptionAlgorithm, operation: &'static str, ) -> Result<(), PolicyViolation> { if allowlist.map_or(!algorithm.requires_explicit_permission(), |allowed| { allowed.contains(&algorithm) }) { Ok(()) } else { Err(PolicyViolation::Algorithm { operation, algorithm: algorithm.uri().to_owned(), }) } } /// Immutable policy snapshot for `XMLEnc` encryption. #[cfg(feature = "xmlenc")] #[derive(Debug, Clone, Default)] pub struct EncryptionPolicy { /// Explicitly authorized lifetime; checked again at operation entry. pub validity: Option, /// Allowed content-encryption algorithms. pub data_algorithms: Option>, /// Allowed RSA key-transport algorithms. pub key_transport_algorithms: Option>, /// Allowed symmetric key-wrap algorithms. pub key_wrap_algorithms: Option>, /// Allowed OAEP digest algorithms. pub oaep_digests: Option>, /// RSA requirements enforced when producing OAEP key transport. pub rsa_keys: RsaKeyPolicy, /// Shared agreement/KDF permission and cumulative work requirements. pub key_establishment: KeyEstablishmentPolicy, /// XML parser rules. pub xml: XmlInputPolicy, /// Resource ceilings. pub resources: ResourcePolicy, } #[cfg(feature = "xmlenc")] impl EncryptionPolicy { /// Validate the complete snapshot before outbound encryption work begins. /// /// # Errors /// Returns expired/invalid authorization or invalid resource, /// key-establishment and RSA configuration. pub fn validate(&self) -> Result<(), PolicyViolation> { if let Some(validity) = &self.validity { validity.validate_at(clock::authorization_time(None)?)?; } self.resources.validate()?; self.key_establishment.validate()?; self.rsa_keys.validate() } } /// Immutable policy snapshot for `XMLEnc` decryption. #[cfg(feature = "xmlenc")] #[derive(Debug, Clone, Default)] pub struct DecryptionPolicy { /// Explicitly authorized lifetime; checked again at operation entry. pub validity: Option, /// URI-class permissions for ciphertext and indirect key references. pub uris: UriPolicy, /// Shared transform permissions and same-document reference semantics. pub transforms: TransformPolicy, /// Allowed content-decryption algorithms. pub data_algorithms: Option>, /// Allowed RSA key-transport algorithms accepted on input. pub key_transport_algorithms: Option>, /// Allowed symmetric key-wrap algorithms accepted on input. pub key_wrap_algorithms: Option>, /// Allowed OAEP digest algorithms accepted on input. pub oaep_digests: Option>, /// RSA requirements enforced before OAEP recovery; defaults to 2048 bits. /// Legacy input requires an explicit caller-selected lower minimum. pub rsa_keys: RsaKeyPolicy, /// Shared agreement/KDF permission and cumulative work requirements. pub key_establishment: KeyEstablishmentPolicy, /// XML parser rules. pub xml: XmlInputPolicy, /// Resource ceilings. pub resources: ResourcePolicy, } #[cfg(feature = "xmlenc")] impl DecryptionPolicy { /// Validate the complete snapshot before inbound decryption work begins. /// /// # Errors /// Returns expired/invalid authorization or invalid resource, /// key-establishment and RSA configuration. pub fn validate(&self) -> Result<(), PolicyViolation> { if let Some(validity) = &self.validity { validity.validate_at(clock::authorization_time(None)?)?; } self.resources.validate()?; self.key_establishment.validate()?; self.rsa_keys.validate() } } #[cfg(test)] mod tests { use super::*; #[cfg(all(feature = "legacy-algorithms", feature = "xmldsig"))] #[test] fn legacy_digest_permission_is_explicit_and_operation_local() { // Capability does not imply acceptance, and permission for one digest // or operation must never enable another digest or operation. for algorithm in [DigestAlgorithm::Md5, DigestAlgorithm::Ripemd160] { assert!( VerificationPolicy::default() .check_digest_algorithm(algorithm) .is_err() ); assert!( SigningPolicy::default() .check_digest_algorithm(algorithm) .is_err() ); let verification = VerificationPolicy { digest_algorithms: Some(BTreeSet::from([algorithm])), ..VerificationPolicy::default() }; let signing = SigningPolicy { digest_algorithms: Some(BTreeSet::from([algorithm])), ..SigningPolicy::default() }; assert!(verification.check_digest_algorithm(algorithm).is_ok()); assert!(signing.check_digest_algorithm(algorithm).is_ok()); assert!( verification .check_digest_algorithm(DigestAlgorithm::Sha256) .is_err() ); assert!( signing .check_digest_algorithm(DigestAlgorithm::Sha256) .is_err() ); } } #[cfg(feature = "xmldsig")] #[test] fn certificate_permissions_keep_exact_pss_and_compatibility_distinct() { use crate::provider::X509SignatureAlgorithm; use crate::xmldsig::{DigestAlgorithm, PqAlgorithm}; // Compatibility permission cannot impose an arbitrary PSS salt list; // exact allowlists must still distinguish every parameter tuple. let exact = X509SignatureAlgorithm::RsaPss { digest: DigestAlgorithm::Sha256, mgf_digest: DigestAlgorithm::Sha256, salt_len: 32, }; let allowed = CertificateSignatureAlgorithms::Allowlist(BTreeSet::from([exact])); assert!( KeyTrustPolicy::check_certificate_signature_algorithm(exact, Some(&allowed)).is_ok() ); for salt_len in [0, 17, 64, 255] { let method = X509SignatureAlgorithm::RsaPss { digest: DigestAlgorithm::Sha256, mgf_digest: DigestAlgorithm::Sha256, salt_len, }; assert!( KeyTrustPolicy::check_certificate_signature_algorithm(method, Some(&allowed)) .is_err() ); assert!( KeyTrustPolicy::check_certificate_signature_algorithm( method, Some(&CertificateSignatureAlgorithms::AllSupported) ) .is_ok() ); } let pq = X509SignatureAlgorithm::PostQuantum(PqAlgorithm::MlDsa44); assert!(KeyTrustPolicy::check_certificate_signature_algorithm(pq, None).is_err()); assert!( KeyTrustPolicy::check_certificate_signature_algorithm( pq, Some(&CertificateSignatureAlgorithms::AllSupported) ) .is_ok() ); } #[test] fn resource_policy_cannot_exceed_implementation_ceiling() { let policy = ResourcePolicy { max_xml_nodes: 100_001, ..ResourcePolicy::default() }; assert!(matches!( policy.validate(), Err(PolicyViolation::ResourceLimit { resource: resource_name::XML_NODES, maximum: 100_000, actual: 100_001, }) )); } #[test] fn xml_parse_work_policy_cannot_exceed_implementation_ceiling() { let policy = ResourcePolicy { max_xml_parse_work_bytes: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING .saturating_add(1), ..ResourcePolicy::default() }; assert_eq!( policy.validate(), Err(PolicyViolation::ResourceLimit { resource: resource_name::XML_PARSE_WORK_BYTES, maximum: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING, actual: crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING.saturating_add(1), }) ); } #[test] fn every_resource_policy_field_obeys_its_hard_ceiling() { // Each public tuning knob is only a stricter operational limit; none // may raise the implementation's allocation ceiling. Exact diagnostics // also catch a field accidentally paired with another field's ceiling. type Case = (&'static str, usize, fn(&mut ResourcePolicy) -> &mut usize); // One row per policy field: resource name, hard ceiling, field. macro_rules! cases { ($($resource:ident: $ceiling:expr => $field:ident;)*) => { &[$((resource_name::$resource, $ceiling, |p| &mut p.$field)),*] }; } use crate::hard_limits as limit; let key_import_work = usize::try_from(limit::KEY_IMPORT_KDF_WORK_CEILING) .expect("key import work ceiling fits supported 32-bit and 64-bit targets"); let cases: &[Case] = cases! { X509_WORK: limit::X509_WORK_CEILING => max_x509_work; X509_WORKSPACE_BYTES: limit::X509_WORKSPACE_BYTE_CEILING => max_x509_workspace_bytes; XML_NODES: limit::XML_DOCUMENT_NODE_CEILING as usize => max_xml_nodes; XML_DEPTH: limit::XML_DOCUMENT_DEPTH_CEILING => max_xml_depth; XML_NAMESPACE_BINDINGS: limit::XML_NAMESPACE_BINDING_CEILING => max_xml_namespace_bindings; SIGNATURE_REFERENCES: limit::SIGNATURE_REFERENCE_CEILING => max_references; VERIFICATION_SIGNATURES: limit::VERIFICATION_SIGNATURE_CEILING => max_signatures; REFERENCE_TRANSFORMS: limit::REFERENCE_TRANSFORM_CEILING => max_transforms_per_reference; XML_BASE_COMPONENTS: limit::XML_BASE_COMPONENT_CEILING => max_xml_base_components; XML_BASE_RESOLUTION_BYTES: limit::XML_BASE_RESOLUTION_BYTE_CEILING => max_xml_base_resolution_bytes; CANONICALIZED_BYTES: limit::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING => max_canonicalized_bytes; EXTERNAL_RESOURCE_BYTES: limit::EXTERNAL_RESOURCE_BYTE_CEILING => max_external_resource_bytes; AGGREGATE_EXTERNAL_RESOURCE_BYTES: limit::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING => max_external_resource_total_bytes; DETACHED_PAYLOAD_BYTES: limit::DETACHED_PAYLOAD_BYTE_CEILING => max_detached_payload_bytes; AGGREGATE_DETACHED_PAYLOAD_BYTES: limit::DETACHED_PAYLOAD_TOTAL_BYTE_CEILING => max_detached_payload_total_bytes; ENCRYPTION_PLAINTEXT_BYTES: limit::ENCRYPTION_PLAINTEXT_BYTE_CEILING => max_encryption_plaintext_bytes; XML_DOCUMENT: limit::XML_DOCUMENT_BYTE_CEILING => max_xml_document_bytes; XML_PARSE_WORK_BYTES: limit::XML_PARSE_WORK_BYTE_CEILING => max_xml_parse_work_bytes; ENCRYPTION_RECIPIENTS: limit::ENCRYPTION_RECIPIENT_CEILING => max_encryption_recipients; ENCRYPTION_METADATA_BYTES: limit::ENCRYPTION_METADATA_BYTE_CEILING => max_encryption_metadata_bytes; KEY_CANDIDATES: limit::KEY_CANDIDATE_CEILING => max_key_candidates; KEY_IMPORT_KDF_WORK: key_import_work => max_key_import_kdf_work; KEY_IMPORT_KDF_MEMORY: limit::KEY_IMPORT_KDF_MEMORY_CEILING => max_key_import_kdf_memory_bytes; KEY_INFO_REFERENCE_DEPTH: limit::KEY_INFO_REFERENCE_DEPTH_CEILING => max_key_info_reference_depth; BASE64_TRANSFORM_INPUT_BYTES: limit::BASE64_TRANSFORM_INPUT_BYTE_CEILING => max_base64_transform_input_bytes; BASE64_TRANSFORM_OUTPUT_BYTES: limit::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING => max_base64_transform_output_bytes; XPATH_EXPRESSIONS: limit::XPATH_EXPRESSION_COUNT_CEILING => max_xpath_expressions; XPATH_EXPRESSION_BYTES: limit::XPATH_EXPRESSION_BYTE_CEILING => max_xpath_expression_bytes; XPATH_EXPRESSION_COMPLEXITY: limit::XPATH_EXPRESSION_COMPLEXITY_CEILING => max_xpath_expression_complexity; XPATH_CONTEXT_EVALUATIONS: limit::XPATH_CONTEXT_EVALUATION_CEILING => max_xpath_context_evaluations; XPATH_EVALUATION_WORK: limit::XPATH_EVALUATION_WORK_CEILING => max_xpath_evaluation_work; XPATH_MIRROR_STRING_BYTES: limit::XPATH_MIRROR_STRING_BYTE_CEILING => max_xpath_mirror_string_bytes; XPATH_STRING_WORK_BYTES: limit::XPATH_STRING_WORK_BYTE_CEILING => max_xpath_string_work_bytes; XPATH_NAMESPACE_BINDINGS: limit::XPATH_NAMESPACE_BINDING_CEILING => max_xpath_namespace_bindings; XPATH_NAMESPACE_BYTES: limit::XPATH_NAMESPACE_BYTE_CEILING => max_xpath_namespace_bytes; XPATH_FILTERS: limit::XPATH_FILTER_COUNT_CEILING => max_xpath_filters; NODE_SET_FILTER_WORK: limit::NODE_SET_FILTER_WORK_CEILING => max_node_set_filter_work; OPC_PARAMETER_BYTES: limit::OPC_PARAMETER_BYTE_CEILING => max_opc_parameter_bytes; OPC_WORKSPACE_BYTES: limit::OPC_WORKSPACE_BYTE_CEILING => max_opc_workspace_bytes; NODE_SET_ENTRIES: limit::NODE_SET_ENTRY_CEILING => max_node_set_entries; NODE_SET_OWNED_STRING_BYTES: limit::NODE_SET_OWNED_STRING_BYTE_CEILING => max_node_set_owned_string_bytes; NODE_SET_CUMULATIVE_OWNED_STRING_BYTES: limit::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING => max_node_set_cumulative_owned_string_bytes; XSLT_WORK: limit::XSLT_WORK_CEILING => max_xslt_work; XSLT_WORKSPACE_BYTES: limit::XSLT_WORKSPACE_BYTE_CEILING => max_xslt_workspace_bytes; XSLT_OUTPUT_BYTES: limit::XSLT_OUTPUT_BYTE_CEILING => max_xslt_output_bytes; XSLT_RECURSION_DEPTH: limit::XSLT_RECURSION_DEPTH_CEILING => max_xslt_recursion_depth; }; for &(resource, ceiling, field) in cases { let mut policy = ResourcePolicy::default(); let actual = ceiling.saturating_add(1); *field(&mut policy) = actual; assert_eq!( policy.validate(), Err(PolicyViolation::ResourceLimit { resource, maximum: ceiling, actual, }), "wrong hard-ceiling validation for {resource}", ); } } #[test] fn resource_policy_accepts_zero_as_a_deny_all_ceiling() { // Zero is a valid policy decision for resources that an operation can // avoid consuming; runtime checks must reject only actual non-zero use. let policy = ResourcePolicy { max_xml_nodes: 0, max_xml_depth: 0, max_xml_namespace_bindings: 0, max_references: 0, max_signatures: 0, max_transforms_per_reference: 0, max_xml_base_components: 0, max_xml_base_resolution_bytes: 0, max_canonicalized_bytes: 0, max_external_resource_bytes: 0, max_external_resource_total_bytes: 0, max_detached_payload_bytes: 0, max_detached_payload_total_bytes: 0, max_encryption_plaintext_bytes: 0, max_xml_document_bytes: 0, max_xml_parse_work_bytes: 0, max_encryption_recipients: 0, max_encryption_metadata_bytes: 0, max_key_candidates: 0, max_x509_work: 0, max_x509_workspace_bytes: 0, max_key_import_kdf_work: 0, max_key_import_kdf_memory_bytes: 0, max_key_info_reference_depth: 0, max_base64_transform_input_bytes: 0, max_base64_transform_output_bytes: 0, max_xpath_expressions: 0, max_xpath_expression_bytes: 0, max_xpath_expression_complexity: 0, max_xpath_context_evaluations: 0, max_xpath_evaluation_work: 0, max_xpath_mirror_string_bytes: 0, max_xpath_string_work_bytes: 0, max_xpath_namespace_bindings: 0, max_xpath_namespace_bytes: 0, max_xpath_filters: 0, max_node_set_filter_work: 0, max_node_set_entries: 0, max_node_set_owned_string_bytes: 0, max_node_set_cumulative_owned_string_bytes: 0, max_opc_workspace_bytes: 0, max_opc_parameter_bytes: 0, max_xslt_work: 0, max_xslt_workspace_bytes: 0, max_xslt_output_bytes: 0, max_xslt_recursion_depth: 0, }; assert_eq!(policy.validate(), Ok(())); } #[cfg(any(feature = "xmldsig", feature = "xmlenc"))] #[test] fn node_set_policy_has_headroom_above_its_default() { let mut policy = ResourcePolicy::default(); assert_eq!(policy.max_node_set_entries, 262_144); assert_eq!(policy.validate(), Ok(())); policy.max_node_set_entries = 524_288; assert_eq!(policy.validate(), Ok(())); policy.max_node_set_entries = 524_289; assert_eq!( policy.validate(), Err(PolicyViolation::ResourceLimit { resource: resource_name::NODE_SET_ENTRIES, maximum: 524_288, actual: 524_289, }) ); } #[cfg(any(feature = "xmldsig", feature = "xmlenc"))] #[test] fn rsa_key_policy_enforces_structure_range_and_explicit_relaxation() { let secure = RsaKeyPolicy::default(); assert!(matches!( secure.validate_components("test", &[0x80; 128], &[1, 0, 1]), Err(PolicyViolation::KeySize { minimum_bits: 2048, maximum_bits: 8192, actual_bits: 1024, .. }) )); let mut short_2048_width = [0_u8; 256]; short_2048_width[0] = 1; assert!(matches!( secure.validate_components("test", &short_2048_width, &[1, 0, 1]), Err(PolicyViolation::KeySize { minimum_bits: 2048, actual_bits: 2041, .. }) )); assert!(matches!( secure.validate_components("test", &[1; 1025], &[1, 0, 1]), Err(PolicyViolation::KeySize { actual_bits: 8193, .. }) )); assert!(matches!( secure.validate_components("test", &[0x80; 256], &[2]), Err(PolicyViolation::InvalidKeyMaterial { .. }) )); assert_eq!( secure.validate_components("test", &[0x80; 256], &[0x80, 0, 0, 1]), Ok(256), "normalized RSA components encode the exponent as unsigned bytes" ); let compatibility = RsaKeyPolicy { minimum_modulus_bits: 1024, }; assert_eq!( compatibility.validate_components("test", &[0x80; 128], &[1, 0, 1]), Ok(128) ); assert!( RsaKeyPolicy { minimum_modulus_bits: 2047, } .validate() .is_err() ); } #[cfg(feature = "xmldsig")] #[test] fn mandatory_x509_limits_report_the_nonzero_requirement() { // A lower-bound violation must not be reported as exceeding the upper // ceiling: that diagnostic points callers toward the wrong correction. let policy = KeyTrustPolicy { max_x509_chain_depth: 0, ..KeyTrustPolicy::default() }; let error = policy.validate().expect_err("zero depth must be rejected"); assert!( error.to_string().contains("must be nonzero"), "unexpected lower-bound diagnostic: {error}" ); } #[cfg(feature = "xmldsig")] #[test] fn custom_extended_key_purposes_require_valid_oid_arcs() { // The typed policy rejects impossible OIDs when the immutable snapshot // is validated instead of silently making the purpose unmatchable. let mut policy = KeyTrustPolicy::default(); policy .allowed_extended_key_usages .insert(ExtendedKeyPurpose::Other(vec![1, 40, 7])); assert!(matches!( policy.validate(), Err(PolicyViolation::KeyTrust { reason: "custom extended key purposes must contain valid OID arcs", }) )); } #[cfg(feature = "xmldsig")] #[test] fn crl_checking_requires_x509_chain_validation() { // CRLs authenticate through the validated issuer path. Accepting this // combination would advertise a security control the resolver skips. let policy = KeyTrustPolicy { check_crls: true, ..KeyTrustPolicy::default() }; assert!(matches!( policy.validate(), Err(PolicyViolation::KeyTrust { reason: "CRL checking requires X.509 chain validation" }) )); } #[cfg(feature = "xmldsig")] #[test] fn legacy_signature_algorithms_require_independent_policy_opt_ins() { let legacy = [ SignatureAlgorithm::RsaSha1, SignatureAlgorithm::DsaSha1, SignatureAlgorithm::HmacSha1, SignatureAlgorithm::EcdsaSha1, ]; let mut policy = VerificationPolicy::default(); for algorithm in legacy { assert!(matches!( policy.check_signature_algorithm(algorithm), Err(PolicyViolation::Algorithm { .. }) )); policy .key_trust .allowed_legacy_signature_algorithms .insert(algorithm); assert_eq!(policy.check_signature_algorithm(algorithm), Ok(())); policy .key_trust .allowed_legacy_signature_algorithms .remove(&algorithm); } } #[cfg(feature = "xmldsig")] #[test] fn dsa_key_policy_enforces_configured_minimum_and_hard_ceiling() { let policy = DsaKeyPolicy::default(); assert!(matches!( policy.validate_modulus_bits(1024), Err(PolicyViolation::KeySize { key_type: "DSA", minimum_bits: 2048, maximum_bits: 3072, actual_bits: 1024, .. }) )); assert_eq!(policy.validate_modulus_bits(2048), Ok(())); assert!(matches!( policy.validate_modulus_bits(4096), Err(PolicyViolation::KeySize { key_type: "DSA", minimum_bits: 2048, maximum_bits: 3072, actual_bits: 4096, .. }) )); } #[cfg(feature = "xmldsig")] #[test] fn hmac_output_policy_cannot_weaken_the_xmldsig_floor() { // Caller policy may tighten but cannot weaken XMLDSig section 4.4.2: // truncation is at least 80 bits and at least half the digest width. let compatibility = HmacPolicy { minimum_key_bits: 40, minimum_output_bits: 40, }; assert_eq!( compatibility.validate_output(SignatureAlgorithm::HmacSha1, 80), Ok(()) ); assert!(matches!( compatibility.validate_output(SignatureAlgorithm::HmacSha1, 72), Err(PolicyViolation::HmacOutputLength { minimum: 80, .. }) )); assert!(matches!( compatibility.validate_output(SignatureAlgorithm::HmacSha256, 120), Err(PolicyViolation::HmacOutputLength { minimum: 128, .. }) )); } #[cfg(feature = "xmldsig")] #[test] fn documented_xmldsig_limits_match_hard_limits() { // Public deployment guidance must change in the same commit as the // implementation ceilings from which these values are derived. let docs = include_str!("../docs/xmldsig.md"); let mib = 1024 * 1024; assert!(docs.contains(&format!( "Individual resources are limited to {} MiB", crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING / mib ))); assert!(docs.contains(&format!( "complete map to {} MiB", crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING / mib ))); assert!(docs.contains(&format!( "ceilings are {} components and {} MiB per operation", crate::hard_limits::XML_BASE_COMPONENT_CEILING, crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING / mib ))); } }