Skip to main content

xdoc/signature/
validation_profile.rs

1use crate::core::{Document, NodeId, NodeKind, XmlError, XmlResult};
2
3use super::xmldsig::{
4    element_children, find_signature, optional_attribute, parse_signed_info, required_child,
5    resolve_signature_parent,
6};
7use super::{
8    verify_enveloped, verify_xades_baseline_b_enveloped, verify_xades_bes_enveloped,
9    verify_xades_epes_enveloped, verify_xades_validation_data, DigestAlgorithm, SignatureAlgorithm,
10    SignaturePlacement, SignaturePolicy, SigningProvider, XadesConfig, XadesProfile,
11    XadesValidationDataConfig, XmlDsigConfig, XADES_NAMESPACE_URI,
12    XMLDSIG_SIGNED_PROPERTIES_TYPE_URI,
13};
14
15/// Generic strictness level expected by a validation profile.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum SignatureValidationLevel {
18    XmlDsig,
19    XadesBes,
20    XadesEpes(SignaturePolicy),
21    XadesBaselineB { policy: Option<SignaturePolicy> },
22    XadesT,
23    XadesLt,
24    XadesLta,
25}
26
27/// Required reference categories inside `ds:SignedInfo`.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum SignatureValidationReference {
30    Document,
31    WholeDocument,
32    KeyInfo,
33    SignedProperties,
34}
35
36/// Category of a validation-profile issue.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum SignatureValidationIssueKind {
39    Cryptographic,
40    MissingStructure,
41    MissingExternalMaterial,
42    Algorithm,
43    Policy,
44    Placement,
45}
46
47/// Detailed validation-profile issue.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SignatureValidationIssue {
50    pub kind: SignatureValidationIssueKind,
51    pub code: String,
52    pub message: String,
53}
54
55impl SignatureValidationIssue {
56    pub fn new(
57        kind: SignatureValidationIssueKind,
58        code: impl Into<String>,
59        message: impl Into<String>,
60    ) -> Self {
61        Self {
62            kind,
63            code: code.into(),
64            message: message.into(),
65        }
66    }
67}
68
69/// Report produced by `SignatureValidationProfile`.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct SignatureValidationReport {
72    pub valid: bool,
73    pub xmldsig_valid: Option<bool>,
74    pub xades_valid: Option<bool>,
75    pub required_references_valid: bool,
76    pub algorithms_valid: bool,
77    pub placement_valid: Option<bool>,
78    pub external_material_valid: bool,
79    pub issues: Vec<SignatureValidationIssue>,
80}
81
82impl SignatureValidationReport {
83    fn from_issues(
84        xmldsig_valid: Option<bool>,
85        xades_valid: Option<bool>,
86        placement_valid: Option<bool>,
87        issues: Vec<SignatureValidationIssue>,
88    ) -> Self {
89        let required_references_valid = !issues
90            .iter()
91            .any(|issue| issue.code.starts_with("missing_reference"));
92        let algorithms_valid = !issues
93            .iter()
94            .any(|issue| issue.kind == SignatureValidationIssueKind::Algorithm);
95        let external_material_valid = !issues
96            .iter()
97            .any(|issue| issue.kind == SignatureValidationIssueKind::MissingExternalMaterial);
98        let valid = issues.is_empty()
99            && xmldsig_valid.unwrap_or(false)
100            && xades_valid.unwrap_or(true)
101            && placement_valid.unwrap_or(true);
102
103        Self {
104            valid,
105            xmldsig_valid,
106            xades_valid,
107            required_references_valid,
108            algorithms_valid,
109            placement_valid,
110            external_material_valid,
111            issues,
112        }
113    }
114}
115
116/// Generic validation profile for XMLDSig/XAdES constraints.
117///
118/// The profile is parameter-only: it does not know business domains and does
119/// not perform network access. External material such as revocation data is
120/// reported as present/missing from the XML or provider hooks already supplied.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct SignatureValidationProfile {
123    level: SignatureValidationLevel,
124    xades_config: XadesConfig,
125    required_references: Vec<SignatureValidationReference>,
126    allowed_signature_algorithms: Vec<SignatureAlgorithm>,
127    allowed_digest_algorithms: Vec<DigestAlgorithm>,
128    expected_signature_placement: Option<SignaturePlacement>,
129    require_certificate_chain: bool,
130    require_revocation_values: bool,
131}
132
133impl SignatureValidationProfile {
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    pub fn with_level(mut self, level: SignatureValidationLevel) -> Self {
139        self.level = level;
140        self
141    }
142
143    pub fn with_xades_config(mut self, config: XadesConfig) -> Self {
144        self.xades_config = config;
145        self
146    }
147
148    pub fn require_reference(mut self, reference: SignatureValidationReference) -> Self {
149        self.required_references.push(reference);
150        self
151    }
152
153    pub fn allow_signature_algorithm(mut self, algorithm: SignatureAlgorithm) -> Self {
154        self.allowed_signature_algorithms.push(algorithm);
155        self
156    }
157
158    pub fn allow_digest_algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
159        self.allowed_digest_algorithms.push(algorithm);
160        self
161    }
162
163    pub fn with_expected_signature_placement(mut self, placement: SignaturePlacement) -> Self {
164        self.expected_signature_placement = Some(placement);
165        self
166    }
167
168    pub fn require_certificate_chain(mut self, required: bool) -> Self {
169        self.require_certificate_chain = required;
170        self
171    }
172
173    pub fn require_revocation_values(mut self, required: bool) -> Self {
174        self.require_revocation_values = required;
175        self
176    }
177
178    pub fn validate(
179        &self,
180        document: &Document,
181        provider: &impl SigningProvider,
182    ) -> XmlResult<SignatureValidationReport> {
183        let mut issues = Vec::new();
184        let signature = match find_signature(document) {
185            Ok(signature) => signature,
186            Err(error) => {
187                issues.push(issue_from_error(
188                    SignatureValidationIssueKind::MissingStructure,
189                    "missing_signature",
190                    error,
191                ));
192                return Ok(SignatureValidationReport::from_issues(
193                    None, None, None, issues,
194                ));
195            }
196        };
197        let parsed_signed_info = match required_child(document, signature, "SignedInfo")
198            .and_then(|signed_info| parse_signed_info(document, signed_info))
199        {
200            Ok(parsed) => Some(parsed),
201            Err(error) => {
202                issues.push(issue_from_error(
203                    SignatureValidationIssueKind::MissingStructure,
204                    "invalid_signed_info",
205                    error,
206                ));
207                None
208            }
209        };
210
211        let xmldsig_valid =
212            match verify_enveloped(document, provider, self.xades_config.xmldsig_config()) {
213                Ok(report) => {
214                    if !report.valid {
215                        issues.push(SignatureValidationIssue::new(
216                            SignatureValidationIssueKind::Cryptographic,
217                            "xmldsig_invalid",
218                            "XMLDSig verification failed",
219                        ));
220                    }
221                    Some(report.valid)
222                }
223                Err(error) => {
224                    issues.push(issue_from_error(
225                        SignatureValidationIssueKind::MissingStructure,
226                        "xmldsig_error",
227                        error,
228                    ));
229                    None
230                }
231            };
232
233        if let Some(signed_info) = &parsed_signed_info {
234            self.validate_algorithms(signed_info, &mut issues);
235            self.validate_required_references(document, signature, signed_info, &mut issues)?;
236        }
237        let placement_valid =
238            self.validate_signature_placement(document, signature, &mut issues)?;
239        let xades_valid = self.validate_xades_level(document, provider, signature, &mut issues)?;
240        self.validate_external_material(document, provider, signature, &mut issues)?;
241
242        Ok(SignatureValidationReport::from_issues(
243            xmldsig_valid,
244            xades_valid,
245            placement_valid,
246            issues,
247        ))
248    }
249
250    fn validate_algorithms(
251        &self,
252        signed_info: &super::SignedInfo,
253        issues: &mut Vec<SignatureValidationIssue>,
254    ) {
255        if !self.allowed_signature_algorithms.is_empty()
256            && !self
257                .allowed_signature_algorithms
258                .contains(&signed_info.signature_algorithm)
259        {
260            issues.push(SignatureValidationIssue::new(
261                SignatureValidationIssueKind::Algorithm,
262                "signature_algorithm_not_allowed",
263                format!(
264                    "signature algorithm `{}` is not allowed",
265                    signed_info.signature_algorithm.uri()
266                ),
267            ));
268        }
269
270        if !self.allowed_digest_algorithms.is_empty() {
271            for reference in &signed_info.references {
272                if !self
273                    .allowed_digest_algorithms
274                    .contains(&reference.digest_algorithm)
275                {
276                    issues.push(SignatureValidationIssue::new(
277                        SignatureValidationIssueKind::Algorithm,
278                        "digest_algorithm_not_allowed",
279                        format!(
280                            "digest algorithm `{}` is not allowed for reference `{}`",
281                            reference.digest_algorithm.uri(),
282                            reference.uri
283                        ),
284                    ));
285                }
286            }
287        }
288    }
289
290    fn validate_required_references(
291        &self,
292        document: &Document,
293        signature: NodeId,
294        signed_info: &super::SignedInfo,
295        issues: &mut Vec<SignatureValidationIssue>,
296    ) -> XmlResult<()> {
297        for required in &self.required_references {
298            if !reference_present(document, signature, signed_info, *required)? {
299                issues.push(SignatureValidationIssue::new(
300                    SignatureValidationIssueKind::MissingStructure,
301                    format!("missing_reference_{required:?}"),
302                    format!("required reference `{required:?}` is missing"),
303                ));
304            }
305        }
306        Ok(())
307    }
308
309    fn validate_signature_placement(
310        &self,
311        document: &Document,
312        signature: NodeId,
313        issues: &mut Vec<SignatureValidationIssue>,
314    ) -> XmlResult<Option<bool>> {
315        let Some(expected) = &self.expected_signature_placement else {
316            return Ok(None);
317        };
318
319        let actual_parent = document.parent(signature)?;
320        let expected_parent = match expected {
321            SignaturePlacement::Root => document.root(),
322            SignaturePlacement::ParentNode(node) => Some(*node),
323            SignaturePlacement::Query { .. } => Some(resolve_signature_parent(
324                document,
325                &XmlDsigConfig::new().with_signature_placement(expected.clone()),
326            )?),
327        };
328        let valid = actual_parent == expected_parent;
329        if !valid {
330            issues.push(SignatureValidationIssue::new(
331                SignatureValidationIssueKind::Placement,
332                "signature_placement_mismatch",
333                "signature is not placed at the expected location",
334            ));
335        }
336        Ok(Some(valid))
337    }
338
339    fn validate_xades_level(
340        &self,
341        document: &Document,
342        provider: &impl SigningProvider,
343        signature: NodeId,
344        issues: &mut Vec<SignatureValidationIssue>,
345    ) -> XmlResult<Option<bool>> {
346        match &self.level {
347            SignatureValidationLevel::XmlDsig => Ok(None),
348            SignatureValidationLevel::XadesBes
349            | SignatureValidationLevel::XadesT
350            | SignatureValidationLevel::XadesLt
351            | SignatureValidationLevel::XadesLta => {
352                let config = self.xades_config.clone().with_profile(XadesProfile::Bes);
353                let valid = match verify_xades_bes_enveloped(document, provider, &config) {
354                    Ok(report) => report.valid,
355                    Err(error) => {
356                        issues.push(issue_from_error(
357                            SignatureValidationIssueKind::MissingStructure,
358                            "xades_bes_error",
359                            error,
360                        ));
361                        false
362                    }
363                };
364                if !valid {
365                    issues.push(SignatureValidationIssue::new(
366                        SignatureValidationIssueKind::Cryptographic,
367                        "xades_invalid",
368                        "XAdES verification failed",
369                    ));
370                }
371                self.validate_unsigned_level(document, signature, issues)?;
372                Ok(Some(valid))
373            }
374            SignatureValidationLevel::XadesEpes(policy) => {
375                let config = self
376                    .xades_config
377                    .clone()
378                    .with_profile(XadesProfile::Epes(policy.clone()));
379                let valid = match verify_xades_epes_enveloped(document, provider, &config) {
380                    Ok(report) => {
381                        if report.signature_policy_valid == Some(false) {
382                            issues.push(SignatureValidationIssue::new(
383                                SignatureValidationIssueKind::Policy,
384                                "signature_policy_mismatch",
385                                "signature policy does not match the expected policy",
386                            ));
387                        }
388                        report.valid
389                    }
390                    Err(error) => {
391                        issues.push(issue_from_error(
392                            SignatureValidationIssueKind::MissingStructure,
393                            "xades_epes_error",
394                            error,
395                        ));
396                        false
397                    }
398                };
399                Ok(Some(valid))
400            }
401            SignatureValidationLevel::XadesBaselineB { policy } => {
402                let config = self
403                    .xades_config
404                    .clone()
405                    .with_profile(XadesProfile::BaselineB {
406                        policy: policy.clone(),
407                    });
408                let valid = match verify_xades_baseline_b_enveloped(document, provider, &config) {
409                    Ok(report) => {
410                        if report.signature_policy_valid == Some(false) {
411                            issues.push(SignatureValidationIssue::new(
412                                SignatureValidationIssueKind::Policy,
413                                "signature_policy_mismatch",
414                                "signature policy does not match the expected policy",
415                            ));
416                        }
417                        report.valid
418                    }
419                    Err(error) => {
420                        issues.push(issue_from_error(
421                            SignatureValidationIssueKind::MissingStructure,
422                            "xades_baseline_b_error",
423                            error,
424                        ));
425                        false
426                    }
427                };
428                Ok(Some(valid))
429            }
430        }
431    }
432
433    fn validate_unsigned_level(
434        &self,
435        document: &Document,
436        signature: NodeId,
437        issues: &mut Vec<SignatureValidationIssue>,
438    ) -> XmlResult<()> {
439        match self.level {
440            SignatureValidationLevel::XadesT
441            | SignatureValidationLevel::XadesLt
442            | SignatureValidationLevel::XadesLta => {
443                if !xades_unsigned_child_present(document, signature, "SignatureTimeStamp")? {
444                    issues.push(SignatureValidationIssue::new(
445                        SignatureValidationIssueKind::MissingStructure,
446                        "missing_signature_timestamp",
447                        "required XAdES SignatureTimeStamp is missing",
448                    ));
449                }
450            }
451            _ => {}
452        }
453
454        match self.level {
455            SignatureValidationLevel::XadesLt | SignatureValidationLevel::XadesLta => {
456                let validation_report = verify_xades_validation_data(
457                    document,
458                    &XadesValidationDataConfig::new()
459                        .require_certificate_values(true)
460                        .require_revocation_values(true),
461                )?;
462                if !validation_report.required_material_present() {
463                    issues.push(SignatureValidationIssue::new(
464                        SignatureValidationIssueKind::MissingExternalMaterial,
465                        "missing_validation_data",
466                        "required XAdES validation data is missing",
467                    ));
468                }
469            }
470            _ => {}
471        }
472
473        if matches!(self.level, SignatureValidationLevel::XadesLta)
474            && !xades_unsigned_child_present(document, signature, "ArchiveTimeStamp")?
475        {
476            issues.push(SignatureValidationIssue::new(
477                SignatureValidationIssueKind::MissingStructure,
478                "missing_archive_timestamp",
479                "required XAdES ArchiveTimeStamp is missing",
480            ));
481        }
482
483        Ok(())
484    }
485
486    fn validate_external_material(
487        &self,
488        document: &Document,
489        provider: &impl SigningProvider,
490        signature: NodeId,
491        issues: &mut Vec<SignatureValidationIssue>,
492    ) -> XmlResult<()> {
493        if self.require_certificate_chain {
494            if provider.certificate_chain_details()?.len() < 2 {
495                issues.push(SignatureValidationIssue::new(
496                    SignatureValidationIssueKind::MissingExternalMaterial,
497                    "missing_certificate_chain_provider_material",
498                    "provider did not expose a certificate chain",
499                ));
500            }
501            if signing_certificate_count(document, signature)? < 2 {
502                issues.push(SignatureValidationIssue::new(
503                    SignatureValidationIssueKind::MissingStructure,
504                    "missing_certificate_chain_signed_property",
505                    "SignedProperties does not include the required certificate chain",
506                ));
507            }
508        }
509
510        if self.require_revocation_values {
511            let validation_report = verify_xades_validation_data(
512                document,
513                &XadesValidationDataConfig::new()
514                    .require_certificate_values(false)
515                    .require_revocation_values(true),
516            )?;
517            if !validation_report.required_material_present() {
518                issues.push(SignatureValidationIssue::new(
519                    SignatureValidationIssueKind::MissingExternalMaterial,
520                    "missing_revocation_values",
521                    "required revocation values are missing",
522                ));
523            }
524        }
525
526        Ok(())
527    }
528}
529
530impl Default for SignatureValidationProfile {
531    fn default() -> Self {
532        Self {
533            level: SignatureValidationLevel::XmlDsig,
534            xades_config: XadesConfig::new(),
535            required_references: Vec::new(),
536            allowed_signature_algorithms: Vec::new(),
537            allowed_digest_algorithms: Vec::new(),
538            expected_signature_placement: None,
539            require_certificate_chain: false,
540            require_revocation_values: false,
541        }
542    }
543}
544
545fn issue_from_error(
546    kind: SignatureValidationIssueKind,
547    code: impl Into<String>,
548    error: XmlError,
549) -> SignatureValidationIssue {
550    SignatureValidationIssue::new(kind, code, error.to_string())
551}
552
553fn reference_present(
554    document: &Document,
555    signature: NodeId,
556    signed_info: &super::SignedInfo,
557    required: SignatureValidationReference,
558) -> XmlResult<bool> {
559    let key_info_uri = optional_child_id_uri(document, signature, "KeyInfo")?;
560    let signed_properties_uri = signed_properties_id_uri(document, signature)?;
561
562    Ok(match required {
563        SignatureValidationReference::WholeDocument => signed_info
564            .references
565            .iter()
566            .any(|reference| reference.uri == ""),
567        SignatureValidationReference::KeyInfo => key_info_uri.as_deref().is_some_and(|uri| {
568            signed_info
569                .references
570                .iter()
571                .any(|reference| reference.uri == uri)
572        }),
573        SignatureValidationReference::SignedProperties => {
574            signed_info.references.iter().any(|reference| {
575                reference.type_uri.as_deref() == Some(XMLDSIG_SIGNED_PROPERTIES_TYPE_URI)
576                    || signed_properties_uri
577                        .as_deref()
578                        .is_some_and(|uri| reference.uri == uri)
579            })
580        }
581        SignatureValidationReference::Document => signed_info.references.iter().any(|reference| {
582            reference.type_uri.is_none()
583                && reference.uri.starts_with('#')
584                && key_info_uri.as_deref() != Some(reference.uri.as_str())
585                && signed_properties_uri.as_deref() != Some(reference.uri.as_str())
586        }),
587    })
588}
589
590fn optional_child_id_uri(
591    document: &Document,
592    parent: NodeId,
593    local: &str,
594) -> XmlResult<Option<String>> {
595    let Some(child) = element_children(document, parent)?
596        .into_iter()
597        .find(|child| element_local_name_matches(document, *child, local))
598    else {
599        return Ok(None);
600    };
601    Ok(optional_attribute(document, child, "Id")?.map(|id| format!("#{id}")))
602}
603
604fn signed_properties_id_uri(document: &Document, signature: NodeId) -> XmlResult<Option<String>> {
605    let Some(qualifying_properties) = xades_qualifying_properties(document, signature)? else {
606        return Ok(None);
607    };
608    let Some(signed_properties) =
609        optional_xades_child(document, qualifying_properties, "SignedProperties")?
610    else {
611        return Ok(None);
612    };
613    Ok(optional_attribute(document, signed_properties, "Id")?.map(|id| format!("#{id}")))
614}
615
616fn signing_certificate_count(document: &Document, signature: NodeId) -> XmlResult<usize> {
617    let Some(qualifying_properties) = xades_qualifying_properties(document, signature)? else {
618        return Ok(0);
619    };
620    let Some(signed_properties) =
621        optional_xades_child(document, qualifying_properties, "SignedProperties")?
622    else {
623        return Ok(0);
624    };
625    let Some(signed_signature_properties) =
626        optional_xades_child(document, signed_properties, "SignedSignatureProperties")?
627    else {
628        return Ok(0);
629    };
630    let signing_certificate = optional_xades_child(
631        document,
632        signed_signature_properties,
633        "SigningCertificateV2",
634    )?
635    .or(optional_xades_child(
636        document,
637        signed_signature_properties,
638        "SigningCertificate",
639    )?);
640    let Some(signing_certificate) = signing_certificate else {
641        return Ok(0);
642    };
643    Ok(element_children(document, signing_certificate)?
644        .into_iter()
645        .filter(|node| is_xades_element(document, *node, "Cert"))
646        .count())
647}
648
649fn xades_unsigned_child_present(
650    document: &Document,
651    signature: NodeId,
652    local: &str,
653) -> XmlResult<bool> {
654    let Some(qualifying_properties) = xades_qualifying_properties(document, signature)? else {
655        return Ok(false);
656    };
657    let Some(unsigned_properties) =
658        optional_xades_child(document, qualifying_properties, "UnsignedProperties")?
659    else {
660        return Ok(false);
661    };
662    let Some(unsigned_signature_properties) =
663        optional_xades_child(document, unsigned_properties, "UnsignedSignatureProperties")?
664    else {
665        return Ok(false);
666    };
667    Ok(optional_xades_child(document, unsigned_signature_properties, local)?.is_some())
668}
669
670fn xades_qualifying_properties(
671    document: &Document,
672    signature: NodeId,
673) -> XmlResult<Option<NodeId>> {
674    let object = match required_child(document, signature, "Object") {
675        Ok(object) => object,
676        Err(_) => return Ok(None),
677    };
678    Ok(element_children(document, object)?
679        .into_iter()
680        .find(|child| is_xades_element(document, *child, "QualifyingProperties")))
681}
682
683fn optional_xades_child(
684    document: &Document,
685    parent: NodeId,
686    local: &str,
687) -> XmlResult<Option<NodeId>> {
688    Ok(element_children(document, parent)?
689        .into_iter()
690        .find(|child| is_xades_element(document, *child, local)))
691}
692
693fn is_xades_element(document: &Document, node: NodeId, local: &str) -> bool {
694    matches!(
695        document.node(node).map(|node| node.kind()),
696        Ok(NodeKind::Element(element))
697            if element.name().namespace_uri().map(|uri| uri.as_str()) == Some(XADES_NAMESPACE_URI)
698                && element.name().local() == local
699    )
700}
701
702fn element_local_name_matches(document: &Document, node: NodeId, local: &str) -> bool {
703    matches!(
704        document.node(node).map(|node| node.kind()),
705        Ok(NodeKind::Element(element)) if element.name().local() == local
706    )
707}
708
709#[cfg(test)]
710mod tests {
711    use crate::parser::parse_str;
712    use crate::signature::{
713        add_signature_timestamp, add_xades_validation_data, sign_enveloped,
714        sign_xades_bes_enveloped, sign_xades_epes_enveloped, DeterministicSigningProvider,
715        DeterministicTimestampAuthority, SignaturePolicyId, SignaturePolicyQualifier,
716        StaticValidationDataProvider, XadesTimestampConfig, XmlDsigReferenceConfig,
717    };
718
719    use super::*;
720
721    fn provider() -> DeterministicSigningProvider {
722        DeterministicSigningProvider::new(b"test-cert".to_vec(), b"test-secret".to_vec())
723    }
724
725    fn provider_with_chain() -> DeterministicSigningProvider {
726        use crate::signature::CertificateDetails;
727
728        provider().with_certificate_chain_details(vec![
729            CertificateDetails::new(b"test-cert".to_vec()),
730            CertificateDetails::new(b"issuer-cert".to_vec()),
731        ])
732    }
733
734    fn unsigned_document() -> XmlResult<Document> {
735        parse_str(r#"<Root Id="doc-1"><Extension><Payload>value</Payload></Extension></Root>"#)
736    }
737
738    fn policy() -> XmlResult<SignaturePolicy> {
739        Ok(SignaturePolicy::new(
740            SignaturePolicyId::Uri("urn:example:policy:v1".to_owned()),
741            DigestAlgorithm::Sha256,
742            super::super::digest_bytes(DigestAlgorithm::Sha256, b"example policy")?,
743        )
744        .with_qualifier(SignaturePolicyQualifier::SpUri(
745            "https://example.test/policy.pdf".to_owned(),
746        )))
747    }
748
749    #[test]
750    fn signature_validation_profile_accepts_required_references_and_algorithms() -> XmlResult<()> {
751        let config = XmlDsigConfig::new()
752            .with_key_info_id("key-info-1")
753            .with_references(vec![
754                XmlDsigReferenceConfig::document_id(),
755                XmlDsigReferenceConfig::key_info(),
756            ]);
757        let signed = sign_enveloped(&unsigned_document()?, &provider(), &config)?;
758        let profile = SignatureValidationProfile::new()
759            .require_reference(SignatureValidationReference::Document)
760            .require_reference(SignatureValidationReference::KeyInfo)
761            .allow_signature_algorithm(SignatureAlgorithm::RsaSha256)
762            .allow_digest_algorithm(DigestAlgorithm::Sha256);
763
764        let report = profile.validate(&signed, &provider())?;
765
766        assert!(report.valid);
767        assert!(report.required_references_valid);
768        assert!(report.algorithms_valid);
769        assert!(report.issues.is_empty());
770        Ok(())
771    }
772
773    #[test]
774    fn signature_validation_profile_reports_missing_signed_properties_reference() -> XmlResult<()> {
775        let signed = sign_enveloped(&unsigned_document()?, &provider(), &XmlDsigConfig::new())?;
776        let profile = SignatureValidationProfile::new()
777            .require_reference(SignatureValidationReference::SignedProperties);
778
779        let report = profile.validate(&signed, &provider())?;
780
781        assert!(!report.valid);
782        assert!(!report.required_references_valid);
783        assert!(report
784            .issues
785            .iter()
786            .any(|issue| issue.kind == SignatureValidationIssueKind::MissingStructure));
787        Ok(())
788    }
789
790    #[test]
791    fn signature_validation_profile_validates_epes_policy_and_placement() -> XmlResult<()> {
792        let policy = policy()?;
793        let xades_config = XadesConfig::new()
794            .with_profile(XadesProfile::Epes(policy.clone()))
795            .with_xmldsig_config(
796                XmlDsigConfig::new()
797                    .with_signature_placement(SignaturePlacement::query("/Root/Extension")),
798            );
799        let signed = sign_xades_epes_enveloped(&unsigned_document()?, &provider(), &xades_config)?;
800        let profile = SignatureValidationProfile::new()
801            .with_level(SignatureValidationLevel::XadesEpes(policy))
802            .with_xades_config(xades_config)
803            .require_reference(SignatureValidationReference::Document)
804            .require_reference(SignatureValidationReference::SignedProperties)
805            .with_expected_signature_placement(SignaturePlacement::query("/Root/Extension"));
806
807        let report = profile.validate(&signed, &provider())?;
808
809        assert!(report.valid);
810        assert_eq!(report.xades_valid, Some(true));
811        assert_eq!(report.placement_valid, Some(true));
812        Ok(())
813    }
814
815    #[test]
816    fn signature_validation_profile_reports_placement_mismatch() -> XmlResult<()> {
817        let signed =
818            sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &XadesConfig::new())?;
819        let profile = SignatureValidationProfile::new()
820            .with_level(SignatureValidationLevel::XadesBes)
821            .with_expected_signature_placement(SignaturePlacement::query("/Root/Extension"));
822
823        let report = profile.validate(&signed, &provider())?;
824
825        assert!(!report.valid);
826        assert_eq!(report.placement_valid, Some(false));
827        assert!(report
828            .issues
829            .iter()
830            .any(|issue| issue.kind == SignatureValidationIssueKind::Placement));
831        Ok(())
832    }
833
834    #[test]
835    fn signature_validation_profile_reports_missing_lt_material() -> XmlResult<()> {
836        let signed =
837            sign_xades_bes_enveloped(&unsigned_document()?, &provider(), &XadesConfig::new())?;
838        let profile = SignatureValidationProfile::new()
839            .with_level(SignatureValidationLevel::XadesLt)
840            .require_certificate_chain(true)
841            .require_revocation_values(true);
842
843        let report = profile.validate(&signed, &provider())?;
844
845        assert!(!report.valid);
846        assert!(!report.external_material_valid);
847        assert!(report
848            .issues
849            .iter()
850            .any(|issue| issue.kind == SignatureValidationIssueKind::MissingExternalMaterial));
851        Ok(())
852    }
853
854    #[test]
855    fn signature_validation_profile_accepts_lt_material_when_present() -> XmlResult<()> {
856        let signed = sign_xades_bes_enveloped(
857            &unsigned_document()?,
858            &provider_with_chain(),
859            &XadesConfig::new().with_certificate_chain(true),
860        )?;
861        let timestamped = add_signature_timestamp(
862            &signed,
863            &DeterministicTimestampAuthority::new(b"timestamp-secret"),
864            &XadesTimestampConfig::new(),
865        )?;
866        let enriched = add_xades_validation_data(
867            &timestamped,
868            &StaticValidationDataProvider::new()
869                .with_certificate(b"issuer-cert".to_vec())
870                .with_ocsp(b"ocsp".to_vec()),
871            &XadesValidationDataConfig::new(),
872        )?;
873        let profile = SignatureValidationProfile::new()
874            .with_level(SignatureValidationLevel::XadesLt)
875            .require_certificate_chain(true)
876            .require_revocation_values(true);
877
878        let report = profile.validate(&enriched, &provider_with_chain())?;
879
880        assert!(report.valid);
881        assert!(report.external_material_valid);
882        Ok(())
883    }
884}