use crate::xml::dom::{Document, Node};
use der::{Decode, asn1::ObjectIdentifier};
use std::collections::BTreeMap;
#[cfg(test)]
use super::digest::compute_digest;
use super::digest::{DigestAlgorithm, compute_digest_with_provider, constant_time_eq};
use super::pkix::X509DataSource;
use super::transforms::{self, Transform};
use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_text};
use super::x509::certificate_signature_matches_with_provider;
use crate::c14n::C14nAlgorithm;
use crate::c14n::xml_base::{
XmlBaseResolutionBudget, resolve_uri_from_node_with_document_base_with_budget,
};
pub(crate) use crate::hard_limits::SIGNATURE_REFERENCE_CEILING as MAX_REFERENCES_PER_SIGNATURE;
#[cfg(test)]
use crate::hard_limits::X509_CHAIN_DEPTH_CEILING as MAX_X509_CHAIN_DEPTH;
pub(crate) const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
pub(crate) const XMLDSIG11_NS: &str = "http://www.w3.org/2009/xmldsig11#";
const MAX_DER_ENCODED_KEY_VALUE_LEN: usize = 8192;
const MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN: usize = 65_536;
const MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN: usize = MAX_DER_ENCODED_KEY_VALUE_LEN.div_ceil(3) * 4;
pub(crate) const MAX_KEY_NAME_TEXT_LEN: usize = 4096;
const MAX_KEY_INFO_CHILD_COUNT: usize = 64;
const MAX_HMAC_OUTPUT_LENGTH_TEXT_LEN: usize = 32;
pub(crate) const EXPERIMENTAL_SIGNATURE_NS: &str =
"http://www.aleksey.com/xmlsec/2025/12/xmldsig-more#";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SignatureContext(Vec<u8>);
impl SignatureContext {
pub fn new(bytes: &[u8]) -> Result<Self, ParseError> {
if bytes.len() > 255 {
return Err(ParseError::InvalidStructure(
"signature context exceeds 255 octets".into(),
));
}
Ok(Self(bytes.to_vec()))
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
const MAX_RETRIEVAL_XPATH_TEXT_LEN: usize = 256;
const MAX_RSA_MODULUS_LEN: usize = 1024;
const MAX_RSA_EXPONENT_LEN: usize = 8;
pub(crate) const EC_P256_OID: &str = "1.2.840.10045.3.1.7";
pub(crate) const EC_P384_OID: &str = "1.3.132.0.34";
pub(crate) const EC_P521_OID: &str = "1.3.132.0.35";
const MAX_EC_PUBLIC_KEY_LEN: usize = 133;
const MAX_X509_BASE64_TEXT_LEN: usize = 262_144;
const MAX_X509_BASE64_NORMALIZED_LEN: usize = MAX_X509_BASE64_TEXT_LEN;
pub(crate) const MAX_X509_DECODED_BINARY_LEN: usize =
MAX_X509_BASE64_NORMALIZED_LEN.div_ceil(4) * 3;
const MAX_X509_SUBJECT_NAME_TEXT_LEN: usize = 16_384;
const MAX_X509_ISSUER_NAME_TEXT_LEN: usize = 16_384;
const MAX_X509_SERIAL_NUMBER_RAW_TEXT_LEN: usize = 16_384;
const MAX_X509_SERIAL_NUMBER_VALUE_DIGITS: usize = 49;
const MAX_X509_SERIAL_NUMBER_BYTES: usize = 20;
const MAX_X509_DATA_ENTRY_COUNT: usize = 64;
pub(crate) const MAX_X509_DATA_TOTAL_BINARY_LEN: usize = 1_048_576;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RsaPssParameters {
pub digest: DigestAlgorithm,
pub mgf_digest: DigestAlgorithm,
pub salt_len: usize,
}
impl Default for RsaPssParameters {
fn default() -> Self {
Self::DEFAULT
}
}
impl RsaPssParameters {
pub const DEFAULT: Self = Self {
digest: DigestAlgorithm::Sha256,
mgf_digest: DigestAlgorithm::Sha256,
salt_len: 32,
};
pub(crate) fn fits_modulus_bits(self, bits: usize) -> bool {
if bits < 2 || self.salt_len > i32::MAX as usize {
return false;
}
let em_len = (bits - 1).div_ceil(8);
let hash_len = self.digest.output_len();
em_len >= hash_len + 2 && self.salt_len <= em_len - hash_len - 2
}
}
pub(crate) const RSA_PSS_NS: &str = "http://www.w3.org/2007/05/xmldsig-more#";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SignatureAlgorithm {
#[cfg(feature = "legacy-algorithms")]
RsaMd5,
#[cfg(feature = "legacy-algorithms")]
RsaRipemd160,
#[cfg(feature = "legacy-algorithms")]
HmacMd5,
#[cfg(feature = "legacy-algorithms")]
HmacRipemd160,
#[cfg(feature = "legacy-algorithms")]
EcdsaRipemd160,
DsaSha1,
DsaSha256,
HmacSha1,
HmacSha224,
HmacSha256,
HmacSha384,
HmacSha512,
RsaSha1,
RsaSha224,
RsaSha256,
RsaSha384,
RsaSha512,
RsaPssSha1,
RsaPssSha224,
RsaPssSha256,
RsaPssSha384,
RsaPssSha512,
RsaPssSha3_224,
RsaPssSha3_256,
RsaPssSha3_384,
RsaPssSha3_512,
RsaPss(RsaPssParameters),
EcdsaSha1,
EcdsaSha224,
EcdsaSha256,
EcdsaSha384,
EcdsaSha512,
EcdsaSha3_224,
EcdsaSha3_256,
EcdsaSha3_384,
EcdsaSha3_512,
Ed25519,
Ed25519Ctx,
Ed25519Ph,
Ed448,
Ed448Ph,
PostQuantum(super::PqAlgorithm),
}
impl SignatureAlgorithm {
pub fn rsa_pss_parameters(self) -> Option<RsaPssParameters> {
let digest = match self {
Self::RsaPss(parameters) => return Some(parameters),
Self::RsaPssSha1 => DigestAlgorithm::Sha1,
Self::RsaPssSha224 => DigestAlgorithm::Sha224,
Self::RsaPssSha256 => DigestAlgorithm::Sha256,
Self::RsaPssSha384 => DigestAlgorithm::Sha384,
Self::RsaPssSha512 => DigestAlgorithm::Sha512,
Self::RsaPssSha3_224 => DigestAlgorithm::Sha3_224,
Self::RsaPssSha3_256 => DigestAlgorithm::Sha3_256,
Self::RsaPssSha3_384 => DigestAlgorithm::Sha3_384,
Self::RsaPssSha3_512 => DigestAlgorithm::Sha3_512,
_ => return None,
};
Some(RsaPssParameters {
digest,
mgf_digest: digest,
salt_len: digest.output_len(),
})
}
pub fn uses_legacy_pss_hash(self) -> bool {
self.rsa_pss_parameters().is_some_and(|parameters| {
[parameters.digest, parameters.mgf_digest]
.into_iter()
.any(|digest| match digest {
DigestAlgorithm::Sha1 => true,
#[cfg(feature = "legacy-algorithms")]
DigestAlgorithm::Md5 | DigestAlgorithm::Ripemd160 => true,
_ => false,
})
})
}
pub const ALL: [Self;
45 + if cfg!(feature = "legacy-algorithms") {
5
} else {
0
}] = [
#[cfg(feature = "legacy-algorithms")]
Self::RsaMd5,
#[cfg(feature = "legacy-algorithms")]
Self::RsaRipemd160,
#[cfg(feature = "legacy-algorithms")]
Self::HmacMd5,
#[cfg(feature = "legacy-algorithms")]
Self::HmacRipemd160,
#[cfg(feature = "legacy-algorithms")]
Self::EcdsaRipemd160,
Self::DsaSha1,
Self::DsaSha256,
Self::HmacSha1,
Self::HmacSha224,
Self::HmacSha256,
Self::HmacSha384,
Self::HmacSha512,
Self::RsaSha1,
Self::RsaSha224,
Self::RsaSha256,
Self::RsaSha384,
Self::RsaSha512,
Self::RsaPssSha1,
Self::RsaPssSha224,
Self::RsaPssSha256,
Self::RsaPssSha384,
Self::RsaPssSha512,
Self::RsaPssSha3_224,
Self::RsaPssSha3_256,
Self::RsaPssSha3_384,
Self::RsaPssSha3_512,
Self::RsaPss(RsaPssParameters::DEFAULT),
Self::EcdsaSha1,
Self::EcdsaSha224,
Self::EcdsaSha256,
Self::EcdsaSha384,
Self::EcdsaSha512,
Self::EcdsaSha3_224,
Self::EcdsaSha3_256,
Self::EcdsaSha3_384,
Self::EcdsaSha3_512,
Self::Ed25519,
Self::Ed25519Ctx,
Self::Ed25519Ph,
Self::Ed448,
Self::Ed448Ph,
Self::PostQuantum(super::PqAlgorithm::MlDsa44),
Self::PostQuantum(super::PqAlgorithm::MlDsa65),
Self::PostQuantum(super::PqAlgorithm::MlDsa87),
Self::PostQuantum(super::PqAlgorithm::SlhDsaSha2_128f),
Self::PostQuantum(super::PqAlgorithm::SlhDsaSha2_128s),
Self::PostQuantum(super::PqAlgorithm::SlhDsaSha2_192f),
Self::PostQuantum(super::PqAlgorithm::SlhDsaSha2_192s),
Self::PostQuantum(super::PqAlgorithm::SlhDsaSha2_256f),
Self::PostQuantum(super::PqAlgorithm::SlhDsaSha2_256s),
];
pub const fn ecdsa_digest(self) -> Option<DigestAlgorithm> {
match self {
#[cfg(feature = "legacy-algorithms")]
Self::EcdsaRipemd160 => Some(DigestAlgorithm::Ripemd160),
Self::EcdsaSha1 => Some(DigestAlgorithm::Sha1),
Self::EcdsaSha224 => Some(DigestAlgorithm::Sha224),
Self::EcdsaSha256 => Some(DigestAlgorithm::Sha256),
Self::EcdsaSha384 => Some(DigestAlgorithm::Sha384),
Self::EcdsaSha512 => Some(DigestAlgorithm::Sha512),
Self::EcdsaSha3_224 => Some(DigestAlgorithm::Sha3_224),
Self::EcdsaSha3_256 => Some(DigestAlgorithm::Sha3_256),
Self::EcdsaSha3_384 => Some(DigestAlgorithm::Sha3_384),
Self::EcdsaSha3_512 => Some(DigestAlgorithm::Sha3_512),
_ => None,
}
}
pub(crate) const fn eddsa_signature_len(self) -> Option<usize> {
match self {
Self::Ed25519 | Self::Ed25519Ctx | Self::Ed25519Ph => Some(64),
Self::Ed448 | Self::Ed448Ph => Some(114),
_ => None,
}
}
pub(crate) const fn eddsa_key_algorithm(self) -> Option<Self> {
match self {
Self::Ed25519 | Self::Ed25519Ctx | Self::Ed25519Ph => Some(Self::Ed25519),
Self::Ed448 | Self::Ed448Ph => Some(Self::Ed448),
_ => None,
}
}
pub(crate) const fn context_element(self) -> Option<&'static str> {
match self {
Self::Ed25519Ctx | Self::Ed25519Ph | Self::Ed448 | Self::Ed448Ph => {
Some("EdDSAContextString")
}
Self::PostQuantum(algorithm) => Some(algorithm.context_element()),
_ => None,
}
}
pub(crate) const fn dsa_component_len(self) -> Option<usize> {
match self {
Self::DsaSha1 => Some(20),
Self::DsaSha256 => Some(32),
_ => None,
}
}
#[must_use]
pub const fn hmac_output_bits(self) -> Option<usize> {
match self {
#[cfg(feature = "legacy-algorithms")]
Self::HmacMd5 => Some(128),
#[cfg(feature = "legacy-algorithms")]
Self::HmacRipemd160 => Some(160),
Self::HmacSha1 => Some(160),
Self::HmacSha224 => Some(224),
Self::HmacSha256 => Some(256),
Self::HmacSha384 => Some(384),
Self::HmacSha512 => Some(512),
_ => None,
}
}
#[must_use]
pub fn from_uri(uri: &str) -> Option<Self> {
match uri {
"http://www.w3.org/2007/05/xmldsig-more#sha1-rsa-MGF1" => Some(Self::RsaPssSha1),
"http://www.w3.org/2007/05/xmldsig-more#sha224-rsa-MGF1" => Some(Self::RsaPssSha224),
"http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1" => Some(Self::RsaPssSha256),
"http://www.w3.org/2007/05/xmldsig-more#sha384-rsa-MGF1" => Some(Self::RsaPssSha384),
"http://www.w3.org/2007/05/xmldsig-more#sha512-rsa-MGF1" => Some(Self::RsaPssSha512),
"http://www.w3.org/2007/05/xmldsig-more#sha3-224-rsa-MGF1" => {
Some(Self::RsaPssSha3_224)
}
"http://www.w3.org/2007/05/xmldsig-more#sha3-256-rsa-MGF1" => {
Some(Self::RsaPssSha3_256)
}
"http://www.w3.org/2007/05/xmldsig-more#sha3-384-rsa-MGF1" => {
Some(Self::RsaPssSha3_384)
}
"http://www.w3.org/2007/05/xmldsig-more#sha3-512-rsa-MGF1" => {
Some(Self::RsaPssSha3_512)
}
"http://www.w3.org/2007/05/xmldsig-more#rsa-pss" => {
Some(Self::RsaPss(RsaPssParameters::DEFAULT))
}
#[cfg(feature = "legacy-algorithms")]
"http://www.w3.org/2001/04/xmldsig-more#rsa-md5" => Some(Self::RsaMd5),
#[cfg(feature = "legacy-algorithms")]
"http://www.w3.org/2001/04/xmldsig-more#rsa-ripemd160" => Some(Self::RsaRipemd160),
#[cfg(feature = "legacy-algorithms")]
"http://www.w3.org/2001/04/xmldsig-more#hmac-md5" => Some(Self::HmacMd5),
#[cfg(feature = "legacy-algorithms")]
"http://www.w3.org/2001/04/xmldsig-more#hmac-ripemd160" => Some(Self::HmacRipemd160),
#[cfg(feature = "legacy-algorithms")]
"http://www.w3.org/2007/05/xmldsig-more#ecdsa-ripemd160" => Some(Self::EcdsaRipemd160),
"http://www.w3.org/2000/09/xmldsig#dsa-sha1" => Some(Self::DsaSha1),
"http://www.w3.org/2009/xmldsig11#dsa-sha256" => Some(Self::DsaSha256),
"http://www.w3.org/2000/09/xmldsig#hmac-sha1" => Some(Self::HmacSha1),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha224" => Some(Self::HmacSha224),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256" => Some(Self::HmacSha256),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha384" => Some(Self::HmacSha384),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha512" => Some(Self::HmacSha512),
"http://www.w3.org/2000/09/xmldsig#rsa-sha1" => Some(Self::RsaSha1),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha224" => Some(Self::RsaSha224),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Some(Self::RsaSha256),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => Some(Self::RsaSha384),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => Some(Self::RsaSha512),
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1" => Some(Self::EcdsaSha1),
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224" => Some(Self::EcdsaSha224),
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Some(Self::EcdsaSha256),
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => Some(Self::EcdsaSha384),
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512" => Some(Self::EcdsaSha512),
"http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-224" => Some(Self::EcdsaSha3_224),
"http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-256" => Some(Self::EcdsaSha3_256),
"http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-384" => Some(Self::EcdsaSha3_384),
"http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-512" => Some(Self::EcdsaSha3_512),
"http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519" => Some(Self::Ed25519),
"http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519ctx" => Some(Self::Ed25519Ctx),
"http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519ph" => Some(Self::Ed25519Ph),
"http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448" => Some(Self::Ed448),
"http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448ph" => Some(Self::Ed448Ph),
_ => super::PqAlgorithm::ALL
.into_iter()
.find(|algorithm| algorithm.uri() == uri)
.map(Self::PostQuantum),
}
}
#[must_use]
pub fn uri(self) -> &'static str {
match self {
Self::RsaPssSha1 => "http://www.w3.org/2007/05/xmldsig-more#sha1-rsa-MGF1",
Self::RsaPssSha224 => "http://www.w3.org/2007/05/xmldsig-more#sha224-rsa-MGF1",
Self::RsaPssSha256 => "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1",
Self::RsaPssSha384 => "http://www.w3.org/2007/05/xmldsig-more#sha384-rsa-MGF1",
Self::RsaPssSha512 => "http://www.w3.org/2007/05/xmldsig-more#sha512-rsa-MGF1",
Self::RsaPssSha3_224 => "http://www.w3.org/2007/05/xmldsig-more#sha3-224-rsa-MGF1",
Self::RsaPssSha3_256 => "http://www.w3.org/2007/05/xmldsig-more#sha3-256-rsa-MGF1",
Self::RsaPssSha3_384 => "http://www.w3.org/2007/05/xmldsig-more#sha3-384-rsa-MGF1",
Self::RsaPssSha3_512 => "http://www.w3.org/2007/05/xmldsig-more#sha3-512-rsa-MGF1",
Self::RsaPss(_) => "http://www.w3.org/2007/05/xmldsig-more#rsa-pss",
#[cfg(feature = "legacy-algorithms")]
Self::RsaMd5 => "http://www.w3.org/2001/04/xmldsig-more#rsa-md5",
#[cfg(feature = "legacy-algorithms")]
Self::RsaRipemd160 => "http://www.w3.org/2001/04/xmldsig-more#rsa-ripemd160",
#[cfg(feature = "legacy-algorithms")]
Self::HmacMd5 => "http://www.w3.org/2001/04/xmldsig-more#hmac-md5",
#[cfg(feature = "legacy-algorithms")]
Self::HmacRipemd160 => "http://www.w3.org/2001/04/xmldsig-more#hmac-ripemd160",
#[cfg(feature = "legacy-algorithms")]
Self::EcdsaRipemd160 => "http://www.w3.org/2007/05/xmldsig-more#ecdsa-ripemd160",
Self::DsaSha1 => "http://www.w3.org/2000/09/xmldsig#dsa-sha1",
Self::DsaSha256 => "http://www.w3.org/2009/xmldsig11#dsa-sha256",
Self::HmacSha1 => "http://www.w3.org/2000/09/xmldsig#hmac-sha1",
Self::HmacSha224 => "http://www.w3.org/2001/04/xmldsig-more#hmac-sha224",
Self::HmacSha256 => "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
Self::HmacSha384 => "http://www.w3.org/2001/04/xmldsig-more#hmac-sha384",
Self::HmacSha512 => "http://www.w3.org/2001/04/xmldsig-more#hmac-sha512",
Self::RsaSha1 => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
Self::RsaSha224 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224",
Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
Self::EcdsaSha1 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1",
Self::EcdsaSha224 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224",
Self::EcdsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
Self::EcdsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
Self::EcdsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
Self::EcdsaSha3_224 => "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-224",
Self::EcdsaSha3_256 => "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-256",
Self::EcdsaSha3_384 => "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-384",
Self::EcdsaSha3_512 => "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-512",
Self::Ed25519 => "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519",
Self::Ed25519Ctx => "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519ctx",
Self::Ed25519Ph => "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519ph",
Self::Ed448 => "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448",
Self::Ed448Ph => "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448ph",
Self::PostQuantum(algorithm) => algorithm.uri(),
}
}
#[must_use]
pub fn signing_allowed(self) -> bool {
if self.requires_explicit_permission() || self.uses_legacy_pss_hash() {
return false;
}
!matches!(
self,
Self::RsaSha1 | Self::DsaSha1 | Self::HmacSha1 | Self::EcdsaSha1 | Self::PostQuantum(_)
)
}
pub const fn requires_explicit_permission(self) -> bool {
match self {
#[cfg(feature = "legacy-algorithms")]
Self::RsaMd5
| Self::RsaRipemd160
| Self::HmacMd5
| Self::HmacRipemd160
| Self::EcdsaRipemd160 => true,
_ => false,
}
}
pub const fn is_rsa(self) -> bool {
match self {
Self::RsaPssSha1 => true,
Self::RsaPssSha224 => true,
Self::RsaPssSha256 => true,
Self::RsaPssSha384 => true,
Self::RsaPssSha512 => true,
Self::RsaPssSha3_224 => true,
Self::RsaPssSha3_256 => true,
Self::RsaPssSha3_384 => true,
Self::RsaPssSha3_512 => true,
Self::RsaPss(_) => true,
#[cfg(feature = "legacy-algorithms")]
Self::RsaMd5 | Self::RsaRipemd160 => true,
Self::RsaSha1
| Self::RsaSha224
| Self::RsaSha256
| Self::RsaSha384
| Self::RsaSha512 => true,
_ => false,
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct SignedInfo {
pub c14n_method: C14nAlgorithm,
pub signature_method: SignatureAlgorithm,
pub hmac_output_length_bits: Option<usize>,
pub signature_context: SignatureContext,
pub references: Vec<Reference>,
}
#[derive(Debug)]
pub struct Reference {
pub uri: Option<String>,
pub id: Option<String>,
pub ref_type: Option<String>,
pub transforms: Vec<Transform>,
pub digest_method: DigestAlgorithm,
pub digest_value: Vec<u8>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct KeyInfo {
pub sources: Vec<KeyInfoSource>,
}
impl KeyInfo {
pub(crate) fn embedded_candidate_count(&self) -> usize {
self.sources.iter().fold(0usize, |count, source| {
count.saturating_add(match source {
KeyInfoSource::KeyValue(_) | KeyInfoSource::DerEncodedKeyValue(_) => 1,
KeyInfoSource::X509Data(info) => info.certificates.len(),
KeyInfoSource::KeyName(_)
| KeyInfoSource::RetrievalMethod { .. }
| KeyInfoSource::KeyInfoReference { .. } => 0,
})
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeyInfoSource {
KeyName(String),
KeyValue(KeyValueInfo),
X509Data(X509DataInfo),
DerEncodedKeyValue(Vec<u8>),
RetrievalMethod {
uri: String,
resource_type: Option<String>,
transforms: RetrievalMethodTransforms,
},
KeyInfoReference {
uri: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RetrievalMethodTransforms {
None,
X509DataNodeSetFilter {
expression: String,
namespaces: BTreeMap<String, String>,
},
Unsupported,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeyValueInfo {
Dh {
p: Option<Vec<u8>>,
q: Option<Vec<u8>>,
generator: Option<Vec<u8>>,
public: Vec<u8>,
seed: Option<Vec<u8>>,
pgen_counter: Option<Vec<u8>>,
},
Dsa {
p: Option<Vec<u8>>,
q: Option<Vec<u8>>,
g: Option<Vec<u8>>,
y: Vec<u8>,
},
Rsa {
modulus: Vec<u8>,
exponent: Vec<u8>,
},
Ec {
curve_oid: String,
public_key: Vec<u8>,
},
InvalidEcKeyValue,
Unsupported {
namespace: Option<String>,
local_name: String,
},
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct X509DataInfo {
pub certificates: Vec<Vec<u8>>,
pub subject_names: Vec<String>,
pub issuer_serials: Vec<(String, String)>,
pub skis: Vec<Vec<u8>>,
pub crls: Vec<Vec<u8>>,
pub digests: Vec<(String, Vec<u8>)>,
pub parsed_certificates: Vec<ParsedX509Certificate>,
pub certificate_chain: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ParsedX509Certificate {
pub subject_dn: String,
pub issuer_dn: String,
pub serial_number: Vec<u8>,
pub serial_number_hex: String,
pub subject_key_identifier: Option<Vec<u8>>,
pub public_key: X509PublicKeyInfo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum X509PublicKeyInfo {
Rsa {
modulus: Vec<u8>,
exponent: Vec<u8>,
},
Ec {
curve_oid: String,
public_key: Vec<u8>,
},
Unsupported {
algorithm_oid: String,
},
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ParseError {
#[error("X.509 processing failed: {0}")]
Certificate(#[source] structured_pkix::Error),
#[error("XMLDSig policy violation: {0}")]
Policy(#[from] crate::policy::PolicyViolation),
#[error("cryptographic provider error: {0}")]
Provider(#[from] crate::provider::ProviderError),
#[error("missing required element: <{element}>")]
MissingElement {
element: &'static str,
},
#[error("invalid structure: {0}")]
InvalidStructure(String),
#[error("invalid structure: {0}")]
AccountedInvalidStructure(structured_asn1::memory::OwnedText),
#[error("unsupported algorithm: {uri}")]
UnsupportedAlgorithm {
uri: String,
},
#[error("unsupported algorithm: {uri}")]
AccountedUnsupportedAlgorithm {
uri: structured_asn1::memory::OwnedText,
},
#[error("base64 decode error: {0}")]
Base64(String),
#[error(
"digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
)]
DigestLengthMismatch {
algorithm: &'static str,
expected: usize,
actual: usize,
},
#[error("transform error: {0}")]
Transform(#[from] super::types::TransformError),
}
#[must_use]
pub fn find_signature_node<'a>(doc: &'a Document<'a>) -> Option<Node<'a, 'a>> {
doc.descendants().find(|n| {
n.is_element()
&& n.tag_name().name() == "Signature"
&& n.tag_name().namespace() == Some(XMLDSIG_NS)
})
}
pub fn parse_signed_info(signed_info_node: Node) -> Result<SignedInfo, ParseError> {
parse_signed_info_with_xpath_budget(
signed_info_node,
&mut transforms::XPathSignatureParseBudget::default(),
&crate::policy::ResourcePolicy::default(),
)
}
pub(crate) fn parse_signed_info_with_xpath_budget(
signed_info_node: Node,
xpath_budget: &mut transforms::XPathSignatureParseBudget,
resources: &crate::policy::ResourcePolicy,
) -> Result<SignedInfo, ParseError> {
verify_ds_element(signed_info_node, "SignedInfo")?;
let mut children = element_children(signed_info_node);
let c14n_node = children.next().ok_or(ParseError::MissingElement {
element: "CanonicalizationMethod",
})?;
verify_ds_element(c14n_node, "CanonicalizationMethod")?;
let c14n_uri = required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
let mut c14n_method =
C14nAlgorithm::from_uri(c14n_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
uri: c14n_uri.to_string(),
})?;
if let Some(prefix_list) = parse_inclusive_prefixes(c14n_node)? {
if c14n_method.mode() == crate::c14n::C14nMode::Exclusive1_0 {
c14n_method = c14n_method.with_prefix_list(prefix_list);
} else {
return Err(ParseError::UnsupportedAlgorithm {
uri: c14n_uri.to_string(),
});
}
}
let sig_method_node = children.next().ok_or(ParseError::MissingElement {
element: "SignatureMethod",
})?;
let (signature_method, hmac_output_length_bits, signature_context) =
parse_signature_method(sig_method_node, resources)?;
let mut references = Vec::new();
for child in children {
verify_ds_element(child, "Reference")?;
if references.len() == crate::hard_limits::SIGNATURE_REFERENCE_CEILING {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
maximum: crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
actual: references.len().saturating_add(1),
}
.into());
}
references.push(parse_reference_with_xpath_budget(child, xpath_budget)?);
}
if references.is_empty() {
return Err(ParseError::MissingElement {
element: "Reference",
});
}
Ok(SignedInfo {
c14n_method,
signature_method,
hmac_output_length_bits,
signature_context,
references,
})
}
#[derive(Clone, Copy)]
struct ByteAlignedHmacOutputLength(usize);
pub fn parse_signature_method(
node: Node<'_, '_>,
resources: &crate::policy::ResourcePolicy,
) -> Result<(SignatureAlgorithm, Option<usize>, SignatureContext), ParseError> {
verify_ds_element(node, "SignatureMethod")?;
let uri = required_algorithm_attr(node, "SignatureMethod")?;
let mut algorithm =
SignatureAlgorithm::from_uri(uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
uri: uri.to_string(),
})?;
ensure_no_non_whitespace_text(node, "SignatureMethod")?;
if matches!(algorithm, SignatureAlgorithm::RsaPss(_)) {
algorithm = SignatureAlgorithm::RsaPss(parse_rsa_pss_parameters(node)?);
return Ok((algorithm, None, SignatureContext::default()));
}
let (hmac, context) = if algorithm.context_element().is_some() {
(None, parse_signature_context(node, algorithm, resources)?)
} else {
(
parse_hmac_output_length(node, algorithm)?,
SignatureContext::default(),
)
};
Ok((algorithm, hmac, context))
}
fn parse_rsa_pss_parameters(node: Node<'_, '_>) -> Result<RsaPssParameters, ParseError> {
let mut parameters = RsaPssParameters::DEFAULT;
let mut children = element_children(node);
let Some(params) = children.next() else {
return Ok(parameters);
};
if !params.has_tag_name((RSA_PSS_NS, "RSAPSSParams")) || children.next().is_some() {
return Err(ParseError::InvalidStructure(
"invalid RSAPSSParams element".into(),
));
}
ensure_no_non_whitespace_text(params, "RSAPSSParams")?;
let mut last = 0;
let mut salt_seen = false;
let mut mgf_seen = false;
for child in element_children(params) {
let rank = match (child.tag_name().namespace(), child.tag_name().name()) {
(Some(XMLDSIG_NS), "DigestMethod") => 1,
(Some(RSA_PSS_NS), "MaskGenerationFunction") => 2,
(Some(RSA_PSS_NS), "SaltLength") => 3,
(Some(RSA_PSS_NS), "TrailerField") => 4,
_ => {
return Err(ParseError::InvalidStructure(
"unknown RSAPSSParams child".into(),
));
}
};
if rank <= last {
return Err(ParseError::InvalidStructure(
"duplicate or unordered RSAPSSParams child".into(),
));
}
last = rank;
match rank {
1 => parameters.digest = parse_pss_digest(child)?,
2 => {
ensure_no_non_whitespace_text(child, "MaskGenerationFunction")?;
if child
.attribute("Algorithm")
.is_some_and(|uri| uri != "http://www.w3.org/2007/05/xmldsig-more#MGF1")
{
return Err(ParseError::InvalidStructure(
"unsupported PSS mask generation function".into(),
));
}
let mut digests = element_children(child);
parameters.mgf_digest = match digests.next() {
Some(digest) => parse_pss_digest(digest)?,
None => DigestAlgorithm::Sha256,
};
if digests.next().is_some() {
return Err(ParseError::InvalidStructure(
"duplicate MGF1 DigestMethod".into(),
));
}
mgf_seen = true;
}
3 => {
parameters.salt_len = parse_pss_integer(child)?;
salt_seen = true;
}
4 => {
if parse_pss_integer(child)? != 1 {
return Err(ParseError::InvalidStructure(
"PSS TrailerField must be 1".into(),
));
}
}
_ => unreachable!(),
}
}
if !mgf_seen {
parameters.mgf_digest = parameters.digest;
}
if !salt_seen {
parameters.salt_len = parameters.digest.output_len();
}
Ok(parameters)
}
fn parse_pss_digest(node: Node<'_, '_>) -> Result<DigestAlgorithm, ParseError> {
verify_ds_element(node, "DigestMethod")?;
ensure_no_element_children(node, "DigestMethod")?;
ensure_no_non_whitespace_text(node, "DigestMethod")?;
let uri = required_algorithm_attr(node, "DigestMethod")?;
DigestAlgorithm::from_uri(uri)
.ok_or_else(|| ParseError::UnsupportedAlgorithm { uri: uri.into() })
}
fn parse_pss_integer(node: Node<'_, '_>) -> Result<usize, ParseError> {
ensure_no_element_children(node, "PSS integer")?;
let invalid = || ParseError::InvalidStructure("PSS integer must be nonnegative xs:int".into());
let mut value = 0usize;
let mut started = false;
let mut digit_seen = false;
let mut ended = false;
let mut negative = false;
for character in node
.children()
.filter_map(|child| child.text().filter(|_| child.is_text()))
.flat_map(str::chars)
{
if matches!(character, ' ' | '\t' | '\r' | '\n') {
if started {
ended = true;
}
continue;
}
if ended {
return Err(invalid());
}
if !started && matches!(character, '+' | '-') {
started = true;
negative = character == '-';
continue;
}
let Some(digit) = character
.to_digit(10)
.filter(|_| character.is_ascii_digit())
else {
return Err(invalid());
};
if value > (i32::MAX as usize - digit as usize) / 10 {
return Err(invalid());
}
value = value * 10 + digit as usize;
started = true;
digit_seen = true;
}
if !digit_seen || (negative && value != 0) {
return Err(invalid());
}
Ok(value)
}
impl ByteAlignedHmacOutputLength {
fn parse(text: &str, maximum_bits: usize) -> Result<Self, ParseError> {
let bits = text
.trim_matches([' ', '\t', '\r', '\n'])
.parse::<usize>()
.map_err(|_| ParseError::InvalidStructure("invalid HMACOutputLength".into()))?;
if bits == 0 || bits > maximum_bits || !bits.is_multiple_of(8) {
return Err(ParseError::InvalidStructure(format!(
"HMACOutputLength must be a positive byte-aligned value no greater than {maximum_bits}"
)));
}
Ok(Self(bits))
}
const fn bits(self) -> usize {
self.0
}
}
fn parse_hmac_output_length(
node: Node<'_, '_>,
algorithm: SignatureAlgorithm,
) -> Result<Option<usize>, ParseError> {
ensure_no_non_whitespace_text(node, "SignatureMethod")?;
let mut children = element_children(node);
let Some(child) = children.next() else {
return Ok(None);
};
let Some(maximum_bits) = algorithm.hmac_output_bits() else {
return Err(ParseError::InvalidStructure(
"SignatureMethod parameters do not match the selected algorithm".into(),
));
};
if child.tag_name().namespace() != Some(XMLDSIG_NS)
|| child.tag_name().name() != "HMACOutputLength"
|| children.next().is_some()
{
return Err(ParseError::InvalidStructure(
"SignatureMethod parameters do not match the selected algorithm".into(),
));
}
ensure_no_element_children(child, "HMACOutputLength")?;
let text =
collect_text_content_bounded(child, MAX_HMAC_OUTPUT_LENGTH_TEXT_LEN, "HMACOutputLength")?;
Ok(Some(
ByteAlignedHmacOutputLength::parse(&text, maximum_bits)?.bits(),
))
}
fn parse_signature_context(
node: Node<'_, '_>,
algorithm: SignatureAlgorithm,
resources: &crate::policy::ResourcePolicy,
) -> Result<SignatureContext, ParseError> {
let Some(element_name) = algorithm.context_element() else {
return Ok(SignatureContext::default());
};
let mut children = element_children(node);
let Some(child) = children.next() else {
return Ok(SignatureContext::default());
};
if child.tag_name().namespace() != Some(EXPERIMENTAL_SIGNATURE_NS)
|| child.tag_name().name() != element_name
|| children.next().is_some()
{
return Err(ParseError::InvalidStructure(
"SignatureMethod parameters do not match the selected algorithm".into(),
));
}
let payload =
super::whitespace::XmlBase64Payload::bounded(child, resources.max_xml_document_bytes, 340)
.map_err(|reason| {
ParseError::InvalidStructure(format!("signature context: {reason}"))
})?;
if payload.decoded_len > 255 {
return Err(ParseError::InvalidStructure(
"signature context exceeds 255 octets".into(),
));
}
let bytes = payload
.decode()
.map_err(|reason| ParseError::InvalidStructure(format!("signature context: {reason}")))?;
Ok(SignatureContext(bytes))
}
pub fn parse_reference(reference_node: Node) -> Result<Reference, ParseError> {
parse_reference_with_xpath_budget(
reference_node,
&mut transforms::XPathSignatureParseBudget::default(),
)
}
pub(crate) fn parse_reference_with_xpath_budget(
reference_node: Node,
xpath_budget: &mut transforms::XPathSignatureParseBudget,
) -> Result<Reference, ParseError> {
verify_ds_element(reference_node, "Reference")?;
ensure_no_non_whitespace_text(reference_node, "Reference")?;
let uri = reference_node.attribute("URI").map(String::from);
let id = reference_node.attribute("Id").map(String::from);
let ref_type = reference_node.attribute("Type").map(String::from);
let mut transforms = Vec::new();
let mut transform_error = None;
let mut children = element_children(reference_node);
let (transforms_node, digest_method_node) =
reference_transforms_and_digest_method(&mut children)?;
if let Some(transforms_node) = transforms_node {
match transforms::parse_transforms_with_budget(transforms_node, xpath_budget) {
Ok(parsed) => transforms = parsed,
Err(error) => transform_error = Some(error),
}
}
let digest_uri = required_algorithm_attr(digest_method_node, "DigestMethod")?;
let digest_method =
DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
uri: digest_uri.to_string(),
})?;
let digest_value_node = reference_digest_value_node(&mut children)?;
let digest_value = decode_digest_value_children(digest_value_node, digest_method)?;
finish_reference_children(&mut children)?;
if let Some(error) = transform_error {
return Err(ParseError::Transform(error));
}
Ok(Reference {
uri,
id,
ref_type,
transforms,
digest_method,
digest_value,
})
}
pub(crate) type ReferenceLayout<'a> = (Option<Node<'a, 'a>>, Node<'a, 'a>, Node<'a, 'a>);
#[derive(Debug)]
pub(crate) enum ReferenceLayoutFault<'a, 'input> {
Missing(&'static str),
WrongElement(Node<'a, 'input>, &'static str),
MixedContent,
TrailingElement(Node<'a, 'input>),
}
impl ReferenceLayoutFault<'_, '_> {
fn diagnostic(self) -> ParseError {
self.diagnostic_in(&crate::c14n::scratch::Native)
}
fn diagnostic_in<'s, S: transforms::ParseStorage<'s>>(self, storage: &S) -> ParseError {
match self {
Self::Missing(element) => ParseError::MissingElement { element },
Self::WrongElement(node, expected) => {
element_mismatch(node, expected, XMLDSIG_NS, "ds", storage)
.expect_err("layout fault records a failed element check")
}
Self::MixedContent => storage.structure(format_args!(
"Reference must not contain non-whitespace mixed content"
)),
Self::TrailingElement(node) => storage.structure(format_args!(
"unexpected element <{}> after <DigestValue> in <Reference>",
node.tag_name().name()
)),
}
}
}
fn layout_element<'a, 'input>(
node: Node<'a, 'input>,
expected: &'static str,
) -> Result<(), ReferenceLayoutFault<'a, 'input>> {
if node.has_tag_name((XMLDSIG_NS, expected)) {
Ok(())
} else {
Err(ReferenceLayoutFault::WrongElement(node, expected))
}
}
pub(crate) fn inspect_reference_layout<'a>(
node: Node<'a, 'a>,
) -> Result<ReferenceLayout<'a>, ReferenceLayoutFault<'a, 'a>> {
match inspect_charged_reference_layout(node, |_| Ok::<(), core::convert::Infallible>(())) {
Ok(layout) => layout,
Err(never) => match never {},
}
}
fn inspect_charged_reference_layout<'a, E>(
node: Node<'a, 'a>,
mut charge: impl FnMut(usize) -> Result<(), E>,
) -> Result<Result<ReferenceLayout<'a>, ReferenceLayoutFault<'a, 'a>>, E> {
if let Err(fault) = layout_element(node, "Reference") {
return Ok(Err(fault));
}
for child in node.children() {
if let Some(text) = child.text().filter(|_| child.is_text()) {
charge(text.len())?;
if !is_xml_whitespace_only(text) {
return Ok(Err(ReferenceLayoutFault::MixedContent));
}
}
}
Ok(layout_reference_children(node))
}
fn layout_reference_children<'a>(
node: Node<'a, 'a>,
) -> Result<ReferenceLayout<'a>, ReferenceLayoutFault<'a, 'a>> {
let mut children = element_children(node);
let (transforms, method) = layout_transforms_and_digest_method(&mut children)?;
let value = layout_digest_value(&mut children)?;
layout_finish(&mut children)?;
Ok((transforms, method, value))
}
fn reference_digest_value_node<'a>(
children: &mut impl Iterator<Item = Node<'a, 'a>>,
) -> Result<Node<'a, 'a>, ParseError> {
layout_digest_value(children).map_err(ReferenceLayoutFault::diagnostic)
}
fn layout_digest_value<'a, 'input>(
children: &mut impl Iterator<Item = Node<'a, 'input>>,
) -> Result<Node<'a, 'input>, ReferenceLayoutFault<'a, 'input>> {
let value = children
.next()
.ok_or(ReferenceLayoutFault::Missing("DigestValue"))?;
layout_element(value, "DigestValue")?;
Ok(value)
}
fn finish_reference_children<'a>(
children: &mut impl Iterator<Item = Node<'a, 'a>>,
) -> Result<(), ParseError> {
layout_finish(children).map_err(ReferenceLayoutFault::diagnostic)
}
fn layout_finish<'a, 'input>(
children: &mut impl Iterator<Item = Node<'a, 'input>>,
) -> Result<(), ReferenceLayoutFault<'a, 'input>> {
if let Some(unexpected) = children.next() {
return Err(ReferenceLayoutFault::TrailingElement(unexpected));
}
Ok(())
}
pub(crate) fn parse_replay_transforms<'a, S: transforms::ParseStorage<'a>>(
node: Node<'a, 'a>,
budget: &mut transforms::XPathSignatureParseBudget,
scratch: &S,
) -> Result<S::Values<S::Instruction>, ParseError> {
scratch
.charge(node.children().len())
.map_err(super::types::TransformError::from)?;
let (transforms, method, _) =
inspect_charged_reference_layout(node, |bytes| scratch.charge(bytes))
.map_err(super::types::TransformError::from)?
.map_err(|fault| fault.diagnostic_in(scratch))?;
scratch
.charge(method.attributes().len())
.map_err(super::types::TransformError::from)?;
required_algorithm_attr(method, "DigestMethod")?;
transforms.map_or_else(
|| Ok(scratch.values()),
|node| {
transforms::parse_reference_transforms_in(node, XMLDSIG_NS, budget, scratch)
.map_err(Into::into)
},
)
}
pub(crate) fn reference_digest_method(
reference_node: Node<'_, '_>,
) -> Result<DigestAlgorithm, ParseError> {
verify_ds_element(reference_node, "Reference")?;
let mut children = element_children(reference_node);
let (_, digest_method_node) = reference_transforms_and_digest_method(&mut children)?;
let uri = required_algorithm_attr(digest_method_node, "DigestMethod")?;
DigestAlgorithm::from_uri(uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
uri: uri.to_owned(),
})
}
fn reference_transforms_and_digest_method<'a, 'input>(
children: &mut impl Iterator<Item = Node<'a, 'input>>,
) -> Result<(Option<Node<'a, 'input>>, Node<'a, 'input>), ParseError> {
layout_transforms_and_digest_method(children).map_err(ReferenceLayoutFault::diagnostic)
}
fn layout_transforms_and_digest_method<'a, 'input>(
children: &mut impl Iterator<Item = Node<'a, 'input>>,
) -> Result<(Option<Node<'a, 'input>>, Node<'a, 'input>), ReferenceLayoutFault<'a, 'input>> {
let first = children
.next()
.ok_or(ReferenceLayoutFault::Missing("DigestMethod"))?;
let transforms_node = is_ds_element(first, "Transforms").then_some(first);
let digest_method_node = if transforms_node.is_some() {
children
.next()
.ok_or(ReferenceLayoutFault::Missing("DigestMethod"))?
} else {
first
};
layout_element(digest_method_node, "DigestMethod")?;
Ok((transforms_node, digest_method_node))
}
pub fn parse_key_info(key_info_node: Node) -> Result<KeyInfo, ParseError> {
parse_key_info_with_provider(key_info_node, crate::provider::default_provider())
}
pub struct KeyInfoParsingSession<'a> {
resources: &'a crate::policy::ResourcePolicy,
xml_base: XmlBaseResolutionBudget,
usage: KeyInfoParseUsage,
}
pub(crate) struct KeyInfoParseUsage {
embedded_candidates: usize,
x509_binary_bytes: usize,
pub(crate) certificates: super::pkix::CertificateSession,
}
impl KeyInfoParseUsage {
pub(crate) fn new(resources: &crate::policy::ResourcePolicy) -> Self {
Self {
embedded_candidates: 0,
x509_binary_bytes: 0,
certificates: super::pkix::CertificateSession::new(resources),
}
}
}
impl<'a> KeyInfoParsingSession<'a> {
pub(crate) fn resources(&self) -> &crate::policy::ResourcePolicy {
self.resources
}
pub(crate) fn with_certificate_account(
resources: &'a crate::policy::ResourcePolicy,
certificates: &super::pkix::CertificateSession,
) -> Result<Self, ParseError> {
certificates.require_resources(resources)?;
Ok(Self {
resources,
xml_base: XmlBaseResolutionBudget::with_limits(
resources.effective_xml_base_components(),
resources.effective_xml_base_resolution_bytes(),
),
usage: KeyInfoParseUsage {
embedded_candidates: 0,
x509_binary_bytes: 0,
certificates: certificates.share(),
},
})
}
pub fn with_certificate_public_key<R>(
&mut self,
der: &[u8],
inspect: impl FnOnce(&[u8]) -> R,
) -> Result<R, ParseError> {
self.admit_certificate(der)?;
let certificate = self
.usage
.certificates
.read(der)
.map_err(|error| self.usage.certificates.parse_error(error))?;
Ok(inspect(certificate.tbs().public_key().as_bytes()))
}
pub fn certificate_matches_selectors(
&mut self,
info: &X509DataInfo,
der: &[u8],
provider: &dyn crate::provider::CryptoProvider,
) -> Result<bool, ParseError> {
self.admit_certificate(der)?;
x509_selector_categories_match_candidates(
info,
core::iter::once(der),
provider,
&self.usage.certificates,
)
}
fn admit_certificate(&mut self, der: &[u8]) -> Result<(), ParseError> {
charge_embedded_key_candidate(&mut self.usage.embedded_candidates, self.resources)?;
if der.len() > self.resources.max_external_resource_bytes {
return Err(ParseError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum: self.resources.max_external_resource_bytes,
},
));
}
let total = self.usage.x509_binary_bytes.checked_add(der.len());
if total.is_none_or(|bytes| bytes > self.resources.max_external_resource_total_bytes) {
return Err(ParseError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
maximum: self.resources.max_external_resource_total_bytes,
},
));
}
self.usage.x509_binary_bytes = total.expect("admitted checked sum");
Ok(())
}
#[cfg(feature = "xmlenc")]
pub(crate) fn charge_encryption_source(&mut self) -> Result<(), ParseError> {
charge_embedded_key_candidate(&mut self.usage.embedded_candidates, self.resources)
}
#[cfg(feature = "xmlenc")]
pub(crate) fn preflight_agreement_role_candidates(&self, node: Node) -> Result<(), ParseError> {
let mut projected = self.usage.embedded_candidates;
for child in element_children(node) {
match (child.tag_name().namespace(), child.tag_name().name()) {
(Some(XMLDSIG_NS), "KeyValue") | (Some(XMLDSIG11_NS), "DEREncodedKeyValue") => {
charge_embedded_key_candidate(&mut projected, self.resources)?;
}
(Some(XMLDSIG_NS), "X509Data") => {
for _ in element_children(child)
.filter(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate")))
{
charge_embedded_key_candidate(&mut projected, self.resources)?;
}
}
_ => {}
}
}
Ok(())
}
pub fn new(resources: &'a crate::policy::ResourcePolicy) -> Result<Self, ParseError> {
resources.validate()?;
Ok(Self {
resources,
xml_base: XmlBaseResolutionBudget::with_limits(
resources.effective_xml_base_components(),
resources.effective_xml_base_resolution_bytes(),
),
usage: KeyInfoParseUsage::new(resources),
})
}
pub fn parse(&mut self, node: Node) -> Result<KeyInfo, ParseError> {
self.parse_with_provider(node, crate::provider::default_provider())
}
pub fn parse_with_provider(
&mut self,
node: Node,
provider: &dyn crate::provider::CryptoProvider,
) -> Result<KeyInfo, ParseError> {
self.parse_with_provider_and_document_base(node, provider, None)
}
pub(crate) fn parse_with_provider_and_document_base(
&mut self,
node: Node,
provider: &dyn crate::provider::CryptoProvider,
document_base: Option<&str>,
) -> Result<KeyInfo, ParseError> {
parse_key_info_in_session(
node,
provider,
&self.xml_base,
self.resources,
document_base,
None,
&mut self.usage,
)
}
#[cfg(feature = "xmlenc")]
pub(crate) fn parse_agreement_role(
&mut self,
node: Node,
provider: &dyn crate::provider::CryptoProvider,
) -> Result<KeyInfo, ParseError> {
let namespace = "http://www.w3.org/2001/04/xmlenc#";
if !node.has_tag_name((namespace, "OriginatorKeyInfo"))
&& !node.has_tag_name((namespace, "RecipientKeyInfo"))
{
return Err(ParseError::InvalidStructure(
"invalid agreement key role".into(),
));
}
ensure_no_non_whitespace_text(node, "agreement KeyInfo")?;
parse_key_info_contents_in_session(
node,
provider,
&self.xml_base,
self.resources,
None,
None,
&mut self.usage,
)
}
}
pub(crate) fn parse_key_info_with_provider(
key_info_node: Node,
provider: &dyn crate::provider::CryptoProvider,
) -> Result<KeyInfo, ParseError> {
let xml_base_budget = XmlBaseResolutionBudget::default();
parse_key_info_with_policy_budgets(
key_info_node,
provider,
&xml_base_budget,
&crate::policy::ResourcePolicy::default(),
None,
)
}
pub(crate) fn parse_key_info_with_policy_budgets(
key_info_node: Node,
provider: &dyn crate::provider::CryptoProvider,
xml_base_budget: &XmlBaseResolutionBudget,
resources: &crate::policy::ResourcePolicy,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
) -> Result<KeyInfo, ParseError> {
parse_key_info_in_session(
key_info_node,
provider,
xml_base_budget,
resources,
None,
allowed,
&mut KeyInfoParseUsage::new(resources),
)
}
pub(crate) fn parse_key_info_in_session(
key_info_node: Node,
provider: &dyn crate::provider::CryptoProvider,
xml_base_budget: &XmlBaseResolutionBudget,
resources: &crate::policy::ResourcePolicy,
document_base: Option<&str>,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
usage: &mut KeyInfoParseUsage,
) -> Result<KeyInfo, ParseError> {
validate_key_info_container(key_info_node)?;
parse_key_info_contents_in_session(
key_info_node,
provider,
xml_base_budget,
resources,
document_base,
allowed,
usage,
)
}
fn parse_key_info_contents_in_session(
key_info_node: Node,
provider: &dyn crate::provider::CryptoProvider,
xml_base_budget: &XmlBaseResolutionBudget,
resources: &crate::policy::ResourcePolicy,
document_base: Option<&str>,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
usage: &mut KeyInfoParseUsage,
) -> Result<KeyInfo, ParseError> {
let mut sources = Vec::new();
for (index, child) in element_children(key_info_node).enumerate() {
if index >= MAX_KEY_INFO_CHILD_COUNT {
return Err(ParseError::InvalidStructure(
"KeyInfo contains too many child elements".into(),
));
}
match (child.tag_name().namespace(), child.tag_name().name()) {
(Some(XMLDSIG_NS), "KeyName") => {
ensure_no_element_children(child, "KeyName")?;
let key_name =
collect_text_content_bounded(child, MAX_KEY_NAME_TEXT_LEN, "KeyName")?;
sources.push(KeyInfoSource::KeyName(key_name));
}
(Some(XMLDSIG_NS), "KeyValue") => {
charge_embedded_key_candidate(&mut usage.embedded_candidates, resources)?;
let key_value = parse_key_value_dispatch(child)?;
sources.push(KeyInfoSource::KeyValue(key_value));
}
(Some(XMLDSIG_NS), "X509Data") => {
let x509 = parse_x509_data_in_session(
child,
&mut usage.x509_binary_bytes,
&mut usage.embedded_candidates,
provider,
resources,
allowed,
&usage.certificates,
)?;
sources.push(KeyInfoSource::X509Data(x509));
}
(Some(XMLDSIG_NS), "RetrievalMethod") => {
ensure_no_non_whitespace_text(child, "RetrievalMethod")?;
let lexical_uri = child.attribute("URI").ok_or_else(|| {
ParseError::InvalidStructure("RetrievalMethod requires URI".into())
})?;
if lexical_uri.len() > MAX_KEY_NAME_TEXT_LEN {
return Err(ParseError::InvalidStructure(
"RetrievalMethod URI exceeds maximum length".into(),
));
}
let uri = if lexical_uri.is_empty() || lexical_uri.starts_with('#') {
lexical_uri.to_owned()
} else {
resolve_uri_from_node_with_document_base_with_budget(
child,
lexical_uri,
document_base,
xml_base_budget,
)
.map_err(|error| ParseError::InvalidStructure(error.to_string()))?
};
let resource_type = child.attribute("Type");
if resource_type.is_some_and(|value| value.len() > MAX_KEY_NAME_TEXT_LEN) {
return Err(ParseError::InvalidStructure(
"RetrievalMethod Type exceeds maximum length".into(),
));
}
let resource_type = resource_type.map(str::to_owned);
let transforms = if resource_type.as_deref()
== Some("http://www.w3.org/2000/09/xmldsig#X509Data")
{
parse_retrieval_method_transforms(child, resources)?
} else if element_children(child).next().is_some() {
RetrievalMethodTransforms::Unsupported
} else {
RetrievalMethodTransforms::None
};
sources.push(KeyInfoSource::RetrievalMethod {
uri,
resource_type,
transforms,
});
}
(Some(XMLDSIG11_NS), "DEREncodedKeyValue") => {
charge_embedded_key_candidate(&mut usage.embedded_candidates, resources)?;
ensure_no_element_children(child, "DEREncodedKeyValue")?;
let der = decode_der_encoded_key_value_base64(child)?;
sources.push(KeyInfoSource::DerEncodedKeyValue(der));
}
(Some(XMLDSIG11_NS), "KeyInfoReference") => {
ensure_no_element_children(child, "KeyInfoReference")?;
ensure_no_non_whitespace_text(child, "KeyInfoReference")?;
let lexical_uri = child.attribute("URI").ok_or_else(|| {
ParseError::InvalidStructure("KeyInfoReference requires URI".into())
})?;
if lexical_uri.len() > MAX_KEY_NAME_TEXT_LEN {
return Err(ParseError::InvalidStructure(
"KeyInfoReference URI exceeds maximum length".into(),
));
}
let uri = if lexical_uri.is_empty() || lexical_uri.starts_with('#') {
lexical_uri.to_owned()
} else {
resolve_uri_from_node_with_document_base_with_budget(
child,
lexical_uri,
document_base,
xml_base_budget,
)
.map_err(|error| ParseError::InvalidStructure(error.to_string()))?
};
sources.push(KeyInfoSource::KeyInfoReference { uri });
}
_ => {}
}
}
Ok(KeyInfo { sources })
}
fn charge_embedded_key_candidate(
embedded_key_candidates: &mut usize,
resources: &crate::policy::ResourcePolicy,
) -> Result<(), ParseError> {
*embedded_key_candidates = embedded_key_candidates.saturating_add(1);
resources.validate_key_candidates(*embedded_key_candidates)?;
Ok(())
}
fn parse_retrieval_method_transforms(
node: Node<'_, '_>,
resources: &crate::policy::ResourcePolicy,
) -> Result<RetrievalMethodTransforms, ParseError> {
let mut children = element_children(node);
let Some(transforms) = children.next() else {
return Ok(RetrievalMethodTransforms::None);
};
if children.next().is_some()
|| transforms.tag_name().namespace() != Some(XMLDSIG_NS)
|| transforms.tag_name().name() != "Transforms"
{
return Err(ParseError::InvalidStructure(
"RetrievalMethod accepts only one optional ds:Transforms child".into(),
));
}
ensure_no_non_whitespace_text(transforms, "Transforms")?;
let mut transform_children = element_children(transforms);
let transform = transform_children.next().ok_or_else(|| {
ParseError::InvalidStructure("RetrievalMethod Transforms must not be empty".into())
})?;
if transform_children.next().is_some()
|| transform.tag_name().namespace() != Some(XMLDSIG_NS)
|| transform.tag_name().name() != "Transform"
|| transform.attribute("Algorithm") != Some(transforms::XPATH_TRANSFORM_URI)
{
return Err(ParseError::InvalidStructure(
"unsupported RetrievalMethod transform chain".into(),
));
}
ensure_no_non_whitespace_text(transform, "Transform")?;
let mut parameters = element_children(transform);
let xpath = parameters.next().ok_or_else(|| {
ParseError::InvalidStructure("RetrievalMethod XPath parameter is missing".into())
})?;
if parameters.next().is_some()
|| xpath.tag_name().namespace() != Some(XMLDSIG_NS)
|| xpath.tag_name().name() != "XPath"
{
return Err(ParseError::InvalidStructure(
"unsupported RetrievalMethod transform chain".into(),
));
}
ensure_no_element_children(xpath, "XPath")?;
let expression =
collect_text_content_bounded(xpath, MAX_RETRIEVAL_XPATH_TEXT_LEN, "RetrievalMethod XPath")?;
let normalized_expression = expression.trim();
let selects_x509_data = normalized_expression
.strip_prefix("ancestor-or-self::")
.and_then(|step| step.split_once(':'))
.is_some_and(|(prefix, local)| {
local == "X509Data" && xpath.lookup_namespace_uri(Some(prefix)) == Some(XMLDSIG_NS)
});
if !selects_x509_data {
return Err(ParseError::InvalidStructure(
"unsupported RetrievalMethod XPath selection".into(),
));
}
let namespaces = transforms::collect_xpath_namespaces_with_resources(xpath, resources)?;
Ok(RetrievalMethodTransforms::X509DataNodeSetFilter {
expression,
namespaces,
})
}
fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
node.children().filter(|n| n.is_element())
}
fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
verify_namespaced_element(node, expected_name, XMLDSIG_NS, "ds")
}
fn verify_dsig11_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
verify_namespaced_element(node, expected_name, XMLDSIG11_NS, "dsig11")
}
fn verify_namespaced_element(
node: Node,
expected_name: &'static str,
expected_namespace: &'static str,
diagnostic_prefix: &'static str,
) -> Result<(), ParseError> {
element_mismatch(
node,
expected_name,
expected_namespace,
diagnostic_prefix,
&crate::c14n::scratch::Native,
)
}
fn element_mismatch<'s, S: transforms::ParseStorage<'s>>(
node: Node,
expected_name: &'static str,
expected_namespace: &'static str,
diagnostic_prefix: &'static str,
storage: &S,
) -> Result<(), ParseError> {
if !node.is_element() {
return Err(storage.structure(format_args!(
"expected element <{expected_name}>, got non-element node"
)));
}
let tag = node.tag_name();
if tag.name() != expected_name || tag.namespace() != Some(expected_namespace) {
let name = tag.name();
return Err(match tag.namespace() {
Some(namespace) => storage.structure(format_args!(
"expected <{diagnostic_prefix}:{expected_name}>, got <{{{namespace}}}{name}>"
)),
None => storage.structure(format_args!(
"expected <{diagnostic_prefix}:{expected_name}>, got <{name}>"
)),
});
}
Ok(())
}
fn required_algorithm_attr<'a>(
node: Node<'a, 'a>,
element_name: &'static str,
) -> Result<&'a str, ParseError> {
node.attribute("Algorithm").ok_or_else(|| {
ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
})
}
fn parse_inclusive_prefixes<'a>(node: Node<'a, '_>) -> Result<Option<&'a str>, ParseError> {
const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
for child in node.children() {
if child.is_element() {
let tag = child.tag_name();
if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
{
return child
.attribute("PrefixList")
.ok_or_else(|| {
ParseError::InvalidStructure(
"missing PrefixList attribute on <InclusiveNamespaces>".into(),
)
})
.map(Some);
}
}
}
Ok(None)
}
pub(crate) fn parse_replay_canonicalization_method<'a, 's, S: transforms::ParseStorage<'s>>(
node: Node<'a, '_>,
scratch: &S,
) -> Result<(C14nAlgorithm, Option<&'a str>), ParseError> {
element_mismatch(node, "CanonicalizationMethod", XMLDSIG_NS, "ds", scratch)?;
ensure_charged_whitespace_text(node, "CanonicalizationMethod", scratch)?;
scratch
.charge(node.attributes().len())
.map_err(super::types::TransformError::from)?;
let uri = node.attribute("Algorithm").ok_or_else(|| {
scratch.structure(format_args!(
"missing Algorithm attribute on <CanonicalizationMethod>"
))
})?;
let algorithm =
C14nAlgorithm::from_uri(uri).ok_or_else(|| scratch.unsupported_algorithm(uri))?;
let mut prefixes = None;
for child in element_children(node) {
if prefixes.is_some()
|| algorithm.mode() != crate::c14n::C14nMode::Exclusive1_0
|| !child.has_tag_name((
"http://www.w3.org/2001/10/xml-exc-c14n#",
"InclusiveNamespaces",
))
{
return Err(scratch.structure(format_args!("invalid canonicalization parameter")));
}
ensure_charged_whitespace_text(child, "InclusiveNamespaces", scratch)?;
if child.children().any(|child| child.is_element()) {
return Err(scratch.structure(format_args!(
"InclusiveNamespaces must not contain child elements"
)));
}
scratch
.charge(child.attributes().len())
.map_err(super::types::TransformError::from)?;
prefixes = Some(child.attribute("PrefixList").ok_or_else(|| {
scratch.structure(format_args!(
"missing PrefixList attribute on <InclusiveNamespaces>"
))
})?);
}
Ok((algorithm, prefixes))
}
pub(crate) fn parse_key_value_dispatch(node: Node) -> Result<KeyValueInfo, ParseError> {
verify_ds_element(node, "KeyValue")?;
ensure_no_non_whitespace_text(node, "KeyValue")?;
let mut children = element_children(node);
let Some(first_child) = children.next() else {
return Err(ParseError::InvalidStructure(
"KeyValue must contain exactly one key-value child".into(),
));
};
if children.next().is_some() {
return Err(ParseError::InvalidStructure(
"KeyValue must contain exactly one key-value child".into(),
));
}
match (
first_child.tag_name().namespace(),
first_child.tag_name().name(),
) {
(Some(XMLDSIG_NS), "RSAKeyValue") => parse_rsa_key_value(first_child),
(Some(XMLDSIG_NS), "DSAKeyValue") => parse_dsa_key_value(first_child),
(Some(XMLDSIG11_NS), "ECKeyValue") => parse_ec_key_value(first_child),
(Some("http://www.w3.org/2001/04/xmlenc#"), "DHKeyValue") => {
parse_dh_key_value(first_child)
}
(namespace, child_name) => Ok(KeyValueInfo::Unsupported {
namespace: namespace.map(str::to_string),
local_name: child_name.to_string(),
}),
}
}
pub(crate) fn validate_key_info_container(node: Node) -> Result<(), ParseError> {
verify_ds_element(node, "KeyInfo")?;
ensure_no_non_whitespace_text(node, "KeyInfo")
}
fn parse_dsa_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
verify_ds_element(node, "DSAKeyValue")?;
ensure_no_non_whitespace_text(node, "DSAKeyValue")?;
let children = element_children(node).collect::<Vec<_>>();
let mut index = 0;
let p = take_dsa_crypto_binary(&children, &mut index, "P")?;
let q = take_dsa_crypto_binary(&children, &mut index, "Q")?;
if p.is_some() != q.is_some() {
return Err(ParseError::InvalidStructure(
"DSAKeyValue P and Q must be present together".into(),
));
}
let g = take_dsa_crypto_binary(&children, &mut index, "G")?;
let y = take_dsa_crypto_binary(&children, &mut index, "Y")?
.ok_or_else(|| ParseError::InvalidStructure("DSAKeyValue requires Y".into()))?;
let _j = take_dsa_crypto_binary(&children, &mut index, "J")?;
let seed = take_dsa_crypto_binary(&children, &mut index, "Seed")?;
let counter = take_dsa_crypto_binary(&children, &mut index, "PgenCounter")?;
if seed.is_some() != counter.is_some() {
return Err(ParseError::InvalidStructure(
"DSAKeyValue Seed and PgenCounter must be present together".into(),
));
}
if index != children.len() {
return Err(ParseError::InvalidStructure(
"DSAKeyValue children do not match the XMLDSig schema order".into(),
));
}
Ok(KeyValueInfo::Dsa { p, q, g, y })
}
fn parse_dh_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
const NS: &str = "http://www.w3.org/2001/04/xmlenc#";
ensure_no_non_whitespace_text(node, "DHKeyValue")?;
let mut children = element_children(node).peekable();
let mut take = |name: &'static str| -> Result<Option<Vec<u8>>, ParseError> {
if !children
.peek()
.is_some_and(|child| child.has_tag_name((NS, name)))
{
return Ok(None);
}
let child = children.next().expect("peeked child");
ensure_no_element_children(child, name)?;
decode_crypto_binary(
child,
name,
crate::hard_limits::DH_MODULUS_BIT_CEILING.div_ceil(8),
)
.map(Some)
};
let p = take("P")?;
let q = take("Q")?;
let generator = take("Generator")?;
if p.is_some() != q.is_some() || p.is_some() != generator.is_some() {
return Err(ParseError::InvalidStructure(
"DHKeyValue requires P, Q and Generator together".into(),
));
}
let public = take("Public")?
.ok_or_else(|| ParseError::InvalidStructure("DHKeyValue requires Public".into()))?;
let seed = take("seed")?;
let pgen_counter = take("pgenCounter")?;
if seed.is_some() != pgen_counter.is_some() || children.next().is_some() {
return Err(ParseError::InvalidStructure(
"DHKeyValue children do not match the XMLEnc schema order".into(),
));
}
Ok(KeyValueInfo::Dh {
p,
q,
generator,
public,
seed,
pgen_counter,
})
}
fn take_dsa_crypto_binary(
children: &[Node<'_, '_>],
index: &mut usize,
name: &'static str,
) -> Result<Option<Vec<u8>>, ParseError> {
let Some(&child) = children.get(*index) else {
return Ok(None);
};
if !is_ds_element(child, name) {
return Ok(None);
}
*index += 1;
ensure_no_element_children(child, name)?;
decode_crypto_binary(child, name, MAX_RSA_MODULUS_LEN).map(Some)
}
fn is_ds_element(node: Node<'_, '_>, name: &str) -> bool {
node.tag_name().namespace() == Some(XMLDSIG_NS) && node.tag_name().name() == name
}
fn parse_ec_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
verify_dsig11_element(node, "ECKeyValue")?;
ensure_no_non_whitespace_text(node, "ECKeyValue")?;
let mut children = element_children(node);
let Some(named_curve_node) = children.next() else {
return Ok(KeyValueInfo::InvalidEcKeyValue);
};
if named_curve_node.tag_name().namespace() == Some(XMLDSIG11_NS)
&& named_curve_node.tag_name().name() == "ECParameters"
{
return Ok(KeyValueInfo::Unsupported {
namespace: Some(XMLDSIG11_NS.to_string()),
local_name: "ECKeyValue".into(),
});
}
if named_curve_node.tag_name().namespace() != Some(XMLDSIG11_NS)
|| named_curve_node.tag_name().name() != "NamedCurve"
{
return Ok(KeyValueInfo::InvalidEcKeyValue);
}
ensure_no_element_children(named_curve_node, "NamedCurve")?;
ensure_no_non_whitespace_text(named_curve_node, "NamedCurve")?;
let Some((curve_oid, expected_public_key_len)) =
(match parse_ec_named_curve_oid(named_curve_node) {
Ok(curve) => curve,
Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue),
})
else {
return Ok(KeyValueInfo::Unsupported {
namespace: Some(XMLDSIG11_NS.to_string()),
local_name: "ECKeyValue".into(),
});
};
let Some(public_key_node) = children.next() else {
return Ok(KeyValueInfo::InvalidEcKeyValue);
};
if public_key_node.tag_name().namespace() != Some(XMLDSIG11_NS)
|| public_key_node.tag_name().name() != "PublicKey"
{
return Ok(KeyValueInfo::InvalidEcKeyValue);
}
ensure_no_element_children(public_key_node, "PublicKey")?;
if children.next().is_some() {
return Ok(KeyValueInfo::InvalidEcKeyValue);
}
let public_key = match decode_crypto_binary(public_key_node, "PublicKey", MAX_EC_PUBLIC_KEY_LEN)
{
Ok(public_key) => public_key,
Err(_) => return Ok(KeyValueInfo::InvalidEcKeyValue),
};
if validate_ec_public_key_point(&public_key, expected_public_key_len).is_err() {
return Ok(KeyValueInfo::InvalidEcKeyValue);
}
Ok(KeyValueInfo::Ec {
curve_oid,
public_key,
})
}
fn parse_ec_named_curve_oid(node: Node<'_, '_>) -> Result<Option<(String, usize)>, ParseError> {
let uri = node.attribute("URI").ok_or_else(|| {
ParseError::InvalidStructure("ECKeyValue NamedCurve must include URI attribute".into())
})?;
let curve_oid = uri.strip_prefix("urn:oid:").unwrap_or(uri);
if curve_oid.is_empty() {
return Err(ParseError::InvalidStructure(
"ECKeyValue NamedCurve URI must not be empty".into(),
));
}
let Some(public_key_len) = ec_public_key_len(curve_oid) else {
return Ok(None);
};
Ok(Some((curve_oid.to_string(), public_key_len)))
}
fn ec_public_key_len(curve_oid: &str) -> Option<usize> {
match curve_oid {
EC_P256_OID => Some(65),
EC_P384_OID => Some(97),
EC_P521_OID => Some(133),
_ => None,
}
}
fn validate_ec_public_key_point(public_key: &[u8], expected_len: usize) -> Result<(), ParseError> {
if public_key.len() != expected_len {
return Err(ParseError::InvalidStructure(
"ECKeyValue PublicKey length does not match NamedCurve".into(),
));
}
if public_key.first().copied() != Some(0x04) {
return Err(ParseError::InvalidStructure(
"ECKeyValue PublicKey must be an uncompressed SEC1 point".into(),
));
}
Ok(())
}
fn parse_rsa_key_value(node: Node<'_, '_>) -> Result<KeyValueInfo, ParseError> {
verify_ds_element(node, "RSAKeyValue")?;
ensure_no_non_whitespace_text(node, "RSAKeyValue")?;
let mut children = element_children(node);
let modulus_node = children.next().ok_or_else(|| {
ParseError::InvalidStructure("RSAKeyValue requires Modulus and Exponent".into())
})?;
verify_ds_element(modulus_node, "Modulus")?;
ensure_no_element_children(modulus_node, "Modulus")?;
let exponent_node = children.next().ok_or_else(|| {
ParseError::InvalidStructure("RSAKeyValue requires Modulus and Exponent".into())
})?;
verify_ds_element(exponent_node, "Exponent")?;
ensure_no_element_children(exponent_node, "Exponent")?;
if children.next().is_some() {
return Err(ParseError::InvalidStructure(
"RSAKeyValue must contain exactly Modulus followed by Exponent".into(),
));
}
Ok(KeyValueInfo::Rsa {
modulus: decode_crypto_binary(modulus_node, "Modulus", MAX_RSA_MODULUS_LEN)?,
exponent: decode_crypto_binary(exponent_node, "Exponent", MAX_RSA_EXPONENT_LEN)?,
})
}
fn decode_crypto_binary(
node: Node<'_, '_>,
element_name: &'static str,
max_decoded_len: usize,
) -> Result<Vec<u8>, ParseError> {
let max_base64_len = max_decoded_len.div_ceil(3) * 4;
let payload = super::whitespace::XmlBase64Payload::bounded(node, usize::MAX, max_base64_len)
.map_err(|reason| xml_base64_payload_error(element_name, reason))?;
if payload.normalized_len > max_base64_len {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed base64 length"
)));
}
if payload.decoded_len == 0 {
return Err(ParseError::InvalidStructure(format!(
"{element_name} must not be empty"
)));
}
if payload.decoded_len > max_decoded_len {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed binary length"
)));
}
payload
.decode()
.map_err(|reason| ParseError::Base64(format!("{element_name}: {reason}")))
}
pub(crate) fn parse_x509_data_dispatch_with_budget_and_provider(
node: Node,
total_binary_len: &mut usize,
embedded_key_candidates: &mut usize,
provider: &dyn crate::provider::CryptoProvider,
resources: &crate::policy::ResourcePolicy,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
certificates: &super::pkix::CertificateSession,
) -> Result<X509DataInfo, ParseError> {
parse_x509_data_in_session(
node,
total_binary_len,
embedded_key_candidates,
provider,
resources,
allowed,
certificates,
)
}
fn parse_x509_data_in_session(
node: Node,
total_binary_len: &mut usize,
embedded_key_candidates: &mut usize,
provider: &dyn crate::provider::CryptoProvider,
resources: &crate::policy::ResourcePolicy,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
certificates: &super::pkix::CertificateSession,
) -> Result<X509DataInfo, ParseError> {
verify_ds_element(node, "X509Data")?;
ensure_no_non_whitespace_text(node, "X509Data")?;
let mut info = X509DataInfo::default();
for child in element_children(node) {
match (child.tag_name().namespace(), child.tag_name().name()) {
(Some(XMLDSIG_NS), "X509Certificate") => {
charge_embedded_key_candidate(embedded_key_candidates, resources)?;
ensure_no_element_children(child, "X509Certificate")?;
ensure_x509_data_entry_budget(&info)?;
let cert =
decode_x509_base64(child, "X509Certificate", total_binary_len, resources)?;
let parsed_cert = parse_x509_certificate_in(cert.as_slice(), certificates)?;
info.parsed_certificates.push(parsed_cert);
info.certificates.push(cert);
}
(Some(XMLDSIG_NS), "X509SubjectName") => {
ensure_no_element_children(child, "X509SubjectName")?;
ensure_x509_data_entry_budget(&info)?;
let subject_name = collect_text_content_bounded(
child,
MAX_X509_SUBJECT_NAME_TEXT_LEN,
"X509SubjectName",
)?;
info.subject_names.push(subject_name);
}
(Some(XMLDSIG_NS), "X509IssuerSerial") => {
ensure_x509_data_entry_budget(&info)?;
let issuer_serial = parse_x509_issuer_serial(child)?;
info.issuer_serials.push(issuer_serial);
}
(Some(XMLDSIG_NS), "X509SKI") => {
ensure_no_element_children(child, "X509SKI")?;
ensure_x509_data_entry_budget(&info)?;
let ski = decode_x509_base64(child, "X509SKI", total_binary_len, resources)?;
info.skis.push(ski);
}
(Some(XMLDSIG_NS), "X509CRL") => {
ensure_no_element_children(child, "X509CRL")?;
ensure_x509_data_entry_budget(&info)?;
let crl = decode_x509_base64(child, "X509CRL", total_binary_len, resources)?;
info.crls.push(crl);
}
(Some(XMLDSIG11_NS), "X509Digest") => {
ensure_no_element_children(child, "X509Digest")?;
ensure_x509_data_entry_budget(&info)?;
let algorithm = required_algorithm_attr(child, "X509Digest")?;
let digest = decode_x509_base64(child, "X509Digest", total_binary_len, resources)?;
info.digests.push((algorithm.to_string(), digest));
}
(Some(XMLDSIG_NS), child_name) | (Some(XMLDSIG11_NS), child_name) => {
return Err(ParseError::InvalidStructure(format!(
"X509Data contains unsupported XMLDSig child element <{child_name}>"
)));
}
_ => {}
}
}
info.certificate_chain = build_x509_certificate_chain(&info, provider, allowed, certificates)?;
Ok(info)
}
pub(crate) fn validate_x509_digest_policy(
node: Node,
check: &impl Fn(DigestAlgorithm) -> Result<(), crate::policy::PolicyViolation>,
) -> Result<(), ParseError> {
fn check_data(
data: Node,
check: &impl Fn(DigestAlgorithm) -> Result<(), crate::policy::PolicyViolation>,
) -> Result<(), ParseError> {
for child in element_children(data) {
if child.has_tag_name((XMLDSIG11_NS, "X509Digest")) {
let uri = required_algorithm_attr(child, "X509Digest")?;
if let Some(algorithm) = DigestAlgorithm::from_uri(uri) {
check(algorithm)?;
}
}
}
Ok(())
}
if node.has_tag_name((XMLDSIG_NS, "X509Data")) {
return check_data(node, check);
}
for child in element_children(node) {
if child.has_tag_name((XMLDSIG_NS, "X509Data")) {
check_data(child, check)?;
}
}
Ok(())
}
fn build_x509_certificate_chain(
info: &X509DataInfo,
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
certificates: &super::pkix::CertificateSession,
) -> Result<Vec<usize>, ParseError> {
if info.parsed_certificates.is_empty() {
return Ok(Vec::new());
}
let signing_idx = select_x509_signing_certificate(info, provider, certificates)?;
build_x509_certificate_chain_from(info, signing_idx, provider, allowed, certificates)
.map_err(ParseError::from)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum X509ChainBuildError {
Certificate(super::X509ChainError),
Policy(crate::policy::PolicyViolation),
InconsistentMetadata,
DepthExceeded,
Cycle,
IssuerSignatureMismatch,
AmbiguousIssuer,
UnsupportedSignatureAlgorithm { oid: String },
Provider(crate::provider::ProviderError),
}
impl From<super::X509ChainError> for X509ChainBuildError {
fn from(error: super::X509ChainError) -> Self {
match error {
super::X509ChainError::Policy(error) => Self::Policy(error),
super::X509ChainError::Provider(error) => Self::Provider(error),
error => Self::Certificate(error),
}
}
}
impl From<X509ChainBuildError> for ParseError {
fn from(error: X509ChainBuildError) -> Self {
let reason = match error {
X509ChainBuildError::Certificate(error) => {
return match error {
super::X509ChainError::Policy(error) => Self::Policy(error),
super::X509ChainError::Processing(error) => Self::Certificate(error),
error => Self::InvalidStructure(error.to_string()),
};
}
X509ChainBuildError::InconsistentMetadata => {
"X509Data certificate metadata is inconsistent"
}
X509ChainBuildError::DepthExceeded => {
"X509Data certificate chain exceeds maximum depth"
}
X509ChainBuildError::Cycle => "X509Data certificate chain contains a cycle",
X509ChainBuildError::IssuerSignatureMismatch => {
"X509Data issuer candidates do not verify the certificate signature"
}
X509ChainBuildError::AmbiguousIssuer => {
"X509Data certificate chain contains ambiguous issuer certificates"
}
X509ChainBuildError::UnsupportedSignatureAlgorithm { oid } => {
return Self::InvalidStructure(format!(
"X509Data certificate chain uses unsupported signature algorithm {oid}"
));
}
X509ChainBuildError::Provider(error) => return Self::Provider(error),
X509ChainBuildError::Policy(error) => return Self::Policy(error),
};
Self::InvalidStructure(reason.into())
}
}
pub(crate) fn build_x509_certificate_chain_from(
info: &X509DataInfo,
signing_idx: usize,
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
certificates: &super::pkix::CertificateSession,
) -> Result<Vec<usize>, X509ChainBuildError> {
if signing_idx >= info.parsed_certificates.len()
|| info.parsed_certificates.len() != info.certificates.len()
{
return Err(X509ChainBuildError::InconsistentMetadata);
}
let mut chain = vec![signing_idx];
let source = certificates
.read_pool(&info.certificates)
.map_err(X509ChainBuildError::from)?;
loop {
let current_idx = *chain
.last()
.expect("chain starts with signing certificate index");
let current = &source[current_idx];
if certificates
.source_names_equal(current.tbs().subject(), current.tbs().issuer())
.map_err(X509ChainBuildError::from)?
{
break;
}
let mut candidates = Vec::new();
for (idx, cert) in source.iter().enumerate() {
if idx != current_idx
&& certificates
.source_names_equal(cert.tbs().subject(), current.tbs().issuer())
.map_err(X509ChainBuildError::from)?
{
candidates.push(idx);
}
}
let issuer_idx = match candidates.as_slice() {
[] => break,
[issuer_idx] => *issuer_idx,
_ => {
let mut verified = Vec::new();
let mut unsupported_oid = None;
for issuer_idx in candidates {
match certificate_signature_matches_with_provider(
&info.certificates[current_idx],
&info.certificates[issuer_idx],
provider,
allowed,
certificates,
) {
Ok(true) => verified.push(issuer_idx),
Ok(false) => {}
Err(super::X509ChainError::Provider(
crate::provider::ProviderError::Unsupported {
operation: crate::provider::ProviderOperation::VerifyCertificate,
algorithm: Some(oid),
},
)) => {
unsupported_oid.get_or_insert(oid);
}
Err(super::X509ChainError::Provider(error)) => {
return Err(X509ChainBuildError::Provider(error));
}
Err(super::X509ChainError::UnsupportedSignatureAlgorithm { oid }) => {
unsupported_oid.get_or_insert(oid);
}
Err(super::X509ChainError::Policy(error)) => {
return Err(X509ChainBuildError::Policy(error));
}
Err(_) => return Err(X509ChainBuildError::IssuerSignatureMismatch),
}
}
match verified.as_slice() {
[issuer_idx] => *issuer_idx,
[] => {
if let Some(oid) = unsupported_oid {
return Err(X509ChainBuildError::UnsupportedSignatureAlgorithm { oid });
}
return Err(X509ChainBuildError::IssuerSignatureMismatch);
}
_ => return Err(X509ChainBuildError::AmbiguousIssuer),
}
}
};
if chain.contains(&issuer_idx) {
return Err(X509ChainBuildError::Cycle);
}
if chain.len() == crate::hard_limits::X509_CHAIN_DEPTH_CEILING {
return Err(X509ChainBuildError::DepthExceeded);
}
chain.push(issuer_idx);
}
Ok(chain)
}
pub(crate) fn build_x509_certificate_paths_to_trusted_prefix(
info: &impl X509DataSource,
signing_idx: usize,
trusted_prefix_len: usize,
bounds: X509PathBounds,
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
certificates: &super::pkix::CertificateSession,
) -> Result<X509CandidatePaths, X509ChainBuildError> {
if trusted_prefix_len > info.certificate_count() {
return Err(X509ChainBuildError::InconsistentMetadata);
}
build_x509_certificate_paths(
info,
signing_idx,
|index| index < trusted_prefix_len,
false,
bounds,
super::pkix::ProviderVerifier { provider, allowed },
certificates,
)
}
pub(crate) fn build_x509_certificate_paths_to_selector_targets(
info: &impl X509DataSource,
signing_idx: usize,
targets: &[usize],
bounds: X509PathBounds,
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
certificates: &super::pkix::CertificateSession,
) -> Result<X509CandidatePaths, X509ChainBuildError> {
if targets
.iter()
.any(|index| *index >= info.certificate_count())
{
return Err(X509ChainBuildError::InconsistentMetadata);
}
build_x509_certificate_paths(
info,
signing_idx,
|index| targets.contains(&index),
true,
bounds,
super::pkix::ProviderVerifier { provider, allowed },
certificates,
)
}
pub(crate) struct X509PathBounds {
pub(crate) max_depth: usize,
pub(crate) max_candidate_paths: usize,
}
type X509CandidatePaths =
structured_asn1::memory::OwnedVec<structured_asn1::memory::OwnedVec<usize>>;
fn build_x509_certificate_paths(
info: &impl X509DataSource,
signing_idx: usize,
is_terminal: impl Fn(usize) -> bool,
continue_after_terminal: bool,
bounds: X509PathBounds,
verifier: super::pkix::ProviderVerifier<'_>,
certificates: &super::pkix::CertificateSession,
) -> Result<X509CandidatePaths, X509ChainBuildError> {
let X509PathBounds {
max_depth,
max_candidate_paths,
} = bounds;
if signing_idx >= info.certificate_count() {
return Err(X509ChainBuildError::InconsistentMetadata);
}
if max_candidate_paths == 0 {
return Err(X509ChainBuildError::AmbiguousIssuer);
}
let source = certificates
.read_sources(info.encoded_certificates())
.map_err(X509ChainBuildError::from)?;
let memory = certificates.memory();
let allocation_error = |reason| {
X509ChainBuildError::from(certificates.error(structured_pkix::Error::resource(
reason,
"certificate.paths",
0,
)))
};
let mut initial = memory.vector();
initial.try_push(signing_idx).map_err(allocation_error)?;
let mut pending = memory.vector();
pending.try_push(initial).map_err(allocation_error)?;
let mut completed = memory.vector();
let mut generated_paths = 1usize;
let mut depth_exceeded = false;
let mut unsupported_oid = None;
let mut policy_rejection = None;
let mut issuer_cache = memory.vector();
issuer_cache
.try_reserve_exact(info.certificate_count())
.map_err(allocation_error)?;
for _ in 0..info.certificate_count() {
issuer_cache.try_push(None).map_err(allocation_error)?;
}
while let Some(path) = pending.pop() {
memory
.charge_work(info.certificate_count() as u64)
.map_err(allocation_error)?;
let current_idx = *path
.last()
.expect("candidate path starts with signing certificate index");
if is_terminal(current_idx) {
if !continue_after_terminal {
completed.try_push(path).map_err(allocation_error)?;
continue;
}
memory
.charge_work(path.len() as u64)
.map_err(allocation_error)?;
completed
.try_push(path.try_copy().map_err(allocation_error)?)
.map_err(allocation_error)?;
}
if path.len() == max_depth {
depth_exceeded = true;
continue;
}
let current = &source[current_idx];
if issuer_cache[current_idx].is_none() {
let mut verified = memory.vector();
for (issuer_idx, issuer) in source.iter().enumerate() {
if !certificates
.source_names_equal(issuer.tbs().subject(), current.tbs().issuer())
.map_err(X509ChainBuildError::from)?
{
continue;
}
match super::x509::shared_certificate_signature_outcome(
current,
issuer,
verifier.provider,
verifier.allowed,
certificates,
) {
Ok(
structured_pkix::path::Verified::Yes
| structured_pkix::path::Verified::Unsupported,
) => {
verified.try_push(issuer_idx).map_err(allocation_error)?;
}
Ok(structured_pkix::path::Verified::No) => {}
Ok(_) => return Err(X509ChainBuildError::IssuerSignatureMismatch),
Err(super::X509ChainError::Provider(
crate::provider::ProviderError::Unsupported {
operation: crate::provider::ProviderOperation::VerifyCertificate,
algorithm: Some(oid),
},
)) => {
unsupported_oid.get_or_insert(oid);
continue;
}
Err(super::X509ChainError::Provider(error)) => {
return Err(X509ChainBuildError::Provider(error));
}
Err(super::X509ChainError::UnsupportedSignatureAlgorithm { oid }) => {
unsupported_oid.get_or_insert(oid);
break;
}
Err(super::X509ChainError::Policy(error)) => {
if matches!(
error,
crate::policy::PolicyViolation::ResourceLimit { .. }
| crate::policy::PolicyViolation::ResourceLimitExceeded { .. }
) {
return Err(X509ChainBuildError::Policy(error));
}
policy_rejection.get_or_insert(error);
break;
}
Err(_) => return Err(X509ChainBuildError::IssuerSignatureMismatch),
}
}
issuer_cache[current_idx] = Some(verified);
}
let issuers = issuer_cache[current_idx]
.as_ref()
.expect("issuer cache entry was initialized");
for &issuer_idx in issuers {
memory
.charge_work(path.len() as u64 + 1)
.map_err(allocation_error)?;
if path.contains(&issuer_idx) {
continue;
}
if generated_paths == max_candidate_paths {
return Err(X509ChainBuildError::AmbiguousIssuer);
}
generated_paths += 1;
memory
.charge_work(path.len() as u64 + 1)
.map_err(allocation_error)?;
let mut candidate = memory.vector();
candidate
.try_reserve_exact(path.len() + 1)
.map_err(allocation_error)?;
candidate
.try_extend_from_slice(&path)
.map_err(allocation_error)?;
candidate.try_push(issuer_idx).map_err(allocation_error)?;
pending.try_push(candidate).map_err(allocation_error)?;
}
}
if completed.is_empty() {
if let Some(error) = policy_rejection {
return Err(X509ChainBuildError::Policy(error));
}
if let Some(oid) = unsupported_oid {
return Err(X509ChainBuildError::UnsupportedSignatureAlgorithm { oid });
}
if depth_exceeded {
return Err(X509ChainBuildError::DepthExceeded);
}
}
Ok(completed)
}
fn select_x509_signing_certificate(
info: &X509DataInfo,
provider: &dyn crate::provider::CryptoProvider,
certificates: &super::pkix::CertificateSession,
) -> Result<usize, ParseError> {
let has_lookup_identifiers = x509_data_has_lookup_identifiers(info);
let mut candidates = Vec::new();
if has_lookup_identifiers {
for (idx, der) in info.certificates.iter().enumerate() {
if x509_certificate_matches_any_selector(info, der, provider, certificates)? {
candidates.push(idx);
}
}
if !x509_selector_categories_match_chain(info, provider, certificates)? {
return Err(ParseError::InvalidStructure(
"X509Data lookup identifiers do not match the embedded certificate chain".into(),
));
}
}
match candidates.as_slice() {
[idx] => return Ok(*idx),
[] if has_lookup_identifiers => {
return Err(ParseError::InvalidStructure(
"X509Data lookup identifiers do not match any embedded certificate".into(),
));
}
[] => {}
_ => {}
}
let source = certificates
.read_pool(&info.certificates)
.map_err(|error| certificates.parse_error(error))?;
let mut leaf_candidates = Vec::new();
for (idx, cert) in source.iter().enumerate() {
if certificates
.source_names_equal(cert.tbs().subject(), cert.tbs().issuer())
.map_err(|error| certificates.parse_error(error))?
{
continue;
}
let mut issuer = false;
for other in &source {
if certificates
.source_names_equal(other.tbs().issuer(), cert.tbs().subject())
.map_err(|error| certificates.parse_error(error))?
{
issuer = true;
break;
}
}
if !issuer {
leaf_candidates.push(idx);
}
}
let selected_leaves = leaf_candidates
.iter()
.filter(|idx| !has_lookup_identifiers || candidates.contains(idx))
.copied()
.collect::<Vec<_>>();
match selected_leaves.as_slice() {
[idx] => Ok(*idx),
[] if !has_lookup_identifiers => Ok(0),
[] => Err(ParseError::InvalidStructure(
"X509Data lookup identifiers match multiple certificates without a unique signing certificate"
.into(),
)),
_ => Err(ParseError::InvalidStructure(
if has_lookup_identifiers {
"X509Data lookup identifiers match multiple certificates"
} else {
"X509Data contains multiple possible signing certificates"
}
.into(),
)),
}
}
pub(crate) fn x509_data_has_lookup_identifiers(info: &X509DataInfo) -> bool {
!info.subject_names.is_empty()
|| !info.issuer_serials.is_empty()
|| !info.skis.is_empty()
|| !info.digests.is_empty()
}
pub(crate) fn x509_certificate_matches_any_selector(
info: &X509DataInfo,
certificate_der: &[u8],
provider: &dyn crate::provider::CryptoProvider,
certificates: &super::pkix::CertificateSession,
) -> Result<bool, ParseError> {
let decoded = certificates
.read(certificate_der)
.map_err(|error| certificates.parse_error(error))?;
let mut subject_match = false;
for subject in &info.subject_names {
let selector = certificates
.selector(subject)
.map_err(|error| certificates.parse_error(error))?;
subject_match |= selector
.matches_name(decoded.tbs().subject(), certificates.memory())
.map_err(|error| certificates.parse_error(certificates.error(error)))?;
}
let mut issuer_serial_match = false;
for (issuer, serial) in &info.issuer_serials {
let serial = selector_serial(serial, certificates)?;
let selector = certificates
.selector(issuer)
.map_err(|error| certificates.parse_error(error))?;
issuer_serial_match |= selector
.matches_name(decoded.tbs().issuer(), certificates.memory())
.map_err(|error| certificates.parse_error(certificates.error(error)))?
&& source_serial_matches(&serial, &decoded, certificates)?;
}
let mut ski_match = false;
for ski in &info.skis {
ski_match |= source_ski_matches(ski, &decoded, certificates)?;
}
let mut digest_match = false;
for (algorithm_uri, expected) in &info.digests {
let algorithm = DigestAlgorithm::from_uri(algorithm_uri).ok_or_else(|| {
ParseError::UnsupportedAlgorithm {
uri: algorithm_uri.clone(),
}
})?;
digest_match |=
source_digest_matches(provider, algorithm, certificate_der, expected, certificates)?;
}
Ok(subject_match || issuer_serial_match || ski_match || digest_match)
}
pub fn x509_certificate_matches_selectors(
info: &X509DataInfo,
certificate_der: &[u8],
provider: &dyn crate::provider::CryptoProvider,
) -> Result<bool, ParseError> {
let certificates =
super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default());
x509_selector_categories_match_candidates(
info,
core::iter::once(certificate_der),
provider,
&certificates,
)
}
pub(crate) fn x509_selector_categories_match_chain(
info: &X509DataInfo,
provider: &dyn crate::provider::CryptoProvider,
certificates: &super::pkix::CertificateSession,
) -> Result<bool, ParseError> {
x509_selector_categories_match_candidates(
info,
info.certificates.iter().map(Vec::as_slice),
provider,
certificates,
)
}
pub(crate) fn x509_selector_categories_match_candidates<'a>(
info: &X509DataInfo,
candidates: impl Iterator<Item = &'a [u8]>,
provider: &dyn crate::provider::CryptoProvider,
certificates: &super::pkix::CertificateSession,
) -> Result<bool, ParseError> {
let mut decoded = certificates.memory().vector();
for der in candidates {
let certificate = certificates
.read(der)
.map_err(|error| certificates.parse_error(error))?;
decoded.try_push(certificate).map_err(|reason| {
certificates.parse_error(certificates.error(structured_pkix::Error::resource(
reason,
"name.candidates",
0,
)))
})?;
}
let mut subject_match = true;
for subject in &info.subject_names {
let selector = certificates
.selector(subject)
.map_err(|error| certificates.parse_error(error))?;
let mut matched = false;
for certificate in &decoded {
if selector
.matches_name(certificate.tbs().subject(), certificates.memory())
.map_err(|error| certificates.parse_error(certificates.error(error)))?
{
matched = true;
break;
}
}
subject_match &= matched;
}
let mut issuer_serial_match = true;
for (issuer, serial) in &info.issuer_serials {
let serial = selector_serial(serial, certificates)?;
let selector = certificates
.selector(issuer)
.map_err(|error| certificates.parse_error(error))?;
let mut matched = false;
for certificate in &decoded {
if source_serial_matches(&serial, certificate, certificates)?
&& selector
.matches_name(certificate.tbs().issuer(), certificates.memory())
.map_err(|error| certificates.parse_error(certificates.error(error)))?
{
matched = true;
break;
}
}
issuer_serial_match &= matched;
}
let mut ski_match = true;
for ski in &info.skis {
let mut matched = false;
for certificate in &decoded {
if source_ski_matches(ski, certificate, certificates)? {
matched = true;
break;
}
}
ski_match &= matched;
}
let mut digest_match = true;
for (algorithm_uri, expected) in &info.digests {
let algorithm = DigestAlgorithm::from_uri(algorithm_uri).ok_or_else(|| {
ParseError::UnsupportedAlgorithm {
uri: algorithm_uri.clone(),
}
})?;
let mut category_match = false;
for certificate in &decoded {
category_match |= source_digest_matches(
provider,
algorithm,
certificate.as_bytes(),
expected,
certificates,
)?;
}
digest_match &= category_match;
}
Ok(subject_match && issuer_serial_match && ski_match && digest_match)
}
#[cfg(test)]
fn distinguished_names_equal(left: &str, right: &str) -> bool {
super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default())
.names_equal(left, right)
.expect("supported test names")
}
fn selector_serial(
serial: &str,
session: &super::pkix::CertificateSession,
) -> Result<[u8; MAX_X509_SERIAL_NUMBER_BYTES], ParseError> {
let work = (serial.len() as u64)
.checked_mul(3)
.and_then(|work| {
work.checked_add(
(MAX_X509_SERIAL_NUMBER_BYTES * MAX_X509_SERIAL_NUMBER_VALUE_DIGITS) as u64,
)
})
.ok_or_else(|| {
session.parse_error(session.error(structured_pkix::Error::resource(
structured_asn1::memory::MemoryError::WorkExceeded,
"certificate.serial_selector",
0,
)))
})?;
session.memory().charge_work(work).map_err(|reason| {
session.parse_error(session.error(structured_pkix::Error::resource(
reason,
"certificate.serial_selector",
0,
)))
})?;
x509_serial_decimal_bytes(serial).ok_or_else(|| {
ParseError::InvalidStructure(
"X509Data lookup identifiers contain an invalid serial number".into(),
)
})
}
fn source_serial_matches(
expected: &[u8; MAX_X509_SERIAL_NUMBER_BYTES],
certificate: &structured_pkix::Certificate<'_>,
session: &super::pkix::CertificateSession,
) -> Result<bool, ParseError> {
let bytes = certificate.tbs().serial().as_bytes();
if bytes.first().is_none_or(|first| first & 0x80 != 0) {
return Ok(false);
}
let bytes = bytes.strip_prefix(&[0]).unwrap_or(bytes);
if bytes.len() > expected.len() {
return Ok(false);
}
session
.memory()
.charge_work(expected.len() as u64 + bytes.len() as u64)
.map_err(|reason| {
session.parse_error(session.error(structured_pkix::Error::resource(
reason,
"certificate.serial_selector",
certificate.span().start,
)))
})?;
let padding = expected.len() - bytes.len();
Ok(expected[..padding].iter().all(|byte| *byte == 0) && expected[padding..] == *bytes)
}
fn source_ski_matches(
expected: &[u8],
certificate: &structured_pkix::Certificate<'_>,
session: &super::pkix::CertificateSession,
) -> Result<bool, ParseError> {
let Some(ski) = certificate
.tbs()
.extensions()
.subject_key_identifier(session.memory())
.map_err(|error| session.parse_error(session.error(error)))?
else {
return Ok(false);
};
if ski.as_bytes().len() != expected.len() {
return Ok(false);
}
session
.memory()
.charge_work(expected.len() as u64)
.map_err(|reason| {
session.parse_error(session.error(structured_pkix::Error::resource(
reason,
"certificate.ski_selector",
certificate.span().start,
)))
})?;
Ok(ski.as_bytes() == expected)
}
fn source_digest_matches(
provider: &dyn crate::provider::CryptoProvider,
algorithm: DigestAlgorithm,
source: &[u8],
expected: &[u8],
session: &super::pkix::CertificateSession,
) -> Result<bool, ParseError> {
if !provider.supports(crate::provider::ProviderCapability::Digest(algorithm)) {
return Err(ParseError::Provider(
crate::provider::ProviderError::Unsupported {
operation: crate::provider::ProviderOperation::Digest,
algorithm: Some(algorithm.uri().into()),
},
));
}
if expected.len() != algorithm.output_len() {
return Ok(false);
}
let resource = |reason| {
session.parse_error(session.error(structured_pkix::Error::resource(
reason,
"certificate.digest_selector",
0,
)))
};
session
.memory()
.charge_work(source.len() as u64 + expected.len() as u64)
.map_err(resource)?;
let _output = session
.memory()
.reserve(algorithm.output_len())
.map_err(resource)?;
let actual = compute_digest_with_provider(provider, algorithm, source)?;
Ok(constant_time_eq(&actual, expected))
}
fn ensure_x509_data_entry_budget(info: &X509DataInfo) -> Result<(), ParseError> {
let total_entries = info.certificates.len()
+ info.subject_names.len()
+ info.issuer_serials.len()
+ info.skis.len()
+ info.crls.len()
+ info.digests.len();
if total_entries >= MAX_X509_DATA_ENTRY_COUNT {
return Err(ParseError::InvalidStructure(
"X509Data contains too many entries".into(),
));
}
Ok(())
}
fn add_x509_data_usage(total_binary_len: &mut usize, delta: usize) -> Result<(), ParseError> {
*total_binary_len = total_binary_len.checked_add(delta).ok_or_else(|| {
ParseError::InvalidStructure("X509Data exceeds maximum allowed total binary length".into())
})?;
if *total_binary_len > MAX_X509_DATA_TOTAL_BINARY_LEN {
return Err(ParseError::InvalidStructure(
"X509Data exceeds maximum allowed total binary length".into(),
));
}
Ok(())
}
fn decode_x509_base64(
node: Node<'_, '_>,
element_name: &'static str,
total_binary_len: &mut usize,
resources: &crate::policy::ResourcePolicy,
) -> Result<Vec<u8>, ParseError> {
let payload = super::whitespace::XmlBase64Payload::bounded(
node,
MAX_X509_BASE64_TEXT_LEN,
MAX_X509_BASE64_NORMALIZED_LEN,
)
.map_err(|reason| xml_base64_payload_error(element_name, reason))?;
if payload.text_len > MAX_X509_BASE64_TEXT_LEN {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed text length"
)));
}
if payload.normalized_len > MAX_X509_BASE64_NORMALIZED_LEN {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed base64 length"
)));
}
if payload.decoded_len == 0 {
return Err(ParseError::InvalidStructure(format!(
"{element_name} must not be empty"
)));
}
if payload.decoded_len > MAX_X509_DECODED_BINARY_LEN {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed binary length"
)));
}
if payload.decoded_len > resources.max_external_resource_bytes {
return Err(crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_bytes,
}
.into());
}
if *total_binary_len > resources.max_external_resource_total_bytes
|| payload.decoded_len > resources.max_external_resource_total_bytes - *total_binary_len
{
return Err(crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_total_bytes,
}
.into());
}
add_x509_data_usage(total_binary_len, payload.decoded_len)?;
payload
.decode()
.map_err(|reason| ParseError::Base64(format!("{element_name}: {reason}")))
}
fn xml_base64_payload_error(element: &str, reason: &'static str) -> ParseError {
match reason {
"maximum allowed text length" | "maximum allowed base64 length" => {
ParseError::InvalidStructure(format!("{element} exceeds {reason}"))
}
_ => ParseError::Base64(format!("{element}: {reason}")),
}
}
#[cfg(test)]
pub(crate) fn parse_x509_certificate(cert_der: &[u8]) -> Result<ParsedX509Certificate, ParseError> {
let session = super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default());
parse_x509_certificate_in(cert_der, &session)
}
pub(crate) fn parse_x509_certificate_in(
cert_der: &[u8],
session: &super::pkix::CertificateSession,
) -> Result<ParsedX509Certificate, ParseError> {
let cert = session
.read(cert_der)
.map_err(|error| session.parse_error(error))?;
certificate_metadata(&cert, session)
}
pub(crate) fn certificate_metadata(
cert: &structured_pkix::Certificate<'_>,
session: &super::pkix::CertificateSession,
) -> Result<ParsedX509Certificate, ParseError> {
let subject_dn = shared_name_text(cert.tbs().subject(), session)?;
let issuer_dn = shared_name_text(cert.tbs().issuer(), session)?;
let serial_number = cert.tbs().serial().as_bytes().to_vec();
let serial_number_hex = format_x509_serial_value_hex(&serial_number);
let subject_key_identifier = cert
.tbs()
.extensions()
.subject_key_identifier(session.memory())
.map_err(|error| session.parse_error(session.error(error)))?
.map(|ski| ski.as_bytes().to_vec());
let spki = cert.tbs().public_key();
let public_key = if spki.algorithm().algorithm() == structured_pkix::oid::RSA_ENCRYPTION {
let rsa = structured_pkix::key::rsa::read(spki, session.memory()).map_err(|error| {
if error.is_resource() {
session.parse_error(session.error(error))
} else {
ParseError::InvalidStructure(format!("X509Certificate RSA key: {error}"))
}
})?;
X509PublicKeyInfo::Rsa {
modulus: rsa.modulus().as_bytes().to_vec(),
exponent: rsa.exponent().as_bytes().to_vec(),
}
} else if spki.algorithm().algorithm() == structured_pkix::oid::EC_PUBLIC_KEY {
let key_octets = spki.key_octets().ok_or_else(|| {
ParseError::InvalidStructure(
"X509Certificate EC public key is not octet-aligned".into(),
)
})?;
let Some(params) = spki.algorithm().parameters() else {
return Err(ParseError::InvalidStructure(
"X509Certificate EC key is missing curve parameters".into(),
));
};
match <ObjectIdentifier>::from_der(params) {
Ok(oid) => X509PublicKeyInfo::Ec {
curve_oid: oid.to_string(),
public_key: key_octets.to_vec(),
},
Err(_) => X509PublicKeyInfo::Unsupported {
algorithm_oid: shared_oid_text(spki.algorithm().algorithm(), session)?,
},
}
} else {
X509PublicKeyInfo::Unsupported {
algorithm_oid: shared_oid_text(spki.algorithm().algorithm(), session)?,
}
};
Ok(ParsedX509Certificate {
subject_dn,
issuer_dn,
serial_number,
serial_number_hex,
subject_key_identifier,
public_key,
})
}
fn shared_name_text(
name: &structured_pkix::name::Name<'_>,
session: &super::pkix::CertificateSession,
) -> Result<String, ParseError> {
let text = name
.rfc4514_readable(session.memory())
.map_err(|error| session.parse_error(session.error(error)))?;
text.as_str(session.memory())
.map(str::to_owned)
.map_err(|reason| {
session.parse_error(session.error(structured_pkix::Error::resource(reason, "name", 0)))
})
}
fn shared_oid_text(
oid: structured_asn1::Oid<'_>,
session: &super::pkix::CertificateSession,
) -> Result<String, ParseError> {
let text = oid.text(session.memory()).map_err(|error| {
session.parse_error(session.error(structured_pkix::Error::resource(error, "algorithm", 0)))
})?;
text.as_str(session.memory())
.map(str::to_owned)
.map_err(|error| {
session.parse_error(session.error(structured_pkix::Error::resource(
error,
"algorithm",
0,
)))
})
}
fn format_x509_serial_hex(serial: &[u8]) -> String {
serial
.iter()
.map(|byte| format!("{byte:02X}"))
.collect::<String>()
}
fn format_x509_serial_value_hex(serial: &[u8]) -> String {
let first_non_zero = serial
.iter()
.position(|byte| *byte != 0)
.unwrap_or(serial.len());
let canonical = if first_non_zero == serial.len() {
&[0]
} else {
&serial[first_non_zero..]
};
format_x509_serial_hex(canonical)
}
fn x509_serial_decimal_bytes(serial: &str) -> Option<[u8; MAX_X509_SERIAL_NUMBER_BYTES]> {
let serial = serial.trim();
let serial = serial.strip_prefix('+').unwrap_or(serial);
let serial = serial.trim_start_matches('0');
let serial = if serial.is_empty() { "0" } else { serial };
if serial.len() > MAX_X509_SERIAL_NUMBER_VALUE_DIGITS
|| !serial.bytes().all(|byte| byte.is_ascii_digit())
{
return None;
}
let mut bytes = [0_u8; MAX_X509_SERIAL_NUMBER_BYTES];
for digit in serial.bytes().map(|byte| byte - b'0') {
let mut carry = u16::from(digit);
for byte in bytes.iter_mut().rev() {
let value = u16::from(*byte) * 10 + carry;
*byte = value as u8;
carry = value >> 8;
}
if carry != 0 {
return None;
}
}
if bytes.iter().all(|byte| *byte == 0) {
return None;
}
Some(bytes)
}
#[cfg(test)]
fn x509_serial_decimal_to_hex(serial: &str) -> Option<String> {
x509_serial_decimal_bytes(serial).map(|bytes| format_x509_serial_value_hex(&bytes))
}
fn parse_x509_issuer_serial(node: Node<'_, '_>) -> Result<(String, String), ParseError> {
verify_ds_element(node, "X509IssuerSerial")?;
ensure_no_non_whitespace_text(node, "X509IssuerSerial")?;
let children = element_children(node).collect::<Vec<_>>();
if children.len() != 2 {
return Err(ParseError::InvalidStructure(
"X509IssuerSerial must contain exactly X509IssuerName then X509SerialNumber".into(),
));
}
if !matches!(
(
children[0].tag_name().namespace(),
children[0].tag_name().name()
),
(Some(XMLDSIG_NS), "X509IssuerName")
) {
return Err(ParseError::InvalidStructure(
"X509IssuerSerial must contain X509IssuerName as the first child element".into(),
));
}
if !matches!(
(
children[1].tag_name().namespace(),
children[1].tag_name().name()
),
(Some(XMLDSIG_NS), "X509SerialNumber")
) {
return Err(ParseError::InvalidStructure(
"X509IssuerSerial must contain X509SerialNumber as the second child element".into(),
));
}
let issuer_node = children[0];
ensure_no_element_children(issuer_node, "X509IssuerName")?;
let issuer_name =
collect_text_content_bounded(issuer_node, MAX_X509_ISSUER_NAME_TEXT_LEN, "X509IssuerName")?;
let serial_node = children[1];
ensure_no_element_children(serial_node, "X509SerialNumber")?;
let serial_number = collect_x509_serial_number(serial_node)?;
if issuer_name.trim().is_empty() {
return Err(ParseError::InvalidStructure(
"X509IssuerSerial requires non-empty X509IssuerName and X509SerialNumber".into(),
));
}
Ok((issuer_name, serial_number))
}
fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
let expected = digest_method.output_len();
let max_base64_len = expected.div_ceil(3) * 4;
let mut cleaned = String::with_capacity(b64.len().min(max_base64_len));
normalize_xml_base64_text(b64, &mut cleaned).map_err(|err| {
ParseError::Base64(format!(
"invalid XML whitespace U+{:04X} in DigestValue",
err.invalid_byte
))
})?;
if cleaned.len() > max_base64_len {
return Err(ParseError::Base64(
"DigestValue exceeds maximum allowed base64 length".into(),
));
}
let digest = STANDARD
.decode(&cleaned)
.map_err(|e| ParseError::Base64(e.to_string()))?;
let actual = digest.len();
if actual != expected {
return Err(ParseError::DigestLengthMismatch {
algorithm: digest_method.uri(),
expected,
actual,
});
}
Ok(digest)
}
fn decode_digest_value_children(
digest_value_node: Node<'_, '_>,
digest_method: DigestAlgorithm,
) -> Result<Vec<u8>, ParseError> {
let max_base64_len = digest_method.output_len().div_ceil(3) * 4;
let mut cleaned = String::with_capacity(max_base64_len);
for child in digest_value_node.children() {
if child.is_element() {
return Err(ParseError::InvalidStructure(
"DigestValue must not contain element children".into(),
));
}
if let Some(text) = child.text() {
normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
ParseError::Base64(format!(
"invalid XML whitespace U+{:04X} in DigestValue",
err.invalid_byte
))
})?;
if cleaned.len() > max_base64_len {
return Err(ParseError::Base64(
"DigestValue exceeds maximum allowed base64 length".into(),
));
}
}
}
base64_decode_digest(&cleaned, digest_method)
}
fn decode_der_encoded_key_value_base64(node: Node<'_, '_>) -> Result<Vec<u8>, ParseError> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
let mut cleaned = String::new();
let mut raw_text_len = 0usize;
for text in node
.children()
.filter(|child| child.is_text())
.filter_map(|child| child.text())
{
if raw_text_len.saturating_add(text.len()) > MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN {
return Err(ParseError::InvalidStructure(
"DEREncodedKeyValue exceeds maximum allowed text length".into(),
));
}
raw_text_len = raw_text_len.saturating_add(text.len());
normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
ParseError::Base64(format!(
"invalid XML whitespace U+{:04X} in base64 text",
err.invalid_byte
))
})?;
if cleaned.len() > MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN {
return Err(ParseError::InvalidStructure(
"DEREncodedKeyValue exceeds maximum allowed length".into(),
));
}
}
let der = STANDARD
.decode(&cleaned)
.map_err(|e| ParseError::Base64(e.to_string()))?;
if der.is_empty() {
return Err(ParseError::InvalidStructure(
"DEREncodedKeyValue must not be empty".into(),
));
}
if der.len() > MAX_DER_ENCODED_KEY_VALUE_LEN {
return Err(ParseError::InvalidStructure(
"DEREncodedKeyValue exceeds maximum allowed length".into(),
));
}
Ok(der)
}
fn collect_text_content_bounded(
node: Node<'_, '_>,
max_len: usize,
element_name: &'static str,
) -> Result<String, ParseError> {
let mut text = String::new();
for chunk in node
.children()
.filter_map(|child| child.is_text().then(|| child.text()).flatten())
{
if text.len().saturating_add(chunk.len()) > max_len {
return Err(ParseError::InvalidStructure(format!(
"{element_name} exceeds maximum allowed text length"
)));
}
text.push_str(chunk);
}
Ok(text)
}
fn collect_x509_serial_number(node: Node<'_, '_>) -> Result<String, ParseError> {
let mut serial = String::with_capacity(MAX_X509_SERIAL_NUMBER_VALUE_DIGITS);
let mut raw_text_len = 0usize;
let mut trailing_whitespace = false;
let mut explicit_positive = false;
let mut saw_digit = false;
for chunk in node
.children()
.filter_map(|child| child.is_text().then(|| child.text()).flatten())
{
raw_text_len = raw_text_len.saturating_add(chunk.len());
if raw_text_len > MAX_X509_SERIAL_NUMBER_RAW_TEXT_LEN {
return Err(ParseError::InvalidStructure(
"X509SerialNumber exceeds maximum allowed text length".into(),
));
}
for byte in chunk.bytes() {
if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
trailing_whitespace |= explicit_positive || saw_digit;
continue;
}
if byte == b'+' && !saw_digit && !explicit_positive && !trailing_whitespace {
explicit_positive = true;
continue;
}
if trailing_whitespace || !byte.is_ascii_digit() {
return Err(ParseError::InvalidStructure(
"invalid X509SerialNumber decimal value".into(),
));
}
saw_digit = true;
if byte == b'0' && serial.is_empty() {
continue;
}
if serial.len() == MAX_X509_SERIAL_NUMBER_VALUE_DIGITS {
return Err(ParseError::InvalidStructure(
"X509SerialNumber exceeds maximum allowed decimal value".into(),
));
}
serial.push(char::from(byte));
}
}
if !saw_digit {
return Err(ParseError::InvalidStructure(
"X509IssuerSerial requires non-empty X509IssuerName and X509SerialNumber".into(),
));
}
if serial.is_empty() {
serial.push('0');
}
if x509_serial_decimal_bytes(&serial).is_none() {
return Err(ParseError::InvalidStructure(
"invalid X509SerialNumber decimal value or RFC 5280 range".into(),
));
}
Ok(serial)
}
fn ensure_no_element_children(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
if node.children().any(|child| child.is_element()) {
return Err(ParseError::InvalidStructure(format!(
"{element_name} must not contain child elements"
)));
}
Ok(())
}
fn ensure_no_non_whitespace_text(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
ensure_charged_whitespace_text(node, element_name, &crate::c14n::scratch::Native)
}
fn ensure_charged_whitespace_text<'s, S: transforms::ParseStorage<'s>>(
node: Node<'_, '_>,
element_name: &str,
storage: &S,
) -> Result<(), ParseError> {
storage
.charge(node.children().len())
.map_err(super::types::TransformError::from)?;
for child in node.children() {
if let Some(text) = child.text().filter(|_| child.is_text()) {
storage
.charge(text.len())
.map_err(super::types::TransformError::from)?;
if !is_xml_whitespace_only(text) {
return Err(storage.structure(format_args!(
"{element_name} must not contain non-whitespace mixed content"
)));
}
}
}
Ok(())
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
mod tests {
#[test]
fn replay_layout_admits_ignored_siblings_under_current_work() {
use structured_asn1::memory::{Memory, MemoryAccount};
let cost = |comments: usize| {
let source = format!(
"<Reference xmlns='{XMLDSIG_NS}'>{}<DigestMethod Algorithm='urn:historical'/><DigestValue/></Reference>",
"<!--ignored-->".repeat(comments)
);
let document = Document::parse(&source).expect("fixed Reference layout");
let current = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 20)));
let transforms = parse_replay_transforms(
document.root_element(),
&mut transforms::XPathSignatureParseBudget::default(),
&crate::c14n::scratch::Accounted(¤t),
)
.expect("valid empty transform chain");
assert!(transforms.is_empty());
drop(transforms);
assert_eq!(current.used(), 0);
current.work_used()
};
assert!(cost(1024) >= cost(1) + 1023);
}
#[test]
fn inspected_reference_layout_errors_do_not_own_diagnostics() {
let doc = crate::xml::dom::Document::parse(
"<Reference xmlns='http://www.w3.org/2000/09/xmldsig#'><wrong/></Reference>",
)
.unwrap();
let fault = super::inspect_reference_layout(doc.root_element()).unwrap_err();
fn owns_storage<T>(_: &T) -> bool {
std::mem::needs_drop::<T>()
}
assert!(!owns_storage(&fault));
}
#[test]
fn certificate_processing_diagnostic_does_not_blame_der_for_selector() {
let error = super::ParseError::Certificate(structured_pkix::Error::new(
structured_pkix::ErrorCode::UnsupportedNameSyntax,
"name.selector",
0,
));
assert!(error.to_string().starts_with("X.509 processing failed:"));
}
#[test]
fn certificate_spki_inspection_borrows_source_and_retains_ingestion_budget() {
let der = pem::parse(include_bytes!(
"../../tests/fixtures/keys/rsa/rsa-2048-cert.pem"
))
.expect("public certificate PEM")
.into_contents();
let resources = crate::policy::ResourcePolicy {
max_key_candidates: 1,
..crate::policy::ResourcePolicy::default()
};
let mut session = super::KeyInfoParsingSession::new(&resources).expect("resource policy");
session
.with_certificate_public_key(&der, |spki| {
assert!(!spki.is_empty());
assert!(spki.as_ptr() >= der.as_ptr());
assert!(spki.as_ptr() as usize + spki.len() <= der.as_ptr() as usize + der.len());
})
.expect("borrowed SPKI");
assert!(matches!(
session.with_certificate_public_key(&der, |_| panic!("unadmitted inspection")),
Err(super::ParseError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: 1,
actual: 2
}
))
));
}
use super::*;
use crate::xmldsig::TransformError;
use base64::Engine;
fn fixture_rsa_cert_base64() -> String {
fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
}
fn fixture_cert_base64(path: &str) -> String {
match path {
"../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" => {
include_str!("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
}
"../../tests/fixtures/keys/rsa/rsa-4096-cert.pem" => {
include_str!("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem")
}
"../../tests/fixtures/keys/ca2cert.pem" => {
include_str!("../../tests/fixtures/keys/ca2cert.pem")
}
"../../tests/fixtures/keys/cacert.pem" => {
include_str!("../../tests/fixtures/keys/cacert.pem")
}
_ => unreachable!("unknown certificate fixture"),
}
.lines()
.skip_while(|line| *line != "-----BEGIN CERTIFICATE-----")
.skip(1)
.take_while(|line| *line != "-----END CERTIFICATE-----")
.collect::<String>()
}
#[test]
fn signature_algorithm_from_uri_rsa_sha256() {
assert_eq!(
SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
Some(SignatureAlgorithm::RsaSha256)
);
}
#[test]
fn signature_algorithm_from_uri_rsa_sha1() {
assert_eq!(
SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
Some(SignatureAlgorithm::RsaSha1)
);
}
#[test]
fn signature_algorithm_from_uri_ecdsa_sha256() {
assert_eq!(
SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
Some(SignatureAlgorithm::EcdsaSha256)
);
}
#[test]
fn signature_algorithm_from_uri_unknown() {
assert_eq!(
SignatureAlgorithm::from_uri("http://example.com/unknown"),
None
);
}
#[test]
fn signature_algorithm_uri_round_trip() {
for algo in SignatureAlgorithm::ALL {
assert_eq!(
SignatureAlgorithm::from_uri(algo.uri()),
Some(algo),
"round-trip failed for {algo:?}"
);
}
}
#[test]
fn legacy_algorithms_are_verify_only() {
assert!(!SignatureAlgorithm::DsaSha1.signing_allowed());
assert!(!SignatureAlgorithm::HmacSha1.signing_allowed());
assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
assert!(SignatureAlgorithm::EcdsaSha256.signing_allowed());
}
#[test]
fn find_signature_in_saml() {
let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo/>
</ds:Signature>
</samlp:Response>"#;
let doc = Document::parse(xml).unwrap();
let sig = find_signature_node(&doc);
assert!(sig.is_some());
assert_eq!(sig.unwrap().tag_name().name(), "Signature");
}
#[test]
fn find_signature_missing() {
let xml = "<root><child/></root>";
let doc = Document::parse(xml).unwrap();
assert!(find_signature_node(&doc).is_none());
}
#[test]
fn find_signature_ignores_wrong_namespace() {
let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
let doc = Document::parse(xml).unwrap();
assert!(find_signature_node(&doc).is_none());
}
#[test]
fn key_info_session_applies_x509_byte_limits_before_decode() {
let document = Document::parse("<KeyInfo xmlns='http://www.w3.org/2000/09/xmldsig#'><X509Data><X509SKI>AQID</X509SKI></X509Data></KeyInfo>").unwrap();
for individual in [true, false] {
let resources = crate::policy::ResourcePolicy {
max_external_resource_bytes: if individual { 2 } else { 3 },
max_external_resource_total_bytes: if individual { 100 } else { 5 },
..Default::default()
};
let mut session = KeyInfoParsingSession::new(&resources).unwrap();
if !individual {
session.parse(document.root_element()).unwrap();
}
assert!(matches!(
session.parse(document.root_element()),
Err(ParseError::Policy(_))
));
}
}
#[test]
fn all_x509_binary_fields_preflight_active_byte_policy() {
for element in ["X509Certificate", "X509SKI", "X509CRL", "X509Digest"] {
let namespace = if element == "X509Digest" {
XMLDSIG11_NS
} else {
XMLDSIG_NS
};
let xml = format!(
"<KeyInfo xmlns='{XMLDSIG_NS}'><X509Data><v:{element} xmlns:v='{namespace}' Algorithm='test'>AQID</v:{element}></X509Data></KeyInfo>"
);
let document = Document::parse(&xml).unwrap();
let resources = crate::policy::ResourcePolicy {
max_external_resource_bytes: 2,
..Default::default()
};
let mut session = KeyInfoParsingSession::new(&resources).unwrap();
assert!(
matches!(
session.parse(document.root_element()),
Err(ParseError::Policy(_))
),
"{element}"
);
}
}
#[test]
fn failed_x509_der_parse_keeps_session_byte_charge() {
let invalid = Document::parse("<KeyInfo xmlns='http://www.w3.org/2000/09/xmldsig#'><X509Data><X509Certificate>AQID</X509Certificate></X509Data></KeyInfo>").unwrap();
let valid = Document::parse("<KeyInfo xmlns='http://www.w3.org/2000/09/xmldsig#'><X509Data><X509SKI>AQID</X509SKI></X509Data></KeyInfo>").unwrap();
let resources = crate::policy::ResourcePolicy {
max_external_resource_bytes: 3,
max_external_resource_total_bytes: 5,
..Default::default()
};
let mut session = KeyInfoParsingSession::new(&resources).unwrap();
assert!(matches!(
session.parse(invalid.root_element()),
Err(ParseError::Certificate(error)) if !error.is_resource()
));
assert!(matches!(
session.parse(valid.root_element()),
Err(ParseError::Policy(_))
));
assert!(
KeyInfoParsingSession::new(&resources)
.unwrap()
.parse(valid.root_element())
.is_ok()
);
}
#[test]
fn key_info_session_retains_failed_work_and_applies_active_policy() {
let resources = crate::policy::ResourcePolicy {
max_key_candidates: 1,
..Default::default()
};
let xml = "<KeyInfo xmlns='http://www.w3.org/2000/09/xmldsig#'><KeyValue/></KeyInfo>";
let document = Document::parse(xml).unwrap();
let mut session = KeyInfoParsingSession::new(&resources).unwrap();
assert!(matches!(
session.parse(document.root_element()),
Err(ParseError::InvalidStructure(_))
));
assert!(matches!(
session.parse(document.root_element()),
Err(ParseError::Policy(_))
));
let zero = crate::policy::ResourcePolicy {
max_key_candidates: 0,
..resources
};
assert!(matches!(
KeyInfoParsingSession::new(&zero)
.unwrap()
.parse(document.root_element()),
Err(ParseError::Policy(_))
));
}
#[test]
fn key_info_candidate_budget_precedes_key_value_parsing() {
let document = Document::parse(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><RSAKeyValue><Modulus>AQAB</Modulus></RSAKeyValue></KeyValue>
</KeyInfo>"#,
)
.expect("fixed KeyInfo fixture must parse as XML");
let resources = crate::policy::ResourcePolicy {
max_key_candidates: 0,
..crate::policy::ResourcePolicy::default()
};
let error = parse_key_info_with_policy_budgets(
document.root_element(),
crate::provider::default_provider(),
&XmlBaseResolutionBudget::default(),
&resources,
None,
)
.expect_err("candidate policy must reject KeyValue before RSA parsing");
assert!(matches!(
error,
ParseError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: 0,
actual: 1,
})
));
}
#[test]
fn key_info_candidate_budget_precedes_der_key_decoding() {
let document = Document::parse(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
</KeyInfo>"#,
)
.expect("fixed KeyInfo fixture must parse as XML");
let resources = crate::policy::ResourcePolicy {
max_key_candidates: 0,
..crate::policy::ResourcePolicy::default()
};
let error = parse_key_info_with_policy_budgets(
document.root_element(),
crate::provider::default_provider(),
&XmlBaseResolutionBudget::default(),
&resources,
None,
)
.expect_err("candidate policy must reject DEREncodedKeyValue before decoding");
assert!(matches!(
error,
ParseError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: 0,
actual: 1,
})
));
}
#[test]
fn key_info_embedded_candidate_count_matches_materialized_key_kinds() {
let key_info = KeyInfo {
sources: vec![
KeyInfoSource::KeyName("configured-key".into()),
KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
namespace: Some(XMLDSIG_NS.into()),
local_name: "FutureKeyValue".into(),
}),
KeyInfoSource::DerEncodedKeyValue(vec![1]),
KeyInfoSource::X509Data(X509DataInfo {
certificates: vec![vec![2], vec![3]],
..X509DataInfo::default()
}),
KeyInfoSource::RetrievalMethod {
uri: "urn:certificate".into(),
resource_type: None,
transforms: RetrievalMethodTransforms::None,
},
],
};
assert_eq!(key_info.embedded_candidate_count(), 4);
}
#[test]
fn parse_key_info_dispatches_supported_children() {
let cert_base64 = fixture_rsa_cert_base64();
let expected_cert = base64::engine::general_purpose::STANDARD
.decode(&cert_base64)
.expect("fixture PEM must contain valid base64");
let cert_digest = base64::engine::general_purpose::STANDARD
.encode(compute_digest(DigestAlgorithm::Sha256, &expected_cert));
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyName>idp-signing-key</KeyName>
<KeyValue>
<RSAKeyValue>
<Modulus>AQAB</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
</KeyValue>
<X509Data>
<X509Certificate>{cert_base64}</X509Certificate>
<X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
<X509IssuerSerial>
<X509IssuerName>Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509IssuerName>
<X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
</X509IssuerSerial>
<X509SKI>bcOXN/nsVl8GatRbcKrPbzIbw0Y=</X509SKI>
<X509CRL>BAUGBw==</X509CRL>
<dsig11:X509Digest Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">{cert_digest}</dsig11:X509Digest>
</X509Data>
<dsig11:DEREncodedKeyValue>AQIDBA==</dsig11:DEREncodedKeyValue>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(key_info.sources.len(), 4);
assert_eq!(
key_info.sources[0],
KeyInfoSource::KeyName("idp-signing-key".to_string())
);
assert_eq!(
key_info.sources[1],
KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
modulus: vec![1, 0, 1],
exponent: vec![1, 0, 1],
})
);
let x509_info = match &key_info.sources[2] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificates, vec![expected_cert]);
assert_eq!(
x509_info.subject_names,
vec![
"CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US"
.to_string()
]
);
assert_eq!(
x509_info.issuer_serials,
vec![(
"Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US".to_string(),
"680572598617295163017172295025714171905498632019".to_string()
)]
);
assert_eq!(
x509_info.skis,
vec![vec![
109, 195, 151, 55, 249, 236, 86, 95, 6, 106, 212, 91, 112, 170, 207, 111, 50, 27,
195, 70
]]
);
assert_eq!(x509_info.crls, vec![vec![4, 5, 6, 7]]);
assert_eq!(
x509_info.digests,
vec![(
"http://www.w3.org/2001/04/xmlenc#sha256".to_string(),
compute_digest(DigestAlgorithm::Sha256, &x509_info.certificates[0])
)]
);
assert_eq!(x509_info.parsed_certificates.len(), 1);
assert_eq!(x509_info.certificate_chain, vec![0]);
let parsed_cert = &x509_info.parsed_certificates[0];
assert!(!parsed_cert.subject_dn.is_empty());
assert!(!parsed_cert.issuer_dn.is_empty());
assert_eq!(
parsed_cert.serial_number_hex,
"7735EE487F6862DAF1B3956D961CCB0FA6F34F53"
);
assert!(parsed_cert.subject_key_identifier.is_some());
assert!(matches!(
parsed_cert.public_key,
X509PublicKeyInfo::Rsa { .. }
));
assert_eq!(
key_info.sources[3],
KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])
);
}
#[test]
fn parse_rsa_key_value_preserves_wrapped_crypto_binary() {
let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><RSAKeyValue>
<Modulus> AQID
BA== </Modulus>
<Exponent> AQAB </Exponent>
</RSAKeyValue></KeyValue>
</KeyInfo>"##;
let doc = Document::parse(xml).unwrap();
assert_eq!(
parse_key_info(doc.root_element()).unwrap().sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
modulus: vec![1, 2, 3, 4],
exponent: vec![1, 0, 1],
})]
);
}
#[test]
fn parse_rsa_key_value_rejects_reordered_parameters() {
let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><RSAKeyValue>
<Exponent>AQAB</Exponent><Modulus>AQID</Modulus>
</RSAKeyValue></KeyValue>
</KeyInfo>"##;
let doc = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element()),
Err(ParseError::InvalidStructure(_))
));
}
#[test]
fn parse_rsa_key_value_rejects_missing_exponent() {
let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><RSAKeyValue><Modulus>AQID</Modulus></RSAKeyValue></KeyValue>
</KeyInfo>"##;
let doc = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element()),
Err(ParseError::InvalidStructure(_))
));
}
#[test]
fn parse_rsa_key_value_rejects_duplicate_exponent() {
let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><RSAKeyValue>
<Modulus>AQID</Modulus><Exponent>AQAB</Exponent><Exponent>AQAB</Exponent>
</RSAKeyValue></KeyValue>
</KeyInfo>"##;
let doc = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element()),
Err(ParseError::InvalidStructure(_))
));
}
#[test]
fn parse_rsa_key_value_rejects_wrong_parameter_namespace() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:bad="urn:bad">
<KeyValue><RSAKeyValue>
<bad:Modulus>AQID</bad:Modulus><Exponent>AQAB</Exponent>
</RSAKeyValue></KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element()),
Err(ParseError::InvalidStructure(_))
));
}
#[test]
fn parse_rsa_key_value_rejects_nested_crypto_binary() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><RSAKeyValue>
<Modulus><chunk>AQID</chunk></Modulus><Exponent>AQAB</Exponent>
</RSAKeyValue></KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element()),
Err(ParseError::InvalidStructure(_))
));
}
#[test]
fn parse_rsa_key_value_rejects_malformed_base64() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><RSAKeyValue>
<Modulus>%%%%</Modulus><Exponent>AQAB</Exponent>
</RSAKeyValue></KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element()),
Err(ParseError::Base64(_))
));
}
#[test]
fn parse_rsa_key_value_rejects_oversized_exponent_before_decode() {
let exponent = "A".repeat(MAX_RSA_EXPONENT_LEN.div_ceil(3) * 4 + 1);
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><RSAKeyValue>
<Modulus>AQID</Modulus><Exponent>{exponent}</Exponent>
</RSAKeyValue></KeyValue>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element()),
Err(ParseError::InvalidStructure(_))
));
}
#[test]
fn parse_key_info_ignores_unknown_children() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<Foo>bar</Foo>
<KeyName>ok</KeyName>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(key_info.sources, vec![KeyInfoSource::KeyName("ok".into())]);
}
#[test]
fn parse_key_info_keyvalue_requires_single_child() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue/>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_accepts_empty_x509data() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data/>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::X509Data(X509DataInfo::default())]
);
}
#[test]
fn parse_key_info_rejects_unknown_xmlsig_child_in_x509data() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<Foo/>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_unknown_xmlsig11_child_in_x509data() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<X509Data>
<dsig11:Foo/>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_without_required_children() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=CA</X509IssuerName>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_issuer_name() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=CA-1</X509IssuerName>
<X509IssuerName>CN=CA-2</X509IssuerName>
<X509SerialNumber>42</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_serial_number() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=CA</X509IssuerName>
<X509SerialNumber>1</X509SerialNumber>
<X509SerialNumber>2</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_whitespace_only_values() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName> </X509IssuerName>
<X509SerialNumber>
</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_wrong_child_order() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509SerialNumber>42</X509SerialNumber>
<X509IssuerName>CN=CA</X509IssuerName>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_issuer_serial_with_extra_child_element() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:foo="urn:example:foo">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=CA</X509IssuerName>
<X509SerialNumber>42</X509SerialNumber>
<foo:Extra/>
</X509IssuerSerial>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_digest_without_algorithm() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<X509Data>
<dsig11:X509Digest>AQID</dsig11:X509Digest>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_invalid_x509_certificate_base64() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>%%%invalid%%%</X509Certificate>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::Base64(_)));
}
#[test]
fn parse_key_info_rejects_x509_data_exceeding_entry_budget() {
let subjects = (0..(MAX_X509_DATA_ENTRY_COUNT + 1))
.map(|idx| format!("<X509SubjectName>CN={idx}</X509SubjectName>"))
.collect::<Vec<_>>()
.join("");
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{subjects}</X509Data></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_data_exceeding_total_binary_budget() {
let payload = base64::engine::general_purpose::STANDARD.encode(vec![0u8; 190_000]);
let entries = (0..6)
.map(|_| format!("<X509SKI>{payload}</X509SKI>"))
.collect::<Vec<_>>()
.join("");
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{entries}</X509Data></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_x509_certificate_with_invalid_der() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>AQID</X509Certificate>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::Certificate(error) if !error.is_resource() && error.field() == "certificate")
);
}
#[test]
fn parse_key_info_rejects_x509_certificate_with_trailing_der_bytes() {
let mut cert = base64::engine::general_purpose::STANDARD
.decode(fixture_rsa_cert_base64())
.unwrap();
cert.extend_from_slice(&[0x00, 0x01]);
let cert_base64 = base64::engine::general_purpose::STANDARD.encode(cert);
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>{cert_base64}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::Certificate(error) if error.code() == structured_pkix::ErrorCode::TrailingData)
);
}
#[test]
fn parse_key_info_marks_unsupported_spki_algorithm_as_unsupported() {
let xml = include_str!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml"
);
let doc = Document::parse(xml).unwrap();
let key_info_node = doc
.descendants()
.find(|node| {
node.is_element()
&& node.tag_name().namespace() == Some(XMLDSIG_NS)
&& node.tag_name().name() == "KeyInfo"
})
.expect("fixture must contain ds:KeyInfo");
let key_info = parse_key_info(key_info_node).expect("KeyInfo parse should succeed");
let x509_info = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificates.len(), 1);
assert_eq!(x509_info.parsed_certificates.len(), 1);
assert_eq!(x509_info.certificate_chain, vec![0]);
let parsed_cert = &x509_info.parsed_certificates[0];
assert!(!parsed_cert.subject_dn.is_empty());
assert!(!parsed_cert.issuer_dn.is_empty());
assert!(parsed_cert.subject_key_identifier.is_some());
assert!(matches!(
parsed_cert.public_key,
X509PublicKeyInfo::Unsupported { .. }
));
}
#[test]
fn parse_key_info_orders_x509_certificate_chain_from_signing_cert() {
let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>{root}</X509Certificate>
<X509Certificate>{intermediate}</X509Certificate>
<X509Certificate>{leaf}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
let x509_info = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
}
#[test]
fn certificate_metadata_preserves_opaque_nonaligned_public_keys()
-> Result<(), Box<dyn std::error::Error>> {
let key = rcgen::KeyPair::generate()?;
let certificate = rcgen::CertificateParams::new(Vec::<String>::new())?.self_signed(&key)?;
let mut bytes = certificate.der().to_vec();
let session =
super::super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default());
let (start, length) = {
let decoded = session.read(&bytes)?;
let octets = decoded
.tbs()
.public_key()
.key_octets()
.ok_or("EC key octets")?;
(
octets.as_ptr() as usize - bytes.as_ptr() as usize,
octets.len(),
)
};
let ec_oid = [0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01];
let oid_start = bytes
.windows(ec_oid.len())
.position(|value| value == ec_oid)
.ok_or("EC algorithm identifier")?;
bytes[oid_start + ec_oid.len() - 1] = 99;
bytes[start - 1] = 1;
bytes[start + length - 1] &= 0xfe;
let metadata = parse_x509_certificate_in(&bytes, &session)?;
assert!(
matches!(metadata.public_key, X509PublicKeyInfo::Unsupported { algorithm_oid }
if algorithm_oid == "1.2.840.10045.2.99")
);
Ok(())
}
#[test]
fn certificate_digest_selector_reserves_work_and_output_before_hashing() {
let source = b"certificate identity bytes";
let expected = super::super::compute_digest(DigestAlgorithm::Sha256, source);
for (work, bytes, resource) in [
(0, usize::MAX, crate::policy::resource_name::X509_WORK),
(
usize::MAX,
expected.len() - 1,
crate::policy::resource_name::X509_WORKSPACE_BYTES,
),
] {
let resources = crate::policy::ResourcePolicy {
max_x509_work: work,
max_x509_workspace_bytes: bytes,
..Default::default()
};
let session = super::super::pkix::CertificateSession::new(&resources);
assert!(
matches!(source_digest_matches(crate::provider::default_provider(), DigestAlgorithm::Sha256, source, &expected, &session),
Err(ParseError::Policy(crate::policy::PolicyViolation::ResourceLimitExceeded { resource: actual, .. })) if actual == resource)
);
assert_eq!(
session.memory().used(),
0,
"failed admission retains no output"
);
}
}
#[test]
fn certificate_selectors_ignore_rewritten_display_metadata() {
let der = base64::engine::general_purpose::STANDARD
.decode(fixture_cert_base64(
"../../tests/fixtures/keys/rsa/rsa-2048-cert.pem",
))
.unwrap();
let mut metadata = parse_x509_certificate(&der).unwrap();
let session =
super::super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default());
let mut serial = X509DataInfo {
issuer_serials: vec![(metadata.issuer_dn.clone(), "1".into())],
certificates: vec![der.clone()],
..Default::default()
};
metadata.serial_number_hex = "01".into();
serial.parsed_certificates.push(metadata.clone());
assert!(
!x509_selector_categories_match_chain(
&serial,
crate::provider::default_provider(),
&session
)
.unwrap()
);
let mut ski = X509DataInfo {
skis: vec![vec![0x12, 0x34]],
certificates: vec![der],
..Default::default()
};
metadata.subject_key_identifier = Some(vec![0x12, 0x34]);
ski.parsed_certificates.push(metadata);
assert!(
!x509_selector_categories_match_chain(
&ski,
crate::provider::default_provider(),
&session
)
.unwrap()
);
}
#[test]
fn chain_discovery_uses_source_names_not_display_metadata() {
let certificates = [
fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem"),
fixture_cert_base64("../../tests/fixtures/keys/cacert.pem"),
]
.map(|encoded| {
base64::engine::general_purpose::STANDARD
.decode(encoded)
.unwrap()
})
.to_vec();
let mut parsed_certificates = certificates
.iter()
.map(|certificate| parse_x509_certificate(certificate).unwrap())
.collect::<Vec<_>>();
parsed_certificates[0].subject_dn = parsed_certificates[0].issuer_dn.clone();
let info = X509DataInfo {
certificates,
parsed_certificates,
..Default::default()
};
let session =
super::super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default());
assert_eq!(
build_x509_certificate_chain_from(
&info,
0,
crate::provider::default_provider(),
None,
&session
)
.unwrap(),
vec![0, 1, 2]
);
}
#[test]
fn chain_builder_matches_x509_equivalent_distinguished_names() {
let certificates = [
fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem"),
fixture_cert_base64("../../tests/fixtures/keys/cacert.pem"),
]
.map(|encoded| {
base64::engine::general_purpose::STANDARD
.decode(encoded)
.unwrap()
})
.to_vec();
let mut parsed_certificates = certificates
.iter()
.map(|certificate| parse_x509_certificate(certificate).unwrap())
.collect::<Vec<_>>();
parsed_certificates[0].issuer_dn = parsed_certificates[1].subject_dn.to_ascii_lowercase();
parsed_certificates[1].issuer_dn = parsed_certificates[2].subject_dn.to_ascii_lowercase();
let info = X509DataInfo {
certificates,
parsed_certificates,
..X509DataInfo::default()
};
assert_eq!(
select_x509_signing_certificate(
&info,
crate::provider::default_provider(),
&super::super::pkix::CertificateSession::new(
&crate::policy::ResourcePolicy::default()
)
)
.unwrap(),
0
);
assert_eq!(
build_x509_certificate_chain_from(
&info,
0,
crate::provider::default_provider(),
None,
&super::super::pkix::CertificateSession::new(
&crate::policy::ResourcePolicy::default()
),
)
.unwrap(),
vec![0, 1, 2]
);
}
#[test]
fn parse_key_info_uses_issuer_serial_to_select_x509_signing_certificate() {
let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509IssuerName>
<X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
</X509IssuerSerial>
<X509Certificate>{root}</X509Certificate>
<X509Certificate>{intermediate}</X509Certificate>
<X509Certificate>{leaf}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
let x509_info = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
}
#[test]
fn parse_key_info_allows_selectors_for_multiple_chain_members() {
let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
<X509SKI>0X0XrEVCio75sBcl1TxymJ2IOiU=</X509SKI>
<X509Certificate>{root}</X509Certificate>
<X509Certificate>{intermediate}</X509Certificate>
<X509Certificate>{leaf}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
let x509_info = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
}
#[test]
fn parse_key_info_uses_decimal_issuer_serial_to_select_x509_signing_certificate() {
let serial = "680572598617295163017172295025714171905498632019";
let padded_serial = format!("{}{}", "0".repeat(64), serial);
assert_eq!(
x509_serial_decimal_to_hex(&padded_serial).as_deref(),
Some("7735EE487F6862DAF1B3956D961CCB0FA6F34F53")
);
let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let other_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509IssuerName>
<X509SerialNumber>{padded_serial}</X509SerialNumber>
</X509IssuerSerial>
<X509Certificate>{root}</X509Certificate>
<X509Certificate>{intermediate}</X509Certificate>
<X509Certificate>{leaf}</X509Certificate>
<X509Certificate>{other_leaf}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
let x509_info = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
other => panic!("expected X509Data source, got {other:?}"),
};
assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
}
#[test]
fn parse_key_info_rejects_ambiguous_x509_signing_certificate_candidates() {
let first_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let second_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>{first_leaf}</X509Certificate>
<X509Certificate>{second_leaf}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_unmatched_x509_lookup_identifier() {
let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>CN=Not The Embedded Certificate</X509SubjectName>
<X509Certificate>{cert}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
);
}
#[test]
fn parse_key_info_rejects_partially_matched_selector_category() {
let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
<X509SubjectName>CN=Not In The Embedded Chain</X509SubjectName>
<X509Certificate>{cert}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
);
}
#[test]
fn parse_key_info_rejects_malformed_issuer_serial_even_with_matching_subject() {
let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
<X509IssuerSerial>
<X509IssuerName>Email=xmlsec@aleksey.com,CN=Aleksey Sanin,OU=Second level CA,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509IssuerName>
<X509SerialNumber>not-a-decimal-serial</X509SerialNumber>
</X509IssuerSerial>
<X509Certificate>{cert}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("invalid X509SerialNumber"))
);
}
#[test]
fn parse_key_info_rejects_unmatched_ski_even_with_matching_subject() {
let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
<X509SKI>AQIDBA==</X509SKI>
<X509Certificate>{cert}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
);
}
#[test]
fn parse_key_info_rejects_lookup_hints_for_different_certificates() {
let first_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let second_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
let xml = format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509SubjectName>CN=Test Key rsa-2048,O=XML Security Library (http://www.aleksey.com/xmlsec),ST=California,C=US</X509SubjectName>
<X509SKI>60zMLKCfzQ3qnXAzABzRNpdgQ8Q=</X509SKI>
<X509Certificate>{first_cert}</X509Certificate>
<X509Certificate>{second_cert}</X509Certificate>
</X509Data>
</KeyInfo>"#
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers match multiple certificates"))
);
}
#[test]
fn configured_certificate_matching_requires_every_x509_selector_category() {
let certificate = base64::engine::general_purpose::STANDARD
.decode(fixture_rsa_cert_base64())
.unwrap();
let parsed = parse_x509_certificate(&certificate).unwrap();
let digest = compute_digest_with_provider(
crate::provider::default_provider(),
DigestAlgorithm::Sha256,
&certificate,
)
.unwrap();
let matching = X509DataInfo {
subject_names: vec![parsed.subject_dn.clone()],
issuer_serials: vec![(
parsed.issuer_dn.clone(),
"680572598617295163017172295025714171905498632019".into(),
)],
skis: vec![parsed.subject_key_identifier.clone().unwrap()],
digests: vec![(DigestAlgorithm::Sha256.uri().into(), digest)],
..X509DataInfo::default()
};
assert!(
x509_certificate_matches_selectors(
&matching,
&certificate,
crate::provider::default_provider()
)
.unwrap()
);
for mismatching in [
X509DataInfo {
subject_names: vec!["CN=other".into()],
..matching.clone()
},
X509DataInfo {
issuer_serials: vec![(parsed.issuer_dn.clone(), "1".into())],
..matching.clone()
},
X509DataInfo {
skis: vec![vec![0]],
..matching.clone()
},
X509DataInfo {
digests: vec![(DigestAlgorithm::Sha256.uri().into(), vec![0; 32])],
..matching.clone()
},
] {
assert!(
!x509_certificate_matches_selectors(
&mismatching,
&certificate,
crate::provider::default_provider()
)
.unwrap()
);
}
}
#[test]
fn build_x509_certificate_chain_rejects_chain_exceeding_max_depth() {
let parameters = (0..=MAX_X509_CHAIN_DEPTH)
.map(|index| {
let mut params = rcgen::CertificateParams::new(Vec::new()).unwrap();
params
.distinguished_name
.push(rcgen::DnType::CommonName, format!("cert-{index}"));
params
})
.collect::<Vec<_>>();
let key = rcgen::KeyPair::generate().unwrap();
let certificates = parameters
.iter()
.enumerate()
.map(|(index, params)| {
let issuer = rcgen::Issuer::from_params(
¶meters[(index + 1).min(MAX_X509_CHAIN_DEPTH)],
&key,
);
params
.clone()
.signed_by(&key, &issuer)
.unwrap()
.der()
.to_vec()
})
.collect::<Vec<_>>();
let parsed_certificates = certificates
.iter()
.map(|certificate| parse_x509_certificate(certificate).unwrap())
.collect();
let info = X509DataInfo {
certificates,
parsed_certificates,
..X509DataInfo::default()
};
let err = build_x509_certificate_chain(
&info,
crate::provider::default_provider(),
None,
&super::super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default()),
)
.unwrap_err();
assert!(
matches!(err, ParseError::InvalidStructure(message) if message.contains("maximum depth"))
);
}
#[test]
fn x509_serial_hex_strips_der_sign_extension_zeroes() {
assert_eq!(format_x509_serial_value_hex(&[0x00, 0xFF]), "FF");
assert_eq!(format_x509_serial_value_hex(&[0x00, 0x7F]), "7F");
assert_eq!(format_x509_serial_value_hex(&[0x00, 0x00]), "00");
}
#[test]
fn x509_serial_decimal_parser_enforces_rfc5280_positive_range() {
let max_serial = "1461501637330902918203684832716283019655932542975";
assert_eq!(
x509_serial_decimal_to_hex(max_serial),
Some("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".into())
);
assert_eq!(
x509_serial_decimal_to_hex("730750818665451459101842416358141509827966271488"),
Some("8000000000000000000000000000000000000000".into())
);
assert_eq!(
x509_serial_decimal_to_hex("0000000000000000000000000000000000000000000000001"),
Some("01".into())
);
assert_eq!(
x509_serial_decimal_to_hex("00000000000000000000000000000000000000000000000001"),
Some("01".into())
);
assert_eq!(x509_serial_decimal_to_hex("+1"), Some("01".into()));
for invalid in [
"",
"0",
"000",
"+0",
"++1",
"-1",
"1a",
"1461501637330902918203684832716283019655932542976",
] {
assert_eq!(
x509_serial_decimal_to_hex(invalid),
None,
"invalid serial {invalid:?} must be rejected"
);
}
}
#[test]
fn parse_x509_serial_normalizes_boundary_whitespace_and_rejects_overflow() {
let max_serial = "1461501637330902918203684832716283019655932542975";
let valid = format!(
"<KeyInfo xmlns=\"{XMLDSIG_NS}\"><X509Data><X509IssuerSerial><X509IssuerName>CN=issuer</X509IssuerName><X509SerialNumber>\n {max_serial}\t</X509SerialNumber></X509IssuerSerial></X509Data></KeyInfo>"
);
let doc = Document::parse(&valid).unwrap();
let parsed = parse_key_info(doc.root_element()).unwrap();
let KeyInfoSource::X509Data(x509) = &parsed.sources[0] else {
panic!("expected X509Data source");
};
assert_eq!(x509.issuer_serials[0].1, max_serial);
let explicit_positive = valid.replace(max_serial, "+42");
let doc = Document::parse(&explicit_positive).unwrap();
let parsed = parse_key_info(doc.root_element()).unwrap();
let KeyInfoSource::X509Data(x509) = &parsed.sources[0] else {
panic!("expected X509Data source");
};
assert_eq!(x509.issuer_serials[0].1, "42");
let overflow = valid.replace(
max_serial,
"1461501637330902918203684832716283019655932542976",
);
let doc = Document::parse(&overflow).unwrap();
assert!(matches!(
parse_key_info(doc.root_element()),
Err(ParseError::InvalidStructure(message))
if message.contains("invalid X509SerialNumber")
));
}
#[test]
fn issuer_selector_matches_a_sign_padded_twenty_octet_serial() {
let serial_hex = "8000000000000000000000000000000000000000";
let mut magnitude = [0; 20];
magnitude[0] = 0x80;
let mut parameters = rcgen::CertificateParams::new(Vec::new()).unwrap();
parameters
.distinguished_name
.push(rcgen::DnType::CommonName, "issuer");
parameters.serial_number = Some(rcgen::SerialNumber::from_slice(&magnitude));
let key = rcgen::KeyPair::generate().unwrap();
let certificate = parameters.self_signed(&key).unwrap();
let metadata = parse_x509_certificate(certificate.der()).unwrap();
assert_eq!(
metadata.serial_number,
[vec![0], magnitude.to_vec()].concat()
);
assert_eq!(metadata.serial_number_hex, serial_hex);
let info = X509DataInfo {
issuer_serials: vec![(
"CN=issuer".into(),
"730750818665451459101842416358141509827966271488".into(),
)],
certificates: vec![certificate.der().to_vec()],
parsed_certificates: vec![metadata],
..X509DataInfo::default()
};
assert!(
x509_selector_categories_match_chain(
&info,
crate::provider::default_provider(),
&super::super::pkix::CertificateSession::new(
&crate::policy::ResourcePolicy::default()
)
)
.unwrap()
);
}
#[test]
fn distinguished_name_matching_preserves_rdn_order() {
assert!(distinguished_names_equal(
"CN=leaf, O=example",
"CN=leaf,O=example"
));
assert!(!distinguished_names_equal(
"CN=leaf,O=example",
"O=example,CN=leaf"
));
}
#[test]
fn distinguished_name_matching_applies_x520_string_preparation() {
assert!(distinguished_names_equal(
"CN= TEST key ,O=Example",
"CN=test key,O=example"
));
assert!(distinguished_names_equal(
"CN=Straße,O=Example",
"CN=STRASSE,O=EXAMPLE"
));
assert!(distinguished_names_equal(
"CN=test+OU=security,O=example",
"OU=SECURITY+CN=TEST,O=EXAMPLE"
));
assert!(!distinguished_names_equal(
"1.2.3.4=#040141,O=example",
"1.2.3.4=#040142,O=example"
));
}
#[test]
fn distinguished_name_matching_applies_ia5_matching_rules() {
assert!(distinguished_names_equal(
"EMAIL=ops@EXAMPLE.COM,DC=EXAMPLE,DC=COM",
"EMAIL=ops@example.com,DC=example,DC=com"
));
assert!(distinguished_names_equal(
"EMAIL=OPS@example.com,DC=example,DC=com",
"EMAIL=ops@example.com,DC=example,DC=com"
));
}
#[test]
fn distinguished_name_matching_handles_rfc4514_escaped_values() {
let value = " leading,plus+equals=slash\\trailing ";
let mut params = rcgen::CertificateParams::new(Vec::new()).unwrap();
params
.distinguished_name
.push(rcgen::DnType::CommonName, value);
let key = rcgen::KeyPair::generate().unwrap();
let certificate = params.self_signed(&key).unwrap();
let parsed = parse_x509_certificate(certificate.der()).unwrap();
assert_eq!(
parsed.subject_dn,
r"CN=\ leading\,plus\+equals\=slash\\trailing\ "
);
assert!(distinguished_names_equal(
r"CN=\ leading\,plus\+equals=slash\\trailing\ ",
&parsed.subject_dn
));
assert!(distinguished_names_equal(
"\n CN=\\ leading\\,plus\\+equals=slash\\\\trailing\\ \n",
&parsed.subject_dn
));
}
#[test]
fn distinguished_name_trailing_escape_covers_all_xml_whitespace() {
for whitespace in [' ', '\t', '\r', '\n'] {
let session = super::super::pkix::CertificateSession::new(
&crate::policy::ResourcePolicy::default(),
);
let escaped = format!("CN=value\\{whitespace}");
if whitespace == ' ' {
assert!(session.names_equal(&escaped, &escaped).unwrap());
} else {
assert!(
matches!(session.selector(&escaped), Err(super::super::X509ChainError::Processing(error))
if error.code() == structured_pkix::ErrorCode::MalformedValue)
);
}
assert!(
session
.names_equal(&format!("CN=value{whitespace}"), "CN=value")
.unwrap()
);
}
}
#[test]
fn parse_key_info_accepts_large_textual_x509_entries_within_entry_budget() {
let issuer_name = "C".repeat(MAX_X509_ISSUER_NAME_TEXT_LEN);
let serial_number = "0".repeat(MAX_X509_SERIAL_NUMBER_VALUE_DIGITS - 1) + "1";
let issuer_serials = (0..52)
.map(|_| {
format!(
"<X509IssuerSerial><X509IssuerName>{issuer_name}</X509IssuerName><X509SerialNumber>{serial_number}</X509SerialNumber></X509IssuerSerial>"
)
})
.collect::<Vec<_>>()
.join("");
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{issuer_serials}</X509Data></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
let parsed = match &key_info.sources[0] {
KeyInfoSource::X509Data(x509) => x509,
_ => panic!("expected X509Data source"),
};
assert_eq!(parsed.issuer_serials.len(), 52);
}
#[test]
fn parse_key_info_bounds_raw_x509_serial_text() {
let serial = "0".repeat(MAX_X509_SERIAL_NUMBER_RAW_TEXT_LEN + 1);
let xml = format!(
"<KeyInfo xmlns=\"{XMLDSIG_NS}\"><X509Data><X509IssuerSerial><X509IssuerName>CN=issuer</X509IssuerName><X509SerialNumber>{serial}</X509SerialNumber></X509IssuerSerial></X509Data></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let error = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(
error,
ParseError::InvalidStructure(reason)
if reason == "X509SerialNumber exceeds maximum allowed text length"
));
}
#[test]
fn parse_key_info_accepts_x509data_with_only_foreign_namespace_children() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:foo="urn:example:foo">
<X509Data>
<foo:Bar/>
</X509Data>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::X509Data(X509DataInfo::default())]
);
}
#[test]
fn parse_key_info_der_encoded_key_value_rejects_invalid_base64() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::Base64(_)));
}
#[test]
fn parse_key_info_der_encoded_key_value_accepts_xml_whitespace() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<dsig11:DEREncodedKeyValue>
AQID
BA==
</dsig11:DEREncodedKeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])]
);
}
#[test]
fn parse_key_info_dispatches_dsig11_ec_keyvalue() {
let public_key = "BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=";
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyValue>
<dsig11:ECKeyValue>
<dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
<dsig11:PublicKey>BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
</dsig11:ECKeyValue>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let expected_public_key = base64::engine::general_purpose::STANDARD
.decode(public_key)
.expect("fixture EC point must be valid base64");
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::Ec {
curve_oid: "1.2.840.10045.3.1.7".into(),
public_key: expected_public_key,
})]
);
}
#[test]
fn parse_ec_key_value_accepts_bare_curve_oid() {
use base64::Engine;
let encoded_public_key = "BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==";
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyValue>
<dsig11:ECKeyValue>
<dsig11:NamedCurve URI="1.3.132.0.34"/>
<dsig11:PublicKey>BO/yd/OZzDfjX4qivDY/vsUIuh6KWAxoxW5P4ukvwd+T6pVljWsX2UBJNNy5MdhTwB8e2YwB8kUbJwdsAS/XGi/fz8unFrs+lVlAgIs6s/xBYFbfUoRiAacD2SpVDe6XBA==</dsig11:PublicKey>
</dsig11:ECKeyValue>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let expected_public_key = base64::engine::general_purpose::STANDARD
.decode(encoded_public_key)
.unwrap();
let sources = parse_key_info(doc.root_element()).unwrap().sources;
assert!(matches!(
&sources[0],
KeyInfoSource::KeyValue(KeyValueInfo::Ec { curve_oid, public_key })
if curve_oid == EC_P384_OID && public_key == &expected_public_key
));
}
#[test]
fn parse_ec_key_value_marks_ec_parameters_as_unsupported() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyValue>
<dsig11:ECKeyValue>
<dsig11:ECParameters/>
<dsig11:PublicKey>BA==</dsig11:PublicKey>
</dsig11:ECKeyValue>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
namespace: Some(XMLDSIG11_NS.to_string()),
local_name: "ECKeyValue".into(),
})]
);
}
#[test]
fn parse_ec_key_value_marks_unsupported_curve_as_unsupported() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyValue>
<dsig11:ECKeyValue>
<dsig11:NamedCurve URI="urn:oid:1.3.132.0.36"/>
<dsig11:PublicKey>BA==</dsig11:PublicKey>
</dsig11:ECKeyValue>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
namespace: Some(XMLDSIG11_NS.to_string()),
local_name: "ECKeyValue".into(),
})]
);
}
#[test]
fn parse_ec_key_value_marks_missing_named_curve_uri_invalid() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyValue>
<dsig11:ECKeyValue>
<dsig11:NamedCurve/>
<dsig11:PublicKey>BA==</dsig11:PublicKey>
</dsig11:ECKeyValue>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
);
}
#[test]
fn parse_ec_key_value_marks_reordered_children_invalid() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyValue>
<dsig11:ECKeyValue>
<dsig11:PublicKey>BJ/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
<dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
</dsig11:ECKeyValue>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
);
}
#[test]
fn parse_ec_key_value_marks_non_uncompressed_point_invalid() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<KeyValue>
<dsig11:ECKeyValue>
<dsig11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
<dsig11:PublicKey>Ap/yaXNlq4FRObyJCBhb5jAz8GVzinK3bBGLjSDfjbJwNfydtgjnlS4EsDmxSRhWyJWq6GIqy5wvnaiARK04uB4=</dsig11:PublicKey>
</dsig11:ECKeyValue>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue)]
);
}
#[test]
fn parse_key_info_marks_ds_namespace_ec_keyvalue_as_unsupported() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue>
<ECKeyValue/>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
namespace: Some(XMLDSIG_NS.to_string()),
local_name: "ECKeyValue".into(),
})]
);
}
#[test]
fn parse_key_info_keeps_unsupported_keyvalue_child_as_marker() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue>
<FutureKeyValue/>
</KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
namespace: Some(XMLDSIG_NS.to_string()),
local_name: "FutureKeyValue".into(),
})]
);
}
#[test]
fn parse_key_info_accepts_supported_x509_retrieval_xpath() {
let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
<Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::dsig:X509Data</XPath>
</Transform></Transforms>
</RetrievalMethod>
</KeyInfo>"##;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert!(matches!(
key_info.sources.as_slice(),
[KeyInfoSource::RetrievalMethod {
uri,
resource_type: Some(resource_type),
transforms: RetrievalMethodTransforms::X509DataNodeSetFilter { .. },
}] if uri == "#keys"
&& resource_type == "http://www.w3.org/2000/09/xmldsig#X509Data"
));
}
#[test]
fn retrieval_xpath_namespace_binding_policy_precedes_materialization() {
let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
<Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::dsig:X509Data</XPath>
</Transform></Transforms>
</RetrievalMethod>
</KeyInfo>"##;
let document = Document::parse(xml).expect("fixed KeyInfo fixture must parse");
let resources = crate::policy::ResourcePolicy {
max_xpath_namespace_bindings: 0,
..crate::policy::ResourcePolicy::default()
};
let error = parse_key_info_with_policy_budgets(
document.root_element(),
crate::provider::default_provider(),
&XmlBaseResolutionBudget::default(),
&resources,
None,
)
.expect_err("zero namespace bindings must reject RetrievalMethod XPath");
assert!(matches!(
error,
ParseError::Transform(TransformError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XPATH_NAMESPACE_BINDINGS,
maximum: 0,
actual: 1,
}
))
));
}
#[test]
fn retrieval_xpath_namespace_byte_policy_precedes_materialization() {
let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
<Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::dsig:X509Data</XPath>
</Transform></Transforms>
</RetrievalMethod>
</KeyInfo>"##;
let document = Document::parse(xml).expect("fixed KeyInfo fixture must parse");
let resources = crate::policy::ResourcePolicy {
max_xpath_namespace_bytes: 0,
..crate::policy::ResourcePolicy::default()
};
let error = parse_key_info_with_policy_budgets(
document.root_element(),
crate::provider::default_provider(),
&XmlBaseResolutionBudget::default(),
&resources,
None,
)
.expect_err("zero namespace bytes must reject RetrievalMethod XPath");
assert!(matches!(
error,
ParseError::Transform(TransformError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::XPATH_NAMESPACE_BYTES,
maximum: 0,
actual,
}
)) if actual == "dsig".len() + XMLDSIG_NS.len()
));
}
#[test]
fn parse_key_info_rejects_oversized_retrieval_method_type() {
let oversized_type = "x".repeat(MAX_KEY_NAME_TEXT_LEN + 1);
let xml = format!(
r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><RetrievalMethod URI="#key" Type="{oversized_type}"/></KeyInfo>"##
);
let document = Document::parse(&xml).unwrap();
assert!(matches!(
parse_key_info(document.root_element()),
Err(ParseError::InvalidStructure(reason))
if reason == "RetrievalMethod Type exceeds maximum length"
));
}
#[test]
fn parse_key_info_bounds_retrieval_method_xml_base_chain() {
let mut xml =
format!(r#"<KeyInfo xmlns="{XMLDSIG_NS}"><RetrievalMethod URI="key.der"/></KeyInfo>"#);
for _ in 0..65 {
xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
}
let document = Document::parse(&xml).unwrap();
let key_info = document
.descendants()
.find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
.unwrap();
assert!(matches!(
parse_key_info(key_info),
Err(ParseError::InvalidStructure(reason))
if reason.contains("XML Base resolution")
));
}
#[test]
fn parse_key_info_normalizes_external_retrieval_without_xml_base() {
let xml = format!(
r#"<KeyInfo xmlns="{XMLDSIG_NS}"><RetrievalMethod URI="https://example.test/a/../key.der"/></KeyInfo>"#
);
let document = Document::parse(&xml).unwrap();
let key_info = parse_key_info(document.root_element()).unwrap();
assert!(matches!(
key_info.sources.as_slice(),
[KeyInfoSource::RetrievalMethod { uri, .. }]
if uri == "https://example.test/key.der"
));
}
#[test]
fn parse_key_info_absolute_retrieval_bypasses_xml_base_chain() {
let mut xml = format!(
r#"<KeyInfo xmlns="{XMLDSIG_NS}"><RetrievalMethod URI="https://example.test/a/../key.der"/></KeyInfo>"#
);
for _ in 0..65 {
xml = format!(r#"<n xml:base="segment/">{xml}</n>"#);
}
let document = Document::parse(&xml).unwrap();
let key_info_node = document
.descendants()
.find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
.unwrap();
let key_info = parse_key_info(key_info_node)
.expect("absolute RetrievalMethod must not consume ancestor-base budget");
assert!(matches!(
key_info.sources.as_slice(),
[KeyInfoSource::RetrievalMethod { uri, .. }]
if uri == "https://example.test/key.der"
));
}
#[test]
fn parse_key_info_accepts_namespace_equivalent_retrieval_xpath_prefix() {
let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
<Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:ds="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::ds:X509Data</XPath>
</Transform></Transforms>
</RetrievalMethod>
</KeyInfo>"##;
let doc = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element())
.unwrap()
.sources
.as_slice(),
[KeyInfoSource::RetrievalMethod {
transforms: RetrievalMethodTransforms::X509DataNodeSetFilter { .. },
..
}]
));
}
#[test]
fn parse_key_info_reads_complete_retrieval_xpath_text() {
let valid = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<RetrievalMethod Type="http://www.w3.org/2000/09/xmldsig#X509Data" URI="#keys">
<Transforms><Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:ds="http://www.w3.org/2000/09/xmldsig#">ancestor-or-self::ds:X509<!-- split -->Data</XPath>
</Transform></Transforms>
</RetrievalMethod>
</KeyInfo>"##;
let document = Document::parse(valid).unwrap();
assert!(parse_key_info(document.root_element()).is_ok());
let unsupported =
valid.replace("X509<!-- split -->Data", "X509Data<!-- split -->[false()]");
let document = Document::parse(&unsupported).unwrap();
assert!(matches!(
parse_key_info(document.root_element()),
Err(ParseError::InvalidStructure(reason))
if reason == "unsupported RetrievalMethod XPath selection"
));
}
#[test]
fn parse_dsa_key_value_accepts_schema_optional_parameters_and_rejects_half_pair() {
let key_info = |parameters: &str| {
format!(
r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><KeyValue><DSAKeyValue>
{parameters}
</DSAKeyValue></KeyValue></KeyInfo>"#
)
};
for parameters in [
"<Y>AQ==</Y>",
"<G>AQ==</G><Y>AQ==</Y>",
"<P>AQ==</P><Q>AQ==</Q><Y>AQ==</Y>",
"<P>AQ==</P><Q>AQ==</Q><G>AQ==</G><Y>AQ==</Y><J>AQ==</J>",
"<Y>AQ==</Y><Seed>AQ==</Seed><PgenCounter>AQ==</PgenCounter>",
"<Y>AQ==</Y><J>AQ==</J><Seed>AQ==</Seed><PgenCounter>AQ==</PgenCounter>",
] {
let xml = key_info(parameters);
let doc = Document::parse(&xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element())
.unwrap()
.sources
.as_slice(),
[KeyInfoSource::KeyValue(KeyValueInfo::Dsa { .. })]
));
}
for invalid_parameters in [
"<P>AQ==</P><Y>AQ==</Y>",
"<Q>AQ==</Q><Y>AQ==</Y>",
"<Y>AQ==</Y><Seed>AQ==</Seed>",
"<Y>AQ==</Y><PgenCounter>AQ==</PgenCounter>",
] {
let xml = key_info(invalid_parameters);
let doc = Document::parse(&xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element()),
Err(ParseError::InvalidStructure(_))
));
}
}
#[test]
fn parse_dsa_crypto_binary_ignores_comment_nodes() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><DSAKeyValue><Y>AQ<!-- split -->ID</Y></DSAKeyValue></KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element())
.unwrap()
.sources
.as_slice(),
[KeyInfoSource::KeyValue(KeyValueInfo::Dsa { y, .. })] if y == &[1, 2, 3]
));
}
#[test]
fn parse_rsa_crypto_binary_ignores_comment_nodes() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue><RSAKeyValue>
<Modulus>AQ<!-- split -->ID</Modulus><Exponent>Aw==</Exponent>
</RSAKeyValue></KeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(doc.root_element())
.unwrap()
.sources
.as_slice(),
[KeyInfoSource::KeyValue(KeyValueInfo::Rsa { modulus, exponent })]
if modulus == &[1, 2, 3] && exponent == &[3]
));
}
#[test]
fn parse_key_info_preserves_advisory_unsupported_retrieval_transform() {
let xml = r##"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<RetrievalMethod URI="#keys" Type="urn:vendor:key"><Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#base64"/>
</Transforms></RetrievalMethod>
<KeyName>fallback</KeyName>
</KeyInfo>"##;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element())
.expect("unsupported advisory retrieval must not reject all KeyInfo sources");
assert!(matches!(
key_info.sources.as_slice(),
[
KeyInfoSource::RetrievalMethod { resource_type: Some(resource_type), .. },
KeyInfoSource::KeyName(name),
] if resource_type == "urn:vendor:key" && name == "fallback"
));
}
#[test]
fn parse_key_info_preserves_empty_same_document_reference_uri() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<dsig11:KeyInfoReference URI=""/>
</KeyInfo>"#;
let document = Document::parse(xml).unwrap();
assert!(matches!(
parse_key_info(document.root_element())
.unwrap()
.sources
.as_slice(),
[KeyInfoSource::KeyInfoReference { uri }] if uri.is_empty()
));
}
#[test]
fn parse_key_info_rejects_excessive_child_sources() {
let children = (0..=64)
.map(|index| format!(r#"<extension xmlns="urn:test" index="{index}"/>"#))
.collect::<String>();
let xml =
format!(r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">{children}</KeyInfo>"#);
let document = Document::parse(&xml).unwrap();
assert!(matches!(
parse_key_info(document.root_element()),
Err(ParseError::InvalidStructure(reason))
if reason == "KeyInfo contains too many child elements"
));
}
#[test]
fn parse_key_info_rejects_keyname_with_child_elements() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName>ok<foo/></KeyName>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_preserves_keyname_text_without_trimming() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName> signing key </KeyName>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let key_info = parse_key_info(doc.root_element()).unwrap();
assert_eq!(
key_info.sources,
vec![KeyInfoSource::KeyName(" signing key ".into())]
);
}
#[test]
fn parse_key_info_rejects_oversized_keyname_text() {
let oversized = "A".repeat(4097);
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><KeyName>{oversized}</KeyName></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_non_whitespace_mixed_content() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">oops<KeyName>k</KeyName></KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_rejects_nbsp_as_non_xml_whitespace_mixed_content() {
let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\u{00A0}<KeyName>k</KeyName></KeyInfo>";
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_der_encoded_key_value_rejects_oversized_payload() {
let oversized =
base64::engine::general_purpose::STANDARD
.encode(vec![0u8; MAX_DER_ENCODED_KEY_VALUE_LEN + 1]);
let xml = format!(
"<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>{oversized}</dsig11:DEREncodedKeyValue></KeyInfo>"
);
let doc = Document::parse(&xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_der_encoded_key_value_rejects_empty_payload() {
let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
<dsig11:DEREncodedKeyValue>
</dsig11:DEREncodedKeyValue>
</KeyInfo>"#;
let doc = Document::parse(xml).unwrap();
let err = parse_key_info(doc.root_element()).unwrap_err();
assert!(matches!(err, ParseError::InvalidStructure(_)));
}
#[test]
fn parse_key_info_der_encoded_key_value_non_xml_ascii_whitespace_is_not_parseable_xml() {
let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>\u{000C}</dsig11:DEREncodedKeyValue></KeyInfo>";
assert!(Document::parse(xml).is_err());
}
#[test]
fn parse_hmac_output_length_reads_all_text_nodes() {
let xml = r#"<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#">
<HMACOutputLength>8<!-- split -->0</HMACOutputLength>
</SignatureMethod>"#;
let document = Document::parse(xml).unwrap();
assert_eq!(
parse_hmac_output_length(document.root_element(), SignatureAlgorithm::HmacSha1)
.unwrap(),
Some(80)
);
}
#[test]
fn hmac_sha256_accepts_byte_aligned_output_length() {
let document = Document::parse(
r#"<ds:SignatureMethod xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"><ds:HMACOutputLength>128</ds:HMACOutputLength></ds:SignatureMethod>"#,
)
.unwrap();
let algorithm =
SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#hmac-sha256")
.expect("HMAC-SHA256 must be recognized");
assert_eq!(
parse_hmac_output_length(document.root_element(), algorithm).unwrap(),
Some(128)
);
}
#[test]
fn parse_hmac_output_length_rejects_hidden_suffix_text() {
let xml = r#"<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#">
<HMACOutputLength>80<!-- split -->0</HMACOutputLength>
</SignatureMethod>"#;
let document = Document::parse(xml).unwrap();
assert!(matches!(
parse_hmac_output_length(document.root_element(), SignatureAlgorithm::HmacSha1),
Err(ParseError::InvalidStructure(reason))
if reason == "HMACOutputLength must be a positive byte-aligned value no greater than 160"
));
}
#[test]
fn hmac_output_length_rejects_non_xml_whitespace() {
for separator in ['\u{a0}', '\u{2003}', '\u{85}'] {
let text = format!("{separator}128{separator}");
assert!(ByteAlignedHmacOutputLength::parse(&text, 256).is_err());
}
assert_eq!(
ByteAlignedHmacOutputLength::parse(" \t\r\n+00128 \t", 256)
.unwrap()
.bits(),
128
);
}
#[test]
fn xmldsig_1_1_rejects_non_octet_hmac_output_length() {
let xml = r#"<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#">
<HMACOutputLength>129</HMACOutputLength>
</SignatureMethod>"#;
let document = Document::parse(xml).unwrap();
assert!(matches!(
parse_hmac_output_length(document.root_element(), SignatureAlgorithm::HmacSha256),
Err(ParseError::InvalidStructure(reason))
if reason == "HMACOutputLength must be a positive byte-aligned value no greater than 256"
));
}
#[test]
fn parse_signed_info_rsa_sha256_with_reference() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
assert_eq!(si.references.len(), 1);
let r = &si.references[0];
assert_eq!(r.uri.as_deref(), Some(""));
assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
assert_eq!(r.digest_value, vec![0u8; 32]);
assert_eq!(r.transforms.len(), 2);
}
#[test]
fn parse_signed_info_multiple_references() {
let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
<Reference URI="#a">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
<Reference URI="#b">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"##;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaSha256);
assert_eq!(si.references.len(), 2);
assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
}
#[test]
fn parse_signed_info_rejects_too_many_references() {
let references = (0..=MAX_REFERENCES_PER_SIGNATURE)
.map(|index| {
format!(
r##"<Reference URI="#item-{index}">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>"##
)
})
.collect::<String>();
let xml = format!(
r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
{references}
</SignedInfo>"#
);
let document = Document::parse(&xml).expect("fixed oversized fixture must parse");
let error = parse_signed_info(document.root_element())
.expect_err("the parser must reject the 65th Reference");
assert!(matches!(
error,
ParseError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
maximum: MAX_REFERENCES_PER_SIGNATURE,
actual: 65,
})
));
}
#[test]
fn parse_signed_info_bounds_xpath_expressions_across_references() {
let filters = r#"<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</XPath>"#
.repeat(64);
let filter_transform = format!(
r#"<Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{filters}</Transform>"#
);
let reference = |index, transforms: &str| {
format!(
r##"<Reference URI="#item-{index}">
<Transforms>{transforms}</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>"##
)
};
let signed_info = |references: &str| {
format!(
r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
{references}
</SignedInfo>"#
)
};
let max_reference = reference(0, &filter_transform.repeat(64));
let boundary_xml = signed_info(&max_reference);
let boundary_document =
Document::parse(&boundary_xml).expect("fixed boundary fixture must parse");
parse_signed_info(boundary_document.root_element())
.expect("one maximum-shaped Reference must remain accepted");
let extra_transform = r#"<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><XPath>true()</XPath></Transform>"#;
let xml = signed_info(&format!("{max_reference}{}", reference(1, extra_transform)));
let document = Document::parse(&xml).expect("fixed aggregate fixture must parse");
let error = parse_signed_info(document.root_element())
.expect_err("signature-wide XPath expression count must be bounded");
assert!(matches!(
error,
ParseError::Transform(TransformError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: "XPath expressions",
..
}
))
));
}
#[test]
fn parse_reference_without_transforms() {
let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#obj">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"##;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert!(si.references[0].transforms.is_empty());
}
#[test]
fn parse_reference_with_all_attributes() {
let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"##;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
let r = &si.references[0];
assert_eq!(r.uri.as_deref(), Some("#data"));
assert_eq!(r.id.as_deref(), Some("ref1"));
assert_eq!(
r.ref_type.as_deref(),
Some("http://www.w3.org/2000/09/xmldsig#Object")
);
}
#[test]
fn parse_reference_absent_uri() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert!(si.references[0].uri.is_none());
}
#[test]
fn parse_signed_info_preserves_inclusive_prefixes() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<ec:InclusiveNamespaces PrefixList="ds saml #default"/>
</CanonicalizationMethod>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
assert!(si.c14n_method.inclusive_prefixes().contains(""));
}
#[test]
fn missing_canonicalization_method() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn missing_signature_method() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn no_references() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingElement {
element: "Reference"
}
));
}
#[test]
fn unsupported_c14n_algorithm() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::UnsupportedAlgorithm { .. }
));
}
#[test]
fn unsupported_signature_algorithm() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://example.com/bogus-sign"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::UnsupportedAlgorithm { .. }
));
}
#[test]
fn unsupported_digest_algorithm() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://example.com/bogus-digest"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::UnsupportedAlgorithm { .. }
));
}
#[test]
fn missing_digest_method() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(result.is_err());
}
#[test]
fn missing_digest_value() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingElement {
element: "DigestValue"
}
));
}
#[test]
fn invalid_base64_digest_value() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>!!!not-base64!!!</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
}
#[test]
fn digest_value_length_must_match_digest_method() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>dGVzdA==</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::DigestLengthMismatch {
algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
expected: 32,
actual: 4,
}
));
}
#[test]
fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
<ec:InclusiveNamespaces PrefixList="ds"/>
</CanonicalizationMethod>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::UnsupportedAlgorithm { .. }
));
}
#[test]
fn extra_element_after_digest_value() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
<Unexpected/>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn digest_value_with_element_child_is_rejected() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn wrong_namespace_on_signed_info() {
let xml = r#"<SignedInfo xmlns="http://example.com/fake">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let result = parse_signed_info(doc.root_element());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidStructure(_)
));
}
#[test]
fn base64_with_whitespace() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>
AAAAAAAA
AAAAAAAAAAAAAAAAAAA=
</DigestValue>
</Reference>
</SignedInfo>"#;
let doc = Document::parse(xml).unwrap();
let si = parse_signed_info(doc.root_element()).unwrap();
assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
}
#[test]
fn digest_value_ignores_processing_instruction_data() {
let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>AAAAAAAA<?ignored not-base64?>AAAAAAAAAAAAAAAAAAA=</DigestValue>
</Reference>
</SignedInfo>"#;
let document = Document::parse(xml).expect("fixture must parse");
let signed_info =
parse_signed_info(document.root_element()).expect("PI must not alter digest data");
assert_eq!(signed_info.references[0].digest_value, vec![0_u8; 20]);
}
#[test]
fn base64_decode_digest_accepts_xml_whitespace_chars() {
let digest =
base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
.expect("XML whitespace in DigestValue must be accepted");
assert_eq!(digest, vec![0u8; 20]);
}
#[test]
fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
let err = base64_decode_digest(
"AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
DigestAlgorithm::Sha1,
)
.expect_err("form-feed/vertical-tab in DigestValue must be rejected");
assert!(matches!(err, ParseError::Base64(_)));
}
#[test]
fn base64_decode_digest_rejects_oversized_base64_before_decode() {
let err = base64_decode_digest("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", DigestAlgorithm::Sha1)
.expect_err("oversized DigestValue base64 must fail before decode");
match err {
ParseError::Base64(message) => {
assert!(
message.contains("DigestValue exceeds maximum allowed base64 length"),
"unexpected message: {message}"
);
}
other => panic!("expected ParseError::Base64, got {other:?}"),
}
}
#[test]
fn saml_response_signed_info() {
let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_resp1">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
</ds:Signature>"##;
let doc = Document::parse(xml).unwrap();
let sig_node = doc.root_element();
let signed_info_node = sig_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
.unwrap();
let si = parse_signed_info(signed_info_node).unwrap();
assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
assert_eq!(si.references.len(), 1);
assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
assert_eq!(si.references[0].transforms.len(), 2);
assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
}
}