Skip to main content

confium_deployment/
validate.rs

1//! Manifest validation.
2
3use crate::manifest::{DeploymentMode, Manifest};
4
5#[derive(Debug, Clone, Default)]
6pub struct ValidationReport {
7    pub valid: bool,
8    pub errors: Vec<String>,
9    pub warnings: Vec<String>,
10}
11
12impl ValidationReport {
13    pub fn is_ok(&self) -> bool {
14        self.errors.is_empty()
15    }
16}
17
18pub fn validate_manifest(manifest: &Manifest) -> ValidationReport {
19    let mut report = ValidationReport {
20        valid: true,
21        errors: Vec::new(),
22        warnings: Vec::new(),
23    };
24
25    if manifest.deployment.manifest_version != 1 {
26        report.errors.push(format!(
27            "unsupported manifest version {}",
28            manifest.deployment.manifest_version
29        ));
30    }
31
32    match manifest.mode {
33        DeploymentMode::CertificatePki => {
34            if manifest.tiers.is_empty() {
35                report
36                    .errors
37                    .push("certificate_pki mode requires at least one tier".into());
38            }
39            validate_tier_chain(&manifest.tiers, &mut report);
40        }
41        DeploymentMode::Pkcs11Replacement => {
42            if manifest.pkcs11_server.is_none() {
43                report
44                    .errors
45                    .push("pkcs11_replacement mode requires [pkcs11_server] section".into());
46            }
47            if manifest.quorums.is_empty() {
48                report
49                    .errors
50                    .push("pkcs11_replacement mode requires at least one [[quorums]] entry".into());
51            }
52        }
53        DeploymentMode::PeerToPeer => {
54            // Mode 1: minimal requirements
55        }
56    }
57
58    for tier in &manifest.tiers {
59        if tier.threshold.t == 0 || tier.threshold.t > tier.threshold.n {
60            report.errors.push(format!(
61                "tier {} has invalid threshold t={} n={}",
62                tier.name, tier.threshold.t, tier.threshold.n
63            ));
64        }
65    }
66
67    report.valid = report.errors.is_empty();
68    report
69}
70
71fn validate_tier_chain(tiers: &[crate::manifest::Tier], report: &mut ValidationReport) {
72    let names: std::collections::HashSet<&str> = tiers.iter().map(|t| t.name.as_str()).collect();
73
74    for tier in tiers {
75        if let Some(parent) = &tier.delegated_by {
76            if !names.contains(parent.as_str()) {
77                report.errors.push(format!(
78                    "tier {} delegates to unknown tier {}",
79                    tier.name, parent
80                ));
81            }
82        }
83    }
84
85    let has_root = tiers.iter().any(|t| t.delegated_by.is_none());
86    if !has_root {
87        report
88            .errors
89            .push("tier chain has no root (no tier with delegated_by absent)".into());
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::manifest::*;
97
98    fn valid_mode3_manifest() -> Manifest {
99        Manifest {
100            deployment: DeploymentHeader {
101                name: "Test".into(),
102                operator: "Op".into(),
103                charter_url: None,
104                manifest_version: 1,
105            },
106            mode: DeploymentMode::CertificatePki,
107            tiers: vec![Tier {
108                name: "root".into(),
109                role: "root".into(),
110                signing_algorithm: "FROST-ed25519".into(),
111                encryption_algorithm: None,
112                threshold: Threshold { t: 3, n: 5 },
113                delegated_by: None,
114                delegation_scope: None,
115                ceremony: None,
116                attributes: vec![],
117            }],
118            transparency: TransparencyConfig::default(),
119            async_signing: AsyncSigningConfig::default(),
120            archival: ArchivalConfig::default(),
121            quorums: vec![],
122            pkcs11_server: None,
123            pqc_migration: None,
124        }
125    }
126
127    #[test]
128    fn valid_manifest_passes() {
129        let manifest = valid_mode3_manifest();
130        let report = validate_manifest(&manifest);
131        assert!(report.is_ok(), "errors: {:?}", report.errors);
132    }
133
134    #[test]
135    fn invalid_threshold_fails() {
136        let mut manifest = valid_mode3_manifest();
137        manifest.tiers[0].threshold = Threshold { t: 0, n: 5 };
138        let report = validate_manifest(&manifest);
139        assert!(!report.is_ok());
140    }
141
142    #[test]
143    fn mode2_without_pkcs11_fails() {
144        let mut manifest = valid_mode3_manifest();
145        manifest.mode = DeploymentMode::Pkcs11Replacement;
146        manifest.tiers.clear();
147        let report = validate_manifest(&manifest);
148        assert!(!report.is_ok());
149    }
150}