1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Metadata {
8 pub name: String,
10 #[serde(skip_serializing_if = "Option::is_none")]
12 pub description: Option<String>,
13}
14
15pub fn role_type(id: &str) -> &str {
19 id.split_once("__").map_or(id, |(prefix, _)| prefix)
20}
21
22pub fn derive_step_name(id: &str) -> String {
31 id.split(['_', '-'])
32 .map(|word| {
33 let mut chars = word.chars();
34 match chars.next() {
35 None => String::new(),
36 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
37 }
38 })
39 .collect::<Vec<_>>()
40 .join(" ")
41}
42
43pub fn derive_role_name(id: &str) -> String {
48 id.replace("__", " ")
49 .split(['_', '-', ' '])
50 .filter(|s| !s.is_empty())
51 .map(|word| {
52 let mut chars = word.chars();
53 match chars.next() {
54 None => String::new(),
55 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
56 }
57 })
58 .collect::<Vec<_>>()
59 .join(" ")
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65#[non_exhaustive]
66pub enum ActionType {
67 ClockCheck,
72 Confirm,
77 CheckValue,
82 OralReadback,
87 MachineInfo,
92
93 GenerateKeypair,
95 WrapKey,
97 UnwrapKey,
99 ExportPublic,
101 Attest,
103 GatherEntropy,
110 TpmAttest,
114
115 PivReadCertificate,
120 PivSign,
125 YubikeyAttestSlot,
130
131 IssueCertificate,
137 GenerateCsr,
143}
144
145impl ActionType {
146 pub fn requires_backend(self) -> bool {
153 matches!(
154 self,
155 ActionType::GenerateKeypair
156 | ActionType::WrapKey
157 | ActionType::UnwrapKey
158 | ActionType::ExportPublic
159 | ActionType::GenerateCsr
160 | ActionType::IssueCertificate
161 | ActionType::PivReadCertificate
162 | ActionType::PivSign
163 | ActionType::YubikeyAttestSlot
164 | ActionType::TpmAttest
165 )
166 }
167
168 pub fn required_with_fields(self) -> &'static [&'static str] {
177 match self {
178 ActionType::CheckValue => &["actual", "expected"],
179 ActionType::GenerateCsr => &["subject"],
180 _ => &[],
181 }
182 }
183
184 pub fn describe(&self) -> &'static str {
189 match self {
190 ActionType::ClockCheck => "Verify system clock against a reference time.",
191 ActionType::Confirm => "Confirm readiness or completion of a manual step.",
192 ActionType::CheckValue => "Verify a value matches an expected result.",
193 ActionType::OralReadback => "Read back a value aloud for verification.",
194 ActionType::MachineInfo => "Record system and environment information.",
195 ActionType::Attest => "Record a signed attestation from a participant.",
196 ActionType::GatherEntropy => "Fold human-supplied entropy into the ceremony seed.",
197 ActionType::TpmAttest => "Record TPM platform attestation (PCR values).",
198 ActionType::GenerateKeypair => "Generate an asymmetric keypair.",
199 ActionType::ExportPublic => "Export the public component of a keypair.",
200 ActionType::WrapKey => "Wrap (encrypt) a key for secure transport.",
201 ActionType::UnwrapKey => "Unwrap (decrypt) a transported key.",
202 ActionType::GenerateCsr => "Generate a Certificate Signing Request.",
203 ActionType::IssueCertificate => "Issue an X.509 certificate from a CSR.",
204 ActionType::PivReadCertificate => "Read a certificate from a PIV smart card slot.",
205 ActionType::PivSign => "Sign data using a PIV smart card key.",
206 ActionType::YubikeyAttestSlot => "Attest a YubiKey PIV slot key.",
207 }
208 }
209}
210
211impl std::fmt::Display for ActionType {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 match self {
214 ActionType::ClockCheck => write!(f, "clock_check"),
215 ActionType::Confirm => write!(f, "confirm"),
216 ActionType::CheckValue => write!(f, "check_value"),
217 ActionType::OralReadback => write!(f, "oral_readback"),
218 ActionType::MachineInfo => write!(f, "machine_info"),
219 ActionType::GenerateKeypair => write!(f, "generate_keypair"),
220 ActionType::WrapKey => write!(f, "wrap_key"),
221 ActionType::UnwrapKey => write!(f, "unwrap_key"),
222 ActionType::ExportPublic => write!(f, "export_public"),
223 ActionType::Attest => write!(f, "attest"),
224 ActionType::GatherEntropy => write!(f, "gather_entropy"),
225 ActionType::TpmAttest => write!(f, "tpm_attest"),
226 ActionType::PivReadCertificate => write!(f, "piv_read_certificate"),
227 ActionType::PivSign => write!(f, "piv_sign"),
228 ActionType::YubikeyAttestSlot => write!(f, "yubikey_attest_slot"),
229 ActionType::IssueCertificate => write!(f, "issue_certificate"),
230 ActionType::GenerateCsr => write!(f, "generate_csr"),
231 }
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
237#[serde(rename_all = "snake_case")]
238#[non_exhaustive]
239pub enum OutputType {
240 PublicKey,
242 WrappedKey,
244 Certificate,
246 SignedRrset,
248 Sct,
250
251 Document,
254 CeremonyLog,
256}
257
258impl std::fmt::Display for OutputType {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 match self {
261 OutputType::PublicKey => write!(f, "public_key"),
262 OutputType::WrappedKey => write!(f, "wrapped_key"),
263 OutputType::Certificate => write!(f, "certificate"),
264 OutputType::SignedRrset => write!(f, "signed_rrset"),
265 OutputType::Sct => write!(f, "sct"),
266 OutputType::Document => write!(f, "document"),
267 OutputType::CeremonyLog => write!(f, "ceremony_log"),
268 }
269 }
270}
271
272impl OutputType {
273 pub fn default_extension(&self) -> &'static str {
275 match self {
276 OutputType::PublicKey | OutputType::Certificate => "pem",
277 OutputType::WrappedKey | OutputType::SignedRrset | OutputType::Sct => "bin",
278 OutputType::Document => "txt",
279 OutputType::CeremonyLog => "json",
280 }
281 }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286#[serde(rename_all = "snake_case")]
287#[non_exhaustive]
288pub enum ParameterType {
289 String,
291 Date,
293 Integer,
295 Boolean,
297}
298
299impl std::fmt::Display for ParameterType {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 match self {
302 ParameterType::String => write!(f, "string"),
303 ParameterType::Date => write!(f, "date (YYYY-MM-DD)"),
304 ParameterType::Integer => write!(f, "integer"),
305 ParameterType::Boolean => write!(f, "boolean (true/false)"),
306 }
307 }
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
318#[serde(rename_all = "snake_case")]
319#[non_exhaustive]
320pub enum DutyType {
321 ReturnToVault,
323 DistributeShares,
325 DistributeMedia,
327 ArchiveMaterials,
329 PublishRecord,
331 NotifyStakeholders,
333 ImportKeys,
335 Custom,
337}
338
339impl DutyType {
340 pub fn display_name(&self) -> &'static str {
342 match self {
343 DutyType::ReturnToVault => "Return to Vault",
344 DutyType::DistributeShares => "Distribute Key Shares",
345 DutyType::DistributeMedia => "Distribute Backup Media",
346 DutyType::ArchiveMaterials => "Archive Materials",
347 DutyType::PublishRecord => "Publish Record",
348 DutyType::NotifyStakeholders => "Notify Stakeholders",
349 DutyType::ImportKeys => "Import Keys",
350 DutyType::Custom => "Custom Duty",
351 }
352 }
353
354 pub fn built_in_prose(&self) -> Option<&'static str> {
358 match self {
359 DutyType::ReturnToVault => Some("Return materials to secure storage"),
360 DutyType::DistributeShares => Some("Distribute key shares to assigned custodians"),
361 DutyType::DistributeMedia => Some("Distribute backup media to designated recipients"),
362 DutyType::ArchiveMaterials => {
363 Some("Archive ceremony materials at designated storage locations")
364 }
365 DutyType::PublishRecord => Some("Publish ceremony record and witness attestations"),
366 DutyType::NotifyStakeholders => Some("Notify stakeholders of ceremony completion"),
367 DutyType::ImportKeys => Some("Import generated keys into operational system"),
368 DutyType::Custom => None,
369 }
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn role_type_extracts_prefix() {
379 assert_eq!(role_type("witness__1"), "witness");
380 assert_eq!(role_type("hsm_operator__primary"), "hsm_operator");
381 assert_eq!(role_type("operator"), "operator");
382 assert_eq!(role_type("witness__"), "witness");
383 }
384
385 #[test]
386 fn derive_step_name_title_cases() {
387 assert_eq!(derive_step_name("verify_time"), "Verify Time");
388 assert_eq!(derive_step_name("generate_root_ca"), "Generate Root Ca");
389 assert_eq!(derive_step_name("wrap_root_ca_key"), "Wrap Root Ca Key");
390 assert_eq!(derive_step_name("witness1_attest"), "Witness1 Attest");
391 assert_eq!(derive_step_name("clock-check"), "Clock Check");
392 assert_eq!(derive_step_name("opening"), "Opening");
393 }
394
395 #[test]
396 fn derive_role_name_title_cases() {
397 assert_eq!(derive_role_name("witness__1"), "Witness 1");
398 assert_eq!(
399 derive_role_name("hsm_operator__primary"),
400 "Hsm Operator Primary"
401 );
402 assert_eq!(derive_role_name("operator"), "Operator");
403 assert_eq!(derive_role_name("ceremony-admin"), "Ceremony Admin");
404 }
405
406 #[test]
407 fn action_type_serde_roundtrip() {
408 let cases: &[(ActionType, &str)] = &[
411 (ActionType::ClockCheck, "\"clock_check\""),
412 (ActionType::Confirm, "\"confirm\""),
413 (ActionType::CheckValue, "\"check_value\""),
414 (ActionType::OralReadback, "\"oral_readback\""),
415 (ActionType::MachineInfo, "\"machine_info\""),
416 (ActionType::GenerateKeypair, "\"generate_keypair\""),
417 (ActionType::WrapKey, "\"wrap_key\""),
418 (ActionType::UnwrapKey, "\"unwrap_key\""),
419 (ActionType::ExportPublic, "\"export_public\""),
420 (ActionType::Attest, "\"attest\""),
421 (ActionType::TpmAttest, "\"tpm_attest\""),
422 (ActionType::PivReadCertificate, "\"piv_read_certificate\""),
423 (ActionType::PivSign, "\"piv_sign\""),
424 (ActionType::YubikeyAttestSlot, "\"yubikey_attest_slot\""),
425 (ActionType::IssueCertificate, "\"issue_certificate\""),
426 (ActionType::GenerateCsr, "\"generate_csr\""),
427 ];
428 for &(variant, expected) in cases {
429 let serialized = serde_json::to_string(&variant).unwrap();
430 assert_eq!(serialized, expected, "serialize {variant:?}");
431 let deserialized: ActionType = serde_json::from_str(expected).unwrap();
432 assert_eq!(deserialized, variant, "deserialize {expected}");
433 }
434 }
435
436 #[test]
437 fn action_type_display_matches_serde() {
438 let actions = [
441 ActionType::ClockCheck,
442 ActionType::Confirm,
443 ActionType::CheckValue,
444 ActionType::OralReadback,
445 ActionType::MachineInfo,
446 ActionType::GenerateKeypair,
447 ActionType::WrapKey,
448 ActionType::UnwrapKey,
449 ActionType::ExportPublic,
450 ActionType::Attest,
451 ActionType::TpmAttest,
452 ActionType::PivReadCertificate,
453 ActionType::PivSign,
454 ActionType::YubikeyAttestSlot,
455 ActionType::IssueCertificate,
456 ActionType::GenerateCsr,
457 ];
458 for action in actions {
459 let display = action.to_string();
460 let serde_json = serde_json::to_string(&action).unwrap();
461 assert_eq!(
462 display,
463 serde_json.trim_matches('"'),
464 "Display and serde disagree for {action:?}"
465 );
466 }
467 }
468
469 #[test]
470 fn output_type_serde_roundtrip() {
471 let cases: &[(OutputType, &str)] = &[
474 (OutputType::PublicKey, "\"public_key\""),
475 (OutputType::WrappedKey, "\"wrapped_key\""),
476 (OutputType::Certificate, "\"certificate\""),
477 (OutputType::Document, "\"document\""),
478 (OutputType::CeremonyLog, "\"ceremony_log\""),
479 ];
480 for &(variant, expected) in cases {
481 let serialized = serde_json::to_string(&variant).unwrap();
482 assert_eq!(serialized, expected, "serialize {variant:?}");
483 let deserialized: OutputType = serde_json::from_str(expected).unwrap();
484 assert_eq!(deserialized, variant, "deserialize {expected}");
485 }
486 }
487
488 #[test]
489 fn parameter_type_serde_roundtrip() {
490 let cases: &[(ParameterType, &str)] = &[
492 (ParameterType::String, "\"string\""),
493 (ParameterType::Date, "\"date\""),
494 (ParameterType::Integer, "\"integer\""),
495 (ParameterType::Boolean, "\"boolean\""),
496 ];
497 for (variant, expected) in cases {
498 let serialized = serde_json::to_string(variant).unwrap();
499 assert_eq!(serialized, *expected, "serialize {variant:?}");
500 let deserialized: ParameterType = serde_json::from_str(expected).unwrap();
501 assert_eq!(
503 serde_json::to_string(&deserialized).unwrap(),
504 *expected,
505 "deserialize {expected}"
506 );
507 }
508 }
509
510 #[test]
511 fn duty_type_serde_roundtrip() {
512 let cases: &[(DutyType, &str)] = &[
514 (DutyType::ReturnToVault, "\"return_to_vault\""),
515 (DutyType::DistributeShares, "\"distribute_shares\""),
516 (DutyType::DistributeMedia, "\"distribute_media\""),
517 (DutyType::ArchiveMaterials, "\"archive_materials\""),
518 (DutyType::PublishRecord, "\"publish_record\""),
519 (DutyType::NotifyStakeholders, "\"notify_stakeholders\""),
520 (DutyType::ImportKeys, "\"import_keys\""),
521 (DutyType::Custom, "\"custom\""),
522 ];
523 for (variant, expected) in cases {
524 let serialized = serde_json::to_string(variant).unwrap();
525 assert_eq!(serialized, *expected, "serialize {variant:?}");
526 let deserialized: DutyType = serde_json::from_str(expected).unwrap();
527 assert_eq!(deserialized, *variant, "deserialize {expected}");
528 }
529 }
530
531 #[test]
532 fn duty_type_custom_has_no_built_in_prose() {
533 assert!(
536 DutyType::Custom.built_in_prose().is_none(),
537 "Custom duty must have no built-in prose"
538 );
539 for duty in [
541 DutyType::ReturnToVault,
542 DutyType::DistributeShares,
543 DutyType::DistributeMedia,
544 DutyType::ArchiveMaterials,
545 DutyType::PublishRecord,
546 DutyType::NotifyStakeholders,
547 DutyType::ImportKeys,
548 ] {
549 assert!(
550 duty.built_in_prose().is_some(),
551 "{duty:?} must have built-in prose"
552 );
553 }
554 }
555}