confium_deployment/
validate.rs1use 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 }
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}