Skip to main content

rite_model/
types.rs

1//! Shared semantic types for the ceremony domain model.
2
3use serde::{Deserialize, Serialize};
4
5/// Ceremony metadata (name and optional description).
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Metadata {
8    /// Human-readable ceremony name.
9    pub name: String,
10    /// Optional description.
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub description: Option<String>,
13}
14
15/// Extract the role type from a role ID.
16///
17/// `"witness__1"` → `"witness"`, `"operator"` → `"operator"`.
18pub fn role_type(id: &str) -> &str {
19    id.split_once("__").map_or(id, |(prefix, _)| prefix)
20}
21
22/// Derive a display name from a step or section ID.
23///
24/// Splits on `_` and `-`, title-cases each word, joins with space.
25/// `"verify_time"` → `"Verify Time"`, `"generate_root_ca"` → `"Generate Root Ca"`.
26///
27/// Note: this naive title-casing does not handle acronyms; `"root_ca"` becomes `"Root Ca"`
28/// rather than `"Root CA"`. A lookup table for common ceremony acronyms (CA, CSR, PKI, HSM,
29/// TPM) could improve this. See `derive_role_name` for the same limitation.
30pub 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
43/// Derive a display name from a role ID.
44///
45/// Title-cases each word (split on `_`, `-`, and `__`), including the discriminator suffix.
46/// `"witness__1"` → `"Witness 1"`, `"hsm_operator__primary"` → `"Hsm Operator Primary"`.
47pub 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/// Action types available in ceremony steps.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65#[non_exhaustive]
66pub enum ActionType {
67    /// Verify system clock is correct before ceremony proceeds.
68    ///
69    /// Displays current time and requires operator confirmation.
70    /// Should be placed as the first step to ensure all timestamps are valid.
71    ClockCheck,
72    /// Human attests to something with yes/no decision (single person).
73    ///
74    /// Use when the verification requires human judgment about external state.
75    /// Example: "Verify all network cables are disconnected".
76    Confirm,
77    /// Machine compares two known values and records pass/fail (automatic).
78    ///
79    /// Use when both values are known to the system and comparison is deterministic.
80    /// Example: Compare computed `SHA-256` hash against expected value.
81    CheckValue,
82    /// Two-party verbal verification: reader speaks value aloud, confirmer verifies.
83    ///
84    /// Use when a value must be verified against something external (physical label,
85    /// document). Supports NATO phonetic alphabet and hex formatting.
86    OralReadback,
87    /// Capture machine information (hostname, CPU, OS) as evidence.
88    ///
89    /// Records device identity to prove which machine ran the ceremony.
90    /// Should be placed early in ceremony to establish machine context.
91    MachineInfo,
92
93    /// Generate RSA or EC keypair.
94    GenerateKeypair,
95    /// Wrap key using CMS `EnvelopedData`.
96    WrapKey,
97    /// Unwrap key using CMS `EnvelopedData`.
98    UnwrapKey,
99    /// Export public key from keypair.
100    ExportPublic,
101    /// Formal attestation statement.
102    Attest,
103    /// Fold human-supplied entropy into the ceremony seed.
104    ///
105    /// A participant supplies a free-form random value (for example, the
106    /// result of rolling physical dice), which is mixed into the entropy
107    /// source's ratchet. Any later drawn value reflects the contribution and
108    /// stays re-derivable by `rite verify`.
109    GatherEntropy,
110    /// TPM attestation with PCR measurements and cryptographic quotes.
111    ///
112    /// Requires the `rite-tpm` backend.
113    TpmAttest,
114
115    /// Read X.509 certificate from PIV smart card slot.
116    ///
117    /// No PIN required; reading certificates is unauthenticated on PIV cards.
118    /// Requires a PIV backend.
119    PivReadCertificate,
120    /// Sign data using PIV smart card on-device key.
121    ///
122    /// Handles PIN verification internally before signing.
123    /// Requires a PIV backend.
124    PivSign,
125    /// Generate a `YubiKey` attestation certificate for a PIV slot (Yubico extension).
126    ///
127    /// Slot `F9` signs the key's certificate to prove it was generated on-device.
128    /// Requires the `rite-yubikey` backend.
129    YubikeyAttestSlot,
130
131    /// Issue an X.509 certificate from a PKCS#10 CSR.
132    ///
133    /// Takes a CSR and a backend-managed signing key, assembles the `TBSCertificate`,
134    /// signs it via the backend's `SignBackend`, and produces a DER-encoded certificate.
135    /// Works with any backend implementing `SignBackend` (software, PKCS#11, `YubiKey`).
136    IssueCertificate,
137    /// Generate a PKCS#10 CSR signed by a backend-managed key.
138    ///
139    /// Takes a backend-managed signing key and subject parameters, assembles a
140    /// `CertReqInfo`, signs it via the backend's `SignBackend`, and produces a
141    /// `DER`-encoded `CSR`.
142    GenerateCsr,
143}
144
145impl ActionType {
146    /// Returns `true` if this action requires a `backend:` field in the step.
147    ///
148    /// TODO: Replace this with a nested enum split — `ActionType::Backend(BackendAction)` vs
149    /// `ActionType::Local(LocalAction)`. The `requires_backend` check then becomes
150    /// `matches!(self, ActionType::Backend(_))` with no hardcoded list, and each group gains
151    /// its own `impl` block. This is a breaking change to every match on `ActionType` variants.
152    pub fn requires_backend(self) -> bool {
153        matches!(
154            self,
155            ActionType::GenerateKeypair
156                | ActionType::WrapKey
157                | ActionType::UnwrapKey
158                | ActionType::ExportPublic
159                | ActionType::GenerateCsr
160                | ActionType::IssueCertificate
161                | ActionType::PivReadCertificate
162                | ActionType::PivSign
163                | ActionType::YubikeyAttestSlot
164                | ActionType::TpmAttest
165        )
166    }
167
168    /// Returns the `with:` field names that are required for this action.
169    ///
170    /// The resolver reports a diagnostic for each missing field.
171    ///
172    /// TODO: Replace the stringly-typed `with: serde_json::Value` in `schema::StepBody` with a
173    /// typed `WithFields` enum (one variant per action, carrying its required fields as named
174    /// struct members). Serde would then enforce required fields at parse time and this method
175    /// would no longer be needed. This is a breaking change to the schema and lowering layers.
176    pub fn required_with_fields(self) -> &'static [&'static str] {
177        match self {
178            ActionType::CheckValue => &["actual", "expected"],
179            ActionType::GenerateCsr => &["subject"],
180            _ => &[],
181        }
182    }
183
184    /// Short human-readable description of what this action does.
185    ///
186    /// Used as fallback prose in script generation, TUI step display, and LSP
187    /// hover when no explicit `description` or `message` parameter is present.
188    pub fn describe(&self) -> &'static str {
189        match self {
190            ActionType::ClockCheck => "Verify system clock against a reference time.",
191            ActionType::Confirm => "Confirm readiness or completion of a manual step.",
192            ActionType::CheckValue => "Verify a value matches an expected result.",
193            ActionType::OralReadback => "Read back a value aloud for verification.",
194            ActionType::MachineInfo => "Record system and environment information.",
195            ActionType::Attest => "Record a signed attestation from a participant.",
196            ActionType::GatherEntropy => "Fold human-supplied entropy into the ceremony seed.",
197            ActionType::TpmAttest => "Record TPM platform attestation (PCR values).",
198            ActionType::GenerateKeypair => "Generate an asymmetric keypair.",
199            ActionType::ExportPublic => "Export the public component of a keypair.",
200            ActionType::WrapKey => "Wrap (encrypt) a key for secure transport.",
201            ActionType::UnwrapKey => "Unwrap (decrypt) a transported key.",
202            ActionType::GenerateCsr => "Generate a Certificate Signing Request.",
203            ActionType::IssueCertificate => "Issue an X.509 certificate from a CSR.",
204            ActionType::PivReadCertificate => "Read a certificate from a PIV smart card slot.",
205            ActionType::PivSign => "Sign data using a PIV smart card key.",
206            ActionType::YubikeyAttestSlot => "Attest a YubiKey PIV slot key.",
207        }
208    }
209}
210
211impl std::fmt::Display for ActionType {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        match self {
214            ActionType::ClockCheck => write!(f, "clock_check"),
215            ActionType::Confirm => write!(f, "confirm"),
216            ActionType::CheckValue => write!(f, "check_value"),
217            ActionType::OralReadback => write!(f, "oral_readback"),
218            ActionType::MachineInfo => write!(f, "machine_info"),
219            ActionType::GenerateKeypair => write!(f, "generate_keypair"),
220            ActionType::WrapKey => write!(f, "wrap_key"),
221            ActionType::UnwrapKey => write!(f, "unwrap_key"),
222            ActionType::ExportPublic => write!(f, "export_public"),
223            ActionType::Attest => write!(f, "attest"),
224            ActionType::GatherEntropy => write!(f, "gather_entropy"),
225            ActionType::TpmAttest => write!(f, "tpm_attest"),
226            ActionType::PivReadCertificate => write!(f, "piv_read_certificate"),
227            ActionType::PivSign => write!(f, "piv_sign"),
228            ActionType::YubikeyAttestSlot => write!(f, "yubikey_attest_slot"),
229            ActionType::IssueCertificate => write!(f, "issue_certificate"),
230            ActionType::GenerateCsr => write!(f, "generate_csr"),
231        }
232    }
233}
234
235/// Type of output produced by a ceremony.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
237#[serde(rename_all = "snake_case")]
238#[non_exhaustive]
239pub enum OutputType {
240    /// Public key in PEM format.
241    PublicKey,
242    /// Wrapped (encrypted) key in CMS format.
243    WrappedKey,
244    /// X.509 certificate.
245    Certificate,
246    /// DNSSEC signed resource record set.
247    SignedRrset,
248    /// Certificate Transparency Signed Certificate Timestamp.
249    Sct,
250
251    // Documents
252    /// Generic document.
253    Document,
254    /// Ceremony log or transcript.
255    CeremonyLog,
256}
257
258impl std::fmt::Display for OutputType {
259    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260        match self {
261            OutputType::PublicKey => write!(f, "public_key"),
262            OutputType::WrappedKey => write!(f, "wrapped_key"),
263            OutputType::Certificate => write!(f, "certificate"),
264            OutputType::SignedRrset => write!(f, "signed_rrset"),
265            OutputType::Sct => write!(f, "sct"),
266            OutputType::Document => write!(f, "document"),
267            OutputType::CeremonyLog => write!(f, "ceremony_log"),
268        }
269    }
270}
271
272impl OutputType {
273    /// Returns the default file extension for this output type.
274    pub fn default_extension(&self) -> &'static str {
275        match self {
276            OutputType::PublicKey | OutputType::Certificate => "pem",
277            OutputType::WrappedKey | OutputType::SignedRrset | OutputType::Sct => "bin",
278            OutputType::Document => "txt",
279            OutputType::CeremonyLog => "json",
280        }
281    }
282}
283
284/// Type of a ceremony parameter.
285#[derive(Debug, Clone, Serialize, Deserialize)]
286#[serde(rename_all = "snake_case")]
287#[non_exhaustive]
288pub enum ParameterType {
289    /// Text value.
290    String,
291    /// Date value (`YYYY-MM-DD`).
292    Date,
293    /// Integer value.
294    Integer,
295    /// Boolean value.
296    Boolean,
297}
298
299impl std::fmt::Display for ParameterType {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        match self {
302            ParameterType::String => write!(f, "string"),
303            ParameterType::Date => write!(f, "date (YYYY-MM-DD)"),
304            ParameterType::Integer => write!(f, "integer"),
305            ParameterType::Boolean => write!(f, "boolean (true/false)"),
306        }
307    }
308}
309
310/// Typed preset for common post-ceremony duty categories.
311///
312/// Provides built-in prose for scripts when no description is given.
313/// Use `Custom` for duties that don't fit a preset; description is then required.
314///
315/// Note: physical handling steps (sealing hardware, etc.) belong as `physical_action`
316/// steps in ceremony execution, not here.
317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
318#[serde(rename_all = "snake_case")]
319#[non_exhaustive]
320pub enum DutyType {
321    /// Return materials to secure storage location.
322    ReturnToVault,
323    /// Hand key shares to assigned custodians.
324    DistributeShares,
325    /// Distribute backup media to designated recipients.
326    DistributeMedia,
327    /// Archive ceremony materials at storage locations.
328    ArchiveMaterials,
329    /// Publish ceremony record and witness attestations.
330    PublishRecord,
331    /// Notify stakeholders of ceremony completion.
332    NotifyStakeholders,
333    /// Import generated keys into operational system.
334    ImportKeys,
335    /// Free-form duty; description is required.
336    Custom,
337}
338
339impl DutyType {
340    /// Returns the built-in display name for this duty type.
341    pub fn display_name(&self) -> &'static str {
342        match self {
343            DutyType::ReturnToVault => "Return to Vault",
344            DutyType::DistributeShares => "Distribute Key Shares",
345            DutyType::DistributeMedia => "Distribute Backup Media",
346            DutyType::ArchiveMaterials => "Archive Materials",
347            DutyType::PublishRecord => "Publish Record",
348            DutyType::NotifyStakeholders => "Notify Stakeholders",
349            DutyType::ImportKeys => "Import Keys",
350            DutyType::Custom => "Custom Duty",
351        }
352    }
353
354    /// Returns the built-in prose description for this duty type.
355    ///
356    /// Returns `None` for `Custom` (description must be provided explicitly).
357    pub fn built_in_prose(&self) -> Option<&'static str> {
358        match self {
359            DutyType::ReturnToVault => Some("Return materials to secure storage"),
360            DutyType::DistributeShares => Some("Distribute key shares to assigned custodians"),
361            DutyType::DistributeMedia => Some("Distribute backup media to designated recipients"),
362            DutyType::ArchiveMaterials => {
363                Some("Archive ceremony materials at designated storage locations")
364            }
365            DutyType::PublishRecord => Some("Publish ceremony record and witness attestations"),
366            DutyType::NotifyStakeholders => Some("Notify stakeholders of ceremony completion"),
367            DutyType::ImportKeys => Some("Import generated keys into operational system"),
368            DutyType::Custom => None,
369        }
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn role_type_extracts_prefix() {
379        assert_eq!(role_type("witness__1"), "witness");
380        assert_eq!(role_type("hsm_operator__primary"), "hsm_operator");
381        assert_eq!(role_type("operator"), "operator");
382        assert_eq!(role_type("witness__"), "witness");
383    }
384
385    #[test]
386    fn derive_step_name_title_cases() {
387        assert_eq!(derive_step_name("verify_time"), "Verify Time");
388        assert_eq!(derive_step_name("generate_root_ca"), "Generate Root Ca");
389        assert_eq!(derive_step_name("wrap_root_ca_key"), "Wrap Root Ca Key");
390        assert_eq!(derive_step_name("witness1_attest"), "Witness1 Attest");
391        assert_eq!(derive_step_name("clock-check"), "Clock Check");
392        assert_eq!(derive_step_name("opening"), "Opening");
393    }
394
395    #[test]
396    fn derive_role_name_title_cases() {
397        assert_eq!(derive_role_name("witness__1"), "Witness 1");
398        assert_eq!(
399            derive_role_name("hsm_operator__primary"),
400            "Hsm Operator Primary"
401        );
402        assert_eq!(derive_role_name("operator"), "Operator");
403        assert_eq!(derive_role_name("ceremony-admin"), "Ceremony Admin");
404    }
405
406    #[test]
407    fn action_type_serde_roundtrip() {
408        // `snake_case` rename means these exact strings appear in ceremony YAML.
409        // A variant rename or serde attr change breaks YAML parsing silently.
410        let cases: &[(ActionType, &str)] = &[
411            (ActionType::ClockCheck, "\"clock_check\""),
412            (ActionType::Confirm, "\"confirm\""),
413            (ActionType::CheckValue, "\"check_value\""),
414            (ActionType::OralReadback, "\"oral_readback\""),
415            (ActionType::MachineInfo, "\"machine_info\""),
416            (ActionType::GenerateKeypair, "\"generate_keypair\""),
417            (ActionType::WrapKey, "\"wrap_key\""),
418            (ActionType::UnwrapKey, "\"unwrap_key\""),
419            (ActionType::ExportPublic, "\"export_public\""),
420            (ActionType::Attest, "\"attest\""),
421            (ActionType::TpmAttest, "\"tpm_attest\""),
422            (ActionType::PivReadCertificate, "\"piv_read_certificate\""),
423            (ActionType::PivSign, "\"piv_sign\""),
424            (ActionType::YubikeyAttestSlot, "\"yubikey_attest_slot\""),
425            (ActionType::IssueCertificate, "\"issue_certificate\""),
426            (ActionType::GenerateCsr, "\"generate_csr\""),
427        ];
428        for &(variant, expected) in cases {
429            let serialized = serde_json::to_string(&variant).unwrap();
430            assert_eq!(serialized, expected, "serialize {variant:?}");
431            let deserialized: ActionType = serde_json::from_str(expected).unwrap();
432            assert_eq!(deserialized, variant, "deserialize {expected}");
433        }
434    }
435
436    #[test]
437    fn action_type_display_matches_serde() {
438        // Display is used in transcript output and error messages; serde in YAML parsing.
439        // They must agree or transcripts reference action names that differ from YAML.
440        let actions = [
441            ActionType::ClockCheck,
442            ActionType::Confirm,
443            ActionType::CheckValue,
444            ActionType::OralReadback,
445            ActionType::MachineInfo,
446            ActionType::GenerateKeypair,
447            ActionType::WrapKey,
448            ActionType::UnwrapKey,
449            ActionType::ExportPublic,
450            ActionType::Attest,
451            ActionType::TpmAttest,
452            ActionType::PivReadCertificate,
453            ActionType::PivSign,
454            ActionType::YubikeyAttestSlot,
455            ActionType::IssueCertificate,
456            ActionType::GenerateCsr,
457        ];
458        for action in actions {
459            let display = action.to_string();
460            let serde_json = serde_json::to_string(&action).unwrap();
461            assert_eq!(
462                display,
463                serde_json.trim_matches('"'),
464                "Display and serde disagree for {action:?}"
465            );
466        }
467    }
468
469    #[test]
470    fn output_type_serde_roundtrip() {
471        // These strings appear in ceremony YAML `output:` blocks and in file extension
472        // mapping. A rename breaks both YAML parsing and output file naming.
473        let cases: &[(OutputType, &str)] = &[
474            (OutputType::PublicKey, "\"public_key\""),
475            (OutputType::WrappedKey, "\"wrapped_key\""),
476            (OutputType::Certificate, "\"certificate\""),
477            (OutputType::Document, "\"document\""),
478            (OutputType::CeremonyLog, "\"ceremony_log\""),
479        ];
480        for &(variant, expected) in cases {
481            let serialized = serde_json::to_string(&variant).unwrap();
482            assert_eq!(serialized, expected, "serialize {variant:?}");
483            let deserialized: OutputType = serde_json::from_str(expected).unwrap();
484            assert_eq!(deserialized, variant, "deserialize {expected}");
485        }
486    }
487
488    #[test]
489    fn parameter_type_serde_roundtrip() {
490        // These strings appear in ceremony YAML `parameters:` blocks.
491        let cases: &[(ParameterType, &str)] = &[
492            (ParameterType::String, "\"string\""),
493            (ParameterType::Date, "\"date\""),
494            (ParameterType::Integer, "\"integer\""),
495            (ParameterType::Boolean, "\"boolean\""),
496        ];
497        for (variant, expected) in cases {
498            let serialized = serde_json::to_string(variant).unwrap();
499            assert_eq!(serialized, *expected, "serialize {variant:?}");
500            let deserialized: ParameterType = serde_json::from_str(expected).unwrap();
501            // ParameterType does not implement PartialEq; check the display instead.
502            assert_eq!(
503                serde_json::to_string(&deserialized).unwrap(),
504                *expected,
505                "deserialize {expected}"
506            );
507        }
508    }
509
510    #[test]
511    fn duty_type_serde_roundtrip() {
512        // These strings appear in ceremony YAML `after:` blocks.
513        let cases: &[(DutyType, &str)] = &[
514            (DutyType::ReturnToVault, "\"return_to_vault\""),
515            (DutyType::DistributeShares, "\"distribute_shares\""),
516            (DutyType::DistributeMedia, "\"distribute_media\""),
517            (DutyType::ArchiveMaterials, "\"archive_materials\""),
518            (DutyType::PublishRecord, "\"publish_record\""),
519            (DutyType::NotifyStakeholders, "\"notify_stakeholders\""),
520            (DutyType::ImportKeys, "\"import_keys\""),
521            (DutyType::Custom, "\"custom\""),
522        ];
523        for (variant, expected) in cases {
524            let serialized = serde_json::to_string(variant).unwrap();
525            assert_eq!(serialized, *expected, "serialize {variant:?}");
526            let deserialized: DutyType = serde_json::from_str(expected).unwrap();
527            assert_eq!(deserialized, *variant, "deserialize {expected}");
528        }
529    }
530
531    #[test]
532    fn duty_type_custom_has_no_built_in_prose() {
533        // The runtime branches on this: if built_in_prose returns None, the description
534        // field is required. A regression here would panic or produce empty script output.
535        assert!(
536            DutyType::Custom.built_in_prose().is_none(),
537            "Custom duty must have no built-in prose"
538        );
539        // All other variants must have prose.
540        for duty in [
541            DutyType::ReturnToVault,
542            DutyType::DistributeShares,
543            DutyType::DistributeMedia,
544            DutyType::ArchiveMaterials,
545            DutyType::PublishRecord,
546            DutyType::NotifyStakeholders,
547            DutyType::ImportKeys,
548        ] {
549            assert!(
550                duty.built_in_prose().is_some(),
551                "{duty:?} must have built-in prose"
552            );
553        }
554    }
555}