Skip to main content

pdf_ast/security/
etsi.rs

1use crate::security::{
2    DigitalSignature, SignatureType, SignatureValidity, ValidationResult, ValidationStatus,
3};
4
5#[derive(Debug, Clone, Default)]
6pub struct EtsiValidationOptions {
7    pub require_dss_for_pades: bool,
8}
9
10pub fn validate_etsi_profiles(
11    signatures: &[DigitalSignature],
12    has_dss: bool,
13    options: EtsiValidationOptions,
14) -> Vec<ValidationResult> {
15    let mut results = Vec::new();
16
17    let mut cades_found = 0usize;
18    let mut pades_found = 0usize;
19    let mut rfc3161_found = 0usize;
20
21    for sig in signatures {
22        match sig.signature_type {
23            SignatureType::EtsiCadEsDetached => {
24                cades_found += 1;
25                results.push(result_for_signature(sig, "ETSI:CAdES"));
26            }
27            SignatureType::EtsiRfc3161 => {
28                rfc3161_found += 1;
29                results.push(result_for_timestamp(sig));
30            }
31            SignatureType::AdbePkcs7Detached
32            | SignatureType::AdbePkcs7Sha1
33            | SignatureType::AdbeX509RsaSha1 => {
34                // Potential PAdES baseline
35                pades_found += 1;
36            }
37        }
38    }
39
40    if pades_found > 0 {
41        let status = if options.require_dss_for_pades && !has_dss {
42            ValidationStatus::Fail
43        } else if !has_dss {
44            ValidationStatus::Warning
45        } else {
46            ValidationStatus::Pass
47        };
48        results.push(ValidationResult {
49            check_type: "ETSI:PAdES-LTV".to_string(),
50            status,
51            message: if has_dss {
52                format!(
53                    "PAdES signatures detected: {} with DSS present",
54                    pades_found
55                )
56            } else {
57                format!("PAdES signatures detected: {} without DSS", pades_found)
58            },
59        });
60    }
61
62    if cades_found == 0 && rfc3161_found == 0 && pades_found == 0 {
63        results.push(ValidationResult {
64            check_type: "ETSI:Profiles".to_string(),
65            status: ValidationStatus::Warning,
66            message: "No ETSI/PAdES signatures detected".to_string(),
67        });
68    }
69
70    results
71}
72
73fn result_for_signature(sig: &DigitalSignature, label: &str) -> ValidationResult {
74    match &sig.validity {
75        SignatureValidity::Valid => ValidationResult {
76            check_type: label.to_string(),
77            status: ValidationStatus::Pass,
78            message: format!("{} signature valid", label),
79        },
80        SignatureValidity::Invalid(msg) => ValidationResult {
81            check_type: label.to_string(),
82            status: ValidationStatus::Fail,
83            message: format!("{} signature invalid: {}", label, msg),
84        },
85        SignatureValidity::Unknown(msg) => ValidationResult {
86            check_type: label.to_string(),
87            status: ValidationStatus::Warning,
88            message: format!("{} signature unknown: {}", label, msg),
89        },
90    }
91}
92
93fn result_for_timestamp(sig: &DigitalSignature) -> ValidationResult {
94    if let Some(ts) = &sig.timestamp {
95        let status = if ts.signature_valid {
96            ValidationStatus::Pass
97        } else {
98            ValidationStatus::Fail
99        };
100        return ValidationResult {
101            check_type: "ETSI:RFC3161".to_string(),
102            status,
103            message: if ts.signature_valid {
104                "RFC3161 timestamp valid".to_string()
105            } else {
106                "RFC3161 timestamp invalid".to_string()
107            },
108        };
109    }
110
111    ValidationResult {
112        check_type: "ETSI:RFC3161".to_string(),
113        status: ValidationStatus::Warning,
114        message: "RFC3161 timestamp not present".to_string(),
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::security::{DigitalSignature, SignatureType, SignatureValidity, TimestampDetails};
122
123    fn base_sig(signature_type: SignatureType) -> DigitalSignature {
124        DigitalSignature {
125            field_name: "Sig1".to_string(),
126            signature_type,
127            signer: None,
128            signing_time: None,
129            certificate_info: None,
130            validity: SignatureValidity::Valid,
131            location: None,
132            reason: None,
133            contact_info: None,
134            timestamp: None,
135        }
136    }
137
138    #[test]
139    fn validate_pades_requires_dss() {
140        let sigs = vec![base_sig(SignatureType::AdbePkcs7Detached)];
141        let res = validate_etsi_profiles(
142            &sigs,
143            false,
144            EtsiValidationOptions {
145                require_dss_for_pades: true,
146            },
147        );
148        assert!(res.iter().any(|r| r.status == ValidationStatus::Fail));
149    }
150
151    #[test]
152    fn validate_cades_and_rfc3161() {
153        let mut sig = base_sig(SignatureType::EtsiRfc3161);
154        sig.timestamp = Some(TimestampDetails {
155            time: None,
156            policy_oid: None,
157            hash_algorithm: None,
158            signature_valid: true,
159            tsa_chain_valid: None,
160            tsa_pin_valid: None,
161            tsa_revocation_events: Vec::new(),
162        });
163        let sigs = vec![sig, base_sig(SignatureType::EtsiCadEsDetached)];
164        let res = validate_etsi_profiles(&sigs, true, EtsiValidationOptions::default());
165        assert!(res.iter().any(|r| r.check_type == "ETSI:CAdES"));
166        assert!(res.iter().any(|r| r.check_type == "ETSI:RFC3161"));
167    }
168}