diff --git a/Cargo.toml b/Cargo.toml index 8c02de2..6bd30f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ rsa = { version = "0.10.0-rc.18", optional = true } sha1 = { version = "0.11", features = ["oid"], optional = true } sha2 = { version = "0.11", features = ["oid"], optional = true } p256 = { version = "0.14", features = ["ecdsa"], optional = true } -p384 = { version = "0.14.0-rc.15", features = ["ecdsa"], optional = true } +p384 = { version = "0.14", features = ["ecdsa"], optional = true } p521 = { version = "0.14.0-rc.15", features = ["ecdsa"], optional = true } signature = { version = "3", optional = true } subtle = { version = "2", optional = true } @@ -31,6 +31,7 @@ subtle = { version = "2", optional = true } x509-parser = { version = "0.18", features = ["verify"], optional = true } der = { version = "0.8", optional = true } crypto-bigint = { version = "0.7", optional = true } +crypto-common = { version = "0.2", features = ["getrandom"], optional = true } # Base64 encoding/decoding base64 = "0.22" @@ -47,6 +48,7 @@ default = ["xmldsig", "c14n"] xmldsig = [ # XML Digital Signatures (sign + verify) "dep:der", "dep:crypto-bigint", + "dep:crypto-common", "dep:p256", "dep:p384", "dep:p521", diff --git a/README.md b/README.md index a1157c1..767ca41 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Pure Rust XML Security library. Drop-in replacement for libxmlsec1. ## Features - **C14N** — XML Canonicalization (inclusive + exclusive, W3C compliant) -- **XMLDSig** — XML Digital Signatures (verify pipeline implemented; signing in progress, enveloped/enveloping/detached) +- **XMLDSig** — XML Digital Signatures (verify pipeline + template signing implemented; broader signing interop in progress) - **XMLEnc** — XML Encryption (symmetric + asymmetric) - **X.509** — Certificate-based key extraction and validation @@ -39,13 +39,15 @@ 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) +- XMLDSig template signing pipeline (`DigestValue` fill + `SignedInfo` canonicalization + `SignatureValue` fill) - 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 +- RSA PKCS#1 v1.5 and ECDSA P-256/P-384 signing from PKCS#8 private keys - Opt-in X.509 certificate-chain validation with explicit trust anchors, validity checks, CA constraints, and CRLs Still in progress: -- XMLDSig signing pipeline +- XMLDSig signing KeyInfo writer, examples, and broader donor/CLI interop coverage - XMLEnc encryption/decryption pipeline Current toolchain target: latest stable Rust. diff --git a/src/xmldsig/mod.rs b/src/xmldsig/mod.rs index 93f6252..174a157 100644 --- a/src/xmldsig/mod.rs +++ b/src/xmldsig/mod.rs @@ -13,6 +13,7 @@ pub mod digest; pub mod keys; pub mod mutation; pub mod parse; +pub mod sign; pub mod signature; pub mod transforms; pub mod types; @@ -28,6 +29,11 @@ pub use parse::{ KeyInfo, KeyInfoSource, KeyValueInfo, ParseError, Reference, SignatureAlgorithm, SignedInfo, X509DataInfo, find_signature_node, parse_key_info, parse_signed_info, }; +pub use sign::{ + ComputedReferenceDigest, EcdsaP256SigningKey, EcdsaP384SigningKey, RsaSigningKey, SignContext, + SigningDigestError, SigningError, SigningKey, SigningKeyError, compute_reference_digest_values, + fill_reference_digest_values, +}; pub use signature::{ SignatureVerificationError, verify_ecdsa_signature_pem, verify_ecdsa_signature_spki, verify_rsa_signature_pem, verify_rsa_signature_spki, diff --git a/src/xmldsig/sign.rs b/src/xmldsig/sign.rs new file mode 100644 index 0000000..0895809 --- /dev/null +++ b/src/xmldsig/sign.rs @@ -0,0 +1,568 @@ +//! Signing-side XMLDSig digest computation. +//! +//! This pass fills `` elements before `` is +//! canonicalized and signed. It intentionally uses a signing-template parser +//! instead of [`crate::xmldsig::parse::parse_signed_info`], because verification +//! must continue to reject empty or malformed stored digest values. + +use base64::Engine; +use crypto_common::getrandom::SysRng; +use p256::ecdsa::{Signature as P256Signature, SigningKey as P256SigningKey}; +use p256::pkcs8::DecodePrivateKey; +use p384::ecdsa::{Signature as P384Signature, SigningKey as P384SigningKey}; +use roxmltree::{Document, Node}; +use rsa::RsaPrivateKey; +use rsa::pkcs1v15::Signature as RsaPkcs1v15Signature; +use rsa::pkcs1v15::SigningKey as RsaPkcs1v15SigningKey; +use rsa::signature::{RandomizedSigner, SignatureEncoding, Signer}; +use sha2::{Sha256, Sha384, Sha512}; +use std::collections::HashSet; + +use crate::c14n::canonicalize; + +use super::builder::{SignatureBuilder, SignatureBuilderError}; +use super::digest::{DigestAlgorithm, compute_digest}; +use super::mutation::{ + XmlMutationError, append_signature_to_root, fill_digest_values, fill_signature_values, +}; +use super::parse::{SignatureAlgorithm, XMLDSIG_NS, parse_signed_info}; +use super::transforms::{Transform, execute_transforms, parse_transforms}; +use super::types::TransformError; +use super::uri::UriReferenceResolver; + +/// Result for one computed signing-template reference digest. +#[derive(Debug, Clone, PartialEq, Eq)] +#[must_use = "use the computed digest value to fill the corresponding "] +pub struct ComputedReferenceDigest { + /// Zero-based reference index in `` document order. + pub index: usize, + /// Reference URI used for same-document dereference. + pub uri: String, + /// Digest algorithm declared by ``. + pub digest_method: DigestAlgorithm, + /// Base64-encoded digest value ready for ``. + pub digest_value: String, +} + +/// Errors returned by the XMLDSig signing digest pass. +#[derive(Debug, thiserror::Error)] +pub enum SigningDigestError { + /// The input XML document is not well-formed. + #[error("XML parse error: {0}")] + XmlParse(#[from] roxmltree::Error), + + /// Required XMLDSig element is missing. + #[error("missing required element: <{element}>")] + MissingElement { + /// Required element name. + element: &'static str, + }, + + /// XMLDSig template structure is invalid. + #[error("invalid signing template: {0}")] + InvalidStructure(String), + + /// Digest algorithm URI is not supported. + #[error("unsupported digest algorithm: {uri}")] + UnsupportedAlgorithm { + /// Unrecognized algorithm URI. + uri: String, + }, + + /// Digest algorithm is supported for verification but disabled for signing. + #[error("digest algorithm is disabled for signing: {uri}")] + SigningAlgorithmDisabled { + /// Algorithm URI rejected for new signatures. + uri: &'static str, + }, + + /// URI dereference or transform execution failed. + #[error("reference processing error: {0}")] + Transform(#[from] TransformError), + + /// Writing computed digest values back into XML failed. + #[error("XML mutation error: {0}")] + XmlMutation(#[from] XmlMutationError), +} + +/// Errors returned by the full XMLDSig signing pipeline. +#[derive(Debug, thiserror::Error)] +pub enum SigningError { + /// Reference digest computation failed. + #[error("signing digest pass failed: {0}")] + Digest(#[from] SigningDigestError), + + /// Parsing the digest-filled `` failed. + #[error("failed to parse SignedInfo after digest fill: {0}")] + ParseSignedInfo(#[from] super::parse::ParseError), + + /// SignedInfo canonicalization failed. + #[error("SignedInfo canonicalization failed: {0}")] + Canonicalization(#[from] crate::c14n::C14nError), + + /// Signing key preparation or signing failed. + #[error("signing key error: {0}")] + Key(#[from] SigningKeyError), + + /// Writing `` failed. + #[error("XML mutation error: {0}")] + XmlMutation(#[from] XmlMutationError), + + /// Signature template generation failed. + #[error("signature template error: {0}")] + Template(#[from] SignatureBuilderError), +} + +/// Errors while parsing or using XMLDSig signing keys. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum SigningKeyError { + /// PEM input could not be parsed. + #[error("invalid PEM private key")] + InvalidKeyPem, + + /// PEM block was not an unencrypted PKCS#8 private key. + #[error("invalid key format: expected PRIVATE KEY PEM, got {label}")] + InvalidKeyFormat { + /// Actual PEM label. + label: String, + }, + + /// DER bytes could not be decoded for the requested key type. + #[error("invalid PKCS#8 private key DER")] + InvalidKeyDer, + + /// The signing key cannot produce the requested XMLDSig algorithm. + #[error("signing key does not support algorithm: {uri}")] + UnsupportedAlgorithm { + /// XMLDSig signature algorithm URI. + uri: String, + }, + + /// The private-key signing operation failed. + #[error("private-key signing operation failed")] + SigningFailed, +} + +/// Private key abstraction used by [`SignContext`]. +pub trait SigningKey { + /// Sign canonicalized `` bytes for the declared XMLDSig method. + fn sign( + &self, + algorithm: SignatureAlgorithm, + canonical_signed_info: &[u8], + ) -> Result, SigningKeyError>; +} + +/// RSA PKCS#1 v1.5 private key for XMLDSig signing. +pub struct RsaSigningKey { + key: RsaPrivateKey, +} + +impl RsaSigningKey { + /// Parse an unencrypted PKCS#8 `PRIVATE KEY` PEM block. + pub fn from_pkcs8_pem(private_key_pem: &str) -> Result { + let private_key_der = parse_private_key_pem(private_key_pem)?; + Self::from_pkcs8_der(&private_key_der) + } + + /// Parse unencrypted PKCS#8 private key DER. + pub fn from_pkcs8_der(private_key_der: &[u8]) -> Result { + let key = RsaPrivateKey::from_pkcs8_der(private_key_der) + .map_err(|_| SigningKeyError::InvalidKeyDer)?; + Ok(Self { key }) + } +} + +impl SigningKey for RsaSigningKey { + fn sign( + &self, + algorithm: SignatureAlgorithm, + canonical_signed_info: &[u8], + ) -> Result, SigningKeyError> { + match algorithm { + SignatureAlgorithm::RsaSha256 => sign_rsa_pkcs1v15_with_rng( + RsaPkcs1v15SigningKey::::new(self.key.clone()), + canonical_signed_info, + ), + SignatureAlgorithm::RsaSha384 => sign_rsa_pkcs1v15_with_rng( + RsaPkcs1v15SigningKey::::new(self.key.clone()), + canonical_signed_info, + ), + SignatureAlgorithm::RsaSha512 => sign_rsa_pkcs1v15_with_rng( + RsaPkcs1v15SigningKey::::new(self.key.clone()), + canonical_signed_info, + ), + _ => Err(SigningKeyError::UnsupportedAlgorithm { + uri: algorithm.uri().to_string(), + }), + } + } +} + +fn sign_rsa_pkcs1v15_with_rng( + key: impl RandomizedSigner, + canonical_signed_info: &[u8], +) -> Result, SigningKeyError> { + let signature = key + .try_sign_with_rng(&mut SysRng, canonical_signed_info) + .map_err(|_| SigningKeyError::SigningFailed)?; + Ok(signature.to_vec()) +} + +/// ECDSA P-256 private key for XMLDSig signing. +pub struct EcdsaP256SigningKey { + key: P256SigningKey, +} + +impl EcdsaP256SigningKey { + /// Parse an unencrypted PKCS#8 `PRIVATE KEY` PEM block. + pub fn from_pkcs8_pem(private_key_pem: &str) -> Result { + let private_key_der = parse_private_key_pem(private_key_pem)?; + Self::from_pkcs8_der(&private_key_der) + } + + /// Parse unencrypted PKCS#8 private key DER. + pub fn from_pkcs8_der(private_key_der: &[u8]) -> Result { + let key = P256SigningKey::from_pkcs8_der(private_key_der) + .map_err(|_| SigningKeyError::InvalidKeyDer)?; + Ok(Self { key }) + } +} + +impl SigningKey for EcdsaP256SigningKey { + fn sign( + &self, + algorithm: SignatureAlgorithm, + canonical_signed_info: &[u8], + ) -> Result, SigningKeyError> { + if algorithm != SignatureAlgorithm::EcdsaP256Sha256 { + return Err(SigningKeyError::UnsupportedAlgorithm { + uri: algorithm.uri().to_string(), + }); + } + let signature: P256Signature = self + .key + .try_sign(canonical_signed_info) + .map_err(|_| SigningKeyError::SigningFailed)?; + Ok(signature.to_bytes().to_vec()) + } +} + +/// ECDSA P-384 private key for XMLDSig signing. +pub struct EcdsaP384SigningKey { + key: P384SigningKey, +} + +impl EcdsaP384SigningKey { + /// Parse an unencrypted PKCS#8 `PRIVATE KEY` PEM block. + pub fn from_pkcs8_pem(private_key_pem: &str) -> Result { + let private_key_der = parse_private_key_pem(private_key_pem)?; + Self::from_pkcs8_der(&private_key_der) + } + + /// Parse unencrypted PKCS#8 private key DER. + pub fn from_pkcs8_der(private_key_der: &[u8]) -> Result { + let key = P384SigningKey::from_pkcs8_der(private_key_der) + .map_err(|_| SigningKeyError::InvalidKeyDer)?; + Ok(Self { key }) + } +} + +impl SigningKey for EcdsaP384SigningKey { + fn sign( + &self, + algorithm: SignatureAlgorithm, + canonical_signed_info: &[u8], + ) -> Result, SigningKeyError> { + if algorithm != SignatureAlgorithm::EcdsaP384Sha384 { + return Err(SigningKeyError::UnsupportedAlgorithm { + uri: algorithm.uri().to_string(), + }); + } + let signature: P384Signature = self + .key + .try_sign(canonical_signed_info) + .map_err(|_| SigningKeyError::SigningFailed)?; + Ok(signature.to_bytes().to_vec()) + } +} + +/// XMLDSig signing context. +pub struct SignContext<'a> { + signing_key: &'a dyn SigningKey, +} + +impl<'a> SignContext<'a> { + /// Create a signing context using the supplied private key. + pub fn new(signing_key: &'a dyn SigningKey) -> Self { + Self { signing_key } + } + + /// Sign XML that already contains a `` template. + /// + /// The template must include empty `` and `` + /// targets. The pipeline fills reference digests, reparses the result, + /// canonicalizes ``, signs those canonical bytes, and fills the + /// base64 ``. + pub fn sign_template(&self, xml: &str) -> Result { + let with_digests = fill_reference_digest_values(xml)?; + let (algorithm, canonical_signed_info) = canonicalize_signed_info(&with_digests)?; + let signature_value = self.signing_key.sign(algorithm, &canonical_signed_info)?; + let signature_b64 = base64::engine::general_purpose::STANDARD.encode(signature_value); + Ok(fill_signature_values(&with_digests, [signature_b64])?) + } + + /// Build a signature template, append it to the source root, then sign it. + pub fn sign_with_builder( + &self, + xml: &str, + builder: &SignatureBuilder, + ) -> Result { + let template = builder.build_template()?; + let templated = append_signature_to_root(xml, &template)?; + self.sign_template(&templated) + } +} + +#[derive(Debug)] +struct SigningReference { + uri: String, + transforms: Vec, + digest_method: DigestAlgorithm, +} + +/// Compute base64 digest values for every `` in the signing template. +/// +/// References are processed in `` document order. The input must +/// contain exactly one XMLDSig `` element so an enveloped-signature +/// transform cannot accidentally target the wrong signature subtree. +pub fn compute_reference_digest_values( + xml: &str, +) -> Result, SigningDigestError> { + let doc = Document::parse(xml)?; + let signature = find_single_signature_node(&doc)?; + let signed_info = find_required_child(signature, "SignedInfo")?; + let references = parse_signing_references(signed_info)?; + let resolver = UriReferenceResolver::new(&doc); + + references + .into_iter() + .enumerate() + .map(|(index, reference)| { + let initial_data = resolver.dereference(&reference.uri)?; + let pre_digest = execute_transforms(signature, initial_data, &reference.transforms)?; + let digest = compute_digest(reference.digest_method, &pre_digest); + let digest_value = base64::engine::general_purpose::STANDARD.encode(digest); + Ok(ComputedReferenceDigest { + index, + uri: reference.uri, + digest_method: reference.digest_method, + digest_value, + }) + }) + .collect() +} + +/// Compute and fill all signing-template `` elements. +/// +/// This is the signing counterpart to verification reference processing: it +/// dereferences each ``, applies transforms, computes the digest, +/// and writes the base64 digest into the matching `` in document +/// order. +pub fn fill_reference_digest_values(xml: &str) -> Result { + let digest_values = compute_reference_digest_values(xml)? + .into_iter() + .map(|digest| digest.digest_value); + Ok(fill_digest_values(xml, digest_values)?) +} + +fn canonicalize_signed_info(xml: &str) -> Result<(SignatureAlgorithm, Vec), SigningError> { + let doc = Document::parse(xml).map_err(SigningDigestError::XmlParse)?; + let signature = find_single_signature_node(&doc).map_err(SigningError::Digest)?; + let signed_info_node = + find_required_child(signature, "SignedInfo").map_err(SigningError::Digest)?; + let signed_info = parse_signed_info(signed_info_node)?; + let signed_info_subtree: HashSet<_> = signed_info_node + .descendants() + .map(|node: Node<'_, '_>| node.id()) + .collect(); + let mut canonical_signed_info = Vec::new(); + canonicalize( + &doc, + Some(&|node| signed_info_subtree.contains(&node.id())), + &signed_info.c14n_method, + &mut canonical_signed_info, + )?; + Ok((signed_info.signature_method, canonical_signed_info)) +} + +fn parse_private_key_pem(private_key_pem: &str) -> Result, SigningKeyError> { + let (rest, pem) = x509_parser::pem::parse_x509_pem(private_key_pem.as_bytes()) + .map_err(|_| SigningKeyError::InvalidKeyPem)?; + if !rest.iter().all(|byte| byte.is_ascii_whitespace()) { + return Err(SigningKeyError::InvalidKeyPem); + } + if pem.label != "PRIVATE KEY" { + return Err(SigningKeyError::InvalidKeyFormat { label: pem.label }); + } + Ok(pem.contents) +} + +fn find_single_signature_node<'a>( + doc: &'a Document<'a>, +) -> Result, SigningDigestError> { + let mut signatures = doc.descendants().filter(|node| { + node.is_element() + && node.tag_name().name() == "Signature" + && node.tag_name().namespace() == Some(XMLDSIG_NS) + }); + let signature = signatures + .next() + .ok_or(SigningDigestError::MissingElement { + element: "Signature", + })?; + if signatures.next().is_some() { + return Err(SigningDigestError::InvalidStructure( + "expected exactly one element".into(), + )); + } + Ok(signature) +} + +fn parse_signing_references( + signed_info: Node<'_, '_>, +) -> Result, SigningDigestError> { + verify_ds_element(signed_info, "SignedInfo")?; + let mut children = element_children(signed_info); + + let c14n_node = children.next().ok_or(SigningDigestError::MissingElement { + element: "CanonicalizationMethod", + })?; + verify_ds_element(c14n_node, "CanonicalizationMethod")?; + required_algorithm_attr(c14n_node, "CanonicalizationMethod")?; + + let signature_method_node = children.next().ok_or(SigningDigestError::MissingElement { + element: "SignatureMethod", + })?; + verify_ds_element(signature_method_node, "SignatureMethod")?; + required_algorithm_attr(signature_method_node, "SignatureMethod")?; + + let mut references = Vec::new(); + for child in children { + verify_ds_element(child, "Reference")?; + references.push(parse_signing_reference(child)?); + } + if references.is_empty() { + return Err(SigningDigestError::MissingElement { + element: "Reference", + }); + } + Ok(references) +} + +fn parse_signing_reference( + reference_node: Node<'_, '_>, +) -> Result { + let uri = reference_node + .attribute("URI") + .ok_or_else(|| { + SigningDigestError::InvalidStructure( + "signing Reference must include URI attribute".into(), + ) + })? + .to_string(); + let mut children = element_children(reference_node); + + let mut transforms = Vec::new(); + let mut next = children.next().ok_or(SigningDigestError::MissingElement { + element: "DigestMethod", + })?; + if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) { + transforms = parse_transforms(next)?; + next = children.next().ok_or(SigningDigestError::MissingElement { + element: "DigestMethod", + })?; + } + + verify_ds_element(next, "DigestMethod")?; + let digest_uri = required_algorithm_attr(next, "DigestMethod")?; + let digest_method = DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| { + SigningDigestError::UnsupportedAlgorithm { + uri: digest_uri.to_string(), + } + })?; + if !digest_method.signing_allowed() { + return Err(SigningDigestError::SigningAlgorithmDisabled { + uri: digest_method.uri(), + }); + } + + let digest_value_node = children.next().ok_or(SigningDigestError::MissingElement { + element: "DigestValue", + })?; + verify_ds_element(digest_value_node, "DigestValue")?; + + if let Some(unexpected) = children.next() { + return Err(SigningDigestError::InvalidStructure(format!( + "unexpected element <{}> after in ", + unexpected.tag_name().name() + ))); + } + + Ok(SigningReference { + uri, + transforms, + digest_method, + }) +} + +fn find_required_child<'a>( + parent: Node<'a, 'a>, + child_name: &'static str, +) -> Result, SigningDigestError> { + parent + .children() + .find(|node| { + node.is_element() + && node.tag_name().name() == child_name + && node.tag_name().namespace() == Some(XMLDSIG_NS) + }) + .ok_or(SigningDigestError::MissingElement { + element: child_name, + }) +} + +fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator> { + node.children().filter(Node::is_element) +} + +fn verify_ds_element( + node: Node<'_, '_>, + expected_name: &'static str, +) -> Result<(), SigningDigestError> { + if !node.is_element() { + return Err(SigningDigestError::InvalidStructure(format!( + "expected element <{expected_name}>, got non-element node" + ))); + } + let tag = node.tag_name(); + if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) { + return Err(SigningDigestError::InvalidStructure(format!( + "expected , got <{}>", + tag.name() + ))); + } + Ok(()) +} + +fn required_algorithm_attr<'a>( + node: Node<'a, 'a>, + element_name: &'static str, +) -> Result<&'a str, SigningDigestError> { + node.attribute("Algorithm").ok_or_else(|| { + SigningDigestError::InvalidStructure(format!( + "missing Algorithm attribute on <{element_name}>" + )) + }) +} diff --git a/tests/signing_digest.rs b/tests/signing_digest.rs new file mode 100644 index 0000000..c982791 --- /dev/null +++ b/tests/signing_digest.rs @@ -0,0 +1,234 @@ +use xml_sec::c14n::{C14nAlgorithm, C14nMode}; +use xml_sec::xmldsig::mutation::append_signature_to_root; +use xml_sec::xmldsig::parse::{find_signature_node, parse_signed_info}; +use xml_sec::xmldsig::uri::UriReferenceResolver; +use xml_sec::xmldsig::verify::process_all_references; +use xml_sec::xmldsig::{ + DigestAlgorithm, DsigStatus, EcdsaP256SigningKey, EcdsaP384SigningKey, ReferenceBuilder, + RsaSigningKey, SignContext, SignatureAlgorithm, SignatureBuilder, SigningDigestError, + Transform, compute_reference_digest_values, fill_reference_digest_values, + verify_signature_with_pem_key, +}; + +fn exclusive_c14n() -> C14nAlgorithm { + C14nAlgorithm::new(C14nMode::Exclusive1_0, false) +} + +fn template_with_reference(reference: ReferenceBuilder) -> String { + SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256) + .add_reference(reference) + .build_template() + .expect("valid signature template") +} + +fn assert_reference_digests_verify(xml: &str) { + let document = roxmltree::Document::parse(xml).expect("filled XML must parse"); + let signature = find_signature_node(&document).expect("Signature element"); + let signed_info_node = signature + .children() + .find(|node| { + node.is_element() + && node.tag_name().name() == "SignedInfo" + && node.tag_name().namespace() == Some("http://www.w3.org/2000/09/xmldsig#") + }) + .expect("SignedInfo element"); + let signed_info = parse_signed_info(signed_info_node).expect("filled SignedInfo must parse"); + let resolver = UriReferenceResolver::new(&document); + let result = process_all_references(&signed_info.references, &resolver, signature, true) + .expect("reference verification must run"); + assert!(result.all_valid(), "filled digest values must verify"); +} + +fn read_fixture(path: &str) -> String { + std::fs::read_to_string(path).unwrap_or_else(|err| panic!("failed to read {path}: {err}")) +} + +#[test] +fn fills_single_same_document_reference_digest() { + // Signing templates start with an empty DigestValue; the digest pass must + // compute bytes from the referenced node and make the template parseable by + // the stricter verification parser. + let template = template_with_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#payload") + .transform(Transform::C14n(exclusive_c14n())), + ); + let xml = append_signature_to_root( + "hello", + &template, + ) + .expect("append signature"); + + let digests = compute_reference_digest_values(&xml).expect("compute digest"); + assert_eq!(digests.len(), 1); + assert_eq!(digests[0].index, 0); + assert_eq!(digests[0].uri, "#payload"); + assert_eq!(digests[0].digest_method, DigestAlgorithm::Sha256); + assert!(!digests[0].digest_value.is_empty()); + + let filled = fill_reference_digest_values(&xml).expect("fill digest values"); + assert_reference_digests_verify(&filled); +} + +#[test] +fn preserves_multiple_reference_digest_order() { + // DigestValue replacement is positional; this prevents accidentally sorting + // or otherwise normalizing Reference order before the SignedInfo pass. + let template = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256) + .add_reference(ReferenceBuilder::new(DigestAlgorithm::Sha256).uri("#first")) + .add_reference(ReferenceBuilder::new(DigestAlgorithm::Sha384).uri("#second")) + .build_template() + .expect("valid signature template"); + let xml = append_signature_to_root( + "onetwo", + &template, + ) + .expect("append signature"); + + let digests = compute_reference_digest_values(&xml).expect("compute digests"); + assert_eq!(digests.len(), 2); + assert_eq!(digests[0].index, 0); + assert_eq!(digests[0].uri, "#first"); + assert_eq!(digests[0].digest_method, DigestAlgorithm::Sha256); + assert_eq!(digests[1].index, 1); + assert_eq!(digests[1].uri, "#second"); + assert_eq!(digests[1].digest_method, DigestAlgorithm::Sha384); + assert_ne!(digests[0].digest_value, digests[1].digest_value); + + let filled = fill_reference_digest_values(&xml).expect("fill digest values"); + assert_reference_digests_verify(&filled); +} + +#[test] +fn computes_enveloped_signature_digest_for_whole_document() { + // URI="" signs the full document; the enveloped transform must exclude the + // generated Signature subtree before digesting, matching verification. + let template = template_with_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha512) + .uri("") + .transform(Transform::Enveloped) + .transform(Transform::C14n(exclusive_c14n())), + ); + let xml = append_signature_to_root("hello", &template) + .expect("append signature"); + + let filled = fill_reference_digest_values(&xml).expect("fill digest values"); + assert_reference_digests_verify(&filled); +} + +#[test] +fn rejects_reference_without_uri() { + // External/object reference support is not implicit: signing must know what + // bytes are being digested, so an omitted URI fails before mutation. + let template = template_with_reference(ReferenceBuilder::new(DigestAlgorithm::Sha256)); + let xml = append_signature_to_root("hello", &template) + .expect("append signature"); + + let err = compute_reference_digest_values(&xml).expect_err("missing URI must fail"); + assert!( + matches!(err, SigningDigestError::InvalidStructure(message) if message.contains("URI")) + ); +} + +#[test] +fn rejects_sha1_digest_for_signing_template() { + // SHA-1 remains verify-only. This manually crafted template bypasses the + // builder, so the digest pass must enforce the same policy before signing. + let xml = r##"hello"##; + + let err = compute_reference_digest_values(xml).expect_err("SHA-1 signing digest must fail"); + assert!(matches!( + err, + SigningDigestError::SigningAlgorithmDisabled { + uri: "http://www.w3.org/2000/09/xmldsig#sha1" + } + )); +} + +#[test] +fn signs_rsa_sha256_template_and_verifies_round_trip() { + // Full signing pipeline: append template, compute Reference digest, + // canonicalize SignedInfo, RSA-sign it, fill SignatureValue, then verify + // the final XML through the existing end-to-end verifier. + let private_key = + RsaSigningKey::from_pkcs8_pem(&read_fixture("tests/fixtures/keys/rsa/rsa-2048-key.pem")) + .expect("RSA private key fixture must parse"); + let public_key_pem = read_fixture("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let builder = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("") + .transform(Transform::Enveloped) + .transform(Transform::C14n(exclusive_c14n())), + ); + + let signed = SignContext::new(&private_key) + .sign_with_builder("hello", &builder) + .expect("RSA signing pipeline must succeed"); + let verify_result = verify_signature_with_pem_key(&signed, &public_key_pem, true) + .expect("signed RSA XML must verify without pipeline errors"); + + assert_eq!(verify_result.status, DsigStatus::Valid); + assert!(signed.contains("")); + assert!(!signed.contains("")); +} + +#[test] +fn signs_ecdsa_p256_template_and_verifies_round_trip() { + // ECDSA XMLDSig SignatureValue must be fixed-width r||s bytes, not ASN.1 + // DER. The verifier accepts the generated value as a final interop check. + let private_key = EcdsaP256SigningKey::from_pkcs8_pem(&read_fixture( + "tests/fixtures/keys/ec/ec-prime256v1-key.pem", + )) + .expect("P-256 private key fixture must parse"); + let public_key_pem = read_fixture("tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem"); + let builder = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::EcdsaP256Sha256) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#payload") + .transform(Transform::C14n(exclusive_c14n())), + ); + + let signed = SignContext::new(&private_key) + .sign_with_builder( + "hello", + &builder, + ) + .expect("ECDSA signing pipeline must succeed"); + let verify_result = verify_signature_with_pem_key(&signed, &public_key_pem, true) + .expect("signed ECDSA XML must verify without pipeline errors"); + + assert_eq!(verify_result.status, DsigStatus::Valid); + assert!(signed.contains("")); + assert!(!signed.contains("")); +} + +#[test] +fn signs_ecdsa_p384_template_and_verifies_round_trip() { + // P-384 uses the XMLDSig ecdsa-sha384 URI and the same fixed-width r||s + // SignatureValue encoding as P-256, with a wider component size. + let private_key = EcdsaP384SigningKey::from_pkcs8_pem(&read_fixture( + "tests/fixtures/keys/ec/ec-prime384v1-key.pem", + )) + .expect("P-384 private key fixture must parse"); + let public_key_pem = read_fixture("tests/fixtures/keys/ec/ec-prime384v1-pubkey.pem"); + let builder = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::EcdsaP384Sha384) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha384) + .uri("#payload") + .transform(Transform::C14n(exclusive_c14n())), + ); + + let signed = SignContext::new(&private_key) + .sign_with_builder( + "hello", + &builder, + ) + .expect("P-384 signing pipeline must succeed"); + let verify_result = verify_signature_with_pem_key(&signed, &public_key_pem, true) + .expect("signed P-384 XML must verify without pipeline errors"); + + assert_eq!(verify_result.status, DsigStatus::Valid); + assert!(signed.contains("")); + assert!(!signed.contains("")); +}