#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
use std::collections::HashSet;
#[cfg(feature = "xmldsig")]
use std::time::SystemTime;
#[cfg(feature = "xmldsig")]
use crate::xmldsig::{DigestAlgorithm, SignatureAlgorithm, UriTypeSet, XPathHereSemantics};
#[cfg(feature = "xmlenc")]
use crate::xmlenc::{
DataEncryptionAlgorithm, KeyTransportAlgorithm, KeyWrapAlgorithm, OaepDigestAlgorithm,
};
#[cfg(feature = "xmldsig")]
mod key_establishment;
#[cfg(feature = "xmldsig")]
pub use key_establishment::{
KemContentAuthentication, KeyAgreementAlgorithm, KeyDerivationAlgorithm, KeyEstablishmentPolicy,
};
#[cfg(feature = "xmldsig")]
mod profiles;
#[cfg(feature = "xmldsig")]
pub use profiles::{
CompiledPolicies, PolicyCompileContext, PolicyCompiler, PolicyProfile, PolicySet,
PolicyValidity, SignatureConstraints,
};
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RsaPssPermission {
#[default]
ExactOnly,
SecureSupported,
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()))
}
}
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 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 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";
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum PolicyViolation {
#[error("policy compilation failed: {reason}")]
Configuration {
reason: &'static str,
},
#[cfg(feature = "xmldsig")]
#[error("Manifest reference processing is disabled by policy")]
ManifestProcessingDisabled,
#[cfg(feature = "xmldsig")]
#[error("{profile:?} signature profile rejects document: {reason}")]
SignatureProfile {
profile: SignatureConstraints,
reason: &'static str,
},
#[error("policy authorization is outside its validity interval")]
AuthorizationExpired,
#[error("{operation} policy rejects algorithm {algorithm}")]
Algorithm {
operation: &'static str,
algorithm: String,
},
#[cfg(feature = "xmldsig")]
#[error("HMAC output length must be between {minimum} and {maximum} bits: got {actual}")]
HmacOutputLength {
minimum: usize,
maximum: usize,
actual: usize,
},
#[error("{resource} exceeds policy maximum {maximum}: got {actual}")]
ResourceLimit {
resource: &'static str,
maximum: usize,
actual: usize,
},
#[error("{resource} exceeds policy maximum {maximum}")]
ResourceLimitExceeded {
resource: &'static str,
maximum: usize,
},
#[error("key import KDF iterations must be between 1 and {maximum}")]
KdfIterationsOutsideLimit {
maximum: usize,
},
#[error("{resource} has invalid policy limit {actual}: {requirement}")]
InvalidResourceLimit {
resource: &'static str,
requirement: &'static str,
actual: usize,
},
#[error("key/trust policy rejected the operation: {reason}")]
KeyTrust {
reason: &'static str,
},
#[error("XML input policy rejected the operation: {reason}")]
XmlInput {
reason: &'static str,
},
#[error("{operation} URI policy rejected the operation: {reason}")]
Uri {
operation: &'static str,
reason: &'static str,
},
#[error(
"{operation} policy requires {key_type} keys between {minimum_bits} and {maximum_bits} bits: got {actual_bits}"
)]
KeySize {
operation: &'static str,
key_type: &'static str,
minimum_bits: usize,
maximum_bits: usize,
actual_bits: usize,
},
#[error("{operation} policy rejects invalid {key_type} key material: {reason}")]
InvalidKeyMaterial {
operation: &'static str,
key_type: &'static str,
reason: &'static str,
},
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HmacPolicy {
pub minimum_key_bits: usize,
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(())
}
pub(crate) 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(())
}
pub(crate) 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(),
})?;
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(())
}
}
#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RsaKeyPolicy {
pub minimum_modulus_bits: usize,
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DsaKeyPolicy {
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 {
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 {
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<usize, PolicyViolation> {
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<u64>,
) -> Result<usize, PolicyViolation> {
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)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourcePolicy {
pub max_xml_nodes: usize,
pub max_xml_depth: usize,
pub max_xml_namespace_bindings: usize,
pub max_references: usize,
pub max_signatures: usize,
pub max_transforms_per_reference: usize,
pub max_xml_base_components: usize,
pub max_xml_base_resolution_bytes: usize,
pub max_canonicalized_bytes: usize,
pub max_external_resource_bytes: usize,
pub max_external_resource_total_bytes: usize,
pub max_encryption_plaintext_bytes: usize,
pub max_xml_document_bytes: usize,
pub max_xml_parse_work_bytes: usize,
pub max_encryption_recipients: usize,
pub max_encryption_metadata_bytes: usize,
pub max_key_candidates: usize,
pub max_x509_work: usize,
pub max_x509_workspace_bytes: usize,
pub max_key_import_kdf_work: usize,
pub max_key_import_kdf_memory_bytes: usize,
pub max_key_info_reference_depth: usize,
pub max_base64_transform_input_bytes: usize,
pub max_base64_transform_output_bytes: usize,
pub max_xpath_expressions: usize,
pub max_xpath_expression_bytes: usize,
pub max_xpath_expression_complexity: usize,
pub max_xpath_context_evaluations: usize,
pub max_xpath_evaluation_work: usize,
pub max_xpath_mirror_string_bytes: usize,
pub max_xpath_string_work_bytes: usize,
pub max_xpath_namespace_bindings: usize,
pub max_xpath_namespace_bytes: usize,
pub max_xpath_filters: usize,
pub max_node_set_filter_work: usize,
pub max_node_set_entries: usize,
pub max_node_set_owned_string_bytes: usize,
pub max_node_set_cumulative_owned_string_bytes: usize,
pub max_opc_parameter_bytes: usize,
pub max_opc_workspace_bytes: 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_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: crate::hard_limits::KEY_IMPORT_KDF_WORK_CEILING as usize,
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,
}
}
}
impl ResourcePolicy {
pub fn validate(&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::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,
)?;
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,
crate::hard_limits::KEY_IMPORT_KDF_WORK_CEILING as usize,
),
(
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,
),
(
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)?;
}
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,
)?;
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"))]
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(())
}
#[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)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct XmlInputPolicy {
pub allow_internal_dtd: bool,
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum OpcRelationshipEdition {
Ecma2012,
#[default]
Ecma2021,
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SameDocumentIdSemantics {
#[default]
Specification,
XmlSecBarename,
XmlSecVisa3d,
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum EcdsaSignatureValueEncoding {
#[default]
XmlDsig,
XmlSecAsn1Der,
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TransformPolicy {
pub allowed_algorithms: Option<HashSet<String>>,
pub xpath_here_semantics: XPathHereSemantics,
pub same_document_id_semantics: SameDocumentIdSemantics,
pub opc_relationship_edition: OpcRelationshipEdition,
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UriPolicy {
pub references: UriTypeSet,
pub retrieval_methods: UriTypeSet,
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,
}
}
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeySourcePolicy {
pub preset_key: bool,
pub key_name: bool,
pub key_value: bool,
pub der_encoded_key_value: bool,
pub x509_data: bool,
pub key_info_reference: bool,
}
#[cfg(feature = "xmldsig")]
impl Default for KeySourcePolicy {
fn default() -> Self {
Self {
preset_key: true,
key_name: true,
key_value: true,
der_encoded_key_value: true,
x509_data: true,
key_info_reference: true,
}
}
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ExtendedKeyPurpose {
ServerAuth,
ClientAuth,
CodeSigning,
EmailProtection,
TimeStamping,
OcspSigning,
Other(Vec<u64>),
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum VerificationTrustMode {
#[default]
RequireTrustedKey,
CryptographicOnly,
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyTrustPolicy {
pub mode: VerificationTrustMode,
pub verify_x509_chains: bool,
pub max_x509_chain_depth: usize,
pub max_x509_candidate_paths: usize,
pub allowed_legacy_signature_algorithms: HashSet<SignatureAlgorithm>,
pub parameterized_rsa_pss: RsaPssPermission,
pub certificate_signature_algorithms: Option<CertificateSignatureAlgorithms>,
pub rsa_keys: RsaKeyPolicy,
pub dsa_keys: DsaKeyPolicy,
pub allowed_extended_key_usages: HashSet<ExtendedKeyPurpose>,
pub check_crls: bool,
pub verification_time: Option<SystemTime>,
}
#[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: HashSet::new(),
parameterized_rsa_pss: RsaPssPermission::ExactOnly,
certificate_signature_algorithms: None,
rsa_keys: RsaKeyPolicy::default(),
dsa_keys: DsaKeyPolicy::default(),
allowed_extended_key_usages: HashSet::new(),
check_crls: false,
verification_time: None,
}
}
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CertificateSignatureAlgorithms {
AllSupported,
Allowlist(HashSet<crate::provider::X509SignatureAlgorithm>),
}
#[cfg(feature = "xmldsig")]
impl KeyTrustPolicy {
pub(crate) fn check_certificate_signature_algorithm(
algorithm: crate::provider::X509SignatureAlgorithm,
allowed: Option<&CertificateSignatureAlgorithms>,
) -> Result<(), PolicyViolation> {
let permitted = match allowed {
Some(CertificateSignatureAlgorithms::AllSupported) => true,
Some(CertificateSignatureAlgorithms::Allowlist(allowed)) => {
allowed.contains(&algorithm)
}
None => !matches!(
algorithm,
crate::provider::X509SignatureAlgorithm::PostQuantum(_)
),
};
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,
)
}
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ManifestProcessing {
#[default]
Ignore,
Process,
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Default)]
pub struct VerificationPolicy {
pub validity: Option<PolicyValidity>,
pub signature_constraints: SignatureConstraints,
pub key_establishment: KeyEstablishmentPolicy,
pub parameterized_rsa_pss: RsaPssPermission,
pub signature_algorithms: Option<HashSet<SignatureAlgorithm>>,
pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
pub hmac: HmacPolicy,
pub ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding,
pub key_trust: KeyTrustPolicy,
pub key_sources: KeySourcePolicy,
pub uris: UriPolicy,
pub transforms: TransformPolicy,
pub manifest_processing: ManifestProcessing,
pub xml: XmlInputPolicy,
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(),
})
}
}
pub fn validate(&self) -> Result<(), PolicyViolation> {
if let Some(validity) = &self.validity {
validity.validate_at(SystemTime::now())?;
}
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()
}
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(())
}
}
#[cfg(feature = "xmldsig")]
#[derive(Debug, Clone, Default)]
pub struct SigningPolicy {
pub validity: Option<PolicyValidity>,
pub signature_constraints: SignatureConstraints,
pub key_establishment: KeyEstablishmentPolicy,
pub parameterized_rsa_pss: RsaPssPermission,
pub signature_algorithms: Option<HashSet<SignatureAlgorithm>>,
pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
pub hmac: HmacPolicy,
pub ecdsa_signature_value_encoding: EcdsaSignatureValueEncoding,
pub rsa_keys: RsaKeyPolicy,
pub dsa_keys: DsaKeyPolicy,
pub uris: UriPolicy,
pub transforms: TransformPolicy,
pub manifest_processing: ManifestProcessing,
pub xml: XmlInputPolicy,
pub resources: ResourcePolicy,
}
#[cfg(feature = "xmldsig")]
impl SigningPolicy {
pub fn validate(&self) -> Result<(), PolicyViolation> {
if let Some(validity) = &self.validity {
validity.validate_at(SystemTime::now())?;
}
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<T: Eq + std::hash::Hash>(
allowlist: Option<&HashSet<T>>,
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(),
})
}
}
#[cfg(feature = "xmlenc")]
pub(crate) fn check_content_algorithm(
allowlist: Option<&HashSet<DataEncryptionAlgorithm>>,
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(),
})
}
}
#[cfg(feature = "xmlenc")]
#[derive(Debug, Clone, Default)]
pub struct EncryptionPolicy {
pub validity: Option<PolicyValidity>,
pub data_algorithms: Option<HashSet<DataEncryptionAlgorithm>>,
pub key_transport_algorithms: Option<HashSet<KeyTransportAlgorithm>>,
pub key_wrap_algorithms: Option<HashSet<KeyWrapAlgorithm>>,
pub oaep_digests: Option<HashSet<OaepDigestAlgorithm>>,
pub rsa_keys: RsaKeyPolicy,
pub key_establishment: KeyEstablishmentPolicy,
pub xml: XmlInputPolicy,
pub resources: ResourcePolicy,
}
#[cfg(feature = "xmlenc")]
impl EncryptionPolicy {
pub fn validate(&self) -> Result<(), PolicyViolation> {
if let Some(validity) = &self.validity {
validity.validate_at(SystemTime::now())?;
}
self.resources.validate()?;
self.key_establishment.validate()?;
self.rsa_keys.validate()
}
}
#[cfg(feature = "xmlenc")]
#[derive(Debug, Clone, Default)]
pub struct DecryptionPolicy {
pub validity: Option<PolicyValidity>,
pub uris: UriPolicy,
pub transforms: TransformPolicy,
pub data_algorithms: Option<HashSet<DataEncryptionAlgorithm>>,
pub key_transport_algorithms: Option<HashSet<KeyTransportAlgorithm>>,
pub key_wrap_algorithms: Option<HashSet<KeyWrapAlgorithm>>,
pub oaep_digests: Option<HashSet<OaepDigestAlgorithm>>,
pub rsa_keys: RsaKeyPolicy,
pub key_establishment: KeyEstablishmentPolicy,
pub xml: XmlInputPolicy,
pub resources: ResourcePolicy,
}
#[cfg(feature = "xmlenc")]
impl DecryptionPolicy {
pub fn validate(&self) -> Result<(), PolicyViolation> {
if let Some(validity) = &self.validity {
validity.validate_at(SystemTime::now())?;
}
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() {
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(HashSet::from([algorithm])),
..VerificationPolicy::default()
};
let signing = SigningPolicy {
digest_algorithms: Some(HashSet::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};
let exact = X509SignatureAlgorithm::RsaPss {
digest: DigestAlgorithm::Sha256,
mgf_digest: DigestAlgorithm::Sha256,
salt_len: 32,
};
let allowed = CertificateSignatureAlgorithms::Allowlist(HashSet::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() {
type Case = (&'static str, usize, fn(&mut ResourcePolicy) -> &mut usize);
let cases: &[Case] = &[
(
resource_name::X509_WORK,
crate::hard_limits::X509_WORK_CEILING,
|p| &mut p.max_x509_work,
),
(
resource_name::X509_WORKSPACE_BYTES,
crate::hard_limits::X509_WORKSPACE_BYTE_CEILING,
|p| &mut p.max_x509_workspace_bytes,
),
(
resource_name::XML_NODES,
crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize,
|p| &mut p.max_xml_nodes,
),
(
resource_name::XML_DEPTH,
crate::hard_limits::XML_DOCUMENT_DEPTH_CEILING,
|p| &mut p.max_xml_depth,
),
(
resource_name::XML_NAMESPACE_BINDINGS,
crate::hard_limits::XML_NAMESPACE_BINDING_CEILING,
|p| &mut p.max_xml_namespace_bindings,
),
(
resource_name::SIGNATURE_REFERENCES,
crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
|p| &mut p.max_references,
),
(
resource_name::VERIFICATION_SIGNATURES,
crate::hard_limits::VERIFICATION_SIGNATURE_CEILING,
|p| &mut p.max_signatures,
),
(
resource_name::REFERENCE_TRANSFORMS,
crate::hard_limits::REFERENCE_TRANSFORM_CEILING,
|p| &mut p.max_transforms_per_reference,
),
(
resource_name::XML_BASE_COMPONENTS,
crate::hard_limits::XML_BASE_COMPONENT_CEILING,
|p| &mut p.max_xml_base_components,
),
(
resource_name::XML_BASE_RESOLUTION_BYTES,
crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING,
|p| &mut p.max_xml_base_resolution_bytes,
),
(
resource_name::CANONICALIZED_BYTES,
crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
|p| &mut p.max_canonicalized_bytes,
),
(
resource_name::EXTERNAL_RESOURCE_BYTES,
crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
|p| &mut p.max_external_resource_bytes,
),
(
resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
|p| &mut p.max_external_resource_total_bytes,
),
(
resource_name::ENCRYPTION_PLAINTEXT_BYTES,
crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
|p| &mut p.max_encryption_plaintext_bytes,
),
(
resource_name::XML_DOCUMENT,
crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
|p| &mut p.max_xml_document_bytes,
),
(
resource_name::XML_PARSE_WORK_BYTES,
crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING,
|p| &mut p.max_xml_parse_work_bytes,
),
(
resource_name::ENCRYPTION_RECIPIENTS,
crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING,
|p| &mut p.max_encryption_recipients,
),
(
resource_name::ENCRYPTION_METADATA_BYTES,
crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING,
|p| &mut p.max_encryption_metadata_bytes,
),
(
resource_name::KEY_CANDIDATES,
crate::hard_limits::KEY_CANDIDATE_CEILING,
|p| &mut p.max_key_candidates,
),
(
resource_name::KEY_IMPORT_KDF_WORK,
crate::hard_limits::KEY_IMPORT_KDF_WORK_CEILING as usize,
|p| &mut p.max_key_import_kdf_work,
),
(
resource_name::KEY_IMPORT_KDF_MEMORY,
crate::hard_limits::KEY_IMPORT_KDF_MEMORY_CEILING,
|p| &mut p.max_key_import_kdf_memory_bytes,
),
(
resource_name::KEY_INFO_REFERENCE_DEPTH,
crate::hard_limits::KEY_INFO_REFERENCE_DEPTH_CEILING,
|p| &mut p.max_key_info_reference_depth,
),
(
resource_name::BASE64_TRANSFORM_INPUT_BYTES,
crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING,
|p| &mut p.max_base64_transform_input_bytes,
),
(
resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING,
|p| &mut p.max_base64_transform_output_bytes,
),
(
resource_name::XPATH_EXPRESSIONS,
crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING,
|p| &mut p.max_xpath_expressions,
),
(
resource_name::XPATH_EXPRESSION_BYTES,
crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING,
|p| &mut p.max_xpath_expression_bytes,
),
(
resource_name::XPATH_EXPRESSION_COMPLEXITY,
crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
|p| &mut p.max_xpath_expression_complexity,
),
(
resource_name::XPATH_CONTEXT_EVALUATIONS,
crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING,
|p| &mut p.max_xpath_context_evaluations,
),
(
resource_name::XPATH_EVALUATION_WORK,
crate::hard_limits::XPATH_EVALUATION_WORK_CEILING,
|p| &mut p.max_xpath_evaluation_work,
),
(
resource_name::XPATH_MIRROR_STRING_BYTES,
crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING,
|p| &mut p.max_xpath_mirror_string_bytes,
),
(
resource_name::XPATH_STRING_WORK_BYTES,
crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING,
|p| &mut p.max_xpath_string_work_bytes,
),
(
resource_name::XPATH_NAMESPACE_BINDINGS,
crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING,
|p| &mut p.max_xpath_namespace_bindings,
),
(
resource_name::XPATH_NAMESPACE_BYTES,
crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING,
|p| &mut p.max_xpath_namespace_bytes,
),
(
resource_name::XPATH_FILTERS,
crate::hard_limits::XPATH_FILTER_COUNT_CEILING,
|p| &mut p.max_xpath_filters,
),
(
resource_name::NODE_SET_FILTER_WORK,
crate::hard_limits::NODE_SET_FILTER_WORK_CEILING,
|p| &mut p.max_node_set_filter_work,
),
(
resource_name::OPC_PARAMETER_BYTES,
crate::hard_limits::OPC_PARAMETER_BYTE_CEILING,
|p| &mut p.max_opc_parameter_bytes,
),
(
resource_name::OPC_WORKSPACE_BYTES,
crate::hard_limits::OPC_WORKSPACE_BYTE_CEILING,
|p| &mut p.max_opc_workspace_bytes,
),
(
resource_name::NODE_SET_ENTRIES,
crate::hard_limits::NODE_SET_ENTRY_CEILING,
|p| &mut p.max_node_set_entries,
),
(
resource_name::NODE_SET_OWNED_STRING_BYTES,
crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING,
|p| &mut p.max_node_set_owned_string_bytes,
),
(
resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING,
|p| &mut p.max_node_set_cumulative_owned_string_bytes,
),
];
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() {
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_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,
};
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() {
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() {
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() {
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() {
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() {
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
)));
}
}