Skip to main content

xdoc/signature/
xmldsig.rs

1use crate::core::{
2    Attribute, Document, ErrorKind, NamespaceDeclaration, NodeId, NodeKind, QName, XmlError,
3    XmlResult,
4};
5use crate::query::{NamespaceContext, Query, QueryValue};
6
7use super::{
8    canonicalization::canonicalize_node_excluding, canonicalize_node, decode_standard_base64,
9    digest_bytes, encode_standard_base64, find_element_by_id, CanonicalizationAlgorithm,
10    CanonicalizationConfig, DigestAlgorithm, IdAttributePolicy, SignatureAlgorithm,
11    SigningProvider, XMLDSIG_ENVELOPED_SIGNATURE_URI,
12};
13
14pub const XMLDSIG_NAMESPACE_URI: &str = "http://www.w3.org/2000/09/xmldsig#";
15pub(crate) const DEFAULT_KEY_INFO_ID: &str = "xdoc-key-info-1";
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct XmlDsigConfig {
19    id_policy: IdAttributePolicy,
20    canonicalization: CanonicalizationAlgorithm,
21    digest_algorithm: DigestAlgorithm,
22    signature_algorithm: SignatureAlgorithm,
23    document_id: String,
24    signature_id: String,
25    key_info_id: Option<String>,
26    references: Vec<XmlDsigReferenceConfig>,
27    signature_placement: SignaturePlacement,
28}
29
30impl XmlDsigConfig {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    pub fn with_document_id(mut self, id: impl Into<String>) -> Self {
36        self.document_id = id.into();
37        self
38    }
39
40    pub fn with_signature_id(mut self, id: impl Into<String>) -> Self {
41        self.signature_id = id.into();
42        self
43    }
44
45    pub fn with_canonicalization(mut self, algorithm: CanonicalizationAlgorithm) -> Self {
46        self.canonicalization = algorithm;
47        self
48    }
49
50    pub fn with_key_info_id(mut self, id: impl Into<String>) -> Self {
51        self.key_info_id = Some(id.into());
52        self
53    }
54
55    pub fn with_references(mut self, references: Vec<XmlDsigReferenceConfig>) -> Self {
56        self.references = references;
57        self
58    }
59
60    pub fn with_signature_placement(mut self, placement: SignaturePlacement) -> Self {
61        self.signature_placement = placement;
62        self
63    }
64
65    pub fn id_policy(&self) -> &IdAttributePolicy {
66        &self.id_policy
67    }
68
69    pub fn canonicalization(&self) -> CanonicalizationAlgorithm {
70        self.canonicalization
71    }
72
73    pub fn digest_algorithm(&self) -> DigestAlgorithm {
74        self.digest_algorithm
75    }
76
77    pub fn signature_algorithm(&self) -> SignatureAlgorithm {
78        self.signature_algorithm
79    }
80
81    pub fn document_id(&self) -> &str {
82        &self.document_id
83    }
84
85    pub fn signature_id(&self) -> &str {
86        &self.signature_id
87    }
88
89    pub fn key_info_id(&self) -> Option<&str> {
90        self.key_info_id.as_deref()
91    }
92
93    pub fn references(&self) -> &[XmlDsigReferenceConfig] {
94        &self.references
95    }
96
97    pub fn signature_placement(&self) -> &SignaturePlacement {
98        &self.signature_placement
99    }
100}
101
102impl Default for XmlDsigConfig {
103    fn default() -> Self {
104        Self {
105            id_policy: IdAttributePolicy::Standard,
106            canonicalization: CanonicalizationAlgorithm::CanonicalXml11,
107            digest_algorithm: DigestAlgorithm::Sha256,
108            signature_algorithm: SignatureAlgorithm::RsaSha256,
109            document_id: "xdoc-doc-1".to_owned(),
110            signature_id: "xdoc-sig-1".to_owned(),
111            key_info_id: None,
112            references: vec![XmlDsigReferenceConfig::document_id()],
113            signature_placement: SignaturePlacement::Root,
114        }
115    }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum SignaturePlacement {
120    Root,
121    ParentNode(NodeId),
122    Query {
123        source: String,
124        namespaces: NamespaceContext,
125    },
126}
127
128impl SignaturePlacement {
129    pub fn root() -> Self {
130        Self::Root
131    }
132
133    pub fn parent_node(node: NodeId) -> Self {
134        Self::ParentNode(node)
135    }
136
137    pub fn query(source: impl Into<String>) -> Self {
138        Self::Query {
139            source: source.into(),
140            namespaces: NamespaceContext::new(),
141        }
142    }
143
144    pub fn query_with_context(source: impl Into<String>, namespaces: NamespaceContext) -> Self {
145        Self::Query {
146            source: source.into(),
147            namespaces,
148        }
149    }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct XmlDsigReferenceConfig {
154    target: XmlDsigReferenceTarget,
155    transforms: Option<Vec<Transform>>,
156    digest_algorithm: Option<DigestAlgorithm>,
157}
158
159impl XmlDsigReferenceConfig {
160    pub fn document_id() -> Self {
161        Self::new(XmlDsigReferenceTarget::DocumentId)
162    }
163
164    pub fn whole_document() -> Self {
165        Self::new(XmlDsigReferenceTarget::WholeDocument)
166    }
167
168    pub fn key_info() -> Self {
169        Self::new(XmlDsigReferenceTarget::KeyInfo)
170    }
171
172    pub fn new(target: XmlDsigReferenceTarget) -> Self {
173        Self {
174            target,
175            transforms: None,
176            digest_algorithm: None,
177        }
178    }
179
180    pub fn with_transforms(mut self, transforms: Vec<Transform>) -> Self {
181        self.transforms = Some(transforms);
182        self
183    }
184
185    pub fn with_digest_algorithm(mut self, digest_algorithm: DigestAlgorithm) -> Self {
186        self.digest_algorithm = Some(digest_algorithm);
187        self
188    }
189
190    pub fn target(&self) -> XmlDsigReferenceTarget {
191        self.target
192    }
193
194    pub fn transforms(&self) -> Option<&[Transform]> {
195        self.transforms.as_deref()
196    }
197
198    pub fn digest_algorithm(&self) -> Option<DigestAlgorithm> {
199        self.digest_algorithm
200    }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum XmlDsigReferenceTarget {
205    DocumentId,
206    WholeDocument,
207    KeyInfo,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct SignedInfo {
212    pub canonicalization_algorithm: CanonicalizationAlgorithm,
213    pub signature_algorithm: SignatureAlgorithm,
214    pub references: Vec<Reference>,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct Reference {
219    pub uri: String,
220    pub type_uri: Option<String>,
221    pub transforms: Vec<Transform>,
222    pub digest_algorithm: DigestAlgorithm,
223    pub digest_value: Vec<u8>,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub enum Transform {
228    EnvelopedSignature,
229    Canonicalization(CanonicalizationAlgorithm),
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct SignatureValue(pub Vec<u8>);
234
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct KeyInfo {
237    pub certificate_der: Vec<u8>,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct VerificationReport {
242    pub valid: bool,
243    pub reference_results: Vec<ReferenceValidationResult>,
244    pub signature_value_valid: bool,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct ReferenceValidationResult {
249    pub uri: String,
250    pub valid: bool,
251}
252
253pub fn sign_enveloped(
254    document: &Document,
255    provider: &impl SigningProvider,
256    config: &XmlDsigConfig,
257) -> XmlResult<Document> {
258    config.digest_algorithm.ensure_allowed_for_generation()?;
259    config.signature_algorithm.ensure_allowed_for_generation()?;
260    validate_reference_configs(config)?;
261
262    let mut signed = document.clone();
263    let root = signed.root().ok_or_else(|| {
264        XmlError::new(
265            ErrorKind::Signature,
266            "cannot sign a document without a root element",
267        )
268    })?;
269    let document_id = if config
270        .references
271        .iter()
272        .any(|reference| reference.target == XmlDsigReferenceTarget::DocumentId)
273    {
274        Some(ensure_root_id(&mut signed, root, config)?)
275    } else {
276        None
277    };
278    let signature_parent = resolve_signature_parent(&signed, config)?;
279    let certificate = provider.certificate_der()?;
280    let key_info_id = key_info_reference_id(config);
281    let canonicalization = CanonicalizationConfig::new(config.canonicalization);
282
283    let signature = signed.add_element(
284        signature_parent,
285        QName::qualified("ds", "Signature", XMLDSIG_NAMESPACE_URI)?,
286    )?;
287    signed.add_namespace_declaration(
288        signature,
289        NamespaceDeclaration::prefixed("ds", XMLDSIG_NAMESPACE_URI)?,
290    )?;
291    signed.add_attribute(
292        signature,
293        Attribute::new(QName::new("Id")?, config.signature_id.clone()),
294    )?;
295
296    let signed_info = add_signed_info_element(&mut signed, signature)?;
297    let signature_value_node = signed.add_element(
298        signature,
299        QName::qualified("ds", "SignatureValue", XMLDSIG_NAMESPACE_URI)?,
300    )?;
301    let key_info = add_key_info(&mut signed, signature, &certificate, key_info_id)?;
302    let references = build_references(
303        &signed,
304        root,
305        signature,
306        document_id.as_deref(),
307        Some(key_info),
308        config,
309    )?;
310    populate_signed_info(&mut signed, signed_info, config, &references)?;
311    let signed_info_bytes = canonicalize_node(&signed, signed_info, &canonicalization)?;
312    let signature_value = provider.sign(config.signature_algorithm, &signed_info_bytes)?;
313
314    signed.add_text(
315        signature_value_node,
316        encode_standard_base64(&signature_value),
317    )?;
318    super::ensure_unique_ids(&signed, &config.id_policy)?;
319
320    Ok(signed)
321}
322
323pub fn verify_enveloped(
324    document: &Document,
325    provider: &impl SigningProvider,
326    config: &XmlDsigConfig,
327) -> XmlResult<VerificationReport> {
328    let signature = find_signature(document)?;
329    let signed_info = required_child(document, signature, "SignedInfo")?;
330    let signature_value = required_child_text(document, signature, "SignatureValue")?;
331    let signature_value = decode_standard_base64(&signature_value)?;
332    let parsed = parse_signed_info(document, signed_info)?;
333
334    let mut reference_results = Vec::new();
335    for reference in &parsed.references {
336        let valid = verify_reference(document, signature, reference, config)?;
337        reference_results.push(ReferenceValidationResult {
338            uri: reference.uri.clone(),
339            valid,
340        });
341    }
342
343    let canonicalization = CanonicalizationConfig::new(parsed.canonicalization_algorithm);
344    let signed_info_bytes = canonicalize_node(document, signed_info, &canonicalization)?;
345    let signature_value_valid = provider.verify(
346        parsed.signature_algorithm,
347        &signed_info_bytes,
348        &signature_value,
349    )?;
350    let references_valid = reference_results.iter().all(|result| result.valid);
351
352    Ok(VerificationReport {
353        valid: references_valid && signature_value_valid,
354        reference_results,
355        signature_value_valid,
356    })
357}
358
359pub(crate) fn resolve_signature_parent(
360    document: &Document,
361    config: &XmlDsigConfig,
362) -> XmlResult<NodeId> {
363    let parent = match &config.signature_placement {
364        SignaturePlacement::Root => document.root().ok_or_else(|| {
365            XmlError::new(
366                ErrorKind::Signature,
367                "cannot place signature in a document without a root element",
368            )
369        })?,
370        SignaturePlacement::ParentNode(node) => *node,
371        SignaturePlacement::Query { source, namespaces } => {
372            let query = Query::parse(source)?;
373            let result = query.evaluate_with_context(document, namespaces)?;
374            let values = result.values();
375            match values {
376                [QueryValue::Node(node)] => *node,
377                [] => {
378                    return Err(XmlError::new(
379                        ErrorKind::Signature,
380                        format!("signature placement query `{source}` did not match any node"),
381                    ));
382                }
383                [_] => {
384                    return Err(XmlError::new(
385                        ErrorKind::Signature,
386                        format!("signature placement query `{source}` must select an element node"),
387                    ));
388                }
389                _ => {
390                    return Err(XmlError::new(
391                        ErrorKind::Signature,
392                        format!("signature placement query `{source}` is ambiguous"),
393                    ));
394                }
395            }
396        }
397    };
398
399    ensure_signature_parent(document, parent)?;
400    Ok(parent)
401}
402
403fn ensure_signature_parent(document: &Document, parent: NodeId) -> XmlResult<()> {
404    match document.node(parent)?.kind() {
405        NodeKind::Element(_) => Ok(()),
406        _ => Err(XmlError::new(
407            ErrorKind::Signature,
408            "signature placement target must be an element node",
409        )),
410    }
411}
412
413pub(crate) fn validate_reference_configs(config: &XmlDsigConfig) -> XmlResult<()> {
414    if config.references.is_empty() {
415        return Err(XmlError::new(
416            ErrorKind::Signature,
417            "XMLDSig config must contain at least one reference",
418        ));
419    }
420    for reference in &config.references {
421        let digest_algorithm = reference
422            .digest_algorithm
423            .unwrap_or(config.digest_algorithm);
424        digest_algorithm.ensure_allowed_for_generation()?;
425        let transforms = reference_transforms(reference, config);
426        ensure_reference_transforms(reference.target, &transforms)?;
427    }
428    Ok(())
429}
430
431pub(crate) fn key_info_reference_id(config: &XmlDsigConfig) -> Option<&str> {
432    config
433        .references
434        .iter()
435        .any(|reference| reference.target == XmlDsigReferenceTarget::KeyInfo)
436        .then(|| config.key_info_id.as_deref().unwrap_or(DEFAULT_KEY_INFO_ID))
437}
438
439pub(crate) fn build_references(
440    document: &Document,
441    root: NodeId,
442    signature: NodeId,
443    document_id: Option<&str>,
444    key_info: Option<NodeId>,
445    config: &XmlDsigConfig,
446) -> XmlResult<Vec<Reference>> {
447    let mut references = Vec::with_capacity(config.references.len());
448    for reference in &config.references {
449        references.push(build_reference(
450            document,
451            root,
452            signature,
453            document_id,
454            key_info,
455            config,
456            reference,
457        )?);
458    }
459    Ok(references)
460}
461
462fn build_reference(
463    document: &Document,
464    root: NodeId,
465    signature: NodeId,
466    document_id: Option<&str>,
467    key_info: Option<NodeId>,
468    config: &XmlDsigConfig,
469    reference: &XmlDsigReferenceConfig,
470) -> XmlResult<Reference> {
471    let transforms = reference_transforms(reference, config);
472    let digest_algorithm = reference
473        .digest_algorithm
474        .unwrap_or(config.digest_algorithm);
475    let (uri, canonicalized) = match reference.target {
476        XmlDsigReferenceTarget::DocumentId => {
477            let document_id = document_id.ok_or_else(|| {
478                XmlError::new(
479                    ErrorKind::Signature,
480                    "document ID reference requires a document Id",
481                )
482            })?;
483            (
484                format!("#{document_id}"),
485                canonicalize_reference_node(document, root, Some(signature), &transforms)?,
486            )
487        }
488        XmlDsigReferenceTarget::WholeDocument => (
489            String::new(),
490            canonicalize_reference_node(document, root, Some(signature), &transforms)?,
491        ),
492        XmlDsigReferenceTarget::KeyInfo => {
493            let key_info = key_info.ok_or_else(|| {
494                XmlError::new(
495                    ErrorKind::Signature,
496                    "KeyInfo reference requires a KeyInfo element",
497                )
498            })?;
499            let key_info_id = key_info_reference_id(config).ok_or_else(|| {
500                XmlError::new(
501                    ErrorKind::Signature,
502                    "KeyInfo reference requires a KeyInfo Id",
503                )
504            })?;
505            (
506                format!("#{key_info_id}"),
507                canonicalize_reference_node(document, key_info, None, &transforms)?,
508            )
509        }
510    };
511
512    Ok(Reference {
513        uri,
514        type_uri: None,
515        transforms,
516        digest_algorithm,
517        digest_value: digest_bytes(digest_algorithm, canonicalized)?,
518    })
519}
520
521fn reference_transforms(
522    reference: &XmlDsigReferenceConfig,
523    config: &XmlDsigConfig,
524) -> Vec<Transform> {
525    reference
526        .transforms
527        .clone()
528        .unwrap_or_else(|| default_transforms_for_target(reference.target, config.canonicalization))
529}
530
531fn default_transforms_for_target(
532    target: XmlDsigReferenceTarget,
533    canonicalization: CanonicalizationAlgorithm,
534) -> Vec<Transform> {
535    match target {
536        XmlDsigReferenceTarget::DocumentId | XmlDsigReferenceTarget::WholeDocument => vec![
537            Transform::EnvelopedSignature,
538            Transform::Canonicalization(canonicalization),
539        ],
540        XmlDsigReferenceTarget::KeyInfo => vec![Transform::Canonicalization(canonicalization)],
541    }
542}
543
544fn ensure_reference_transforms(
545    target: XmlDsigReferenceTarget,
546    transforms: &[Transform],
547) -> XmlResult<()> {
548    if canonicalization_from_transforms(transforms).is_none() {
549        return Err(XmlError::new(
550            ErrorKind::Signature,
551            "XMLDSig reference must include a canonicalization transform",
552        ));
553    }
554    if matches!(
555        target,
556        XmlDsigReferenceTarget::DocumentId | XmlDsigReferenceTarget::WholeDocument
557    ) && !transforms
558        .iter()
559        .any(|transform| matches!(transform, Transform::EnvelopedSignature))
560    {
561        return Err(XmlError::new(
562            ErrorKind::Signature,
563            "document XMLDSig reference must include enveloped-signature transform",
564        ));
565    }
566    Ok(())
567}
568
569fn canonicalization_from_transforms(transforms: &[Transform]) -> Option<CanonicalizationAlgorithm> {
570    transforms.iter().find_map(|transform| match transform {
571        Transform::Canonicalization(algorithm) => Some(*algorithm),
572        Transform::EnvelopedSignature => None,
573    })
574}
575
576fn canonicalize_reference_node(
577    document: &Document,
578    target: NodeId,
579    signature: Option<NodeId>,
580    transforms: &[Transform],
581) -> XmlResult<Vec<u8>> {
582    let canonicalization = canonicalization_from_transforms(transforms).ok_or_else(|| {
583        XmlError::new(
584            ErrorKind::Signature,
585            "reference is missing a canonicalization transform",
586        )
587    })?;
588    let c14n_config = CanonicalizationConfig::new(canonicalization);
589    if let Some(signature) = signature.filter(|_| {
590        transforms
591            .iter()
592            .any(|transform| matches!(transform, Transform::EnvelopedSignature))
593    }) {
594        canonicalize_node_excluding(document, target, &[signature], &c14n_config)
595    } else {
596        canonicalize_node(document, target, &c14n_config)
597    }
598}
599
600pub(crate) fn add_key_info(
601    document: &mut Document,
602    signature: NodeId,
603    certificate: &[u8],
604    key_info_id: Option<&str>,
605) -> XmlResult<NodeId> {
606    let key_info = document.add_element(
607        signature,
608        QName::qualified("ds", "KeyInfo", XMLDSIG_NAMESPACE_URI)?,
609    )?;
610    populate_key_info(document, key_info, certificate, key_info_id)?;
611    Ok(key_info)
612}
613
614fn populate_key_info(
615    document: &mut Document,
616    key_info: NodeId,
617    certificate: &[u8],
618    key_info_id: Option<&str>,
619) -> XmlResult<()> {
620    if let Some(key_info_id) = key_info_id {
621        document.add_namespace_declaration(
622            key_info,
623            NamespaceDeclaration::prefixed("ds", XMLDSIG_NAMESPACE_URI)?,
624        )?;
625        document.add_attribute(key_info, Attribute::new(QName::new("Id")?, key_info_id))?;
626    }
627
628    let x509_data = document.add_element(
629        key_info,
630        QName::qualified("ds", "X509Data", XMLDSIG_NAMESPACE_URI)?,
631    )?;
632    let x509_cert = document.add_element(
633        x509_data,
634        QName::qualified("ds", "X509Certificate", XMLDSIG_NAMESPACE_URI)?,
635    )?;
636    document.add_text(x509_cert, encode_standard_base64(certificate))?;
637    Ok(())
638}
639
640pub(crate) fn ensure_root_id(
641    document: &mut Document,
642    root: NodeId,
643    config: &XmlDsigConfig,
644) -> XmlResult<String> {
645    if let Some(id) = element_id(document, root, &config.id_policy)? {
646        return Ok(id);
647    }
648
649    document.add_attribute(
650        root,
651        Attribute::new(QName::new("Id")?, config.document_id.clone()),
652    )?;
653    super::ensure_unique_ids(document, &config.id_policy)?;
654    Ok(config.document_id.clone())
655}
656
657fn element_id(
658    document: &Document,
659    node: NodeId,
660    policy: &IdAttributePolicy,
661) -> XmlResult<Option<String>> {
662    super::ensure_unique_ids(document, policy)?;
663    let NodeKind::Element(element) = document.node(node)?.kind() else {
664        return Ok(None);
665    };
666    for attribute in element.attributes() {
667        if !super::ids::is_id_attribute_name(policy, attribute.name()) {
668            continue;
669        }
670        let found = find_element_by_id(document, attribute.value(), policy);
671        if matches!(found, Ok(found_node) if found_node == node) {
672            return Ok(Some(attribute.value().to_owned()));
673        }
674    }
675    Ok(None)
676}
677
678pub(crate) fn add_signed_info_element(
679    document: &mut Document,
680    signature: NodeId,
681) -> XmlResult<NodeId> {
682    let signed_info = document.add_element(
683        signature,
684        QName::qualified("ds", "SignedInfo", XMLDSIG_NAMESPACE_URI)?,
685    )?;
686    document.add_namespace_declaration(
687        signed_info,
688        NamespaceDeclaration::prefixed("ds", XMLDSIG_NAMESPACE_URI)?,
689    )?;
690    Ok(signed_info)
691}
692
693pub(crate) fn populate_signed_info(
694    document: &mut Document,
695    signed_info: NodeId,
696    config: &XmlDsigConfig,
697    references: &[Reference],
698) -> XmlResult<()> {
699    let c14n_method = document.add_element(
700        signed_info,
701        QName::qualified("ds", "CanonicalizationMethod", XMLDSIG_NAMESPACE_URI)?,
702    )?;
703    document.add_attribute(
704        c14n_method,
705        Attribute::new(QName::new("Algorithm")?, config.canonicalization.uri()),
706    )?;
707
708    let signature_method = document.add_element(
709        signed_info,
710        QName::qualified("ds", "SignatureMethod", XMLDSIG_NAMESPACE_URI)?,
711    )?;
712    document.add_attribute(
713        signature_method,
714        Attribute::new(QName::new("Algorithm")?, config.signature_algorithm.uri()),
715    )?;
716
717    for reference in references {
718        add_reference(document, signed_info, reference)?;
719    }
720
721    Ok(())
722}
723
724fn add_reference(
725    document: &mut Document,
726    signed_info: NodeId,
727    reference: &Reference,
728) -> XmlResult<()> {
729    let reference_node = document.add_element(
730        signed_info,
731        QName::qualified("ds", "Reference", XMLDSIG_NAMESPACE_URI)?,
732    )?;
733    document.add_attribute(
734        reference_node,
735        Attribute::new(QName::new("URI")?, reference.uri.clone()),
736    )?;
737    if let Some(type_uri) = &reference.type_uri {
738        document.add_attribute(
739            reference_node,
740            Attribute::new(QName::new("Type")?, type_uri),
741        )?;
742    }
743
744    let transforms = document.add_element(
745        reference_node,
746        QName::qualified("ds", "Transforms", XMLDSIG_NAMESPACE_URI)?,
747    )?;
748    for transform in &reference.transforms {
749        let transform_node = document.add_element(
750            transforms,
751            QName::qualified("ds", "Transform", XMLDSIG_NAMESPACE_URI)?,
752        )?;
753        document.add_attribute(
754            transform_node,
755            Attribute::new(QName::new("Algorithm")?, transform.uri()),
756        )?;
757    }
758
759    let digest_method = document.add_element(
760        reference_node,
761        QName::qualified("ds", "DigestMethod", XMLDSIG_NAMESPACE_URI)?,
762    )?;
763    document.add_attribute(
764        digest_method,
765        Attribute::new(QName::new("Algorithm")?, reference.digest_algorithm.uri()),
766    )?;
767
768    let digest_value = document.add_element(
769        reference_node,
770        QName::qualified("ds", "DigestValue", XMLDSIG_NAMESPACE_URI)?,
771    )?;
772    document.add_text(
773        digest_value,
774        encode_standard_base64(&reference.digest_value),
775    )?;
776    Ok(())
777}
778
779fn verify_reference(
780    document: &Document,
781    signature: NodeId,
782    reference: &Reference,
783    config: &XmlDsigConfig,
784) -> XmlResult<bool> {
785    let target = reference_target(document, &reference.uri, config)?;
786    let canonicalized =
787        canonicalize_reference_node(document, target, Some(signature), &reference.transforms)?;
788    let actual = digest_bytes(reference.digest_algorithm, canonicalized)?;
789    Ok(actual == reference.digest_value)
790}
791
792fn reference_target(document: &Document, uri: &str, config: &XmlDsigConfig) -> XmlResult<NodeId> {
793    if uri.is_empty() {
794        return document.root().ok_or_else(|| {
795            XmlError::new(
796                ErrorKind::Signature,
797                "cannot verify whole-document reference without a root element",
798            )
799        });
800    }
801
802    let id = uri.strip_prefix('#').ok_or_else(|| {
803        XmlError::new(
804            ErrorKind::Signature,
805            format!("unsupported reference URI `{uri}`"),
806        )
807    })?;
808    find_element_by_id(document, id, &config.id_policy)
809}
810
811pub(crate) fn parse_signed_info(document: &Document, signed_info: NodeId) -> XmlResult<SignedInfo> {
812    let canonicalization_method = required_child(document, signed_info, "CanonicalizationMethod")?;
813    let signature_method = required_child(document, signed_info, "SignatureMethod")?;
814    let canonicalization_algorithm = CanonicalizationAlgorithm::from_uri(&required_attribute(
815        document,
816        canonicalization_method,
817        "Algorithm",
818    )?)?;
819    let signature_algorithm = SignatureAlgorithm::from_uri(&required_attribute(
820        document,
821        signature_method,
822        "Algorithm",
823    )?)?;
824    signature_algorithm.ensure_allowed_for_generation()?;
825
826    let mut references = Vec::new();
827    for child in element_children(document, signed_info)? {
828        if element_local_name(document, child)? == "Reference" {
829            references.push(parse_reference(document, child)?);
830        }
831    }
832    if references.is_empty() {
833        return Err(XmlError::new(
834            ErrorKind::Signature,
835            "SignedInfo must contain at least one Reference",
836        ));
837    }
838
839    Ok(SignedInfo {
840        canonicalization_algorithm,
841        signature_algorithm,
842        references,
843    })
844}
845
846fn parse_reference(document: &Document, reference: NodeId) -> XmlResult<Reference> {
847    let uri = required_attribute(document, reference, "URI")?;
848    let type_uri = optional_attribute(document, reference, "Type")?;
849    let transforms_node = required_child(document, reference, "Transforms")?;
850    let mut transforms = Vec::new();
851    for transform in element_children(document, transforms_node)? {
852        if element_local_name(document, transform)? != "Transform" {
853            continue;
854        }
855        transforms.push(Transform::from_uri(&required_attribute(
856            document,
857            transform,
858            "Algorithm",
859        )?)?);
860    }
861
862    let digest_method = required_child(document, reference, "DigestMethod")?;
863    let digest_algorithm =
864        DigestAlgorithm::from_uri(&required_attribute(document, digest_method, "Algorithm")?)?;
865    digest_algorithm.ensure_allowed_for_generation()?;
866    let digest_value =
867        decode_standard_base64(&required_child_text(document, reference, "DigestValue")?)?;
868
869    Ok(Reference {
870        uri,
871        type_uri,
872        transforms,
873        digest_algorithm,
874        digest_value,
875    })
876}
877
878pub(crate) fn find_signature(document: &Document) -> XmlResult<NodeId> {
879    let root = document.root().ok_or_else(|| {
880        XmlError::new(
881            ErrorKind::Signature,
882            "cannot verify a document without a root element",
883        )
884    })?;
885    let mut signatures = Vec::new();
886    collect_elements_by_local_name(document, root, "Signature", &mut signatures)?;
887    match signatures.as_slice() {
888        [signature] => Ok(*signature),
889        [] => Err(XmlError::new(
890            ErrorKind::Signature,
891            "document does not contain a ds:Signature element",
892        )),
893        _ => Err(XmlError::new(
894            ErrorKind::Signature,
895            "document contains multiple Signature elements; verification target is ambiguous",
896        )),
897    }
898}
899
900fn collect_elements_by_local_name(
901    document: &Document,
902    node: NodeId,
903    local: &str,
904    matches: &mut Vec<NodeId>,
905) -> XmlResult<()> {
906    let NodeKind::Element(element) = document.node(node)?.kind() else {
907        return Ok(());
908    };
909    if element.name().namespace_uri().map(|uri| uri.as_str()) == Some(XMLDSIG_NAMESPACE_URI)
910        && element.name().local() == local
911    {
912        matches.push(node);
913    }
914    for child in element.children() {
915        collect_elements_by_local_name(document, *child, local, matches)?;
916    }
917    Ok(())
918}
919
920pub(crate) fn required_child(
921    document: &Document,
922    parent: NodeId,
923    local: &str,
924) -> XmlResult<NodeId> {
925    element_children(document, parent)?
926        .into_iter()
927        .find(|child| element_local_name(document, *child).as_deref() == Ok(local))
928        .ok_or_else(|| {
929            XmlError::new(
930                ErrorKind::Signature,
931                format!("missing required XMLDSig child `{local}`"),
932            )
933        })
934}
935
936pub(crate) fn required_child_text(
937    document: &Document,
938    parent: NodeId,
939    local: &str,
940) -> XmlResult<String> {
941    let child = required_child(document, parent, local)?;
942    let mut text = String::new();
943    for node in element_children_or_text(document, child)? {
944        if let NodeKind::Text(value) = document.node(node)?.kind() {
945            text.push_str(value);
946        }
947    }
948    if text.is_empty() {
949        return Err(XmlError::new(
950            ErrorKind::Signature,
951            format!("XMLDSig child `{local}` must contain text"),
952        ));
953    }
954    Ok(text)
955}
956
957pub(crate) fn required_attribute(
958    document: &Document,
959    node: NodeId,
960    local: &str,
961) -> XmlResult<String> {
962    optional_attribute(document, node, local)?.ok_or_else(|| {
963        XmlError::new(
964            ErrorKind::Signature,
965            format!("missing required XMLDSig attribute `{local}`"),
966        )
967    })
968}
969
970pub(crate) fn optional_attribute(
971    document: &Document,
972    node: NodeId,
973    local: &str,
974) -> XmlResult<Option<String>> {
975    let NodeKind::Element(element) = document.node(node)?.kind() else {
976        return Ok(None);
977    };
978    Ok(element
979        .attributes()
980        .iter()
981        .find(|attribute| attribute.name().local() == local)
982        .map(|attribute| attribute.value().to_owned()))
983}
984
985pub(crate) fn element_children(document: &Document, parent: NodeId) -> XmlResult<Vec<NodeId>> {
986    let NodeKind::Element(element) = document.node(parent)?.kind() else {
987        return Ok(Vec::new());
988    };
989    Ok(element
990        .children()
991        .iter()
992        .copied()
993        .filter(|child| {
994            matches!(
995                document.node(*child).map(|node| node.kind()),
996                Ok(NodeKind::Element(_))
997            )
998        })
999        .collect())
1000}
1001
1002fn element_children_or_text(document: &Document, parent: NodeId) -> XmlResult<Vec<NodeId>> {
1003    let NodeKind::Element(element) = document.node(parent)?.kind() else {
1004        return Ok(Vec::new());
1005    };
1006    Ok(element.children().to_vec())
1007}
1008
1009pub(crate) fn element_local_name(document: &Document, node: NodeId) -> XmlResult<String> {
1010    match document.node(node)?.kind() {
1011        NodeKind::Element(element) => Ok(element.name().local().to_owned()),
1012        _ => Err(XmlError::new(
1013            ErrorKind::Signature,
1014            "expected XMLDSig element node",
1015        )),
1016    }
1017}
1018
1019impl Transform {
1020    fn uri(&self) -> &'static str {
1021        match self {
1022            Self::EnvelopedSignature => XMLDSIG_ENVELOPED_SIGNATURE_URI,
1023            Self::Canonicalization(algorithm) => algorithm.uri(),
1024        }
1025    }
1026
1027    fn from_uri(uri: &str) -> XmlResult<Self> {
1028        if uri == XMLDSIG_ENVELOPED_SIGNATURE_URI {
1029            return Ok(Self::EnvelopedSignature);
1030        }
1031        Ok(Self::Canonicalization(CanonicalizationAlgorithm::from_uri(
1032            uri,
1033        )?))
1034    }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039    use crate::core::Attribute;
1040    use crate::parser::parse_str;
1041    use crate::signature::DeterministicSigningProvider;
1042    use crate::writer::to_string_compact;
1043
1044    use super::*;
1045
1046    fn provider() -> DeterministicSigningProvider {
1047        DeterministicSigningProvider::new(b"test-cert".to_vec(), b"test-secret".to_vec())
1048    }
1049
1050    fn unsigned_document() -> XmlResult<Document> {
1051        parse_str(r#"<Root Id="doc-1"><Item>value</Item></Root>"#)
1052    }
1053
1054    fn extension_document() -> XmlResult<Document> {
1055        parse_str(r#"<Root Id="doc-1"><Extension><Payload>value</Payload></Extension></Root>"#)
1056    }
1057
1058    #[test]
1059    fn xmldsig_signs_and_verifies_enveloped_document() -> XmlResult<()> {
1060        let signed = sign_enveloped(&unsigned_document()?, &provider(), &XmlDsigConfig::new())?;
1061        let report = verify_enveloped(&signed, &provider(), &XmlDsigConfig::new())?;
1062        let xml = to_string_compact(&signed)?;
1063
1064        assert!(report.valid);
1065        assert!(report.signature_value_valid);
1066        assert_eq!(report.reference_results.len(), 1);
1067        assert!(xml.contains("<ds:Signature"));
1068        assert!(xml.contains("<ds:SignedInfo"));
1069        assert!(xml.contains("<ds:KeyInfo>"));
1070
1071        Ok(())
1072    }
1073
1074    #[test]
1075    fn xmldsig_adds_root_id_when_missing() -> XmlResult<()> {
1076        let document = parse_str("<Root><Item>value</Item></Root>")?;
1077        let signed = sign_enveloped(&document, &provider(), &XmlDsigConfig::new())?;
1078        let root = signed.root().expect("root");
1079        let NodeKind::Element(element) = signed.node(root)?.kind() else {
1080            panic!("root is element");
1081        };
1082
1083        assert!(
1084            element
1085                .attributes()
1086                .iter()
1087                .any(|attribute| attribute.name().local() == "Id"
1088                    && attribute.value() == "xdoc-doc-1")
1089        );
1090        assert!(verify_enveloped(&signed, &provider(), &XmlDsigConfig::new())?.valid);
1091
1092        Ok(())
1093    }
1094
1095    #[test]
1096    fn signature_placement_can_use_explicit_parent_node() -> XmlResult<()> {
1097        let document = extension_document()?;
1098        let extension = Query::parse("/Root/Extension")?
1099            .evaluate(&document)?
1100            .nodes()
1101            .pop()
1102            .expect("extension node");
1103        let config = XmlDsigConfig::new()
1104            .with_signature_placement(SignaturePlacement::parent_node(extension));
1105        let signed = sign_enveloped(&document, &provider(), &config)?;
1106        let report = verify_enveloped(&signed, &provider(), &config)?;
1107        let xml = to_string_compact(&signed)?;
1108
1109        assert!(report.valid);
1110        assert!(xml.contains("<Extension><Payload>value</Payload><ds:Signature"));
1111        Ok(())
1112    }
1113
1114    #[test]
1115    fn signature_placement_can_use_query() -> XmlResult<()> {
1116        let config = XmlDsigConfig::new()
1117            .with_signature_placement(SignaturePlacement::query("/Root/Extension"));
1118        let signed = sign_enveloped(&extension_document()?, &provider(), &config)?;
1119        let report = verify_enveloped(&signed, &provider(), &config)?;
1120        let xml = to_string_compact(&signed)?;
1121
1122        assert!(report.valid);
1123        assert!(xml.contains("<Extension><Payload>value</Payload><ds:Signature"));
1124        Ok(())
1125    }
1126
1127    #[test]
1128    fn signature_placement_rejects_ambiguous_query() {
1129        let document = parse_str(r#"<Root Id="doc-1"><Extension/><Extension/></Root>"#)
1130            .expect("valid document");
1131        let config =
1132            XmlDsigConfig::new().with_signature_placement(SignaturePlacement::query("//Extension"));
1133
1134        let error = sign_enveloped(&document, &provider(), &config)
1135            .expect_err("ambiguous placement must fail");
1136
1137        assert_eq!(error.kind(), &ErrorKind::Signature);
1138        assert!(error.message().contains("ambiguous"));
1139    }
1140
1141    #[test]
1142    fn xmldsig_can_select_c14n10_by_config() -> XmlResult<()> {
1143        let config =
1144            XmlDsigConfig::new().with_canonicalization(CanonicalizationAlgorithm::CanonicalXml10);
1145        let signed = sign_enveloped(&unsigned_document()?, &provider(), &config)?;
1146        let report = verify_enveloped(&signed, &provider(), &config)?;
1147        let xml = to_string_compact(&signed)?;
1148
1149        assert!(report.valid);
1150        assert!(xml.contains("ds:CanonicalizationMethod"));
1151        assert!(xml.contains("ds:Transform"));
1152        assert_eq!(
1153            xml.matches(r#"Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315""#)
1154                .count(),
1155            2
1156        );
1157        Ok(())
1158    }
1159
1160    #[test]
1161    fn xmldsig_references_can_target_whole_document_uri() -> XmlResult<()> {
1162        let document = parse_str("<Root><Item>value</Item></Root>")?;
1163        let config =
1164            XmlDsigConfig::new().with_references(vec![XmlDsigReferenceConfig::whole_document()]);
1165        let signed = sign_enveloped(&document, &provider(), &config)?;
1166        let report = verify_enveloped(&signed, &provider(), &config)?;
1167        let xml = to_string_compact(&signed)?;
1168
1169        assert!(report.valid);
1170        assert_eq!(report.reference_results.len(), 1);
1171        assert_eq!(report.reference_results[0].uri, "");
1172        assert!(!xml.contains("xdoc-doc-1"));
1173        assert!(xml.contains(r#"<ds:Reference URI="">"#));
1174        assert!(xml.contains(XMLDSIG_ENVELOPED_SIGNATURE_URI));
1175        Ok(())
1176    }
1177
1178    #[test]
1179    fn key_info_reference_is_signed_and_detects_tampering() -> XmlResult<()> {
1180        let config = XmlDsigConfig::new()
1181            .with_key_info_id("key-info-1")
1182            .with_references(vec![
1183                XmlDsigReferenceConfig::document_id(),
1184                XmlDsigReferenceConfig::key_info(),
1185            ]);
1186        let signed = sign_enveloped(&unsigned_document()?, &provider(), &config)?;
1187        let report = verify_enveloped(&signed, &provider(), &config)?;
1188        let xml = to_string_compact(&signed)?;
1189
1190        assert!(report.valid);
1191        assert_eq!(report.reference_results.len(), 2);
1192        assert!(report
1193            .reference_results
1194            .iter()
1195            .any(|result| result.uri == "#key-info-1" && result.valid));
1196        assert!(xml.contains(r##"<ds:Reference URI="#key-info-1">"##));
1197        assert!(xml.contains(
1198            r##"<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="key-info-1">"##
1199        ));
1200
1201        let tampered = parse_str(&xml.replace(
1202            &encode_standard_base64(b"test-cert"),
1203            &encode_standard_base64(b"other-cert"),
1204        ))?;
1205        let report = verify_enveloped(&tampered, &provider(), &config)?;
1206
1207        assert!(!report.valid);
1208        assert!(report
1209            .reference_results
1210            .iter()
1211            .any(|result| result.uri == "#doc-1" && result.valid));
1212        assert!(report
1213            .reference_results
1214            .iter()
1215            .any(|result| result.uri == "#key-info-1" && !result.valid));
1216        assert!(report.signature_value_valid);
1217        Ok(())
1218    }
1219
1220    #[test]
1221    fn key_info_reference_rejects_duplicate_id() {
1222        let config = XmlDsigConfig::new()
1223            .with_key_info_id("doc-1")
1224            .with_references(vec![
1225                XmlDsigReferenceConfig::document_id(),
1226                XmlDsigReferenceConfig::key_info(),
1227            ]);
1228        let error = sign_enveloped(
1229            &unsigned_document().expect("valid document"),
1230            &provider(),
1231            &config,
1232        )
1233        .expect_err("duplicate KeyInfo Id must fail");
1234
1235        assert_eq!(error.kind(), &ErrorKind::Signature);
1236    }
1237
1238    #[test]
1239    fn xmldsig_references_reject_document_without_enveloped_transform() {
1240        let config =
1241            XmlDsigConfig::new().with_references(vec![XmlDsigReferenceConfig::whole_document()
1242                .with_transforms(vec![Transform::Canonicalization(
1243                    CanonicalizationAlgorithm::CanonicalXml11,
1244                )])]);
1245        let error = sign_enveloped(
1246            &parse_str("<Root/>").expect("valid document"),
1247            &provider(),
1248            &config,
1249        )
1250        .expect_err("document references require enveloped transform");
1251
1252        assert_eq!(error.kind(), &ErrorKind::Signature);
1253        assert!(error.message().contains("enveloped-signature"));
1254    }
1255
1256    #[test]
1257    fn xmldsig_verify_fails_when_signed_content_changes() -> XmlResult<()> {
1258        let mut signed = sign_enveloped(&unsigned_document()?, &provider(), &XmlDsigConfig::new())?;
1259        let root = signed.root().expect("root");
1260        signed.add_text(root, "tampered")?;
1261
1262        let report = verify_enveloped(&signed, &provider(), &XmlDsigConfig::new())?;
1263
1264        assert!(!report.valid);
1265        assert!(!report.reference_results[0].valid);
1266        Ok(())
1267    }
1268
1269    #[test]
1270    fn xmldsig_verify_fails_when_signature_value_changes() -> XmlResult<()> {
1271        let signed = sign_enveloped(&unsigned_document()?, &provider(), &XmlDsigConfig::new())?;
1272        let signature = find_signature(&signed)?;
1273        let original = required_child_text(&signed, signature, "SignatureValue")?;
1274        let mut tampered = decode_standard_base64(&original)?;
1275        tampered[0] ^= 0xff;
1276        let xml = to_string_compact(&signed)?.replace(&original, &encode_standard_base64(tampered));
1277        let signed = parse_str(&xml)?;
1278
1279        let report = verify_enveloped(&signed, &provider(), &XmlDsigConfig::new())?;
1280
1281        assert!(!report.valid);
1282        assert!(!report.signature_value_valid);
1283        Ok(())
1284    }
1285
1286    #[test]
1287    fn xmldsig_verify_rejects_unsupported_transform() -> XmlResult<()> {
1288        let signed = sign_enveloped(&unsigned_document()?, &provider(), &XmlDsigConfig::new())?;
1289        let xml = to_string_compact(&signed)?
1290            .replace(XMLDSIG_ENVELOPED_SIGNATURE_URI, "urn:unsupported-transform");
1291        let document = parse_str(&xml)?;
1292
1293        let error = verify_enveloped(&document, &provider(), &XmlDsigConfig::new())
1294            .expect_err("unsupported transform must fail");
1295
1296        assert_eq!(error.kind(), &ErrorKind::Signature);
1297        assert!(error
1298            .message()
1299            .contains("unsupported canonicalization algorithm"));
1300        Ok(())
1301    }
1302
1303    #[test]
1304    fn xmldsig_verify_rejects_ambiguous_references() -> XmlResult<()> {
1305        let mut document = unsigned_document()?;
1306        let root = document.root().expect("root");
1307        let duplicate = document.add_element(root, QName::new("Duplicate")?)?;
1308        document.add_attribute(duplicate, Attribute::new(QName::new("Id")?, "doc-1"))?;
1309
1310        let error = sign_enveloped(&document, &provider(), &XmlDsigConfig::new())
1311            .expect_err("duplicate IDs must fail");
1312
1313        assert_eq!(error.kind(), &ErrorKind::Signature);
1314        Ok(())
1315    }
1316}