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}