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};
20use x509_parser::extensions::ParsedExtension;
21use x509_parser::prelude::FromDer;
22use x509_parser::public_key::PublicKey;
23
24use super::digest::DigestAlgorithm;
25use super::transforms::{self, Transform};
26use super::whitespace::{is_xml_whitespace_only, normalize_xml_base64_text};
27use crate::c14n::C14nAlgorithm;
28
29/// XMLDSig namespace URI.
30pub(crate) const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
31/// XMLDSig 1.1 namespace URI.
32pub(crate) const XMLDSIG11_NS: &str = "http://www.w3.org/2009/xmldsig11#";
33const MAX_DER_ENCODED_KEY_VALUE_LEN: usize = 8192;
34const MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN: usize = 65_536;
35const MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN: usize = MAX_DER_ENCODED_KEY_VALUE_LEN.div_ceil(3) * 4;
36const MAX_KEY_NAME_TEXT_LEN: usize = 4096;
37const MAX_X509_BASE64_TEXT_LEN: usize = 262_144;
38const MAX_X509_BASE64_NORMALIZED_LEN: usize = MAX_X509_BASE64_TEXT_LEN;
39const MAX_X509_DECODED_BINARY_LEN: usize = MAX_X509_BASE64_NORMALIZED_LEN.div_ceil(4) * 3;
40const MAX_X509_SUBJECT_NAME_TEXT_LEN: usize = 16_384;
41const MAX_X509_ISSUER_NAME_TEXT_LEN: usize = 16_384;
42const MAX_X509_SERIAL_NUMBER_TEXT_LEN: usize = 4096;
43const MAX_X509_DATA_ENTRY_COUNT: usize = 64;
44const MAX_X509_DATA_TOTAL_BINARY_LEN: usize = 1_048_576;
45const MAX_X509_CHAIN_DEPTH: usize = 9;
46
47/// Signature algorithms supported for signing and verification.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum SignatureAlgorithm {
50    /// RSA with SHA-1. **Verify-only** — signing disabled.
51    RsaSha1,
52    /// RSA with SHA-256 (most common in SAML).
53    RsaSha256,
54    /// RSA with SHA-384.
55    RsaSha384,
56    /// RSA with SHA-512.
57    RsaSha512,
58    /// ECDSA P-256 with SHA-256.
59    EcdsaP256Sha256,
60    /// XMLDSig `ecdsa-sha384` URI.
61    ///
62    /// The variant name is historical.
63    ///
64    /// Verification currently accepts this XMLDSig URI for P-384 and for the
65    /// donor P-521 interop case.
66    EcdsaP384Sha384,
67}
68
69impl SignatureAlgorithm {
70    /// Parse from an XML algorithm URI.
71    #[must_use]
72    pub fn from_uri(uri: &str) -> Option<Self> {
73        match uri {
74            "http://www.w3.org/2000/09/xmldsig#rsa-sha1" => Some(Self::RsaSha1),
75            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Some(Self::RsaSha256),
76            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => Some(Self::RsaSha384),
77            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => Some(Self::RsaSha512),
78            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Some(Self::EcdsaP256Sha256),
79            "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => Some(Self::EcdsaP384Sha384),
80            _ => None,
81        }
82    }
83
84    /// Return the XML namespace URI.
85    #[must_use]
86    pub fn uri(self) -> &'static str {
87        match self {
88            Self::RsaSha1 => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
89            Self::RsaSha256 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
90            Self::RsaSha384 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
91            Self::RsaSha512 => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
92            Self::EcdsaP256Sha256 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
93            Self::EcdsaP384Sha384 => "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
94        }
95    }
96
97    /// Whether this algorithm is allowed for signing (not just verification).
98    #[must_use]
99    pub fn signing_allowed(self) -> bool {
100        !matches!(self, Self::RsaSha1)
101    }
102}
103
104/// Parsed `<SignedInfo>` element.
105#[derive(Debug)]
106pub struct SignedInfo {
107    /// Canonicalization method for SignedInfo itself.
108    pub c14n_method: C14nAlgorithm,
109    /// Signature algorithm.
110    pub signature_method: SignatureAlgorithm,
111    /// One or more `<Reference>` elements.
112    pub references: Vec<Reference>,
113}
114
115/// Parsed `<Reference>` element.
116#[derive(Debug)]
117pub struct Reference {
118    /// URI attribute (e.g., `""`, `"#_assert1"`).
119    pub uri: Option<String>,
120    /// Id attribute.
121    pub id: Option<String>,
122    /// Type attribute.
123    pub ref_type: Option<String>,
124    /// Transform chain.
125    pub transforms: Vec<Transform>,
126    /// Digest algorithm.
127    pub digest_method: DigestAlgorithm,
128    /// Raw digest value (base64-decoded).
129    pub digest_value: Vec<u8>,
130}
131
132/// Parsed `<KeyInfo>` element.
133#[derive(Debug, Default, Clone, PartialEq, Eq)]
134#[non_exhaustive]
135pub struct KeyInfo {
136    /// Sources discovered under `<KeyInfo>` in document order.
137    pub sources: Vec<KeyInfoSource>,
138}
139
140/// Top-level key material source parsed from `<KeyInfo>`.
141#[derive(Debug, Clone, PartialEq, Eq)]
142#[non_exhaustive]
143pub enum KeyInfoSource {
144    /// `<KeyName>` source.
145    KeyName(String),
146    /// `<KeyValue>` source.
147    KeyValue(KeyValueInfo),
148    /// `<X509Data>` source.
149    X509Data(X509DataInfo),
150    /// `dsig11:DEREncodedKeyValue` source (base64-decoded DER bytes).
151    DerEncodedKeyValue(Vec<u8>),
152}
153
154/// Parsed `<KeyValue>` dispatch result.
155#[derive(Debug, Clone, PartialEq, Eq)]
156#[non_exhaustive]
157pub enum KeyValueInfo {
158    /// `<RSAKeyValue>`.
159    RsaKeyValue,
160    /// `dsig11:ECKeyValue` (the XMLDSig 1.1 namespace form).
161    EcKeyValue,
162    /// Any other `<KeyValue>` child not yet supported by this phase.
163    Unsupported {
164        /// Namespace URI of the unsupported child, when present.
165        namespace: Option<String>,
166        /// Local name of the unsupported child element.
167        local_name: String,
168    },
169}
170
171/// Parsed `<X509Data>` children.
172#[derive(Debug, Default, Clone, PartialEq, Eq)]
173#[non_exhaustive]
174pub struct X509DataInfo {
175    /// DER-encoded certificates from `<X509Certificate>`.
176    ///
177    /// This vector has a 1:1 index correspondence with `parsed_certificates`.
178    pub certificates: Vec<Vec<u8>>,
179    /// Text values from `<X509SubjectName>`.
180    pub subject_names: Vec<String>,
181    /// `(IssuerName, SerialNumber)` tuples from `<X509IssuerSerial>`.
182    pub issuer_serials: Vec<(String, String)>,
183    /// Raw bytes from `<X509SKI>`.
184    pub skis: Vec<Vec<u8>>,
185    /// DER-encoded CRLs from `<X509CRL>`.
186    pub crls: Vec<Vec<u8>>,
187    /// `(Algorithm URI, digest bytes)` tuples from `dsig11:X509Digest`.
188    pub digests: Vec<(String, Vec<u8>)>,
189    /// Parsed metadata for each `<X509Certificate>` entry.
190    ///
191    /// This vector has a 1:1 index correspondence with `certificates`.
192    pub parsed_certificates: Vec<ParsedX509Certificate>,
193    /// Ordered certificate indexes, starting with the signing certificate.
194    pub certificate_chain: Vec<usize>,
195}
196
197/// Parsed X.509 certificate details extracted from DER.
198#[derive(Debug, Clone, PartialEq, Eq)]
199#[non_exhaustive]
200pub struct ParsedX509Certificate {
201    /// Subject distinguished name.
202    pub subject_dn: String,
203    /// Issuer distinguished name.
204    pub issuer_dn: String,
205    /// Certificate serial number bytes.
206    pub serial_number: Vec<u8>,
207    /// Uppercase hexadecimal certificate serial number without separators.
208    pub serial_number_hex: String,
209    /// Subject Key Identifier extension bytes (if present).
210    pub subject_key_identifier: Option<Vec<u8>>,
211    /// Parsed certificate public key material.
212    pub public_key: X509PublicKeyInfo,
213}
214
215/// Public key material extracted from certificate SubjectPublicKeyInfo.
216#[derive(Debug, Clone, PartialEq, Eq)]
217#[non_exhaustive]
218pub enum X509PublicKeyInfo {
219    /// RSA public key (`modulus`, `exponent`).
220    Rsa {
221        /// Unsigned big-endian RSA modulus (`n`), normalized without leading zeroes.
222        modulus: Vec<u8>,
223        /// Unsigned big-endian RSA public exponent (`e`), normalized without leading zeroes.
224        exponent: Vec<u8>,
225    },
226    /// EC public key (`curve_oid`, encoded point bytes).
227    Ec {
228        /// Named-curve OID from SubjectPublicKeyInfo parameters.
229        curve_oid: String,
230        /// Encoded EC point bytes from SubjectPublicKeyInfo.
231        public_key: Vec<u8>,
232    },
233    /// Public key algorithm is present but not parsed into a concrete key type.
234    Unsupported {
235        /// SubjectPublicKeyInfo algorithm OID.
236        algorithm_oid: String,
237    },
238}
239
240/// Errors during XMLDSig element parsing.
241#[derive(Debug, thiserror::Error)]
242#[non_exhaustive]
243pub enum ParseError {
244    /// Missing required element.
245    #[error("missing required element: <{element}>")]
246    MissingElement {
247        /// Name of the missing element.
248        element: &'static str,
249    },
250
251    /// Invalid structure (wrong child order, unexpected element, etc.).
252    #[error("invalid structure: {0}")]
253    InvalidStructure(String),
254
255    /// Unsupported algorithm URI.
256    #[error("unsupported algorithm: {uri}")]
257    UnsupportedAlgorithm {
258        /// The unrecognized algorithm URI.
259        uri: String,
260    },
261
262    /// Base64 decode error.
263    #[error("base64 decode error: {0}")]
264    Base64(String),
265
266    /// DigestValue length did not match the declared DigestMethod.
267    #[error(
268        "digest length mismatch for {algorithm}: expected {expected} bytes, got {actual} bytes"
269    )]
270    DigestLengthMismatch {
271        /// Digest algorithm URI/name used for diagnostics.
272        algorithm: &'static str,
273        /// Expected decoded digest length in bytes.
274        expected: usize,
275        /// Actual decoded digest length in bytes.
276        actual: usize,
277    },
278
279    /// Transform parsing error.
280    #[error("transform error: {0}")]
281    Transform(#[from] super::types::TransformError),
282}
283
284/// Find the first `<ds:Signature>` element in the document.
285#[must_use]
286pub fn find_signature_node<'a>(doc: &'a Document<'a>) -> Option<Node<'a, 'a>> {
287    doc.descendants().find(|n| {
288        n.is_element()
289            && n.tag_name().name() == "Signature"
290            && n.tag_name().namespace() == Some(XMLDSIG_NS)
291    })
292}
293
294/// Parse a `<ds:SignedInfo>` element.
295///
296/// Enforces strict child order per XMLDSig spec:
297/// `<CanonicalizationMethod>` → `<SignatureMethod>` → `<Reference>`+
298pub fn parse_signed_info(signed_info_node: Node) -> Result<SignedInfo, ParseError> {
299    verify_ds_element(signed_info_node, "SignedInfo")?;
300
301    let mut children = element_children(signed_info_node);
302
303    // 1. CanonicalizationMethod (required, first)
304    let c14n_node = children.next().ok_or(ParseError::MissingElement {
305        element: "CanonicalizationMethod",
306    })?;
307    verify_ds_element(c14n_node, "CanonicalizationMethod")?;
308    let c14n_uri = required_algorithm_attr(c14n_node, "CanonicalizationMethod")?;
309    let mut c14n_method =
310        C14nAlgorithm::from_uri(c14n_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
311            uri: c14n_uri.to_string(),
312        })?;
313    if let Some(prefix_list) = parse_inclusive_prefixes(c14n_node)? {
314        if c14n_method.mode() == crate::c14n::C14nMode::Exclusive1_0 {
315            c14n_method = c14n_method.with_prefix_list(&prefix_list);
316        } else {
317            return Err(ParseError::UnsupportedAlgorithm {
318                uri: c14n_uri.to_string(),
319            });
320        }
321    }
322
323    // 2. SignatureMethod (required, second)
324    let sig_method_node = children.next().ok_or(ParseError::MissingElement {
325        element: "SignatureMethod",
326    })?;
327    verify_ds_element(sig_method_node, "SignatureMethod")?;
328    let sig_uri = required_algorithm_attr(sig_method_node, "SignatureMethod")?;
329    let signature_method =
330        SignatureAlgorithm::from_uri(sig_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
331            uri: sig_uri.to_string(),
332        })?;
333
334    // 3. One or more Reference elements
335    let mut references = Vec::new();
336    for child in children {
337        verify_ds_element(child, "Reference")?;
338        references.push(parse_reference(child)?);
339    }
340    if references.is_empty() {
341        return Err(ParseError::MissingElement {
342            element: "Reference",
343        });
344    }
345
346    Ok(SignedInfo {
347        c14n_method,
348        signature_method,
349        references,
350    })
351}
352
353/// Parse a single `<ds:Reference>` element.
354///
355/// Structure: `<Transforms>?` → `<DigestMethod>` → `<DigestValue>`
356pub(crate) fn parse_reference(reference_node: Node) -> Result<Reference, ParseError> {
357    let uri = reference_node.attribute("URI").map(String::from);
358    let id = reference_node.attribute("Id").map(String::from);
359    let ref_type = reference_node.attribute("Type").map(String::from);
360
361    let mut children = element_children(reference_node);
362
363    // Optional <Transforms>
364    let mut transforms = Vec::new();
365    let mut next = children.next().ok_or(ParseError::MissingElement {
366        element: "DigestMethod",
367    })?;
368
369    if next.tag_name().name() == "Transforms" && next.tag_name().namespace() == Some(XMLDSIG_NS) {
370        transforms = transforms::parse_transforms(next)?;
371        next = children.next().ok_or(ParseError::MissingElement {
372            element: "DigestMethod",
373        })?;
374    }
375
376    // Required <DigestMethod>
377    verify_ds_element(next, "DigestMethod")?;
378    let digest_uri = required_algorithm_attr(next, "DigestMethod")?;
379    let digest_method =
380        DigestAlgorithm::from_uri(digest_uri).ok_or_else(|| ParseError::UnsupportedAlgorithm {
381            uri: digest_uri.to_string(),
382        })?;
383
384    // Required <DigestValue>
385    let digest_value_node = children.next().ok_or(ParseError::MissingElement {
386        element: "DigestValue",
387    })?;
388    verify_ds_element(digest_value_node, "DigestValue")?;
389    let digest_value = decode_digest_value_children(digest_value_node, digest_method)?;
390
391    // No more children expected
392    if let Some(unexpected) = children.next() {
393        return Err(ParseError::InvalidStructure(format!(
394            "unexpected element <{}> after <DigestValue> in <Reference>",
395            unexpected.tag_name().name()
396        )));
397    }
398
399    Ok(Reference {
400        uri,
401        id,
402        ref_type,
403        transforms,
404        digest_method,
405        digest_value,
406    })
407}
408
409/// Parse `<ds:KeyInfo>` and dispatch supported child sources.
410///
411/// Supported source elements:
412/// - `<ds:KeyName>`
413/// - `<ds:KeyValue>` (dispatch by child QName; only `dsig11:ECKeyValue` is treated as supported EC)
414/// - `<ds:X509Data>`
415/// - `<dsig11:DEREncodedKeyValue>`
416///
417/// Unknown top-level `<KeyInfo>` children are ignored (lax processing), while
418/// unknown XMLDSig-owned (`ds:*` / `dsig11:*`) children inside `<X509Data>` are
419/// rejected fail-closed.
420/// `<X509Data>` may still be empty or contain only non-XMLDSig extension children.
421pub fn parse_key_info(key_info_node: Node) -> Result<KeyInfo, ParseError> {
422    verify_ds_element(key_info_node, "KeyInfo")?;
423    ensure_no_non_whitespace_text(key_info_node, "KeyInfo")?;
424
425    let mut sources = Vec::new();
426    for child in element_children(key_info_node) {
427        match (child.tag_name().namespace(), child.tag_name().name()) {
428            (Some(XMLDSIG_NS), "KeyName") => {
429                ensure_no_element_children(child, "KeyName")?;
430                let key_name =
431                    collect_text_content_bounded(child, MAX_KEY_NAME_TEXT_LEN, "KeyName")?;
432                sources.push(KeyInfoSource::KeyName(key_name));
433            }
434            (Some(XMLDSIG_NS), "KeyValue") => {
435                let key_value = parse_key_value_dispatch(child)?;
436                sources.push(KeyInfoSource::KeyValue(key_value));
437            }
438            (Some(XMLDSIG_NS), "X509Data") => {
439                let x509 = parse_x509_data_dispatch(child)?;
440                sources.push(KeyInfoSource::X509Data(x509));
441            }
442            (Some(XMLDSIG11_NS), "DEREncodedKeyValue") => {
443                ensure_no_element_children(child, "DEREncodedKeyValue")?;
444                let der = decode_der_encoded_key_value_base64(child)?;
445                sources.push(KeyInfoSource::DerEncodedKeyValue(der));
446            }
447            _ => {}
448        }
449    }
450
451    Ok(KeyInfo { sources })
452}
453
454// ── Helpers ──────────────────────────────────────────────────────────────────
455
456/// Iterate only element children (skip text, comments, PIs).
457fn element_children<'a>(node: Node<'a, 'a>) -> impl Iterator<Item = Node<'a, 'a>> {
458    node.children().filter(|n| n.is_element())
459}
460
461/// Verify that a node is a `<ds:{expected_name}>` element.
462fn verify_ds_element(node: Node, expected_name: &'static str) -> Result<(), ParseError> {
463    if !node.is_element() {
464        return Err(ParseError::InvalidStructure(format!(
465            "expected element <{expected_name}>, got non-element node"
466        )));
467    }
468    let tag = node.tag_name();
469    if tag.name() != expected_name || tag.namespace() != Some(XMLDSIG_NS) {
470        return Err(ParseError::InvalidStructure(format!(
471            "expected <ds:{expected_name}>, got <{}{}>",
472            tag.namespace()
473                .map(|ns| format!("{{{ns}}}"))
474                .unwrap_or_default(),
475            tag.name()
476        )));
477    }
478    Ok(())
479}
480
481/// Get the required `Algorithm` attribute from an element.
482fn required_algorithm_attr<'a>(
483    node: Node<'a, 'a>,
484    element_name: &'static str,
485) -> Result<&'a str, ParseError> {
486    node.attribute("Algorithm").ok_or_else(|| {
487        ParseError::InvalidStructure(format!("missing Algorithm attribute on <{element_name}>"))
488    })
489}
490
491/// Parse the `PrefixList` attribute from an `<ec:InclusiveNamespaces>` child of
492/// `<CanonicalizationMethod>`, if present.
493///
494/// This mirrors transform parsing for Exclusive C14N and keeps SignedInfo
495/// canonicalization parameters lossless.
496fn parse_inclusive_prefixes(node: Node) -> Result<Option<String>, ParseError> {
497    const EXCLUSIVE_C14N_NS_URI: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
498
499    for child in node.children() {
500        if child.is_element() {
501            let tag = child.tag_name();
502            if tag.name() == "InclusiveNamespaces" && tag.namespace() == Some(EXCLUSIVE_C14N_NS_URI)
503            {
504                return child
505                    .attribute("PrefixList")
506                    .map(str::to_string)
507                    .ok_or_else(|| {
508                        ParseError::InvalidStructure(
509                            "missing PrefixList attribute on <InclusiveNamespaces>".into(),
510                        )
511                    })
512                    .map(Some);
513            }
514        }
515    }
516
517    Ok(None)
518}
519
520fn parse_key_value_dispatch(node: Node) -> Result<KeyValueInfo, ParseError> {
521    verify_ds_element(node, "KeyValue")?;
522    ensure_no_non_whitespace_text(node, "KeyValue")?;
523
524    let mut children = element_children(node);
525    let Some(first_child) = children.next() else {
526        return Err(ParseError::InvalidStructure(
527            "KeyValue must contain exactly one key-value child".into(),
528        ));
529    };
530    if children.next().is_some() {
531        return Err(ParseError::InvalidStructure(
532            "KeyValue must contain exactly one key-value child".into(),
533        ));
534    }
535
536    match (
537        first_child.tag_name().namespace(),
538        first_child.tag_name().name(),
539    ) {
540        (Some(XMLDSIG_NS), "RSAKeyValue") => Ok(KeyValueInfo::RsaKeyValue),
541        (Some(XMLDSIG11_NS), "ECKeyValue") => Ok(KeyValueInfo::EcKeyValue),
542        (namespace, child_name) => Ok(KeyValueInfo::Unsupported {
543            namespace: namespace.map(str::to_string),
544            local_name: child_name.to_string(),
545        }),
546    }
547}
548
549fn parse_x509_data_dispatch(node: Node) -> Result<X509DataInfo, ParseError> {
550    verify_ds_element(node, "X509Data")?;
551    ensure_no_non_whitespace_text(node, "X509Data")?;
552
553    let mut info = X509DataInfo::default();
554    let mut total_binary_len = 0usize;
555    for child in element_children(node) {
556        match (child.tag_name().namespace(), child.tag_name().name()) {
557            (Some(XMLDSIG_NS), "X509Certificate") => {
558                ensure_no_element_children(child, "X509Certificate")?;
559                ensure_x509_data_entry_budget(&info)?;
560                let cert = decode_x509_base64(child, "X509Certificate")?;
561                add_x509_data_usage(&mut total_binary_len, cert.len())?;
562                let parsed_cert = parse_x509_certificate(cert.as_slice())?;
563                info.parsed_certificates.push(parsed_cert);
564                info.certificates.push(cert);
565            }
566            (Some(XMLDSIG_NS), "X509SubjectName") => {
567                ensure_no_element_children(child, "X509SubjectName")?;
568                ensure_x509_data_entry_budget(&info)?;
569                let subject_name = collect_text_content_bounded(
570                    child,
571                    MAX_X509_SUBJECT_NAME_TEXT_LEN,
572                    "X509SubjectName",
573                )?;
574                info.subject_names.push(subject_name);
575            }
576            (Some(XMLDSIG_NS), "X509IssuerSerial") => {
577                ensure_x509_data_entry_budget(&info)?;
578                let issuer_serial = parse_x509_issuer_serial(child)?;
579                info.issuer_serials.push(issuer_serial);
580            }
581            (Some(XMLDSIG_NS), "X509SKI") => {
582                ensure_no_element_children(child, "X509SKI")?;
583                ensure_x509_data_entry_budget(&info)?;
584                let ski = decode_x509_base64(child, "X509SKI")?;
585                add_x509_data_usage(&mut total_binary_len, ski.len())?;
586                info.skis.push(ski);
587            }
588            (Some(XMLDSIG_NS), "X509CRL") => {
589                ensure_no_element_children(child, "X509CRL")?;
590                ensure_x509_data_entry_budget(&info)?;
591                let crl = decode_x509_base64(child, "X509CRL")?;
592                add_x509_data_usage(&mut total_binary_len, crl.len())?;
593                info.crls.push(crl);
594            }
595            (Some(XMLDSIG11_NS), "X509Digest") => {
596                ensure_no_element_children(child, "X509Digest")?;
597                ensure_x509_data_entry_budget(&info)?;
598                let algorithm = required_algorithm_attr(child, "X509Digest")?;
599                let digest = decode_x509_base64(child, "X509Digest")?;
600                add_x509_data_usage(&mut total_binary_len, digest.len())?;
601                info.digests.push((algorithm.to_string(), digest));
602            }
603            (Some(XMLDSIG_NS), child_name) | (Some(XMLDSIG11_NS), child_name) => {
604                return Err(ParseError::InvalidStructure(format!(
605                    "X509Data contains unsupported XMLDSig child element <{child_name}>"
606                )));
607            }
608            _ => {}
609        }
610    }
611
612    info.certificate_chain = build_x509_certificate_chain(&info)?;
613    Ok(info)
614}
615
616fn build_x509_certificate_chain(info: &X509DataInfo) -> Result<Vec<usize>, ParseError> {
617    if info.parsed_certificates.is_empty() {
618        return Ok(Vec::new());
619    }
620
621    let signing_idx = select_x509_signing_certificate(info)?;
622    let mut chain = vec![signing_idx];
623
624    loop {
625        if chain.len() > MAX_X509_CHAIN_DEPTH {
626            return Err(ParseError::InvalidStructure(
627                "X509Data certificate chain exceeds maximum depth".into(),
628            ));
629        }
630
631        let current_idx = *chain
632            .last()
633            .expect("chain starts with signing certificate index");
634        let current = &info.parsed_certificates[current_idx];
635        if current.subject_dn == current.issuer_dn {
636            break;
637        }
638
639        let candidates = info
640            .parsed_certificates
641            .iter()
642            .enumerate()
643            .filter(|(idx, cert)| *idx != current_idx && cert.subject_dn == current.issuer_dn)
644            .map(|(idx, _)| idx)
645            .collect::<Vec<_>>();
646
647        match candidates.as_slice() {
648            [] => break,
649            [issuer_idx] => {
650                if chain.contains(issuer_idx) {
651                    return Err(ParseError::InvalidStructure(
652                        "X509Data certificate chain contains a cycle".into(),
653                    ));
654                }
655                if chain.len() == MAX_X509_CHAIN_DEPTH {
656                    return Err(ParseError::InvalidStructure(
657                        "X509Data certificate chain exceeds maximum depth".into(),
658                    ));
659                }
660                chain.push(*issuer_idx);
661            }
662            _ => {
663                return Err(ParseError::InvalidStructure(
664                    "X509Data certificate chain contains ambiguous issuer certificates".into(),
665                ));
666            }
667        }
668    }
669
670    Ok(chain)
671}
672
673fn select_x509_signing_certificate(info: &X509DataInfo) -> Result<usize, ParseError> {
674    let mut candidates = Vec::new();
675    let has_lookup_identifiers =
676        !info.subject_names.is_empty() || !info.issuer_serials.is_empty() || !info.skis.is_empty();
677
678    for subject_name in &info.subject_names {
679        let subject_name = subject_name.trim();
680        let matches = info
681            .parsed_certificates
682            .iter()
683            .enumerate()
684            .filter(|(_, cert)| subject_name == cert.subject_dn)
685            .map(|(idx, _)| idx)
686            .collect::<Vec<_>>();
687        if matches.is_empty() {
688            return Err(ParseError::InvalidStructure(
689                "X509Data lookup identifiers do not match any embedded certificate".into(),
690            ));
691        }
692        candidates.extend(matches);
693    }
694
695    for (issuer_name, serial) in &info.issuer_serials {
696        let serial_hex = x509_serial_decimal_to_hex(serial).ok_or_else(|| {
697            ParseError::InvalidStructure(
698                "X509Data lookup identifiers do not match any embedded certificate".into(),
699            )
700        })?;
701        let issuer_name = issuer_name.trim();
702        let matches = info
703            .parsed_certificates
704            .iter()
705            .enumerate()
706            .filter(|(_, cert)| {
707                issuer_name == cert.issuer_dn && serial_hex == cert.serial_number_hex
708            })
709            .map(|(idx, _)| idx)
710            .collect::<Vec<_>>();
711        if matches.is_empty() {
712            return Err(ParseError::InvalidStructure(
713                "X509Data lookup identifiers do not match any embedded certificate".into(),
714            ));
715        }
716        candidates.extend(matches);
717    }
718
719    for ski in &info.skis {
720        let matches = info
721            .parsed_certificates
722            .iter()
723            .enumerate()
724            .filter(|(_, cert)| {
725                cert.subject_key_identifier
726                    .as_ref()
727                    .is_some_and(|subject_key_identifier| subject_key_identifier == ski)
728            })
729            .map(|(idx, _)| idx)
730            .collect::<Vec<_>>();
731        if matches.is_empty() {
732            return Err(ParseError::InvalidStructure(
733                "X509Data lookup identifiers do not match any embedded certificate".into(),
734            ));
735        }
736        candidates.extend(matches);
737    }
738
739    if has_lookup_identifiers {
740        candidates.sort_unstable();
741        candidates.dedup();
742    }
743
744    match candidates.as_slice() {
745        [idx] => return Ok(*idx),
746        [] if has_lookup_identifiers => {
747            return Err(ParseError::InvalidStructure(
748                "X509Data lookup identifiers do not match any embedded certificate".into(),
749            ));
750        }
751        [] => {}
752        _ => {
753            return Err(ParseError::InvalidStructure(
754                "X509Data lookup identifiers match multiple certificates".into(),
755            ));
756        }
757    }
758
759    let leaf_candidates = info
760        .parsed_certificates
761        .iter()
762        .enumerate()
763        .filter(|(_, cert)| {
764            cert.subject_dn != cert.issuer_dn
765                && !info
766                    .parsed_certificates
767                    .iter()
768                    .any(|other| other.issuer_dn == cert.subject_dn)
769        })
770        .map(|(idx, _)| idx)
771        .collect::<Vec<_>>();
772
773    match leaf_candidates.as_slice() {
774        [idx] => Ok(*idx),
775        [] => Ok(0),
776        _ => Err(ParseError::InvalidStructure(
777            "X509Data contains multiple possible signing certificates".into(),
778        )),
779    }
780}
781
782fn ensure_x509_data_entry_budget(info: &X509DataInfo) -> Result<(), ParseError> {
783    let total_entries = info.certificates.len()
784        + info.subject_names.len()
785        + info.issuer_serials.len()
786        + info.skis.len()
787        + info.crls.len()
788        + info.digests.len();
789    if total_entries >= MAX_X509_DATA_ENTRY_COUNT {
790        return Err(ParseError::InvalidStructure(
791            "X509Data contains too many entries".into(),
792        ));
793    }
794    Ok(())
795}
796
797fn add_x509_data_usage(total_binary_len: &mut usize, delta: usize) -> Result<(), ParseError> {
798    *total_binary_len = total_binary_len.checked_add(delta).ok_or_else(|| {
799        ParseError::InvalidStructure("X509Data exceeds maximum allowed total binary length".into())
800    })?;
801    if *total_binary_len > MAX_X509_DATA_TOTAL_BINARY_LEN {
802        return Err(ParseError::InvalidStructure(
803            "X509Data exceeds maximum allowed total binary length".into(),
804        ));
805    }
806    Ok(())
807}
808
809fn decode_x509_base64(
810    node: Node<'_, '_>,
811    element_name: &'static str,
812) -> Result<Vec<u8>, ParseError> {
813    use base64::Engine;
814    use base64::engine::general_purpose::STANDARD;
815
816    let mut cleaned = String::new();
817    let mut raw_text_len = 0usize;
818    for text in node
819        .children()
820        .filter(|child| child.is_text())
821        .filter_map(|child| child.text())
822    {
823        if raw_text_len.saturating_add(text.len()) > MAX_X509_BASE64_TEXT_LEN {
824            return Err(ParseError::InvalidStructure(format!(
825                "{element_name} exceeds maximum allowed text length"
826            )));
827        }
828        raw_text_len = raw_text_len.saturating_add(text.len());
829        normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
830            ParseError::Base64(format!(
831                "invalid XML whitespace U+{:04X} in {element_name}",
832                err.invalid_byte
833            ))
834        })?;
835        if cleaned.len() > MAX_X509_BASE64_NORMALIZED_LEN {
836            return Err(ParseError::InvalidStructure(format!(
837                "{element_name} exceeds maximum allowed base64 length"
838            )));
839        }
840    }
841
842    let decoded = STANDARD
843        .decode(&cleaned)
844        .map_err(|e| ParseError::Base64(format!("{element_name}: {e}")))?;
845    if decoded.is_empty() {
846        return Err(ParseError::InvalidStructure(format!(
847            "{element_name} must not be empty"
848        )));
849    }
850    if decoded.len() > MAX_X509_DECODED_BINARY_LEN {
851        return Err(ParseError::InvalidStructure(format!(
852            "{element_name} exceeds maximum allowed binary length"
853        )));
854    }
855    Ok(decoded)
856}
857
858fn parse_x509_certificate(cert_der: &[u8]) -> Result<ParsedX509Certificate, ParseError> {
859    let (rest, cert) =
860        x509_parser::certificate::X509Certificate::from_der(cert_der).map_err(|err| {
861            ParseError::InvalidStructure(format!("X509Certificate is not valid DER X.509: {err}"))
862        })?;
863    if !rest.is_empty() {
864        return Err(ParseError::InvalidStructure(
865            "X509Certificate contains trailing bytes after DER certificate".into(),
866        ));
867    }
868
869    let subject_dn = cert.subject().to_string();
870    let issuer_dn = cert.issuer().to_string();
871    let serial_number = cert.tbs_certificate.raw_serial().to_vec();
872    let serial_number_hex = format_x509_serial_value_hex(&serial_number);
873
874    let subject_key_identifier = cert.extensions().iter().find_map(|ext| {
875        if let ParsedExtension::SubjectKeyIdentifier(ski) = ext.parsed_extension() {
876            Some(ski.0.to_vec())
877        } else {
878            None
879        }
880    });
881
882    let spki = cert.public_key();
883    let public_key = match spki.parsed().map_err(|err| {
884        ParseError::InvalidStructure(format!("X509Certificate public key parse error: {err}"))
885    })? {
886        PublicKey::RSA(rsa) => {
887            let modulus = trim_leading_zeroes(rsa.modulus);
888            let exponent = trim_leading_zeroes(rsa.exponent);
889            if modulus.is_empty() || exponent.is_empty() {
890                return Err(ParseError::InvalidStructure(
891                    "X509Certificate RSA key contains empty modulus or exponent".into(),
892                ));
893            }
894            X509PublicKeyInfo::Rsa { modulus, exponent }
895        }
896        PublicKey::EC(ec_point) => {
897            let Some(params) = spki.algorithm.parameters.as_ref() else {
898                return Err(ParseError::InvalidStructure(
899                    "X509Certificate EC key is missing curve parameters".into(),
900                ));
901            };
902
903            match params.as_oid() {
904                Ok(oid) => X509PublicKeyInfo::Ec {
905                    curve_oid: oid.to_id_string(),
906                    public_key: ec_point.data().to_vec(),
907                },
908                Err(_) => X509PublicKeyInfo::Unsupported {
909                    algorithm_oid: spki.algorithm.algorithm.to_id_string(),
910                },
911            }
912        }
913        _ => X509PublicKeyInfo::Unsupported {
914            algorithm_oid: spki.algorithm.algorithm.to_id_string(),
915        },
916    };
917
918    Ok(ParsedX509Certificate {
919        subject_dn,
920        issuer_dn,
921        serial_number,
922        serial_number_hex,
923        subject_key_identifier,
924        public_key,
925    })
926}
927
928fn format_x509_serial_hex(serial: &[u8]) -> String {
929    serial
930        .iter()
931        .map(|byte| format!("{byte:02X}"))
932        .collect::<String>()
933}
934
935fn format_x509_serial_value_hex(serial: &[u8]) -> String {
936    let first_non_zero = serial
937        .iter()
938        .position(|byte| *byte != 0)
939        .unwrap_or(serial.len());
940    let canonical = if first_non_zero == serial.len() {
941        &[0]
942    } else {
943        &serial[first_non_zero..]
944    };
945    format_x509_serial_hex(canonical)
946}
947
948fn x509_serial_decimal_to_hex(serial: &str) -> Option<String> {
949    let serial = serial.trim();
950    let serial = serial.strip_prefix('+').unwrap_or(serial);
951    if serial.is_empty() || !serial.bytes().all(|byte| byte.is_ascii_digit()) {
952        return None;
953    }
954
955    let mut bytes = Vec::<u8>::new();
956    for digit in serial.bytes().map(|byte| byte - b'0') {
957        let mut carry = u16::from(digit);
958        for byte in bytes.iter_mut().rev() {
959            let value = u16::from(*byte) * 10 + carry;
960            *byte = value as u8;
961            carry = value >> 8;
962        }
963        while carry > 0 {
964            bytes.insert(0, carry as u8);
965            carry >>= 8;
966        }
967    }
968
969    Some(format_x509_serial_value_hex(&bytes))
970}
971
972fn trim_leading_zeroes(bytes: &[u8]) -> Vec<u8> {
973    let first_non_zero = bytes
974        .iter()
975        .position(|byte| *byte != 0)
976        .unwrap_or(bytes.len());
977    bytes[first_non_zero..].to_vec()
978}
979
980fn parse_x509_issuer_serial(node: Node<'_, '_>) -> Result<(String, String), ParseError> {
981    verify_ds_element(node, "X509IssuerSerial")?;
982    ensure_no_non_whitespace_text(node, "X509IssuerSerial")?;
983
984    let children = element_children(node).collect::<Vec<_>>();
985    if children.len() != 2 {
986        return Err(ParseError::InvalidStructure(
987            "X509IssuerSerial must contain exactly X509IssuerName then X509SerialNumber".into(),
988        ));
989    }
990    if !matches!(
991        (
992            children[0].tag_name().namespace(),
993            children[0].tag_name().name()
994        ),
995        (Some(XMLDSIG_NS), "X509IssuerName")
996    ) {
997        return Err(ParseError::InvalidStructure(
998            "X509IssuerSerial must contain X509IssuerName as the first child element".into(),
999        ));
1000    }
1001    if !matches!(
1002        (
1003            children[1].tag_name().namespace(),
1004            children[1].tag_name().name()
1005        ),
1006        (Some(XMLDSIG_NS), "X509SerialNumber")
1007    ) {
1008        return Err(ParseError::InvalidStructure(
1009            "X509IssuerSerial must contain X509SerialNumber as the second child element".into(),
1010        ));
1011    }
1012
1013    let issuer_node = children[0];
1014    ensure_no_element_children(issuer_node, "X509IssuerName")?;
1015    let issuer_name =
1016        collect_text_content_bounded(issuer_node, MAX_X509_ISSUER_NAME_TEXT_LEN, "X509IssuerName")?;
1017
1018    let serial_node = children[1];
1019    ensure_no_element_children(serial_node, "X509SerialNumber")?;
1020    let serial_number = collect_text_content_bounded(
1021        serial_node,
1022        MAX_X509_SERIAL_NUMBER_TEXT_LEN,
1023        "X509SerialNumber",
1024    )?;
1025    if issuer_name.trim().is_empty() || serial_number.trim().is_empty() {
1026        return Err(ParseError::InvalidStructure(
1027            "X509IssuerSerial requires non-empty X509IssuerName and X509SerialNumber".into(),
1028        ));
1029    }
1030
1031    Ok((issuer_name, serial_number))
1032}
1033
1034/// Base64-decode a digest value string, stripping whitespace.
1035///
1036/// XMLDSig allows whitespace within base64 content (line-wrapped encodings).
1037fn base64_decode_digest(b64: &str, digest_method: DigestAlgorithm) -> Result<Vec<u8>, ParseError> {
1038    use base64::Engine;
1039    use base64::engine::general_purpose::STANDARD;
1040
1041    let expected = digest_method.output_len();
1042    let max_base64_len = expected.div_ceil(3) * 4;
1043    let mut cleaned = String::with_capacity(b64.len().min(max_base64_len));
1044    normalize_xml_base64_text(b64, &mut cleaned).map_err(|err| {
1045        ParseError::Base64(format!(
1046            "invalid XML whitespace U+{:04X} in DigestValue",
1047            err.invalid_byte
1048        ))
1049    })?;
1050    if cleaned.len() > max_base64_len {
1051        return Err(ParseError::Base64(
1052            "DigestValue exceeds maximum allowed base64 length".into(),
1053        ));
1054    }
1055    let digest = STANDARD
1056        .decode(&cleaned)
1057        .map_err(|e| ParseError::Base64(e.to_string()))?;
1058    let actual = digest.len();
1059    if actual != expected {
1060        return Err(ParseError::DigestLengthMismatch {
1061            algorithm: digest_method.uri(),
1062            expected,
1063            actual,
1064        });
1065    }
1066    Ok(digest)
1067}
1068
1069fn decode_digest_value_children(
1070    digest_value_node: Node<'_, '_>,
1071    digest_method: DigestAlgorithm,
1072) -> Result<Vec<u8>, ParseError> {
1073    let max_base64_len = digest_method.output_len().div_ceil(3) * 4;
1074    let mut cleaned = String::with_capacity(max_base64_len);
1075
1076    for child in digest_value_node.children() {
1077        if child.is_element() {
1078            return Err(ParseError::InvalidStructure(
1079                "DigestValue must not contain element children".into(),
1080            ));
1081        }
1082        if let Some(text) = child.text() {
1083            normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
1084                ParseError::Base64(format!(
1085                    "invalid XML whitespace U+{:04X} in DigestValue",
1086                    err.invalid_byte
1087                ))
1088            })?;
1089            if cleaned.len() > max_base64_len {
1090                return Err(ParseError::Base64(
1091                    "DigestValue exceeds maximum allowed base64 length".into(),
1092                ));
1093            }
1094        }
1095    }
1096
1097    base64_decode_digest(&cleaned, digest_method)
1098}
1099
1100fn decode_der_encoded_key_value_base64(node: Node<'_, '_>) -> Result<Vec<u8>, ParseError> {
1101    use base64::Engine;
1102    use base64::engine::general_purpose::STANDARD;
1103
1104    let mut cleaned = String::new();
1105    let mut raw_text_len = 0usize;
1106    for text in node
1107        .children()
1108        .filter(|child| child.is_text())
1109        .filter_map(|child| child.text())
1110    {
1111        if raw_text_len.saturating_add(text.len()) > MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN {
1112            return Err(ParseError::InvalidStructure(
1113                "DEREncodedKeyValue exceeds maximum allowed text length".into(),
1114            ));
1115        }
1116        raw_text_len = raw_text_len.saturating_add(text.len());
1117        normalize_xml_base64_text(text, &mut cleaned).map_err(|err| {
1118            ParseError::Base64(format!(
1119                "invalid XML whitespace U+{:04X} in base64 text",
1120                err.invalid_byte
1121            ))
1122        })?;
1123        if cleaned.len() > MAX_DER_ENCODED_KEY_VALUE_BASE64_LEN {
1124            return Err(ParseError::InvalidStructure(
1125                "DEREncodedKeyValue exceeds maximum allowed length".into(),
1126            ));
1127        }
1128    }
1129
1130    let der = STANDARD
1131        .decode(&cleaned)
1132        .map_err(|e| ParseError::Base64(e.to_string()))?;
1133    if der.is_empty() {
1134        return Err(ParseError::InvalidStructure(
1135            "DEREncodedKeyValue must not be empty".into(),
1136        ));
1137    }
1138    if der.len() > MAX_DER_ENCODED_KEY_VALUE_LEN {
1139        return Err(ParseError::InvalidStructure(
1140            "DEREncodedKeyValue exceeds maximum allowed length".into(),
1141        ));
1142    }
1143    Ok(der)
1144}
1145
1146fn collect_text_content_bounded(
1147    node: Node<'_, '_>,
1148    max_len: usize,
1149    element_name: &'static str,
1150) -> Result<String, ParseError> {
1151    let mut text = String::new();
1152    for chunk in node
1153        .children()
1154        .filter_map(|child| child.is_text().then(|| child.text()).flatten())
1155    {
1156        if text.len().saturating_add(chunk.len()) > max_len {
1157            return Err(ParseError::InvalidStructure(format!(
1158                "{element_name} exceeds maximum allowed text length"
1159            )));
1160        }
1161        text.push_str(chunk);
1162    }
1163    Ok(text)
1164}
1165
1166fn ensure_no_element_children(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
1167    if node.children().any(|child| child.is_element()) {
1168        return Err(ParseError::InvalidStructure(format!(
1169            "{element_name} must not contain child elements"
1170        )));
1171    }
1172    Ok(())
1173}
1174
1175fn ensure_no_non_whitespace_text(node: Node<'_, '_>, element_name: &str) -> Result<(), ParseError> {
1176    for child in node.children().filter(|child| child.is_text()) {
1177        if let Some(text) = child.text()
1178            && !is_xml_whitespace_only(text)
1179        {
1180            return Err(ParseError::InvalidStructure(format!(
1181                "{element_name} must not contain non-whitespace mixed content"
1182            )));
1183        }
1184    }
1185    Ok(())
1186}
1187
1188#[cfg(test)]
1189#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
1190mod tests {
1191    use super::*;
1192    use base64::Engine;
1193
1194    fn fixture_rsa_cert_base64() -> String {
1195        fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
1196    }
1197
1198    fn fixture_cert_base64(path: &str) -> String {
1199        match path {
1200            "../../tests/fixtures/keys/rsa/rsa-2048-cert.pem" => {
1201                include_str!("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem")
1202            }
1203            "../../tests/fixtures/keys/rsa/rsa-4096-cert.pem" => {
1204                include_str!("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem")
1205            }
1206            "../../tests/fixtures/keys/ca2cert.pem" => {
1207                include_str!("../../tests/fixtures/keys/ca2cert.pem")
1208            }
1209            "../../tests/fixtures/keys/cacert.pem" => {
1210                include_str!("../../tests/fixtures/keys/cacert.pem")
1211            }
1212            _ => unreachable!("unknown certificate fixture"),
1213        }
1214        .lines()
1215        .skip_while(|line| *line != "-----BEGIN CERTIFICATE-----")
1216        .skip(1)
1217        .take_while(|line| *line != "-----END CERTIFICATE-----")
1218        .collect::<String>()
1219    }
1220
1221    // ── SignatureAlgorithm ───────────────────────────────────────────
1222
1223    #[test]
1224    fn signature_algorithm_from_uri_rsa_sha256() {
1225        assert_eq!(
1226            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"),
1227            Some(SignatureAlgorithm::RsaSha256)
1228        );
1229    }
1230
1231    #[test]
1232    fn signature_algorithm_from_uri_rsa_sha1() {
1233        assert_eq!(
1234            SignatureAlgorithm::from_uri("http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
1235            Some(SignatureAlgorithm::RsaSha1)
1236        );
1237    }
1238
1239    #[test]
1240    fn signature_algorithm_from_uri_ecdsa_sha256() {
1241        assert_eq!(
1242            SignatureAlgorithm::from_uri("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"),
1243            Some(SignatureAlgorithm::EcdsaP256Sha256)
1244        );
1245    }
1246
1247    #[test]
1248    fn signature_algorithm_from_uri_unknown() {
1249        assert_eq!(
1250            SignatureAlgorithm::from_uri("http://example.com/unknown"),
1251            None
1252        );
1253    }
1254
1255    #[test]
1256    fn signature_algorithm_uri_round_trip() {
1257        for algo in [
1258            SignatureAlgorithm::RsaSha1,
1259            SignatureAlgorithm::RsaSha256,
1260            SignatureAlgorithm::RsaSha384,
1261            SignatureAlgorithm::RsaSha512,
1262            SignatureAlgorithm::EcdsaP256Sha256,
1263            SignatureAlgorithm::EcdsaP384Sha384,
1264        ] {
1265            assert_eq!(
1266                SignatureAlgorithm::from_uri(algo.uri()),
1267                Some(algo),
1268                "round-trip failed for {algo:?}"
1269            );
1270        }
1271    }
1272
1273    #[test]
1274    fn rsa_sha1_verify_only() {
1275        assert!(!SignatureAlgorithm::RsaSha1.signing_allowed());
1276        assert!(SignatureAlgorithm::RsaSha256.signing_allowed());
1277        assert!(SignatureAlgorithm::EcdsaP256Sha256.signing_allowed());
1278    }
1279
1280    // ── find_signature_node ──────────────────────────────────────────
1281
1282    #[test]
1283    fn find_signature_in_saml() {
1284        let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
1285            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
1286                <ds:SignedInfo/>
1287            </ds:Signature>
1288        </samlp:Response>"#;
1289        let doc = Document::parse(xml).unwrap();
1290        let sig = find_signature_node(&doc);
1291        assert!(sig.is_some());
1292        assert_eq!(sig.unwrap().tag_name().name(), "Signature");
1293    }
1294
1295    #[test]
1296    fn find_signature_missing() {
1297        let xml = "<root><child/></root>";
1298        let doc = Document::parse(xml).unwrap();
1299        assert!(find_signature_node(&doc).is_none());
1300    }
1301
1302    #[test]
1303    fn find_signature_ignores_wrong_namespace() {
1304        let xml = r#"<root><Signature xmlns="http://example.com/fake"/></root>"#;
1305        let doc = Document::parse(xml).unwrap();
1306        assert!(find_signature_node(&doc).is_none());
1307    }
1308
1309    // ── parse_key_info: dispatch parsing ──────────────────────────────
1310
1311    #[test]
1312    fn parse_key_info_dispatches_supported_children() {
1313        let cert_base64 = fixture_rsa_cert_base64();
1314        let xml = format!(
1315            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1316                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
1317            <KeyName>idp-signing-key</KeyName>
1318            <KeyValue>
1319                <RSAKeyValue>
1320                    <Modulus>AQAB</Modulus>
1321                    <Exponent>AQAB</Exponent>
1322                </RSAKeyValue>
1323            </KeyValue>
1324            <X509Data>
1325                <X509Certificate>{cert_base64}</X509Certificate>
1326                <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
1327                <X509IssuerSerial>
1328                    <X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
1329                    <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
1330                </X509IssuerSerial>
1331                <X509SKI>bcOXN/nsVl8GatRbcKrPbzIbw0Y=</X509SKI>
1332                <X509CRL>BAUGBw==</X509CRL>
1333                <dsig11:X509Digest Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">CAkK</dsig11:X509Digest>
1334            </X509Data>
1335            <dsig11:DEREncodedKeyValue>AQIDBA==</dsig11:DEREncodedKeyValue>
1336        </KeyInfo>"#
1337        );
1338        let doc = Document::parse(&xml).unwrap();
1339
1340        let key_info = parse_key_info(doc.root_element()).unwrap();
1341        assert_eq!(key_info.sources.len(), 4);
1342
1343        assert_eq!(
1344            key_info.sources[0],
1345            KeyInfoSource::KeyName("idp-signing-key".to_string())
1346        );
1347        assert_eq!(
1348            key_info.sources[1],
1349            KeyInfoSource::KeyValue(KeyValueInfo::RsaKeyValue)
1350        );
1351        let x509_info = match &key_info.sources[2] {
1352            KeyInfoSource::X509Data(x509) => x509,
1353            other => panic!("expected X509Data source, got {other:?}"),
1354        };
1355        let expected_cert = base64::engine::general_purpose::STANDARD
1356            .decode(&cert_base64)
1357            .expect("fixture PEM must contain valid base64");
1358        assert_eq!(x509_info.certificates, vec![expected_cert]);
1359        assert_eq!(
1360            x509_info.subject_names,
1361            vec![
1362                "C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048"
1363                    .to_string()
1364            ]
1365        );
1366        assert_eq!(
1367            x509_info.issuer_serials,
1368            vec![(
1369                "C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com".to_string(),
1370                "680572598617295163017172295025714171905498632019".to_string()
1371            )]
1372        );
1373        assert_eq!(
1374            x509_info.skis,
1375            vec![vec![
1376                109, 195, 151, 55, 249, 236, 86, 95, 6, 106, 212, 91, 112, 170, 207, 111, 50, 27,
1377                195, 70
1378            ]]
1379        );
1380        assert_eq!(x509_info.crls, vec![vec![4, 5, 6, 7]]);
1381        assert_eq!(
1382            x509_info.digests,
1383            vec![(
1384                "http://www.w3.org/2001/04/xmlenc#sha256".to_string(),
1385                vec![8, 9, 10]
1386            )]
1387        );
1388        assert_eq!(x509_info.parsed_certificates.len(), 1);
1389        assert_eq!(x509_info.certificate_chain, vec![0]);
1390        let parsed_cert = &x509_info.parsed_certificates[0];
1391        assert!(!parsed_cert.subject_dn.is_empty());
1392        assert!(!parsed_cert.issuer_dn.is_empty());
1393        assert_eq!(
1394            parsed_cert.serial_number_hex,
1395            "7735EE487F6862DAF1B3956D961CCB0FA6F34F53"
1396        );
1397        assert!(parsed_cert.subject_key_identifier.is_some());
1398        assert!(matches!(
1399            parsed_cert.public_key,
1400            X509PublicKeyInfo::Rsa { .. }
1401        ));
1402
1403        assert_eq!(
1404            key_info.sources[3],
1405            KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])
1406        );
1407    }
1408
1409    #[test]
1410    fn parse_key_info_ignores_unknown_children() {
1411        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1412            <Foo>bar</Foo>
1413            <KeyName>ok</KeyName>
1414        </KeyInfo>"#;
1415        let doc = Document::parse(xml).unwrap();
1416
1417        let key_info = parse_key_info(doc.root_element()).unwrap();
1418        assert_eq!(key_info.sources, vec![KeyInfoSource::KeyName("ok".into())]);
1419    }
1420
1421    #[test]
1422    fn parse_key_info_keyvalue_requires_single_child() {
1423        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1424            <KeyValue/>
1425        </KeyInfo>"#;
1426        let doc = Document::parse(xml).unwrap();
1427
1428        let err = parse_key_info(doc.root_element()).unwrap_err();
1429        assert!(matches!(err, ParseError::InvalidStructure(_)));
1430    }
1431
1432    #[test]
1433    fn parse_key_info_accepts_empty_x509data() {
1434        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1435            <X509Data/>
1436        </KeyInfo>"#;
1437        let doc = Document::parse(xml).unwrap();
1438
1439        let key_info = parse_key_info(doc.root_element()).unwrap();
1440        assert_eq!(
1441            key_info.sources,
1442            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
1443        );
1444    }
1445
1446    #[test]
1447    fn parse_key_info_rejects_unknown_xmlsig_child_in_x509data() {
1448        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1449            <X509Data>
1450                <Foo/>
1451            </X509Data>
1452        </KeyInfo>"#;
1453        let doc = Document::parse(xml).unwrap();
1454
1455        let err = parse_key_info(doc.root_element()).unwrap_err();
1456        assert!(matches!(err, ParseError::InvalidStructure(_)));
1457    }
1458
1459    #[test]
1460    fn parse_key_info_rejects_unknown_xmlsig11_child_in_x509data() {
1461        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1462                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
1463            <X509Data>
1464                <dsig11:Foo/>
1465            </X509Data>
1466        </KeyInfo>"#;
1467        let doc = Document::parse(xml).unwrap();
1468
1469        let err = parse_key_info(doc.root_element()).unwrap_err();
1470        assert!(matches!(err, ParseError::InvalidStructure(_)));
1471    }
1472
1473    #[test]
1474    fn parse_key_info_rejects_x509_issuer_serial_without_required_children() {
1475        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1476            <X509Data>
1477                <X509IssuerSerial>
1478                    <X509IssuerName>CN=CA</X509IssuerName>
1479                </X509IssuerSerial>
1480            </X509Data>
1481        </KeyInfo>"#;
1482        let doc = Document::parse(xml).unwrap();
1483
1484        let err = parse_key_info(doc.root_element()).unwrap_err();
1485        assert!(matches!(err, ParseError::InvalidStructure(_)));
1486    }
1487
1488    #[test]
1489    fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_issuer_name() {
1490        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1491            <X509Data>
1492                <X509IssuerSerial>
1493                    <X509IssuerName>CN=CA-1</X509IssuerName>
1494                    <X509IssuerName>CN=CA-2</X509IssuerName>
1495                    <X509SerialNumber>42</X509SerialNumber>
1496                </X509IssuerSerial>
1497            </X509Data>
1498        </KeyInfo>"#;
1499        let doc = Document::parse(xml).unwrap();
1500
1501        let err = parse_key_info(doc.root_element()).unwrap_err();
1502        assert!(matches!(err, ParseError::InvalidStructure(_)));
1503    }
1504
1505    #[test]
1506    fn parse_key_info_rejects_x509_issuer_serial_with_duplicate_serial_number() {
1507        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1508            <X509Data>
1509                <X509IssuerSerial>
1510                    <X509IssuerName>CN=CA</X509IssuerName>
1511                    <X509SerialNumber>1</X509SerialNumber>
1512                    <X509SerialNumber>2</X509SerialNumber>
1513                </X509IssuerSerial>
1514            </X509Data>
1515        </KeyInfo>"#;
1516        let doc = Document::parse(xml).unwrap();
1517
1518        let err = parse_key_info(doc.root_element()).unwrap_err();
1519        assert!(matches!(err, ParseError::InvalidStructure(_)));
1520    }
1521
1522    #[test]
1523    fn parse_key_info_rejects_x509_issuer_serial_with_whitespace_only_values() {
1524        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1525            <X509Data>
1526                <X509IssuerSerial>
1527                    <X509IssuerName>   </X509IssuerName>
1528                    <X509SerialNumber>
1529                        
1530                    </X509SerialNumber>
1531                </X509IssuerSerial>
1532            </X509Data>
1533        </KeyInfo>"#;
1534        let doc = Document::parse(xml).unwrap();
1535
1536        let err = parse_key_info(doc.root_element()).unwrap_err();
1537        assert!(matches!(err, ParseError::InvalidStructure(_)));
1538    }
1539
1540    #[test]
1541    fn parse_key_info_rejects_x509_issuer_serial_with_wrong_child_order() {
1542        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1543            <X509Data>
1544                <X509IssuerSerial>
1545                    <X509SerialNumber>42</X509SerialNumber>
1546                    <X509IssuerName>CN=CA</X509IssuerName>
1547                </X509IssuerSerial>
1548            </X509Data>
1549        </KeyInfo>"#;
1550        let doc = Document::parse(xml).unwrap();
1551
1552        let err = parse_key_info(doc.root_element()).unwrap_err();
1553        assert!(matches!(err, ParseError::InvalidStructure(_)));
1554    }
1555
1556    #[test]
1557    fn parse_key_info_rejects_x509_issuer_serial_with_extra_child_element() {
1558        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1559                              xmlns:foo="urn:example:foo">
1560            <X509Data>
1561                <X509IssuerSerial>
1562                    <X509IssuerName>CN=CA</X509IssuerName>
1563                    <X509SerialNumber>42</X509SerialNumber>
1564                    <foo:Extra/>
1565                </X509IssuerSerial>
1566            </X509Data>
1567        </KeyInfo>"#;
1568        let doc = Document::parse(xml).unwrap();
1569
1570        let err = parse_key_info(doc.root_element()).unwrap_err();
1571        assert!(matches!(err, ParseError::InvalidStructure(_)));
1572    }
1573
1574    #[test]
1575    fn parse_key_info_rejects_x509_digest_without_algorithm() {
1576        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1577                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
1578            <X509Data>
1579                <dsig11:X509Digest>AQID</dsig11:X509Digest>
1580            </X509Data>
1581        </KeyInfo>"#;
1582        let doc = Document::parse(xml).unwrap();
1583
1584        let err = parse_key_info(doc.root_element()).unwrap_err();
1585        assert!(matches!(err, ParseError::InvalidStructure(_)));
1586    }
1587
1588    #[test]
1589    fn parse_key_info_rejects_invalid_x509_certificate_base64() {
1590        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1591            <X509Data>
1592                <X509Certificate>%%%invalid%%%</X509Certificate>
1593            </X509Data>
1594        </KeyInfo>"#;
1595        let doc = Document::parse(xml).unwrap();
1596
1597        let err = parse_key_info(doc.root_element()).unwrap_err();
1598        assert!(matches!(err, ParseError::Base64(_)));
1599    }
1600
1601    #[test]
1602    fn parse_key_info_rejects_x509_data_exceeding_entry_budget() {
1603        let subjects = (0..(MAX_X509_DATA_ENTRY_COUNT + 1))
1604            .map(|idx| format!("<X509SubjectName>CN={idx}</X509SubjectName>"))
1605            .collect::<Vec<_>>()
1606            .join("");
1607        let xml = format!(
1608            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{subjects}</X509Data></KeyInfo>"
1609        );
1610        let doc = Document::parse(&xml).unwrap();
1611
1612        let err = parse_key_info(doc.root_element()).unwrap_err();
1613        assert!(matches!(err, ParseError::InvalidStructure(_)));
1614    }
1615
1616    #[test]
1617    fn parse_key_info_rejects_x509_data_exceeding_total_binary_budget() {
1618        let payload = base64::engine::general_purpose::STANDARD.encode(vec![0u8; 190_000]);
1619        let entries = (0..6)
1620            .map(|_| format!("<X509SKI>{payload}</X509SKI>"))
1621            .collect::<Vec<_>>()
1622            .join("");
1623        let xml = format!(
1624            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{entries}</X509Data></KeyInfo>"
1625        );
1626        let doc = Document::parse(&xml).unwrap();
1627
1628        let err = parse_key_info(doc.root_element()).unwrap_err();
1629        assert!(matches!(err, ParseError::InvalidStructure(_)));
1630    }
1631
1632    #[test]
1633    fn parse_key_info_rejects_x509_certificate_with_invalid_der() {
1634        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1635            <X509Data>
1636                <X509Certificate>AQID</X509Certificate>
1637            </X509Data>
1638        </KeyInfo>"#;
1639        let doc = Document::parse(xml).unwrap();
1640
1641        let err = parse_key_info(doc.root_element()).unwrap_err();
1642        assert!(matches!(err, ParseError::InvalidStructure(_)));
1643    }
1644
1645    #[test]
1646    fn parse_key_info_rejects_x509_certificate_with_trailing_der_bytes() {
1647        let mut cert = base64::engine::general_purpose::STANDARD
1648            .decode(fixture_rsa_cert_base64())
1649            .unwrap();
1650        cert.extend_from_slice(&[0x00, 0x01]);
1651        let cert_base64 = base64::engine::general_purpose::STANDARD.encode(cert);
1652        let xml = format!(
1653            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1654                <X509Data>
1655                    <X509Certificate>{cert_base64}</X509Certificate>
1656                </X509Data>
1657            </KeyInfo>"#
1658        );
1659        let doc = Document::parse(&xml).unwrap();
1660
1661        let err = parse_key_info(doc.root_element()).unwrap_err();
1662        assert!(matches!(err, ParseError::InvalidStructure(_)));
1663    }
1664
1665    #[test]
1666    fn parse_key_info_marks_unsupported_spki_algorithm_as_unsupported() {
1667        let xml = include_str!(
1668            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt.xml"
1669        );
1670        let doc = Document::parse(xml).unwrap();
1671        let key_info_node = doc
1672            .descendants()
1673            .find(|node| {
1674                node.is_element()
1675                    && node.tag_name().namespace() == Some(XMLDSIG_NS)
1676                    && node.tag_name().name() == "KeyInfo"
1677            })
1678            .expect("fixture must contain ds:KeyInfo");
1679
1680        let key_info = parse_key_info(key_info_node).expect("KeyInfo parse should succeed");
1681        let x509_info = match &key_info.sources[0] {
1682            KeyInfoSource::X509Data(x509) => x509,
1683            other => panic!("expected X509Data source, got {other:?}"),
1684        };
1685        assert_eq!(x509_info.certificates.len(), 1);
1686        assert_eq!(x509_info.parsed_certificates.len(), 1);
1687        assert_eq!(x509_info.certificate_chain, vec![0]);
1688        let parsed_cert = &x509_info.parsed_certificates[0];
1689        assert!(!parsed_cert.subject_dn.is_empty());
1690        assert!(!parsed_cert.issuer_dn.is_empty());
1691        assert!(parsed_cert.subject_key_identifier.is_some());
1692        assert!(matches!(
1693            parsed_cert.public_key,
1694            X509PublicKeyInfo::Unsupported { .. }
1695        ));
1696    }
1697
1698    #[test]
1699    fn parse_key_info_orders_x509_certificate_chain_from_signing_cert() {
1700        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
1701        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
1702        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
1703        let xml = format!(
1704            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1705                <X509Data>
1706                    <X509Certificate>{root}</X509Certificate>
1707                    <X509Certificate>{intermediate}</X509Certificate>
1708                    <X509Certificate>{leaf}</X509Certificate>
1709                </X509Data>
1710            </KeyInfo>"#
1711        );
1712        let doc = Document::parse(&xml).unwrap();
1713
1714        let key_info = parse_key_info(doc.root_element()).unwrap();
1715        let x509_info = match &key_info.sources[0] {
1716            KeyInfoSource::X509Data(x509) => x509,
1717            other => panic!("expected X509Data source, got {other:?}"),
1718        };
1719
1720        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
1721    }
1722
1723    #[test]
1724    fn parse_key_info_uses_issuer_serial_to_select_x509_signing_certificate() {
1725        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
1726        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
1727        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
1728        let xml = format!(
1729            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1730                <X509Data>
1731                    <X509IssuerSerial>
1732                        <X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
1733                        <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
1734                    </X509IssuerSerial>
1735                    <X509Certificate>{root}</X509Certificate>
1736                    <X509Certificate>{intermediate}</X509Certificate>
1737                    <X509Certificate>{leaf}</X509Certificate>
1738                </X509Data>
1739            </KeyInfo>"#
1740        );
1741        let doc = Document::parse(&xml).unwrap();
1742
1743        let key_info = parse_key_info(doc.root_element()).unwrap();
1744        let x509_info = match &key_info.sources[0] {
1745            KeyInfoSource::X509Data(x509) => x509,
1746            other => panic!("expected X509Data source, got {other:?}"),
1747        };
1748
1749        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
1750    }
1751
1752    #[test]
1753    fn parse_key_info_uses_decimal_issuer_serial_to_select_x509_signing_certificate() {
1754        assert_eq!(
1755            x509_serial_decimal_to_hex("680572598617295163017172295025714171905498632019")
1756                .as_deref(),
1757            Some("7735EE487F6862DAF1B3956D961CCB0FA6F34F53")
1758        );
1759        let root = fixture_cert_base64("../../tests/fixtures/keys/cacert.pem");
1760        let intermediate = fixture_cert_base64("../../tests/fixtures/keys/ca2cert.pem");
1761        let leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
1762        let other_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
1763        let xml = format!(
1764            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1765                <X509Data>
1766                    <X509IssuerSerial>
1767                        <X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
1768                        <X509SerialNumber>680572598617295163017172295025714171905498632019</X509SerialNumber>
1769                    </X509IssuerSerial>
1770                    <X509Certificate>{root}</X509Certificate>
1771                    <X509Certificate>{intermediate}</X509Certificate>
1772                    <X509Certificate>{leaf}</X509Certificate>
1773                    <X509Certificate>{other_leaf}</X509Certificate>
1774                </X509Data>
1775            </KeyInfo>"#
1776        );
1777        let doc = Document::parse(&xml).unwrap();
1778
1779        let key_info = parse_key_info(doc.root_element()).unwrap();
1780        let x509_info = match &key_info.sources[0] {
1781            KeyInfoSource::X509Data(x509) => x509,
1782            other => panic!("expected X509Data source, got {other:?}"),
1783        };
1784
1785        assert_eq!(x509_info.certificate_chain, vec![2, 1, 0]);
1786    }
1787
1788    #[test]
1789    fn parse_key_info_rejects_ambiguous_x509_signing_certificate_candidates() {
1790        let first_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
1791        let second_leaf = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
1792        let xml = format!(
1793            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1794                <X509Data>
1795                    <X509Certificate>{first_leaf}</X509Certificate>
1796                    <X509Certificate>{second_leaf}</X509Certificate>
1797                </X509Data>
1798            </KeyInfo>"#
1799        );
1800        let doc = Document::parse(&xml).unwrap();
1801
1802        let err = parse_key_info(doc.root_element()).unwrap_err();
1803        assert!(matches!(err, ParseError::InvalidStructure(_)));
1804    }
1805
1806    #[test]
1807    fn parse_key_info_rejects_unmatched_x509_lookup_identifier() {
1808        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
1809        let xml = format!(
1810            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1811                <X509Data>
1812                    <X509SubjectName>CN=Not The Embedded Certificate</X509SubjectName>
1813                    <X509Certificate>{cert}</X509Certificate>
1814                </X509Data>
1815            </KeyInfo>"#
1816        );
1817        let doc = Document::parse(&xml).unwrap();
1818
1819        let err = parse_key_info(doc.root_element()).unwrap_err();
1820        assert!(
1821            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
1822        );
1823    }
1824
1825    #[test]
1826    fn parse_key_info_rejects_malformed_issuer_serial_even_with_matching_subject() {
1827        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
1828        let xml = format!(
1829            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1830                <X509Data>
1831                    <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
1832                    <X509IssuerSerial>
1833                        <X509IssuerName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Second level CA, CN=Aleksey Sanin, Email=xmlsec@aleksey.com</X509IssuerName>
1834                        <X509SerialNumber>not-a-decimal-serial</X509SerialNumber>
1835                    </X509IssuerSerial>
1836                    <X509Certificate>{cert}</X509Certificate>
1837                </X509Data>
1838            </KeyInfo>"#
1839        );
1840        let doc = Document::parse(&xml).unwrap();
1841
1842        let err = parse_key_info(doc.root_element()).unwrap_err();
1843        assert!(
1844            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
1845        );
1846    }
1847
1848    #[test]
1849    fn parse_key_info_rejects_unmatched_ski_even_with_matching_subject() {
1850        let cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
1851        let xml = format!(
1852            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1853                <X509Data>
1854                    <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
1855                    <X509SKI>AQIDBA==</X509SKI>
1856                    <X509Certificate>{cert}</X509Certificate>
1857                </X509Data>
1858            </KeyInfo>"#
1859        );
1860        let doc = Document::parse(&xml).unwrap();
1861
1862        let err = parse_key_info(doc.root_element()).unwrap_err();
1863        assert!(
1864            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers"))
1865        );
1866    }
1867
1868    #[test]
1869    fn parse_key_info_rejects_lookup_hints_for_different_certificates() {
1870        let first_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
1871        let second_cert = fixture_cert_base64("../../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
1872        let xml = format!(
1873            r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
1874                <X509Data>
1875                    <X509SubjectName>C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), CN=Test Key rsa-2048</X509SubjectName>
1876                    <X509SKI>60zMLKCfzQ3qnXAzABzRNpdgQ8Q=</X509SKI>
1877                    <X509Certificate>{first_cert}</X509Certificate>
1878                    <X509Certificate>{second_cert}</X509Certificate>
1879                </X509Data>
1880            </KeyInfo>"#
1881        );
1882        let doc = Document::parse(&xml).unwrap();
1883
1884        let err = parse_key_info(doc.root_element()).unwrap_err();
1885        assert!(
1886            matches!(err, ParseError::InvalidStructure(message) if message.contains("lookup identifiers match multiple certificates"))
1887        );
1888    }
1889
1890    #[test]
1891    fn build_x509_certificate_chain_rejects_chain_exceeding_max_depth() {
1892        let parsed_certificates = (0..=MAX_X509_CHAIN_DEPTH)
1893            .map(|idx| ParsedX509Certificate {
1894                subject_dn: format!("CN=cert-{idx}"),
1895                issuer_dn: if idx == MAX_X509_CHAIN_DEPTH {
1896                    format!("CN=cert-{idx}")
1897                } else {
1898                    format!("CN=cert-{}", idx + 1)
1899                },
1900                serial_number: vec![u8::try_from(idx).unwrap()],
1901                serial_number_hex: format!("{idx:02X}"),
1902                subject_key_identifier: None,
1903                public_key: X509PublicKeyInfo::Unsupported {
1904                    algorithm_oid: "1.2.3.4".into(),
1905                },
1906            })
1907            .collect();
1908        let info = X509DataInfo {
1909            parsed_certificates,
1910            ..X509DataInfo::default()
1911        };
1912
1913        let err = build_x509_certificate_chain(&info).unwrap_err();
1914        assert!(
1915            matches!(err, ParseError::InvalidStructure(message) if message.contains("maximum depth"))
1916        );
1917    }
1918
1919    #[test]
1920    fn x509_serial_hex_strips_der_sign_extension_zeroes() {
1921        assert_eq!(format_x509_serial_value_hex(&[0x00, 0xFF]), "FF");
1922        assert_eq!(format_x509_serial_value_hex(&[0x00, 0x7F]), "7F");
1923        assert_eq!(format_x509_serial_value_hex(&[0x00, 0x00]), "00");
1924    }
1925
1926    #[test]
1927    fn parse_key_info_accepts_large_textual_x509_entries_within_entry_budget() {
1928        let issuer_name = "C".repeat(MAX_X509_ISSUER_NAME_TEXT_LEN);
1929        let serial_number = "7".repeat(MAX_X509_SERIAL_NUMBER_TEXT_LEN);
1930        let issuer_serials = (0..52)
1931            .map(|_| {
1932                format!(
1933                    "<X509IssuerSerial><X509IssuerName>{issuer_name}</X509IssuerName><X509SerialNumber>{serial_number}</X509SerialNumber></X509IssuerSerial>"
1934                )
1935            })
1936            .collect::<Vec<_>>()
1937            .join("");
1938        let xml = format!(
1939            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><X509Data>{issuer_serials}</X509Data></KeyInfo>"
1940        );
1941        let doc = Document::parse(&xml).unwrap();
1942
1943        let key_info = parse_key_info(doc.root_element()).unwrap();
1944        let parsed = match &key_info.sources[0] {
1945            KeyInfoSource::X509Data(x509) => x509,
1946            _ => panic!("expected X509Data source"),
1947        };
1948        assert_eq!(parsed.issuer_serials.len(), 52);
1949    }
1950
1951    #[test]
1952    fn parse_key_info_accepts_x509data_with_only_foreign_namespace_children() {
1953        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1954                              xmlns:foo="urn:example:foo">
1955            <X509Data>
1956                <foo:Bar/>
1957            </X509Data>
1958        </KeyInfo>"#;
1959        let doc = Document::parse(xml).unwrap();
1960
1961        let key_info = parse_key_info(doc.root_element()).unwrap();
1962        assert_eq!(
1963            key_info.sources,
1964            vec![KeyInfoSource::X509Data(X509DataInfo::default())]
1965        );
1966    }
1967
1968    #[test]
1969    fn parse_key_info_der_encoded_key_value_rejects_invalid_base64() {
1970        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1971                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
1972            <dsig11:DEREncodedKeyValue>%%%invalid%%%</dsig11:DEREncodedKeyValue>
1973        </KeyInfo>"#;
1974        let doc = Document::parse(xml).unwrap();
1975
1976        let err = parse_key_info(doc.root_element()).unwrap_err();
1977        assert!(matches!(err, ParseError::Base64(_)));
1978    }
1979
1980    #[test]
1981    fn parse_key_info_der_encoded_key_value_accepts_xml_whitespace() {
1982        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
1983                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
1984            <dsig11:DEREncodedKeyValue>
1985                AQID
1986                BA==
1987            </dsig11:DEREncodedKeyValue>
1988        </KeyInfo>"#;
1989        let doc = Document::parse(xml).unwrap();
1990
1991        let key_info = parse_key_info(doc.root_element()).unwrap();
1992        assert_eq!(
1993            key_info.sources,
1994            vec![KeyInfoSource::DerEncodedKeyValue(vec![1, 2, 3, 4])]
1995        );
1996    }
1997
1998    #[test]
1999    fn parse_key_info_dispatches_dsig11_ec_keyvalue() {
2000        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2001                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2002            <KeyValue>
2003                <dsig11:ECKeyValue/>
2004            </KeyValue>
2005        </KeyInfo>"#;
2006        let doc = Document::parse(xml).unwrap();
2007
2008        let key_info = parse_key_info(doc.root_element()).unwrap();
2009        assert_eq!(
2010            key_info.sources,
2011            vec![KeyInfoSource::KeyValue(KeyValueInfo::EcKeyValue)]
2012        );
2013    }
2014
2015    #[test]
2016    fn parse_key_info_marks_ds_namespace_ec_keyvalue_as_unsupported() {
2017        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2018            <KeyValue>
2019                <ECKeyValue/>
2020            </KeyValue>
2021        </KeyInfo>"#;
2022        let doc = Document::parse(xml).unwrap();
2023
2024        let key_info = parse_key_info(doc.root_element()).unwrap();
2025        assert_eq!(
2026            key_info.sources,
2027            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
2028                namespace: Some(XMLDSIG_NS.to_string()),
2029                local_name: "ECKeyValue".into(),
2030            })]
2031        );
2032    }
2033
2034    #[test]
2035    fn parse_key_info_keeps_unsupported_keyvalue_child_as_marker() {
2036        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2037            <KeyValue>
2038                <DSAKeyValue/>
2039            </KeyValue>
2040        </KeyInfo>"#;
2041        let doc = Document::parse(xml).unwrap();
2042
2043        let key_info = parse_key_info(doc.root_element()).unwrap();
2044        assert_eq!(
2045            key_info.sources,
2046            vec![KeyInfoSource::KeyValue(KeyValueInfo::Unsupported {
2047                namespace: Some(XMLDSIG_NS.to_string()),
2048                local_name: "DSAKeyValue".into(),
2049            })]
2050        );
2051    }
2052
2053    #[test]
2054    fn parse_key_info_rejects_keyname_with_child_elements() {
2055        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2056            <KeyName>ok<foo/></KeyName>
2057        </KeyInfo>"#;
2058        let doc = Document::parse(xml).unwrap();
2059
2060        let err = parse_key_info(doc.root_element()).unwrap_err();
2061        assert!(matches!(err, ParseError::InvalidStructure(_)));
2062    }
2063
2064    #[test]
2065    fn parse_key_info_preserves_keyname_text_without_trimming() {
2066        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2067            <KeyName>  signing key  </KeyName>
2068        </KeyInfo>"#;
2069        let doc = Document::parse(xml).unwrap();
2070
2071        let key_info = parse_key_info(doc.root_element()).unwrap();
2072        assert_eq!(
2073            key_info.sources,
2074            vec![KeyInfoSource::KeyName("  signing key  ".into())]
2075        );
2076    }
2077
2078    #[test]
2079    fn parse_key_info_rejects_oversized_keyname_text() {
2080        let oversized = "A".repeat(4097);
2081        let xml = format!(
2082            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><KeyName>{oversized}</KeyName></KeyInfo>"
2083        );
2084        let doc = Document::parse(&xml).unwrap();
2085
2086        let err = parse_key_info(doc.root_element()).unwrap_err();
2087        assert!(matches!(err, ParseError::InvalidStructure(_)));
2088    }
2089
2090    #[test]
2091    fn parse_key_info_rejects_non_whitespace_mixed_content() {
2092        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">oops<KeyName>k</KeyName></KeyInfo>"#;
2093        let doc = Document::parse(xml).unwrap();
2094
2095        let err = parse_key_info(doc.root_element()).unwrap_err();
2096        assert!(matches!(err, ParseError::InvalidStructure(_)));
2097    }
2098
2099    #[test]
2100    fn parse_key_info_rejects_nbsp_as_non_xml_whitespace_mixed_content() {
2101        let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\u{00A0}<KeyName>k</KeyName></KeyInfo>";
2102        let doc = Document::parse(xml).unwrap();
2103
2104        let err = parse_key_info(doc.root_element()).unwrap_err();
2105        assert!(matches!(err, ParseError::InvalidStructure(_)));
2106    }
2107
2108    #[test]
2109    fn parse_key_info_der_encoded_key_value_rejects_oversized_payload() {
2110        let oversized =
2111            base64::engine::general_purpose::STANDARD
2112                .encode(vec![0u8; MAX_DER_ENCODED_KEY_VALUE_LEN + 1]);
2113        let xml = format!(
2114            "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>{oversized}</dsig11:DEREncodedKeyValue></KeyInfo>"
2115        );
2116        let doc = Document::parse(&xml).unwrap();
2117
2118        let err = parse_key_info(doc.root_element()).unwrap_err();
2119        assert!(matches!(err, ParseError::InvalidStructure(_)));
2120    }
2121
2122    #[test]
2123    fn parse_key_info_der_encoded_key_value_rejects_empty_payload() {
2124        let xml = r#"<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2125                              xmlns:dsig11="http://www.w3.org/2009/xmldsig11#">
2126            <dsig11:DEREncodedKeyValue>
2127                
2128            </dsig11:DEREncodedKeyValue>
2129        </KeyInfo>"#;
2130        let doc = Document::parse(xml).unwrap();
2131
2132        let err = parse_key_info(doc.root_element()).unwrap_err();
2133        assert!(matches!(err, ParseError::InvalidStructure(_)));
2134    }
2135
2136    #[test]
2137    fn parse_key_info_der_encoded_key_value_non_xml_ascii_whitespace_is_not_parseable_xml() {
2138        let xml = "<KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:dsig11=\"http://www.w3.org/2009/xmldsig11#\"><dsig11:DEREncodedKeyValue>\u{000C}</dsig11:DEREncodedKeyValue></KeyInfo>";
2139        assert!(Document::parse(xml).is_err());
2140    }
2141
2142    // ── parse_signed_info: happy path ────────────────────────────────
2143
2144    #[test]
2145    fn parse_signed_info_rsa_sha256_with_reference() {
2146        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2147            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2148            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2149            <Reference URI="">
2150                <Transforms>
2151                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
2152                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2153                </Transforms>
2154                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2155                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2156            </Reference>
2157        </SignedInfo>"#;
2158        let doc = Document::parse(xml).unwrap();
2159        let si = parse_signed_info(doc.root_element()).unwrap();
2160
2161        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
2162        assert_eq!(si.references.len(), 1);
2163
2164        let r = &si.references[0];
2165        assert_eq!(r.uri.as_deref(), Some(""));
2166        assert_eq!(r.digest_method, DigestAlgorithm::Sha256);
2167        assert_eq!(r.digest_value, vec![0u8; 32]);
2168        assert_eq!(r.transforms.len(), 2);
2169    }
2170
2171    #[test]
2172    fn parse_signed_info_multiple_references() {
2173        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2174            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
2175            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
2176            <Reference URI="#a">
2177                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2178                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2179            </Reference>
2180            <Reference URI="#b">
2181                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
2182                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2183            </Reference>
2184        </SignedInfo>"##;
2185        let doc = Document::parse(xml).unwrap();
2186        let si = parse_signed_info(doc.root_element()).unwrap();
2187
2188        assert_eq!(si.signature_method, SignatureAlgorithm::EcdsaP256Sha256);
2189        assert_eq!(si.references.len(), 2);
2190        assert_eq!(si.references[0].uri.as_deref(), Some("#a"));
2191        assert_eq!(si.references[0].digest_method, DigestAlgorithm::Sha256);
2192        assert_eq!(si.references[1].uri.as_deref(), Some("#b"));
2193        assert_eq!(si.references[1].digest_method, DigestAlgorithm::Sha1);
2194    }
2195
2196    #[test]
2197    fn parse_reference_without_transforms() {
2198        // Transforms element is optional
2199        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2200            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2201            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2202            <Reference URI="#obj">
2203                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2204                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2205            </Reference>
2206        </SignedInfo>"##;
2207        let doc = Document::parse(xml).unwrap();
2208        let si = parse_signed_info(doc.root_element()).unwrap();
2209
2210        assert!(si.references[0].transforms.is_empty());
2211    }
2212
2213    #[test]
2214    fn parse_reference_with_all_attributes() {
2215        let xml = r##"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2216            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2217            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2218            <Reference URI="#data" Id="ref1" Type="http://www.w3.org/2000/09/xmldsig#Object">
2219                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2220                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2221            </Reference>
2222        </SignedInfo>"##;
2223        let doc = Document::parse(xml).unwrap();
2224        let si = parse_signed_info(doc.root_element()).unwrap();
2225        let r = &si.references[0];
2226
2227        assert_eq!(r.uri.as_deref(), Some("#data"));
2228        assert_eq!(r.id.as_deref(), Some("ref1"));
2229        assert_eq!(
2230            r.ref_type.as_deref(),
2231            Some("http://www.w3.org/2000/09/xmldsig#Object")
2232        );
2233    }
2234
2235    #[test]
2236    fn parse_reference_absent_uri() {
2237        // URI attribute is optional per spec
2238        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2239            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2240            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2241            <Reference>
2242                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2243                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2244            </Reference>
2245        </SignedInfo>"#;
2246        let doc = Document::parse(xml).unwrap();
2247        let si = parse_signed_info(doc.root_element()).unwrap();
2248        assert!(si.references[0].uri.is_none());
2249    }
2250
2251    #[test]
2252    fn parse_signed_info_preserves_inclusive_prefixes() {
2253        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2254                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
2255            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
2256                <ec:InclusiveNamespaces PrefixList="ds saml #default"/>
2257            </CanonicalizationMethod>
2258            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2259            <Reference URI="">
2260                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2261                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2262            </Reference>
2263        </SignedInfo>"#;
2264        let doc = Document::parse(xml).unwrap();
2265
2266        let si = parse_signed_info(doc.root_element()).unwrap();
2267        assert!(si.c14n_method.inclusive_prefixes().contains("ds"));
2268        assert!(si.c14n_method.inclusive_prefixes().contains("saml"));
2269        assert!(si.c14n_method.inclusive_prefixes().contains(""));
2270    }
2271
2272    // ── parse_signed_info: error cases ───────────────────────────────
2273
2274    #[test]
2275    fn missing_canonicalization_method() {
2276        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2277            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2278            <Reference URI="">
2279                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2280                <DigestValue>dGVzdA==</DigestValue>
2281            </Reference>
2282        </SignedInfo>"#;
2283        let doc = Document::parse(xml).unwrap();
2284        let result = parse_signed_info(doc.root_element());
2285        assert!(result.is_err());
2286        // SignatureMethod is first child but expected CanonicalizationMethod
2287        assert!(matches!(
2288            result.unwrap_err(),
2289            ParseError::InvalidStructure(_)
2290        ));
2291    }
2292
2293    #[test]
2294    fn missing_signature_method() {
2295        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2296            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2297            <Reference URI="">
2298                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2299                <DigestValue>dGVzdA==</DigestValue>
2300            </Reference>
2301        </SignedInfo>"#;
2302        let doc = Document::parse(xml).unwrap();
2303        let result = parse_signed_info(doc.root_element());
2304        assert!(result.is_err());
2305        // Reference is second child but expected SignatureMethod
2306        assert!(matches!(
2307            result.unwrap_err(),
2308            ParseError::InvalidStructure(_)
2309        ));
2310    }
2311
2312    #[test]
2313    fn no_references() {
2314        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2315            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2316            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2317        </SignedInfo>"#;
2318        let doc = Document::parse(xml).unwrap();
2319        let result = parse_signed_info(doc.root_element());
2320        assert!(matches!(
2321            result.unwrap_err(),
2322            ParseError::MissingElement {
2323                element: "Reference"
2324            }
2325        ));
2326    }
2327
2328    #[test]
2329    fn unsupported_c14n_algorithm() {
2330        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2331            <CanonicalizationMethod Algorithm="http://example.com/bogus-c14n"/>
2332            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2333            <Reference URI="">
2334                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2335                <DigestValue>dGVzdA==</DigestValue>
2336            </Reference>
2337        </SignedInfo>"#;
2338        let doc = Document::parse(xml).unwrap();
2339        let result = parse_signed_info(doc.root_element());
2340        assert!(matches!(
2341            result.unwrap_err(),
2342            ParseError::UnsupportedAlgorithm { .. }
2343        ));
2344    }
2345
2346    #[test]
2347    fn unsupported_signature_algorithm() {
2348        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2349            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2350            <SignatureMethod Algorithm="http://example.com/bogus-sign"/>
2351            <Reference URI="">
2352                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2353                <DigestValue>dGVzdA==</DigestValue>
2354            </Reference>
2355        </SignedInfo>"#;
2356        let doc = Document::parse(xml).unwrap();
2357        let result = parse_signed_info(doc.root_element());
2358        assert!(matches!(
2359            result.unwrap_err(),
2360            ParseError::UnsupportedAlgorithm { .. }
2361        ));
2362    }
2363
2364    #[test]
2365    fn unsupported_digest_algorithm() {
2366        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2367            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2368            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2369            <Reference URI="">
2370                <DigestMethod Algorithm="http://example.com/bogus-digest"/>
2371                <DigestValue>dGVzdA==</DigestValue>
2372            </Reference>
2373        </SignedInfo>"#;
2374        let doc = Document::parse(xml).unwrap();
2375        let result = parse_signed_info(doc.root_element());
2376        assert!(matches!(
2377            result.unwrap_err(),
2378            ParseError::UnsupportedAlgorithm { .. }
2379        ));
2380    }
2381
2382    #[test]
2383    fn missing_digest_method() {
2384        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2385            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2386            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2387            <Reference URI="">
2388                <DigestValue>dGVzdA==</DigestValue>
2389            </Reference>
2390        </SignedInfo>"#;
2391        let doc = Document::parse(xml).unwrap();
2392        let result = parse_signed_info(doc.root_element());
2393        // DigestValue is not DigestMethod
2394        assert!(result.is_err());
2395    }
2396
2397    #[test]
2398    fn missing_digest_value() {
2399        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2400            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2401            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2402            <Reference URI="">
2403                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2404            </Reference>
2405        </SignedInfo>"#;
2406        let doc = Document::parse(xml).unwrap();
2407        let result = parse_signed_info(doc.root_element());
2408        assert!(matches!(
2409            result.unwrap_err(),
2410            ParseError::MissingElement {
2411                element: "DigestValue"
2412            }
2413        ));
2414    }
2415
2416    #[test]
2417    fn invalid_base64_digest_value() {
2418        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2419            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2420            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2421            <Reference URI="">
2422                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2423                <DigestValue>!!!not-base64!!!</DigestValue>
2424            </Reference>
2425        </SignedInfo>"#;
2426        let doc = Document::parse(xml).unwrap();
2427        let result = parse_signed_info(doc.root_element());
2428        assert!(matches!(result.unwrap_err(), ParseError::Base64(_)));
2429    }
2430
2431    #[test]
2432    fn digest_value_length_must_match_digest_method() {
2433        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2434            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2435            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2436            <Reference URI="">
2437                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2438                <DigestValue>dGVzdA==</DigestValue>
2439            </Reference>
2440        </SignedInfo>"#;
2441        let doc = Document::parse(xml).unwrap();
2442
2443        let result = parse_signed_info(doc.root_element());
2444        assert!(matches!(
2445            result.unwrap_err(),
2446            ParseError::DigestLengthMismatch {
2447                algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
2448                expected: 32,
2449                actual: 4,
2450            }
2451        ));
2452    }
2453
2454    #[test]
2455    fn inclusive_prefixes_on_inclusive_c14n_is_rejected() {
2456        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"
2457                                 xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#">
2458            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">
2459                <ec:InclusiveNamespaces PrefixList="ds"/>
2460            </CanonicalizationMethod>
2461            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2462            <Reference URI="">
2463                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
2464                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2465            </Reference>
2466        </SignedInfo>"#;
2467        let doc = Document::parse(xml).unwrap();
2468
2469        let result = parse_signed_info(doc.root_element());
2470        assert!(matches!(
2471            result.unwrap_err(),
2472            ParseError::UnsupportedAlgorithm { .. }
2473        ));
2474    }
2475
2476    #[test]
2477    fn extra_element_after_digest_value() {
2478        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2479            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2480            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2481            <Reference URI="">
2482                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2483                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</DigestValue>
2484                <Unexpected/>
2485            </Reference>
2486        </SignedInfo>"#;
2487        let doc = Document::parse(xml).unwrap();
2488        let result = parse_signed_info(doc.root_element());
2489        assert!(matches!(
2490            result.unwrap_err(),
2491            ParseError::InvalidStructure(_)
2492        ));
2493    }
2494
2495    #[test]
2496    fn digest_value_with_element_child_is_rejected() {
2497        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2498            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2499            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2500            <Reference URI="">
2501                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
2502                <DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAA=<Junk/>AAAA</DigestValue>
2503            </Reference>
2504        </SignedInfo>"#;
2505        let doc = Document::parse(xml).unwrap();
2506
2507        let result = parse_signed_info(doc.root_element());
2508        assert!(matches!(
2509            result.unwrap_err(),
2510            ParseError::InvalidStructure(_)
2511        ));
2512    }
2513
2514    #[test]
2515    fn wrong_namespace_on_signed_info() {
2516        let xml = r#"<SignedInfo xmlns="http://example.com/fake">
2517            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2518        </SignedInfo>"#;
2519        let doc = Document::parse(xml).unwrap();
2520        let result = parse_signed_info(doc.root_element());
2521        assert!(matches!(
2522            result.unwrap_err(),
2523            ParseError::InvalidStructure(_)
2524        ));
2525    }
2526
2527    // ── Whitespace-wrapped base64 ────────────────────────────────────
2528
2529    #[test]
2530    fn base64_with_whitespace() {
2531        let xml = r#"<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
2532            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2533            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
2534            <Reference URI="">
2535                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
2536                <DigestValue>
2537                    AAAAAAAA
2538                    AAAAAAAAAAAAAAAAAAA=
2539                </DigestValue>
2540            </Reference>
2541        </SignedInfo>"#;
2542        let doc = Document::parse(xml).unwrap();
2543        let si = parse_signed_info(doc.root_element()).unwrap();
2544        assert_eq!(si.references[0].digest_value, vec![0u8; 20]);
2545    }
2546
2547    #[test]
2548    fn base64_decode_digest_accepts_xml_whitespace_chars() {
2549        let digest =
2550            base64_decode_digest("AAAA\tAAAA\rAAAA\nAAAA AAAAAAAAAAA=", DigestAlgorithm::Sha1)
2551                .expect("XML whitespace in DigestValue must be accepted");
2552        assert_eq!(digest, vec![0u8; 20]);
2553    }
2554
2555    #[test]
2556    fn base64_decode_digest_rejects_non_xml_ascii_whitespace() {
2557        let err = base64_decode_digest(
2558            "AAAA\u{000C}AAAAAAAAAAAAAAAAAAAAAAA=",
2559            DigestAlgorithm::Sha1,
2560        )
2561        .expect_err("form-feed/vertical-tab in DigestValue must be rejected");
2562        assert!(matches!(err, ParseError::Base64(_)));
2563    }
2564
2565    #[test]
2566    fn base64_decode_digest_rejects_oversized_base64_before_decode() {
2567        let err = base64_decode_digest("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", DigestAlgorithm::Sha1)
2568            .expect_err("oversized DigestValue base64 must fail before decode");
2569        match err {
2570            ParseError::Base64(message) => {
2571                assert!(
2572                    message.contains("DigestValue exceeds maximum allowed base64 length"),
2573                    "unexpected message: {message}"
2574                );
2575            }
2576            other => panic!("expected ParseError::Base64, got {other:?}"),
2577        }
2578    }
2579
2580    // ── Real-world SAML structure ────────────────────────────────────
2581
2582    #[test]
2583    fn saml_response_signed_info() {
2584        let xml = r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2585            <ds:SignedInfo>
2586                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2587                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
2588                <ds:Reference URI="#_resp1">
2589                    <ds:Transforms>
2590                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
2591                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
2592                    </ds:Transforms>
2593                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
2594                    <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
2595                </ds:Reference>
2596            </ds:SignedInfo>
2597            <ds:SignatureValue>ZmFrZQ==</ds:SignatureValue>
2598        </ds:Signature>"##;
2599        let doc = Document::parse(xml).unwrap();
2600
2601        // Find SignedInfo within Signature
2602        let sig_node = doc.root_element();
2603        let signed_info_node = sig_node
2604            .children()
2605            .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
2606            .unwrap();
2607
2608        let si = parse_signed_info(signed_info_node).unwrap();
2609        assert_eq!(si.signature_method, SignatureAlgorithm::RsaSha256);
2610        assert_eq!(si.references.len(), 1);
2611        assert_eq!(si.references[0].uri.as_deref(), Some("#_resp1"));
2612        assert_eq!(si.references[0].transforms.len(), 2);
2613        assert_eq!(si.references[0].digest_value, vec![0u8; 32]);
2614    }
2615}