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,
48}
49
50impl SignatureAlgorithm {
51 #[must_use]
53 pub fn from_uri(uri: &str) -> Option<Self> {
54 match uri {
55 "http://www.w3.org/2000/09/xmldsig#rsa-sha1" => Some(Self::RsaSha1),
56 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Some(Self::RsaSha256),
57 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => Some(Self::RsaSha384),
58 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => Some(Self::RsaSha512),
59 "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Some(Self::EcdsaP256Sha256),
60 "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => Some(Self::EcdsaP384Sha384),
61 _ => None,
62 }
63 }
64
65 #[must_use]
67 pub fn uri(self) -> &'static str {
68 match self {
69 Self::RsaSha1 => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
70 Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
71 Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
72 Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
73 Self::EcdsaP256Sha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
74 Self::EcdsaP384Sha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
75 }
76 }
77
78 #[must_use]
80 pub fn signing_allowed(self) -> bool {
81 !matches!(self, Self::RsaSha1)
82 }
83}
84
85#[derive(Debug)]
87pub struct SignedInfo {
88 pub c14n_method: C14nAlgorithm,
90 pub signature_method: SignatureAlgorithm,
92 pub references: Vec<Reference>,
94}
95
96#[derive(Debug)]
98pub struct Reference {
99 pub uri: Option<String>,
101 pub id: Option<String>,
103 pub ref_type: Option<String>,
105 pub transforms: Vec<Transform>,
107 pub digest_method: DigestAlgorithm,
109 pub digest_value: Vec<u8>,
111}
112
113#[derive(Debug, thiserror::Error)]
115#[non_exhaustive]
116pub enum ParseError {
117 #[error("missing required element: <{element}>")]
119 MissingElement {
120 element: &'static str,
122 },
123
124 #[error("invalid structure: {0}")]
126 InvalidStructure(String),
127
128 #[error("unsupported algorithm: {uri}")]
130 UnsupportedAlgorithm {
131 uri: String,
133 },
134
135 #[error("base64 decode error: {0}")]
137 Base64(String),
138
139 #[error(
141 "digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
142 )]
143 DigestLengthMismatch {
144 algorithm: &'static str,
146 expected: usize,
148 actual: usize,
150 },
151
152 #[error("transform error: {0}")]
154 Transform(#[from] super::types::TransformError),
155}
156
157#[must_use]
159pub fn find_signature_node<'a>(doc: &'a Document<'a>) -> Option<Node<'a, 'a>> {
160 doc.descendants().find(|n| {
161 n.is_element()
162 && n.tag_name().name() == "Signature"
163 && n.tag_name().namespace() == Some(XMLDSIG_NS)
164 })
165}
166
167pub fn parse_signed_info(signed_info_node: Node) -> Result<SignedInfo, ParseError> {
172 verify_ds_element(signed_info_node, "SignedInfo")?;
173
174 let mut children = element_children(signed_info_node);
175
176 let c14n_node = children.next().ok_or(ParseError::MissingElement {
178 element: "CanonicalizationMethod",
179 })?;
180 verify_ds_element(c14n_node, "CanonicalizationMethod")?;
181 let c14n_uri = required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
182 let mut c14n_method =
183 C14nAlgorithm::from_uri(c14n_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
184 uri: c14n_uri.to_string(),
185 })?;
186 if let Some(prefix_list) = parse_inclusive_prefixes(c14n_node)? {
187 if c14n_method.mode() == crate::c14n::C14nMode::Exclusive1_0 {
188 c14n_method = c14n_method.with_prefix_list(&prefix_list);
189 } else {
190 return Err(ParseError::UnsupportedAlgorithm {
191 uri: c14n_uri.to_string(),
192 });
193 }
194 }
195
196 let sig_method_node = children.next().ok_or(ParseError::MissingElement {
198 element: "SignatureMethod",
199 })?;
200 verify_ds_element(sig_method_node, "SignatureMethod")?;
201 let sig_uri = required_algorithm_attr(sig_method_node, "SignatureMethod")?;
202 let signature_method =
203 SignatureAlgorithm::from_uri(sig_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
204 uri: sig_uri.to_string(),
205 })?;
206
207 let mut references = Vec::new();
209 for child in children {
210 verify_ds_element(child, "Reference")?;
211 references.push(parse_reference(child)?);
212 }
213 if references.is_empty() {
214 return Err(ParseError::MissingElement {
215 element: "Reference",
216 });
217 }
218
219 Ok(SignedInfo {
220 c14n_method,
221 signature_method,
222 references,
223 })
224}
225
226pub(crate) fn parse_reference(reference_node: Node) -> Result<Reference, ParseError> {
230 let uri = reference_node.attribute("URI").map(String::from);
231 let id = reference_node.attribute("Id").map(String::from);
232 let ref_type = reference_node.attribute("Type").map(String::from);
233
234 let mut children = element_children(reference_node);
235
236 let mut transforms = Vec::new();
238 let mut next = children.next().ok_or(ParseError::MissingElement {
239 element: "DigestMethod",
240 })?;
241
242 if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) {
243 transforms = transforms::parse_transforms(next)?;
244 next = children.next().ok_or(ParseError::MissingElement {
245 element: "DigestMethod",
246 })?;
247 }
248
249 verify_ds_element(next, "DigestMethod")?;
251 let digest_uri = required_algorithm_attr(next, "DigestMethod")?;
252 let digest_method =
253 DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
254 uri: digest_uri.to_string(),
255 })?;
256
257 let digest_value_node = children.next().ok_or(ParseError::MissingElement {
259 element: "DigestValue",
260 })?;
261 verify_ds_element(digest_value_node, "DigestValue")?;
262 let mut digest_b64 = String::new();
263 for child in digest_value_node.children() {
264 if child.is_element() {
265 return Err(ParseError::InvalidStructure(
266 "DigestValue must not contain element children".into(),
267 ));
268 }
269 if child.is_text()
270 && let Some(text) = child.text()
271 {
272 digest_b64.push_str(text);
273 }
274 }
275 let digest_value = base64_decode_digest(&digest_b64, digest_method)?;
276
277 if let Some(unexpected) = children.next() {
279 return Err(ParseError::InvalidStructure(format!(
280 "unexpected element <{}> after <DigestValue> in <Reference>",
281 unexpected.tag_name().name()
282 )));
283 }
284
285 Ok(Reference {
286 uri,
287 id,
288 ref_type,
289 transforms,
290 digest_method,
291 digest_value,
292 })
293}
294
295fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
299 node.children().filter(|n| n.is_element())
300}
301
302fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
304 if !node.is_element() {
305 return Err(ParseError::InvalidStructure(format!(
306 "expected element <{expected_name}>, got non-element node"
307 )));
308 }
309 let tag = node.tag_name();
310 if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
311 return Err(ParseError::InvalidStructure(format!(
312 "expected <ds:{expected_name}>, got <{}{}>",
313 tag.namespace()
314 .map(|ns| format!("{{{ns}}}"))
315 .unwrap_or_default(),
316 tag.name()
317 )));
318 }
319 Ok(())
320}
321
322fn required_algorithm_attr<'a>(
324 node: Node<'a, 'a>,
325 element_name: &'static str,
326) -> Result<&'a str, ParseError> {
327 node.attribute("Algorithm").ok_or_else(|| {
328 ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
329 })
330}
331
332fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
338 const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
339
340 for child in node.children() {
341 if child.is_element() {
342 let tag = child.tag_name();
343 if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
344 {
345 return child
346 .attribute("PrefixList")
347 .map(str::to_string)
348 .ok_or_else(|| {
349 ParseError::InvalidStructure(
350 "missing PrefixList attribute on <InclusiveNamespaces>".into(),
351 )
352 })
353 .map(Some);
354 }
355 }
356 }
357
358 Ok(None)
359}
360
361fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
365 use base64::Engine;
366 use base64::engine::general_purpose::STANDARD;
367
368 let mut cleaned = String::with_capacity(b64.len());
369 for ch in b64.chars() {
370 if matches!(ch, ' ' | '\t' | '\r' | '\n') {
371 continue;
372 }
373 if ch.is_ascii_whitespace() {
374 return Err(ParseError::Base64(format!(
375 "invalid XML whitespace U+{:04X} in DigestValue",
376 u32::from(ch)
377 )));
378 }
379 cleaned.push(ch);
380 }
381 let digest = STANDARD
382 .decode(&cleaned)
383 .map_err(|e| ParseError::Base64(e.to_string()))?;
384 let expected = digest_method.output_len();
385 let actual = digest.len();
386 if actual != expected {
387 return Err(ParseError::DigestLengthMismatch {
388 algorithm: digest_method.uri(),
389 expected,
390 actual,
391 });
392 }
393 Ok(digest)
394}
395
396#[cfg(test)]
397#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
398mod tests {
399 use super::*;
400
401 #[test]
404 fn signature_algorithm_from_uri_rsa_sha256() {
405 assert_eq!(
406 SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
407 Some(SignatureAlgorithm::RsaSha256)
408 );
409 }
410
411 #[test]
412 fn signature_algorithm_from_uri_rsa_sha1() {
413 assert_eq!(
414 SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
415 Some(SignatureAlgorithm::RsaSha1)
416 );
417 }
418
419 #[test]
420 fn signature_algorithm_from_uri_ecdsa_sha256() {
421 assert_eq!(
422 SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
423 Some(SignatureAlgorithm::EcdsaP256Sha256)
424 );
425 }
426
427 #[test]
428 fn signature_algorithm_from_uri_unknown() {
429 assert_eq!(
430 SignatureAlgorithm::from_uri("http://example.com/unknown"),
431 None
432 );
433 }
434
435 #[test]
436 fn signature_algorithm_uri_round_trip() {
437 for algo in [
438 SignatureAlgorithm::RsaSha1,
439 SignatureAlgorithm::RsaSha256,
440 SignatureAlgorithm::RsaSha384,
441 SignatureAlgorithm::RsaSha512,
442 SignatureAlgorithm::EcdsaP256Sha256,
443 SignatureAlgorithm::EcdsaP384Sha384,
444 ] {
445 assert_eq!(
446 SignatureAlgorithm::from_uri(algo.uri()),
447 Some(algo),
448 "round-trip failed for {algo:?}"
449 );
450 }
451 }
452
453 #[test]
454 fn rsa_sha1_verify_only() {
455 assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
456 assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
457 assert!(SignatureAlgorithm::EcdsaP256Sha256.signing_allowed());
458 }
459
460 #[test]
463 fn find_signature_in_saml() {
464 let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
465 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
466 <ds:SignedInfo/>
467 </ds:Signature>
468 </samlp:Response>"#;
469 let doc = Document::parse(xml).unwrap();
470 let sig = find_signature_node(&doc);
471 assert!(sig.is_some());
472 assert_eq!(sig.unwrap().tag_name().name(), "Signature");
473 }
474
475 #[test]
476 fn find_signature_missing() {
477 let xml = "<root><child/></root>";
478 let doc = Document::parse(xml).unwrap();
479 assert!(find_signature_node(&doc).is_none());
480 }
481
482 #[test]
483 fn find_signature_ignores_wrong_namespace() {
484 let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
485 let doc = Document::parse(xml).unwrap();
486 assert!(find_signature_node(&doc).is_none());
487 }
488
489 #[test]
492 fn parse_signed_info_rsa_sha256_with_reference() {
493 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
494 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
495 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
496 <Reference URI="">
497 <Transforms>
498 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
499 <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
500 </Transforms>
501 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
502 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
503 </Reference>
504 </SignedInfo>"#;
505 let doc = Document::parse(xml).unwrap();
506 let si = parse_signed_info(doc.root_element()).unwrap();
507
508 assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
509 assert_eq!(si.references.len(), 1);
510
511 let r = &si.references[0];
512 assert_eq!(r.uri.as_deref(), Some(""));
513 assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
514 assert_eq!(r.digest_value, vec![0u8; 32]);
515 assert_eq!(r.transforms.len(), 2);
516 }
517
518 #[test]
519 fn parse_signed_info_multiple_references() {
520 let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
521 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
522 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
523 <Reference URI="#a">
524 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
525 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
526 </Reference>
527 <Reference URI="#b">
528 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
529 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
530 </Reference>
531 </SignedInfo>"##;
532 let doc = Document::parse(xml).unwrap();
533 let si = parse_signed_info(doc.root_element()).unwrap();
534
535 assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaP256Sha256);
536 assert_eq!(si.references.len(), 2);
537 assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
538 assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
539 assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
540 assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
541 }
542
543 #[test]
544 fn parse_reference_without_transforms() {
545 let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
547 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
548 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
549 <Reference URI="#obj">
550 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
551 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
552 </Reference>
553 </SignedInfo>"##;
554 let doc = Document::parse(xml).unwrap();
555 let si = parse_signed_info(doc.root_element()).unwrap();
556
557 assert!(si.references[0].transforms.is_empty());
558 }
559
560 #[test]
561 fn parse_reference_with_all_attributes() {
562 let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
563 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
564 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
565 <Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
566 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
567 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
568 </Reference>
569 </SignedInfo>"##;
570 let doc = Document::parse(xml).unwrap();
571 let si = parse_signed_info(doc.root_element()).unwrap();
572 let r = &si.references[0];
573
574 assert_eq!(r.uri.as_deref(), Some("#data"));
575 assert_eq!(r.id.as_deref(), Some("ref1"));
576 assert_eq!(
577 r.ref_type.as_deref(),
578 Some("http://www.w3.org/2000/09/xmldsig#Object")
579 );
580 }
581
582 #[test]
583 fn parse_reference_absent_uri() {
584 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
586 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
587 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
588 <Reference>
589 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
590 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
591 </Reference>
592 </SignedInfo>"#;
593 let doc = Document::parse(xml).unwrap();
594 let si = parse_signed_info(doc.root_element()).unwrap();
595 assert!(si.references[0].uri.is_none());
596 }
597
598 #[test]
599 fn parse_signed_info_preserves_inclusive_prefixes() {
600 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
601 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
602 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
603 <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
604 </CanonicalizationMethod>
605 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
606 <Reference URI="">
607 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
608 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
609 </Reference>
610 </SignedInfo>"#;
611 let doc = Document::parse(xml).unwrap();
612
613 let si = parse_signed_info(doc.root_element()).unwrap();
614 assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
615 assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
616 assert!(si.c14n_method.inclusive_prefixes().contains(""));
617 }
618
619 #[test]
622 fn missing_canonicalization_method() {
623 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
624 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
625 <Reference URI="">
626 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
627 <DigestValue>dGVzdA==</DigestValue>
628 </Reference>
629 </SignedInfo>"#;
630 let doc = Document::parse(xml).unwrap();
631 let result = parse_signed_info(doc.root_element());
632 assert!(result.is_err());
633 assert!(matches!(
635 result.unwrap_err(),
636 ParseError::InvalidStructure(_)
637 ));
638 }
639
640 #[test]
641 fn missing_signature_method() {
642 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
643 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
644 <Reference URI="">
645 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
646 <DigestValue>dGVzdA==</DigestValue>
647 </Reference>
648 </SignedInfo>"#;
649 let doc = Document::parse(xml).unwrap();
650 let result = parse_signed_info(doc.root_element());
651 assert!(result.is_err());
652 assert!(matches!(
654 result.unwrap_err(),
655 ParseError::InvalidStructure(_)
656 ));
657 }
658
659 #[test]
660 fn no_references() {
661 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
662 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
663 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
664 </SignedInfo>"#;
665 let doc = Document::parse(xml).unwrap();
666 let result = parse_signed_info(doc.root_element());
667 assert!(matches!(
668 result.unwrap_err(),
669 ParseError::MissingElement {
670 element: "Reference"
671 }
672 ));
673 }
674
675 #[test]
676 fn unsupported_c14n_algorithm() {
677 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
678 <CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
679 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
680 <Reference URI="">
681 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
682 <DigestValue>dGVzdA==</DigestValue>
683 </Reference>
684 </SignedInfo>"#;
685 let doc = Document::parse(xml).unwrap();
686 let result = parse_signed_info(doc.root_element());
687 assert!(matches!(
688 result.unwrap_err(),
689 ParseError::UnsupportedAlgorithm { .. }
690 ));
691 }
692
693 #[test]
694 fn unsupported_signature_algorithm() {
695 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
696 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
697 <SignatureMethod Algorithm="http://example.com/bogus-sign"/>
698 <Reference URI="">
699 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
700 <DigestValue>dGVzdA==</DigestValue>
701 </Reference>
702 </SignedInfo>"#;
703 let doc = Document::parse(xml).unwrap();
704 let result = parse_signed_info(doc.root_element());
705 assert!(matches!(
706 result.unwrap_err(),
707 ParseError::UnsupportedAlgorithm { .. }
708 ));
709 }
710
711 #[test]
712 fn unsupported_digest_algorithm() {
713 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
714 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
715 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
716 <Reference URI="">
717 <DigestMethod Algorithm="http://example.com/bogus-digest"/>
718 <DigestValue>dGVzdA==</DigestValue>
719 </Reference>
720 </SignedInfo>"#;
721 let doc = Document::parse(xml).unwrap();
722 let result = parse_signed_info(doc.root_element());
723 assert!(matches!(
724 result.unwrap_err(),
725 ParseError::UnsupportedAlgorithm { .. }
726 ));
727 }
728
729 #[test]
730 fn missing_digest_method() {
731 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
732 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
733 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
734 <Reference URI="">
735 <DigestValue>dGVzdA==</DigestValue>
736 </Reference>
737 </SignedInfo>"#;
738 let doc = Document::parse(xml).unwrap();
739 let result = parse_signed_info(doc.root_element());
740 assert!(result.is_err());
742 }
743
744 #[test]
745 fn missing_digest_value() {
746 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
747 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
748 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
749 <Reference URI="">
750 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
751 </Reference>
752 </SignedInfo>"#;
753 let doc = Document::parse(xml).unwrap();
754 let result = parse_signed_info(doc.root_element());
755 assert!(matches!(
756 result.unwrap_err(),
757 ParseError::MissingElement {
758 element: "DigestValue"
759 }
760 ));
761 }
762
763 #[test]
764 fn invalid_base64_digest_value() {
765 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
766 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
767 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
768 <Reference URI="">
769 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
770 <DigestValue>!!!not-base64!!!</DigestValue>
771 </Reference>
772 </SignedInfo>"#;
773 let doc = Document::parse(xml).unwrap();
774 let result = parse_signed_info(doc.root_element());
775 assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
776 }
777
778 #[test]
779 fn digest_value_length_must_match_digest_method() {
780 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
781 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
782 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
783 <Reference URI="">
784 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
785 <DigestValue>dGVzdA==</DigestValue>
786 </Reference>
787 </SignedInfo>"#;
788 let doc = Document::parse(xml).unwrap();
789
790 let result = parse_signed_info(doc.root_element());
791 assert!(matches!(
792 result.unwrap_err(),
793 ParseError::DigestLengthMismatch {
794 algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
795 expected: 32,
796 actual: 4,
797 }
798 ));
799 }
800
801 #[test]
802 fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
803 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
804 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
805 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
806 <ec:InclusiveNamespaces PrefixList="ds"/>
807 </CanonicalizationMethod>
808 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
809 <Reference URI="">
810 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
811 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
812 </Reference>
813 </SignedInfo>"#;
814 let doc = Document::parse(xml).unwrap();
815
816 let result = parse_signed_info(doc.root_element());
817 assert!(matches!(
818 result.unwrap_err(),
819 ParseError::UnsupportedAlgorithm { .. }
820 ));
821 }
822
823 #[test]
824 fn extra_element_after_digest_value() {
825 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
826 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
827 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
828 <Reference URI="">
829 <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
830 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
831 <Unexpected/>
832 </Reference>
833 </SignedInfo>"#;
834 let doc = Document::parse(xml).unwrap();
835 let result = parse_signed_info(doc.root_element());
836 assert!(matches!(
837 result.unwrap_err(),
838 ParseError::InvalidStructure(_)
839 ));
840 }
841
842 #[test]
843 fn digest_value_with_element_child_is_rejected() {
844 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
845 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
846 <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
847 <Reference URI="">
848 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
849 <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
850 </Reference>
851 </SignedInfo>"#;
852 let doc = Document::parse(xml).unwrap();
853
854 let result = parse_signed_info(doc.root_element());
855 assert!(matches!(
856 result.unwrap_err(),
857 ParseError::InvalidStructure(_)
858 ));
859 }
860
861 #[test]
862 fn wrong_namespace_on_signed_info() {
863 let xml = r#"<SignedInfo xmlns="http://example.com/fake">
864 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
865 </SignedInfo>"#;
866 let doc = Document::parse(xml).unwrap();
867 let result = parse_signed_info(doc.root_element());
868 assert!(matches!(
869 result.unwrap_err(),
870 ParseError::InvalidStructure(_)
871 ));
872 }
873
874 #[test]
877 fn base64_with_whitespace() {
878 let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
879 <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
880 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
881 <Reference URI="">
882 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
883 <DigestValue>
884 AAAAAAAA
885 AAAAAAAAAAAAAAAAAAA=
886 </DigestValue>
887 </Reference>
888 </SignedInfo>"#;
889 let doc = Document::parse(xml).unwrap();
890 let si = parse_signed_info(doc.root_element()).unwrap();
891 assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
892 }
893
894 #[test]
895 fn base64_decode_digest_accepts_xml_whitespace_chars() {
896 let digest =
897 base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
898 .expect("XML whitespace in DigestValue must be accepted");
899 assert_eq!(digest, vec![0u8; 20]);
900 }
901
902 #[test]
903 fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
904 let err = base64_decode_digest(
905 "AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
906 DigestAlgorithm::Sha1,
907 )
908 .expect_err("form-feed/vertical-tab in DigestValue must be rejected");
909 assert!(matches!(err, ParseError::Base64(_)));
910 }
911
912 #[test]
915 fn saml_response_signed_info() {
916 let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
917 <ds:SignedInfo>
918 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
919 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
920 <ds:Reference URI="#_resp1">
921 <ds:Transforms>
922 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
923 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
924 </ds:Transforms>
925 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
926 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
927 </ds:Reference>
928 </ds:SignedInfo>
929 <ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
930 </ds:Signature>"##;
931 let doc = Document::parse(xml).unwrap();
932
933 let sig_node = doc.root_element();
935 let signed_info_node = sig_node
936 .children()
937 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
938 .unwrap();
939
940 let si = parse_signed_info(signed_info_node).unwrap();
941 assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
942 assert_eq!(si.references.len(), 1);
943 assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
944 assert_eq!(si.references[0].transforms.len(), 2);
945 assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
946 }
947}