1use roxmltree::{Document, Node};
20
21use super::digest::DigestAlgorithm;
22use super::transforms::{self, Transform};
23use crate::c14n::C14nAlgorithm;
24
25pub(crate) const 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 mut digest_b64 = String::new();
258 for child in digest_value_node.children() {
259 if child.is_element() {
260 return Err(ParseError::InvalidStructure(
261 "DigestValue must not contain element children".into(),
262 ));
263 }
264 if child.is_text()
265 && let Some(text) = child.text()
266 {
267 digest_b64.push_str(text);
268 }
269 }
270 let digest_value = base64_decode_digest(&digest_b64, digest_method)?;
271
272 if let Some(unexpected) = children.next() {
274 return Err(ParseError::InvalidStructure(format!(
275 "unexpected element <{}> after <DigestValue> in <Reference>",
276 unexpected.tag_name().name()
277 )));
278 }
279
280 Ok(Reference {
281 uri,
282 id,
283 ref_type,
284 transforms,
285 digest_method,
286 digest_value,
287 })
288}
289
290fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
294 node.children().filter(|n| n.is_element())
295}
296
297fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
299 if !node.is_element() {
300 return Err(ParseError::InvalidStructure(format!(
301 "expected element <{expected_name}>, got non-element node"
302 )));
303 }
304 let tag = node.tag_name();
305 if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
306 return Err(ParseError::InvalidStructure(format!(
307 "expected <ds:{expected_name}>, got <{}{}>",
308 tag.namespace()
309 .map(|ns| format!("{{{ns}}}"))
310 .unwrap_or_default(),
311 tag.name()
312 )));
313 }
314 Ok(())
315}
316
317fn required_algorithm_attr<'a>(
319 node: Node<'a, 'a>,
320 element_name: &'static str,
321) -> Result<&'a str, ParseError> {
322 node.attribute("Algorithm").ok_or_else(|| {
323 ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
324 })
325}
326
327fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
333 const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
334
335 for child in node.children() {
336 if child.is_element() {
337 let tag = child.tag_name();
338 if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
339 {
340 return child
341 .attribute("PrefixList")
342 .map(str::to_string)
343 .ok_or_else(|| {
344 ParseError::InvalidStructure(
345 "missing PrefixList attribute on <InclusiveNamespaces>".into(),
346 )
347 })
348 .map(Some);
349 }
350 }
351 }
352
353 Ok(None)
354}
355
356fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
360 use base64::Engine;
361 use base64::engine::general_purpose::STANDARD;
362
363 let mut cleaned = String::with_capacity(b64.len());
364 for ch in b64.chars() {
365 if matches!(ch, ' ' | '\t' | '\r' | '\n') {
366 continue;
367 }
368 if ch.is_ascii_whitespace() {
369 return Err(ParseError::Base64(format!(
370 "invalid XML whitespace U+{:04X} in DigestValue",
371 u32::from(ch)
372 )));
373 }
374 cleaned.push(ch);
375 }
376 let digest = STANDARD
377 .decode(&cleaned)
378 .map_err(|e| ParseError::Base64(e.to_string()))?;
379 let expected = digest_method.output_len();
380 let actual = digest.len();
381 if actual != expected {
382 return Err(ParseError::DigestLengthMismatch {
383 algorithm: digest_method.uri(),
384 expected,
385 actual,
386 });
387 }
388 Ok(digest)
389}
390
391#[cfg(test)]
392#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
393mod tests {
394 use super::*;
395
396 #[test]
399 fn signature_algorithm_from_uri_rsa_sha256() {
400 assert_eq!(
401 SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
402 Some(SignatureAlgorithm::RsaSha256)
403 );
404 }
405
406 #[test]
407 fn signature_algorithm_from_uri_rsa_sha1() {
408 assert_eq!(
409 SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
410 Some(SignatureAlgorithm::RsaSha1)
411 );
412 }
413
414 #[test]
415 fn signature_algorithm_from_uri_ecdsa_sha256() {
416 assert_eq!(
417 SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
418 Some(SignatureAlgorithm::EcdsaP256Sha256)
419 );
420 }
421
422 #[test]
423 fn signature_algorithm_from_uri_unknown() {
424 assert_eq!(
425 SignatureAlgorithm::from_uri("http://example.com/unknown"),
426 None
427 );
428 }
429
430 #[test]
431 fn signature_algorithm_uri_round_trip() {
432 for algo in [
433 SignatureAlgorithm::RsaSha1,
434 SignatureAlgorithm::RsaSha256,
435 SignatureAlgorithm::RsaSha384,
436 SignatureAlgorithm::RsaSha512,
437 SignatureAlgorithm::EcdsaP256Sha256,
438 SignatureAlgorithm::EcdsaP384Sha384,
439 ] {
440 assert_eq!(
441 SignatureAlgorithm::from_uri(algo.uri()),
442 Some(algo),
443 "round-trip failed for {algo:?}"
444 );
445 }
446 }
447
448 #[test]
449 fn rsa_sha1_verify_only() {
450 assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
451 assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
452 assert!(SignatureAlgorithm::EcdsaP256Sha256.signing_allowed());
453 }
454
455 #[test]
458 fn find_signature_in_saml() {
459 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
460 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
461 <ds:SignedInfo/>
462 </ds:Signature>
463 </samlp:Response>"#;
464 let doc = Document::parse(xml).unwrap();
465 let sig = find_signature_node(&doc);
466 assert!(sig.is_some());
467 assert_eq!(sig.unwrap().tag_name().name(), "Signature");
468 }
469
470 #[test]
471 fn find_signature_missing() {
472 let xml = "<root><child/></root>";
473 let doc = Document::parse(xml).unwrap();
474 assert!(find_signature_node(&doc).is_none());
475 }
476
477 #[test]
478 fn find_signature_ignores_wrong_namespace() {
479 let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
480 let doc = Document::parse(xml).unwrap();
481 assert!(find_signature_node(&doc).is_none());
482 }
483
484 #[test]
487 fn parse_signed_info_rsa_sha256_with_reference() {
488 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
489 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
490 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
491 <Reference URI="">
492 <Transforms>
493 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
494 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
495 </Transforms>
496 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
497 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
498 </Reference>
499 </SignedInfo>"#;
500 let doc = Document::parse(xml).unwrap();
501 let si = parse_signed_info(doc.root_element()).unwrap();
502
503 assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
504 assert_eq!(si.references.len(), 1);
505
506 let r = &si.references[0];
507 assert_eq!(r.uri.as_deref(), Some(""));
508 assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
509 assert_eq!(r.digest_value, vec![0u8; 32]);
510 assert_eq!(r.transforms.len(), 2);
511 }
512
513 #[test]
514 fn parse_signed_info_multiple_references() {
515 let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
516 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
517 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
518 <Reference URI="#a">
519 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
520 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
521 </Reference>
522 <Reference URI="#b">
523 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
524 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
525 </Reference>
526 </SignedInfo>"##;
527 let doc = Document::parse(xml).unwrap();
528 let si = parse_signed_info(doc.root_element()).unwrap();
529
530 assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaP256Sha256);
531 assert_eq!(si.references.len(), 2);
532 assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
533 assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
534 assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
535 assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
536 }
537
538 #[test]
539 fn parse_reference_without_transforms() {
540 let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
542 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
543 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
544 <Reference URI="#obj">
545 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
546 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
547 </Reference>
548 </SignedInfo>"##;
549 let doc = Document::parse(xml).unwrap();
550 let si = parse_signed_info(doc.root_element()).unwrap();
551
552 assert!(si.references[0].transforms.is_empty());
553 }
554
555 #[test]
556 fn parse_reference_with_all_attributes() {
557 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 URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
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 let r = &si.references[0];
568
569 assert_eq!(r.uri.as_deref(), Some("#data"));
570 assert_eq!(r.id.as_deref(), Some("ref1"));
571 assert_eq!(
572 r.ref_type.as_deref(),
573 Some("http://www.w3.org/2000/09/xmldsig#Object")
574 );
575 }
576
577 #[test]
578 fn parse_reference_absent_uri() {
579 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
581 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
582 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
583 <Reference>
584 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
585 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
586 </Reference>
587 </SignedInfo>"#;
588 let doc = Document::parse(xml).unwrap();
589 let si = parse_signed_info(doc.root_element()).unwrap();
590 assert!(si.references[0].uri.is_none());
591 }
592
593 #[test]
594 fn parse_signed_info_preserves_inclusive_prefixes() {
595 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
596 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
597 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
598 <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
599 </CanonicalizationMethod>
600 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
601 <Reference URI="">
602 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
603 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
604 </Reference>
605 </SignedInfo>"#;
606 let doc = Document::parse(xml).unwrap();
607
608 let si = parse_signed_info(doc.root_element()).unwrap();
609 assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
610 assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
611 assert!(si.c14n_method.inclusive_prefixes().contains(""));
612 }
613
614 #[test]
617 fn missing_canonicalization_method() {
618 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
619 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
620 <Reference URI="">
621 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
622 <DigestValue>dGVzdA==</DigestValue>
623 </Reference>
624 </SignedInfo>"#;
625 let doc = Document::parse(xml).unwrap();
626 let result = parse_signed_info(doc.root_element());
627 assert!(result.is_err());
628 assert!(matches!(
630 result.unwrap_err(),
631 ParseError::InvalidStructure(_)
632 ));
633 }
634
635 #[test]
636 fn missing_signature_method() {
637 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
638 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
639 <Reference URI="">
640 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
641 <DigestValue>dGVzdA==</DigestValue>
642 </Reference>
643 </SignedInfo>"#;
644 let doc = Document::parse(xml).unwrap();
645 let result = parse_signed_info(doc.root_element());
646 assert!(result.is_err());
647 assert!(matches!(
649 result.unwrap_err(),
650 ParseError::InvalidStructure(_)
651 ));
652 }
653
654 #[test]
655 fn no_references() {
656 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
657 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
658 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
659 </SignedInfo>"#;
660 let doc = Document::parse(xml).unwrap();
661 let result = parse_signed_info(doc.root_element());
662 assert!(matches!(
663 result.unwrap_err(),
664 ParseError::MissingElement {
665 element: "Reference"
666 }
667 ));
668 }
669
670 #[test]
671 fn unsupported_c14n_algorithm() {
672 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
673 <CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
674 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
675 <Reference URI="">
676 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
677 <DigestValue>dGVzdA==</DigestValue>
678 </Reference>
679 </SignedInfo>"#;
680 let doc = Document::parse(xml).unwrap();
681 let result = parse_signed_info(doc.root_element());
682 assert!(matches!(
683 result.unwrap_err(),
684 ParseError::UnsupportedAlgorithm { .. }
685 ));
686 }
687
688 #[test]
689 fn unsupported_signature_algorithm() {
690 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
691 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
692 <SignatureMethod Algorithm="http://example.com/bogus-sign"/>
693 <Reference URI="">
694 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
695 <DigestValue>dGVzdA==</DigestValue>
696 </Reference>
697 </SignedInfo>"#;
698 let doc = Document::parse(xml).unwrap();
699 let result = parse_signed_info(doc.root_element());
700 assert!(matches!(
701 result.unwrap_err(),
702 ParseError::UnsupportedAlgorithm { .. }
703 ));
704 }
705
706 #[test]
707 fn unsupported_digest_algorithm() {
708 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
709 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
710 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
711 <Reference URI="">
712 <DigestMethod Algorithm="http://example.com/bogus-digest"/>
713 <DigestValue>dGVzdA==</DigestValue>
714 </Reference>
715 </SignedInfo>"#;
716 let doc = Document::parse(xml).unwrap();
717 let result = parse_signed_info(doc.root_element());
718 assert!(matches!(
719 result.unwrap_err(),
720 ParseError::UnsupportedAlgorithm { .. }
721 ));
722 }
723
724 #[test]
725 fn missing_digest_method() {
726 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
727 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
728 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
729 <Reference URI="">
730 <DigestValue>dGVzdA==</DigestValue>
731 </Reference>
732 </SignedInfo>"#;
733 let doc = Document::parse(xml).unwrap();
734 let result = parse_signed_info(doc.root_element());
735 assert!(result.is_err());
737 }
738
739 #[test]
740 fn missing_digest_value() {
741 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
742 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
743 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
744 <Reference URI="">
745 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
746 </Reference>
747 </SignedInfo>"#;
748 let doc = Document::parse(xml).unwrap();
749 let result = parse_signed_info(doc.root_element());
750 assert!(matches!(
751 result.unwrap_err(),
752 ParseError::MissingElement {
753 element: "DigestValue"
754 }
755 ));
756 }
757
758 #[test]
759 fn invalid_base64_digest_value() {
760 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
761 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
762 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
763 <Reference URI="">
764 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
765 <DigestValue>!!!not-base64!!!</DigestValue>
766 </Reference>
767 </SignedInfo>"#;
768 let doc = Document::parse(xml).unwrap();
769 let result = parse_signed_info(doc.root_element());
770 assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
771 }
772
773 #[test]
774 fn digest_value_length_must_match_digest_method() {
775 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
776 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
777 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
778 <Reference URI="">
779 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
780 <DigestValue>dGVzdA==</DigestValue>
781 </Reference>
782 </SignedInfo>"#;
783 let doc = Document::parse(xml).unwrap();
784
785 let result = parse_signed_info(doc.root_element());
786 assert!(matches!(
787 result.unwrap_err(),
788 ParseError::DigestLengthMismatch {
789 algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
790 expected: 32,
791 actual: 4,
792 }
793 ));
794 }
795
796 #[test]
797 fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
798 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
799 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
800 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
801 <ec:InclusiveNamespaces PrefixList="ds"/>
802 </CanonicalizationMethod>
803 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
804 <Reference URI="">
805 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
806 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
807 </Reference>
808 </SignedInfo>"#;
809 let doc = Document::parse(xml).unwrap();
810
811 let result = parse_signed_info(doc.root_element());
812 assert!(matches!(
813 result.unwrap_err(),
814 ParseError::UnsupportedAlgorithm { .. }
815 ));
816 }
817
818 #[test]
819 fn extra_element_after_digest_value() {
820 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
821 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
822 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
823 <Reference URI="">
824 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
825 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
826 <Unexpected/>
827 </Reference>
828 </SignedInfo>"#;
829 let doc = Document::parse(xml).unwrap();
830 let result = parse_signed_info(doc.root_element());
831 assert!(matches!(
832 result.unwrap_err(),
833 ParseError::InvalidStructure(_)
834 ));
835 }
836
837 #[test]
838 fn digest_value_with_element_child_is_rejected() {
839 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
840 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
841 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
842 <Reference URI="">
843 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
844 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
845 </Reference>
846 </SignedInfo>"#;
847 let doc = Document::parse(xml).unwrap();
848
849 let result = parse_signed_info(doc.root_element());
850 assert!(matches!(
851 result.unwrap_err(),
852 ParseError::InvalidStructure(_)
853 ));
854 }
855
856 #[test]
857 fn wrong_namespace_on_signed_info() {
858 let xml = r#"<SignedInfo xmlns="http://example.com/fake">
859 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
860 </SignedInfo>"#;
861 let doc = Document::parse(xml).unwrap();
862 let result = parse_signed_info(doc.root_element());
863 assert!(matches!(
864 result.unwrap_err(),
865 ParseError::InvalidStructure(_)
866 ));
867 }
868
869 #[test]
872 fn base64_with_whitespace() {
873 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
874 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
875 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
876 <Reference URI="">
877 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
878 <DigestValue>
879 AAAAAAAA
880 AAAAAAAAAAAAAAAAAAA=
881 </DigestValue>
882 </Reference>
883 </SignedInfo>"#;
884 let doc = Document::parse(xml).unwrap();
885 let si = parse_signed_info(doc.root_element()).unwrap();
886 assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
887 }
888
889 #[test]
890 fn base64_decode_digest_accepts_xml_whitespace_chars() {
891 let digest =
892 base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
893 .expect("XML whitespace in DigestValue must be accepted");
894 assert_eq!(digest, vec![0u8; 20]);
895 }
896
897 #[test]
898 fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
899 let err = base64_decode_digest(
900 "AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
901 DigestAlgorithm::Sha1,
902 )
903 .expect_err("form-feed/vertical-tab in DigestValue must be rejected");
904 assert!(matches!(err, ParseError::Base64(_)));
905 }
906
907 #[test]
910 fn saml_response_signed_info() {
911 let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
912 <ds:SignedInfo>
913 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
914 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
915 <ds:Reference URI="#_resp1">
916 <ds:Transforms>
917 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
918 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
919 </ds:Transforms>
920 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
921 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
922 </ds:Reference>
923 </ds:SignedInfo>
924 <ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
925 </ds:Signature>"##;
926 let doc = Document::parse(xml).unwrap();
927
928 let sig_node = doc.root_element();
930 let signed_info_node = sig_node
931 .children()
932 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
933 .unwrap();
934
935 let si = parse_signed_info(signed_info_node).unwrap();
936 assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
937 assert_eq!(si.references.len(), 1);
938 assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
939 assert_eq!(si.references[0].transforms.len(), 2);
940 assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
941 }
942}