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 getrandom::SysRng;
10use p256::ecdsa::{Signature as P256Signature, SigningKey as P256SigningKey};
11use p256::pkcs8::DecodePrivateKey;
12use p384::ecdsa::{Signature as P384Signature, SigningKey as P384SigningKey};
13use roxmltree::{Document, Node};
14use rsa::RsaPrivateKey;
15use rsa::pkcs1v15::Signature as RsaPkcs1v15Signature;
16use rsa::pkcs1v15::SigningKey as RsaPkcs1v15SigningKey;
17use rsa::signature::{RandomizedSigner, SignatureEncoding, Signer};
18use sha2::{Sha256, Sha384, Sha512};
19use std::collections::HashSet;
20
21use crate::c14n::canonicalize;
22
23use super::builder::{SignatureBuilder, SignatureBuilderError};
24use super::digest::{DigestAlgorithm, compute_digest};
25use super::mutation::{
26    XmlMutationError, append_signature_to_root, fill_signature_value,
27    fill_signed_info_digest_values,
28};
29use super::parse::{SignatureAlgorithm, XMLDSIG_NS, parse_signed_info};
30use super::transforms::{Transform, execute_transforms, parse_transforms};
31use super::types::TransformError;
32use super::uri::UriReferenceResolver;
33
34/// Result for one computed signing-template reference digest.
35#[derive(Debug, Clone, PartialEq, Eq)]
36#[must_use = "use the computed digest value to fill the corresponding <DigestValue>"]
37pub struct ComputedReferenceDigest {
38    /// Zero-based reference index in `<SignedInfo>` document order.
39    pub index: usize,
40    /// Reference URI used for same-document dereference.
41    pub uri: String,
42    /// Digest algorithm declared by `<DigestMethod>`.
43    pub digest_method: DigestAlgorithm,
44    /// Base64-encoded digest value ready for `<DigestValue>`.
45    pub digest_value: String,
46}
47
48/// Errors returned by the XMLDSig signing digest pass.
49#[derive(Debug, thiserror::Error)]
50pub enum SigningDigestError {
51    /// The input XML document is not well-formed.
52    #[error("XML parse error: {0}")]
53    XmlParse(#[from] roxmltree::Error),
54
55    /// Required XMLDSig element is missing.
56    #[error("missing required element: <{element}>")]
57    MissingElement {
58        /// Required element name.
59        element: &'static str,
60    },
61
62    /// XMLDSig template structure is invalid.
63    #[error("invalid signing template: {0}")]
64    InvalidStructure(String),
65
66    /// Digest algorithm URI is not supported.
67    #[error("unsupported digest algorithm: {uri}")]
68    UnsupportedAlgorithm {
69        /// Unrecognized algorithm URI.
70        uri: String,
71    },
72
73    /// Digest algorithm is supported for verification but disabled for signing.
74    #[error("digest algorithm is disabled for signing: {uri}")]
75    SigningAlgorithmDisabled {
76        /// Algorithm URI rejected for new signatures.
77        uri: &'static str,
78    },
79
80    /// URI dereference or transform execution failed.
81    #[error("reference processing error: {0}")]
82    Transform(#[from] TransformError),
83
84    /// Writing computed digest values back into XML failed.
85    #[error("XML mutation error: {0}")]
86    XmlMutation(#[from] XmlMutationError),
87}
88
89/// Errors returned by the full XMLDSig signing pipeline.
90#[derive(Debug, thiserror::Error)]
91pub enum SigningError {
92    /// Reference digest computation failed.
93    #[error("signing digest pass failed: {0}")]
94    Digest(#[from] SigningDigestError),
95
96    /// Parsing the digest-filled `<SignedInfo>` failed.
97    #[error("failed to parse SignedInfo after digest fill: {0}")]
98    ParseSignedInfo(#[from] super::parse::ParseError),
99
100    /// SignedInfo canonicalization failed.
101    #[error("SignedInfo canonicalization failed: {0}")]
102    Canonicalization(#[from] crate::c14n::C14nError),
103
104    /// Signing key preparation or signing failed.
105    #[error("signing key error: {0}")]
106    Key(#[from] SigningKeyError),
107
108    /// Writing `<SignatureValue>` failed.
109    #[error("XML mutation error: {0}")]
110    XmlMutation(#[from] XmlMutationError),
111
112    /// Signature template generation failed.
113    #[error("signature template error: {0}")]
114    Template(#[from] SignatureBuilderError),
115}
116
117/// Errors while parsing or using XMLDSig signing keys.
118#[derive(Debug, thiserror::Error)]
119#[non_exhaustive]
120pub enum SigningKeyError {
121    /// PEM input could not be parsed.
122    #[error("invalid PEM private key")]
123    InvalidKeyPem,
124
125    /// PEM block was not an unencrypted PKCS#8 private key.
126    #[error("invalid key format: expected PRIVATE KEY PEM, got {label}")]
127    InvalidKeyFormat {
128        /// Actual PEM label.
129        label: String,
130    },
131
132    /// DER bytes could not be decoded for the requested key type.
133    #[error("invalid PKCS#8 private key DER")]
134    InvalidKeyDer,
135
136    /// The signing key cannot produce the requested XMLDSig algorithm.
137    #[error("signing key does not support algorithm: {uri}")]
138    UnsupportedAlgorithm {
139        /// XMLDSig signature algorithm URI.
140        uri: String,
141    },
142
143    /// The private-key signing operation failed.
144    #[error("private-key signing operation failed")]
145    SigningFailed,
146}
147
148/// Private key abstraction used by [`SignContext`].
149pub trait SigningKey {
150    /// Sign canonicalized `<SignedInfo>` bytes for the declared XMLDSig method.
151    fn sign(
152        &self,
153        algorithm: SignatureAlgorithm,
154        canonical_signed_info: &[u8],
155    ) -> Result<Vec<u8>, SigningKeyError>;
156}
157
158/// RSA PKCS#1 v1.5 private key for XMLDSig signing.
159pub struct RsaSigningKey {
160    key: RsaPrivateKey,
161}
162
163impl RsaSigningKey {
164    /// Parse an unencrypted PKCS#8 `PRIVATE KEY` PEM block.
165    pub fn from_pkcs8_pem(private_key_pem: &str) -> Result<Self, SigningKeyError> {
166        let private_key_der = parse_private_key_pem(private_key_pem)?;
167        Self::from_pkcs8_der(&private_key_der)
168    }
169
170    /// Parse unencrypted PKCS#8 private key DER.
171    pub fn from_pkcs8_der(private_key_der: &[u8]) -> Result<Self, SigningKeyError> {
172        let key = RsaPrivateKey::from_pkcs8_der(private_key_der)
173            .map_err(|_| SigningKeyError::InvalidKeyDer)?;
174        Ok(Self { key })
175    }
176}
177
178impl SigningKey for RsaSigningKey {
179    fn sign(
180        &self,
181        algorithm: SignatureAlgorithm,
182        canonical_signed_info: &[u8],
183    ) -> Result<Vec<u8>, SigningKeyError> {
184        match algorithm {
185            SignatureAlgorithm::RsaSha256 => sign_rsa_pkcs1v15_with_rng(
186                RsaPkcs1v15SigningKey::<Sha256>::new(self.key.clone()),
187                canonical_signed_info,
188            ),
189            SignatureAlgorithm::RsaSha384 => sign_rsa_pkcs1v15_with_rng(
190                RsaPkcs1v15SigningKey::<Sha384>::new(self.key.clone()),
191                canonical_signed_info,
192            ),
193            SignatureAlgorithm::RsaSha512 => sign_rsa_pkcs1v15_with_rng(
194                RsaPkcs1v15SigningKey::<Sha512>::new(self.key.clone()),
195                canonical_signed_info,
196            ),
197            _ => Err(SigningKeyError::UnsupportedAlgorithm {
198                uri: algorithm.uri().to_string(),
199            }),
200        }
201    }
202}
203
204fn sign_rsa_pkcs1v15_with_rng(
205    key: impl RandomizedSigner<RsaPkcs1v15Signature>,
206    canonical_signed_info: &[u8],
207) -> Result<Vec<u8>, SigningKeyError> {
208    let signature = key
209        .try_sign_with_rng(&mut SysRng, canonical_signed_info)
210        .map_err(|_| SigningKeyError::SigningFailed)?;
211    Ok(signature.to_vec())
212}
213
214/// ECDSA P-256 private key for XMLDSig signing.
215pub struct EcdsaP256SigningKey {
216    key: P256SigningKey,
217}
218
219impl EcdsaP256SigningKey {
220    /// Parse an unencrypted PKCS#8 `PRIVATE KEY` PEM block.
221    pub fn from_pkcs8_pem(private_key_pem: &str) -> Result<Self, SigningKeyError> {
222        let private_key_der = parse_private_key_pem(private_key_pem)?;
223        Self::from_pkcs8_der(&private_key_der)
224    }
225
226    /// Parse unencrypted PKCS#8 private key DER.
227    pub fn from_pkcs8_der(private_key_der: &[u8]) -> Result<Self, SigningKeyError> {
228        let key = P256SigningKey::from_pkcs8_der(private_key_der)
229            .map_err(|_| SigningKeyError::InvalidKeyDer)?;
230        Ok(Self { key })
231    }
232}
233
234impl SigningKey for EcdsaP256SigningKey {
235    fn sign(
236        &self,
237        algorithm: SignatureAlgorithm,
238        canonical_signed_info: &[u8],
239    ) -> Result<Vec<u8>, SigningKeyError> {
240        if algorithm != SignatureAlgorithm::EcdsaP256Sha256 {
241            return Err(SigningKeyError::UnsupportedAlgorithm {
242                uri: algorithm.uri().to_string(),
243            });
244        }
245        let signature: P256Signature = self
246            .key
247            .try_sign(canonical_signed_info)
248            .map_err(|_| SigningKeyError::SigningFailed)?;
249        Ok(signature.to_bytes().to_vec())
250    }
251}
252
253/// ECDSA P-384 private key for XMLDSig signing.
254pub struct EcdsaP384SigningKey {
255    key: P384SigningKey,
256}
257
258impl EcdsaP384SigningKey {
259    /// Parse an unencrypted PKCS#8 `PRIVATE KEY` PEM block.
260    pub fn from_pkcs8_pem(private_key_pem: &str) -> Result<Self, SigningKeyError> {
261        let private_key_der = parse_private_key_pem(private_key_pem)?;
262        Self::from_pkcs8_der(&private_key_der)
263    }
264
265    /// Parse unencrypted PKCS#8 private key DER.
266    pub fn from_pkcs8_der(private_key_der: &[u8]) -> Result<Self, SigningKeyError> {
267        let key = P384SigningKey::from_pkcs8_der(private_key_der)
268            .map_err(|_| SigningKeyError::InvalidKeyDer)?;
269        Ok(Self { key })
270    }
271}
272
273impl SigningKey for EcdsaP384SigningKey {
274    fn sign(
275        &self,
276        algorithm: SignatureAlgorithm,
277        canonical_signed_info: &[u8],
278    ) -> Result<Vec<u8>, SigningKeyError> {
279        if algorithm != SignatureAlgorithm::EcdsaP384Sha384 {
280            return Err(SigningKeyError::UnsupportedAlgorithm {
281                uri: algorithm.uri().to_string(),
282            });
283        }
284        let signature: P384Signature = self
285            .key
286            .try_sign(canonical_signed_info)
287            .map_err(|_| SigningKeyError::SigningFailed)?;
288        Ok(signature.to_bytes().to_vec())
289    }
290}
291
292/// XMLDSig signing context.
293pub struct SignContext<'a> {
294    signing_key: &'a dyn SigningKey,
295}
296
297impl<'a> SignContext<'a> {
298    /// Create a signing context using the supplied private key.
299    pub fn new(signing_key: &'a dyn SigningKey) -> Self {
300        Self { signing_key }
301    }
302
303    /// Sign XML that already contains a `<Signature>` template.
304    ///
305    /// The template must include empty `<DigestValue>` and `<SignatureValue>`
306    /// targets. The pipeline fills reference digests, reparses the result,
307    /// canonicalizes `<SignedInfo>`, signs those canonical bytes, and fills the
308    /// base64 `<SignatureValue>`.
309    pub fn sign_template(&self, xml: &str) -> Result<String, SigningError> {
310        let with_digests = fill_reference_digest_values(xml)?;
311        let (algorithm, canonical_signed_info) = canonicalize_signed_info(&with_digests)?;
312        let signature_value = self.signing_key.sign(algorithm, &canonical_signed_info)?;
313        let signature_b64 = base64::engine::general_purpose::STANDARD.encode(signature_value);
314        Ok(fill_signature_value(&with_digests, &signature_b64)?)
315    }
316
317    /// Build a signature template, append it to the source root, then sign it.
318    pub fn sign_with_builder(
319        &self,
320        xml: &str,
321        builder: &SignatureBuilder,
322    ) -> Result<String, SigningError> {
323        let template = builder.build_template()?;
324        let templated = append_signature_to_root(xml, &template)?;
325        self.sign_template(&templated)
326    }
327}
328
329#[derive(Debug)]
330struct SigningReference {
331    uri: String,
332    transforms: Vec<Transform>,
333    digest_method: DigestAlgorithm,
334}
335
336/// Compute base64 digest values for every `<Reference>` in the signing template.
337///
338/// References are processed in `<SignedInfo>` document order. The input must
339/// contain exactly one XMLDSig `<Signature>` element so an enveloped-signature
340/// transform cannot accidentally target the wrong signature subtree.
341pub fn compute_reference_digest_values(
342    xml: &str,
343) -> Result<Vec<ComputedReferenceDigest>, SigningDigestError> {
344    let doc = Document::parse(xml)?;
345    let signature = find_single_signature_node(&doc)?;
346    let signed_info = find_required_child(signature, "SignedInfo")?;
347    let references = parse_signing_references(signed_info)?;
348    let resolver = UriReferenceResolver::new(&doc);
349
350    references
351        .into_iter()
352        .enumerate()
353        .map(|(index, reference)| {
354            let initial_data = resolver.dereference(&reference.uri)?;
355            let pre_digest = execute_transforms(signature, initial_data, &reference.transforms)?;
356            let digest = compute_digest(reference.digest_method, &pre_digest);
357            let digest_value = base64::engine::general_purpose::STANDARD.encode(digest);
358            Ok(ComputedReferenceDigest {
359                index,
360                uri: reference.uri,
361                digest_method: reference.digest_method,
362                digest_value,
363            })
364        })
365        .collect()
366}
367
368/// Compute and fill all signing-template `<DigestValue>` elements.
369///
370/// This is the signing counterpart to verification reference processing: it
371/// dereferences each `<Reference>`, applies transforms, computes the digest,
372/// and writes the base64 digest into the matching `<DigestValue>` in document
373/// order.
374pub fn fill_reference_digest_values(xml: &str) -> Result<String, SigningDigestError> {
375    let digest_values = compute_reference_digest_values(xml)?
376        .into_iter()
377        .map(|digest| digest.digest_value);
378    Ok(fill_signed_info_digest_values(xml, digest_values)?)
379}
380
381fn canonicalize_signed_info(xml: &str) -> Result<(SignatureAlgorithm, Vec<u8>), SigningError> {
382    let doc = Document::parse(xml).map_err(SigningDigestError::XmlParse)?;
383    let signature = find_single_signature_node(&doc).map_err(SigningError::Digest)?;
384    let signed_info_node =
385        find_required_child(signature, "SignedInfo").map_err(SigningError::Digest)?;
386    let signed_info = parse_signed_info(signed_info_node)?;
387    let signed_info_subtree: HashSet<_> = signed_info_node
388        .descendants()
389        .map(|node: Node<'_, '_>| node.id())
390        .collect();
391    let mut canonical_signed_info = Vec::new();
392    canonicalize(
393        &doc,
394        Some(&|node| signed_info_subtree.contains(&node.id())),
395        &signed_info.c14n_method,
396        &mut canonical_signed_info,
397    )?;
398    Ok((signed_info.signature_method, canonical_signed_info))
399}
400
401fn parse_private_key_pem(private_key_pem: &str) -> Result<Vec<u8>, SigningKeyError> {
402    let (rest, pem) = x509_parser::pem::parse_x509_pem(private_key_pem.as_bytes())
403        .map_err(|_| SigningKeyError::InvalidKeyPem)?;
404    if !rest.iter().all(|byte| byte.is_ascii_whitespace()) {
405        return Err(SigningKeyError::InvalidKeyPem);
406    }
407    if pem.label != "PRIVATE KEY" {
408        return Err(SigningKeyError::InvalidKeyFormat { label: pem.label });
409    }
410    Ok(pem.contents)
411}
412
413fn find_single_signature_node<'a>(
414    doc: &'a Document<'a>,
415) -> Result<Node<'a, 'a>, SigningDigestError> {
416    let mut signatures = doc.descendants().filter(|node| {
417        node.is_element()
418            && node.tag_name().name() == "Signature"
419            && node.tag_name().namespace() == Some(XMLDSIG_NS)
420    });
421    let signature = signatures
422        .next()
423        .ok_or(SigningDigestError::MissingElement {
424            element: "Signature",
425        })?;
426    if signatures.next().is_some() {
427        return Err(SigningDigestError::InvalidStructure(
428            "expected exactly one <ds:Signature> element".into(),
429        ));
430    }
431    Ok(signature)
432}
433
434fn parse_signing_references(
435    signed_info: Node<'_, '_>,
436) -> Result<Vec<SigningReference>, SigningDigestError> {
437    verify_ds_element(signed_info, "SignedInfo")?;
438    let mut children = element_children(signed_info);
439
440    let c14n_node = children.next().ok_or(SigningDigestError::MissingElement {
441        element: "CanonicalizationMethod",
442    })?;
443    verify_ds_element(c14n_node, "CanonicalizationMethod")?;
444    required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
445
446    let signature_method_node = children.next().ok_or(SigningDigestError::MissingElement {
447        element: "SignatureMethod",
448    })?;
449    verify_ds_element(signature_method_node, "SignatureMethod")?;
450    required_algorithm_attr(signature_method_node, "SignatureMethod")?;
451
452    let mut references = Vec::new();
453    for child in children {
454        verify_ds_element(child, "Reference")?;
455        references.push(parse_signing_reference(child)?);
456    }
457    if references.is_empty() {
458        return Err(SigningDigestError::MissingElement {
459            element: "Reference",
460        });
461    }
462    Ok(references)
463}
464
465fn parse_signing_reference(
466    reference_node: Node<'_, '_>,
467) -> Result<SigningReference, SigningDigestError> {
468    let uri = reference_node
469        .attribute("URI")
470        .ok_or_else(|| {
471            SigningDigestError::InvalidStructure(
472                "signing Reference must include URI attribute".into(),
473            )
474        })?
475        .to_string();
476    let mut children = element_children(reference_node);
477
478    let mut transforms = Vec::new();
479    let mut next = children.next().ok_or(SigningDigestError::MissingElement {
480        element: "DigestMethod",
481    })?;
482    if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) {
483        transforms = parse_transforms(next)?;
484        next = children.next().ok_or(SigningDigestError::MissingElement {
485            element: "DigestMethod",
486        })?;
487    }
488
489    verify_ds_element(next, "DigestMethod")?;
490    let digest_uri = required_algorithm_attr(next, "DigestMethod")?;
491    let digest_method = DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| {
492        SigningDigestError::UnsupportedAlgorithm {
493            uri: digest_uri.to_string(),
494        }
495    })?;
496    if !digest_method.signing_allowed() {
497        return Err(SigningDigestError::SigningAlgorithmDisabled {
498            uri: digest_method.uri(),
499        });
500    }
501
502    let digest_value_node = children.next().ok_or(SigningDigestError::MissingElement {
503        element: "DigestValue",
504    })?;
505    verify_ds_element(digest_value_node, "DigestValue")?;
506
507    if let Some(unexpected) = children.next() {
508        return Err(SigningDigestError::InvalidStructure(format!(
509            "unexpected element <{}> after <DigestValue> in <Reference>",
510            unexpected.tag_name().name()
511        )));
512    }
513
514    Ok(SigningReference {
515        uri,
516        transforms,
517        digest_method,
518    })
519}
520
521fn find_required_child<'a>(
522    parent: Node<'a, 'a>,
523    child_name: &'static str,
524) -> Result<Node<'a, 'a>, SigningDigestError> {
525    parent
526        .children()
527        .find(|node| {
528            node.is_element()
529                && node.tag_name().name() == child_name
530                && node.tag_name().namespace() == Some(XMLDSIG_NS)
531        })
532        .ok_or(SigningDigestError::MissingElement {
533            element: child_name,
534        })
535}
536
537fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
538    node.children().filter(Node::is_element)
539}
540
541fn verify_ds_element(
542    node: Node<'_, '_>,
543    expected_name: &'static str,
544) -> Result<(), SigningDigestError> {
545    if !node.is_element() {
546        return Err(SigningDigestError::InvalidStructure(format!(
547            "expected element <{expected_name}>, got non-element node"
548        )));
549    }
550    let tag = node.tag_name();
551    if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
552        return Err(SigningDigestError::InvalidStructure(format!(
553            "expected <ds:{expected_name}>, got <{}>",
554            tag.name()
555        )));
556    }
557    Ok(())
558}
559
560fn required_algorithm_attr<'a>(
561    node: Node<'a, 'a>,
562    element_name: &'static str,
563) -> Result<&'a str, SigningDigestError> {
564    node.attribute("Algorithm").ok_or_else(|| {
565        SigningDigestError::InvalidStructure(format!(
566            "missing Algorithm attribute on <{element_name}>"
567        ))
568    })
569}