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    /// Show a value on screen for a person to write down, then withdraw it.
89    ///
90    /// Reads `value:`, a share or any byte artifact, and shows it in a
91    /// chosen encoding while a prompt is up; on acknowledgement it is gone.
92    /// The transcript records that the value was shown and nothing of it.
93    /// `rite script` prints a page for the step, one box per character
94    /// in rows, the parity cells set apart.
95    Reveal,
96    /// Capture machine information (hostname, CPU, OS) as evidence.
97    ///
98    /// Records device identity to prove which machine ran the ceremony.
99    /// Should be placed early in ceremony to establish machine context.
100    MachineInfo,
101    /// A person types a value the ceremony needs and can record: a serial
102    /// number read off a device, an address shown on a screen.
103    ///
104    /// The value becomes a text artifact, so a later step compares it or
105    /// prints it, and the transcript carries it. `format:` and the length
106    /// fields say what shape the value has, so a slip is refused at the
107    /// keyboard rather than found at the end.
108    EnterValue,
109    /// A person types a secret the ceremony needs and must not record: a
110    /// passphrase, a PIN.
111    ///
112    /// `enter_value` for a value the transcript may not carry. Echo is off,
113    /// the artifact is held wiped in memory, and the transcript records that
114    /// a secret was entered at this step and nothing derived from it. A step
115    /// names it in `reads:`, which borrows the value. An expression in
116    /// `with:` copies it into the step's parameters, as it would opened
117    /// content.
118    EnterSecret,
119
120    /// Generate a key: a keypair for an asymmetric algorithm, one secret for
121    /// a symmetric one.
122    GenerateKey,
123    /// Encrypt a private key under another key so it can leave the machine.
124    ///
125    /// The OpenSSL backend produces a CMS `AuthEnvelopedData` under
126    /// AES-256-GCM. Which encapsulation that takes follows the recipient key,
127    /// and the transcript records what the wrap actually did.
128    ///
129    /// Reads `wrapping_key:` to wrap under a key the backend holds, or
130    /// `recipient:` to wrap to a public key held outside the ceremony.
131    WrapKey,
132    /// Decrypt a wrapped key and import it into a backend under a new label.
133    UnwrapKey,
134    /// Encrypt content for a recipient, producing an encrypted-data artifact.
135    ///
136    /// `wrap_key` for bytes that are not a key. The container is the same one,
137    /// and the claim is not: a wrap says a key left a backend under protection,
138    /// while encrypted content makes no custody claim at all.
139    ///
140    /// Reads `encryption_key:`, a key-encryption key the backend holds. The key
141    /// protects a fresh content-encryption key, and the content is encrypted
142    /// under that.
143    EncryptData,
144    /// Decrypt an encrypted-data artifact back to bytes.
145    ///
146    /// The bytes become an ordinary artifact, so a later step reads them as a
147    /// material: `import_key` lifts them into a key, `check_value` compares
148    /// them, `sign_data` signs them.
149    DecryptData,
150    /// Split a secret into shares of which a threshold reconstruct it.
151    ///
152    /// Shamir over GF(2^8), the `rite-sss/v1` format, with the polynomial
153    /// coefficients drawn from the step's backend. Reads `secret:` and
154    /// creates one artifact holding every share, reached as
155    /// `${artifact.<name>.share_N}` under a later step's `reads:`. At most
156    /// 100 shares. Every subset of `threshold` shares is combined and
157    /// checked before the step completes, so a share that leaves the room
158    /// has been shown to work; a split with more than 100,000 such subsets
159    /// is refused at run time.
160    SplitSecret,
161    /// Reconstruct a secret from shares `split_secret` made.
162    ///
163    /// Reads `shares:`, a list of at least two, each a share of a set or a
164    /// share a custodian typed back. Each share records how many are
165    /// needed, so too few shares is an error before anything is computed.
166    /// The result stays in memory and is erased when the run ends, like the
167    /// content `decrypt_data` opens.
168    CombineShares,
169    /// Type a share back from its sheet, row by row.
170    ///
171    /// Each row is checked as it is typed: a wrong character is corrected
172    /// and named, two unreadable ones are recovered, and a row that needs
173    /// more is typed again. Rows that are not a share are refused before
174    /// the step ends. Creates a share `combine_shares` reads, named without
175    /// a `share_N` property. The transcript records the share's index and
176    /// which rows were repaired, never the rows.
177    EnterShare,
178    /// Install key material the ceremony holds as a key of a named algorithm.
179    ///
180    /// `unwrap_key` without the decrypt. The bytes can be a material carried
181    /// into the room or an artifact an earlier step produced, and `algorithm:`
182    /// says how to read them, because raw material says nothing about itself.
183    ImportKey,
184    /// Export public key from keypair.
185    ExportPublic,
186    /// Sign arbitrary data with a backend-managed key.
187    ///
188    /// The signature algorithm follows from the key unless `algorithm:` names
189    /// another one the key accepts. Requires a backend implementing `SignBackend`.
190    SignData,
191    /// Verify a signature over data, given the signer's public key.
192    ///
193    /// Needs no backend: verification takes only a public key, so it works on
194    /// evidence the ceremony did not produce. The key may be a bare public key
195    /// or a certificate carrying one. Naming a `backend:` delegates the check
196    /// to that backend instead.
197    VerifySignature,
198    /// Formal attestation statement.
199    Attest,
200    /// Fold human-supplied entropy into the ceremony seed.
201    ///
202    /// A participant supplies a free-form random value (for example, the
203    /// result of rolling physical dice), which is mixed into the entropy
204    /// source's ratchet. Any later drawn value reflects the contribution and
205    /// stays re-derivable by `rite verify`.
206    GatherEntropy,
207    /// TPM attestation with PCR measurements and cryptographic quotes.
208    ///
209    /// Requires the `rite-tpm` backend.
210    TpmAttest,
211
212    /// Read X.509 certificate from PIV smart card slot.
213    ///
214    /// No PIN required; reading certificates is unauthenticated on PIV cards.
215    /// Requires a PIV backend.
216    PivReadCertificate,
217    /// Sign data using PIV smart card on-device key.
218    ///
219    /// Handles PIN verification internally before signing.
220    /// Requires a PIV backend.
221    PivSign,
222    /// Generate a `YubiKey` attestation certificate for a PIV slot (Yubico extension).
223    ///
224    /// Slot `F9` signs the key's certificate to prove it was generated on-device.
225    /// Requires the `rite-yubikey` backend.
226    YubikeyAttestSlot,
227
228    /// Issue an X.509 certificate from a PKCS#10 CSR.
229    ///
230    /// Takes a CSR and a backend-managed signing key, assembles the `TBSCertificate`,
231    /// signs it via the backend's `SignBackend`, and produces a DER-encoded certificate.
232    /// Works with any backend implementing `SignBackend` (software, PKCS#11, `YubiKey`).
233    IssueCertificate,
234    /// Generate a PKCS#10 CSR signed by a backend-managed key.
235    ///
236    /// Takes a backend-managed signing key and subject parameters, assembles a
237    /// `CertReqInfo`, signs it via the backend's `SignBackend`, and produces a
238    /// `DER`-encoded `CSR`.
239    GenerateCsr,
240}
241
242/// Whether an action needs the `backend:` field on its step.
243///
244/// Three states, not two: an action can also run without a backend and accept
245/// one anyway. Verification is the case that forces the distinction, since a
246/// signature check needs only a public key but a validated deployment may
247/// require it inside the boundary.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
249#[serde(rename_all = "snake_case")]
250#[non_exhaustive]
251pub enum BackendUsage {
252    /// The step must name a backend; omitting it is an error.
253    Required,
254    /// The step runs in software unless it names a backend, which then does the
255    /// work instead. Naming one changes who performs the operation, not what
256    /// the step accepts or produces.
257    SoftwareUnlessNamed,
258    /// The action never uses a backend; naming one is a mistake worth warning about.
259    Unused,
260}
261
262/// The certificate shape an `issue_certificate` step asks for.
263///
264/// DSL vocabulary rather than a backend concern: the author writes the name,
265/// and the handler turns it into X.509 extensions. `path_len` is a separate
266/// parameter, not part of a profile's identity, so this enum carries no
267/// run-specific values.
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(into = "String", try_from = "String")]
270#[non_exhaustive]
271pub enum CertProfile {
272    /// Self-signed CA at the top of a chain.
273    RootCa,
274    /// CA below another, constrained by `path_len`. Also written
275    /// `intermediate_ca`.
276    SubCa,
277    /// TLS server certificate.
278    TlsServer,
279    /// Code-signing certificate.
280    CodeSigning,
281    /// A leaf with no CA rights and no extended key usage.
282    EndEntity,
283}
284
285crate::types::vocabulary!(CertProfile {
286    RootCa => "root_ca",
287    SubCa => "sub_ca" | "intermediate_ca",
288    TlsServer => "tls_server",
289    CodeSigning => "code_signing",
290    EndEntity => "end_entity",
291});
292
293impl fmt::Display for CertProfile {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        f.write_str(self.as_str())
296    }
297}
298
299impl std::str::FromStr for CertProfile {
300    type Err = UnknownCertProfile;
301
302    fn from_str(s: &str) -> Result<Self, Self::Err> {
303        Self::from_name(s).ok_or(UnknownCertProfile)
304    }
305}
306
307impl From<CertProfile> for String {
308    fn from(profile: CertProfile) -> Self {
309        profile.as_str().to_string()
310    }
311}
312
313impl TryFrom<String> for CertProfile {
314    type Error = UnknownCertProfile;
315
316    fn try_from(s: String) -> Result<Self, Self::Error> {
317        s.parse()
318    }
319}
320
321impl schemars::JsonSchema for CertProfile {
322    fn schema_name() -> std::borrow::Cow<'static, str> {
323        "CertProfile".into()
324    }
325
326    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
327        Self::names_schema()
328    }
329}
330
331/// A `profile:` value naming no certificate shape.
332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
333pub struct UnknownCertProfile;
334
335impl fmt::Display for UnknownCertProfile {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        let names: Vec<&str> = CertProfile::ALL.iter().map(|p| p.as_str()).collect();
338        write!(f, "supported profiles: {}", names.join(", "))
339    }
340}
341
342/// A DSL enum a ceremony writes as one of a fixed set of names.
343///
344/// Parsing and the schema both read the names from here, so an alias is
345/// written once and the schema accepts exactly what parsing does.
346pub(crate) trait Vocabulary: Copy + 'static {
347    /// Every value, in declaration order.
348    const VALUES: &'static [Self];
349    /// Other names a ceremony may write, and the value each stands for.
350    const ALIASES: &'static [(&'static str, Self)] = &[];
351
352    /// The name a ceremony writes and a transcript records.
353    fn name(self) -> &'static str;
354
355    /// The value `name` stands for, under its own name or an alias.
356    fn from_name(name: &str) -> Option<Self> {
357        Self::VALUES
358            .iter()
359            .map(|&value| (value.name(), value))
360            .chain(Self::ALIASES.iter().copied())
361            .find(|(candidate, _)| *candidate == name)
362            .map(|(_, value)| value)
363    }
364
365    /// A string schema that accepts every name and alias.
366    fn names_schema() -> schemars::Schema {
367        crate::params::names(
368            Self::VALUES
369                .iter()
370                .map(|value| value.name())
371                .chain(Self::ALIASES.iter().map(|(alias, _)| *alias)),
372        )
373    }
374}
375
376/// Give a DSL enum its names from one list: `ALL`, `as_str`, and its
377/// [`Vocabulary`] impl, aliases written `Variant => "name" | "alias"`.
378///
379/// `as_str` matches every variant, so a variant left out of the list fails
380/// to compile, and `ALL` is complete because it is built from the same list.
381macro_rules! vocabulary {
382    ($ty:ident { $($variant:ident => $name:literal $(| $alias:literal)*,)+ }) => {
383        impl $ty {
384            /// Every value, in declaration order.
385            pub const ALL: &'static [$ty] = &[$($ty::$variant),+];
386
387            /// The name a ceremony writes and a transcript records.
388            #[must_use]
389            pub fn as_str(self) -> &'static str {
390                match self {
391                    $($ty::$variant => $name,)+
392                }
393            }
394        }
395
396        impl $crate::types::Vocabulary for $ty {
397            const VALUES: &'static [Self] = Self::ALL;
398            const ALIASES: &'static [(&'static str, Self)] =
399                &[$($(($alias, $ty::$variant),)*)+];
400
401            fn name(self) -> &'static str {
402                self.as_str()
403            }
404        }
405    };
406}
407
408pub(crate) use vocabulary;
409
410/// Names as a person reads a list of alternatives: `a, b or c`.
411pub(crate) fn either<'a>(names: impl Iterator<Item = &'a str>) -> String {
412    let names: Vec<&str> = names.collect();
413    match names.split_last() {
414        Some((last, rest)) if !rest.is_empty() => format!("{} or {last}", rest.join(", ")),
415        Some((last, _)) => (*last).to_string(),
416        None => String::new(),
417    }
418}
419
420/// The secret sharing scheme a `split_secret` step names.
421///
422/// DSL vocabulary, like [`CertProfile`]: the author writes the name and the
423/// transcript records it. One scheme today. The enum exists so a second one
424/// (a prime field, a verifiable variant, codex32) is a variant rather than a
425/// flag, and so a name this build does not implement is refused at `rite
426/// check`.
427#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(into = "String", try_from = "String")]
429#[non_exhaustive]
430pub enum SharingScheme {
431    /// Shamir over GF(2^8), one polynomial per byte, the `rite-sss/v1`
432    /// share format.
433    RiteSssV1,
434}
435
436crate::types::vocabulary!(SharingScheme {
437    RiteSssV1 => "rite-sss/v1",
438});
439
440impl SharingScheme {
441    /// The most shares a split under this scheme may make.
442    ///
443    /// A policy limit rather than what the arithmetic allows: a share index
444    /// is a byte, so `rite-sss/v1` could evaluate 255 points, and 100 is a
445    /// round number well past any split a room of custodians receives. Kept
446    /// low on purpose, since raising it later costs nothing and lowering it
447    /// would break a ceremony that ran. It also leaves the indexes SLIP-0039
448    /// reserves (254 for a digest, 255 for the secret) free for a scheme that
449    /// wants them.
450    pub fn max_shares(self) -> u8 {
451        match self {
452            SharingScheme::RiteSssV1 => 100,
453        }
454    }
455
456    /// The bytes in front of a share's values: for `rite-sss/v1`, a
457    /// version, a threshold and an index.
458    #[must_use]
459    pub fn header_len(self) -> usize {
460        match self {
461            SharingScheme::RiteSssV1 => 3,
462        }
463    }
464
465    /// The length in bytes of one share of a secret of `secret_len` bytes,
466    /// as a sheet or a transport holds it: the header, then one byte per
467    /// byte of the secret.
468    #[must_use]
469    pub fn share_len(self, secret_len: usize) -> usize {
470        match self {
471            SharingScheme::RiteSssV1 => secret_len.saturating_add(self.header_len()),
472        }
473    }
474}
475
476impl fmt::Display for SharingScheme {
477    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478        f.write_str(self.as_str())
479    }
480}
481
482impl From<SharingScheme> for String {
483    fn from(scheme: SharingScheme) -> Self {
484        scheme.as_str().to_string()
485    }
486}
487
488impl std::str::FromStr for SharingScheme {
489    type Err = UnknownSharingScheme;
490
491    fn from_str(s: &str) -> Result<Self, Self::Err> {
492        Self::from_name(s).ok_or(UnknownSharingScheme)
493    }
494}
495
496impl schemars::JsonSchema for SharingScheme {
497    fn schema_name() -> std::borrow::Cow<'static, str> {
498        "SharingScheme".into()
499    }
500
501    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
502        Self::names_schema()
503    }
504}
505
506impl TryFrom<String> for SharingScheme {
507    type Error = UnknownSharingScheme;
508
509    fn try_from(s: String) -> Result<Self, Self::Error> {
510        s.parse()
511    }
512}
513
514/// A `scheme:` value naming no sharing scheme this build implements.
515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516pub struct UnknownSharingScheme;
517
518impl fmt::Display for UnknownSharingScheme {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        let names: Vec<&str> = SharingScheme::ALL.iter().map(|s| s.as_str()).collect();
521        write!(f, "supported sharing schemes: {}", names.join(", "))
522    }
523}
524
525impl std::error::Error for UnknownSharingScheme {}
526
527/// What an action requires of its step's `reads:` map.
528///
529/// See [`ActionType::reads_contract`].
530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
531#[non_exhaustive]
532pub struct ReadsContract {
533    /// Inputs every step of this action must name.
534    pub required: &'static [&'static str],
535    /// Groups from which exactly one input must be named. Each group holds the
536    /// alternatives in the order they should be listed to the author.
537    pub exactly_one_of: &'static [&'static [&'static str]],
538    /// Inputs a step may name and the action reads when it does.
539    ///
540    /// Listed so that a key the action would never look at is refused rather
541    /// than dropped: a `reads:` entry that nothing reads is a step running
542    /// without what its author gave it.
543    pub optional: &'static [&'static str],
544    /// `with:` fields that mean nothing unless the step also names a given
545    /// input, as `(field, input)` pairs.
546    ///
547    /// Where an action offers two paths over one operation, a parameter can
548    /// belong to only one of them. On the other path it would be parsed and
549    /// never read, so the step would run without whatever the parameter asked
550    /// for. This says which parameter needs which input.
551    pub with_field_requires: &'static [(&'static str, &'static str)],
552    /// Inputs that hold a list of references rather than one, each with the
553    /// fewest entries a step may give.
554    ///
555    /// For an action that takes a set whose size the ceremony chooses, such
556    /// as the shares of a split. Every other input named in this contract
557    /// holds one reference, and the resolver reports a list where one is
558    /// expected as it reports one where a list is.
559    pub lists: &'static [ListInput],
560}
561
562/// A `reads:` input that holds a list of references.
563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
564pub struct ListInput {
565    /// The key under `reads:`.
566    pub name: &'static str,
567    /// Fewest references the list may hold.
568    pub at_least: usize,
569}
570
571impl ReadsContract {
572    /// The contract of an action that constrains its inputs in neither way.
573    pub const NONE: Self = Self::required(&[]);
574
575    /// The contract of an action that requires these inputs and offers no
576    /// alternatives between them.
577    #[must_use]
578    pub const fn required(fields: &'static [&'static str]) -> Self {
579        Self {
580            required: fields,
581            exactly_one_of: &[],
582            optional: &[],
583            with_field_requires: &[],
584            lists: &[],
585        }
586    }
587
588    /// Whether the input named `key` holds a list under this contract.
589    pub fn is_list(&self, key: &str) -> bool {
590        self.lists.iter().any(|list| list.name == key)
591    }
592
593    /// Whether this contract constrains the `reads:` map at all.
594    ///
595    /// `with_field_requires` is a rule over `with:`, so it is not counted here.
596    pub fn is_empty(&self) -> bool {
597        self.required.is_empty()
598            && self.exactly_one_of.is_empty()
599            && self.optional.is_empty()
600            && self.lists.is_empty()
601    }
602
603    /// Whether a step of this action may name this input.
604    pub fn accepts(&self, key: &str) -> bool {
605        self.required.contains(&key)
606            || self.optional.contains(&key)
607            || self.exactly_one_of.iter().any(|group| group.contains(&key))
608            || self.lists.iter().any(|list| list.name == key)
609    }
610}
611
612impl ActionType {
613    /// Every action type, for callers that need to enumerate them.
614    ///
615    /// Maintained by hand: `#[non_exhaustive]` means no downstream crate can
616    /// derive this list, and adding a variant produces no error outside this
617    /// crate. Adding one here is what keeps editor completion and any other
618    /// catalogue from silently missing it. The test below catches omissions.
619    pub const ALL: &'static [ActionType] = &[
620        ActionType::ClockCheck,
621        ActionType::Confirm,
622        ActionType::CheckValue,
623        ActionType::OralReadback,
624        ActionType::Reveal,
625        ActionType::MachineInfo,
626        ActionType::EnterValue,
627        ActionType::EnterSecret,
628        ActionType::GenerateKey,
629        ActionType::WrapKey,
630        ActionType::UnwrapKey,
631        ActionType::ImportKey,
632        ActionType::EncryptData,
633        ActionType::DecryptData,
634        ActionType::SplitSecret,
635        ActionType::CombineShares,
636        ActionType::EnterShare,
637        ActionType::ExportPublic,
638        ActionType::SignData,
639        ActionType::VerifySignature,
640        ActionType::Attest,
641        ActionType::GatherEntropy,
642        ActionType::TpmAttest,
643        ActionType::PivReadCertificate,
644        ActionType::PivSign,
645        ActionType::YubikeyAttestSlot,
646        ActionType::IssueCertificate,
647        ActionType::GenerateCsr,
648    ];
649
650    /// How this action relates to the `backend:` field on its step.
651    ///
652    /// TODO: Replace this with a nested enum split — `ActionType::Backend(BackendAction)` vs
653    /// `ActionType::Local(LocalAction)`, once a home is found for the actions that are
654    /// neither. This is a breaking change to every match on `ActionType` variants.
655    pub fn backend_usage(self) -> BackendUsage {
656        match self {
657            ActionType::GenerateKey
658            | ActionType::SignData
659            | ActionType::WrapKey
660            | ActionType::UnwrapKey
661            | ActionType::ExportPublic
662            | ActionType::GenerateCsr
663            | ActionType::IssueCertificate
664            | ActionType::PivReadCertificate
665            | ActionType::PivSign
666            | ActionType::YubikeyAttestSlot
667            | ActionType::ImportKey
668            | ActionType::EncryptData
669            | ActionType::DecryptData
670            | ActionType::SplitSecret
671            | ActionType::TpmAttest => BackendUsage::Required,
672
673            ActionType::VerifySignature => BackendUsage::SoftwareUnlessNamed,
674
675            ActionType::ClockCheck
676            | ActionType::Confirm
677            | ActionType::CheckValue
678            | ActionType::OralReadback
679            | ActionType::Reveal
680            | ActionType::MachineInfo
681            | ActionType::EnterValue
682            | ActionType::EnterSecret
683            | ActionType::Attest
684            | ActionType::CombineShares
685            | ActionType::EnterShare
686            | ActionType::GatherEntropy => BackendUsage::Unused,
687        }
688    }
689
690    /// Whether the artifact this action creates is shares of a secret, which
691    /// `rite check` tells from the step that made it, since the definition
692    /// does not say what kind an artifact is.
693    pub fn creates_shares(self) -> bool {
694        match self {
695            ActionType::SplitSecret | ActionType::EnterShare => true,
696            ActionType::ClockCheck
697            | ActionType::Confirm
698            | ActionType::CheckValue
699            | ActionType::OralReadback
700            | ActionType::Reveal
701            | ActionType::MachineInfo
702            | ActionType::EnterValue
703            | ActionType::EnterSecret
704            | ActionType::GenerateKey
705            | ActionType::WrapKey
706            | ActionType::UnwrapKey
707            | ActionType::ImportKey
708            | ActionType::EncryptData
709            | ActionType::DecryptData
710            | ActionType::CombineShares
711            | ActionType::ExportPublic
712            | ActionType::SignData
713            | ActionType::VerifySignature
714            | ActionType::GenerateCsr
715            | ActionType::IssueCertificate
716            | ActionType::PivReadCertificate
717            | ActionType::PivSign
718            | ActionType::YubikeyAttestSlot
719            | ActionType::Attest
720            | ActionType::GatherEntropy
721            | ActionType::TpmAttest => false,
722        }
723    }
724
725    /// Returns the `reads:` inputs this action requires, and the groups from
726    /// which exactly one input must be named.
727    ///
728    /// A group is how an action offers two paths over the same operation. The
729    /// input the author names selects the path, so naming both is ambiguous and
730    /// naming neither leaves the path undetermined. The resolver reports either
731    /// on the step.
732    pub fn reads_contract(self) -> ReadsContract {
733        match self {
734            ActionType::WrapKey => ReadsContract {
735                required: &["key_to_wrap"],
736                exactly_one_of: &[&["wrapping_key", "recipient"]],
737                optional: &[],
738                // `expect_recipient` is compared against the recipient a wrap
739                // is given, and only the external path has one.
740                with_field_requires: &[("expect_recipient", "recipient")],
741                lists: &[],
742            },
743            ActionType::UnwrapKey => ReadsContract::required(&["unwrapping_key", "wrapped_data"]),
744            ActionType::ImportKey => ReadsContract {
745                required: &["key_material"],
746                exactly_one_of: &[],
747                // What opens an encrypted private key. Read from the store
748                // rather than given in `with:`, so the value is borrowed and
749                // never copied into the step's parameters.
750                optional: &["passphrase"],
751                with_field_requires: &[],
752                lists: &[],
753            },
754            ActionType::EncryptData => ReadsContract::required(&["data", "encryption_key"]),
755            ActionType::DecryptData => {
756                ReadsContract::required(&["encrypted_data", "decryption_key"])
757            }
758            ActionType::SplitSecret => ReadsContract::required(&["secret"]),
759            // Two is the smallest threshold; the shares themselves say
760            // whether two is enough, at run time.
761            ActionType::CombineShares => ReadsContract {
762                required: &[],
763                exactly_one_of: &[],
764                optional: &[],
765                with_field_requires: &[],
766                lists: &[ListInput {
767                    name: "shares",
768                    at_least: 2,
769                }],
770            },
771            ActionType::SignData => ReadsContract::required(&["key", "data"]),
772            ActionType::VerifySignature => ReadsContract::required(&["key", "data", "signature"]),
773            // Without `issuer_cert` the certificate is self-issued under the
774            // CSR's subject; with it, the issuer name and key identifier come
775            // from the CA certificate.
776            ActionType::IssueCertificate => ReadsContract {
777                required: &["signing_key", "csr"],
778                exactly_one_of: &[],
779                optional: &["issuer_cert"],
780                with_field_requires: &[],
781                lists: &[],
782            },
783            ActionType::GenerateCsr => ReadsContract::required(&["signing_key"]),
784            ActionType::Reveal => ReadsContract::required(&["value"]),
785
786            ActionType::ClockCheck
787            | ActionType::Confirm
788            | ActionType::CheckValue
789            | ActionType::OralReadback
790            | ActionType::MachineInfo
791            | ActionType::EnterValue
792            | ActionType::EnterSecret
793            | ActionType::GenerateKey
794            | ActionType::ExportPublic
795            | ActionType::Attest
796            | ActionType::GatherEntropy
797            | ActionType::TpmAttest
798            | ActionType::PivReadCertificate
799            | ActionType::PivSign
800            | ActionType::YubikeyAttestSlot
801            | ActionType::EnterShare => ReadsContract::NONE,
802        }
803    }
804
805    /// Short human-readable description of what this action does.
806    ///
807    /// Used as fallback prose in script generation, TUI step display, and LSP
808    /// hover when no explicit `description` or `message` parameter is present.
809    pub fn describe(&self) -> &'static str {
810        match self {
811            ActionType::ClockCheck => "Verify system clock against a reference time.",
812            ActionType::Confirm => "Confirm readiness or completion of a manual step.",
813            ActionType::CheckValue => "Verify a value matches an expected result.",
814            ActionType::OralReadback => "Read back a value aloud for verification.",
815            ActionType::Reveal => "Show a value for a person to write down.",
816            ActionType::MachineInfo => "Record system and environment information.",
817            ActionType::EnterValue => "Type a value the ceremony records.",
818            ActionType::EnterSecret => "Type a secret the ceremony holds and never records.",
819            ActionType::Attest => "Record a signed attestation from a participant.",
820            ActionType::GatherEntropy => "Fold human-supplied entropy into the ceremony seed.",
821            ActionType::TpmAttest => "Record TPM platform attestation (PCR values).",
822            ActionType::GenerateKey => "Generate a cryptographic key.",
823            ActionType::ExportPublic => "Export the public component of a keypair.",
824            ActionType::SignData => "Sign data with a ceremony key.",
825            ActionType::VerifySignature => "Verify a signature against a public key.",
826            ActionType::WrapKey => "Encrypt a key under another key for transport.",
827            ActionType::UnwrapKey => "Decrypt a wrapped key and import it into a backend.",
828            ActionType::GenerateCsr => "Generate a Certificate Signing Request.",
829            ActionType::IssueCertificate => "Issue an X.509 certificate from a CSR.",
830            ActionType::PivReadCertificate => "Read a certificate from a PIV smart card slot.",
831            ActionType::PivSign => "Sign data using a PIV smart card key.",
832            ActionType::YubikeyAttestSlot => "Attest a YubiKey PIV slot key.",
833            ActionType::ImportKey => "Import a key from material the ceremony holds.",
834            ActionType::EncryptData => "Encrypt content under a key held by a backend.",
835            ActionType::DecryptData => "Decrypt an encrypted-data artifact back to bytes.",
836            ActionType::SplitSecret => {
837                "Split a secret into shares a threshold of which reconstruct it."
838            }
839            ActionType::CombineShares => "Reconstruct a secret from its shares.",
840            ActionType::EnterShare => "Type a share back from its sheet.",
841        }
842    }
843}
844
845impl std::fmt::Display for ActionType {
846    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
847        match self {
848            ActionType::ClockCheck => write!(f, "clock_check"),
849            ActionType::Confirm => write!(f, "confirm"),
850            ActionType::CheckValue => write!(f, "check_value"),
851            ActionType::OralReadback => write!(f, "oral_readback"),
852            ActionType::Reveal => write!(f, "reveal"),
853            ActionType::MachineInfo => write!(f, "machine_info"),
854            ActionType::EnterValue => write!(f, "enter_value"),
855            ActionType::EnterSecret => write!(f, "enter_secret"),
856            ActionType::GenerateKey => write!(f, "generate_key"),
857            ActionType::WrapKey => write!(f, "wrap_key"),
858            ActionType::UnwrapKey => write!(f, "unwrap_key"),
859            ActionType::ImportKey => write!(f, "import_key"),
860            ActionType::EncryptData => write!(f, "encrypt_data"),
861            ActionType::DecryptData => write!(f, "decrypt_data"),
862            ActionType::SplitSecret => write!(f, "split_secret"),
863            ActionType::CombineShares => write!(f, "combine_shares"),
864            ActionType::EnterShare => write!(f, "enter_share"),
865            ActionType::ExportPublic => write!(f, "export_public"),
866            ActionType::SignData => write!(f, "sign_data"),
867            ActionType::VerifySignature => write!(f, "verify_signature"),
868            ActionType::Attest => write!(f, "attest"),
869            ActionType::GatherEntropy => write!(f, "gather_entropy"),
870            ActionType::TpmAttest => write!(f, "tpm_attest"),
871            ActionType::PivReadCertificate => write!(f, "piv_read_certificate"),
872            ActionType::PivSign => write!(f, "piv_sign"),
873            ActionType::YubikeyAttestSlot => write!(f, "yubikey_attest_slot"),
874            ActionType::IssueCertificate => write!(f, "issue_certificate"),
875            ActionType::GenerateCsr => write!(f, "generate_csr"),
876        }
877    }
878}
879
880/// Type of output produced by a ceremony.
881#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
882#[serde(rename_all = "snake_case")]
883#[non_exhaustive]
884pub enum OutputType {
885    /// Public key in PEM format.
886    PublicKey,
887    /// A key wrapped for transport, in the container its scheme names.
888    WrappedKey,
889    /// Content encrypted for a recipient, in the container its scheme names.
890    EncryptedData,
891    /// X.509 certificate.
892    Certificate,
893    /// DNSSEC signed resource record set.
894    SignedRrset,
895    /// Certificate Transparency Signed Certificate Timestamp.
896    Sct,
897
898    // Documents
899    /// Generic document.
900    Document,
901    /// Ceremony log or transcript.
902    CeremonyLog,
903}
904
905impl std::fmt::Display for OutputType {
906    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
907        match self {
908            OutputType::PublicKey => write!(f, "public_key"),
909            OutputType::WrappedKey => write!(f, "wrapped_key"),
910            OutputType::EncryptedData => write!(f, "encrypted_data"),
911            OutputType::Certificate => write!(f, "certificate"),
912            OutputType::SignedRrset => write!(f, "signed_rrset"),
913            OutputType::Sct => write!(f, "sct"),
914            OutputType::Document => write!(f, "document"),
915            OutputType::CeremonyLog => write!(f, "ceremony_log"),
916        }
917    }
918}
919
920impl OutputType {
921    /// Returns the default file extension for this output type.
922    pub fn default_extension(&self) -> &'static str {
923        match self {
924            OutputType::PublicKey | OutputType::Certificate => "pem",
925            OutputType::WrappedKey
926            | OutputType::EncryptedData
927            | OutputType::SignedRrset
928            | OutputType::Sct => "bin",
929            OutputType::Document => "txt",
930            OutputType::CeremonyLog => "json",
931        }
932    }
933}
934
935/// Type of a ceremony parameter.
936#[derive(Debug, Clone, Serialize, Deserialize)]
937#[serde(rename_all = "snake_case")]
938#[non_exhaustive]
939pub enum ParameterType {
940    /// Text value.
941    String,
942    /// Date value (`YYYY-MM-DD`).
943    Date,
944    /// Integer value.
945    Integer,
946    /// Boolean value.
947    Boolean,
948}
949
950impl std::fmt::Display for ParameterType {
951    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
952        match self {
953            ParameterType::String => write!(f, "string"),
954            ParameterType::Date => write!(f, "date (YYYY-MM-DD)"),
955            ParameterType::Integer => write!(f, "integer"),
956            ParameterType::Boolean => write!(f, "boolean (true/false)"),
957        }
958    }
959}
960
961/// Typed preset for common post-ceremony duty categories.
962///
963/// Provides built-in prose for scripts when no description is given.
964/// Use `Custom` for duties that don't fit a preset; description is then required.
965///
966/// Note: physical handling steps (sealing hardware, etc.) belong as `physical_action`
967/// steps in ceremony execution, not here.
968#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
969#[serde(rename_all = "snake_case")]
970#[non_exhaustive]
971pub enum DutyType {
972    /// Return materials to secure storage location.
973    ReturnToVault,
974    /// Hand key shares to assigned custodians.
975    DistributeShares,
976    /// Distribute backup media to designated recipients.
977    DistributeMedia,
978    /// Archive ceremony materials at storage locations.
979    ArchiveMaterials,
980    /// Publish ceremony record and witness attestations.
981    PublishRecord,
982    /// Notify stakeholders of ceremony completion.
983    NotifyStakeholders,
984    /// Import generated keys into operational system.
985    ImportKeys,
986    /// Free-form duty; description is required.
987    Custom,
988}
989
990impl DutyType {
991    /// Returns the built-in display name for this duty type.
992    pub fn display_name(&self) -> &'static str {
993        match self {
994            DutyType::ReturnToVault => "Return to Vault",
995            DutyType::DistributeShares => "Distribute Key Shares",
996            DutyType::DistributeMedia => "Distribute Backup Media",
997            DutyType::ArchiveMaterials => "Archive Materials",
998            DutyType::PublishRecord => "Publish Record",
999            DutyType::NotifyStakeholders => "Notify Stakeholders",
1000            DutyType::ImportKeys => "Import Keys",
1001            DutyType::Custom => "Custom Duty",
1002        }
1003    }
1004
1005    /// Returns the built-in prose description for this duty type.
1006    ///
1007    /// Returns `None` for `Custom` (description must be provided explicitly).
1008    pub fn built_in_prose(&self) -> Option<&'static str> {
1009        match self {
1010            DutyType::ReturnToVault => Some("Return materials to secure storage"),
1011            DutyType::DistributeShares => Some("Distribute key shares to assigned custodians"),
1012            DutyType::DistributeMedia => Some("Distribute backup media to designated recipients"),
1013            DutyType::ArchiveMaterials => {
1014                Some("Archive ceremony materials at designated storage locations")
1015            }
1016            DutyType::PublishRecord => Some("Publish ceremony record and witness attestations"),
1017            DutyType::NotifyStakeholders => Some("Notify stakeholders of ceremony completion"),
1018            DutyType::ImportKeys => Some("Import generated keys into operational system"),
1019            DutyType::Custom => None,
1020        }
1021    }
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026    use super::*;
1027
1028    #[test]
1029    fn role_type_extracts_prefix() {
1030        assert_eq!(role_type("witness__1"), "witness");
1031        assert_eq!(role_type("hsm_operator__primary"), "hsm_operator");
1032        assert_eq!(role_type("operator"), "operator");
1033        assert_eq!(role_type("witness__"), "witness");
1034    }
1035
1036    #[test]
1037    fn derive_step_name_title_cases() {
1038        assert_eq!(derive_step_name("verify_time"), "Verify Time");
1039        assert_eq!(derive_step_name("generate_root_ca"), "Generate Root Ca");
1040        assert_eq!(derive_step_name("wrap_root_ca_key"), "Wrap Root Ca Key");
1041        assert_eq!(derive_step_name("witness1_attest"), "Witness1 Attest");
1042        assert_eq!(derive_step_name("clock-check"), "Clock Check");
1043        assert_eq!(derive_step_name("opening"), "Opening");
1044    }
1045
1046    #[test]
1047    fn derive_role_name_title_cases() {
1048        assert_eq!(derive_role_name("witness__1"), "Witness 1");
1049        assert_eq!(
1050            derive_role_name("hsm_operator__primary"),
1051            "Hsm Operator Primary"
1052        );
1053        assert_eq!(derive_role_name("operator"), "Operator");
1054        assert_eq!(derive_role_name("ceremony-admin"), "Ceremony Admin");
1055    }
1056
1057    #[test]
1058    fn action_type_serde_roundtrip() {
1059        // `snake_case` rename means these exact strings appear in ceremony YAML.
1060        // A variant rename or serde attr change breaks YAML parsing silently.
1061        let cases: &[(ActionType, &str)] = &[
1062            (ActionType::ClockCheck, "\"clock_check\""),
1063            (ActionType::Confirm, "\"confirm\""),
1064            (ActionType::CheckValue, "\"check_value\""),
1065            (ActionType::OralReadback, "\"oral_readback\""),
1066            (ActionType::Reveal, "\"reveal\""),
1067            (ActionType::MachineInfo, "\"machine_info\""),
1068            (ActionType::GenerateKey, "\"generate_key\""),
1069            (ActionType::WrapKey, "\"wrap_key\""),
1070            (ActionType::UnwrapKey, "\"unwrap_key\""),
1071            (ActionType::ImportKey, "\"import_key\""),
1072            (ActionType::EncryptData, "\"encrypt_data\""),
1073            (ActionType::DecryptData, "\"decrypt_data\""),
1074            (ActionType::SplitSecret, "\"split_secret\""),
1075            (ActionType::CombineShares, "\"combine_shares\""),
1076            (ActionType::EnterShare, "\"enter_share\""),
1077            (ActionType::ExportPublic, "\"export_public\""),
1078            (ActionType::Attest, "\"attest\""),
1079            (ActionType::TpmAttest, "\"tpm_attest\""),
1080            (ActionType::PivReadCertificate, "\"piv_read_certificate\""),
1081            (ActionType::PivSign, "\"piv_sign\""),
1082            (ActionType::YubikeyAttestSlot, "\"yubikey_attest_slot\""),
1083            (ActionType::IssueCertificate, "\"issue_certificate\""),
1084            (ActionType::GenerateCsr, "\"generate_csr\""),
1085        ];
1086        for &(variant, expected) in cases {
1087            let serialized = serde_json::to_string(&variant).unwrap();
1088            assert_eq!(serialized, expected, "serialize {variant:?}");
1089            let deserialized: ActionType = serde_json::from_str(expected).unwrap();
1090            assert_eq!(deserialized, variant, "deserialize {expected}");
1091        }
1092    }
1093
1094    #[test]
1095    fn action_type_display_matches_serde() {
1096        // Display is used in transcript output and error messages; serde in YAML parsing.
1097        // They must agree or transcripts reference action names that differ from YAML.
1098        let actions = [
1099            ActionType::ClockCheck,
1100            ActionType::Confirm,
1101            ActionType::CheckValue,
1102            ActionType::OralReadback,
1103            ActionType::Reveal,
1104            ActionType::MachineInfo,
1105            ActionType::GenerateKey,
1106            ActionType::WrapKey,
1107            ActionType::UnwrapKey,
1108            ActionType::ImportKey,
1109            ActionType::ExportPublic,
1110            ActionType::Attest,
1111            ActionType::TpmAttest,
1112            ActionType::PivReadCertificate,
1113            ActionType::PivSign,
1114            ActionType::YubikeyAttestSlot,
1115            ActionType::IssueCertificate,
1116            ActionType::GenerateCsr,
1117        ];
1118        for action in actions {
1119            let display = action.to_string();
1120            let serde_json = serde_json::to_string(&action).unwrap();
1121            assert_eq!(
1122                display,
1123                serde_json.trim_matches('"'),
1124                "Display and serde disagree for {action:?}"
1125            );
1126        }
1127    }
1128
1129    #[test]
1130    fn output_type_serde_roundtrip() {
1131        // These strings appear in ceremony YAML `output:` blocks and in file extension
1132        // mapping. A rename breaks both YAML parsing and output file naming.
1133        let cases: &[(OutputType, &str)] = &[
1134            (OutputType::PublicKey, "\"public_key\""),
1135            (OutputType::WrappedKey, "\"wrapped_key\""),
1136            (OutputType::EncryptedData, "\"encrypted_data\""),
1137            (OutputType::Certificate, "\"certificate\""),
1138            (OutputType::Document, "\"document\""),
1139            (OutputType::CeremonyLog, "\"ceremony_log\""),
1140        ];
1141        for &(variant, expected) in cases {
1142            let serialized = serde_json::to_string(&variant).unwrap();
1143            assert_eq!(serialized, expected, "serialize {variant:?}");
1144            let deserialized: OutputType = serde_json::from_str(expected).unwrap();
1145            assert_eq!(deserialized, variant, "deserialize {expected}");
1146        }
1147    }
1148
1149    #[test]
1150    fn parameter_type_serde_roundtrip() {
1151        // These strings appear in ceremony YAML `parameters:` blocks.
1152        let cases: &[(ParameterType, &str)] = &[
1153            (ParameterType::String, "\"string\""),
1154            (ParameterType::Date, "\"date\""),
1155            (ParameterType::Integer, "\"integer\""),
1156            (ParameterType::Boolean, "\"boolean\""),
1157        ];
1158        for (variant, expected) in cases {
1159            let serialized = serde_json::to_string(variant).unwrap();
1160            assert_eq!(serialized, *expected, "serialize {variant:?}");
1161            let deserialized: ParameterType = serde_json::from_str(expected).unwrap();
1162            // ParameterType does not implement PartialEq; check the display instead.
1163            assert_eq!(
1164                serde_json::to_string(&deserialized).unwrap(),
1165                *expected,
1166                "deserialize {expected}"
1167            );
1168        }
1169    }
1170
1171    #[test]
1172    fn duty_type_serde_roundtrip() {
1173        // These strings appear in ceremony YAML `after:` blocks.
1174        let cases: &[(DutyType, &str)] = &[
1175            (DutyType::ReturnToVault, "\"return_to_vault\""),
1176            (DutyType::DistributeShares, "\"distribute_shares\""),
1177            (DutyType::DistributeMedia, "\"distribute_media\""),
1178            (DutyType::ArchiveMaterials, "\"archive_materials\""),
1179            (DutyType::PublishRecord, "\"publish_record\""),
1180            (DutyType::NotifyStakeholders, "\"notify_stakeholders\""),
1181            (DutyType::ImportKeys, "\"import_keys\""),
1182            (DutyType::Custom, "\"custom\""),
1183        ];
1184        for (variant, expected) in cases {
1185            let serialized = serde_json::to_string(variant).unwrap();
1186            assert_eq!(serialized, *expected, "serialize {variant:?}");
1187            let deserialized: DutyType = serde_json::from_str(expected).unwrap();
1188            assert_eq!(deserialized, *variant, "deserialize {expected}");
1189        }
1190    }
1191
1192    #[test]
1193    fn duty_type_custom_has_no_built_in_prose() {
1194        // The runtime branches on this: if built_in_prose returns None, the description
1195        // field is required. A regression here would panic or produce empty script output.
1196        assert!(
1197            DutyType::Custom.built_in_prose().is_none(),
1198            "Custom duty must have no built-in prose"
1199        );
1200        // All other variants must have prose.
1201        for duty in [
1202            DutyType::ReturnToVault,
1203            DutyType::DistributeShares,
1204            DutyType::DistributeMedia,
1205            DutyType::ArchiveMaterials,
1206            DutyType::PublishRecord,
1207            DutyType::NotifyStakeholders,
1208            DutyType::ImportKeys,
1209        ] {
1210            assert!(
1211                duty.built_in_prose().is_some(),
1212                "{duty:?} must have built-in prose"
1213            );
1214        }
1215    }
1216
1217    /// `vocabulary!` builds each `ALL` and `as_str` from one list, so neither
1218    /// can miss a variant. Each name parses back, and is the name serde
1219    /// writes, which for a derived serde is a second spelling.
1220    #[test]
1221    fn every_vocabulary_lists_every_value_under_its_serde_name() {
1222        use crate::{Format, RevealFormat};
1223
1224        fn check<T>(all: &[T])
1225        where
1226            T: Copy + PartialEq + std::fmt::Debug + std::fmt::Display + Serialize,
1227            T: std::str::FromStr,
1228        {
1229            for &value in all {
1230                let name = value.to_string();
1231                assert!(
1232                    matches!(name.parse::<T>(), Ok(parsed) if parsed == value),
1233                    "{name}"
1234                );
1235                assert_eq!(serde_json::to_value(value).unwrap(), name.as_str());
1236            }
1237        }
1238
1239        check(CertProfile::ALL);
1240        check(SharingScheme::ALL);
1241        check(RevealFormat::ALL);
1242        check(Format::ALL);
1243    }
1244
1245    /// serde, `FromStr` and the schema accept the same names, aliases
1246    /// included, for every vocabulary whose schema is its names.
1247    #[test]
1248    fn a_vocabulary_schema_accepts_what_serde_reads() {
1249        use crate::params::ReadbackFormat;
1250
1251        fn check<T>()
1252        where
1253            T: Vocabulary + std::fmt::Debug + PartialEq + schemars::JsonSchema,
1254            T: serde::de::DeserializeOwned,
1255        {
1256            let schema = schemars::schema_for!(T);
1257            let listed: Vec<&str> = schema
1258                .get("enum")
1259                .and_then(|names| names.as_array())
1260                .unwrap()
1261                .iter()
1262                .filter_map(|name| name.as_str())
1263                .collect();
1264            let named = T::VALUES.iter().map(|&value| (value.name(), value));
1265            let mut expected = Vec::new();
1266            for (name, value) in named.chain(T::ALIASES.iter().copied()) {
1267                assert_eq!(
1268                    serde_json::from_value::<T>(serde_json::json!(name)).unwrap(),
1269                    value
1270                );
1271                expected.push(name);
1272            }
1273            assert_eq!(listed, expected);
1274        }
1275
1276        check::<CertProfile>();
1277        check::<SharingScheme>();
1278        check::<ReadbackFormat>();
1279        assert_eq!("intermediate_ca".parse(), Ok(CertProfile::SubCa));
1280        assert_eq!(
1281            serde_json::from_value::<ReadbackFormat>(serde_json::json!("nato")).unwrap(),
1282            ReadbackFormat::NatoPhonetic
1283        );
1284    }
1285
1286    /// `ActionType::ALL` is written by hand, so it can fall behind the enum.
1287    ///
1288    /// The match below is exhaustive, so adding a variant fails to compile
1289    /// here. That is the whole mechanism: it puts a compiler error next to the
1290    /// list an author has to extend. The name check then catches a variant
1291    /// listed twice, and `rite-ls` compares its own catalogue against `ALL`,
1292    /// which is what catches one left out.
1293    #[test]
1294    fn all_lists_every_action_type() {
1295        for action in ActionType::ALL {
1296            match action {
1297                ActionType::ClockCheck
1298                | ActionType::Confirm
1299                | ActionType::CheckValue
1300                | ActionType::OralReadback
1301                | ActionType::Reveal
1302                | ActionType::MachineInfo
1303                | ActionType::EnterValue
1304                | ActionType::EnterSecret
1305                | ActionType::GenerateKey
1306                | ActionType::WrapKey
1307                | ActionType::UnwrapKey
1308                | ActionType::ImportKey
1309                | ActionType::EncryptData
1310                | ActionType::DecryptData
1311                | ActionType::SplitSecret
1312                | ActionType::CombineShares
1313                | ActionType::EnterShare
1314                | ActionType::ExportPublic
1315                | ActionType::SignData
1316                | ActionType::VerifySignature
1317                | ActionType::Attest
1318                | ActionType::GatherEntropy
1319                | ActionType::TpmAttest
1320                | ActionType::PivReadCertificate
1321                | ActionType::PivSign
1322                | ActionType::YubikeyAttestSlot
1323                | ActionType::IssueCertificate
1324                | ActionType::GenerateCsr => {}
1325            }
1326        }
1327
1328        let names: std::collections::BTreeSet<String> =
1329            ActionType::ALL.iter().map(ToString::to_string).collect();
1330        assert_eq!(
1331            names.len(),
1332            ActionType::ALL.len(),
1333            "two actions share a DSL name"
1334        );
1335    }
1336}