Skip to main content

xml_sec/xmldsig/
sign.rs

1//! Signing-side XMLDSig digest computation.
2//!
3//! This pass fills `<DigestValue>` elements before `<SignedInfo>` is
4//! canonicalized and signed. It intentionally uses a signing-template parser
5//! instead of [`crate::xmldsig::parse::parse_signed_info`], because verification
6//! must continue to reject empty or malformed stored digest values.
7
8use base64::Engine;
9use p256::ecdsa::{Signature as P256Signature, SigningKey as P256SigningKey};
10use p256::pkcs8::{DecodePrivateKey, EncodePublicKey};
11use p384::ecdsa::{Signature as P384Signature, SigningKey as P384SigningKey};
12use roxmltree::{Document, Node, NodeId};
13use rsa::RsaPrivateKey;
14use rsa::pkcs1v15::Signature as RsaPkcs1v15Signature;
15use rsa::pkcs1v15::SigningKey as RsaPkcs1v15SigningKey;
16use rsa::signature::{RandomizedSigner, SignatureEncoding};
17use rsa::traits::PublicKeyParts;
18use sha2::{Sha256, Sha384, Sha512};
19use signature::hazmat::PrehashSigner;
20use std::{collections::HashSet, ops::Range};
21use x509_parser::prelude::FromDer;
22
23use crate::c14n::canonicalize_bounded_with_xml_base_budget;
24
25use super::builder::{SignatureBuilder, SignatureBuilderError};
26use super::digest::DigestAlgorithm;
27use super::mutation::{
28    XmlMutationError, fill_signed_info_digest_values_at_index_with_budget,
29    fill_signed_info_digest_values_with_budget, merge_key_info_source_at_index_with_budget,
30    padded_base64_len_for_xml,
31};
32use super::parse::{
33    MAX_REFERENCES_PER_SIGNATURE, SignatureAlgorithm, XMLDSIG_NS,
34    parse_signed_info_with_xpath_budget,
35};
36use super::signature::{encode_ecdsa_signature_as_der, maximum_ecdsa_der_signature_len};
37use super::transforms::{
38    Transform, TransformExecutionBudget, TransformOptions, XPathHereSemantics,
39    XPathSignatureParseBudget, execute_transforms_with_dependency_nodes,
40    execute_transforms_with_options_and_budget, map_c14n_resource_policy_violation,
41    parse_transforms_with_budget, validate_signing_transform_policy,
42};
43use super::types::TransformError;
44use super::uri::{UriReferenceResolver, validate_signing_reference_uri};
45use super::verify::parse_signature_children;
46use crate::document::{DocumentParseSettings, XmlDocument, XmlDocumentError, XmlParseWorkBudget};
47
48/// Result for one computed signing-template reference digest.
49#[derive(Debug, Clone, PartialEq, Eq)]
50#[must_use = "use the computed digest value to fill the corresponding <DigestValue>"]
51pub struct ComputedReferenceDigest {
52    /// Zero-based reference index in `<SignedInfo>` document order.
53    pub index: usize,
54    /// Reference URI used for same-document dereference.
55    pub uri: String,
56    /// Digest algorithm declared by `<DigestMethod>`.
57    pub digest_method: DigestAlgorithm,
58    /// Base64-encoded digest value ready for `<DigestValue>`.
59    pub digest_value: String,
60}
61
62/// Errors returned by the XMLDSig signing digest pass.
63#[derive(Debug, thiserror::Error)]
64pub enum SigningDigestError {
65    /// The selected provider could not compute a reference digest.
66    #[error("cryptographic provider error: {0}")]
67    Provider(#[from] crate::provider::ProviderError),
68
69    /// The compiled signing policy rejected input while processing References.
70    ///
71    /// The lower-level digest APIs return this variant directly. The full
72    /// signing pipeline promotes every policy failure to [`SigningError::Policy`].
73    #[error("signing policy violation: {0}")]
74    Policy(#[from] crate::policy::PolicyViolation),
75
76    /// The input XML document is not well-formed.
77    #[error("XML parse error: {0}")]
78    XmlParse(#[from] roxmltree::Error),
79
80    /// The owned document boundary rejected a signing mutation.
81    #[error("XML document error: {0}")]
82    Document(#[from] XmlDocumentError),
83
84    /// Required XMLDSig element is missing.
85    #[error("missing required element: <{element}>")]
86    MissingElement {
87        /// Required element name.
88        element: &'static str,
89    },
90
91    /// XMLDSig template structure is invalid.
92    #[error("invalid signing template: {0}")]
93    InvalidStructure(String),
94
95    /// Digest algorithm URI is not supported.
96    #[error("unsupported digest algorithm: {uri}")]
97    UnsupportedAlgorithm {
98        /// Unrecognized algorithm URI.
99        uri: String,
100    },
101
102    /// Digest algorithm is supported for verification but disabled for signing.
103    #[error("digest algorithm is disabled for signing: {uri}")]
104    SigningAlgorithmDisabled {
105        /// Algorithm URI rejected for new signatures.
106        uri: &'static str,
107    },
108
109    /// URI dereference or transform execution failed.
110    #[error("reference processing error: {0}")]
111    Transform(#[from] TransformError),
112
113    /// Writing computed digest values back into XML failed.
114    #[error("XML mutation error: {0}")]
115    XmlMutation(#[from] XmlMutationError),
116}
117
118/// Errors returned by the full XMLDSig signing pipeline.
119#[derive(Debug, thiserror::Error)]
120pub enum SigningError {
121    /// The compiled signing policy rejected input outside the Reference digest stage.
122    #[error("signing policy violation: {0}")]
123    Policy(#[from] crate::policy::PolicyViolation),
124
125    /// Reference digest computation failed.
126    #[error("signing digest pass failed: {0}")]
127    Digest(SigningDigestError),
128
129    /// Parsing the digest-filled `<SignedInfo>` failed.
130    #[error("failed to parse SignedInfo after digest fill: {0}")]
131    ParseSignedInfo(super::parse::ParseError),
132
133    /// SignedInfo canonicalization failed.
134    #[error("SignedInfo canonicalization failed: {0}")]
135    Canonicalization(#[from] crate::c14n::C14nError),
136
137    /// Signing key preparation or signing failed.
138    #[error("signing key error: {0}")]
139    Key(#[from] SigningKeyError),
140
141    /// A signing provider returned bytes that cannot encode this key's signature.
142    #[error("signature output must be {expected} bytes, got {actual}")]
143    InvalidSignatureOutputLength {
144        /// Exact XMLDSig wire length implied by the signing public key.
145        expected: usize,
146        /// Actual provider output length.
147        actual: usize,
148    },
149
150    /// Writing `<SignatureValue>` failed.
151    #[error("XML mutation error: {0}")]
152    XmlMutation(XmlMutationError),
153
154    /// Writing `<KeyInfo>` failed.
155    #[error("KeyInfo writer error: {0}")]
156    KeyInfo(#[from] KeyInfoWriteError),
157
158    /// The owned XML document boundary rejected an identity or mutation.
159    #[error("XML document error: {0}")]
160    Document(#[from] XmlDocumentError),
161
162    /// Signature template generation failed.
163    #[error("signature template error: {0}")]
164    Template(SignatureBuilderError),
165}
166
167impl From<SigningDigestError> for SigningError {
168    fn from(error: SigningDigestError) -> Self {
169        match error {
170            SigningDigestError::XmlMutation(XmlMutationError::Policy(error)) => Self::Policy(error),
171            SigningDigestError::Policy(error)
172            | SigningDigestError::Transform(TransformError::Policy(error)) => Self::Policy(error),
173            SigningDigestError::Document(error) => Self::Document(error),
174            error => Self::Digest(error),
175        }
176    }
177}
178
179impl From<super::parse::ParseError> for SigningError {
180    fn from(error: super::parse::ParseError) -> Self {
181        match error {
182            super::parse::ParseError::Policy(error)
183            | super::parse::ParseError::Transform(TransformError::Policy(error)) => {
184                Self::Policy(error)
185            }
186            error => Self::ParseSignedInfo(error),
187        }
188    }
189}
190
191impl From<XmlMutationError> for SigningError {
192    fn from(error: XmlMutationError) -> Self {
193        match error {
194            XmlMutationError::Policy(error) => Self::Policy(error),
195            error => Self::XmlMutation(error),
196        }
197    }
198}
199
200impl From<SignatureBuilderError> for SigningError {
201    fn from(error: SignatureBuilderError) -> Self {
202        match error {
203            SignatureBuilderError::Policy(error) => Self::Policy(error),
204            error => Self::Template(error),
205        }
206    }
207}
208
209/// Errors while parsing or using XMLDSig signing keys.
210#[derive(Debug, thiserror::Error)]
211#[non_exhaustive]
212pub enum SigningKeyError {
213    /// The selected provider cannot execute the requested operation.
214    #[error("cryptographic provider error: {0}")]
215    Provider(#[from] crate::provider::ProviderError),
216
217    /// PEM input could not be parsed.
218    #[error("invalid PEM private key")]
219    InvalidKeyPem,
220
221    /// PEM block was not an unencrypted PKCS#8 private key.
222    #[error("invalid key format: expected PRIVATE KEY PEM, got {label}")]
223    InvalidKeyFormat {
224        /// Actual PEM label.
225        label: String,
226    },
227
228    /// DER bytes could not be decoded for the requested key type.
229    #[error("invalid PKCS#8 private key DER")]
230    InvalidKeyDer,
231
232    /// The signing key cannot produce the requested XMLDSig algorithm.
233    #[error("signing key does not support algorithm: {uri}")]
234    UnsupportedAlgorithm {
235        /// XMLDSig signature algorithm URI.
236        uri: String,
237    },
238
239    /// The private-key signing operation failed.
240    #[error("private-key signing operation failed")]
241    SigningFailed,
242
243    /// Public-key encoding failed for a supported signing key.
244    #[error("failed to encode signing public key as SPKI DER")]
245    PublicKeyEncodingFailed,
246
247    /// Public-key metadata cannot determine the XMLDSig signature framing.
248    #[error("invalid signing public-key metadata")]
249    InvalidPublicKeyInfo,
250}
251
252/// Public key material corresponding to a private XMLDSig signing key.
253#[derive(Debug, Clone, PartialEq, Eq)]
254#[non_exhaustive]
255pub enum SigningPublicKeyInfo {
256    /// RSA public key with DER SubjectPublicKeyInfo and normalized parameters.
257    Rsa {
258        /// DER-encoded SubjectPublicKeyInfo bytes.
259        spki_der: Vec<u8>,
260        /// Unsigned big-endian RSA modulus (`n`), normalized without leading zeroes.
261        modulus: Vec<u8>,
262        /// Unsigned big-endian RSA public exponent (`e`), normalized without leading zeroes.
263        exponent: Vec<u8>,
264    },
265    /// EC public key with DER SubjectPublicKeyInfo and XMLDSig 1.1 KeyValue data.
266    Ec {
267        /// DER-encoded SubjectPublicKeyInfo bytes.
268        spki_der: Vec<u8>,
269        /// Bare named-curve OID, without the XMLDSig `urn:oid:` prefix.
270        curve_oid: &'static str,
271        /// Uncompressed SEC1 point (`0x04 || x || y`).
272        public_key: Vec<u8>,
273    },
274}
275
276impl SigningPublicKeyInfo {
277    /// Return DER-encoded SubjectPublicKeyInfo bytes for this public key.
278    #[must_use]
279    pub fn spki_der(&self) -> &[u8] {
280        match self {
281            Self::Rsa { spki_der, .. } | Self::Ec { spki_der, .. } => spki_der,
282        }
283    }
284}
285
286/// Validate that a key can produce the requested algorithm under `policy`.
287///
288/// Key registries can use this preflight before selecting a candidate, ensuring
289/// lax ordered searches skip keys that the signing operation would reject.
290pub fn validate_signing_key(
291    key: &dyn SigningKey,
292    algorithm: SignatureAlgorithm,
293    policy: &crate::policy::SigningPolicy,
294) -> Result<(), SigningError> {
295    policy.resources.validate_key_candidates(1)?;
296    if !algorithm.signing_allowed() {
297        return Err(SigningKeyError::UnsupportedAlgorithm {
298            uri: algorithm.uri().to_owned(),
299        }
300        .into());
301    }
302    expected_signature_output_len(key, algorithm, policy).map(|_| ())
303}
304
305fn expected_signature_output_len(
306    key: &dyn SigningKey,
307    algorithm: SignatureAlgorithm,
308    policy: &crate::policy::SigningPolicy,
309) -> Result<usize, SigningError> {
310    let public_key = key.public_key_info()?;
311    let expected = match (algorithm, public_key) {
312        (
313            SignatureAlgorithm::RsaSha1
314            | SignatureAlgorithm::RsaSha256
315            | SignatureAlgorithm::RsaSha384
316            | SignatureAlgorithm::RsaSha512,
317            SigningPublicKeyInfo::Rsa {
318                modulus, exponent, ..
319            },
320        ) => policy
321            .rsa_keys
322            .validate_components("signing", &modulus, &exponent)?,
323        (
324            SignatureAlgorithm::EcdsaSha256 | SignatureAlgorithm::EcdsaSha384,
325            SigningPublicKeyInfo::Ec { public_key, .. },
326        ) if public_key.first() == Some(&0x04)
327            && public_key.len() > 1
328            && (public_key.len() - 1).is_multiple_of(2) =>
329        {
330            // XMLDSig serializes ECDSA as fixed-width r || s. An uncompressed
331            // SEC1 public point is 0x04 || x || y with the same field width.
332            public_key.len() - 1
333        }
334        (
335            SignatureAlgorithm::RsaSha1
336            | SignatureAlgorithm::RsaSha256
337            | SignatureAlgorithm::RsaSha384
338            | SignatureAlgorithm::RsaSha512,
339            SigningPublicKeyInfo::Ec { .. },
340        )
341        | (
342            SignatureAlgorithm::EcdsaSha256 | SignatureAlgorithm::EcdsaSha384,
343            SigningPublicKeyInfo::Rsa { .. },
344        ) => {
345            return Err(SigningKeyError::UnsupportedAlgorithm {
346                uri: algorithm.uri().to_owned(),
347            }
348            .into());
349        }
350        _ => return Err(SigningKeyError::InvalidPublicKeyInfo.into()),
351    };
352    Ok(expected)
353}
354
355fn validate_signature_output(expected: usize, signature: &[u8]) -> Result<(), SigningError> {
356    if signature.len() != expected {
357        return Err(SigningError::InvalidSignatureOutputLength {
358            expected,
359            actual: signature.len(),
360        });
361    }
362    Ok(())
363}
364
365/// Private key abstraction used by [`SignContext`].
366pub trait SigningKey {
367    /// Sign canonicalized `<SignedInfo>` bytes for the declared XMLDSig method.
368    fn sign(
369        &self,
370        algorithm: SignatureAlgorithm,
371        canonical_signed_info: &[u8],
372    ) -> Result<Vec<u8>, SigningKeyError>;
373
374    /// Sign while sourcing any primitive randomness from the selected provider.
375    ///
376    /// Deterministic or externally managed keys can rely on this default. Keys
377    /// whose primitive uses randomness, including RSA blinding, must override it.
378    fn sign_with_provider(
379        &self,
380        provider: &dyn crate::provider::CryptoProvider,
381        algorithm: SignatureAlgorithm,
382        canonical_signed_info: &[u8],
383    ) -> Result<Vec<u8>, SigningKeyError> {
384        let _ = provider;
385        self.sign(algorithm, canonical_signed_info)
386    }
387
388    /// Return structured public key material corresponding to this signing key.
389    fn public_key_info(&self) -> Result<SigningPublicKeyInfo, SigningKeyError>;
390}
391
392/// Writes signing key metadata into a template `<KeyInfo>` element.
393pub trait KeyInfoWriter {
394    /// Return XML child content for the direct `<Signature>/<KeyInfo>` element.
395    fn write_key_info(&self, signing_key: &dyn SigningKey) -> Result<String, KeyInfoWriteError>;
396}
397
398/// Errors while preparing XMLDSig signing `<KeyInfo>` output.
399#[derive(Debug, thiserror::Error)]
400#[non_exhaustive]
401pub enum KeyInfoWriteError {
402    /// PEM input could not be parsed.
403    #[error("invalid PEM certificate")]
404    InvalidCertificatePem,
405
406    /// PEM block was not an X.509 certificate.
407    #[error("invalid certificate format: expected CERTIFICATE PEM, got {label}")]
408    InvalidCertificateFormat {
409        /// Actual PEM label.
410        label: String,
411    },
412
413    /// DER bytes could not be decoded as one complete X.509 certificate.
414    #[error("invalid X.509 certificate DER")]
415    InvalidCertificateDer,
416
417    /// A certificate-backed KeyInfo writer requires at least one certificate.
418    #[error("X.509 certificate chain must not be empty")]
419    EmptyCertificateChain,
420
421    /// The signing key could not expose public-key material for validation.
422    #[error("signing key public-key extraction failed: {0}")]
423    SigningKey(#[from] SigningKeyError),
424
425    /// The configured certificate does not contain the signing key's public key.
426    #[error("X.509 certificate public key does not match signing key")]
427    CertificateKeyMismatch,
428}
429
430/// `<KeyInfo>` writer that embeds an ordered DER X.509 certificate chain.
431pub struct X509CertificateKeyInfoWriter {
432    certificates_der: Vec<Vec<u8>>,
433}
434
435impl X509CertificateKeyInfoWriter {
436    /// Parse a PEM `CERTIFICATE` block for XMLDSig `<X509Certificate>` output.
437    pub fn from_pem(certificate_pem: &str) -> Result<Self, KeyInfoWriteError> {
438        Self::from_pem_chain([certificate_pem])
439    }
440
441    /// Parse a leaf-first sequence of PEM `CERTIFICATE` blocks for `<X509Data>`.
442    ///
443    /// The first certificate must identify the signing key. Remaining issuer
444    /// certificates are emitted in caller order; this writer does not build or
445    /// validate issuer relationships because trust remains caller-owned.
446    pub fn from_pem_chain<I, S>(certificate_pems: I) -> Result<Self, KeyInfoWriteError>
447    where
448        I: IntoIterator<Item = S>,
449        S: AsRef<str>,
450    {
451        let mut certificates_der = Vec::new();
452        for certificate_pem in certificate_pems {
453            certificates_der.push(parse_certificate_pem(certificate_pem.as_ref())?);
454        }
455        Self::from_der_chain(certificates_der)
456    }
457
458    /// Validate and store DER certificate bytes for XMLDSig `<X509Certificate>` output.
459    pub fn from_der(certificate_der: &[u8]) -> Result<Self, KeyInfoWriteError> {
460        Self::from_der_chain([certificate_der])
461    }
462
463    /// Validate and store a leaf-first DER certificate chain for `<X509Data>`.
464    ///
465    /// The first certificate must identify the signing key. Remaining issuer
466    /// certificates are emitted in caller order; this writer does not build or
467    /// validate issuer relationships because trust remains caller-owned.
468    pub fn from_der_chain<I, B>(certificates_der: I) -> Result<Self, KeyInfoWriteError>
469    where
470        I: IntoIterator<Item = B>,
471        B: AsRef<[u8]>,
472    {
473        let certificates_der = certificates_der
474            .into_iter()
475            .map(|certificate_der| {
476                let certificate_der = certificate_der.as_ref();
477                let (rest, _) =
478                    x509_parser::certificate::X509Certificate::from_der(certificate_der)
479                        .map_err(|_| KeyInfoWriteError::InvalidCertificateDer)?;
480                if !rest.is_empty() {
481                    return Err(KeyInfoWriteError::InvalidCertificateDer);
482                }
483                Ok(certificate_der.to_vec())
484            })
485            .collect::<Result<Vec<_>, _>>()?;
486        if certificates_der.is_empty() {
487            return Err(KeyInfoWriteError::EmptyCertificateChain);
488        }
489        Ok(Self { certificates_der })
490    }
491}
492
493fn parse_certificate_pem(certificate_pem: &str) -> Result<Vec<u8>, KeyInfoWriteError> {
494    let (rest, pem) = x509_parser::pem::parse_x509_pem(certificate_pem.as_bytes())
495        .map_err(|_| KeyInfoWriteError::InvalidCertificatePem)?;
496    if !rest.iter().all(|byte| byte.is_ascii_whitespace()) {
497        return Err(KeyInfoWriteError::InvalidCertificatePem);
498    }
499    if pem.label != "CERTIFICATE" {
500        return Err(KeyInfoWriteError::InvalidCertificateFormat { label: pem.label });
501    }
502    Ok(pem.contents)
503}
504
505impl KeyInfoWriter for X509CertificateKeyInfoWriter {
506    fn write_key_info(&self, signing_key: &dyn SigningKey) -> Result<String, KeyInfoWriteError> {
507        let leaf_der = &self.certificates_der[0];
508        let (rest, certificate) = x509_parser::certificate::X509Certificate::from_der(leaf_der)
509            .map_err(|_| KeyInfoWriteError::InvalidCertificateDer)?;
510        if !rest.is_empty() {
511            return Err(KeyInfoWriteError::InvalidCertificateDer);
512        }
513        let signing_public_key = signing_key.public_key_info()?;
514        if certificate.public_key().raw != signing_public_key.spki_der() {
515            return Err(KeyInfoWriteError::CertificateKeyMismatch);
516        }
517
518        let mut xml = format!("<X509Data xmlns=\"{XMLDSIG_NS}\">");
519        for certificate_der in &self.certificates_der {
520            let certificate_b64 = base64::engine::general_purpose::STANDARD.encode(certificate_der);
521            xml.push_str("<X509Certificate>");
522            xml.push_str(&certificate_b64);
523            xml.push_str("</X509Certificate>");
524        }
525        xml.push_str("</X509Data>");
526        Ok(xml)
527    }
528}
529
530/// RSA PKCS#1 v1.5 private key for XMLDSig signing.
531pub struct RsaSigningKey {
532    key: RsaPrivateKey,
533}
534
535impl RsaSigningKey {
536    /// Parse an unencrypted PKCS#8 `PRIVATE KEY` PEM block.
537    pub fn from_pkcs8_pem(private_key_pem: &str) -> Result<Self, SigningKeyError> {
538        let private_key_der = parse_private_key_pem(private_key_pem)?;
539        Self::from_pkcs8_der(&private_key_der)
540    }
541
542    /// Parse unencrypted PKCS#8 private key DER.
543    pub fn from_pkcs8_der(private_key_der: &[u8]) -> Result<Self, SigningKeyError> {
544        let key = RsaPrivateKey::from_pkcs8_der(private_key_der)
545            .map_err(|_| SigningKeyError::InvalidKeyDer)?;
546        Ok(Self { key })
547    }
548}
549
550impl SigningKey for RsaSigningKey {
551    fn sign(
552        &self,
553        algorithm: SignatureAlgorithm,
554        canonical_signed_info: &[u8],
555    ) -> Result<Vec<u8>, SigningKeyError> {
556        self.sign_with_provider(
557            crate::provider::default_provider(),
558            algorithm,
559            canonical_signed_info,
560        )
561    }
562
563    fn sign_with_provider(
564        &self,
565        provider: &dyn crate::provider::CryptoProvider,
566        algorithm: SignatureAlgorithm,
567        canonical_signed_info: &[u8],
568    ) -> Result<Vec<u8>, SigningKeyError> {
569        match algorithm {
570            SignatureAlgorithm::RsaSha256 => sign_rsa_pkcs1v15_with_rng(
571                provider,
572                RsaPkcs1v15SigningKey::<Sha256>::new(self.key.clone()),
573                canonical_signed_info,
574            ),
575            SignatureAlgorithm::RsaSha384 => sign_rsa_pkcs1v15_with_rng(
576                provider,
577                RsaPkcs1v15SigningKey::<Sha384>::new(self.key.clone()),
578                canonical_signed_info,
579            ),
580            SignatureAlgorithm::RsaSha512 => sign_rsa_pkcs1v15_with_rng(
581                provider,
582                RsaPkcs1v15SigningKey::<Sha512>::new(self.key.clone()),
583                canonical_signed_info,
584            ),
585            _ => Err(SigningKeyError::UnsupportedAlgorithm {
586                uri: algorithm.uri().to_string(),
587            }),
588        }
589    }
590
591    fn public_key_info(&self) -> Result<SigningPublicKeyInfo, SigningKeyError> {
592        let public_key = self.key.to_public_key();
593        let spki_der = public_key
594            .to_public_key_der()
595            .map(|doc| doc.as_bytes().to_vec())
596            .map_err(|_| SigningKeyError::PublicKeyEncodingFailed)?;
597        Ok(SigningPublicKeyInfo::Rsa {
598            spki_der,
599            modulus: public_key.n().to_be_bytes_trimmed_vartime().into_vec(),
600            exponent: public_key.e().to_be_bytes_trimmed_vartime().into_vec(),
601        })
602    }
603}
604
605fn sign_rsa_pkcs1v15_with_rng(
606    provider: &dyn crate::provider::CryptoProvider,
607    key: impl RandomizedSigner<RsaPkcs1v15Signature>,
608    canonical_signed_info: &[u8],
609) -> Result<Vec<u8>, SigningKeyError> {
610    let mut rng = crate::provider::ProviderRng(provider);
611    let signature = key
612        .try_sign_with_rng(&mut rng, canonical_signed_info)
613        .map_err(|_| SigningKeyError::SigningFailed)?;
614    Ok(signature.to_vec())
615}
616
617/// ECDSA P-256 private key for XMLDSig signing.
618pub struct EcdsaP256SigningKey {
619    key: P256SigningKey,
620}
621
622impl EcdsaP256SigningKey {
623    /// Parse an unencrypted PKCS#8 `PRIVATE KEY` PEM block.
624    pub fn from_pkcs8_pem(private_key_pem: &str) -> Result<Self, SigningKeyError> {
625        let private_key_der = parse_private_key_pem(private_key_pem)?;
626        Self::from_pkcs8_der(&private_key_der)
627    }
628
629    /// Parse unencrypted PKCS#8 private key DER.
630    pub fn from_pkcs8_der(private_key_der: &[u8]) -> Result<Self, SigningKeyError> {
631        let key = P256SigningKey::from_pkcs8_der(private_key_der)
632            .map_err(|_| SigningKeyError::InvalidKeyDer)?;
633        Ok(Self { key })
634    }
635}
636
637impl SigningKey for EcdsaP256SigningKey {
638    fn sign(
639        &self,
640        algorithm: SignatureAlgorithm,
641        canonical_signed_info: &[u8],
642    ) -> Result<Vec<u8>, SigningKeyError> {
643        self.sign_with_provider(
644            crate::provider::default_provider(),
645            algorithm,
646            canonical_signed_info,
647        )
648    }
649
650    fn sign_with_provider(
651        &self,
652        provider: &dyn crate::provider::CryptoProvider,
653        algorithm: SignatureAlgorithm,
654        canonical_signed_info: &[u8],
655    ) -> Result<Vec<u8>, SigningKeyError> {
656        let digest_algorithm = match algorithm {
657            SignatureAlgorithm::EcdsaSha256 => DigestAlgorithm::Sha256,
658            SignatureAlgorithm::EcdsaSha384 => DigestAlgorithm::Sha384,
659            _ => {
660                return Err(SigningKeyError::UnsupportedAlgorithm {
661                    uri: algorithm.uri().to_string(),
662                });
663            }
664        };
665        let prehash =
666            super::compute_digest_with_provider(provider, digest_algorithm, canonical_signed_info)?;
667        let signature: P256Signature = self
668            .key
669            .sign_prehash(&prehash)
670            .map_err(|_| SigningKeyError::SigningFailed)?;
671        Ok(signature.to_bytes().to_vec())
672    }
673
674    fn public_key_info(&self) -> Result<SigningPublicKeyInfo, SigningKeyError> {
675        let verifying_key = self.key.verifying_key();
676        let spki_der = verifying_key
677            .to_public_key_der()
678            .map(|doc| doc.as_bytes().to_vec())
679            .map_err(|_| SigningKeyError::PublicKeyEncodingFailed)?;
680        Ok(SigningPublicKeyInfo::Ec {
681            spki_der,
682            curve_oid: "1.2.840.10045.3.1.7",
683            public_key: verifying_key.to_sec1_point(false).as_bytes().to_vec(),
684        })
685    }
686}
687
688/// ECDSA P-384 private key for XMLDSig signing.
689pub struct EcdsaP384SigningKey {
690    key: P384SigningKey,
691}
692
693impl EcdsaP384SigningKey {
694    /// Parse an unencrypted PKCS#8 `PRIVATE KEY` PEM block.
695    pub fn from_pkcs8_pem(private_key_pem: &str) -> Result<Self, SigningKeyError> {
696        let private_key_der = parse_private_key_pem(private_key_pem)?;
697        Self::from_pkcs8_der(&private_key_der)
698    }
699
700    /// Parse unencrypted PKCS#8 private key DER.
701    pub fn from_pkcs8_der(private_key_der: &[u8]) -> Result<Self, SigningKeyError> {
702        let key = P384SigningKey::from_pkcs8_der(private_key_der)
703            .map_err(|_| SigningKeyError::InvalidKeyDer)?;
704        Ok(Self { key })
705    }
706}
707
708impl SigningKey for EcdsaP384SigningKey {
709    fn sign(
710        &self,
711        algorithm: SignatureAlgorithm,
712        canonical_signed_info: &[u8],
713    ) -> Result<Vec<u8>, SigningKeyError> {
714        self.sign_with_provider(
715            crate::provider::default_provider(),
716            algorithm,
717            canonical_signed_info,
718        )
719    }
720
721    fn sign_with_provider(
722        &self,
723        provider: &dyn crate::provider::CryptoProvider,
724        algorithm: SignatureAlgorithm,
725        canonical_signed_info: &[u8],
726    ) -> Result<Vec<u8>, SigningKeyError> {
727        let digest_algorithm = match algorithm {
728            SignatureAlgorithm::EcdsaSha256 => DigestAlgorithm::Sha256,
729            SignatureAlgorithm::EcdsaSha384 => DigestAlgorithm::Sha384,
730            _ => {
731                return Err(SigningKeyError::UnsupportedAlgorithm {
732                    uri: algorithm.uri().to_string(),
733                });
734            }
735        };
736        let prehash =
737            super::compute_digest_with_provider(provider, digest_algorithm, canonical_signed_info)?;
738        let signature: P384Signature = self
739            .key
740            .sign_prehash(&prehash)
741            .map_err(|_| SigningKeyError::SigningFailed)?;
742        Ok(signature.to_bytes().to_vec())
743    }
744
745    fn public_key_info(&self) -> Result<SigningPublicKeyInfo, SigningKeyError> {
746        let verifying_key = self.key.verifying_key();
747        let spki_der = verifying_key
748            .to_public_key_der()
749            .map(|doc| doc.as_bytes().to_vec())
750            .map_err(|_| SigningKeyError::PublicKeyEncodingFailed)?;
751        Ok(SigningPublicKeyInfo::Ec {
752            spki_der,
753            curve_oid: "1.3.132.0.34",
754            public_key: verifying_key.to_sec1_point(false).as_bytes().to_vec(),
755        })
756    }
757}
758
759/// Select which existing XMLDSig template [`SignContext::sign_template`] signs.
760#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
761pub enum SignatureTemplateSelection {
762    /// Select the first descendant template in document order.
763    FirstDescendant,
764    /// Select the last descendant template, matching append-then-sign workflows.
765    #[default]
766    LastDescendant,
767}
768
769impl SignatureTemplateSelection {
770    const fn target(self) -> SigningSignatureTarget {
771        match self {
772            Self::FirstDescendant => SigningSignatureTarget::First,
773            Self::LastDescendant => SigningSignatureTarget::Last,
774        }
775    }
776}
777
778/// XMLDSig signing context.
779pub struct SignContext<'a> {
780    signing_key: &'a dyn SigningKey,
781    key_info_writer: Option<&'a dyn KeyInfoWriter>,
782    start_node_id: Option<&'a str>,
783    id_attributes: &'a [crate::IdAttributeRegistration],
784    template_selection: SignatureTemplateSelection,
785    policy: crate::policy::SigningPolicy,
786    provider: &'a dyn crate::provider::CryptoProvider,
787}
788
789impl<'a> SignContext<'a> {
790    /// Create a signing context using the supplied private key.
791    pub fn new(signing_key: &'a dyn SigningKey) -> Self {
792        Self {
793            signing_key,
794            key_info_writer: None,
795            start_node_id: None,
796            id_attributes: &[],
797            template_selection: SignatureTemplateSelection::default(),
798            policy: crate::policy::SigningPolicy::default(),
799            provider: crate::provider::default_provider(),
800        }
801    }
802
803    /// Replace the complete immutable signing policy snapshot.
804    #[must_use]
805    pub fn policy(mut self, policy: crate::policy::SigningPolicy) -> Self {
806        self.policy = policy;
807        self
808    }
809
810    /// Select the cryptographic provider for digest and randomness operations.
811    #[must_use]
812    pub fn provider(mut self, provider: &'a dyn crate::provider::CryptoProvider) -> Self {
813        self.provider = provider;
814        self
815    }
816
817    /// Configure signing to populate the direct `<Signature>/<KeyInfo>` placeholder.
818    #[must_use]
819    pub fn key_info_writer(mut self, writer: &'a dyn KeyInfoWriter) -> Self {
820        self.key_info_writer = Some(writer);
821        self
822    }
823
824    /// Select an operation start node by ID and scope template selection to its subtree.
825    ///
826    /// [`Self::sign_with_builder`] instead uses the selected node as the append
827    /// location and signs the newly appended direct `<Signature>` child.
828    #[must_use]
829    pub fn start_node_id(mut self, id: &'a str) -> Self {
830        self.start_node_id = Some(id);
831        self
832    }
833
834    /// Select which existing `<Signature>` template [`Self::sign_template`] signs.
835    ///
836    /// The default is [`SignatureTemplateSelection::LastDescendant`], preserving
837    /// append-then-sign behavior. Compatibility boundaries that model donor
838    /// document-order lookup can explicitly select `FirstDescendant`.
839    #[must_use]
840    pub fn signature_template_selection(mut self, selection: SignatureTemplateSelection) -> Self {
841        self.template_selection = selection;
842        self
843    }
844
845    /// Add caller-declared ID attributes for start-node and Reference lookup.
846    #[must_use]
847    pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self {
848        self.id_attributes = registrations;
849        self
850    }
851
852    /// Select the node returned by XPath's `here()` extension function.
853    ///
854    /// The default follows XMLDSig and returns the `<XPath>` parameter.
855    /// [`XPathHereSemantics::XmlSecLegacy`] is available only for producing
856    /// signatures compatible with libxmlsec1's `<Transform>` interpretation.
857    #[must_use]
858    pub fn xpath_here_semantics(mut self, semantics: XPathHereSemantics) -> Self {
859        self.policy.transforms.xpath_here_semantics = semantics;
860        self
861    }
862
863    /// Sign XML that already contains a `<Signature>` template.
864    ///
865    /// The template must include empty `<DigestValue>` and `<SignatureValue>`
866    /// targets. The pipeline first materializes configured `<KeyInfo>` content,
867    /// then fills reference digests, reparses the result, canonicalizes
868    /// `<SignedInfo>`, signs those canonical bytes, and fills the base64
869    /// `<SignatureValue>`. This ordering permits `<KeyInfo>` to be referenced
870    /// from `<SignedInfo>` without producing a stale digest.
871    pub fn sign_template(&self, xml: &str) -> Result<String, SigningError> {
872        self.policy.validate()?;
873        self.policy.resources.validate_xml_document_len(xml.len())?;
874        let mut budgets = SigningOperationBudgets::from_resources(&self.policy.resources);
875        let mut document = XmlDocument::parse_with_settings_and_budget(
876            xml.to_owned(),
877            DocumentParseSettings::from_policy(&self.policy.xml, &self.policy.resources),
878            budgets.transforms.xml_parse_work(),
879        )
880        .map_err(|error| match owned_document_policy_violation(error) {
881            Ok(error) => SigningError::Policy(error),
882            Err(XmlDocumentError::Parse(error)) => {
883                SigningError::Digest(SigningDigestError::XmlParse(error))
884            }
885            Err(error) => SigningError::Document(error),
886        })?;
887        self.validate_owned_document_input(&document)?;
888        self.sign_document_in_place(&mut document, &mut budgets)?;
889        Ok(document.into_xml())
890    }
891
892    /// Sign a template in a reusable owned document.
893    ///
894    /// Signing is atomic: failures leave both serialization and generation
895    /// unchanged. Success commits the complete signature as one generation.
896    pub fn sign_document(&self, document: &mut XmlDocument) -> Result<(), SigningError> {
897        self.validate_owned_document_input(document)?;
898        let mut budgets = SigningOperationBudgets::from_resources(&self.policy.resources);
899        let mut staged = document
900            .staged_copy_with_budget(
901                DocumentParseSettings::from_policy(&self.policy.xml, &self.policy.resources),
902                budgets.transforms.xml_parse_work(),
903            )
904            .map_err(map_owned_document_mutation_error)?;
905        self.sign_document_in_place(&mut staged, &mut budgets)?;
906        document
907            .commit_staged(staged)
908            .map_err(map_owned_document_mutation_error)
909    }
910
911    fn sign_document_in_place(
912        &self,
913        document: &mut XmlDocument,
914        budgets: &mut SigningOperationBudgets,
915    ) -> Result<(), SigningError> {
916        let target_signature = document.with_view(|view| {
917            signing_signature_index(
918                view.document(),
919                self.start_node_id,
920                self.id_attributes,
921                self.template_selection,
922            )
923        })?;
924        self.policy.resources.validate_key_candidates(1)?;
925        self.sign_template_at_index_with_budgets(document, target_signature, budgets)?;
926        Ok(())
927    }
928
929    fn validate_owned_document_input(&self, document: &XmlDocument) -> Result<(), SigningError> {
930        self.policy.validate()?;
931        document.validate_operation_policy(&self.policy.xml, &self.policy.resources)?;
932        Ok(())
933    }
934
935    fn sign_template_at_index_with_budgets(
936        &self,
937        document: &mut XmlDocument,
938        target_signature: usize,
939        budgets: &mut SigningOperationBudgets,
940    ) -> Result<(), SigningError> {
941        document.with_view(|view| {
942            let signature = find_signing_signature_node(
943                view.document(),
944                SigningSignatureTarget::Index(target_signature),
945            )?;
946            parse_signature_children(signature)
947                .map_err(|error| SigningDigestError::InvalidStructure(error.to_string()))?;
948            Ok::<_, SigningError>(())
949        })?;
950        let transform_options = TransformOptions::default()
951            .allow_internal_dtd(self.policy.xml.allow_internal_dtd)
952            .xpath_here_semantics(self.policy.transforms.xpath_here_semantics);
953        let with_key_info = if let Some(writer) = self.key_info_writer {
954            let key_info_content = writer.write_key_info(self.signing_key)?;
955            // Writer output is a separate untrusted XML input. Bound it before
956            // namespace wrapping or parsing, then bound the merged document below.
957            self.policy
958                .resources
959                .validate_xml_document_len(key_info_content.len())?;
960            // The mutation helper checks its namespace wrapper and every
961            // projected replacement against this policy before allocation.
962            let populated = merge_key_info_source_at_index_with_budget(
963                document.as_xml(),
964                &key_info_content,
965                target_signature,
966                Some(&self.policy),
967                Some(budgets.transforms.xml_parse_work()),
968            )?;
969            self.policy
970                .resources
971                .validate_xml_document_len(populated.len())?;
972            Some(populated)
973        } else {
974            None
975        };
976        if let Some(populated) = with_key_info {
977            document
978                .replace_serialized_with_settings(
979                    populated,
980                    DocumentParseSettings::from_policy(&self.policy.xml, &self.policy.resources),
981                    Some(budgets.transforms.xml_parse_work()),
982                )
983                .map_err(map_owned_document_mutation_error)?;
984        }
985        fill_reference_digest_values_in_dependency_order(
986            document,
987            transform_options,
988            &self.policy,
989            self.provider,
990            budgets,
991            target_signature,
992            self.id_attributes,
993        )?;
994        self.policy
995            .resources
996            .validate_xml_document_len(document.as_xml().len())?;
997        let (algorithm, canonical_signed_info) =
998            canonicalize_signed_info(document, &self.policy, budgets, target_signature)?;
999        budgets
1000            .transforms
1001            .charge_c14n_output(canonical_signed_info.len())
1002            .map_err(SigningDigestError::Transform)?;
1003        if !algorithm.signing_allowed()
1004            || self
1005                .policy
1006                .signature_algorithms
1007                .as_ref()
1008                .is_some_and(|allowed| !allowed.contains(&algorithm))
1009        {
1010            return Err(crate::policy::PolicyViolation::Algorithm {
1011                operation: "signing",
1012                algorithm: algorithm.uri().to_string(),
1013            }
1014            .into());
1015        }
1016        let expected_signature_len =
1017            expected_signature_output_len(self.signing_key, algorithm, &self.policy)?;
1018        let projected_signature_len = projected_signature_output_len(
1019            algorithm,
1020            expected_signature_len,
1021            self.policy.ecdsa_signature_value_encoding,
1022        )?;
1023        let encoded_signature_len =
1024            padded_base64_len_for_xml(projected_signature_len, &self.policy)?;
1025        let signature_value_node = document.with_view(|view| {
1026            let signature = find_signing_signature_node(
1027                view.document(),
1028                SigningSignatureTarget::Index(target_signature),
1029            )?;
1030            let signature_value = find_required_child(signature, "SignatureValue")?;
1031            Ok::<_, SigningError>(view.node_identity(signature_value))
1032        })?;
1033        let projected_document_len = document
1034            .projected_content_replacement_len(signature_value_node, encoded_signature_len)?;
1035        self.policy
1036            .resources
1037            .validate_xml_document_len(projected_document_len)?;
1038        self.provider
1039            .require_capability(crate::provider::ProviderCapability::Sign(algorithm))
1040            .map_err(SigningKeyError::from)?;
1041        let signature_value =
1042            self.provider
1043                .sign(self.signing_key, algorithm, &canonical_signed_info)?;
1044        validate_signature_output(expected_signature_len, &signature_value)?;
1045        let signature_value = encode_signature_output(
1046            algorithm,
1047            signature_value,
1048            self.policy.ecdsa_signature_value_encoding,
1049        )?;
1050        let signature_b64 = base64::engine::general_purpose::STANDARD.encode(signature_value);
1051        document
1052            .replace_base64_contents_with_budget(
1053                &[(signature_value_node, signature_b64)],
1054                DocumentParseSettings::from_policy(&self.policy.xml, &self.policy.resources),
1055                budgets.transforms.xml_parse_work(),
1056            )
1057            .map_err(map_owned_document_mutation_error)?;
1058        self.policy
1059            .resources
1060            .validate_xml_document_len(document.as_xml().len())?;
1061        Ok(())
1062    }
1063
1064    /// Build a signature template, append it to the selected start node (or
1065    /// the document root when no selector is set), then sign that new template.
1066    pub fn sign_with_builder(
1067        &self,
1068        xml: &str,
1069        builder: &SignatureBuilder,
1070    ) -> Result<String, SigningError> {
1071        self.policy.validate()?;
1072        self.policy.resources.validate_xml_document_len(xml.len())?;
1073        let mut budgets = SigningOperationBudgets::from_resources(&self.policy.resources);
1074        let mut document = XmlDocument::parse_with_settings_and_budget(
1075            xml.to_owned(),
1076            DocumentParseSettings::from_policy(&self.policy.xml, &self.policy.resources),
1077            budgets.transforms.xml_parse_work(),
1078        )
1079        .map_err(|error| match owned_document_policy_violation(error) {
1080            Ok(error) => SigningError::Policy(error),
1081            Err(XmlDocumentError::Parse(error)) => {
1082                SigningError::XmlMutation(XmlMutationError::XmlParse(error))
1083            }
1084            Err(error) => SigningError::Document(error),
1085        })?;
1086        self.validate_owned_document_input(&document)?;
1087        self.sign_document_with_builder_in_place(&mut document, builder, &mut budgets)?;
1088        Ok(document.into_xml())
1089    }
1090
1091    /// Build, append, and sign a signature in an owned document.
1092    pub fn sign_document_with_builder(
1093        &self,
1094        document: &mut XmlDocument,
1095        builder: &SignatureBuilder,
1096    ) -> Result<(), SigningError> {
1097        self.validate_owned_document_input(document)?;
1098        let mut budgets = SigningOperationBudgets::from_resources(&self.policy.resources);
1099        let mut staged = document
1100            .staged_copy_with_budget(
1101                DocumentParseSettings::from_policy(&self.policy.xml, &self.policy.resources),
1102                budgets.transforms.xml_parse_work(),
1103            )
1104            .map_err(map_owned_document_mutation_error)?;
1105        self.sign_document_with_builder_in_place(&mut staged, builder, &mut budgets)?;
1106        document
1107            .commit_staged(staged)
1108            .map_err(map_owned_document_mutation_error)
1109    }
1110
1111    fn sign_document_with_builder_in_place(
1112        &self,
1113        document: &mut XmlDocument,
1114        builder: &SignatureBuilder,
1115        budgets: &mut SigningOperationBudgets,
1116    ) -> Result<(), SigningError> {
1117        self.policy.resources.validate_key_candidates(1)?;
1118        let expected_signature_len = expected_signature_output_len(
1119            self.signing_key,
1120            builder.signature_method(),
1121            &self.policy,
1122        )?;
1123        let template = builder.build_template_with_policy_for_signature_output(
1124            &self.policy,
1125            expected_signature_len,
1126            &budgets.transforms,
1127            &mut budgets.xpath_parse,
1128        )?;
1129        let signature_parent = if let Some(id) = self.start_node_id {
1130            document.with_view(|view| {
1131                let start = signing_start_node(view.document(), id, self.id_attributes)?;
1132                Ok::<_, SigningError>(view.node_identity(start))
1133            })?
1134        } else {
1135            document.with_view(|view| view.root_element())
1136        };
1137        let projected_document_len =
1138            document.projected_child_append_len(signature_parent, template.len())?;
1139        self.policy
1140            .resources
1141            .validate_xml_document_len(projected_document_len)?;
1142        document
1143            .append_generated_child_with_budget(
1144                signature_parent,
1145                &template,
1146                DocumentParseSettings::from_policy(&self.policy.xml, &self.policy.resources),
1147                budgets.transforms.xml_parse_work(),
1148            )
1149            .map_err(map_owned_document_mutation_error)?;
1150        self.policy
1151            .resources
1152            .validate_xml_document_len(document.as_xml().len())?;
1153        let target_signature = document.with_view(|view| {
1154            let parent = if let Some(id) = self.start_node_id {
1155                signing_start_node(view.document(), id, self.id_attributes)?
1156            } else {
1157                view.document().root_element()
1158            };
1159            let appended = parent
1160                .children()
1161                .rfind(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
1162                .ok_or(SigningDigestError::MissingElement {
1163                    element: "Signature",
1164                })?;
1165            signature_index(view.document(), appended).map_err(SigningError::from)
1166        })?;
1167        self.sign_template_at_index_with_budgets(document, target_signature, budgets)?;
1168        Ok(())
1169    }
1170}
1171
1172fn projected_signature_output_len(
1173    algorithm: SignatureAlgorithm,
1174    raw_signature_len: usize,
1175    encoding: crate::policy::EcdsaSignatureValueEncoding,
1176) -> Result<usize, SigningError> {
1177    if matches!(
1178        (algorithm, encoding),
1179        (
1180            SignatureAlgorithm::EcdsaSha256 | SignatureAlgorithm::EcdsaSha384,
1181            crate::policy::EcdsaSignatureValueEncoding::XmlSecAsn1Der
1182        )
1183    ) {
1184        return maximum_ecdsa_der_signature_len(raw_signature_len)
1185            .ok_or(SigningKeyError::InvalidPublicKeyInfo.into());
1186    }
1187    Ok(raw_signature_len)
1188}
1189
1190fn map_owned_document_mutation_error(error: XmlDocumentError) -> SigningError {
1191    match owned_document_policy_violation(error) {
1192        Ok(error) => SigningError::Policy(error),
1193        Err(error) => SigningError::Document(error),
1194    }
1195}
1196
1197fn map_owned_document_digest_mutation_error(error: XmlDocumentError) -> SigningDigestError {
1198    match owned_document_policy_violation(error) {
1199        Ok(error) => SigningDigestError::Policy(error),
1200        Err(error) => SigningDigestError::Document(error),
1201    }
1202}
1203
1204fn owned_document_policy_violation(
1205    error: XmlDocumentError,
1206) -> Result<crate::policy::PolicyViolation, XmlDocumentError> {
1207    match error {
1208        XmlDocumentError::Policy(error) => Ok(error),
1209        XmlDocumentError::DocumentTooLarge { maximum, actual } => {
1210            Ok(crate::policy::PolicyViolation::ResourceLimit {
1211                resource: crate::policy::resource_name::XML_DOCUMENT,
1212                maximum,
1213                actual,
1214            })
1215        }
1216        XmlDocumentError::DocumentTooDeep { maximum, actual } => {
1217            Ok(crate::policy::PolicyViolation::ResourceLimit {
1218                resource: crate::policy::resource_name::XML_DEPTH,
1219                maximum,
1220                actual,
1221            })
1222        }
1223        XmlDocumentError::ProjectedNodeLimit { maximum } => {
1224            Ok(crate::policy::PolicyViolation::ResourceLimit {
1225                resource: crate::policy::resource_name::XML_NODES,
1226                maximum,
1227                actual: maximum.saturating_add(1),
1228            })
1229        }
1230        error => Err(error),
1231    }
1232}
1233
1234fn encode_signature_output(
1235    algorithm: SignatureAlgorithm,
1236    signature: Vec<u8>,
1237    encoding: crate::policy::EcdsaSignatureValueEncoding,
1238) -> Result<Vec<u8>, SigningError> {
1239    if matches!(
1240        (algorithm, encoding),
1241        (
1242            SignatureAlgorithm::EcdsaSha256 | SignatureAlgorithm::EcdsaSha384,
1243            crate::policy::EcdsaSignatureValueEncoding::XmlSecAsn1Der
1244        )
1245    ) {
1246        return encode_ecdsa_signature_as_der(&signature)
1247            .ok_or(SigningKeyError::InvalidPublicKeyInfo.into());
1248    }
1249    Ok(signature)
1250}
1251
1252#[derive(Debug, Clone)]
1253struct SigningReference {
1254    uri: String,
1255    transforms: Vec<Transform>,
1256    digest_method: DigestAlgorithm,
1257    digest_value_range: Range<usize>,
1258    digest_value_node_id: NodeId,
1259}
1260
1261struct SigningOperationBudgets {
1262    transforms: TransformExecutionBudget,
1263    xpath_parse: XPathSignatureParseBudget,
1264}
1265
1266impl SigningOperationBudgets {
1267    fn from_resources(resources: &crate::policy::ResourcePolicy) -> Self {
1268        Self {
1269            transforms: TransformExecutionBudget::from_resources(resources),
1270            xpath_parse: XPathSignatureParseBudget::from_resources(resources),
1271        }
1272    }
1273}
1274
1275impl Default for SigningOperationBudgets {
1276    fn default() -> Self {
1277        Self::from_resources(&crate::policy::ResourcePolicy::default())
1278    }
1279}
1280
1281/// Compute base64 digest values for every `<Reference>` in the signing template.
1282///
1283/// References are processed in `<SignedInfo>` document order under the last
1284/// XMLDSig `<Signature>` element. `sign_with_builder()` appends a new template
1285/// at the end of the source root, so older signatures in an already-signed
1286/// document must not become the signing target.
1287pub fn compute_reference_digest_values(
1288    xml: &str,
1289) -> Result<Vec<ComputedReferenceDigest>, SigningDigestError> {
1290    let execution_budget = TransformExecutionBudget::default();
1291    compute_reference_digest_values_with_options(
1292        xml,
1293        TransformOptions::default(),
1294        None,
1295        crate::provider::default_provider(),
1296        &execution_budget,
1297        None,
1298        &[],
1299    )
1300}
1301
1302fn compute_reference_digest_values_with_options(
1303    xml: &str,
1304    transform_options: TransformOptions,
1305    policy: Option<&crate::policy::SigningPolicy>,
1306    provider: &dyn crate::provider::CryptoProvider,
1307    execution_budget: &TransformExecutionBudget,
1308    target_signature: Option<usize>,
1309    id_attributes: &[crate::IdAttributeRegistration],
1310) -> Result<Vec<ComputedReferenceDigest>, SigningDigestError> {
1311    let doc = parse_signing_document(xml, policy, execution_budget.xml_parse_work())?;
1312    let signature = find_signing_signature_node(
1313        &doc,
1314        target_signature.map_or(SigningSignatureTarget::Last, SigningSignatureTarget::Index),
1315    )?;
1316    let signed_info = find_required_child(signature, "SignedInfo")?;
1317    let references = parse_signing_references(signed_info)?;
1318    validate_signing_references(&references, references.len(), policy)?;
1319    compute_signing_reference_digests(
1320        &doc,
1321        signature,
1322        references,
1323        transform_options,
1324        provider,
1325        execution_budget,
1326        SigningUriResolution {
1327            id_attributes,
1328            same_document_id_semantics: policy.map_or(
1329                crate::policy::SameDocumentIdSemantics::Specification,
1330                |policy| policy.transforms.same_document_id_semantics,
1331            ),
1332        },
1333    )
1334}
1335
1336fn fill_reference_digest_values_in_dependency_order(
1337    document: &mut XmlDocument,
1338    transform_options: TransformOptions,
1339    policy: &crate::policy::SigningPolicy,
1340    provider: &dyn crate::provider::CryptoProvider,
1341    budgets: &mut SigningOperationBudgets,
1342    target_signature: usize,
1343    id_attributes: &[crate::IdAttributeRegistration],
1344) -> Result<(), SigningDigestError> {
1345    let reference_limit = policy
1346        .resources
1347        .max_references
1348        .min(MAX_REFERENCES_PER_SIGNATURE);
1349    let process_manifests =
1350        policy.manifest_processing == crate::policy::ManifestProcessing::Process;
1351    let (signed_info_references, manifest_references) = document.with_view(|view| {
1352        let signature = find_signing_signature_node(
1353            view.document(),
1354            SigningSignatureTarget::Index(target_signature),
1355        )?;
1356        let signed_info = find_required_child(signature, "SignedInfo")?;
1357        let signed_info_references =
1358            parse_signing_references_with_budget(signed_info, &mut budgets.xpath_parse)?;
1359        validate_signing_references(
1360            &signed_info_references,
1361            signed_info_references.len(),
1362            Some(policy),
1363        )?;
1364        let manifest_references = if process_manifests {
1365            parse_signing_manifest_references(
1366                signature,
1367                &mut budgets.xpath_parse,
1368                reference_limit.saturating_sub(signed_info_references.len()),
1369                reference_limit,
1370            )?
1371        } else {
1372            Vec::new()
1373        };
1374        Ok::<_, SigningDigestError>((signed_info_references, manifest_references))
1375    })?;
1376    let total_references = signed_info_references
1377        .len()
1378        .checked_add(manifest_references.len())
1379        .ok_or_else(|| SigningDigestError::InvalidStructure("reference count overflow".into()))?;
1380    validate_signing_references(&manifest_references, total_references, Some(policy))?;
1381    let placeholder = "AA==";
1382    // SignatureValue is the final mutable value in the signing pipeline. Give
1383    // it concrete character data during analysis so references that retain the
1384    // existing or future text cannot be mistaken for stable inputs.
1385    let analysis_replacements = document.with_view(|view| {
1386        let signature = find_signing_signature_node(
1387            view.document(),
1388            SigningSignatureTarget::Index(target_signature),
1389        )?;
1390        let signature_value = find_required_child(signature, "SignatureValue")?;
1391        let mut replacements = signed_info_references
1392            .iter()
1393            .chain(&manifest_references)
1394            .map(|reference| {
1395                (
1396                    view.node_identity_by_id(reference.digest_value_node_id),
1397                    placeholder.to_owned(),
1398                )
1399            })
1400            .collect::<Vec<_>>();
1401        replacements.push((view.node_identity(signature_value), placeholder.to_owned()));
1402        Ok::<_, SigningDigestError>(replacements)
1403    })?;
1404    let analysis_xml = document
1405        .project_base64_contents(
1406            &analysis_replacements,
1407            policy.resources.max_xml_document_bytes,
1408        )
1409        .map_err(map_owned_document_digest_mutation_error)?;
1410    let analysis_doc = parse_signing_document(
1411        &analysis_xml,
1412        Some(policy),
1413        budgets.transforms.xml_parse_work(),
1414    )?;
1415    let analysis_signature = find_signing_signature_node(
1416        &analysis_doc,
1417        SigningSignatureTarget::Index(target_signature),
1418    )?;
1419    let analysis_signed_info = find_required_child(analysis_signature, "SignedInfo")?;
1420    let mut analysis_references =
1421        parse_signing_references_with_budget(analysis_signed_info, &mut budgets.xpath_parse)?;
1422    if process_manifests {
1423        analysis_references.extend(parse_signing_manifest_references(
1424            analysis_signature,
1425            &mut budgets.xpath_parse,
1426            reference_limit.saturating_sub(signed_info_references.len()),
1427            reference_limit,
1428        )?);
1429    }
1430    let dependency_plan = reference_dependency_levels(
1431        &analysis_doc,
1432        analysis_signature,
1433        &analysis_references,
1434        transform_options,
1435        &budgets.transforms,
1436        id_attributes,
1437        policy.transforms.same_document_id_semantics,
1438    )?;
1439    for level in dependency_plan {
1440        let replacements = document.with_view(|view| {
1441            let current_doc = view.document();
1442            let current_signature = find_signing_signature_node(
1443                current_doc,
1444                SigningSignatureTarget::Index(target_signature),
1445            )?;
1446            let current_signed_info = find_required_child(current_signature, "SignedInfo")?;
1447            let current_signed_info_references = parse_signing_references_with_budget(
1448                current_signed_info,
1449                &mut budgets.xpath_parse,
1450            )?;
1451            let current_manifest_references = if process_manifests {
1452                parse_signing_manifest_references(
1453                    current_signature,
1454                    &mut budgets.xpath_parse,
1455                    reference_limit.saturating_sub(signed_info_references.len()),
1456                    reference_limit,
1457                )?
1458            } else {
1459                Vec::new()
1460            };
1461            if current_signed_info_references.len() != signed_info_references.len()
1462                || current_manifest_references.len() != manifest_references.len()
1463            {
1464                return Err(SigningDigestError::InvalidStructure(
1465                    "signing Reference set changed while filling digests".into(),
1466                ));
1467            }
1468            let mut destinations = Vec::with_capacity(level.len());
1469            let mut level_references = Vec::with_capacity(level.len());
1470            for index in &level {
1471                let reference = if *index < signed_info_references.len() {
1472                    current_signed_info_references.get(*index)
1473                } else {
1474                    current_manifest_references.get(*index - signed_info_references.len())
1475                }
1476                .ok_or_else(|| {
1477                    SigningDigestError::InvalidStructure(
1478                        "signing Reference set changed while filling digests".into(),
1479                    )
1480                })?;
1481                destinations.push(view.node_identity_by_id(reference.digest_value_node_id));
1482                level_references.push(reference.clone());
1483            }
1484            let computed = compute_signing_reference_digests(
1485                current_doc,
1486                current_signature,
1487                level_references,
1488                transform_options,
1489                provider,
1490                &budgets.transforms,
1491                SigningUriResolution {
1492                    id_attributes,
1493                    same_document_id_semantics: policy.transforms.same_document_id_semantics,
1494                },
1495            )?;
1496            if computed.len() != destinations.len() {
1497                return Err(SigningDigestError::InvalidStructure(
1498                    "signing Reference set changed while computing digests".into(),
1499                ));
1500            }
1501            Ok::<_, SigningDigestError>(
1502                destinations
1503                    .into_iter()
1504                    .zip(computed)
1505                    .map(|(target, digest)| (target, digest.digest_value))
1506                    .collect::<Vec<_>>(),
1507            )
1508        })?;
1509        document
1510            .replace_base64_contents_with_budget(
1511                &replacements,
1512                DocumentParseSettings::from_policy(&policy.xml, &policy.resources),
1513                budgets.transforms.xml_parse_work(),
1514            )
1515            .map_err(map_owned_document_digest_mutation_error)?;
1516    }
1517    Ok(())
1518}
1519
1520fn reference_dependency_levels(
1521    doc: &Document<'_>,
1522    signature: Node<'_, '_>,
1523    references: &[SigningReference],
1524    transform_options: TransformOptions,
1525    execution_budget: &TransformExecutionBudget,
1526    id_attributes: &[crate::IdAttributeRegistration],
1527    same_document_id_semantics: crate::policy::SameDocumentIdSemantics,
1528) -> Result<Vec<Vec<usize>>, SigningDigestError> {
1529    let resolver = UriReferenceResolver::with_id_registrations(doc, id_attributes)
1530        .with_same_document_id_semantics(same_document_id_semantics);
1531    let terminal_signature_value_index = references.len();
1532    let signature_value = find_required_child(signature, "SignatureValue")?;
1533    let mut tracked_mutable_nodes = references
1534        .iter()
1535        .enumerate()
1536        .flat_map(|(index, reference)| {
1537            std::iter::once((index, reference.digest_value_node_id)).chain(
1538                doc.get_node(reference.digest_value_node_id)
1539                    .into_iter()
1540                    .flat_map(|node| node.children())
1541                    .filter(|node| node.is_text())
1542                    .map(move |node| (index, node.id())),
1543            )
1544        })
1545        .collect::<Vec<_>>();
1546    tracked_mutable_nodes.push((terminal_signature_value_index, signature_value.id()));
1547    tracked_mutable_nodes.extend(
1548        signature_value
1549            .children()
1550            .filter(|node| node.is_text())
1551            .map(|node| (terminal_signature_value_index, node.id())),
1552    );
1553    let analyses = references
1554        .iter()
1555        .map(|reference| {
1556            let initial_data = resolver.dereference_with_budget(
1557                &reference.uri,
1558                execution_budget.node_set_materialization(),
1559            )?;
1560            let output = execute_transforms_with_dependency_nodes(
1561                signature,
1562                initial_data,
1563                &reference.transforms,
1564                transform_options,
1565                execution_budget,
1566                tracked_mutable_nodes.clone(),
1567            )?;
1568            Ok(output.dependencies)
1569        })
1570        .collect::<Result<Vec<_>, SigningDigestError>>()?;
1571    if analyses
1572        .iter()
1573        .any(|dependencies| dependencies.contains(&terminal_signature_value_index))
1574    {
1575        return Err(SigningDigestError::InvalidStructure(
1576            "Reference dependency cycle includes the mutable SignatureValue".into(),
1577        ));
1578    }
1579    let mut dependencies = analyses;
1580    let mut completed = vec![false; references.len()];
1581    let mut levels = Vec::new();
1582    while completed.iter().any(|done| !done) {
1583        let ready = dependencies
1584            .iter()
1585            .enumerate()
1586            .filter_map(|(index, dependencies)| {
1587                (!completed[index] && dependencies.is_empty()).then_some(index)
1588            })
1589            .collect::<Vec<_>>();
1590        if ready.is_empty() {
1591            return Err(SigningDigestError::InvalidStructure(
1592                "Manifest Reference digest dependency cycle".into(),
1593            ));
1594        }
1595        for index in &ready {
1596            completed[*index] = true;
1597        }
1598        for dependency_set in &mut dependencies {
1599            dependency_set.retain(|dependency| !completed[*dependency]);
1600        }
1601        levels.push(ready);
1602    }
1603
1604    // Every mutable DigestValue belongs to the selected Signature;
1605    // an enveloped transform excludes that complete subtree and therefore
1606    // removes all such dependencies from its input node-set.
1607    debug_assert!(references.iter().all(|reference| {
1608        signature.range().start <= reference.digest_value_range.start
1609            && signature.range().end >= reference.digest_value_range.end
1610    }));
1611    Ok(levels)
1612}
1613
1614fn validate_signing_references(
1615    references: &[SigningReference],
1616    total_references: usize,
1617    policy: Option<&crate::policy::SigningPolicy>,
1618) -> Result<(), SigningDigestError> {
1619    let Some(policy) = policy else {
1620        return Ok(());
1621    };
1622    if total_references > policy.resources.max_references {
1623        return Err(crate::policy::PolicyViolation::ResourceLimit {
1624            resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
1625            maximum: policy.resources.max_references,
1626            actual: total_references,
1627        }
1628        .into());
1629    }
1630    for reference in references {
1631        validate_signing_reference_uri(&reference.uri, policy)?;
1632        if reference.transforms.len() > policy.resources.max_transforms_per_reference {
1633            return Err(crate::policy::PolicyViolation::ResourceLimit {
1634                resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
1635                maximum: policy.resources.max_transforms_per_reference,
1636                actual: reference.transforms.len(),
1637            }
1638            .into());
1639        }
1640        if policy
1641            .digest_algorithms
1642            .as_ref()
1643            .is_some_and(|allowed| !allowed.contains(&reference.digest_method))
1644        {
1645            return Err(crate::policy::PolicyViolation::Algorithm {
1646                operation: "signing",
1647                algorithm: reference.digest_method.uri().to_string(),
1648            }
1649            .into());
1650        }
1651        let initial_binary = !reference.uri.is_empty() && !reference.uri.starts_with('#');
1652        validate_signing_transform_policy(
1653            initial_binary,
1654            &reference.transforms,
1655            policy.transforms.allowed_algorithms.as_ref(),
1656        )?;
1657    }
1658    Ok(())
1659}
1660
1661struct SigningUriResolution<'a> {
1662    id_attributes: &'a [crate::IdAttributeRegistration],
1663    same_document_id_semantics: crate::policy::SameDocumentIdSemantics,
1664}
1665
1666fn compute_signing_reference_digests(
1667    doc: &Document<'_>,
1668    signature: Node<'_, '_>,
1669    references: Vec<SigningReference>,
1670    transform_options: TransformOptions,
1671    provider: &dyn crate::provider::CryptoProvider,
1672    execution_budget: &TransformExecutionBudget,
1673    uri_resolution: SigningUriResolution<'_>,
1674) -> Result<Vec<ComputedReferenceDigest>, SigningDigestError> {
1675    let resolver = UriReferenceResolver::with_id_registrations(doc, uri_resolution.id_attributes)
1676        .with_same_document_id_semantics(uri_resolution.same_document_id_semantics);
1677    references
1678        .into_iter()
1679        .enumerate()
1680        .map(|(index, reference)| {
1681            let initial_data = resolver.dereference_with_budget(
1682                &reference.uri,
1683                execution_budget.node_set_materialization(),
1684            )?;
1685            let pre_digest = execute_transforms_with_options_and_budget(
1686                signature,
1687                initial_data,
1688                &reference.transforms,
1689                transform_options,
1690                execution_budget,
1691            )?;
1692            let digest = super::compute_digest_with_provider(
1693                provider,
1694                reference.digest_method,
1695                &pre_digest,
1696            )?;
1697            let digest_value = base64::engine::general_purpose::STANDARD.encode(digest);
1698            Ok(ComputedReferenceDigest {
1699                index,
1700                uri: reference.uri,
1701                digest_method: reference.digest_method,
1702                digest_value,
1703            })
1704        })
1705        .collect()
1706}
1707
1708/// Compute and fill all signing-template `<DigestValue>` elements.
1709///
1710/// This is the signing counterpart to verification reference processing: it
1711/// dereferences each `<Reference>`, applies transforms, computes the digest,
1712/// and writes the base64 digest into the matching `<DigestValue>` in document
1713/// order.
1714pub fn fill_reference_digest_values(xml: &str) -> Result<String, SigningDigestError> {
1715    let execution_budget = TransformExecutionBudget::default();
1716    fill_reference_digest_values_with_options(
1717        xml,
1718        TransformOptions::default(),
1719        None,
1720        crate::provider::default_provider(),
1721        &execution_budget,
1722        None,
1723        &[],
1724    )
1725}
1726
1727fn fill_reference_digest_values_with_options(
1728    xml: &str,
1729    transform_options: TransformOptions,
1730    policy: Option<&crate::policy::SigningPolicy>,
1731    provider: &dyn crate::provider::CryptoProvider,
1732    execution_budget: &TransformExecutionBudget,
1733    target_signature: Option<usize>,
1734    id_attributes: &[crate::IdAttributeRegistration],
1735) -> Result<String, SigningDigestError> {
1736    let digest_values = compute_reference_digest_values_with_options(
1737        xml,
1738        transform_options,
1739        policy,
1740        provider,
1741        execution_budget,
1742        target_signature,
1743        id_attributes,
1744    )?
1745    .into_iter()
1746    .map(|digest| digest.digest_value);
1747    Ok(if let Some(target_signature) = target_signature {
1748        fill_signed_info_digest_values_at_index_with_budget(
1749            xml,
1750            digest_values,
1751            target_signature,
1752            policy,
1753            Some(execution_budget.xml_parse_work()),
1754        )?
1755    } else if let Some(policy) = policy {
1756        fill_signed_info_digest_values_with_budget(
1757            xml,
1758            digest_values,
1759            Some(policy),
1760            Some(execution_budget.xml_parse_work()),
1761        )?
1762    } else {
1763        fill_signed_info_digest_values_with_budget(
1764            xml,
1765            digest_values,
1766            None,
1767            Some(execution_budget.xml_parse_work()),
1768        )?
1769    })
1770}
1771
1772fn canonicalize_signed_info(
1773    document: &XmlDocument,
1774    policy: &crate::policy::SigningPolicy,
1775    budgets: &mut SigningOperationBudgets,
1776    target_signature: usize,
1777) -> Result<(SignatureAlgorithm, Vec<u8>), SigningError> {
1778    document.with_view(|view| {
1779        let doc = view.document();
1780        let signature =
1781            find_signing_signature_node(doc, SigningSignatureTarget::Index(target_signature))
1782                .map_err(SigningError::Digest)?;
1783        let signed_info_node =
1784            find_required_child(signature, "SignedInfo").map_err(SigningError::Digest)?;
1785        let signed_info =
1786            parse_signed_info_with_xpath_budget(signed_info_node, &mut budgets.xpath_parse)?;
1787        if policy
1788            .transforms
1789            .allowed_algorithms
1790            .as_ref()
1791            .is_some_and(|allowed| !allowed.contains(signed_info.c14n_method.uri()))
1792        {
1793            return Err(crate::policy::PolicyViolation::Algorithm {
1794                operation: "SignedInfo canonicalization",
1795                algorithm: signed_info.c14n_method.uri().to_owned(),
1796            }
1797            .into());
1798        }
1799        let signed_info_subtree: HashSet<_> = signed_info_node
1800            .descendants()
1801            .map(|node: Node<'_, '_>| node.id())
1802            .collect();
1803        let mut canonical_signed_info = Vec::new();
1804        canonicalize_bounded_with_xml_base_budget(
1805            doc,
1806            Some(&|node| signed_info_subtree.contains(&node.id())),
1807            &signed_info.c14n_method,
1808            budgets.transforms.remaining_c14n_output(),
1809            budgets.transforms.xml_base_resolution(),
1810            &mut canonical_signed_info,
1811        )
1812        .map_err(|error| {
1813            if let Some(violation) = map_c14n_resource_policy_violation(
1814                &error,
1815                crate::policy::resource_name::CANONICALIZED_BYTES,
1816                budgets.transforms.c14n_output_limit(),
1817            ) {
1818                SigningError::Policy(violation)
1819            } else {
1820                SigningError::Canonicalization(error)
1821            }
1822        })?;
1823        Ok((signed_info.signature_method, canonical_signed_info))
1824    })
1825}
1826
1827fn parse_signing_document<'a>(
1828    xml: &'a str,
1829    policy: Option<&crate::policy::SigningPolicy>,
1830    budget: &XmlParseWorkBudget,
1831) -> Result<Document<'a>, SigningDigestError> {
1832    let settings = policy
1833        .map(|policy| DocumentParseSettings::from_policy(&policy.xml, &policy.resources))
1834        .unwrap_or_default();
1835    super::mutation::parse_with_options_and_budget(xml, settings, Some(budget)).map_err(|error| {
1836        match error.into_policy_violation(settings) {
1837            Ok(error) => SigningDigestError::Policy(error),
1838            Err(XmlDocumentError::Parse(error)) => SigningDigestError::XmlParse(error),
1839            Err(error) => SigningDigestError::Document(error),
1840        }
1841    })
1842}
1843
1844fn parse_private_key_pem(private_key_pem: &str) -> Result<Vec<u8>, SigningKeyError> {
1845    let (rest, pem) = x509_parser::pem::parse_x509_pem(private_key_pem.as_bytes())
1846        .map_err(|_| SigningKeyError::InvalidKeyPem)?;
1847    if !rest.iter().all(|byte| byte.is_ascii_whitespace()) {
1848        return Err(SigningKeyError::InvalidKeyPem);
1849    }
1850    if pem.label != "PRIVATE KEY" {
1851        return Err(SigningKeyError::InvalidKeyFormat { label: pem.label });
1852    }
1853    Ok(pem.contents)
1854}
1855
1856enum SigningSignatureTarget {
1857    First,
1858    Last,
1859    Index(usize),
1860}
1861
1862fn find_signing_signature_node<'a>(
1863    doc: &'a Document<'a>,
1864    target: SigningSignatureTarget,
1865) -> Result<Node<'a, 'a>, SigningDigestError> {
1866    let mut signatures = doc.descendants().filter(|node| {
1867        node.is_element()
1868            && node.tag_name().name() == "Signature"
1869            && node.tag_name().namespace() == Some(XMLDSIG_NS)
1870    });
1871    match target {
1872        SigningSignatureTarget::First => signatures.next(),
1873        SigningSignatureTarget::Last => signatures.next_back(),
1874        SigningSignatureTarget::Index(index) => signatures.nth(index),
1875    }
1876    .ok_or(SigningDigestError::MissingElement {
1877        element: "Signature",
1878    })
1879}
1880
1881fn signing_signature_index(
1882    doc: &Document<'_>,
1883    start_node_id: Option<&str>,
1884    id_attributes: &[crate::IdAttributeRegistration],
1885    selection: SignatureTemplateSelection,
1886) -> Result<usize, SigningDigestError> {
1887    let selected = if let Some(id) = start_node_id {
1888        let start = signing_start_node(doc, id, id_attributes)?;
1889        let mut signatures = start
1890            .descendants()
1891            .filter(|node| node.has_tag_name((XMLDSIG_NS, "Signature")));
1892        match selection.target() {
1893            SigningSignatureTarget::First => signatures.next(),
1894            SigningSignatureTarget::Last => signatures.next_back(),
1895            SigningSignatureTarget::Index(_) => unreachable!("public selection is not indexed"),
1896        }
1897        .ok_or_else(|| {
1898            SigningDigestError::InvalidStructure(format!(
1899                "selected node subtree has no Signature: {id}"
1900            ))
1901        })?
1902    } else {
1903        find_signing_signature_node(doc, selection.target())?
1904    };
1905    signature_index(doc, selected)
1906}
1907
1908fn signing_start_node<'a>(
1909    doc: &'a Document<'a>,
1910    id: &str,
1911    id_attributes: &[crate::IdAttributeRegistration],
1912) -> Result<Node<'a, 'a>, SigningDigestError> {
1913    UriReferenceResolver::with_id_registrations(doc, id_attributes)
1914        .node_for_id(id)
1915        .ok_or_else(|| {
1916            SigningDigestError::InvalidStructure(format!(
1917                "selected node ID is missing or ambiguous: {id}"
1918            ))
1919        })
1920}
1921
1922fn signature_index(
1923    doc: &Document<'_>,
1924    selected: Node<'_, '_>,
1925) -> Result<usize, SigningDigestError> {
1926    doc.descendants()
1927        .filter(|node| node.has_tag_name((XMLDSIG_NS, "Signature")))
1928        .position(|node| node == selected)
1929        .ok_or(SigningDigestError::MissingElement {
1930            element: "Signature",
1931        })
1932}
1933
1934fn parse_signing_references(
1935    signed_info: Node<'_, '_>,
1936) -> Result<Vec<SigningReference>, SigningDigestError> {
1937    parse_signing_references_with_budget(signed_info, &mut XPathSignatureParseBudget::default())
1938}
1939
1940fn parse_signing_references_with_budget(
1941    signed_info: Node<'_, '_>,
1942    xpath_budget: &mut XPathSignatureParseBudget,
1943) -> Result<Vec<SigningReference>, SigningDigestError> {
1944    verify_ds_element(signed_info, "SignedInfo")?;
1945    let mut children = element_children(signed_info);
1946
1947    let c14n_node = children.next().ok_or(SigningDigestError::MissingElement {
1948        element: "CanonicalizationMethod",
1949    })?;
1950    verify_ds_element(c14n_node, "CanonicalizationMethod")?;
1951    required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
1952
1953    let signature_method_node = children.next().ok_or(SigningDigestError::MissingElement {
1954        element: "SignatureMethod",
1955    })?;
1956    verify_ds_element(signature_method_node, "SignatureMethod")?;
1957    required_algorithm_attr(signature_method_node, "SignatureMethod")?;
1958
1959    let mut references = Vec::new();
1960    for child in children {
1961        verify_ds_element(child, "Reference")?;
1962        if references.len() == MAX_REFERENCES_PER_SIGNATURE {
1963            return Err(SigningDigestError::InvalidStructure(format!(
1964                "SignedInfo contains more than {MAX_REFERENCES_PER_SIGNATURE} Reference elements"
1965            )));
1966        }
1967        references.push(parse_signing_reference(child, xpath_budget)?);
1968    }
1969    if references.is_empty() {
1970        return Err(SigningDigestError::MissingElement {
1971            element: "Reference",
1972        });
1973    }
1974    Ok(references)
1975}
1976
1977fn parse_signing_manifest_references(
1978    signature: Node<'_, '_>,
1979    xpath_budget: &mut XPathSignatureParseBudget,
1980    mut remaining_capacity: usize,
1981    maximum_references: usize,
1982) -> Result<Vec<SigningReference>, SigningDigestError> {
1983    let mut references = Vec::new();
1984    for manifest in signature
1985        .children()
1986        .filter(|node| node.has_tag_name((XMLDSIG_NS, "Object")))
1987        .flat_map(|object| {
1988            object
1989                .children()
1990                .filter(|node| node.has_tag_name((XMLDSIG_NS, "Manifest")))
1991        })
1992    {
1993        let mut manifest_references = 0usize;
1994        for child in element_children(manifest) {
1995            verify_ds_element(child, "Reference")?;
1996            if remaining_capacity == 0 {
1997                return Err(crate::policy::PolicyViolation::ResourceLimit {
1998                    resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
1999                    maximum: maximum_references,
2000                    actual: maximum_references.saturating_add(1),
2001                }
2002                .into());
2003            }
2004            remaining_capacity -= 1;
2005            references.push(parse_signing_reference(child, xpath_budget)?);
2006            manifest_references += 1;
2007        }
2008        if manifest_references == 0 {
2009            return Err(SigningDigestError::MissingElement {
2010                element: "Reference",
2011            });
2012        }
2013    }
2014    Ok(references)
2015}
2016
2017fn parse_signing_reference(
2018    reference_node: Node<'_, '_>,
2019    xpath_budget: &mut XPathSignatureParseBudget,
2020) -> Result<SigningReference, SigningDigestError> {
2021    let uri = reference_node
2022        .attribute("URI")
2023        .ok_or_else(|| {
2024            SigningDigestError::InvalidStructure(
2025                "signing Reference must include URI attribute".into(),
2026            )
2027        })?
2028        .to_string();
2029    let mut children = element_children(reference_node);
2030
2031    let mut transforms = Vec::new();
2032    let mut next = children.next().ok_or(SigningDigestError::MissingElement {
2033        element: "DigestMethod",
2034    })?;
2035    if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) {
2036        transforms = parse_transforms_with_budget(next, xpath_budget)?;
2037        next = children.next().ok_or(SigningDigestError::MissingElement {
2038            element: "DigestMethod",
2039        })?;
2040    }
2041
2042    verify_ds_element(next, "DigestMethod")?;
2043    let digest_uri = required_algorithm_attr(next, "DigestMethod")?;
2044    let digest_method = DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| {
2045        SigningDigestError::UnsupportedAlgorithm {
2046            uri: digest_uri.to_string(),
2047        }
2048    })?;
2049    if !digest_method.signing_allowed() {
2050        return Err(SigningDigestError::SigningAlgorithmDisabled {
2051            uri: digest_method.uri(),
2052        });
2053    }
2054
2055    let digest_value_node = children.next().ok_or(SigningDigestError::MissingElement {
2056        element: "DigestValue",
2057    })?;
2058    verify_ds_element(digest_value_node, "DigestValue")?;
2059
2060    if let Some(unexpected) = children.next() {
2061        return Err(SigningDigestError::InvalidStructure(format!(
2062            "unexpected element <{}> after <DigestValue> in <Reference>",
2063            unexpected.tag_name().name()
2064        )));
2065    }
2066
2067    Ok(SigningReference {
2068        uri,
2069        transforms,
2070        digest_method,
2071        digest_value_range: digest_value_node.range(),
2072        digest_value_node_id: digest_value_node.id(),
2073    })
2074}
2075
2076fn find_required_child<'a>(
2077    parent: Node<'a, 'a>,
2078    child_name: &'static str,
2079) -> Result<Node<'a, 'a>, SigningDigestError> {
2080    parent
2081        .children()
2082        .find(|node| {
2083            node.is_element()
2084                && node.tag_name().name() == child_name
2085                && node.tag_name().namespace() == Some(XMLDSIG_NS)
2086        })
2087        .ok_or(SigningDigestError::MissingElement {
2088            element: child_name,
2089        })
2090}
2091
2092fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
2093    node.children().filter(Node::is_element)
2094}
2095
2096fn verify_ds_element(
2097    node: Node<'_, '_>,
2098    expected_name: &'static str,
2099) -> Result<(), SigningDigestError> {
2100    if !node.is_element() {
2101        return Err(SigningDigestError::InvalidStructure(format!(
2102            "expected element <{expected_name}>, got non-element node"
2103        )));
2104    }
2105    let tag = node.tag_name();
2106    if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
2107        return Err(SigningDigestError::InvalidStructure(format!(
2108            "expected <ds:{expected_name}>, got <{}>",
2109            tag.name()
2110        )));
2111    }
2112    Ok(())
2113}
2114
2115fn required_algorithm_attr<'a>(
2116    node: Node<'a, 'a>,
2117    element_name: &'static str,
2118) -> Result<&'a str, SigningDigestError> {
2119    node.attribute("Algorithm").ok_or_else(|| {
2120        SigningDigestError::InvalidStructure(format!(
2121            "missing Algorithm attribute on <{element_name}>"
2122        ))
2123    })
2124}
2125
2126#[cfg(test)]
2127mod error_conversion_tests {
2128    use super::*;
2129    use crate::policy::PolicyViolation;
2130
2131    struct RejectingSigningKey;
2132
2133    impl SigningKey for RejectingSigningKey {
2134        fn sign(
2135            &self,
2136            _algorithm: SignatureAlgorithm,
2137            _canonical_signed_info: &[u8],
2138        ) -> Result<Vec<u8>, SigningKeyError> {
2139            Err(SigningKeyError::SigningFailed)
2140        }
2141
2142        fn public_key_info(&self) -> Result<SigningPublicKeyInfo, SigningKeyError> {
2143            Err(SigningKeyError::PublicKeyEncodingFailed)
2144        }
2145    }
2146
2147    struct FixedRsaSigningKey;
2148
2149    impl SigningKey for FixedRsaSigningKey {
2150        fn sign(
2151            &self,
2152            _algorithm: SignatureAlgorithm,
2153            _canonical_signed_info: &[u8],
2154        ) -> Result<Vec<u8>, SigningKeyError> {
2155            Ok(vec![0x5a; 256])
2156        }
2157
2158        fn public_key_info(&self) -> Result<SigningPublicKeyInfo, SigningKeyError> {
2159            Ok(SigningPublicKeyInfo::Rsa {
2160                spki_der: Vec::new(),
2161                modulus: vec![0x80; 256],
2162                exponent: vec![1, 0, 1],
2163            })
2164        }
2165    }
2166
2167    #[test]
2168    fn signing_error_promotes_every_policy_failure() {
2169        // All policy refusals use one public pipeline variant regardless of
2170        // which internal signing stage first enforces the immutable snapshot.
2171        let digest = SigningError::from(SigningDigestError::Policy(PolicyViolation::Algorithm {
2172            operation: "signing",
2173            algorithm: "urn:test:digest".into(),
2174        }));
2175        assert!(matches!(digest, SigningError::Policy(_)));
2176
2177        let mutation = SigningError::from(SigningDigestError::XmlMutation(
2178            XmlMutationError::Policy(PolicyViolation::ResourceLimit {
2179                resource: crate::policy::resource_name::XML_DOCUMENT,
2180                maximum: 1,
2181                actual: 2,
2182            }),
2183        ));
2184        assert!(matches!(mutation, SigningError::Policy(_)));
2185    }
2186
2187    #[test]
2188    fn manifest_reparse_consumes_the_signature_wide_xpath_budget() {
2189        // Signing reparses Manifest references after each dependency level.
2190        // Repeated parser/compiler work must consume the original signature
2191        // budget rather than receiving a fresh allowance for every level.
2192        let filter = r#"<xf:XPath xmlns:xf="http://www.w3.org/2002/06/xmldsig-filter2" Filter="intersect">true()</xf:XPath>"#;
2193        let signed_info_transforms = format!(
2194            r#"<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">{}</ds:Transform><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform>"#,
2195            filter.repeat(64)
2196        );
2197        let signed_info_references = (0..62)
2198            .map(|index| {
2199                let extra = if index == 0 {
2200                    r#"<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform>"#
2201                } else {
2202                    ""
2203                };
2204                format!(
2205                    r##"<ds:Reference URI="#payload"><ds:Transforms>{signed_info_transforms}{extra}</ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference>"##
2206                )
2207            })
2208            .collect::<String>();
2209        let manifest_reference = |id: &str| {
2210            format!(
2211                r##"<ds:Reference URI="#{id}"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference>"##
2212            )
2213        };
2214        let xml = format!(
2215            r##"<root><payload Id="payload"/><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"/>{signed_info_references}</ds:SignedInfo><ds:SignatureValue/><ds:Object><ds:Manifest>{}{}</ds:Manifest></ds:Object></ds:Signature></root>"##,
2216            manifest_reference("payload"),
2217            manifest_reference("payload")
2218        );
2219        let policy = crate::policy::SigningPolicy {
2220            manifest_processing: crate::policy::ManifestProcessing::Process,
2221            ..crate::policy::SigningPolicy::default()
2222        };
2223
2224        let mut document = XmlDocument::parse(xml).expect("fixture must parse");
2225        let error = fill_reference_digest_values_in_dependency_order(
2226            &mut document,
2227            TransformOptions::default(),
2228            &policy,
2229            crate::provider::default_provider(),
2230            &mut SigningOperationBudgets::default(),
2231            0,
2232            &[],
2233        )
2234        .expect_err("Manifest reparse must not reset the XPath parse budget");
2235
2236        assert!(
2237            matches!(
2238                &error,
2239                SigningDigestError::Transform(TransformError::Policy(
2240                    crate::policy::PolicyViolation::ResourceLimit {
2241                        resource: "XPath expressions",
2242                        ..
2243                    }
2244                ))
2245            ),
2246            "expected the shared XPath budget error, got: {error:?}"
2247        );
2248    }
2249
2250    #[test]
2251    fn dependency_levels_share_the_xml_parse_work_budget() {
2252        // Nested Manifest dependencies require successive digest generations.
2253        // Analysis mutations and every level's validation/commit parses must
2254        // consume one monotonic budget instead of resetting in recursive work.
2255        let xml = r##"<root><payload Id="payload">nested payload</payload><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="#outer"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:SignedInfo><ds:SignatureValue/><ds:Object><ds:Manifest Id="outer"><ds:Reference URI="#inner"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:Manifest><ds:Manifest Id="inner"><ds:Reference URI="#payload"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:Manifest></ds:Object></ds:Signature></root>"##;
2256        let policy = crate::policy::SigningPolicy {
2257            manifest_processing: crate::policy::ManifestProcessing::Process,
2258            ..crate::policy::SigningPolicy::default()
2259        };
2260        let mut document = XmlDocument::parse(xml).expect("fixture must parse");
2261        let mut budgets = SigningOperationBudgets::from_resources(&policy.resources);
2262
2263        fill_reference_digest_values_in_dependency_order(
2264            &mut document,
2265            TransformOptions::default(),
2266            &policy,
2267            crate::provider::default_provider(),
2268            &mut budgets,
2269            0,
2270            &[],
2271        )
2272        .expect("the default cumulative budget must cover nested dependencies");
2273        let consumed = budgets.transforms.xml_parse_work().consumed();
2274        assert!(
2275            consumed > xml.len().saturating_mul(6),
2276            "analysis and dependency reparses must all be charged"
2277        );
2278
2279        let mut constrained_policy = policy;
2280        constrained_policy.resources.max_xml_parse_work_bytes = consumed - 1;
2281        let mut constrained_document = XmlDocument::parse(xml).expect("fixture must parse");
2282        let mut constrained_budgets =
2283            SigningOperationBudgets::from_resources(&constrained_policy.resources);
2284        let error = fill_reference_digest_values_in_dependency_order(
2285            &mut constrained_document,
2286            TransformOptions::default(),
2287            &constrained_policy,
2288            crate::provider::default_provider(),
2289            &mut constrained_budgets,
2290            0,
2291            &[],
2292        )
2293        .expect_err("one byte below measured work must fail closed");
2294
2295        assert!(matches!(
2296            error,
2297            SigningDigestError::Policy(PolicyViolation::ResourceLimit {
2298                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
2299                maximum,
2300                actual,
2301            }) if maximum == consumed - 1 && actual >= consumed
2302        ));
2303    }
2304
2305    #[test]
2306    fn final_signed_info_parse_consumes_the_signing_xpath_budget() {
2307        // One XPath Reference is parsed while discovering references, while
2308        // analyzing dependencies, and while filling its dependency level. The
2309        // final SignedInfo parse must consume the same operation-wide budget.
2310        let xml = r##"<root><payload Id="payload">content</payload><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="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>true()</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:SignedInfo><ds:SignatureValue/></ds:Signature></root>"##;
2311        let mut policy = crate::policy::SigningPolicy::default();
2312        policy.resources.max_xpath_expressions = 3;
2313
2314        let error = SignContext::new(&RejectingSigningKey)
2315            .policy(policy)
2316            .sign_template(xml)
2317            .expect_err("the final SignedInfo parse must not reset the XPath budget");
2318
2319        assert!(
2320            matches!(
2321                error,
2322                SigningError::Policy(PolicyViolation::ResourceLimit {
2323                    resource: crate::policy::resource_name::XPATH_EXPRESSIONS,
2324                    maximum: 3,
2325                    ..
2326                })
2327            ),
2328            "expected the shared XPath parse budget error, got: {error:?}"
2329        );
2330    }
2331
2332    #[test]
2333    fn signing_initial_parse_consumes_the_operation_xml_parse_budget() {
2334        // The input parse and every later retained-document reparse belong to
2335        // one monotonic operation allowance; helpers must not create a fresh
2336        // budget before digest or mutation work begins.
2337        let xml = r##"<root><payload Id="payload"/><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="#payload"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:SignedInfo><ds:SignatureValue/></ds:Signature></root>"##;
2338        let mut policy = crate::policy::SigningPolicy::default();
2339        policy.resources.max_xml_parse_work_bytes = 0;
2340
2341        let error = SignContext::new(&RejectingSigningKey)
2342            .policy(policy)
2343            .sign_template(xml)
2344            .expect_err("a zero parse-work budget must reject the initial parse");
2345
2346        assert!(matches!(
2347            error,
2348            SigningError::Policy(PolicyViolation::ResourceLimit {
2349                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
2350                maximum: 0,
2351                actual,
2352            }) if actual == xml.len()
2353        ));
2354    }
2355
2356    #[test]
2357    fn signing_string_entry_point_enforces_policy_depth() {
2358        // Operation parsing must use the compiled policy depth rather than the
2359        // process-wide hard ceiling, before template discovery or key use.
2360        let mut policy = crate::policy::SigningPolicy::default();
2361        policy.resources.max_xml_depth = 2;
2362        let xml = "<root><child><leaf/></child></root>";
2363
2364        assert!(matches!(
2365            SignContext::new(&RejectingSigningKey)
2366                .policy(policy)
2367                .sign_template(xml),
2368            Err(SigningError::Policy(PolicyViolation::ResourceLimit {
2369                resource: crate::policy::resource_name::XML_DEPTH,
2370                maximum: 2,
2371                actual: 3,
2372            }))
2373        ));
2374    }
2375
2376    #[test]
2377    fn builder_append_reports_policy_depth() {
2378        // The generated template fits as a standalone tree, but its methods
2379        // cross the same depth policy after the Signature is appended.
2380        let mut policy = crate::policy::SigningPolicy::default();
2381        policy.resources.max_xml_depth = 4;
2382        let builder = SignatureBuilder::new(
2383            crate::c14n::C14nAlgorithm::new(crate::c14n::C14nMode::Exclusive1_0, false),
2384            SignatureAlgorithm::RsaSha256,
2385        )
2386        .add_reference(crate::xmldsig::ReferenceBuilder::new(DigestAlgorithm::Sha256).uri(""));
2387
2388        let result = SignContext::new(&FixedRsaSigningKey)
2389            .policy(policy)
2390            .sign_with_builder("<root/>", &builder);
2391        assert!(
2392            matches!(
2393                result,
2394                Err(SigningError::Policy(PolicyViolation::ResourceLimit {
2395                    resource: crate::policy::resource_name::XML_DEPTH,
2396                    maximum: 4,
2397                    actual: 5,
2398                }))
2399            ),
2400            "unexpected builder depth result: {result:?}"
2401        );
2402    }
2403
2404    #[test]
2405    fn owned_signing_staged_copies_preserve_policy_errors() {
2406        // Both retained-document entry points must expose operation policy
2407        // exhaustion directly and leave the caller's generation untouched.
2408        let mut policy = crate::policy::SigningPolicy::default();
2409        policy.resources.max_xml_parse_work_bytes = 0;
2410        let context = SignContext::new(&RejectingSigningKey).policy(policy);
2411
2412        let mut template_document = XmlDocument::parse("<root/>").expect("fixture must parse");
2413        let template_before = template_document.as_xml().to_owned();
2414        let error = context
2415            .sign_document(&mut template_document)
2416            .expect_err("the staged template copy must exhaust the operation budget");
2417        assert!(matches!(
2418            error,
2419            SigningError::Policy(PolicyViolation::ResourceLimit {
2420                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
2421                maximum: 0,
2422                actual,
2423            }) if actual == template_before.len()
2424        ));
2425        assert_eq!(template_document.as_xml(), template_before);
2426        assert_eq!(template_document.generation(), 0);
2427
2428        let mut builder_document = XmlDocument::parse("<root/>").expect("fixture must parse");
2429        let builder_before = builder_document.as_xml().to_owned();
2430        let builder = SignatureBuilder::new(
2431            crate::c14n::C14nAlgorithm::new(crate::c14n::C14nMode::Exclusive1_0, false),
2432            SignatureAlgorithm::RsaSha256,
2433        );
2434        let error = context
2435            .sign_document_with_builder(&mut builder_document, &builder)
2436            .expect_err("the staged builder copy must exhaust the operation budget");
2437        assert!(matches!(
2438            error,
2439            SigningError::Policy(PolicyViolation::ResourceLimit {
2440                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
2441                maximum: 0,
2442                actual,
2443            }) if actual == builder_before.len()
2444        ));
2445        assert_eq!(builder_document.as_xml(), builder_before);
2446        assert_eq!(builder_document.generation(), 0);
2447    }
2448
2449    #[test]
2450    fn owned_signing_commits_the_validated_stage_without_reparsing() {
2451        // Atomic commit must adopt the already validated staged cell. Charging
2452        // another complete backend parse makes valid maximum-size inputs exceed
2453        // the operation ceiling only because they use the owned entry point.
2454        let xml = r##"<root><payload Id="payload">content</payload><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="#payload"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:SignedInfo><ds:SignatureValue/></ds:Signature></root>"##;
2455        let policy = crate::policy::SigningPolicy::default();
2456        let context = SignContext::new(&FixedRsaSigningKey).policy(policy.clone());
2457        let source = XmlDocument::parse(xml).expect("fixture must parse");
2458        let mut measured = SigningOperationBudgets::from_resources(&policy.resources);
2459        let mut staged = source
2460            .staged_copy_with_budget(
2461                DocumentParseSettings::from_policy(&policy.xml, &policy.resources),
2462                measured.transforms.xml_parse_work(),
2463            )
2464            .expect("staging must parse");
2465        context
2466            .sign_document_in_place(&mut staged, &mut measured)
2467            .expect("staged signing must succeed");
2468        let exact_stage_work = measured.transforms.xml_parse_work().consumed();
2469
2470        let mut constrained_policy = policy;
2471        constrained_policy.resources.max_xml_parse_work_bytes = exact_stage_work;
2472        let mut document = XmlDocument::parse(xml).expect("fixture must parse");
2473        SignContext::new(&FixedRsaSigningKey)
2474            .policy(constrained_policy)
2475            .sign_document(&mut document)
2476            .expect("commit must not parse the validated stage again");
2477
2478        assert_eq!(document.generation(), 1);
2479        assert!(!document.as_xml().contains("<ds:DigestValue/>"));
2480        assert!(!document.as_xml().contains("<ds:SignatureValue/>"));
2481    }
2482
2483    #[test]
2484    fn builder_signing_fits_the_document_to_parse_work_ratio() {
2485        // A builder operation at the configured document ceiling must fit the
2486        // implementation's sixteen-pass hard allowance. Generated base64 text
2487        // and the appended generated template need one committed candidate
2488        // parse each, not an untrusted-fragment validation parse plus commit.
2489        let padding = "x".repeat(64 * 1024);
2490        let xml = format!("<root><payload Id=\"payload\"/><padding>{padding}</padding></root>");
2491        let builder = SignatureBuilder::new(
2492            crate::c14n::C14nAlgorithm::new(crate::c14n::C14nMode::Exclusive1_0, false),
2493            SignatureAlgorithm::RsaSha256,
2494        )
2495        .add_reference(
2496            crate::xmldsig::ReferenceBuilder::new(DigestAlgorithm::Sha256).uri("#payload"),
2497        );
2498        let maximum_document_bytes = xml.len() + 4 * 1024;
2499        let mut policy = crate::policy::SigningPolicy::default();
2500        policy.resources.max_xml_document_bytes = maximum_document_bytes;
2501        policy.resources.max_xml_parse_work_bytes = maximum_document_bytes * 16;
2502
2503        let mut measurement_policy = policy.clone();
2504        measurement_policy.resources.max_xml_parse_work_bytes =
2505            crate::hard_limits::XML_PARSE_WORK_BYTE_CEILING;
2506        let measurement_context =
2507            SignContext::new(&FixedRsaSigningKey).policy(measurement_policy.clone());
2508        let mut measurement_budgets =
2509            SigningOperationBudgets::from_resources(&measurement_policy.resources);
2510        let mut measurement_document = XmlDocument::parse_with_settings_and_budget(
2511            xml.clone(),
2512            DocumentParseSettings::from_policy(
2513                &measurement_policy.xml,
2514                &measurement_policy.resources,
2515            ),
2516            measurement_budgets.transforms.xml_parse_work(),
2517        )
2518        .expect("measurement input must parse");
2519        measurement_context
2520            .sign_document_with_builder_in_place(
2521                &mut measurement_document,
2522                &builder,
2523                &mut measurement_budgets,
2524            )
2525            .expect("measurement signing must succeed");
2526        let consumed = measurement_budgets.transforms.xml_parse_work().consumed();
2527        assert!(
2528            consumed <= maximum_document_bytes * 16,
2529            "builder signing consumed {consumed} bytes for a {maximum_document_bytes}-byte ceiling"
2530        );
2531
2532        let signed = SignContext::new(&FixedRsaSigningKey)
2533            .policy(policy.clone())
2534            .sign_with_builder(&xml, &builder)
2535            .expect("string builder signing must fit the advertised parse-work ratio");
2536        assert!(signed.contains("DigestValue>"));
2537        assert!(signed.contains("SignatureValue>"));
2538
2539        let mut owned = XmlDocument::parse(&xml).expect("fixture must parse");
2540        SignContext::new(&FixedRsaSigningKey)
2541            .policy(policy)
2542            .sign_document_with_builder(&mut owned, &builder)
2543            .expect("owned builder signing must fit the advertised parse-work ratio");
2544        assert!(owned.as_xml().contains("DigestValue>"));
2545        assert!(owned.as_xml().contains("SignatureValue>"));
2546    }
2547
2548    #[test]
2549    fn dtd_capable_staged_copy_charges_both_parser_passes() {
2550        // DTD-capable documents run a provenance parse before the retained
2551        // document parse. Both passes belong to the signing operation budget.
2552        let xml = "<root/>";
2553        let mut parsing_policy = crate::policy::SigningPolicy::default();
2554        parsing_policy.xml.allow_internal_dtd = true;
2555        let document = XmlDocument::parse_with_policy(xml, &parsing_policy)
2556            .expect("the fixture must retain DTD-capable parse settings");
2557
2558        parsing_policy.resources.max_xml_parse_work_bytes = xml.len();
2559        let budget = XmlParseWorkBudget::from_resources(&parsing_policy.resources);
2560        assert!(matches!(
2561            document.staged_copy_with_budget(DocumentParseSettings::default(), &budget),
2562            Err(XmlDocumentError::Policy(PolicyViolation::ResourceLimit {
2563                resource: crate::policy::resource_name::XML_PARSE_WORK_BYTES,
2564                maximum,
2565                actual,
2566            })) if maximum == xml.len() && actual == xml.len() * 2
2567        ));
2568    }
2569
2570    #[test]
2571    fn owned_signing_mappers_preserve_document_size_policy_errors() {
2572        // Template and digest mutations share one resource-error contract even
2573        // though their surrounding signing error types differ.
2574        let maximum = 8;
2575        let actual = 9;
2576        assert!(matches!(
2577            map_owned_document_mutation_error(XmlDocumentError::DocumentTooLarge {
2578                maximum,
2579                actual,
2580            }),
2581            SigningError::Policy(PolicyViolation::ResourceLimit {
2582                resource: crate::policy::resource_name::XML_DOCUMENT,
2583                maximum: 8,
2584                actual: 9,
2585            })
2586        ));
2587        assert!(matches!(
2588            map_owned_document_digest_mutation_error(XmlDocumentError::DocumentTooLarge {
2589                maximum,
2590                actual,
2591            }),
2592            SigningDigestError::Policy(PolicyViolation::ResourceLimit {
2593                resource: crate::policy::resource_name::XML_DOCUMENT,
2594                maximum: 8,
2595                actual: 9,
2596            })
2597        ));
2598    }
2599
2600    #[test]
2601    fn signing_rejects_xpath_control_dependencies_on_digest_values() {
2602        // The second Reference excludes DigestValue nodes from its output but
2603        // reads the first DigestValue to decide whether payload remains. Filling
2604        // both references in one level would therefore invalidate the result.
2605        let xml = r##"<root><payload Id="payload">content</payload><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="#payload"><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference><ds:Reference URI=""><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>not(ancestor-or-self::ds:DigestValue) and (not(self::payload) or string(//ds:Reference[1]/ds:DigestValue) = '')</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:SignedInfo><ds:SignatureValue/></ds:Signature></root>"##;
2606
2607        let mut document = XmlDocument::parse(xml).expect("fixture must parse");
2608        let error = fill_reference_digest_values_in_dependency_order(
2609            &mut document,
2610            TransformOptions::default(),
2611            &crate::policy::SigningPolicy::default(),
2612            crate::provider::default_provider(),
2613            &mut SigningOperationBudgets::default(),
2614            0,
2615            &[],
2616        )
2617        .expect_err("mutable XPath control dependencies must fail closed");
2618
2619        assert!(
2620            matches!(
2621                &error,
2622                SigningDigestError::InvalidStructure(message)
2623                    if message.contains("dependency cycle")
2624            ),
2625            "expected a dependency-cycle rejection, got: {error:?}"
2626        );
2627    }
2628
2629    #[test]
2630    fn signing_allows_payload_local_xpath_value_predicates() {
2631        // Attribute comparisons read payload metadata, not mutable values in a
2632        // disjoint Signature subtree. They must not manufacture a self-cycle.
2633        let xml = r##"<root><payload Id="payload" kind="include">content</payload><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="#payload"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>@kind = 'include'</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:SignedInfo><ds:SignatureValue/></ds:Signature></root>"##;
2634
2635        let mut document = XmlDocument::parse(xml).expect("fixture must parse");
2636        fill_reference_digest_values_in_dependency_order(
2637            &mut document,
2638            TransformOptions::default(),
2639            &crate::policy::SigningPolicy::default(),
2640            crate::provider::default_provider(),
2641            &mut SigningOperationBudgets::default(),
2642            0,
2643            &[],
2644        )
2645        .expect("payload-local XPath predicates must not depend on Signature values");
2646
2647        assert_ne!(document.as_xml(), xml);
2648    }
2649
2650    #[test]
2651    fn signing_rejects_references_that_retain_signature_value() {
2652        // SignatureValue is filled after every Reference digest. A Reference
2653        // retaining that node is therefore an unavoidable signing cycle even
2654        // when all DigestValue nodes are excluded from the resulting node-set.
2655        let xml = r##"<root><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=""><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116"><ds:XPath>not(ancestor-or-self::ds:DigestValue)</ds:XPath></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:SignedInfo><ds:SignatureValue/></ds:Signature></root>"##;
2656
2657        let mut document = XmlDocument::parse(xml).expect("fixture must parse");
2658        let error = fill_reference_digest_values_in_dependency_order(
2659            &mut document,
2660            TransformOptions::default(),
2661            &crate::policy::SigningPolicy::default(),
2662            crate::provider::default_provider(),
2663            &mut SigningOperationBudgets::default(),
2664            0,
2665            &[],
2666        )
2667        .expect_err("a mutable SignatureValue dependency must fail before signing");
2668
2669        assert!(
2670            matches!(
2671                &error,
2672                SigningDigestError::InvalidStructure(message)
2673                    if message.contains("SignatureValue") && message.contains("cycle")
2674            ),
2675            "expected a SignatureValue dependency-cycle rejection, got: {error:?}"
2676        );
2677    }
2678}