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