use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
pub fn role_type(id: &str) -> &str {
id.split_once("__").map_or(id, |(prefix, _)| prefix)
}
pub fn derive_step_name(id: &str) -> String {
id.split(['_', '-'])
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
pub fn derive_role_name(id: &str) -> String {
id.replace("__", " ")
.split(['_', '-', ' '])
.filter(|s| !s.is_empty())
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ActionType {
ClockCheck,
Confirm,
CheckValue,
OralReadback,
MachineInfo,
GenerateKeypair,
WrapKey,
UnwrapKey,
ExportPublic,
Attest,
GatherEntropy,
TpmAttest,
PivReadCertificate,
PivSign,
YubikeyAttestSlot,
IssueCertificate,
GenerateCsr,
}
impl ActionType {
pub fn requires_backend(self) -> bool {
matches!(
self,
ActionType::GenerateKeypair
| ActionType::WrapKey
| ActionType::UnwrapKey
| ActionType::ExportPublic
| ActionType::GenerateCsr
| ActionType::IssueCertificate
| ActionType::PivReadCertificate
| ActionType::PivSign
| ActionType::YubikeyAttestSlot
| ActionType::TpmAttest
)
}
pub fn required_with_fields(self) -> &'static [&'static str] {
match self {
ActionType::CheckValue => &["actual", "expected"],
ActionType::GenerateCsr => &["subject"],
_ => &[],
}
}
pub fn describe(&self) -> &'static str {
match self {
ActionType::ClockCheck => "Verify system clock against a reference time.",
ActionType::Confirm => "Confirm readiness or completion of a manual step.",
ActionType::CheckValue => "Verify a value matches an expected result.",
ActionType::OralReadback => "Read back a value aloud for verification.",
ActionType::MachineInfo => "Record system and environment information.",
ActionType::Attest => "Record a signed attestation from a participant.",
ActionType::GatherEntropy => "Fold human-supplied entropy into the ceremony seed.",
ActionType::TpmAttest => "Record TPM platform attestation (PCR values).",
ActionType::GenerateKeypair => "Generate an asymmetric keypair.",
ActionType::ExportPublic => "Export the public component of a keypair.",
ActionType::WrapKey => "Wrap (encrypt) a key for secure transport.",
ActionType::UnwrapKey => "Unwrap (decrypt) a transported key.",
ActionType::GenerateCsr => "Generate a Certificate Signing Request.",
ActionType::IssueCertificate => "Issue an X.509 certificate from a CSR.",
ActionType::PivReadCertificate => "Read a certificate from a PIV smart card slot.",
ActionType::PivSign => "Sign data using a PIV smart card key.",
ActionType::YubikeyAttestSlot => "Attest a YubiKey PIV slot key.",
}
}
}
impl std::fmt::Display for ActionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ActionType::ClockCheck => write!(f, "clock_check"),
ActionType::Confirm => write!(f, "confirm"),
ActionType::CheckValue => write!(f, "check_value"),
ActionType::OralReadback => write!(f, "oral_readback"),
ActionType::MachineInfo => write!(f, "machine_info"),
ActionType::GenerateKeypair => write!(f, "generate_keypair"),
ActionType::WrapKey => write!(f, "wrap_key"),
ActionType::UnwrapKey => write!(f, "unwrap_key"),
ActionType::ExportPublic => write!(f, "export_public"),
ActionType::Attest => write!(f, "attest"),
ActionType::GatherEntropy => write!(f, "gather_entropy"),
ActionType::TpmAttest => write!(f, "tpm_attest"),
ActionType::PivReadCertificate => write!(f, "piv_read_certificate"),
ActionType::PivSign => write!(f, "piv_sign"),
ActionType::YubikeyAttestSlot => write!(f, "yubikey_attest_slot"),
ActionType::IssueCertificate => write!(f, "issue_certificate"),
ActionType::GenerateCsr => write!(f, "generate_csr"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum OutputType {
PublicKey,
WrappedKey,
Certificate,
SignedRrset,
Sct,
Document,
CeremonyLog,
}
impl std::fmt::Display for OutputType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputType::PublicKey => write!(f, "public_key"),
OutputType::WrappedKey => write!(f, "wrapped_key"),
OutputType::Certificate => write!(f, "certificate"),
OutputType::SignedRrset => write!(f, "signed_rrset"),
OutputType::Sct => write!(f, "sct"),
OutputType::Document => write!(f, "document"),
OutputType::CeremonyLog => write!(f, "ceremony_log"),
}
}
}
impl OutputType {
pub fn default_extension(&self) -> &'static str {
match self {
OutputType::PublicKey | OutputType::Certificate => "pem",
OutputType::WrappedKey | OutputType::SignedRrset | OutputType::Sct => "bin",
OutputType::Document => "txt",
OutputType::CeremonyLog => "json",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ParameterType {
String,
Date,
Integer,
Boolean,
}
impl std::fmt::Display for ParameterType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParameterType::String => write!(f, "string"),
ParameterType::Date => write!(f, "date (YYYY-MM-DD)"),
ParameterType::Integer => write!(f, "integer"),
ParameterType::Boolean => write!(f, "boolean (true/false)"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DutyType {
ReturnToVault,
DistributeShares,
DistributeMedia,
ArchiveMaterials,
PublishRecord,
NotifyStakeholders,
ImportKeys,
Custom,
}
impl DutyType {
pub fn display_name(&self) -> &'static str {
match self {
DutyType::ReturnToVault => "Return to Vault",
DutyType::DistributeShares => "Distribute Key Shares",
DutyType::DistributeMedia => "Distribute Backup Media",
DutyType::ArchiveMaterials => "Archive Materials",
DutyType::PublishRecord => "Publish Record",
DutyType::NotifyStakeholders => "Notify Stakeholders",
DutyType::ImportKeys => "Import Keys",
DutyType::Custom => "Custom Duty",
}
}
pub fn built_in_prose(&self) -> Option<&'static str> {
match self {
DutyType::ReturnToVault => Some("Return materials to secure storage"),
DutyType::DistributeShares => Some("Distribute key shares to assigned custodians"),
DutyType::DistributeMedia => Some("Distribute backup media to designated recipients"),
DutyType::ArchiveMaterials => {
Some("Archive ceremony materials at designated storage locations")
}
DutyType::PublishRecord => Some("Publish ceremony record and witness attestations"),
DutyType::NotifyStakeholders => Some("Notify stakeholders of ceremony completion"),
DutyType::ImportKeys => Some("Import generated keys into operational system"),
DutyType::Custom => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn role_type_extracts_prefix() {
assert_eq!(role_type("witness__1"), "witness");
assert_eq!(role_type("hsm_operator__primary"), "hsm_operator");
assert_eq!(role_type("operator"), "operator");
assert_eq!(role_type("witness__"), "witness");
}
#[test]
fn derive_step_name_title_cases() {
assert_eq!(derive_step_name("verify_time"), "Verify Time");
assert_eq!(derive_step_name("generate_root_ca"), "Generate Root Ca");
assert_eq!(derive_step_name("wrap_root_ca_key"), "Wrap Root Ca Key");
assert_eq!(derive_step_name("witness1_attest"), "Witness1 Attest");
assert_eq!(derive_step_name("clock-check"), "Clock Check");
assert_eq!(derive_step_name("opening"), "Opening");
}
#[test]
fn derive_role_name_title_cases() {
assert_eq!(derive_role_name("witness__1"), "Witness 1");
assert_eq!(
derive_role_name("hsm_operator__primary"),
"Hsm Operator Primary"
);
assert_eq!(derive_role_name("operator"), "Operator");
assert_eq!(derive_role_name("ceremony-admin"), "Ceremony Admin");
}
#[test]
fn action_type_serde_roundtrip() {
let cases: &[(ActionType, &str)] = &[
(ActionType::ClockCheck, "\"clock_check\""),
(ActionType::Confirm, "\"confirm\""),
(ActionType::CheckValue, "\"check_value\""),
(ActionType::OralReadback, "\"oral_readback\""),
(ActionType::MachineInfo, "\"machine_info\""),
(ActionType::GenerateKeypair, "\"generate_keypair\""),
(ActionType::WrapKey, "\"wrap_key\""),
(ActionType::UnwrapKey, "\"unwrap_key\""),
(ActionType::ExportPublic, "\"export_public\""),
(ActionType::Attest, "\"attest\""),
(ActionType::TpmAttest, "\"tpm_attest\""),
(ActionType::PivReadCertificate, "\"piv_read_certificate\""),
(ActionType::PivSign, "\"piv_sign\""),
(ActionType::YubikeyAttestSlot, "\"yubikey_attest_slot\""),
(ActionType::IssueCertificate, "\"issue_certificate\""),
(ActionType::GenerateCsr, "\"generate_csr\""),
];
for &(variant, expected) in cases {
let serialized = serde_json::to_string(&variant).unwrap();
assert_eq!(serialized, expected, "serialize {variant:?}");
let deserialized: ActionType = serde_json::from_str(expected).unwrap();
assert_eq!(deserialized, variant, "deserialize {expected}");
}
}
#[test]
fn action_type_display_matches_serde() {
let actions = [
ActionType::ClockCheck,
ActionType::Confirm,
ActionType::CheckValue,
ActionType::OralReadback,
ActionType::MachineInfo,
ActionType::GenerateKeypair,
ActionType::WrapKey,
ActionType::UnwrapKey,
ActionType::ExportPublic,
ActionType::Attest,
ActionType::TpmAttest,
ActionType::PivReadCertificate,
ActionType::PivSign,
ActionType::YubikeyAttestSlot,
ActionType::IssueCertificate,
ActionType::GenerateCsr,
];
for action in actions {
let display = action.to_string();
let serde_json = serde_json::to_string(&action).unwrap();
assert_eq!(
display,
serde_json.trim_matches('"'),
"Display and serde disagree for {action:?}"
);
}
}
#[test]
fn output_type_serde_roundtrip() {
let cases: &[(OutputType, &str)] = &[
(OutputType::PublicKey, "\"public_key\""),
(OutputType::WrappedKey, "\"wrapped_key\""),
(OutputType::Certificate, "\"certificate\""),
(OutputType::Document, "\"document\""),
(OutputType::CeremonyLog, "\"ceremony_log\""),
];
for &(variant, expected) in cases {
let serialized = serde_json::to_string(&variant).unwrap();
assert_eq!(serialized, expected, "serialize {variant:?}");
let deserialized: OutputType = serde_json::from_str(expected).unwrap();
assert_eq!(deserialized, variant, "deserialize {expected}");
}
}
#[test]
fn parameter_type_serde_roundtrip() {
let cases: &[(ParameterType, &str)] = &[
(ParameterType::String, "\"string\""),
(ParameterType::Date, "\"date\""),
(ParameterType::Integer, "\"integer\""),
(ParameterType::Boolean, "\"boolean\""),
];
for (variant, expected) in cases {
let serialized = serde_json::to_string(variant).unwrap();
assert_eq!(serialized, *expected, "serialize {variant:?}");
let deserialized: ParameterType = serde_json::from_str(expected).unwrap();
assert_eq!(
serde_json::to_string(&deserialized).unwrap(),
*expected,
"deserialize {expected}"
);
}
}
#[test]
fn duty_type_serde_roundtrip() {
let cases: &[(DutyType, &str)] = &[
(DutyType::ReturnToVault, "\"return_to_vault\""),
(DutyType::DistributeShares, "\"distribute_shares\""),
(DutyType::DistributeMedia, "\"distribute_media\""),
(DutyType::ArchiveMaterials, "\"archive_materials\""),
(DutyType::PublishRecord, "\"publish_record\""),
(DutyType::NotifyStakeholders, "\"notify_stakeholders\""),
(DutyType::ImportKeys, "\"import_keys\""),
(DutyType::Custom, "\"custom\""),
];
for (variant, expected) in cases {
let serialized = serde_json::to_string(variant).unwrap();
assert_eq!(serialized, *expected, "serialize {variant:?}");
let deserialized: DutyType = serde_json::from_str(expected).unwrap();
assert_eq!(deserialized, *variant, "deserialize {expected}");
}
}
#[test]
fn duty_type_custom_has_no_built_in_prose() {
assert!(
DutyType::Custom.built_in_prose().is_none(),
"Custom duty must have no built-in prose"
);
for duty in [
DutyType::ReturnToVault,
DutyType::DistributeShares,
DutyType::DistributeMedia,
DutyType::ArchiveMaterials,
DutyType::PublishRecord,
DutyType::NotifyStakeholders,
DutyType::ImportKeys,
] {
assert!(
duty.built_in_prose().is_some(),
"{duty:?} must have built-in prose"
);
}
}
}