1use roxmltree::{Document, Node};
20
21use super::digest::DigestAlgorithm;
22use super::transforms::{self, Transform};
23use crate::c14n::C14nAlgorithm;
24
25const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum SignatureAlgorithm {
31 RsaSha1,
33 RsaSha256,
35 RsaSha384,
37 RsaSha512,
39 EcdsaP256Sha256,
41 EcdsaP384Sha384,
43}
44
45impl SignatureAlgorithm {
46 #[must_use]
48 pub fn from_uri(uri: &str) -> Option<Self> {
49 match uri {
50 "http://www.w3.org/2000/09/xmldsig#rsa-sha1" => Some(Self::RsaSha1),
51 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Some(Self::RsaSha256),
52 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => Some(Self::RsaSha384),
53 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => Some(Self::RsaSha512),
54 "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Some(Self::EcdsaP256Sha256),
55 "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => Some(Self::EcdsaP384Sha384),
56 _ => None,
57 }
58 }
59
60 #[must_use]
62 pub fn uri(self) -> &'static str {
63 match self {
64 Self::RsaSha1 => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
65 Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
66 Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
67 Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
68 Self::EcdsaP256Sha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
69 Self::EcdsaP384Sha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
70 }
71 }
72
73 #[must_use]
75 pub fn signing_allowed(self) -> bool {
76 !matches!(self, Self::RsaSha1)
77 }
78}
79
80#[derive(Debug)]
82pub struct SignedInfo {
83 pub c14n_method: C14nAlgorithm,
85 pub signature_method: SignatureAlgorithm,
87 pub references: Vec<Reference>,
89}
90
91#[derive(Debug)]
93pub struct Reference {
94 pub uri: Option<String>,
96 pub id: Option<String>,
98 pub ref_type: Option<String>,
100 pub transforms: Vec<Transform>,
102 pub digest_method: DigestAlgorithm,
104 pub digest_value: Vec<u8>,
106}
107
108#[derive(Debug, thiserror::Error)]
110#[non_exhaustive]
111pub enum ParseError {
112 #[error("missing required element: <{element}>")]
114 MissingElement {
115 element: &'static str,
117 },
118
119 #[error("invalid structure: {0}")]
121 InvalidStructure(String),
122
123 #[error("unsupported algorithm: {uri}")]
125 UnsupportedAlgorithm {
126 uri: String,
128 },
129
130 #[error("base64 decode error: {0}")]
132 Base64(String),
133
134 #[error(
136 "digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
137 )]
138 DigestLengthMismatch {
139 algorithm: &'static str,
141 expected: usize,
143 actual: usize,
145 },
146
147 #[error("transform error: {0}")]
149 Transform(#[from] super::types::TransformError),
150}
151
152#[must_use]
154pub fn find_signature_node<'a>(doc: &'a Document<'a>) -> Option<Node<'a, 'a>> {
155 doc.descendants().find(|n| {
156 n.is_element()
157 && n.tag_name().name() == "Signature"
158 && n.tag_name().namespace() == Some(XMLDSIG_NS)
159 })
160}
161
162pub fn parse_signed_info(signed_info_node: Node) -> Result<SignedInfo, ParseError> {
167 verify_ds_element(signed_info_node, "SignedInfo")?;
168
169 let mut children = element_children(signed_info_node);
170
171 let c14n_node = children.next().ok_or(ParseError::MissingElement {
173 element: "CanonicalizationMethod",
174 })?;
175 verify_ds_element(c14n_node, "CanonicalizationMethod")?;
176 let c14n_uri = required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
177 let mut c14n_method =
178 C14nAlgorithm::from_uri(c14n_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
179 uri: c14n_uri.to_string(),
180 })?;
181 if let Some(prefix_list) = parse_inclusive_prefixes(c14n_node)? {
182 if c14n_method.mode() == crate::c14n::C14nMode::Exclusive1_0 {
183 c14n_method = c14n_method.with_prefix_list(&prefix_list);
184 } else {
185 return Err(ParseError::UnsupportedAlgorithm {
186 uri: c14n_uri.to_string(),
187 });
188 }
189 }
190
191 let sig_method_node = children.next().ok_or(ParseError::MissingElement {
193 element: "SignatureMethod",
194 })?;
195 verify_ds_element(sig_method_node, "SignatureMethod")?;
196 let sig_uri = required_algorithm_attr(sig_method_node, "SignatureMethod")?;
197 let signature_method =
198 SignatureAlgorithm::from_uri(sig_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
199 uri: sig_uri.to_string(),
200 })?;
201
202 let mut references = Vec::new();
204 for child in children {
205 verify_ds_element(child, "Reference")?;
206 references.push(parse_reference(child)?);
207 }
208 if references.is_empty() {
209 return Err(ParseError::MissingElement {
210 element: "Reference",
211 });
212 }
213
214 Ok(SignedInfo {
215 c14n_method,
216 signature_method,
217 references,
218 })
219}
220
221fn parse_reference(reference_node: Node) -> Result<Reference, ParseError> {
225 let uri = reference_node.attribute("URI").map(String::from);
226 let id = reference_node.attribute("Id").map(String::from);
227 let ref_type = reference_node.attribute("Type").map(String::from);
228
229 let mut children = element_children(reference_node);
230
231 let mut transforms = Vec::new();
233 let mut next = children.next().ok_or(ParseError::MissingElement {
234 element: "DigestMethod",
235 })?;
236
237 if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) {
238 transforms = transforms::parse_transforms(next)?;
239 next = children.next().ok_or(ParseError::MissingElement {
240 element: "DigestMethod",
241 })?;
242 }
243
244 verify_ds_element(next, "DigestMethod")?;
246 let digest_uri = required_algorithm_attr(next, "DigestMethod")?;
247 let digest_method =
248 DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
249 uri: digest_uri.to_string(),
250 })?;
251
252 let digest_value_node = children.next().ok_or(ParseError::MissingElement {
254 element: "DigestValue",
255 })?;
256 verify_ds_element(digest_value_node, "DigestValue")?;
257 let digest_b64 = digest_value_node.text().unwrap_or("");
258 let digest_value = base64_decode_digest(digest_b64, digest_method)?;
259
260 if let Some(unexpected) = children.next() {
262 return Err(ParseError::InvalidStructure(format!(
263 "unexpected element <{}> after <DigestValue> in <Reference>",
264 unexpected.tag_name().name()
265 )));
266 }
267
268 Ok(Reference {
269 uri,
270 id,
271 ref_type,
272 transforms,
273 digest_method,
274 digest_value,
275 })
276}
277
278fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
282 node.children().filter(|n| n.is_element())
283}
284
285fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
287 if !node.is_element() {
288 return Err(ParseError::InvalidStructure(format!(
289 "expected element <{expected_name}>, got non-element node"
290 )));
291 }
292 let tag = node.tag_name();
293 if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
294 return Err(ParseError::InvalidStructure(format!(
295 "expected <ds:{expected_name}>, got <{}{}>",
296 tag.namespace()
297 .map(|ns| format!("{{{ns}}}"))
298 .unwrap_or_default(),
299 tag.name()
300 )));
301 }
302 Ok(())
303}
304
305fn required_algorithm_attr<'a>(
307 node: Node<'a, 'a>,
308 element_name: &'static str,
309) -> Result<&'a str, ParseError> {
310 node.attribute("Algorithm").ok_or_else(|| {
311 ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
312 })
313}
314
315fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
321 const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
322
323 for child in node.children() {
324 if child.is_element() {
325 let tag = child.tag_name();
326 if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
327 {
328 return child
329 .attribute("PrefixList")
330 .map(str::to_string)
331 .ok_or_else(|| {
332 ParseError::InvalidStructure(
333 "missing PrefixList attribute on <InclusiveNamespaces>".into(),
334 )
335 })
336 .map(Some);
337 }
338 }
339 }
340
341 Ok(None)
342}
343
344fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
348 use base64::Engine;
349 use base64::engine::general_purpose::STANDARD;
350
351 let cleaned: String = b64.chars().filter(|c| !c.is_ascii_whitespace()).collect();
353 let digest = STANDARD
354 .decode(&cleaned)
355 .map_err(|e| ParseError::Base64(e.to_string()))?;
356 let expected = digest_method.output_len();
357 let actual = digest.len();
358 if actual != expected {
359 return Err(ParseError::DigestLengthMismatch {
360 algorithm: digest_method.uri(),
361 expected,
362 actual,
363 });
364 }
365 Ok(digest)
366}
367
368#[cfg(test)]
369#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
370mod tests {
371 use super::*;
372
373 #[test]
376 fn signature_algorithm_from_uri_rsa_sha256() {
377 assert_eq!(
378 SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
379 Some(SignatureAlgorithm::RsaSha256)
380 );
381 }
382
383 #[test]
384 fn signature_algorithm_from_uri_rsa_sha1() {
385 assert_eq!(
386 SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
387 Some(SignatureAlgorithm::RsaSha1)
388 );
389 }
390
391 #[test]
392 fn signature_algorithm_from_uri_ecdsa_sha256() {
393 assert_eq!(
394 SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
395 Some(SignatureAlgorithm::EcdsaP256Sha256)
396 );
397 }
398
399 #[test]
400 fn signature_algorithm_from_uri_unknown() {
401 assert_eq!(
402 SignatureAlgorithm::from_uri("http://example.com/unknown"),
403 None
404 );
405 }
406
407 #[test]
408 fn signature_algorithm_uri_round_trip() {
409 for algo in [
410 SignatureAlgorithm::RsaSha1,
411 SignatureAlgorithm::RsaSha256,
412 SignatureAlgorithm::RsaSha384,
413 SignatureAlgorithm::RsaSha512,
414 SignatureAlgorithm::EcdsaP256Sha256,
415 SignatureAlgorithm::EcdsaP384Sha384,
416 ] {
417 assert_eq!(
418 SignatureAlgorithm::from_uri(algo.uri()),
419 Some(algo),
420 "round-trip failed for {algo:?}"
421 );
422 }
423 }
424
425 #[test]
426 fn rsa_sha1_verify_only() {
427 assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
428 assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
429 assert!(SignatureAlgorithm::EcdsaP256Sha256.signing_allowed());
430 }
431
432 #[test]
435 fn find_signature_in_saml() {
436 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
437 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
438 <ds:SignedInfo/>
439 </ds:Signature>
440 </samlp:Response>"#;
441 let doc = Document::parse(xml).unwrap();
442 let sig = find_signature_node(&doc);
443 assert!(sig.is_some());
444 assert_eq!(sig.unwrap().tag_name().name(), "Signature");
445 }
446
447 #[test]
448 fn find_signature_missing() {
449 let xml = "<root><child/></root>";
450 let doc = Document::parse(xml).unwrap();
451 assert!(find_signature_node(&doc).is_none());
452 }
453
454 #[test]
455 fn find_signature_ignores_wrong_namespace() {
456 let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
457 let doc = Document::parse(xml).unwrap();
458 assert!(find_signature_node(&doc).is_none());
459 }
460
461 #[test]
464 fn parse_signed_info_rsa_sha256_with_reference() {
465 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
466 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
467 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
468 <Reference URI="">
469 <Transforms>
470 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
471 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
472 </Transforms>
473 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
474 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
475 </Reference>
476 </SignedInfo>"#;
477 let doc = Document::parse(xml).unwrap();
478 let si = parse_signed_info(doc.root_element()).unwrap();
479
480 assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
481 assert_eq!(si.references.len(), 1);
482
483 let r = &si.references[0];
484 assert_eq!(r.uri.as_deref(), Some(""));
485 assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
486 assert_eq!(r.digest_value, vec![0u8; 32]);
487 assert_eq!(r.transforms.len(), 2);
488 }
489
490 #[test]
491 fn parse_signed_info_multiple_references() {
492 let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
493 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
494 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
495 <Reference URI="#a">
496 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
497 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
498 </Reference>
499 <Reference URI="#b">
500 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
501 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
502 </Reference>
503 </SignedInfo>"##;
504 let doc = Document::parse(xml).unwrap();
505 let si = parse_signed_info(doc.root_element()).unwrap();
506
507 assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaP256Sha256);
508 assert_eq!(si.references.len(), 2);
509 assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
510 assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
511 assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
512 assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
513 }
514
515 #[test]
516 fn parse_reference_without_transforms() {
517 let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
519 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
520 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
521 <Reference URI="#obj">
522 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
523 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
524 </Reference>
525 </SignedInfo>"##;
526 let doc = Document::parse(xml).unwrap();
527 let si = parse_signed_info(doc.root_element()).unwrap();
528
529 assert!(si.references[0].transforms.is_empty());
530 }
531
532 #[test]
533 fn parse_reference_with_all_attributes() {
534 let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
535 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
536 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
537 <Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
538 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
539 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
540 </Reference>
541 </SignedInfo>"##;
542 let doc = Document::parse(xml).unwrap();
543 let si = parse_signed_info(doc.root_element()).unwrap();
544 let r = &si.references[0];
545
546 assert_eq!(r.uri.as_deref(), Some("#data"));
547 assert_eq!(r.id.as_deref(), Some("ref1"));
548 assert_eq!(
549 r.ref_type.as_deref(),
550 Some("http://www.w3.org/2000/09/xmldsig#Object")
551 );
552 }
553
554 #[test]
555 fn parse_reference_absent_uri() {
556 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
558 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
559 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
560 <Reference>
561 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
562 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
563 </Reference>
564 </SignedInfo>"#;
565 let doc = Document::parse(xml).unwrap();
566 let si = parse_signed_info(doc.root_element()).unwrap();
567 assert!(si.references[0].uri.is_none());
568 }
569
570 #[test]
571 fn parse_signed_info_preserves_inclusive_prefixes() {
572 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
573 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
574 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
575 <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
576 </CanonicalizationMethod>
577 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
578 <Reference URI="">
579 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
580 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
581 </Reference>
582 </SignedInfo>"#;
583 let doc = Document::parse(xml).unwrap();
584
585 let si = parse_signed_info(doc.root_element()).unwrap();
586 assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
587 assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
588 assert!(si.c14n_method.inclusive_prefixes().contains(""));
589 }
590
591 #[test]
594 fn missing_canonicalization_method() {
595 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
596 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
597 <Reference URI="">
598 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
599 <DigestValue>dGVzdA==</DigestValue>
600 </Reference>
601 </SignedInfo>"#;
602 let doc = Document::parse(xml).unwrap();
603 let result = parse_signed_info(doc.root_element());
604 assert!(result.is_err());
605 assert!(matches!(
607 result.unwrap_err(),
608 ParseError::InvalidStructure(_)
609 ));
610 }
611
612 #[test]
613 fn missing_signature_method() {
614 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
615 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
616 <Reference URI="">
617 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
618 <DigestValue>dGVzdA==</DigestValue>
619 </Reference>
620 </SignedInfo>"#;
621 let doc = Document::parse(xml).unwrap();
622 let result = parse_signed_info(doc.root_element());
623 assert!(result.is_err());
624 assert!(matches!(
626 result.unwrap_err(),
627 ParseError::InvalidStructure(_)
628 ));
629 }
630
631 #[test]
632 fn no_references() {
633 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
634 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
635 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
636 </SignedInfo>"#;
637 let doc = Document::parse(xml).unwrap();
638 let result = parse_signed_info(doc.root_element());
639 assert!(matches!(
640 result.unwrap_err(),
641 ParseError::MissingElement {
642 element: "Reference"
643 }
644 ));
645 }
646
647 #[test]
648 fn unsupported_c14n_algorithm() {
649 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
650 <CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
651 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
652 <Reference URI="">
653 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
654 <DigestValue>dGVzdA==</DigestValue>
655 </Reference>
656 </SignedInfo>"#;
657 let doc = Document::parse(xml).unwrap();
658 let result = parse_signed_info(doc.root_element());
659 assert!(matches!(
660 result.unwrap_err(),
661 ParseError::UnsupportedAlgorithm { .. }
662 ));
663 }
664
665 #[test]
666 fn unsupported_signature_algorithm() {
667 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
668 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
669 <SignatureMethod Algorithm="http://example.com/bogus-sign"/>
670 <Reference URI="">
671 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
672 <DigestValue>dGVzdA==</DigestValue>
673 </Reference>
674 </SignedInfo>"#;
675 let doc = Document::parse(xml).unwrap();
676 let result = parse_signed_info(doc.root_element());
677 assert!(matches!(
678 result.unwrap_err(),
679 ParseError::UnsupportedAlgorithm { .. }
680 ));
681 }
682
683 #[test]
684 fn unsupported_digest_algorithm() {
685 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
686 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
687 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
688 <Reference URI="">
689 <DigestMethod Algorithm="http://example.com/bogus-digest"/>
690 <DigestValue>dGVzdA==</DigestValue>
691 </Reference>
692 </SignedInfo>"#;
693 let doc = Document::parse(xml).unwrap();
694 let result = parse_signed_info(doc.root_element());
695 assert!(matches!(
696 result.unwrap_err(),
697 ParseError::UnsupportedAlgorithm { .. }
698 ));
699 }
700
701 #[test]
702 fn missing_digest_method() {
703 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
704 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
705 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
706 <Reference URI="">
707 <DigestValue>dGVzdA==</DigestValue>
708 </Reference>
709 </SignedInfo>"#;
710 let doc = Document::parse(xml).unwrap();
711 let result = parse_signed_info(doc.root_element());
712 assert!(result.is_err());
714 }
715
716 #[test]
717 fn missing_digest_value() {
718 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
719 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
720 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
721 <Reference URI="">
722 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
723 </Reference>
724 </SignedInfo>"#;
725 let doc = Document::parse(xml).unwrap();
726 let result = parse_signed_info(doc.root_element());
727 assert!(matches!(
728 result.unwrap_err(),
729 ParseError::MissingElement {
730 element: "DigestValue"
731 }
732 ));
733 }
734
735 #[test]
736 fn invalid_base64_digest_value() {
737 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
738 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
739 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
740 <Reference URI="">
741 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
742 <DigestValue>!!!not-base64!!!</DigestValue>
743 </Reference>
744 </SignedInfo>"#;
745 let doc = Document::parse(xml).unwrap();
746 let result = parse_signed_info(doc.root_element());
747 assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
748 }
749
750 #[test]
751 fn digest_value_length_must_match_digest_method() {
752 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
753 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
754 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
755 <Reference URI="">
756 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
757 <DigestValue>dGVzdA==</DigestValue>
758 </Reference>
759 </SignedInfo>"#;
760 let doc = Document::parse(xml).unwrap();
761
762 let result = parse_signed_info(doc.root_element());
763 assert!(matches!(
764 result.unwrap_err(),
765 ParseError::DigestLengthMismatch {
766 algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
767 expected: 32,
768 actual: 4,
769 }
770 ));
771 }
772
773 #[test]
774 fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
775 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
776 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
777 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
778 <ec:InclusiveNamespaces PrefixList="ds"/>
779 </CanonicalizationMethod>
780 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
781 <Reference URI="">
782 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
783 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
784 </Reference>
785 </SignedInfo>"#;
786 let doc = Document::parse(xml).unwrap();
787
788 let result = parse_signed_info(doc.root_element());
789 assert!(matches!(
790 result.unwrap_err(),
791 ParseError::UnsupportedAlgorithm { .. }
792 ));
793 }
794
795 #[test]
796 fn extra_element_after_digest_value() {
797 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
798 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
799 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
800 <Reference URI="">
801 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
802 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
803 <Unexpected/>
804 </Reference>
805 </SignedInfo>"#;
806 let doc = Document::parse(xml).unwrap();
807 let result = parse_signed_info(doc.root_element());
808 assert!(matches!(
809 result.unwrap_err(),
810 ParseError::InvalidStructure(_)
811 ));
812 }
813
814 #[test]
815 fn wrong_namespace_on_signed_info() {
816 let xml = r#"<SignedInfo xmlns="http://example.com/fake">
817 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
818 </SignedInfo>"#;
819 let doc = Document::parse(xml).unwrap();
820 let result = parse_signed_info(doc.root_element());
821 assert!(matches!(
822 result.unwrap_err(),
823 ParseError::InvalidStructure(_)
824 ));
825 }
826
827 #[test]
830 fn base64_with_whitespace() {
831 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
832 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
833 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
834 <Reference URI="">
835 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
836 <DigestValue>
837 AAAAAAAA
838 AAAAAAAAAAAAAAAAAAA=
839 </DigestValue>
840 </Reference>
841 </SignedInfo>"#;
842 let doc = Document::parse(xml).unwrap();
843 let si = parse_signed_info(doc.root_element()).unwrap();
844 assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
845 }
846
847 #[test]
850 fn saml_response_signed_info() {
851 let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
852 <ds:SignedInfo>
853 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
854 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
855 <ds:Reference URI="#_resp1">
856 <ds:Transforms>
857 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
858 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
859 </ds:Transforms>
860 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
861 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
862 </ds:Reference>
863 </ds:SignedInfo>
864 <ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
865 </ds:Signature>"##;
866 let doc = Document::parse(xml).unwrap();
867
868 let sig_node = doc.root_element();
870 let signed_info_node = sig_node
871 .children()
872 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
873 .unwrap();
874
875 let si = parse_signed_info(signed_info_node).unwrap();
876 assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
877 assert_eq!(si.references.len(), 1);
878 assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
879 assert_eq!(si.references[0].transforms.len(), 2);
880 assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
881 }
882}