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