Skip to main content

xml_sec/xmldsig/
parse.rs

1//! Parsing of XMLDSig `<Signature>` and `<SignedInfo>` elements.
2//!
3//! Implements strict child order enforcement per
4//! [XMLDSig §4.1](https://www.w3.org/TR/xmldsig-core1/#sec-Signature):
5//!
6//! ```text
7//! <Signature>
8//!   <SignedInfo>
9//!     <CanonicalizationMethod Algorithm="..."/>
10//!     <SignatureMethod Algorithm="..."/>
11//!     <Reference URI="..." Id="..." Type="...">+
12//!   </SignedInfo>
13//!   <SignatureValue>...</SignatureValue>
14//!   <KeyInfo>?
15//!   <Object>*
16//! </Signature>
17//! ```
18
19use roxmltree::{Document, Node};
20
21use super::digest::DigestAlgorithm;
22use super::transforms::{self, Transform};
23use crate::c14n::C14nAlgorithm;
24
25/// XMLDSig namespace URI.
26pub(crate) const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
27
28/// Signature algorithms supported for signing and verification.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum SignatureAlgorithm {
31    /// RSA with SHA-1. **Verify-only** — signing disabled.
32    RsaSha1,
33    /// RSA with SHA-256 (most common in SAML).
34    RsaSha256,
35    /// RSA with SHA-384.
36    RsaSha384,
37    /// RSA with SHA-512.
38    RsaSha512,
39    /// ECDSA P-256 with SHA-256.
40    EcdsaP256Sha256,
41    /// ECDSA P-384 with SHA-384.
42    EcdsaP384Sha384,
43}
44
45impl SignatureAlgorithm {
46    /// Parse from an XML algorithm URI.
47    #[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    /// Return the XML namespace URI.
61    #[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    /// Whether this algorithm is allowed for signing (not just verification).
74    #[must_use]
75    pub fn signing_allowed(self) -> bool {
76        !matches!(self, Self::RsaSha1)
77    }
78}
79
80/// Parsed `<SignedInfo>` element.
81#[derive(Debug)]
82pub struct SignedInfo {
83    /// Canonicalization method for SignedInfo itself.
84    pub c14n_method: C14nAlgorithm,
85    /// Signature algorithm.
86    pub signature_method: SignatureAlgorithm,
87    /// One or more `<Reference>` elements.
88    pub references: Vec<Reference>,
89}
90
91/// Parsed `<Reference>` element.
92#[derive(Debug)]
93pub struct Reference {
94    /// URI attribute (e.g., `""`, `"#_assert1"`).
95    pub uri: Option<String>,
96    /// Id attribute.
97    pub id: Option<String>,
98    /// Type attribute.
99    pub ref_type: Option<String>,
100    /// Transform chain.
101    pub transforms: Vec<Transform>,
102    /// Digest algorithm.
103    pub digest_method: DigestAlgorithm,
104    /// Raw digest value (base64-decoded).
105    pub digest_value: Vec<u8>,
106}
107
108/// Errors during XMLDSig element parsing.
109#[derive(Debug, thiserror::Error)]
110#[non_exhaustive]
111pub enum ParseError {
112    /// Missing required element.
113    #[error("missing required element: <{element}>")]
114    MissingElement {
115        /// Name of the missing element.
116        element: &'static str,
117    },
118
119    /// Invalid structure (wrong child order, unexpected element, etc.).
120    #[error("invalid structure: {0}")]
121    InvalidStructure(String),
122
123    /// Unsupported algorithm URI.
124    #[error("unsupported algorithm: {uri}")]
125    UnsupportedAlgorithm {
126        /// The unrecognized algorithm URI.
127        uri: String,
128    },
129
130    /// Base64 decode error.
131    #[error("base64 decode error: {0}")]
132    Base64(String),
133
134    /// DigestValue length did not match the declared DigestMethod.
135    #[error(
136        "digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
137    )]
138    DigestLengthMismatch {
139        /// Digest algorithm URI/name used for diagnostics.
140        algorithm: &'static str,
141        /// Expected decoded digest length in bytes.
142        expected: usize,
143        /// Actual decoded digest length in bytes.
144        actual: usize,
145    },
146
147    /// Transform parsing error.
148    #[error("transform error: {0}")]
149    Transform(#[from] super::types::TransformError),
150}
151
152/// Find the first `<ds:Signature>` element in the document.
153#[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
162/// Parse a `<ds:SignedInfo>` element.
163///
164/// Enforces strict child order per XMLDSig spec:
165/// `<CanonicalizationMethod>` → `<SignatureMethod>` → `<Reference>`+
166pub 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    // 1. CanonicalizationMethod (required, first)
172    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    // 2. SignatureMethod (required, second)
192    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    // 3. One or more Reference elements
203    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
221/// Parse a single `<ds:Reference>` element.
222///
223/// Structure: `<Transforms>?` → `<DigestMethod>` → `<DigestValue>`
224fn 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    // Optional <Transforms>
232    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    // Required <DigestMethod>
245    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    // Required <DigestValue>
253    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    // No more children expected
273    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
290// ── Helpers ──────────────────────────────────────────────────────────────────
291
292/// Iterate only element children (skip text, comments, PIs).
293fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
294    node.children().filter(|n| n.is_element())
295}
296
297/// Verify that a node is a `<ds:{expected_name}>` element.
298fn 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
317/// Get the required `Algorithm` attribute from an element.
318fn 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
327/// Parse the `PrefixList` attribute from an `<ec:InclusiveNamespaces>` child of
328/// `<CanonicalizationMethod>`, if present.
329///
330/// This mirrors transform parsing for Exclusive C14N and keeps SignedInfo
331/// canonicalization parameters lossless.
332fn 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
356/// Base64-decode a digest value string, stripping whitespace.
357///
358/// XMLDSig allows whitespace within base64 content (line-wrapped encodings).
359fn 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    // ── SignatureAlgorithm ───────────────────────────────────────────
397
398    #[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    // ── find_signature_node ──────────────────────────────────────────
456
457    #[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    // ── parse_signed_info: happy path ────────────────────────────────
485
486    #[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        // Transforms element is optional
541        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        // URI attribute is optional per spec
580        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    // ── parse_signed_info: error cases ───────────────────────────────
615
616    #[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        // SignatureMethod is first child but expected CanonicalizationMethod
629        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        // Reference is second child but expected SignatureMethod
648        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        // DigestValue is not DigestMethod
736        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    // ── Whitespace-wrapped base64 ────────────────────────────────────
870
871    #[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    // ── Real-world SAML structure ────────────────────────────────────
908
909    #[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        // Find SignedInfo within Signature
929        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}