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 TpmAttest,
107
108 PivReadCertificate,
113 PivSign,
118 YubikeyAttestSlot,
123
124 IssueCertificate,
130 GenerateCsr,
136}
137
138impl ActionType {
139 pub fn requires_backend(self) -> bool {
146 matches!(
147 self,
148 ActionType::GenerateKeypair
149 | ActionType::WrapKey
150 | ActionType::UnwrapKey
151 | ActionType::ExportPublic
152 | ActionType::GenerateCsr
153 | ActionType::IssueCertificate
154 | ActionType::PivReadCertificate
155 | ActionType::PivSign
156 | ActionType::YubikeyAttestSlot
157 | ActionType::TpmAttest
158 )
159 }
160
161 pub fn required_with_fields(self) -> &'static [&'static str] {
170 match self {
171 ActionType::CheckValue => &["actual", "expected"],
172 ActionType::GenerateCsr => &["subject"],
173 _ => &[],
174 }
175 }
176
177 pub fn describe(&self) -> &'static str {
182 match self {
183 ActionType::ClockCheck => "Verify system clock against a reference time.",
184 ActionType::Confirm => "Confirm readiness or completion of a manual step.",
185 ActionType::CheckValue => "Verify a value matches an expected result.",
186 ActionType::OralReadback => "Read back a value aloud for verification.",
187 ActionType::MachineInfo => "Record system and environment information.",
188 ActionType::Attest => "Record a signed attestation from a participant.",
189 ActionType::TpmAttest => "Record TPM platform attestation (PCR values).",
190 ActionType::GenerateKeypair => "Generate an asymmetric keypair.",
191 ActionType::ExportPublic => "Export the public component of a keypair.",
192 ActionType::WrapKey => "Wrap (encrypt) a key for secure transport.",
193 ActionType::UnwrapKey => "Unwrap (decrypt) a transported key.",
194 ActionType::GenerateCsr => "Generate a Certificate Signing Request.",
195 ActionType::IssueCertificate => "Issue an X.509 certificate from a CSR.",
196 ActionType::PivReadCertificate => "Read a certificate from a PIV smart card slot.",
197 ActionType::PivSign => "Sign data using a PIV smart card key.",
198 ActionType::YubikeyAttestSlot => "Attest a YubiKey PIV slot key.",
199 }
200 }
201}
202
203impl std::fmt::Display for ActionType {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 match self {
206 ActionType::ClockCheck => write!(f, "clock_check"),
207 ActionType::Confirm => write!(f, "confirm"),
208 ActionType::CheckValue => write!(f, "check_value"),
209 ActionType::OralReadback => write!(f, "oral_readback"),
210 ActionType::MachineInfo => write!(f, "machine_info"),
211 ActionType::GenerateKeypair => write!(f, "generate_keypair"),
212 ActionType::WrapKey => write!(f, "wrap_key"),
213 ActionType::UnwrapKey => write!(f, "unwrap_key"),
214 ActionType::ExportPublic => write!(f, "export_public"),
215 ActionType::Attest => write!(f, "attest"),
216 ActionType::TpmAttest => write!(f, "tpm_attest"),
217 ActionType::PivReadCertificate => write!(f, "piv_read_certificate"),
218 ActionType::PivSign => write!(f, "piv_sign"),
219 ActionType::YubikeyAttestSlot => write!(f, "yubikey_attest_slot"),
220 ActionType::IssueCertificate => write!(f, "issue_certificate"),
221 ActionType::GenerateCsr => write!(f, "generate_csr"),
222 }
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
228#[serde(rename_all = "snake_case")]
229#[non_exhaustive]
230pub enum OutputType {
231 PublicKey,
233 WrappedKey,
235 Certificate,
237 SignedRrset,
239 Sct,
241
242 Document,
245 CeremonyLog,
247}
248
249impl std::fmt::Display for OutputType {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 match self {
252 OutputType::PublicKey => write!(f, "public_key"),
253 OutputType::WrappedKey => write!(f, "wrapped_key"),
254 OutputType::Certificate => write!(f, "certificate"),
255 OutputType::SignedRrset => write!(f, "signed_rrset"),
256 OutputType::Sct => write!(f, "sct"),
257 OutputType::Document => write!(f, "document"),
258 OutputType::CeremonyLog => write!(f, "ceremony_log"),
259 }
260 }
261}
262
263impl OutputType {
264 pub fn default_extension(&self) -> &'static str {
266 match self {
267 OutputType::PublicKey | OutputType::Certificate => "pem",
268 OutputType::WrappedKey | OutputType::SignedRrset | OutputType::Sct => "bin",
269 OutputType::Document => "txt",
270 OutputType::CeremonyLog => "json",
271 }
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277#[serde(rename_all = "snake_case")]
278#[non_exhaustive]
279pub enum ParameterType {
280 String,
282 Date,
284 Integer,
286 Boolean,
288}
289
290impl std::fmt::Display for ParameterType {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 match self {
293 ParameterType::String => write!(f, "string"),
294 ParameterType::Date => write!(f, "date (YYYY-MM-DD)"),
295 ParameterType::Integer => write!(f, "integer"),
296 ParameterType::Boolean => write!(f, "boolean (true/false)"),
297 }
298 }
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
309#[serde(rename_all = "snake_case")]
310#[non_exhaustive]
311pub enum DutyType {
312 ReturnToVault,
314 DistributeShares,
316 DistributeMedia,
318 ArchiveMaterials,
320 PublishRecord,
322 NotifyStakeholders,
324 ImportKeys,
326 Custom,
328}
329
330impl DutyType {
331 pub fn display_name(&self) -> &'static str {
333 match self {
334 DutyType::ReturnToVault => "Return to Vault",
335 DutyType::DistributeShares => "Distribute Key Shares",
336 DutyType::DistributeMedia => "Distribute Backup Media",
337 DutyType::ArchiveMaterials => "Archive Materials",
338 DutyType::PublishRecord => "Publish Record",
339 DutyType::NotifyStakeholders => "Notify Stakeholders",
340 DutyType::ImportKeys => "Import Keys",
341 DutyType::Custom => "Custom Duty",
342 }
343 }
344
345 pub fn built_in_prose(&self) -> Option<&'static str> {
349 match self {
350 DutyType::ReturnToVault => Some("Return materials to secure storage"),
351 DutyType::DistributeShares => Some("Distribute key shares to assigned custodians"),
352 DutyType::DistributeMedia => Some("Distribute backup media to designated recipients"),
353 DutyType::ArchiveMaterials => {
354 Some("Archive ceremony materials at designated storage locations")
355 }
356 DutyType::PublishRecord => Some("Publish ceremony record and witness attestations"),
357 DutyType::NotifyStakeholders => Some("Notify stakeholders of ceremony completion"),
358 DutyType::ImportKeys => Some("Import generated keys into operational system"),
359 DutyType::Custom => None,
360 }
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn role_type_extracts_prefix() {
370 assert_eq!(role_type("witness__1"), "witness");
371 assert_eq!(role_type("hsm_operator__primary"), "hsm_operator");
372 assert_eq!(role_type("operator"), "operator");
373 assert_eq!(role_type("witness__"), "witness");
374 }
375
376 #[test]
377 fn derive_step_name_title_cases() {
378 assert_eq!(derive_step_name("verify_time"), "Verify Time");
379 assert_eq!(derive_step_name("generate_root_ca"), "Generate Root Ca");
380 assert_eq!(derive_step_name("wrap_root_ca_key"), "Wrap Root Ca Key");
381 assert_eq!(derive_step_name("witness1_attest"), "Witness1 Attest");
382 assert_eq!(derive_step_name("clock-check"), "Clock Check");
383 assert_eq!(derive_step_name("opening"), "Opening");
384 }
385
386 #[test]
387 fn derive_role_name_title_cases() {
388 assert_eq!(derive_role_name("witness__1"), "Witness 1");
389 assert_eq!(
390 derive_role_name("hsm_operator__primary"),
391 "Hsm Operator Primary"
392 );
393 assert_eq!(derive_role_name("operator"), "Operator");
394 assert_eq!(derive_role_name("ceremony-admin"), "Ceremony Admin");
395 }
396
397 #[test]
398 fn action_type_serde_roundtrip() {
399 let cases: &[(ActionType, &str)] = &[
402 (ActionType::ClockCheck, "\"clock_check\""),
403 (ActionType::Confirm, "\"confirm\""),
404 (ActionType::CheckValue, "\"check_value\""),
405 (ActionType::OralReadback, "\"oral_readback\""),
406 (ActionType::MachineInfo, "\"machine_info\""),
407 (ActionType::GenerateKeypair, "\"generate_keypair\""),
408 (ActionType::WrapKey, "\"wrap_key\""),
409 (ActionType::UnwrapKey, "\"unwrap_key\""),
410 (ActionType::ExportPublic, "\"export_public\""),
411 (ActionType::Attest, "\"attest\""),
412 (ActionType::TpmAttest, "\"tpm_attest\""),
413 (ActionType::PivReadCertificate, "\"piv_read_certificate\""),
414 (ActionType::PivSign, "\"piv_sign\""),
415 (ActionType::YubikeyAttestSlot, "\"yubikey_attest_slot\""),
416 (ActionType::IssueCertificate, "\"issue_certificate\""),
417 (ActionType::GenerateCsr, "\"generate_csr\""),
418 ];
419 for &(variant, expected) in cases {
420 let serialized = serde_json::to_string(&variant).unwrap();
421 assert_eq!(serialized, expected, "serialize {variant:?}");
422 let deserialized: ActionType = serde_json::from_str(expected).unwrap();
423 assert_eq!(deserialized, variant, "deserialize {expected}");
424 }
425 }
426
427 #[test]
428 fn action_type_display_matches_serde() {
429 let actions = [
432 ActionType::ClockCheck,
433 ActionType::Confirm,
434 ActionType::CheckValue,
435 ActionType::OralReadback,
436 ActionType::MachineInfo,
437 ActionType::GenerateKeypair,
438 ActionType::WrapKey,
439 ActionType::UnwrapKey,
440 ActionType::ExportPublic,
441 ActionType::Attest,
442 ActionType::TpmAttest,
443 ActionType::PivReadCertificate,
444 ActionType::PivSign,
445 ActionType::YubikeyAttestSlot,
446 ActionType::IssueCertificate,
447 ActionType::GenerateCsr,
448 ];
449 for action in actions {
450 let display = action.to_string();
451 let serde_json = serde_json::to_string(&action).unwrap();
452 assert_eq!(
453 display,
454 serde_json.trim_matches('"'),
455 "Display and serde disagree for {action:?}"
456 );
457 }
458 }
459
460 #[test]
461 fn output_type_serde_roundtrip() {
462 let cases: &[(OutputType, &str)] = &[
465 (OutputType::PublicKey, "\"public_key\""),
466 (OutputType::WrappedKey, "\"wrapped_key\""),
467 (OutputType::Certificate, "\"certificate\""),
468 (OutputType::Document, "\"document\""),
469 (OutputType::CeremonyLog, "\"ceremony_log\""),
470 ];
471 for &(variant, expected) in cases {
472 let serialized = serde_json::to_string(&variant).unwrap();
473 assert_eq!(serialized, expected, "serialize {variant:?}");
474 let deserialized: OutputType = serde_json::from_str(expected).unwrap();
475 assert_eq!(deserialized, variant, "deserialize {expected}");
476 }
477 }
478
479 #[test]
480 fn parameter_type_serde_roundtrip() {
481 let cases: &[(ParameterType, &str)] = &[
483 (ParameterType::String, "\"string\""),
484 (ParameterType::Date, "\"date\""),
485 (ParameterType::Integer, "\"integer\""),
486 (ParameterType::Boolean, "\"boolean\""),
487 ];
488 for (variant, expected) in cases {
489 let serialized = serde_json::to_string(variant).unwrap();
490 assert_eq!(serialized, *expected, "serialize {variant:?}");
491 let deserialized: ParameterType = serde_json::from_str(expected).unwrap();
492 assert_eq!(
494 serde_json::to_string(&deserialized).unwrap(),
495 *expected,
496 "deserialize {expected}"
497 );
498 }
499 }
500
501 #[test]
502 fn duty_type_serde_roundtrip() {
503 let cases: &[(DutyType, &str)] = &[
505 (DutyType::ReturnToVault, "\"return_to_vault\""),
506 (DutyType::DistributeShares, "\"distribute_shares\""),
507 (DutyType::DistributeMedia, "\"distribute_media\""),
508 (DutyType::ArchiveMaterials, "\"archive_materials\""),
509 (DutyType::PublishRecord, "\"publish_record\""),
510 (DutyType::NotifyStakeholders, "\"notify_stakeholders\""),
511 (DutyType::ImportKeys, "\"import_keys\""),
512 (DutyType::Custom, "\"custom\""),
513 ];
514 for (variant, expected) in cases {
515 let serialized = serde_json::to_string(variant).unwrap();
516 assert_eq!(serialized, *expected, "serialize {variant:?}");
517 let deserialized: DutyType = serde_json::from_str(expected).unwrap();
518 assert_eq!(deserialized, *variant, "deserialize {expected}");
519 }
520 }
521
522 #[test]
523 fn duty_type_custom_has_no_built_in_prose() {
524 assert!(
527 DutyType::Custom.built_in_prose().is_none(),
528 "Custom duty must have no built-in prose"
529 );
530 for duty in [
532 DutyType::ReturnToVault,
533 DutyType::DistributeShares,
534 DutyType::DistributeMedia,
535 DutyType::ArchiveMaterials,
536 DutyType::PublishRecord,
537 DutyType::NotifyStakeholders,
538 DutyType::ImportKeys,
539 ] {
540 assert!(
541 duty.built_in_prose().is_some(),
542 "{duty:?} must have built-in prose"
543 );
544 }
545 }
546}