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 SignData,
106 VerifySignature,
113 Attest,
115 GatherEntropy,
122 TpmAttest,
126
127 PivReadCertificate,
132 PivSign,
137 YubikeyAttestSlot,
142
143 IssueCertificate,
149 GenerateCsr,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165#[non_exhaustive]
166pub enum BackendUsage {
167 Required,
169 SoftwareUnlessNamed,
173 Unused,
175}
176
177impl ActionType {
178 pub const ALL: &'static [ActionType] = &[
185 ActionType::ClockCheck,
186 ActionType::Confirm,
187 ActionType::CheckValue,
188 ActionType::OralReadback,
189 ActionType::MachineInfo,
190 ActionType::GenerateKeypair,
191 ActionType::WrapKey,
192 ActionType::UnwrapKey,
193 ActionType::ExportPublic,
194 ActionType::SignData,
195 ActionType::VerifySignature,
196 ActionType::Attest,
197 ActionType::GatherEntropy,
198 ActionType::TpmAttest,
199 ActionType::PivReadCertificate,
200 ActionType::PivSign,
201 ActionType::YubikeyAttestSlot,
202 ActionType::IssueCertificate,
203 ActionType::GenerateCsr,
204 ];
205
206 pub fn backend_usage(self) -> BackendUsage {
212 match self {
213 ActionType::GenerateKeypair
214 | ActionType::SignData
215 | ActionType::WrapKey
216 | ActionType::UnwrapKey
217 | ActionType::ExportPublic
218 | ActionType::GenerateCsr
219 | ActionType::IssueCertificate
220 | ActionType::PivReadCertificate
221 | ActionType::PivSign
222 | ActionType::YubikeyAttestSlot
223 | ActionType::TpmAttest => BackendUsage::Required,
224
225 ActionType::VerifySignature => BackendUsage::SoftwareUnlessNamed,
226
227 ActionType::ClockCheck
228 | ActionType::Confirm
229 | ActionType::CheckValue
230 | ActionType::OralReadback
231 | ActionType::MachineInfo
232 | ActionType::Attest
233 | ActionType::GatherEntropy => BackendUsage::Unused,
234 }
235 }
236
237 pub fn required_with_fields(self) -> &'static [&'static str] {
246 match self {
247 ActionType::CheckValue => &["actual", "expected"],
248 ActionType::GenerateCsr => &["subject"],
249 _ => &[],
250 }
251 }
252
253 pub fn describe(&self) -> &'static str {
258 match self {
259 ActionType::ClockCheck => "Verify system clock against a reference time.",
260 ActionType::Confirm => "Confirm readiness or completion of a manual step.",
261 ActionType::CheckValue => "Verify a value matches an expected result.",
262 ActionType::OralReadback => "Read back a value aloud for verification.",
263 ActionType::MachineInfo => "Record system and environment information.",
264 ActionType::Attest => "Record a signed attestation from a participant.",
265 ActionType::GatherEntropy => "Fold human-supplied entropy into the ceremony seed.",
266 ActionType::TpmAttest => "Record TPM platform attestation (PCR values).",
267 ActionType::GenerateKeypair => "Generate an asymmetric keypair.",
268 ActionType::ExportPublic => "Export the public component of a keypair.",
269 ActionType::SignData => "Sign data with a ceremony key.",
270 ActionType::VerifySignature => "Verify a signature against a public key.",
271 ActionType::WrapKey => "Wrap (encrypt) a key for secure transport.",
272 ActionType::UnwrapKey => "Unwrap (decrypt) a transported key.",
273 ActionType::GenerateCsr => "Generate a Certificate Signing Request.",
274 ActionType::IssueCertificate => "Issue an X.509 certificate from a CSR.",
275 ActionType::PivReadCertificate => "Read a certificate from a PIV smart card slot.",
276 ActionType::PivSign => "Sign data using a PIV smart card key.",
277 ActionType::YubikeyAttestSlot => "Attest a YubiKey PIV slot key.",
278 }
279 }
280}
281
282impl std::fmt::Display for ActionType {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 match self {
285 ActionType::ClockCheck => write!(f, "clock_check"),
286 ActionType::Confirm => write!(f, "confirm"),
287 ActionType::CheckValue => write!(f, "check_value"),
288 ActionType::OralReadback => write!(f, "oral_readback"),
289 ActionType::MachineInfo => write!(f, "machine_info"),
290 ActionType::GenerateKeypair => write!(f, "generate_keypair"),
291 ActionType::WrapKey => write!(f, "wrap_key"),
292 ActionType::UnwrapKey => write!(f, "unwrap_key"),
293 ActionType::ExportPublic => write!(f, "export_public"),
294 ActionType::SignData => write!(f, "sign_data"),
295 ActionType::VerifySignature => write!(f, "verify_signature"),
296 ActionType::Attest => write!(f, "attest"),
297 ActionType::GatherEntropy => write!(f, "gather_entropy"),
298 ActionType::TpmAttest => write!(f, "tpm_attest"),
299 ActionType::PivReadCertificate => write!(f, "piv_read_certificate"),
300 ActionType::PivSign => write!(f, "piv_sign"),
301 ActionType::YubikeyAttestSlot => write!(f, "yubikey_attest_slot"),
302 ActionType::IssueCertificate => write!(f, "issue_certificate"),
303 ActionType::GenerateCsr => write!(f, "generate_csr"),
304 }
305 }
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
310#[serde(rename_all = "snake_case")]
311#[non_exhaustive]
312pub enum OutputType {
313 PublicKey,
315 WrappedKey,
317 Certificate,
319 SignedRrset,
321 Sct,
323
324 Document,
327 CeremonyLog,
329}
330
331impl std::fmt::Display for OutputType {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 match self {
334 OutputType::PublicKey => write!(f, "public_key"),
335 OutputType::WrappedKey => write!(f, "wrapped_key"),
336 OutputType::Certificate => write!(f, "certificate"),
337 OutputType::SignedRrset => write!(f, "signed_rrset"),
338 OutputType::Sct => write!(f, "sct"),
339 OutputType::Document => write!(f, "document"),
340 OutputType::CeremonyLog => write!(f, "ceremony_log"),
341 }
342 }
343}
344
345impl OutputType {
346 pub fn default_extension(&self) -> &'static str {
348 match self {
349 OutputType::PublicKey | OutputType::Certificate => "pem",
350 OutputType::WrappedKey | OutputType::SignedRrset | OutputType::Sct => "bin",
351 OutputType::Document => "txt",
352 OutputType::CeremonyLog => "json",
353 }
354 }
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359#[serde(rename_all = "snake_case")]
360#[non_exhaustive]
361pub enum ParameterType {
362 String,
364 Date,
366 Integer,
368 Boolean,
370}
371
372impl std::fmt::Display for ParameterType {
373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374 match self {
375 ParameterType::String => write!(f, "string"),
376 ParameterType::Date => write!(f, "date (YYYY-MM-DD)"),
377 ParameterType::Integer => write!(f, "integer"),
378 ParameterType::Boolean => write!(f, "boolean (true/false)"),
379 }
380 }
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
391#[serde(rename_all = "snake_case")]
392#[non_exhaustive]
393pub enum DutyType {
394 ReturnToVault,
396 DistributeShares,
398 DistributeMedia,
400 ArchiveMaterials,
402 PublishRecord,
404 NotifyStakeholders,
406 ImportKeys,
408 Custom,
410}
411
412impl DutyType {
413 pub fn display_name(&self) -> &'static str {
415 match self {
416 DutyType::ReturnToVault => "Return to Vault",
417 DutyType::DistributeShares => "Distribute Key Shares",
418 DutyType::DistributeMedia => "Distribute Backup Media",
419 DutyType::ArchiveMaterials => "Archive Materials",
420 DutyType::PublishRecord => "Publish Record",
421 DutyType::NotifyStakeholders => "Notify Stakeholders",
422 DutyType::ImportKeys => "Import Keys",
423 DutyType::Custom => "Custom Duty",
424 }
425 }
426
427 pub fn built_in_prose(&self) -> Option<&'static str> {
431 match self {
432 DutyType::ReturnToVault => Some("Return materials to secure storage"),
433 DutyType::DistributeShares => Some("Distribute key shares to assigned custodians"),
434 DutyType::DistributeMedia => Some("Distribute backup media to designated recipients"),
435 DutyType::ArchiveMaterials => {
436 Some("Archive ceremony materials at designated storage locations")
437 }
438 DutyType::PublishRecord => Some("Publish ceremony record and witness attestations"),
439 DutyType::NotifyStakeholders => Some("Notify stakeholders of ceremony completion"),
440 DutyType::ImportKeys => Some("Import generated keys into operational system"),
441 DutyType::Custom => None,
442 }
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn role_type_extracts_prefix() {
452 assert_eq!(role_type("witness__1"), "witness");
453 assert_eq!(role_type("hsm_operator__primary"), "hsm_operator");
454 assert_eq!(role_type("operator"), "operator");
455 assert_eq!(role_type("witness__"), "witness");
456 }
457
458 #[test]
459 fn derive_step_name_title_cases() {
460 assert_eq!(derive_step_name("verify_time"), "Verify Time");
461 assert_eq!(derive_step_name("generate_root_ca"), "Generate Root Ca");
462 assert_eq!(derive_step_name("wrap_root_ca_key"), "Wrap Root Ca Key");
463 assert_eq!(derive_step_name("witness1_attest"), "Witness1 Attest");
464 assert_eq!(derive_step_name("clock-check"), "Clock Check");
465 assert_eq!(derive_step_name("opening"), "Opening");
466 }
467
468 #[test]
469 fn derive_role_name_title_cases() {
470 assert_eq!(derive_role_name("witness__1"), "Witness 1");
471 assert_eq!(
472 derive_role_name("hsm_operator__primary"),
473 "Hsm Operator Primary"
474 );
475 assert_eq!(derive_role_name("operator"), "Operator");
476 assert_eq!(derive_role_name("ceremony-admin"), "Ceremony Admin");
477 }
478
479 #[test]
480 fn action_type_serde_roundtrip() {
481 let cases: &[(ActionType, &str)] = &[
484 (ActionType::ClockCheck, "\"clock_check\""),
485 (ActionType::Confirm, "\"confirm\""),
486 (ActionType::CheckValue, "\"check_value\""),
487 (ActionType::OralReadback, "\"oral_readback\""),
488 (ActionType::MachineInfo, "\"machine_info\""),
489 (ActionType::GenerateKeypair, "\"generate_keypair\""),
490 (ActionType::WrapKey, "\"wrap_key\""),
491 (ActionType::UnwrapKey, "\"unwrap_key\""),
492 (ActionType::ExportPublic, "\"export_public\""),
493 (ActionType::Attest, "\"attest\""),
494 (ActionType::TpmAttest, "\"tpm_attest\""),
495 (ActionType::PivReadCertificate, "\"piv_read_certificate\""),
496 (ActionType::PivSign, "\"piv_sign\""),
497 (ActionType::YubikeyAttestSlot, "\"yubikey_attest_slot\""),
498 (ActionType::IssueCertificate, "\"issue_certificate\""),
499 (ActionType::GenerateCsr, "\"generate_csr\""),
500 ];
501 for &(variant, expected) in cases {
502 let serialized = serde_json::to_string(&variant).unwrap();
503 assert_eq!(serialized, expected, "serialize {variant:?}");
504 let deserialized: ActionType = serde_json::from_str(expected).unwrap();
505 assert_eq!(deserialized, variant, "deserialize {expected}");
506 }
507 }
508
509 #[test]
510 fn action_type_display_matches_serde() {
511 let actions = [
514 ActionType::ClockCheck,
515 ActionType::Confirm,
516 ActionType::CheckValue,
517 ActionType::OralReadback,
518 ActionType::MachineInfo,
519 ActionType::GenerateKeypair,
520 ActionType::WrapKey,
521 ActionType::UnwrapKey,
522 ActionType::ExportPublic,
523 ActionType::Attest,
524 ActionType::TpmAttest,
525 ActionType::PivReadCertificate,
526 ActionType::PivSign,
527 ActionType::YubikeyAttestSlot,
528 ActionType::IssueCertificate,
529 ActionType::GenerateCsr,
530 ];
531 for action in actions {
532 let display = action.to_string();
533 let serde_json = serde_json::to_string(&action).unwrap();
534 assert_eq!(
535 display,
536 serde_json.trim_matches('"'),
537 "Display and serde disagree for {action:?}"
538 );
539 }
540 }
541
542 #[test]
543 fn output_type_serde_roundtrip() {
544 let cases: &[(OutputType, &str)] = &[
547 (OutputType::PublicKey, "\"public_key\""),
548 (OutputType::WrappedKey, "\"wrapped_key\""),
549 (OutputType::Certificate, "\"certificate\""),
550 (OutputType::Document, "\"document\""),
551 (OutputType::CeremonyLog, "\"ceremony_log\""),
552 ];
553 for &(variant, expected) in cases {
554 let serialized = serde_json::to_string(&variant).unwrap();
555 assert_eq!(serialized, expected, "serialize {variant:?}");
556 let deserialized: OutputType = serde_json::from_str(expected).unwrap();
557 assert_eq!(deserialized, variant, "deserialize {expected}");
558 }
559 }
560
561 #[test]
562 fn parameter_type_serde_roundtrip() {
563 let cases: &[(ParameterType, &str)] = &[
565 (ParameterType::String, "\"string\""),
566 (ParameterType::Date, "\"date\""),
567 (ParameterType::Integer, "\"integer\""),
568 (ParameterType::Boolean, "\"boolean\""),
569 ];
570 for (variant, expected) in cases {
571 let serialized = serde_json::to_string(variant).unwrap();
572 assert_eq!(serialized, *expected, "serialize {variant:?}");
573 let deserialized: ParameterType = serde_json::from_str(expected).unwrap();
574 assert_eq!(
576 serde_json::to_string(&deserialized).unwrap(),
577 *expected,
578 "deserialize {expected}"
579 );
580 }
581 }
582
583 #[test]
584 fn duty_type_serde_roundtrip() {
585 let cases: &[(DutyType, &str)] = &[
587 (DutyType::ReturnToVault, "\"return_to_vault\""),
588 (DutyType::DistributeShares, "\"distribute_shares\""),
589 (DutyType::DistributeMedia, "\"distribute_media\""),
590 (DutyType::ArchiveMaterials, "\"archive_materials\""),
591 (DutyType::PublishRecord, "\"publish_record\""),
592 (DutyType::NotifyStakeholders, "\"notify_stakeholders\""),
593 (DutyType::ImportKeys, "\"import_keys\""),
594 (DutyType::Custom, "\"custom\""),
595 ];
596 for (variant, expected) in cases {
597 let serialized = serde_json::to_string(variant).unwrap();
598 assert_eq!(serialized, *expected, "serialize {variant:?}");
599 let deserialized: DutyType = serde_json::from_str(expected).unwrap();
600 assert_eq!(deserialized, *variant, "deserialize {expected}");
601 }
602 }
603
604 #[test]
605 fn duty_type_custom_has_no_built_in_prose() {
606 assert!(
609 DutyType::Custom.built_in_prose().is_none(),
610 "Custom duty must have no built-in prose"
611 );
612 for duty in [
614 DutyType::ReturnToVault,
615 DutyType::DistributeShares,
616 DutyType::DistributeMedia,
617 DutyType::ArchiveMaterials,
618 DutyType::PublishRecord,
619 DutyType::NotifyStakeholders,
620 DutyType::ImportKeys,
621 ] {
622 assert!(
623 duty.built_in_prose().is_some(),
624 "{duty:?} must have built-in prose"
625 );
626 }
627 }
628
629 #[test]
637 fn all_lists_every_action_type() {
638 for action in ActionType::ALL {
639 match action {
640 ActionType::ClockCheck
641 | ActionType::Confirm
642 | ActionType::CheckValue
643 | ActionType::OralReadback
644 | ActionType::MachineInfo
645 | ActionType::GenerateKeypair
646 | ActionType::WrapKey
647 | ActionType::UnwrapKey
648 | ActionType::ExportPublic
649 | ActionType::SignData
650 | ActionType::VerifySignature
651 | ActionType::Attest
652 | ActionType::GatherEntropy
653 | ActionType::TpmAttest
654 | ActionType::PivReadCertificate
655 | ActionType::PivSign
656 | ActionType::YubikeyAttestSlot
657 | ActionType::IssueCertificate
658 | ActionType::GenerateCsr => {}
659 }
660 }
661
662 let names: std::collections::BTreeSet<String> =
663 ActionType::ALL.iter().map(ToString::to_string).collect();
664 assert_eq!(
665 names.len(),
666 ActionType::ALL.len(),
667 "two actions share a DSL name"
668 );
669 }
670}