use serde::{Deserialize, Serialize};
use std::fmt;
#[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,
Reveal,
MachineInfo,
EnterValue,
EnterSecret,
GenerateKey,
WrapKey,
UnwrapKey,
EncryptData,
DecryptData,
SplitSecret,
CombineShares,
EnterShare,
ImportKey,
ExportPublic,
SignData,
VerifySignature,
Attest,
GatherEntropy,
TpmAttest,
PivReadCertificate,
PivSign,
YubikeyAttestSlot,
IssueCertificate,
GenerateCsr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum BackendUsage {
Required,
SoftwareUnlessNamed,
Unused,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
#[non_exhaustive]
pub enum CertProfile {
RootCa,
SubCa,
TlsServer,
CodeSigning,
EndEntity,
}
crate::types::vocabulary!(CertProfile {
RootCa => "root_ca",
SubCa => "sub_ca" | "intermediate_ca",
TlsServer => "tls_server",
CodeSigning => "code_signing",
EndEntity => "end_entity",
});
impl fmt::Display for CertProfile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for CertProfile {
type Err = UnknownCertProfile;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_name(s).ok_or(UnknownCertProfile)
}
}
impl From<CertProfile> for String {
fn from(profile: CertProfile) -> Self {
profile.as_str().to_string()
}
}
impl TryFrom<String> for CertProfile {
type Error = UnknownCertProfile;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl schemars::JsonSchema for CertProfile {
fn schema_name() -> std::borrow::Cow<'static, str> {
"CertProfile".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
Self::names_schema()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownCertProfile;
impl fmt::Display for UnknownCertProfile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let names: Vec<&str> = CertProfile::ALL.iter().map(|p| p.as_str()).collect();
write!(f, "supported profiles: {}", names.join(", "))
}
}
pub(crate) trait Vocabulary: Copy + 'static {
const VALUES: &'static [Self];
const ALIASES: &'static [(&'static str, Self)] = &[];
fn name(self) -> &'static str;
fn from_name(name: &str) -> Option<Self> {
Self::VALUES
.iter()
.map(|&value| (value.name(), value))
.chain(Self::ALIASES.iter().copied())
.find(|(candidate, _)| *candidate == name)
.map(|(_, value)| value)
}
fn names_schema() -> schemars::Schema {
crate::params::names(
Self::VALUES
.iter()
.map(|value| value.name())
.chain(Self::ALIASES.iter().map(|(alias, _)| *alias)),
)
}
}
macro_rules! vocabulary {
($ty:ident { $($variant:ident => $name:literal $(| $alias:literal)*,)+ }) => {
impl $ty {
pub const ALL: &'static [$ty] = &[$($ty::$variant),+];
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
$($ty::$variant => $name,)+
}
}
}
impl $crate::types::Vocabulary for $ty {
const VALUES: &'static [Self] = Self::ALL;
const ALIASES: &'static [(&'static str, Self)] =
&[$($(($alias, $ty::$variant),)*)+];
fn name(self) -> &'static str {
self.as_str()
}
}
};
}
pub(crate) use vocabulary;
pub(crate) fn either<'a>(names: impl Iterator<Item = &'a str>) -> String {
let names: Vec<&str> = names.collect();
match names.split_last() {
Some((last, rest)) if !rest.is_empty() => format!("{} or {last}", rest.join(", ")),
Some((last, _)) => (*last).to_string(),
None => String::new(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
#[non_exhaustive]
pub enum SharingScheme {
RiteSssV1,
}
crate::types::vocabulary!(SharingScheme {
RiteSssV1 => "rite-sss/v1",
});
impl SharingScheme {
pub fn max_shares(self) -> u8 {
match self {
SharingScheme::RiteSssV1 => 100,
}
}
#[must_use]
pub fn header_len(self) -> usize {
match self {
SharingScheme::RiteSssV1 => 3,
}
}
#[must_use]
pub fn share_len(self, secret_len: usize) -> usize {
match self {
SharingScheme::RiteSssV1 => secret_len.saturating_add(self.header_len()),
}
}
}
impl fmt::Display for SharingScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<SharingScheme> for String {
fn from(scheme: SharingScheme) -> Self {
scheme.as_str().to_string()
}
}
impl std::str::FromStr for SharingScheme {
type Err = UnknownSharingScheme;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_name(s).ok_or(UnknownSharingScheme)
}
}
impl schemars::JsonSchema for SharingScheme {
fn schema_name() -> std::borrow::Cow<'static, str> {
"SharingScheme".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
Self::names_schema()
}
}
impl TryFrom<String> for SharingScheme {
type Error = UnknownSharingScheme;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownSharingScheme;
impl fmt::Display for UnknownSharingScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let names: Vec<&str> = SharingScheme::ALL.iter().map(|s| s.as_str()).collect();
write!(f, "supported sharing schemes: {}", names.join(", "))
}
}
impl std::error::Error for UnknownSharingScheme {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ReadsContract {
pub required: &'static [&'static str],
pub exactly_one_of: &'static [&'static [&'static str]],
pub optional: &'static [&'static str],
pub with_field_requires: &'static [(&'static str, &'static str)],
pub lists: &'static [ListInput],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ListInput {
pub name: &'static str,
pub at_least: usize,
}
impl ReadsContract {
pub const NONE: Self = Self::required(&[]);
#[must_use]
pub const fn required(fields: &'static [&'static str]) -> Self {
Self {
required: fields,
exactly_one_of: &[],
optional: &[],
with_field_requires: &[],
lists: &[],
}
}
pub fn is_list(&self, key: &str) -> bool {
self.lists.iter().any(|list| list.name == key)
}
pub fn is_empty(&self) -> bool {
self.required.is_empty()
&& self.exactly_one_of.is_empty()
&& self.optional.is_empty()
&& self.lists.is_empty()
}
pub fn accepts(&self, key: &str) -> bool {
self.required.contains(&key)
|| self.optional.contains(&key)
|| self.exactly_one_of.iter().any(|group| group.contains(&key))
|| self.lists.iter().any(|list| list.name == key)
}
}
impl ActionType {
pub const ALL: &'static [ActionType] = &[
ActionType::ClockCheck,
ActionType::Confirm,
ActionType::CheckValue,
ActionType::OralReadback,
ActionType::Reveal,
ActionType::MachineInfo,
ActionType::EnterValue,
ActionType::EnterSecret,
ActionType::GenerateKey,
ActionType::WrapKey,
ActionType::UnwrapKey,
ActionType::ImportKey,
ActionType::EncryptData,
ActionType::DecryptData,
ActionType::SplitSecret,
ActionType::CombineShares,
ActionType::EnterShare,
ActionType::ExportPublic,
ActionType::SignData,
ActionType::VerifySignature,
ActionType::Attest,
ActionType::GatherEntropy,
ActionType::TpmAttest,
ActionType::PivReadCertificate,
ActionType::PivSign,
ActionType::YubikeyAttestSlot,
ActionType::IssueCertificate,
ActionType::GenerateCsr,
];
pub fn backend_usage(self) -> BackendUsage {
match self {
ActionType::GenerateKey
| ActionType::SignData
| ActionType::WrapKey
| ActionType::UnwrapKey
| ActionType::ExportPublic
| ActionType::GenerateCsr
| ActionType::IssueCertificate
| ActionType::PivReadCertificate
| ActionType::PivSign
| ActionType::YubikeyAttestSlot
| ActionType::ImportKey
| ActionType::EncryptData
| ActionType::DecryptData
| ActionType::SplitSecret
| ActionType::TpmAttest => BackendUsage::Required,
ActionType::VerifySignature => BackendUsage::SoftwareUnlessNamed,
ActionType::ClockCheck
| ActionType::Confirm
| ActionType::CheckValue
| ActionType::OralReadback
| ActionType::Reveal
| ActionType::MachineInfo
| ActionType::EnterValue
| ActionType::EnterSecret
| ActionType::Attest
| ActionType::CombineShares
| ActionType::EnterShare
| ActionType::GatherEntropy => BackendUsage::Unused,
}
}
pub fn creates_shares(self) -> bool {
match self {
ActionType::SplitSecret | ActionType::EnterShare => true,
ActionType::ClockCheck
| ActionType::Confirm
| ActionType::CheckValue
| ActionType::OralReadback
| ActionType::Reveal
| ActionType::MachineInfo
| ActionType::EnterValue
| ActionType::EnterSecret
| ActionType::GenerateKey
| ActionType::WrapKey
| ActionType::UnwrapKey
| ActionType::ImportKey
| ActionType::EncryptData
| ActionType::DecryptData
| ActionType::CombineShares
| ActionType::ExportPublic
| ActionType::SignData
| ActionType::VerifySignature
| ActionType::GenerateCsr
| ActionType::IssueCertificate
| ActionType::PivReadCertificate
| ActionType::PivSign
| ActionType::YubikeyAttestSlot
| ActionType::Attest
| ActionType::GatherEntropy
| ActionType::TpmAttest => false,
}
}
pub fn reads_contract(self) -> ReadsContract {
match self {
ActionType::WrapKey => ReadsContract {
required: &["key_to_wrap"],
exactly_one_of: &[&["wrapping_key", "recipient"]],
optional: &[],
with_field_requires: &[("expect_recipient", "recipient")],
lists: &[],
},
ActionType::UnwrapKey => ReadsContract::required(&["unwrapping_key", "wrapped_data"]),
ActionType::ImportKey => ReadsContract {
required: &["key_material"],
exactly_one_of: &[],
optional: &["passphrase"],
with_field_requires: &[],
lists: &[],
},
ActionType::EncryptData => ReadsContract::required(&["data", "encryption_key"]),
ActionType::DecryptData => {
ReadsContract::required(&["encrypted_data", "decryption_key"])
}
ActionType::SplitSecret => ReadsContract::required(&["secret"]),
ActionType::CombineShares => ReadsContract {
required: &[],
exactly_one_of: &[],
optional: &[],
with_field_requires: &[],
lists: &[ListInput {
name: "shares",
at_least: 2,
}],
},
ActionType::SignData => ReadsContract::required(&["key", "data"]),
ActionType::VerifySignature => ReadsContract::required(&["key", "data", "signature"]),
ActionType::IssueCertificate => ReadsContract {
required: &["signing_key", "csr"],
exactly_one_of: &[],
optional: &["issuer_cert"],
with_field_requires: &[],
lists: &[],
},
ActionType::GenerateCsr => ReadsContract::required(&["signing_key"]),
ActionType::Reveal => ReadsContract::required(&["value"]),
ActionType::ClockCheck
| ActionType::Confirm
| ActionType::CheckValue
| ActionType::OralReadback
| ActionType::MachineInfo
| ActionType::EnterValue
| ActionType::EnterSecret
| ActionType::GenerateKey
| ActionType::ExportPublic
| ActionType::Attest
| ActionType::GatherEntropy
| ActionType::TpmAttest
| ActionType::PivReadCertificate
| ActionType::PivSign
| ActionType::YubikeyAttestSlot
| ActionType::EnterShare => ReadsContract::NONE,
}
}
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::Reveal => "Show a value for a person to write down.",
ActionType::MachineInfo => "Record system and environment information.",
ActionType::EnterValue => "Type a value the ceremony records.",
ActionType::EnterSecret => "Type a secret the ceremony holds and never records.",
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::GenerateKey => "Generate a cryptographic key.",
ActionType::ExportPublic => "Export the public component of a keypair.",
ActionType::SignData => "Sign data with a ceremony key.",
ActionType::VerifySignature => "Verify a signature against a public key.",
ActionType::WrapKey => "Encrypt a key under another key for transport.",
ActionType::UnwrapKey => "Decrypt a wrapped key and import it into a backend.",
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.",
ActionType::ImportKey => "Import a key from material the ceremony holds.",
ActionType::EncryptData => "Encrypt content under a key held by a backend.",
ActionType::DecryptData => "Decrypt an encrypted-data artifact back to bytes.",
ActionType::SplitSecret => {
"Split a secret into shares a threshold of which reconstruct it."
}
ActionType::CombineShares => "Reconstruct a secret from its shares.",
ActionType::EnterShare => "Type a share back from its sheet.",
}
}
}
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::Reveal => write!(f, "reveal"),
ActionType::MachineInfo => write!(f, "machine_info"),
ActionType::EnterValue => write!(f, "enter_value"),
ActionType::EnterSecret => write!(f, "enter_secret"),
ActionType::GenerateKey => write!(f, "generate_key"),
ActionType::WrapKey => write!(f, "wrap_key"),
ActionType::UnwrapKey => write!(f, "unwrap_key"),
ActionType::ImportKey => write!(f, "import_key"),
ActionType::EncryptData => write!(f, "encrypt_data"),
ActionType::DecryptData => write!(f, "decrypt_data"),
ActionType::SplitSecret => write!(f, "split_secret"),
ActionType::CombineShares => write!(f, "combine_shares"),
ActionType::EnterShare => write!(f, "enter_share"),
ActionType::ExportPublic => write!(f, "export_public"),
ActionType::SignData => write!(f, "sign_data"),
ActionType::VerifySignature => write!(f, "verify_signature"),
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,
EncryptedData,
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::EncryptedData => write!(f, "encrypted_data"),
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::EncryptedData
| 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::Reveal, "\"reveal\""),
(ActionType::MachineInfo, "\"machine_info\""),
(ActionType::GenerateKey, "\"generate_key\""),
(ActionType::WrapKey, "\"wrap_key\""),
(ActionType::UnwrapKey, "\"unwrap_key\""),
(ActionType::ImportKey, "\"import_key\""),
(ActionType::EncryptData, "\"encrypt_data\""),
(ActionType::DecryptData, "\"decrypt_data\""),
(ActionType::SplitSecret, "\"split_secret\""),
(ActionType::CombineShares, "\"combine_shares\""),
(ActionType::EnterShare, "\"enter_share\""),
(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::Reveal,
ActionType::MachineInfo,
ActionType::GenerateKey,
ActionType::WrapKey,
ActionType::UnwrapKey,
ActionType::ImportKey,
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::EncryptedData, "\"encrypted_data\""),
(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"
);
}
}
#[test]
fn every_vocabulary_lists_every_value_under_its_serde_name() {
use crate::{Format, RevealFormat};
fn check<T>(all: &[T])
where
T: Copy + PartialEq + std::fmt::Debug + std::fmt::Display + Serialize,
T: std::str::FromStr,
{
for &value in all {
let name = value.to_string();
assert!(
matches!(name.parse::<T>(), Ok(parsed) if parsed == value),
"{name}"
);
assert_eq!(serde_json::to_value(value).unwrap(), name.as_str());
}
}
check(CertProfile::ALL);
check(SharingScheme::ALL);
check(RevealFormat::ALL);
check(Format::ALL);
}
#[test]
fn a_vocabulary_schema_accepts_what_serde_reads() {
use crate::params::ReadbackFormat;
fn check<T>()
where
T: Vocabulary + std::fmt::Debug + PartialEq + schemars::JsonSchema,
T: serde::de::DeserializeOwned,
{
let schema = schemars::schema_for!(T);
let listed: Vec<&str> = schema
.get("enum")
.and_then(|names| names.as_array())
.unwrap()
.iter()
.filter_map(|name| name.as_str())
.collect();
let named = T::VALUES.iter().map(|&value| (value.name(), value));
let mut expected = Vec::new();
for (name, value) in named.chain(T::ALIASES.iter().copied()) {
assert_eq!(
serde_json::from_value::<T>(serde_json::json!(name)).unwrap(),
value
);
expected.push(name);
}
assert_eq!(listed, expected);
}
check::<CertProfile>();
check::<SharingScheme>();
check::<ReadbackFormat>();
assert_eq!("intermediate_ca".parse(), Ok(CertProfile::SubCa));
assert_eq!(
serde_json::from_value::<ReadbackFormat>(serde_json::json!("nato")).unwrap(),
ReadbackFormat::NatoPhonetic
);
}
#[test]
fn all_lists_every_action_type() {
for action in ActionType::ALL {
match action {
ActionType::ClockCheck
| ActionType::Confirm
| ActionType::CheckValue
| ActionType::OralReadback
| ActionType::Reveal
| ActionType::MachineInfo
| ActionType::EnterValue
| ActionType::EnterSecret
| ActionType::GenerateKey
| ActionType::WrapKey
| ActionType::UnwrapKey
| ActionType::ImportKey
| ActionType::EncryptData
| ActionType::DecryptData
| ActionType::SplitSecret
| ActionType::CombineShares
| ActionType::EnterShare
| ActionType::ExportPublic
| ActionType::SignData
| ActionType::VerifySignature
| ActionType::Attest
| ActionType::GatherEntropy
| ActionType::TpmAttest
| ActionType::PivReadCertificate
| ActionType::PivSign
| ActionType::YubikeyAttestSlot
| ActionType::IssueCertificate
| ActionType::GenerateCsr => {}
}
}
let names: std::collections::BTreeSet<String> =
ActionType::ALL.iter().map(ToString::to_string).collect();
assert_eq!(
names.len(),
ActionType::ALL.len(),
"two actions share a DSL name"
);
}
}