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}