Skip to main content

confium_deployment/
manifest.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct Manifest {
5    pub deployment: DeploymentHeader,
6    #[serde(default)]
7    pub mode: DeploymentMode,
8    #[serde(default)]
9    pub tiers: Vec<Tier>,
10    #[serde(default)]
11    pub transparency: TransparencyConfig,
12    #[serde(default)]
13    pub async_signing: AsyncSigningConfig,
14    #[serde(default)]
15    pub archival: ArchivalConfig,
16    #[serde(default)]
17    pub quorums: Vec<Quorum>,
18    #[serde(default)]
19    pub pkcs11_server: Option<Pkcs11ServerConfig>,
20    #[serde(default)]
21    pub pqc_migration: Option<PqcMigrationPlan>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct DeploymentHeader {
26    pub name: String,
27    pub operator: String,
28    #[serde(default)]
29    pub charter_url: Option<String>,
30    pub manifest_version: u32,
31}
32
33#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "snake_case")]
35pub enum DeploymentMode {
36    PeerToPeer,
37    Pkcs11Replacement,
38    #[default]
39    CertificatePki,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Tier {
44    pub name: String,
45    pub role: String,
46    pub signing_algorithm: String,
47    #[serde(default)]
48    pub encryption_algorithm: Option<String>,
49    pub threshold: Threshold,
50    #[serde(default)]
51    pub delegated_by: Option<String>,
52    #[serde(default)]
53    pub delegation_scope: Option<String>,
54    #[serde(default)]
55    pub ceremony: Option<Ceremony>,
56    #[serde(default)]
57    pub attributes: Vec<String>,
58}
59
60#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
61pub struct Threshold {
62    pub t: u32,
63    pub n: u32,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct Ceremony {
68    pub sync_required: bool,
69    #[serde(default)]
70    pub frequency: Option<String>,
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
74pub struct TransparencyConfig {
75    #[serde(default)]
76    pub log_operator: Option<String>,
77    #[serde(default)]
78    pub anchors: Vec<String>,
79    #[serde(default)]
80    pub gossip: bool,
81    #[serde(default)]
82    pub public_mirror_urls: Vec<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AsyncSigningConfig {
87    #[serde(default = "default_unlock_window")]
88    pub default_unlock_window_minutes: u32,
89    #[serde(default)]
90    pub coordinator_operator: Option<String>,
91}
92
93fn default_unlock_window() -> u32 {
94    240
95}
96
97impl Default for AsyncSigningConfig {
98    fn default() -> Self {
99        Self {
100            default_unlock_window_minutes: default_unlock_window(),
101            coordinator_operator: None,
102        }
103    }
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107pub struct ArchivalConfig {
108    #[serde(default = "default_renewal_period")]
109    pub renewal_period_years: u32,
110    #[serde(default)]
111    pub re_sign_under: Option<String>,
112}
113
114fn default_renewal_period() -> u32 {
115    5
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct Quorum {
120    pub name: String,
121    pub threshold: Threshold,
122    pub coordinator: String,
123    #[serde(default)]
124    pub share_storage_backend: Option<String>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct Pkcs11ServerConfig {
129    pub slot_count: u32,
130    pub default_signing_algorithm: String,
131    pub default_threshold: Threshold,
132    pub share_storage: String,
133    pub hsm_module: String,
134}
135
136#[derive(Debug, Clone, Default, Serialize, Deserialize)]
137pub struct PqcMigrationPlan {
138    #[serde(default)]
139    pub current: Option<String>,
140    #[serde(default)]
141    pub target_2027: Option<String>,
142    #[serde(default)]
143    pub target_2029: Option<String>,
144}
145
146#[derive(Debug, thiserror::Error)]
147pub enum ManifestError {
148    #[error("manifest TOML parse error: {0}")]
149    Parse(#[from] toml::de::Error),
150    #[error("manifest TOML serialize error: {0}")]
151    Serialize(#[from] toml::ser::Error),
152    #[error("manifest validation failed: {0}")]
153    Validation(String),
154}
155
156pub fn parse_manifest(toml_str: &str) -> Result<Manifest, ManifestError> {
157    let manifest: Manifest = toml::from_str(toml_str)?;
158    Ok(manifest)
159}
160
161pub fn manifest_to_toml(manifest: &Manifest) -> Result<String, ManifestError> {
162    Ok(toml::to_string_pretty(manifest)?)
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn parses_minimal_mode3_manifest() {
171        let toml_str = r#"
172[deployment]
173name = "Test Deployment"
174operator = "Test Operator"
175manifest_version = 1
176mode = "certificate_pki"
177
178[[tiers]]
179name = "root"
180role = "root"
181signing_algorithm = "FROST-ed25519"
182threshold = { t = 3, n = 5 }
183"#;
184        let manifest = parse_manifest(toml_str).expect("parse");
185        assert_eq!(manifest.deployment.name, "Test Deployment");
186        assert_eq!(manifest.mode, DeploymentMode::CertificatePki);
187        assert_eq!(manifest.tiers.len(), 1);
188        assert_eq!(manifest.tiers[0].threshold.t, 3);
189        assert_eq!(manifest.tiers[0].threshold.n, 5);
190    }
191
192    #[test]
193    fn parses_mode2_manifest_with_pkcs11_server() {
194        let toml_str = r#"
195mode = "pkcs11_replacement"
196
197[deployment]
198name = "Enterprise PKI"
199operator = "Acme Corp"
200manifest_version = 1
201
202[pkcs11_server]
203slot_count = 8
204default_signing_algorithm = "FROST-P256"
205default_threshold = { t = 3, n = 5 }
206share_storage = "pkcs11-wrap"
207hsm_module = "/usr/lib/pkcs11/yubihsm.so"
208"#;
209        let manifest = parse_manifest(toml_str).expect("parse");
210        assert_eq!(manifest.mode, DeploymentMode::Pkcs11Replacement);
211        let pkcs11 = manifest.pkcs11_server.expect("pkcs11_server");
212        assert_eq!(pkcs11.slot_count, 8);
213    }
214
215    #[test]
216    fn round_trips_manifest_through_toml() {
217        let toml_str = r#"
218[deployment]
219name = "Round Trip"
220operator = "Op"
221manifest_version = 1
222
223[transparency]
224anchors = ["bitcoin-ots"]
225gossip = false
226
227[async_signing]
228default_unlock_window_minutes = 120
229"#;
230        let manifest = parse_manifest(toml_str).expect("parse");
231        let reserialized = manifest_to_toml(&manifest).expect("serialize");
232        let reparsed = parse_manifest(&reserialized).expect("reparse");
233        assert_eq!(manifest.deployment.name, reparsed.deployment.name);
234        assert_eq!(
235            manifest.async_signing.default_unlock_window_minutes,
236            reparsed.async_signing.default_unlock_window_minutes
237        );
238    }
239}