Skip to main content

rite_model/
types.rs

1//! Shared semantic types for the ceremony domain model.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Ceremony metadata (name and optional description).
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Metadata {
9    /// Human-readable ceremony name.
10    pub name: String,
11    /// Optional description.
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub description: Option<String>,
14}
15
16/// Extract the role type from a role ID.
17///
18/// `"witness__1"` → `"witness"`, `"operator"` → `"operator"`.
19pub fn role_type(id: &str) -> &str {
20    id.split_once("__").map_or(id, |(prefix, _)| prefix)
21}
22
23/// Derive a display name from a step or section ID.
24///
25/// Splits on `_` and `-`, title-cases each word, joins with space.
26/// `"verify_time"` → `"Verify Time"`, `"generate_root_ca"` → `"Generate Root Ca"`.
27///
28/// Note: this naive title-casing does not handle acronyms; `"root_ca"` becomes `"Root Ca"`
29/// rather than `"Root CA"`. A lookup table for common ceremony acronyms (CA, CSR, PKI, HSM,
30/// TPM) could improve this. See `derive_role_name` for the same limitation.
31pub fn derive_step_name(id: &str) -> String {
32    id.split(['_', '-'])
33        .map(|word| {
34            let mut chars = word.chars();
35            match chars.next() {
36                None => String::new(),
37                Some(c) => c.to_uppercase().to_string() + chars.as_str(),
38            }
39        })
40        .collect::<Vec<_>>()
41        .join(" ")
42}
43
44/// Derive a display name from a role ID.
45///
46/// Title-cases each word (split on `_`, `-`, and `__`), including the discriminator suffix.
47/// `"witness__1"` → `"Witness 1"`, `"hsm_operator__primary"` → `"Hsm Operator Primary"`.
48pub fn derive_role_name(id: &str) -> String {
49    id.replace("__", " ")
50        .split(['_', '-', ' '])
51        .filter(|s| !s.is_empty())
52        .map(|word| {
53            let mut chars = word.chars();
54            match chars.next() {
55                None => String::new(),
56                Some(c) => c.to_uppercase().to_string() + chars.as_str(),
57            }
58        })
59        .collect::<Vec<_>>()
60        .join(" ")
61}
62
63/// Action types available in ceremony steps.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66#[non_exhaustive]
67pub enum ActionType {
68    /// Verify system clock is correct before ceremony proceeds.
69    ///
70    /// Displays current time and requires operator confirmation.
71    /// Should be placed as the first step to ensure all timestamps are valid.
72    ClockCheck,
73    /// Human attests to something with yes/no decision (single person).
74    ///
75    /// Use when the verification requires human judgment about external state.
76    /// Example: "Verify all network cables are disconnected".
77    Confirm,
78    /// Machine compares two known values and records pass/fail (automatic).
79    ///
80    /// Use when both values are known to the system and comparison is deterministic.
81    /// Example: Compare computed `SHA-256` hash against expected value.
82    CheckValue,
83    /// Two-party verbal verification: reader speaks value aloud, confirmer verifies.
84    ///
85    /// Use when a value must be verified against something external (physical label,
86    /// document). Supports NATO phonetic alphabet and hex formatting.
87    OralReadback,
88    /// Capture machine information (hostname, CPU, OS) as evidence.
89    ///
90    /// Records device identity to prove which machine ran the ceremony.
91    /// Should be placed early in ceremony to establish machine context.
92    MachineInfo,
93
94    /// Generate a key: a keypair for an asymmetric algorithm, one secret for
95    /// a symmetric one.
96    GenerateKey,
97    /// Encrypt a private key under another key so it can leave the machine.
98    ///
99    /// The OpenSSL backend produces a CMS `AuthEnvelopedData` under
100    /// AES-256-GCM. Which encapsulation that takes follows the recipient key,
101    /// and the transcript records what the wrap actually did.
102    ///
103    /// Reads `wrapping_key:` to wrap under a key the backend holds, or
104    /// `recipient:` to wrap to a public key held outside the ceremony.
105    WrapKey,
106    /// Decrypt a wrapped key and import it into a backend under a new label.
107    UnwrapKey,
108    /// Export public key from keypair.
109    ExportPublic,
110    /// Sign arbitrary data with a backend-managed key.
111    ///
112    /// The signature algorithm follows from the key unless `algorithm:` names
113    /// another one the key accepts. Requires a backend implementing `SignBackend`.
114    SignData,
115    /// Verify a signature over data, given the signer's public key.
116    ///
117    /// Needs no backend: verification takes only a public key, so it works on
118    /// evidence the ceremony did not produce. The key may be a bare public key
119    /// or a certificate carrying one. Naming a `backend:` delegates the check
120    /// to that backend instead.
121    VerifySignature,
122    /// Formal attestation statement.
123    Attest,
124    /// Fold human-supplied entropy into the ceremony seed.
125    ///
126    /// A participant supplies a free-form random value (for example, the
127    /// result of rolling physical dice), which is mixed into the entropy
128    /// source's ratchet. Any later drawn value reflects the contribution and
129    /// stays re-derivable by `rite verify`.
130    GatherEntropy,
131    /// TPM attestation with PCR measurements and cryptographic quotes.
132    ///
133    /// Requires the `rite-tpm` backend.
134    TpmAttest,
135
136    /// Read X.509 certificate from PIV smart card slot.
137    ///
138    /// No PIN required; reading certificates is unauthenticated on PIV cards.
139    /// Requires a PIV backend.
140    PivReadCertificate,
141    /// Sign data using PIV smart card on-device key.
142    ///
143    /// Handles PIN verification internally before signing.
144    /// Requires a PIV backend.
145    PivSign,
146    /// Generate a `YubiKey` attestation certificate for a PIV slot (Yubico extension).
147    ///
148    /// Slot `F9` signs the key's certificate to prove it was generated on-device.
149    /// Requires the `rite-yubikey` backend.
150    YubikeyAttestSlot,
151
152    /// Issue an X.509 certificate from a PKCS#10 CSR.
153    ///
154    /// Takes a CSR and a backend-managed signing key, assembles the `TBSCertificate`,
155    /// signs it via the backend's `SignBackend`, and produces a DER-encoded certificate.
156    /// Works with any backend implementing `SignBackend` (software, PKCS#11, `YubiKey`).
157    IssueCertificate,
158    /// Generate a PKCS#10 CSR signed by a backend-managed key.
159    ///
160    /// Takes a backend-managed signing key and subject parameters, assembles a
161    /// `CertReqInfo`, signs it via the backend's `SignBackend`, and produces a
162    /// `DER`-encoded `CSR`.
163    GenerateCsr,
164}
165
166/// Whether an action needs the `backend:` field on its step.
167///
168/// Three states, not two: an action can also run without a backend and accept
169/// one anyway. Verification is the case that forces the distinction, since a
170/// signature check needs only a public key but a validated deployment may
171/// require it inside the boundary.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
173#[serde(rename_all = "snake_case")]
174#[non_exhaustive]
175pub enum BackendUsage {
176    /// The step must name a backend; omitting it is an error.
177    Required,
178    /// The step runs in software unless it names a backend, which then does the
179    /// work instead. Naming one changes who performs the operation, not what
180    /// the step accepts or produces.
181    SoftwareUnlessNamed,
182    /// The action never uses a backend; naming one is a mistake worth warning about.
183    Unused,
184}
185
186/// The certificate shape an `issue_certificate` step asks for.
187///
188/// DSL vocabulary rather than a backend concern: the author writes the name,
189/// and the handler turns it into X.509 extensions. `path_len` is a separate
190/// parameter, not part of a profile's identity, so this enum carries no
191/// run-specific values.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194#[non_exhaustive]
195pub enum CertProfile {
196    /// Self-signed CA at the top of a chain.
197    RootCa,
198    /// CA below another, constrained by `path_len`.
199    SubCa,
200    /// TLS server certificate.
201    TlsServer,
202    /// Code-signing certificate.
203    CodeSigning,
204    /// A leaf with no CA rights and no extended key usage.
205    EndEntity,
206}
207
208impl fmt::Display for CertProfile {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        match self {
211            CertProfile::RootCa => write!(f, "root_ca"),
212            CertProfile::SubCa => write!(f, "sub_ca"),
213            CertProfile::TlsServer => write!(f, "tls_server"),
214            CertProfile::CodeSigning => write!(f, "code_signing"),
215            CertProfile::EndEntity => write!(f, "end_entity"),
216        }
217    }
218}
219
220impl std::str::FromStr for CertProfile {
221    type Err = UnknownCertProfile;
222
223    fn from_str(s: &str) -> Result<Self, Self::Err> {
224        match s {
225            "root_ca" => Ok(Self::RootCa),
226            // `intermediate_ca` is the same shape under the name the PKI
227            // world more often uses for it.
228            "sub_ca" | "intermediate_ca" => Ok(Self::SubCa),
229            "tls_server" => Ok(Self::TlsServer),
230            "code_signing" => Ok(Self::CodeSigning),
231            "end_entity" => Ok(Self::EndEntity),
232            _ => Err(UnknownCertProfile),
233        }
234    }
235}
236
237/// A `profile:` value naming no certificate shape.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub struct UnknownCertProfile;
240
241impl fmt::Display for UnknownCertProfile {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        write!(
244            f,
245            "supported profiles: root_ca, sub_ca, tls_server, code_signing, end_entity"
246        )
247    }
248}
249
250/// What an action requires of its step's `reads:` map.
251///
252/// See [`ActionType::reads_contract`].
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254#[non_exhaustive]
255pub struct ReadsContract {
256    /// Inputs every step of this action must name.
257    pub required: &'static [&'static str],
258    /// Groups from which exactly one input must be named. Each group holds the
259    /// alternatives in the order they should be listed to the author.
260    pub exactly_one_of: &'static [&'static [&'static str]],
261    /// `with:` fields that mean nothing unless the step also names a given
262    /// input, as `(field, input)` pairs.
263    ///
264    /// Where an action offers two paths over one operation, a parameter can
265    /// belong to only one of them. On the other path it would be parsed and
266    /// never read, so the step would run without whatever the parameter asked
267    /// for. This says which parameter needs which input.
268    pub with_field_requires: &'static [(&'static str, &'static str)],
269}
270
271impl ReadsContract {
272    /// The contract of an action that constrains its inputs in neither way.
273    pub const NONE: Self = Self::required(&[]);
274
275    /// The contract of an action that requires these inputs and offers no
276    /// alternatives between them.
277    #[must_use]
278    pub const fn required(fields: &'static [&'static str]) -> Self {
279        Self {
280            required: fields,
281            exactly_one_of: &[],
282            with_field_requires: &[],
283        }
284    }
285
286    /// Whether this contract constrains the `reads:` map at all.
287    ///
288    /// `with_field_requires` is a rule over `with:`, so it is not counted here.
289    pub fn is_empty(&self) -> bool {
290        self.required.is_empty() && self.exactly_one_of.is_empty()
291    }
292}
293
294impl ActionType {
295    /// Every action type, for callers that need to enumerate them.
296    ///
297    /// Maintained by hand: `#[non_exhaustive]` means no downstream crate can
298    /// derive this list, and adding a variant produces no error outside this
299    /// crate. Adding one here is what keeps editor completion and any other
300    /// catalogue from silently missing it. The test below catches omissions.
301    pub const ALL: &'static [ActionType] = &[
302        ActionType::ClockCheck,
303        ActionType::Confirm,
304        ActionType::CheckValue,
305        ActionType::OralReadback,
306        ActionType::MachineInfo,
307        ActionType::GenerateKey,
308        ActionType::WrapKey,
309        ActionType::UnwrapKey,
310        ActionType::ExportPublic,
311        ActionType::SignData,
312        ActionType::VerifySignature,
313        ActionType::Attest,
314        ActionType::GatherEntropy,
315        ActionType::TpmAttest,
316        ActionType::PivReadCertificate,
317        ActionType::PivSign,
318        ActionType::YubikeyAttestSlot,
319        ActionType::IssueCertificate,
320        ActionType::GenerateCsr,
321    ];
322
323    /// How this action relates to the `backend:` field on its step.
324    ///
325    /// TODO: Replace this with a nested enum split — `ActionType::Backend(BackendAction)` vs
326    /// `ActionType::Local(LocalAction)`, once a home is found for the actions that are
327    /// neither. This is a breaking change to every match on `ActionType` variants.
328    pub fn backend_usage(self) -> BackendUsage {
329        match self {
330            ActionType::GenerateKey
331            | ActionType::SignData
332            | ActionType::WrapKey
333            | ActionType::UnwrapKey
334            | ActionType::ExportPublic
335            | ActionType::GenerateCsr
336            | ActionType::IssueCertificate
337            | ActionType::PivReadCertificate
338            | ActionType::PivSign
339            | ActionType::YubikeyAttestSlot
340            | ActionType::TpmAttest => BackendUsage::Required,
341
342            ActionType::VerifySignature => BackendUsage::SoftwareUnlessNamed,
343
344            ActionType::ClockCheck
345            | ActionType::Confirm
346            | ActionType::CheckValue
347            | ActionType::OralReadback
348            | ActionType::MachineInfo
349            | ActionType::Attest
350            | ActionType::GatherEntropy => BackendUsage::Unused,
351        }
352    }
353
354    /// Returns the `with:` field names that are required for this action.
355    ///
356    /// The resolver reports a diagnostic for each missing field.
357    ///
358    /// TODO: Replace the stringly-typed `with: serde_json::Value` in `schema::StepBody` with a
359    /// typed `WithFields` enum (one variant per action, carrying its required fields as named
360    /// struct members). Serde would then enforce required fields at parse time and this method
361    /// would no longer be needed. This is a breaking change to the schema and lowering layers.
362    pub fn required_with_fields(self) -> &'static [&'static str] {
363        match self {
364            ActionType::CheckValue => &["actual", "expected"],
365            ActionType::GenerateCsr => &["subject"],
366
367            ActionType::ClockCheck
368            | ActionType::Confirm
369            | ActionType::OralReadback
370            | ActionType::MachineInfo
371            | ActionType::GenerateKey
372            | ActionType::WrapKey
373            | ActionType::UnwrapKey
374            | ActionType::ExportPublic
375            | ActionType::SignData
376            | ActionType::VerifySignature
377            | ActionType::Attest
378            | ActionType::GatherEntropy
379            | ActionType::TpmAttest
380            | ActionType::PivReadCertificate
381            | ActionType::PivSign
382            | ActionType::YubikeyAttestSlot
383            | ActionType::IssueCertificate => &[],
384        }
385    }
386
387    /// Every `with:` key this action accepts.
388    ///
389    /// Nothing else may appear under `with:`. Serde ignores a key it does not
390    /// know, so without this a misspelled field, or one that used to exist, is
391    /// dropped in silence and the ceremony quietly stops doing what its author
392    /// wrote. That is the failure this list exists to prevent, and it covers
393    /// typos and removed fields alike rather than naming them one at a time.
394    ///
395    /// Required keys are the subset in
396    /// [`required_with_fields`](Self::required_with_fields); the rest are
397    /// optional. Nested keys are not listed: only the top level of the block
398    /// is checked, because a nested shape belongs to the handler's own params
399    /// type.
400    pub fn known_with_fields(self) -> &'static [&'static str] {
401        match self {
402            ActionType::ClockCheck | ActionType::Confirm => &["message"],
403            ActionType::CheckValue => &["actual", "expected", "message", "sensitive"],
404            ActionType::OralReadback => &["value", "format", "characters", "sensitive", "message"],
405            ActionType::MachineInfo => &[
406                "include_machine_id",
407                "include_cpu",
408                "include_os",
409                "include_security_features",
410                "message",
411            ],
412            ActionType::GenerateKey => &["algorithm", "policy", "slot"],
413            ActionType::WrapKey => &["scheme", "expect_recipient"],
414            ActionType::UnwrapKey => &["algorithm", "expect_key", "label", "policy"],
415            ActionType::SignData | ActionType::VerifySignature => &["algorithm", "message"],
416            ActionType::Attest => &["statement"],
417            ActionType::GatherEntropy => &["instruction"],
418            ActionType::IssueCertificate => &["profile", "validity_days", "issuer_cn", "path_len"],
419            ActionType::GenerateCsr => &["subject", "san"],
420            ActionType::PivReadCertificate | ActionType::YubikeyAttestSlot => &["slot", "message"],
421            ActionType::PivSign => &["slot", "algorithm", "message"],
422            // `export_public` takes its inputs through `reads:` alone, and
423            // `tpm_attest` has no handler in any build yet, so neither has a
424            // `with:` shape to accept. For the second, `unsupported_actions`
425            // is what reports the step itself.
426            ActionType::ExportPublic | ActionType::TpmAttest => &[],
427        }
428    }
429
430    /// Returns the `reads:` inputs this action requires, and the groups from
431    /// which exactly one input must be named.
432    ///
433    /// A group is how an action offers two paths over the same operation. The
434    /// input the author names selects the path, so naming both is ambiguous and
435    /// naming neither leaves the path undetermined. The resolver reports either
436    /// on the step.
437    pub fn reads_contract(self) -> ReadsContract {
438        match self {
439            ActionType::WrapKey => ReadsContract {
440                required: &["key_to_wrap"],
441                exactly_one_of: &[&["wrapping_key", "recipient"]],
442                // `expect_recipient` is compared against the recipient a wrap
443                // is given, and only the external path has one.
444                with_field_requires: &[("expect_recipient", "recipient")],
445            },
446            ActionType::UnwrapKey => ReadsContract::required(&["unwrapping_key", "wrapped_data"]),
447            ActionType::SignData => ReadsContract::required(&["key", "data"]),
448            ActionType::VerifySignature => ReadsContract::required(&["key", "data", "signature"]),
449            ActionType::IssueCertificate => ReadsContract::required(&["signing_key", "csr"]),
450            ActionType::GenerateCsr => ReadsContract::required(&["signing_key"]),
451
452            ActionType::ClockCheck
453            | ActionType::Confirm
454            | ActionType::CheckValue
455            | ActionType::OralReadback
456            | ActionType::MachineInfo
457            | ActionType::GenerateKey
458            | ActionType::ExportPublic
459            | ActionType::Attest
460            | ActionType::GatherEntropy
461            | ActionType::TpmAttest
462            | ActionType::PivReadCertificate
463            | ActionType::PivSign
464            | ActionType::YubikeyAttestSlot => ReadsContract::NONE,
465        }
466    }
467
468    /// Short human-readable description of what this action does.
469    ///
470    /// Used as fallback prose in script generation, TUI step display, and LSP
471    /// hover when no explicit `description` or `message` parameter is present.
472    pub fn describe(&self) -> &'static str {
473        match self {
474            ActionType::ClockCheck => "Verify system clock against a reference time.",
475            ActionType::Confirm => "Confirm readiness or completion of a manual step.",
476            ActionType::CheckValue => "Verify a value matches an expected result.",
477            ActionType::OralReadback => "Read back a value aloud for verification.",
478            ActionType::MachineInfo => "Record system and environment information.",
479            ActionType::Attest => "Record a signed attestation from a participant.",
480            ActionType::GatherEntropy => "Fold human-supplied entropy into the ceremony seed.",
481            ActionType::TpmAttest => "Record TPM platform attestation (PCR values).",
482            ActionType::GenerateKey => "Generate a cryptographic key.",
483            ActionType::ExportPublic => "Export the public component of a keypair.",
484            ActionType::SignData => "Sign data with a ceremony key.",
485            ActionType::VerifySignature => "Verify a signature against a public key.",
486            ActionType::WrapKey => "Encrypt a key under another key for transport.",
487            ActionType::UnwrapKey => "Decrypt a wrapped key and import it into a backend.",
488            ActionType::GenerateCsr => "Generate a Certificate Signing Request.",
489            ActionType::IssueCertificate => "Issue an X.509 certificate from a CSR.",
490            ActionType::PivReadCertificate => "Read a certificate from a PIV smart card slot.",
491            ActionType::PivSign => "Sign data using a PIV smart card key.",
492            ActionType::YubikeyAttestSlot => "Attest a YubiKey PIV slot key.",
493        }
494    }
495}
496
497impl std::fmt::Display for ActionType {
498    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499        match self {
500            ActionType::ClockCheck => write!(f, "clock_check"),
501            ActionType::Confirm => write!(f, "confirm"),
502            ActionType::CheckValue => write!(f, "check_value"),
503            ActionType::OralReadback => write!(f, "oral_readback"),
504            ActionType::MachineInfo => write!(f, "machine_info"),
505            ActionType::GenerateKey => write!(f, "generate_key"),
506            ActionType::WrapKey => write!(f, "wrap_key"),
507            ActionType::UnwrapKey => write!(f, "unwrap_key"),
508            ActionType::ExportPublic => write!(f, "export_public"),
509            ActionType::SignData => write!(f, "sign_data"),
510            ActionType::VerifySignature => write!(f, "verify_signature"),
511            ActionType::Attest => write!(f, "attest"),
512            ActionType::GatherEntropy => write!(f, "gather_entropy"),
513            ActionType::TpmAttest => write!(f, "tpm_attest"),
514            ActionType::PivReadCertificate => write!(f, "piv_read_certificate"),
515            ActionType::PivSign => write!(f, "piv_sign"),
516            ActionType::YubikeyAttestSlot => write!(f, "yubikey_attest_slot"),
517            ActionType::IssueCertificate => write!(f, "issue_certificate"),
518            ActionType::GenerateCsr => write!(f, "generate_csr"),
519        }
520    }
521}
522
523/// Type of output produced by a ceremony.
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
525#[serde(rename_all = "snake_case")]
526#[non_exhaustive]
527pub enum OutputType {
528    /// Public key in PEM format.
529    PublicKey,
530    /// A key wrapped for transport, in the container its scheme names.
531    WrappedKey,
532    /// X.509 certificate.
533    Certificate,
534    /// DNSSEC signed resource record set.
535    SignedRrset,
536    /// Certificate Transparency Signed Certificate Timestamp.
537    Sct,
538
539    // Documents
540    /// Generic document.
541    Document,
542    /// Ceremony log or transcript.
543    CeremonyLog,
544}
545
546impl std::fmt::Display for OutputType {
547    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548        match self {
549            OutputType::PublicKey => write!(f, "public_key"),
550            OutputType::WrappedKey => write!(f, "wrapped_key"),
551            OutputType::Certificate => write!(f, "certificate"),
552            OutputType::SignedRrset => write!(f, "signed_rrset"),
553            OutputType::Sct => write!(f, "sct"),
554            OutputType::Document => write!(f, "document"),
555            OutputType::CeremonyLog => write!(f, "ceremony_log"),
556        }
557    }
558}
559
560impl OutputType {
561    /// Returns the default file extension for this output type.
562    pub fn default_extension(&self) -> &'static str {
563        match self {
564            OutputType::PublicKey | OutputType::Certificate => "pem",
565            OutputType::WrappedKey | OutputType::SignedRrset | OutputType::Sct => "bin",
566            OutputType::Document => "txt",
567            OutputType::CeremonyLog => "json",
568        }
569    }
570}
571
572/// Type of a ceremony parameter.
573#[derive(Debug, Clone, Serialize, Deserialize)]
574#[serde(rename_all = "snake_case")]
575#[non_exhaustive]
576pub enum ParameterType {
577    /// Text value.
578    String,
579    /// Date value (`YYYY-MM-DD`).
580    Date,
581    /// Integer value.
582    Integer,
583    /// Boolean value.
584    Boolean,
585}
586
587impl std::fmt::Display for ParameterType {
588    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589        match self {
590            ParameterType::String => write!(f, "string"),
591            ParameterType::Date => write!(f, "date (YYYY-MM-DD)"),
592            ParameterType::Integer => write!(f, "integer"),
593            ParameterType::Boolean => write!(f, "boolean (true/false)"),
594        }
595    }
596}
597
598/// Typed preset for common post-ceremony duty categories.
599///
600/// Provides built-in prose for scripts when no description is given.
601/// Use `Custom` for duties that don't fit a preset; description is then required.
602///
603/// Note: physical handling steps (sealing hardware, etc.) belong as `physical_action`
604/// steps in ceremony execution, not here.
605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
606#[serde(rename_all = "snake_case")]
607#[non_exhaustive]
608pub enum DutyType {
609    /// Return materials to secure storage location.
610    ReturnToVault,
611    /// Hand key shares to assigned custodians.
612    DistributeShares,
613    /// Distribute backup media to designated recipients.
614    DistributeMedia,
615    /// Archive ceremony materials at storage locations.
616    ArchiveMaterials,
617    /// Publish ceremony record and witness attestations.
618    PublishRecord,
619    /// Notify stakeholders of ceremony completion.
620    NotifyStakeholders,
621    /// Import generated keys into operational system.
622    ImportKeys,
623    /// Free-form duty; description is required.
624    Custom,
625}
626
627impl DutyType {
628    /// Returns the built-in display name for this duty type.
629    pub fn display_name(&self) -> &'static str {
630        match self {
631            DutyType::ReturnToVault => "Return to Vault",
632            DutyType::DistributeShares => "Distribute Key Shares",
633            DutyType::DistributeMedia => "Distribute Backup Media",
634            DutyType::ArchiveMaterials => "Archive Materials",
635            DutyType::PublishRecord => "Publish Record",
636            DutyType::NotifyStakeholders => "Notify Stakeholders",
637            DutyType::ImportKeys => "Import Keys",
638            DutyType::Custom => "Custom Duty",
639        }
640    }
641
642    /// Returns the built-in prose description for this duty type.
643    ///
644    /// Returns `None` for `Custom` (description must be provided explicitly).
645    pub fn built_in_prose(&self) -> Option<&'static str> {
646        match self {
647            DutyType::ReturnToVault => Some("Return materials to secure storage"),
648            DutyType::DistributeShares => Some("Distribute key shares to assigned custodians"),
649            DutyType::DistributeMedia => Some("Distribute backup media to designated recipients"),
650            DutyType::ArchiveMaterials => {
651                Some("Archive ceremony materials at designated storage locations")
652            }
653            DutyType::PublishRecord => Some("Publish ceremony record and witness attestations"),
654            DutyType::NotifyStakeholders => Some("Notify stakeholders of ceremony completion"),
655            DutyType::ImportKeys => Some("Import generated keys into operational system"),
656            DutyType::Custom => None,
657        }
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    #[test]
666    fn role_type_extracts_prefix() {
667        assert_eq!(role_type("witness__1"), "witness");
668        assert_eq!(role_type("hsm_operator__primary"), "hsm_operator");
669        assert_eq!(role_type("operator"), "operator");
670        assert_eq!(role_type("witness__"), "witness");
671    }
672
673    #[test]
674    fn derive_step_name_title_cases() {
675        assert_eq!(derive_step_name("verify_time"), "Verify Time");
676        assert_eq!(derive_step_name("generate_root_ca"), "Generate Root Ca");
677        assert_eq!(derive_step_name("wrap_root_ca_key"), "Wrap Root Ca Key");
678        assert_eq!(derive_step_name("witness1_attest"), "Witness1 Attest");
679        assert_eq!(derive_step_name("clock-check"), "Clock Check");
680        assert_eq!(derive_step_name("opening"), "Opening");
681    }
682
683    #[test]
684    fn derive_role_name_title_cases() {
685        assert_eq!(derive_role_name("witness__1"), "Witness 1");
686        assert_eq!(
687            derive_role_name("hsm_operator__primary"),
688            "Hsm Operator Primary"
689        );
690        assert_eq!(derive_role_name("operator"), "Operator");
691        assert_eq!(derive_role_name("ceremony-admin"), "Ceremony Admin");
692    }
693
694    #[test]
695    fn action_type_serde_roundtrip() {
696        // `snake_case` rename means these exact strings appear in ceremony YAML.
697        // A variant rename or serde attr change breaks YAML parsing silently.
698        let cases: &[(ActionType, &str)] = &[
699            (ActionType::ClockCheck, "\"clock_check\""),
700            (ActionType::Confirm, "\"confirm\""),
701            (ActionType::CheckValue, "\"check_value\""),
702            (ActionType::OralReadback, "\"oral_readback\""),
703            (ActionType::MachineInfo, "\"machine_info\""),
704            (ActionType::GenerateKey, "\"generate_key\""),
705            (ActionType::WrapKey, "\"wrap_key\""),
706            (ActionType::UnwrapKey, "\"unwrap_key\""),
707            (ActionType::ExportPublic, "\"export_public\""),
708            (ActionType::Attest, "\"attest\""),
709            (ActionType::TpmAttest, "\"tpm_attest\""),
710            (ActionType::PivReadCertificate, "\"piv_read_certificate\""),
711            (ActionType::PivSign, "\"piv_sign\""),
712            (ActionType::YubikeyAttestSlot, "\"yubikey_attest_slot\""),
713            (ActionType::IssueCertificate, "\"issue_certificate\""),
714            (ActionType::GenerateCsr, "\"generate_csr\""),
715        ];
716        for &(variant, expected) in cases {
717            let serialized = serde_json::to_string(&variant).unwrap();
718            assert_eq!(serialized, expected, "serialize {variant:?}");
719            let deserialized: ActionType = serde_json::from_str(expected).unwrap();
720            assert_eq!(deserialized, variant, "deserialize {expected}");
721        }
722    }
723
724    #[test]
725    fn action_type_display_matches_serde() {
726        // Display is used in transcript output and error messages; serde in YAML parsing.
727        // They must agree or transcripts reference action names that differ from YAML.
728        let actions = [
729            ActionType::ClockCheck,
730            ActionType::Confirm,
731            ActionType::CheckValue,
732            ActionType::OralReadback,
733            ActionType::MachineInfo,
734            ActionType::GenerateKey,
735            ActionType::WrapKey,
736            ActionType::UnwrapKey,
737            ActionType::ExportPublic,
738            ActionType::Attest,
739            ActionType::TpmAttest,
740            ActionType::PivReadCertificate,
741            ActionType::PivSign,
742            ActionType::YubikeyAttestSlot,
743            ActionType::IssueCertificate,
744            ActionType::GenerateCsr,
745        ];
746        for action in actions {
747            let display = action.to_string();
748            let serde_json = serde_json::to_string(&action).unwrap();
749            assert_eq!(
750                display,
751                serde_json.trim_matches('"'),
752                "Display and serde disagree for {action:?}"
753            );
754        }
755    }
756
757    #[test]
758    fn output_type_serde_roundtrip() {
759        // These strings appear in ceremony YAML `output:` blocks and in file extension
760        // mapping. A rename breaks both YAML parsing and output file naming.
761        let cases: &[(OutputType, &str)] = &[
762            (OutputType::PublicKey, "\"public_key\""),
763            (OutputType::WrappedKey, "\"wrapped_key\""),
764            (OutputType::Certificate, "\"certificate\""),
765            (OutputType::Document, "\"document\""),
766            (OutputType::CeremonyLog, "\"ceremony_log\""),
767        ];
768        for &(variant, expected) in cases {
769            let serialized = serde_json::to_string(&variant).unwrap();
770            assert_eq!(serialized, expected, "serialize {variant:?}");
771            let deserialized: OutputType = serde_json::from_str(expected).unwrap();
772            assert_eq!(deserialized, variant, "deserialize {expected}");
773        }
774    }
775
776    #[test]
777    fn parameter_type_serde_roundtrip() {
778        // These strings appear in ceremony YAML `parameters:` blocks.
779        let cases: &[(ParameterType, &str)] = &[
780            (ParameterType::String, "\"string\""),
781            (ParameterType::Date, "\"date\""),
782            (ParameterType::Integer, "\"integer\""),
783            (ParameterType::Boolean, "\"boolean\""),
784        ];
785        for (variant, expected) in cases {
786            let serialized = serde_json::to_string(variant).unwrap();
787            assert_eq!(serialized, *expected, "serialize {variant:?}");
788            let deserialized: ParameterType = serde_json::from_str(expected).unwrap();
789            // ParameterType does not implement PartialEq; check the display instead.
790            assert_eq!(
791                serde_json::to_string(&deserialized).unwrap(),
792                *expected,
793                "deserialize {expected}"
794            );
795        }
796    }
797
798    #[test]
799    fn duty_type_serde_roundtrip() {
800        // These strings appear in ceremony YAML `after:` blocks.
801        let cases: &[(DutyType, &str)] = &[
802            (DutyType::ReturnToVault, "\"return_to_vault\""),
803            (DutyType::DistributeShares, "\"distribute_shares\""),
804            (DutyType::DistributeMedia, "\"distribute_media\""),
805            (DutyType::ArchiveMaterials, "\"archive_materials\""),
806            (DutyType::PublishRecord, "\"publish_record\""),
807            (DutyType::NotifyStakeholders, "\"notify_stakeholders\""),
808            (DutyType::ImportKeys, "\"import_keys\""),
809            (DutyType::Custom, "\"custom\""),
810        ];
811        for (variant, expected) in cases {
812            let serialized = serde_json::to_string(variant).unwrap();
813            assert_eq!(serialized, *expected, "serialize {variant:?}");
814            let deserialized: DutyType = serde_json::from_str(expected).unwrap();
815            assert_eq!(deserialized, *variant, "deserialize {expected}");
816        }
817    }
818
819    #[test]
820    fn duty_type_custom_has_no_built_in_prose() {
821        // The runtime branches on this: if built_in_prose returns None, the description
822        // field is required. A regression here would panic or produce empty script output.
823        assert!(
824            DutyType::Custom.built_in_prose().is_none(),
825            "Custom duty must have no built-in prose"
826        );
827        // All other variants must have prose.
828        for duty in [
829            DutyType::ReturnToVault,
830            DutyType::DistributeShares,
831            DutyType::DistributeMedia,
832            DutyType::ArchiveMaterials,
833            DutyType::PublishRecord,
834            DutyType::NotifyStakeholders,
835            DutyType::ImportKeys,
836        ] {
837            assert!(
838                duty.built_in_prose().is_some(),
839                "{duty:?} must have built-in prose"
840            );
841        }
842    }
843
844    /// `ActionType::ALL` is written by hand, so it can fall behind the enum.
845    ///
846    /// The match below is exhaustive, so adding a variant fails to compile
847    /// here. That is the whole mechanism: it puts a compiler error next to the
848    /// list an author has to extend. The name check then catches a variant
849    /// listed twice, and `rite-ls` compares its own catalogue against `ALL`,
850    /// which is what catches one left out.
851    /// A required field the action does not accept would be reported as
852    /// unknown, which is the opposite of the message intended, and nothing
853    /// makes the two lists agree on their own.
854    #[test]
855    fn every_required_field_is_also_a_known_field() {
856        for action in ActionType::ALL {
857            let known = action.known_with_fields();
858            for field in action.required_with_fields() {
859                assert!(
860                    known.contains(field),
861                    "{action} requires '{field}' and does not accept it"
862                );
863            }
864        }
865    }
866
867    #[test]
868    fn all_lists_every_action_type() {
869        for action in ActionType::ALL {
870            match action {
871                ActionType::ClockCheck
872                | ActionType::Confirm
873                | ActionType::CheckValue
874                | ActionType::OralReadback
875                | ActionType::MachineInfo
876                | ActionType::GenerateKey
877                | ActionType::WrapKey
878                | ActionType::UnwrapKey
879                | ActionType::ExportPublic
880                | ActionType::SignData
881                | ActionType::VerifySignature
882                | ActionType::Attest
883                | ActionType::GatherEntropy
884                | ActionType::TpmAttest
885                | ActionType::PivReadCertificate
886                | ActionType::PivSign
887                | ActionType::YubikeyAttestSlot
888                | ActionType::IssueCertificate
889                | ActionType::GenerateCsr => {}
890            }
891        }
892
893        let names: std::collections::BTreeSet<String> =
894            ActionType::ALL.iter().map(ToString::to_string).collect();
895        assert_eq!(
896            names.len(),
897            ActionType::ALL.len(),
898            "two actions share a DSL name"
899        );
900    }
901}