diff --git a/README.md b/README.md index 231890d..2c415af 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Currently implemented (core paths): - C14N 1.0, C14N 1.1, and Exclusive C14N - XMLDSig parsing, same-document URI dereference, transform chains, and digest verification - XMLDSig full verify pipeline (`SignedInfo` canonicalization + `SignatureValue` verification) -- Built-in verification-key resolution from X.509, DER, `KeyName`, RSA `KeyValue`, and EC `KeyValue` sources +- Built-in verification-key resolution from embedded X.509/DER/`KeyValue` sources and configured `KeyName`, X.509 subject, issuer/serial, SKI, or digest selectors - RSA PKCS#1 v1.5 verification helpers for SHA-1 / SHA-256 / SHA-384 / SHA-512 - ECDSA verification helpers for P-256/SHA-256 and P-384/SHA-384 - Opt-in X.509 certificate-chain validation with explicit trust anchors, validity checks, CA constraints, and CRLs diff --git a/scripts/import-donor-fixtures.sh b/scripts/import-donor-fixtures.sh new file mode 100755 index 0000000..160bd8a --- /dev/null +++ b/scripts/import-donor-fixtures.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +donor_root="$repo_root/donors/xmlsec/tests" +fixture_root="$repo_root/tests/fixtures/xmldsig" + +fixture_paths=("$@") +if (( ${#fixture_paths[@]} == 0 )); then + fixture_paths=( + "aleksey-xmldsig-01/enveloping-rsa-x509chain.xml" + ) +fi + +for relative_path in "${fixture_paths[@]}"; do + target="$fixture_root/$relative_path" + mkdir -p "$(dirname "$target")" + install -m 0644 "$donor_root/$relative_path" "$target" +done diff --git a/src/xmldsig/keys.rs b/src/xmldsig/keys.rs index 3a0f4d0..05aec79 100644 --- a/src/xmldsig/keys.rs +++ b/src/xmldsig/keys.rs @@ -12,7 +12,11 @@ use x509_parser::{ use super::{ DsigError, KeyInfo, KeyInfoSource, KeyResolver, KeyValueInfo, SignatureAlgorithm, VerifyingKey, X509ChainOptions, X509DataInfo, - parse::{EC_P256_OID, EC_P384_OID}, + parse::{ + EC_P256_OID, EC_P384_OID, ParseError, parse_x509_certificate, + x509_certificate_matches_any_selector, x509_data_has_lookup_identifiers, + x509_selector_categories_match_chain, + }, verify_ecdsa_signature_spki, verify_rsa_signature_spki, verify_x509_certificate_chain, }; @@ -75,6 +79,12 @@ pub enum KeyResolutionError { /// Configured or embedded public key DER could not be parsed completely. #[error("invalid public key DER")] InvalidPublicKey, + /// More than one configured certificate satisfies all X.509 selectors. + #[error("X.509 lookup selectors match multiple configured certificates")] + AmbiguousCertificate, + /// An X.509 selector uses a digest algorithm unsupported by this crate. + #[error("unsupported X.509 digest algorithm: {0}")] + UnsupportedDigestAlgorithm(String), /// Embedded certificate path validation failed. #[error("certificate chain validation failed: {0}")] Chain(#[from] super::X509ChainError), @@ -138,26 +148,35 @@ impl DefaultKeyResolver { info: &X509DataInfo, algorithm: SignatureAlgorithm, ) -> Result, KeyResolutionError> { - let Some(&signing_index) = info.certificate_chain.first() else { - return Ok(None); - }; - let certificate_der = info - .certificates - .get(signing_index) - .ok_or(KeyResolutionError::InvalidCertificate)?; - - if self.config.verify_chains { - let options = X509ChainOptions { - trusted_certs: &self.config.trusted_certs, - verification_time: self - .config - .verification_time - .unwrap_or_else(SystemTime::now), - max_chain_depth: self.config.max_chain_depth, - check_crls: false, + let certificate_der = if let Some(&signing_index) = info.certificate_chain.first() { + let certificate_der = info + .certificates + .get(signing_index) + .ok_or(KeyResolutionError::InvalidCertificate)?; + if self.config.verify_chains { + self.verify_x509_policy(info, None)?; + } + certificate_der + } else { + let Some(certificate) = self.resolve_configured_x509(info)? else { + return Ok(None); }; - verify_x509_certificate_chain(info, &options)?; - } + if self.config.verify_chains { + let parsed = parse_x509_certificate(certificate) + .map_err(|_| KeyResolutionError::InvalidCertificate)?; + let selected = X509DataInfo { + certificates: vec![certificate.clone()], + parsed_certificates: vec![parsed], + certificate_chain: vec![0], + ..X509DataInfo::default() + }; + // Validate the selected certificate's own policy before + // requiring a distinct configured certificate as its anchor. + self.verify_x509_policy(&selected, None)?; + self.verify_x509_policy(&selected, Some(certificate))?; + } + certificate + }; let (rest, certificate) = X509Certificate::from_der(certificate_der) .map_err(|_| KeyResolutionError::InvalidCertificate)?; @@ -174,6 +193,103 @@ impl DefaultKeyResolver { })) } + fn verify_x509_policy( + &self, + info: &X509DataInfo, + selected_lookup_certificate: Option<&[u8]>, + ) -> Result<(), KeyResolutionError> { + let trusted_certs = self + .config + .trusted_certs + .iter() + .filter(|certificate| { + selected_lookup_certificate + .is_none_or(|selected| certificate.as_slice() != selected) + }) + .cloned() + .collect::>(); + let options = X509ChainOptions { + trusted_certs: &trusted_certs, + verification_time: self + .config + .verification_time + .unwrap_or_else(SystemTime::now), + max_chain_depth: self.config.max_chain_depth, + check_crls: false, + }; + verify_x509_certificate_chain(info, &options)?; + Ok(()) + } + + fn resolve_configured_x509<'a>( + &'a self, + info: &X509DataInfo, + ) -> Result>, KeyResolutionError> { + if !x509_data_has_lookup_identifiers(info) { + return Ok(None); + } + + let mut matches = Vec::new(); + for certificate_der in &self.config.trusted_certs { + let parsed = parse_x509_certificate(certificate_der) + .map_err(|_| KeyResolutionError::InvalidCertificate)?; + let is_match = x509_certificate_matches_any_selector(info, &parsed, certificate_der) + .map_err(|error| match error { + ParseError::UnsupportedAlgorithm { uri } => { + KeyResolutionError::UnsupportedDigestAlgorithm(uri) + } + _ => KeyResolutionError::InvalidCertificate, + })?; + if is_match { + matches.push((certificate_der, parsed)); + } + } + + let matched_chain = X509DataInfo { + certificates: matches + .iter() + .map(|(certificate, _)| (*certificate).clone()) + .collect(), + parsed_certificates: matches.iter().map(|(_, parsed)| parsed.clone()).collect(), + ..X509DataInfo::default() + }; + if !x509_selector_categories_match_chain(&X509DataInfo { + subject_names: info.subject_names.clone(), + issuer_serials: info.issuer_serials.clone(), + skis: info.skis.clone(), + digests: info.digests.clone(), + ..matched_chain + }) + .map_err(|error| match error { + ParseError::UnsupportedAlgorithm { uri } => { + KeyResolutionError::UnsupportedDigestAlgorithm(uri) + } + _ => KeyResolutionError::InvalidCertificate, + })? { + return Ok(None); + } + + match matches.as_slice() { + [] => Ok(None), + [(certificate, _)] => Ok(Some(certificate)), + _ => { + let leaves = matches + .iter() + .filter(|(_, candidate)| { + candidate.subject_dn != candidate.issuer_dn + && !matches + .iter() + .any(|(_, other)| other.issuer_dn == candidate.subject_dn) + }) + .collect::>(); + match leaves.as_slice() { + [(certificate, _)] => Ok(Some(certificate)), + _ => Err(KeyResolutionError::AmbiguousCertificate), + } + } + } + } + fn resolve_key_value( key_value: &KeyValueInfo, algorithm: SignatureAlgorithm, @@ -378,6 +494,11 @@ mod tests { const SAML_PUBLIC_KEY: &str = include_str!("../../tests/fixtures/keys/ec/saml-idp-ecdsa-pubkey.pem"); const RSA_PUBLIC_KEY: &str = include_str!("../../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + const RSA_4096_CERTIFICATE: &str = + include_str!("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + const X509_DIGEST_SIGNATURE: &str = include_str!( + "../../tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-x509-digest-sha512.xml" + ); const RSA_KEY_VALUE_SIGNATURE: &str = include_str!( "../../tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.xml" ); @@ -406,6 +527,18 @@ mod tests { format!("{}{}{}", &xml[..start], replacement, &xml[end..]) } + fn x509_signature_with_leaf_subject() -> String { + replace_unprefixed_key_info( + X509_DIGEST_SIGNATURE, + "C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-4096", + ) + } + + fn fixture_certificate_time() -> SystemTime { + // 2027-01-15 UTC, inside the donor certificates' 2026-2126 validity window. + SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_800_000_000) + } + fn public_key_der(pem_text: &str) -> Vec { let (rest, pem) = x509_parser::pem::parse_x509_pem(pem_text.as_bytes()) .expect("fixture public key is PEM"); @@ -414,6 +547,14 @@ mod tests { pem.contents } + fn certificate_der(pem_text: &str) -> Vec { + let (rest, pem) = x509_parser::pem::parse_x509_pem(pem_text.as_bytes()) + .expect("fixture certificate is PEM"); + assert!(rest.iter().all(|byte| byte.is_ascii_whitespace())); + assert_eq!(pem.label, "CERTIFICATE"); + pem.contents + } + #[test] fn defaults_match_key_resolution_policy() { // Defaults must remain compatible with xmlsec1's depth and opt-in trust policy. @@ -453,6 +594,212 @@ mod tests { assert_eq!(result.status, super::super::DsigStatus::Valid); } + #[test] + fn resolves_x509_digest_from_configured_certificates() { + // Selector-only X509Data must locate the signing certificate without + // embedding key material or supplying a preset verification key. + let leaf_certificate_der = certificate_der(RSA_4096_CERTIFICATE); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![ + leaf_certificate_der, + certificate_der(include_str!("../../tests/fixtures/keys/ca2cert.pem")), + certificate_der(include_str!("../../tests/fixtures/keys/cacert.pem")), + ], + ..KeyResolverConfig::default() + }); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(X509_DIGEST_SIGNATURE) + .expect("X509Digest should resolve a configured certificate"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn selector_resolved_certificate_obeys_chain_policy() { + // Enabling chain verification must apply validity policy even when + // X509Data contains only selectors and the matching cert is configured. + let certificate_der = certificate_der(RSA_4096_CERTIFICATE); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![certificate_der], + verify_chains: true, + verification_time: Some(SystemTime::UNIX_EPOCH), + ..KeyResolverConfig::default() + }); + let error = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&x509_signature_with_leaf_subject()) + .expect_err("selector-resolved certificate must satisfy chain policy"); + + assert!(matches!( + error, + DsigError::KeyResolution(KeyResolutionError::Chain( + super::super::X509ChainError::CertificateNotValid(_) + )) + )); + } + + #[test] + fn selector_resolved_leaf_does_not_anchor_itself() { + // A certificate available for selector lookup is not automatically a + // trust anchor; chain verification still requires a separate issuer. + let certificate_der = certificate_der(RSA_4096_CERTIFICATE); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![certificate_der], + verify_chains: true, + verification_time: Some(fixture_certificate_time()), + ..KeyResolverConfig::default() + }); + let error = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&x509_signature_with_leaf_subject()) + .expect_err("selector-resolved leaf must not trust itself"); + + assert!(matches!( + error, + DsigError::KeyResolution(KeyResolutionError::Chain( + super::super::X509ChainError::UntrustedRoot + )) + )); + } + + #[test] + fn selector_resolved_leaf_uses_separate_anchor() { + // Selector lookup may use the leaf from the configured set, but chain + // verification must terminate at a different configured certificate. + let leaf = certificate_der(RSA_4096_CERTIFICATE); + let issuer = certificate_der(include_str!("../../tests/fixtures/keys/ca2cert.pem")); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![leaf, issuer], + verify_chains: true, + verification_time: Some(fixture_certificate_time()), + ..KeyResolverConfig::default() + }); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&x509_signature_with_leaf_subject()) + .expect("selector-resolved leaf should chain to its configured issuer"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn resolves_each_x509_selector_from_configured_certificates() { + // Every selector form documented by KeyInfo must independently locate + // the same configured RSA certificate without embedded key material. + let selectors = [ + "C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048", + "C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com680572598617295163017172295025714171905498632019", + "bcOXN/nsVl8GatRbcKrPbzIbw0Y=", + ]; + let configured_certificate = certificate_der(include_str!( + "../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" + )); + + for selector in selectors { + let key_info = format!("{selector}"); + let xml = replace_unprefixed_key_info(RSA_KEY_VALUE_SIGNATURE, &key_info); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![configured_certificate.clone()], + ..KeyResolverConfig::default() + }); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("X509 selector should resolve configured certificate"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + } + + #[test] + fn resolves_configured_chain_selectors_across_certificates() { + // Selector categories may identify different members of one configured + // chain; the unique leaf remains the signing certificate. + let key_info = r#"C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-20480X0XrEVCio75sBcl1TxymJ2IOiU="#; + let xml = replace_unprefixed_key_info(RSA_KEY_VALUE_SIGNATURE, key_info); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![ + certificate_der(include_str!( + "../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" + )), + certificate_der(include_str!("../../tests/fixtures/keys/ca2cert.pem")), + ], + ..KeyResolverConfig::default() + }); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("selectors across one configured chain should resolve its leaf"); + + assert_eq!(result.status, super::super::DsigStatus::Valid); + } + + #[test] + fn unmatched_x509_selector_does_not_resolve() { + // A selector mismatch must not fall back to arbitrary configured key material. + let key_info = "CN=not-the-signer"; + let xml = replace_unprefixed_key_info(RSA_KEY_VALUE_SIGNATURE, key_info); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![certificate_der(include_str!( + "../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" + ))], + ..KeyResolverConfig::default() + }); + let result = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect("an unmatched selector is a key miss, not a parser failure"); + + assert!(matches!( + result.status, + super::super::DsigStatus::Invalid(super::super::FailureReason::KeyNotFound) + )); + } + + #[test] + fn ambiguous_x509_selector_fails_closed() { + // Duplicate configured certificates must not make key selection order-dependent. + let certificate = certificate_der(RSA_4096_CERTIFICATE); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![certificate.clone(), certificate], + ..KeyResolverConfig::default() + }); + let error = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&x509_signature_with_leaf_subject()) + .expect_err("ambiguous X509 selector lookup must fail closed"); + + assert!(matches!( + error, + DsigError::KeyResolution(KeyResolutionError::AmbiguousCertificate) + )); + } + + #[test] + fn unsupported_x509_digest_selector_fails_closed() { + // Unknown digest URIs must not be treated as a normal key miss because + // that would silently weaken the caller's explicit selector policy. + let key_info = "AQ=="; + let xml = replace_unprefixed_key_info(RSA_KEY_VALUE_SIGNATURE, key_info); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![certificate_der(include_str!( + "../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" + ))], + ..KeyResolverConfig::default() + }); + let error = super::super::VerifyContext::new() + .key_resolver(&resolver) + .verify(&xml) + .expect_err("unsupported X509Digest algorithm must fail closed"); + + assert!(matches!( + error, + DsigError::KeyResolution(KeyResolutionError::UnsupportedDigestAlgorithm(uri)) + if uri == "urn:unsupported" + )); + } + #[test] fn resolves_named_key_end_to_end() { // KeyName lookup must preserve the same cryptographic result as embedded X509Data. diff --git a/src/xmldsig/parse.rs b/src/xmldsig/parse.rs index 51f54e8..3e89b15 100644 --- a/src/xmldsig/parse.rs +++ b/src/xmldsig/parse.rs @@ -21,7 +21,7 @@ use x509_parser::extensions::ParsedExtension; use x509_parser::prelude::FromDer; use x509_parser::public_key::PublicKey; -use super::digest::DigestAlgorithm; +use super::digest::{DigestAlgorithm, compute_digest, constant_time_eq}; use super::transforms::{self, Transform}; use super::whitespace::{ XmlBase64NormalizeLimitedError, is_xml_whitespace_only, normalize_xml_base64_text, @@ -882,74 +882,24 @@ fn build_x509_certificate_chain(info: &X509DataInfo) -> Result, Parse } fn select_x509_signing_certificate(info: &X509DataInfo) -> Result { + let has_lookup_identifiers = x509_data_has_lookup_identifiers(info); let mut candidates = Vec::new(); - let has_lookup_identifiers = - !info.subject_names.is_empty() || !info.issuer_serials.is_empty() || !info.skis.is_empty(); - - for subject_name in &info.subject_names { - let subject_name = subject_name.trim(); - let matches = info - .parsed_certificates - .iter() - .enumerate() - .filter(|(_, cert)| subject_name == cert.subject_dn) - .map(|(idx, _)| idx) - .collect::>(); - if matches.is_empty() { - return Err(ParseError::InvalidStructure( - "X509Data lookup identifiers do not match any embedded certificate".into(), - )); - } - candidates.extend(matches); - } - - for (issuer_name, serial) in &info.issuer_serials { - let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| { - ParseError::InvalidStructure( - "X509Data lookup identifiers do not match any embedded certificate".into(), - ) - })?; - let issuer_name = issuer_name.trim(); - let matches = info + if has_lookup_identifiers { + for (idx, (parsed, der)) in info .parsed_certificates .iter() + .zip(&info.certificates) .enumerate() - .filter(|(_, cert)| { - issuer_name == cert.issuer_dn && serial_hex == cert.serial_number_hex - }) - .map(|(idx, _)| idx) - .collect::>(); - if matches.is_empty() { - return Err(ParseError::InvalidStructure( - "X509Data lookup identifiers do not match any embedded certificate".into(), - )); + { + if x509_certificate_matches_any_selector(info, parsed, der)? { + candidates.push(idx); + } } - candidates.extend(matches); - } - - for ski in &info.skis { - let matches = info - .parsed_certificates - .iter() - .enumerate() - .filter(|(_, cert)| { - cert.subject_key_identifier - .as_ref() - .is_some_and(|subject_key_identifier| subject_key_identifier == ski) - }) - .map(|(idx, _)| idx) - .collect::>(); - if matches.is_empty() { + if !x509_selector_categories_match_chain(info)? { return Err(ParseError::InvalidStructure( - "X509Data lookup identifiers do not match any embedded certificate".into(), + "X509Data lookup identifiers do not match the embedded certificate chain".into(), )); } - candidates.extend(matches); - } - - if has_lookup_identifiers { - candidates.sort_unstable(); - candidates.dedup(); } match candidates.as_slice() { @@ -960,11 +910,7 @@ fn select_x509_signing_certificate(info: &X509DataInfo) -> Result {} - _ => { - return Err(ParseError::InvalidStructure( - "X509Data lookup identifiers match multiple certificates".into(), - )); - } + _ => {} } let leaf_candidates = info @@ -981,15 +927,118 @@ fn select_x509_signing_certificate(info: &X509DataInfo) -> Result>(); - match leaf_candidates.as_slice() { + let selected_leaves = leaf_candidates + .iter() + .filter(|idx| !has_lookup_identifiers || candidates.contains(idx)) + .copied() + .collect::>(); + + match selected_leaves.as_slice() { [idx] => Ok(*idx), - [] => Ok(0), + [] if !has_lookup_identifiers => Ok(0), + [] => Err(ParseError::InvalidStructure( + "X509Data lookup identifiers match multiple certificates without a unique signing certificate" + .into(), + )), _ => Err(ParseError::InvalidStructure( - "X509Data contains multiple possible signing certificates".into(), + 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: &ParsedX509Certificate, + certificate_der: &[u8], +) -> Result { + let subject_match = info + .subject_names + .iter() + .any(|subject| subject.trim() == certificate.subject_dn); + let mut issuer_serial_match = false; + for (issuer, serial) in &info.issuer_serials { + let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| { + ParseError::InvalidStructure( + "X509Data lookup identifiers contain an invalid serial number".into(), + ) + })?; + issuer_serial_match |= + issuer.trim() == certificate.issuer_dn && serial_hex == certificate.serial_number_hex; + } + let ski_match = certificate + .subject_key_identifier + .as_ref() + .is_some_and(|certificate_ski| info.skis.iter().any(|ski| ski == certificate_ski)); + 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 |= constant_time_eq(&compute_digest(algorithm, certificate_der), expected); + } + Ok(subject_match || issuer_serial_match || ski_match || digest_match) +} + +pub(crate) fn x509_selector_categories_match_chain( + info: &X509DataInfo, +) -> Result { + let subject_match = info.subject_names.iter().all(|subject| { + info.parsed_certificates + .iter() + .any(|certificate| subject.trim() == certificate.subject_dn) + }); + + let mut issuer_serial_match = true; + for (issuer, serial) in &info.issuer_serials { + let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| { + ParseError::InvalidStructure( + "X509Data lookup identifiers contain an invalid serial number".into(), + ) + })?; + issuer_serial_match &= info.parsed_certificates.iter().any(|certificate| { + issuer.trim() == certificate.issuer_dn && serial_hex == certificate.serial_number_hex + }); + } + + let ski_match = info.skis.iter().all(|ski| { + info.parsed_certificates.iter().any(|certificate| { + certificate + .subject_key_identifier + .as_ref() + .is_some_and(|certificate_ski| ski == certificate_ski) + }) + }); + + 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(), + } + })?; + digest_match &= info + .certificates + .iter() + .any(|certificate| constant_time_eq(&compute_digest(algorithm, certificate), expected)); + } + + Ok(subject_match && issuer_serial_match && ski_match && digest_match) +} + fn ensure_x509_data_entry_budget(info: &X509DataInfo) -> Result<(), ParseError> { let total_entries = info.certificates.len() + info.subject_names.len() @@ -1066,7 +1115,7 @@ fn decode_x509_base64( Ok(decoded) } -fn parse_x509_certificate(cert_der: &[u8]) -> Result { +pub(crate) fn parse_x509_certificate(cert_der: &[u8]) -> Result { let (rest, cert) = x509_parser::certificate::X509Certificate::from_der(cert_der).map_err(|err| { ParseError::InvalidStructure(format!("X509Certificate is not valid DER X.509: {err}")) @@ -1522,6 +1571,11 @@ mod tests { #[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#" @@ -1541,7 +1595,7 @@ mod tests { bcOXN/nsVl8GatRbcKrPbzIbw0Y= BAUGBw== - CAkK + {cert_digest} AQIDBA== "# @@ -1566,9 +1620,6 @@ mod tests { KeyInfoSource::X509Data(x509) => x509, other => panic!("expected X509Data source, got {other:?}"), }; - let expected_cert = base64::engine::general_purpose::STANDARD - .decode(&cert_base64) - .expect("fixture PEM must contain valid base64"); assert_eq!(x509_info.certificates, vec![expected_cert]); assert_eq!( x509_info.subject_names, @@ -1596,7 +1647,7 @@ mod tests { x509_info.digests, vec![( "http://www.w3.org/2001/04/xmlenc#sha256".to_string(), - vec![8, 9, 10] + compute_digest(DigestAlgorithm::Sha256, &x509_info.certificates[0]) )] ); assert_eq!(x509_info.parsed_certificates.len(), 1); @@ -2097,6 +2148,35 @@ BA== assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]); } + #[test] + fn parse_key_info_allows_selectors_for_multiple_chain_members() { + // X509Data may identify both the signing leaf and another certificate + // in its chain; the unique leaf must remain the 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#" + + C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048 + 0X0XrEVCio75sBcl1TxymJ2IOiU= + {root} + {intermediate} + {leaf} + + "# + ); + 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() { assert_eq!( @@ -2170,6 +2250,28 @@ BA== ); } + #[test] + fn parse_key_info_rejects_partially_matched_selector_category() { + // Every selector value is an asserted lookup constraint; one matching + // SubjectName must not mask another value absent from the chain. + let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem"); + let xml = format!( + r#" + + C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048 + CN=Not In The Embedded Chain + {cert} + + "# + ); + 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"); diff --git a/tests/donor_full_verification_suite.rs b/tests/donor_full_verification_suite.rs index 5f84857..75abf85 100644 --- a/tests/donor_full_verification_suite.rs +++ b/tests/donor_full_verification_suite.rs @@ -3,24 +3,35 @@ //! This suite tracks pass/fail/skip accounting across donor vectors and //! enforces that all supported donor vectors verify end-to-end. -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + time::{Duration, SystemTime}, +}; use xml_sec::xmldsig::{ - DefaultKeyResolver, DsigError, DsigStatus, FailureReason, ParseError, VerifyContext, - verify_signature_with_pem_key, + DefaultKeyResolver, DsigError, DsigStatus, KeyResolverConfig, ParseError, SignatureAlgorithm, + VerificationKey, VerifyContext, }; #[derive(Clone, Copy)] enum SkipProbe { - KeyNotFound, WeakRsaKey, UnsupportedSignatureAlgorithm, } #[derive(Clone, Copy)] enum Expectation { - ValidWithKey { + ValidEmbedded, + ValidNamed { + key_name: &'static str, key_path: &'static str, + algorithm: SignatureAlgorithm, + }, + ValidSelected { + certificate_paths: &'static [&'static str], + }, + ValidChain { + trust_anchor_path: &'static str, }, Skip { reason: &'static str, @@ -43,57 +54,81 @@ fn project_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } +fn read_pem_der(path: &Path, expected_label: &str) -> Vec { + let pem_text = read_fixture(path); + let (rest, pem) = x509_parser::pem::parse_x509_pem(pem_text.as_bytes()) + .unwrap_or_else(|err| panic!("failed to parse PEM fixture {}: {err}", path.display())); + assert!(rest.iter().all(|byte| byte.is_ascii_whitespace())); + assert_eq!(pem.label, expected_label); + pem.contents +} + fn cases() -> Vec { vec![ // Aleksey donor vectors: supported algorithms must pass end-to-end. VectorCase { name: "aleksey-rsa-sha1", xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha1-rsa-sha1.xml", - expectation: Expectation::ValidWithKey { + expectation: Expectation::ValidNamed { + key_name: "TestKeyName-rsa-4096", key_path: "tests/fixtures/keys/rsa/rsa-4096-pubkey.pem", + algorithm: SignatureAlgorithm::RsaSha1, }, }, VectorCase { name: "aleksey-rsa-sha256", xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.xml", - expectation: Expectation::ValidWithKey { - key_path: "tests/fixtures/keys/rsa/rsa-2048-pubkey.pem", - }, + expectation: Expectation::ValidEmbedded, }, VectorCase { name: "aleksey-rsa-sha384", xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha384-rsa-sha384.xml", - expectation: Expectation::ValidWithKey { - key_path: "tests/fixtures/keys/rsa/rsa-4096-pubkey.pem", - }, + expectation: Expectation::ValidEmbedded, }, VectorCase { name: "aleksey-rsa-sha512", xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha512-rsa-sha512.xml", - expectation: Expectation::ValidWithKey { - key_path: "tests/fixtures/keys/rsa/rsa-4096-pubkey.pem", - }, + expectation: Expectation::ValidEmbedded, }, VectorCase { name: "aleksey-ecdsa-p256-sha256", xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256.xml", - expectation: Expectation::ValidWithKey { + expectation: Expectation::ValidNamed { + key_name: "TestKeyName-ec-prime256v1", key_path: "tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem", + algorithm: SignatureAlgorithm::EcdsaP256Sha256, }, }, VectorCase { name: "aleksey-ecdsa-p521-sha384", xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-sha384-ecdsa-sha384.xml", - expectation: Expectation::ValidWithKey { + expectation: Expectation::ValidNamed { + key_name: "TestKeyName-ec-prime521v1", key_path: "tests/fixtures/keys/ec/ec-prime521v1-pubkey.pem", + algorithm: SignatureAlgorithm::EcdsaP384Sha384, }, }, VectorCase { name: "aleksey-rsa-sha512-x509-digest", xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloped-x509-digest-sha512.xml", - expectation: Expectation::Skip { - reason: "X509Digest key resolution is not implemented yet (planned P2-009)", - probe: SkipProbe::KeyNotFound, + expectation: Expectation::ValidSelected { + certificate_paths: &[ + "tests/fixtures/keys/rsa/rsa-4096-cert.pem", + "tests/fixtures/keys/ca2cert.pem", + "tests/fixtures/keys/cacert.pem", + ], + }, + }, + VectorCase { + name: "aleksey-rsa-sha1-x509-chain-tofu", + xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-rsa-x509chain.xml", + expectation: Expectation::ValidEmbedded, + }, + VectorCase { + name: "aleksey-rsa-sha1-x509-chain-anchored", + xml_path: "tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-rsa-x509chain.xml", + expectation: Expectation::ValidChain { + trust_anchor_path: "tests/fixtures/keys/cacert.pem", }, }, // Merlin "basic signatures" required by P1-025. @@ -166,10 +201,10 @@ fn donor_full_verification_suite_tracks_pass_fail_skip_counts() { for case in cases() { match case.expectation { - Expectation::ValidWithKey { key_path } => { + Expectation::ValidEmbedded => { let xml = read_fixture(&root.join(case.xml_path)); - let key = read_fixture(&root.join(key_path)); - match verify_signature_with_pem_key(&xml, &key, false) { + let resolver = DefaultKeyResolver::default(); + match VerifyContext::new().key_resolver(&resolver).verify(&xml) { Ok(result) if matches!(result.status, DsigStatus::Valid) => { passed += 1; } @@ -184,26 +219,81 @@ fn donor_full_verification_suite_tracks_pass_fail_skip_counts() { } } } + Expectation::ValidNamed { + key_name, + key_path, + algorithm, + } => { + let xml = read_fixture(&root.join(case.xml_path)); + let mut config = KeyResolverConfig::default(); + config.named_keys.insert( + key_name.into(), + VerificationKey { + algorithm, + public_key_bytes: read_pem_der(&root.join(key_path), "PUBLIC KEY"), + certificate_der: None, + name: Some(key_name.into()), + }, + ); + let resolver = DefaultKeyResolver::new(config); + match VerifyContext::new().key_resolver(&resolver).verify(&xml) { + Ok(result) if matches!(result.status, DsigStatus::Valid) => passed += 1, + Ok(result) => failed.push(format!( + "{}: expected Valid, got {:?}", + case.name, result.status + )), + Err(err) => { + failed.push(format!("{}: verification error {err}", case.name)); + } + } + } + Expectation::ValidSelected { certificate_paths } => { + let xml = read_fixture(&root.join(case.xml_path)); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: certificate_paths + .iter() + .map(|path| read_pem_der(&root.join(path), "CERTIFICATE")) + .collect(), + ..KeyResolverConfig::default() + }); + match VerifyContext::new().key_resolver(&resolver).verify(&xml) { + Ok(result) if matches!(result.status, DsigStatus::Valid) => passed += 1, + Ok(result) => failed.push(format!( + "{}: expected Valid, got {:?}", + case.name, result.status + )), + Err(err) => { + failed.push(format!("{}: verification error {err}", case.name)); + } + } + } + Expectation::ValidChain { trust_anchor_path } => { + let xml = read_fixture(&root.join(case.xml_path)); + let resolver = DefaultKeyResolver::new(KeyResolverConfig { + trusted_certs: vec![read_pem_der(&root.join(trust_anchor_path), "CERTIFICATE")], + verify_chains: true, + // 2027-01-15 UTC, inside the donor chain's 2026-2126 validity window. + verification_time: Some( + SystemTime::UNIX_EPOCH + Duration::from_secs(1_800_000_000), + ), + ..KeyResolverConfig::default() + }); + match VerifyContext::new().key_resolver(&resolver).verify(&xml) { + Ok(result) if matches!(result.status, DsigStatus::Valid) => passed += 1, + Ok(result) => failed.push(format!( + "{}: expected Valid, got {:?}", + case.name, result.status + )), + Err(err) => { + failed.push(format!("{}: verification error {err}", case.name)); + } + } + } Expectation::Skip { reason, probe } => { let xml = read_fixture(&root.join(case.xml_path)); roxmltree::Document::parse(&xml) .unwrap_or_else(|err| panic!("{}: fixture XML must parse: {err}", case.name)); match probe { - SkipProbe::KeyNotFound => match VerifyContext::new().verify(&xml) { - Ok(result) - if matches!( - result.status, - DsigStatus::Invalid(FailureReason::KeyNotFound) - ) => {} - Ok(result) => failed.push(format!( - "{}: expected Invalid(KeyNotFound) for skipped vector, got {:?}", - case.name, result.status - )), - Err(err) => failed.push(format!( - "{}: expected Invalid(KeyNotFound) for skipped vector, got error {err}", - case.name - )), - }, SkipProbe::WeakRsaKey => match VerifyContext::new() .key_resolver(&DefaultKeyResolver::default()) .verify(&xml) @@ -248,7 +338,6 @@ fn donor_full_verification_suite_tracks_pass_fail_skip_counts() { ); let expected_skipped = vec![ - "aleksey-rsa-sha512-x509-digest: X509Digest key resolution is not implemented yet (planned P2-009)", "merlin-enveloped-dsa: DSA signature method is not implemented yet (planned P4-009)", "merlin-enveloping-rsa-keyvalue: RSAKeyValue resolves but its legacy 1024-bit modulus is below policy", "merlin-x509-crt: DSA signature method is not implemented yet (planned P4-009); X509 KeyInfo resolution is not implemented yet (planned P2-009)", @@ -261,6 +350,6 @@ fn donor_full_verification_suite_tracks_pass_fail_skip_counts() { // P1-025 minimum expected accounting: // - all supported aleksey RSA/ECDSA vectors pass // - unsupported/deferred merlin vectors are tracked as skips with explicit reasons - assert_eq!(passed, 6, "unexpected pass count"); + assert_eq!(passed, 9, "unexpected pass count"); assert_eq!(skipped, expected_skipped, "unexpected skip inventory"); } diff --git a/tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-rsa-x509chain.xml b/tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-rsa-x509chain.xml new file mode 100644 index 0000000..5004643 --- /dev/null +++ b/tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-rsa-x509chain.xml @@ -0,0 +1,111 @@ + + + + + + + + 7/XTsHaBSOnJ/jXD5v0zL6VKYsk= + + + rodS64O2XP44pmJjFIJUi2iJPVZKkfcXMM6Y0rkJgoY5pFCUHnZxoNgfnQwMH4/u +RrNybVrEkzO2nhiwKcT6aMcHat34qypHW8MYBmSGroSITRbg/tOx20rNmNjU2t7B +OACH7bKgjegxvtD//p3CiN1P9ANpeyVBWA4xBQ9vGF/9bOXY//aOQvUJ2CP9uv/B +Ahnw3fQy4rYF4kMbAQLMxprk9ZS3PTEx4LTomwxGFbBbZD2vTCsMKH1Iy1DTBXX1 +dfx9I4139JooLhQvOSsogcsYvaeU4EDyRpwwfFCpLxJY75XihBIEEqT+C0SQWths +z94OSh4Wp7apGFQ3YOz4VA== + + TestKeyName-rsa-2048 + + MIIFFjCCA/6gAwIBAgIUdzXuSH9oYtrxs5VtlhzLD6bzT1MwDQYJKoZIhvcNAQEL +BQAwgbYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMT0wOwYDVQQK +EzRYTUwgU2VjdXJpdHkgTGlicmFyeSAoaHR0cDovL3d3dy5hbGVrc2V5LmNvbS94 +bWxzZWMpMRgwFgYDVQQLEw9TZWNvbmQgbGV2ZWwgQ0ExFjAUBgNVBAMTDUFsZWtz +ZXkgU2FuaW4xITAfBgkqhkiG9w0BCQEWEnhtbHNlY0BhbGVrc2V5LmNvbTAgFw0y +NjAzMDgyMjE0MTZaGA8yMTI2MDIxMjIyMTQxNlowfTELMAkGA1UEBhMCVVMxEzAR +BgNVBAgTCkNhbGlmb3JuaWExPTA7BgNVBAoTNFhNTCBTZWN1cml0eSBMaWJyYXJ5 +IChodHRwOi8vd3d3LmFsZWtzZXkuY29tL3htbHNlYykxGjAYBgNVBAMTEVRlc3Qg +S2V5IHJzYS0yMDQ4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3iVn +hDXlgGiWvV2f21bCP4NAeTkwouWvN9K94SNeV01xzuvPg2GRb+ozF0/YbQ8jj7UD +euIgcLoBrC/jSMtp7gJp6zj3oHhX97NZVv5SBUmBOJRbB8efy3apTvTvWlzJQhO4 +WVXBRDqmA0dGHPRRuMB6l125wy+WBMWxO6BzUooe9m0OpQXjKokJKFcl9Zd4ht3E +qXW8cuHgiyrtYzXTcO63W9+J6dFOi1DTYhKMkK53jMsModlleEUdqUHgTbxK0TBS +YMa2sB1rGfcVq+QanOlsRHXAXiZM/BE6uPqlFq+g5osSZbHfH2qC/0G/wKKlWzIx +0YPjQSBq/MbroodKLQIDAQABo4IBUDCCAUwwDAYDVR0TBAUwAwEB/zAsBglghkgB +hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE +FG3Dlzf57FZfBmrUW3Cqz28yG8NGMIHuBgNVHSMEgeYwgeOAFNF9F6xFQoqO+bAX +JdU8cpidiDoloYG0pIGxMIGuMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv +cm5pYTE9MDsGA1UEChM0WE1MIFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6Ly93d3cu +YWxla3NleS5jb20veG1sc2VjKTEQMA4GA1UECxMHUm9vdCBDQTEWMBQGA1UEAxMN +QWxla3NleSBTYW5pbjEhMB8GCSqGSIb3DQEJARYSeG1sc2VjQGFsZWtzZXkuY29t +ghR3Ne5If2hi2vGzlW2WHMsPpvNPTzANBgkqhkiG9w0BAQsFAAOCAQEATAu+Gt18 +Kg0CW8kT+l92sfsNysxS/eYJD3iNyku0oE72jmWVsOvS9phHDF0q01tv8SsIjio6 +sUQXoQ+C+YDAI3g9M5imN5l41TGZF1yRS0i5VucZpnmMcWtNWEpkJd5mB6l4VRDK +IarRS3UuA2cmZdtfRNsXAnG7sCLiiQB5wWF0Gbe6oAb0Y+hURG7D2vnIAimi2lcH +LgCD9eXbGfMNhNYnN+hbTNZuwvbJIDWMLu5VWpOdeX+Axm5MXI7lNPMRda75uPRQ +O52QICz4Cz3lbq4SEhiF4CQ5h/FRj6Yc8m+ZY3/5khICap0dUjAmmezcVwMJlxXr +hwvZjgian+dyQw== + +MIIFEjCCA/qgAwIBAgIUdzXuSH9oYtrxs5VtlhzLD6bzT04wDQYJKoZIhvcNAQEL +BQAwga4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMT0wOwYDVQQK +EzRYTUwgU2VjdXJpdHkgTGlicmFyeSAoaHR0cDovL3d3dy5hbGVrc2V5LmNvbS94 +bWxzZWMpMRAwDgYDVQQLEwdSb290IENBMRYwFAYDVQQDEw1BbGVrc2V5IFNhbmlu +MSEwHwYJKoZIhvcNAQkBFhJ4bWxzZWNAYWxla3NleS5jb20wIBcNMjYwMzA4MjIw +NDQ3WhgPMjEyNjAyMTIyMjA0NDdaMIGuMQswCQYDVQQGEwJVUzETMBEGA1UECBMK +Q2FsaWZvcm5pYTE9MDsGA1UEChM0WE1MIFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6 +Ly93d3cuYWxla3NleS5jb20veG1sc2VjKTEQMA4GA1UECxMHUm9vdCBDQTEWMBQG +A1UEAxMNQWxla3NleSBTYW5pbjEhMB8GCSqGSIb3DQEJARYSeG1sc2VjQGFsZWtz +ZXkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtbKBr4EAoOm8 +zAW/RL8Wrd1+24EPUbz1RYQuSlcuHcyBwt3uGXVvXNQ6fCGLq5ikIi3NkMymecIB +9u1Jc96T0FkpSxQrqIIvlCP6gpaLBa+lz8ix8Xeb0uJ4Dg8RDmwTBfQU2ENagYLc +v0mpW7myAmqzGq1+xdgd2Cbt1FRB5t4YKqNl6+pFbeXL9EGbRoNPyuu+CfWrWVEe +JfWD1YzM6fhB0c/zqCxC32Y1h/sAzNFyYRmYUULh2MwVBVyt839h0jAUBzzBh0/q +EoVL5a9daDx3m6+PS1ACb+nXUSYaOu8lVM2rPxRjVITHBo+NHn012T8JNrwcExMn +FgeaVXo+NwIDAQABo4IBIjCCAR4wHQYDVR0OBBYEFDN5WuQBQ05geQStksygwwDM +bhBEMIHuBgNVHSMEgeYwgeOAFDN5WuQBQ05geQStksygwwDMbhBEoYG0pIGxMIGu +MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTE9MDsGA1UEChM0WE1M +IFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6Ly93d3cuYWxla3NleS5jb20veG1sc2Vj +KTEQMA4GA1UECxMHUm9vdCBDQTEWMBQGA1UEAxMNQWxla3NleSBTYW5pbjEhMB8G +CSqGSIb3DQEJARYSeG1sc2VjQGFsZWtzZXkuY29tghR3Ne5If2hi2vGzlW2WHMsP +pvNPTjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBVUqxGkYxvFZ7s +/Zkmjj1u88PvOjdj36LnGQCyVDwJPXXAXoqW9I3W3BPra/Xy1vjFo5erkdjvNh0f ++iyZVS/9EVdPKssPdZd39p0YIiUyG1RUYmN/IBDzSX/LwBTiLGlMBHFTRj6Lfs+e +Nfu6PISqABh+It3/3jlB882eixesdsjvtZc0J6sDka1byMoqe40twfI0tTMMv4Jr +QwGn7YeiVuWZ/GKUQxYrA+FfIWR5maBWtnhdmjaOBgmUhNtJB8yBcH82cRrxRIMX +f2quQrB3P4/WQC2bw3YPGxSv5wQoZwcwCn6f7g7oQFy6cANONMBG7GJpbw5B/8uB +S1a6d6Rg + +MIIFSDCCBDCgAwIBAgIUdzXuSH9oYtrxs5VtlhzLD6bzT08wDQYJKoZIhvcNAQEL +BQAwga4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMT0wOwYDVQQK +EzRYTUwgU2VjdXJpdHkgTGlicmFyeSAoaHR0cDovL3d3dy5hbGVrc2V5LmNvbS94 +bWxzZWMpMRAwDgYDVQQLEwdSb290IENBMRYwFAYDVQQDEw1BbGVrc2V5IFNhbmlu +MSEwHwYJKoZIhvcNAQkBFhJ4bWxzZWNAYWxla3NleS5jb20wIBcNMjYwMzA4MjIw +NzQyWhgPMjEyNjAyMTIyMjA3NDJaMIG2MQswCQYDVQQGEwJVUzETMBEGA1UECBMK +Q2FsaWZvcm5pYTE9MDsGA1UEChM0WE1MIFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6 +Ly93d3cuYWxla3NleS5jb20veG1sc2VjKTEYMBYGA1UECxMPU2Vjb25kIGxldmVs +IENBMRYwFAYDVQQDEw1BbGVrc2V5IFNhbmluMSEwHwYJKoZIhvcNAQkBFhJ4bWxz +ZWNAYWxla3NleS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDw +fEPVxQD3TLiNRpoh7g1KRYODmnJcdJzi7FMXfKuAgkhNmQaoAHQd7/pcwtg3oNUH +QukupST89AC7/qakF7ykdEQnVzxggYgdXbhfDZhLcaVUuMtFGgM6lHL0hnSZo8U9 +LHKWOlPIhJemE/XziHqgAsQposis7IRhuUlSsDa2xFW7MfS2xF/+UhiclaHgyBZ/ +RDzn2b5K14VAJdt1xRaoMC5zVIzu1uk33+j97L78+z65VRG7fxGTau2c94Mcl2V+ +KjDulHAnxLVJkjczo0mVi+u0Vczq9VhUqbNlig9TERQAPBC3D4ZJHhJsBmJz+47i +7pNL/Pms8qr9U0PgbZaPAgMBAAGjggFQMIIBTDAMBgNVHRMEBTADAQH/MCwGCWCG +SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E +FgQU0X0XrEVCio75sBcl1TxymJ2IOiUwge4GA1UdIwSB5jCB44AUM3la5AFDTmB5 +BK2SzKDDAMxuEEShgbSkgbEwga4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxp +Zm9ybmlhMT0wOwYDVQQKEzRYTUwgU2VjdXJpdHkgTGlicmFyeSAoaHR0cDovL3d3 +dy5hbGVrc2V5LmNvbS94bWxzZWMpMRAwDgYDVQQLEwdSb290IENBMRYwFAYDVQQD +Ew1BbGVrc2V5IFNhbmluMSEwHwYJKoZIhvcNAQkBFhJ4bWxzZWNAYWxla3NleS5j +b22CFHc17kh/aGLa8bOVbZYcyw+m809OMA0GCSqGSIb3DQEBCwUAA4IBAQAhrm/J +FdnYclb8HwQJdgGSYtUw2Wdrl1950H/ZUGwSKs6lGX8YT5xnj55AELLhbetTo+Be +Wwmg9kZbqnRC9tt0vIhFMko/uQZkn7vzrFEIfXgnEm2UGkkULfXH9pgtO4A9EQ2s +bbR4Oyi3n9q1w39aBdkUZnw3uthWKVHjcMW+n4m0RZBh4/snhHHlnxaIJzm4lB/s +DKNcXJTJHUbd1Kch5aOuSXCCmltwpEdEM9yaY1mr+jH9aD7lfo3FEJQxpO6M+AH6 +JDdmS2LzQUSXDO4fibegrI/IeTQeST92mZI4foLxqp6SG19WGs9sNFFDYCIl6lNq +LDAhZycGntLtGaZF + + + + some text + diff --git a/tests/fixtures_smoke.rs b/tests/fixtures_smoke.rs index 40abc54..c2231a5 100644 --- a/tests/fixtures_smoke.rs +++ b/tests/fixtures_smoke.rs @@ -176,8 +176,8 @@ fn fixture_file_count_matches_expected() { let mut count = 0; count_files_recursive(fixtures_dir(), &mut count); assert_eq!( - count, 81, - "expected 81 fixture files total (23 keys + 41 c14n + 16 donor xmldsig + 1 saml); \ + count, 82, + "expected 82 fixture files total (23 keys + 41 c14n + 17 donor xmldsig + 1 saml); \ if you added/removed files, update this count" ); }