Skip to main content

rite_model/
transcript.rs

1//! Persisted transcript schema.
2//!
3//! These types are the **durable** audit surface: what gets written to
4//! `transcript.jsonl`, what `rite verify` reads back, what report and
5//! audit tooling consume. They are deliberately kept independent of the
6//! executor and the channel plumbing, a third-party verifier can parse
7//! a transcript with only `rite-model` on its dependency list.
8//!
9//! The live runtime↔frontend channel protocol (`ExecEvent`, `UiCommand`,
10//! `Response`, `Icon`, …) lives in `rite-runtime` next to the executor
11//! that owns the channels. The boundary is **persisted vs in-flight**.
12
13use std::collections::BTreeMap;
14
15use base64ct::Encoding as _;
16use serde::{Deserialize, Serialize};
17use zeroize::Zeroizing;
18
19use crate::Sha256Digest;
20use crate::ir::{ActId, MaterialId, ParamId, RoleId, StepId};
21
22/// Transcript format version this crate writes and reads: the line envelope
23/// and the chain rule.
24///
25/// 0 while rite is before 1.0: the format changes between releases without
26/// a new number, and the header's `producer` names the release that wrote a
27/// transcript.
28pub const TRANSCRIPT_FORMAT: u32 = 0;
29
30/// Version of the [`StepFact`] vocabulary this crate writes and reads. 0
31/// before 1.0, like [`TRANSCRIPT_FORMAT`].
32pub const FACT_VOCABULARY: u32 = 0;
33
34/// URL of the JSON Schema for the transcript lines this release writes,
35/// written on the header line for editors and other tools. A reader ignores
36/// it.
37pub const TRANSCRIPT_SCHEMA: &str = concat!(
38    "https://ritely.io/schemas/",
39    env!("CARGO_PKG_VERSION"),
40    "/transcript.schema.json"
41);
42
43/// First line of every transcript: what the file is, read before any fact.
44///
45/// Chained like every other line, so it cannot be swapped without breaking
46/// the fingerprint. A reader checks [`rite_transcript`](Self::rite_transcript)
47/// before parsing anything else and refuses a value it does not know.
48#[non_exhaustive]
49#[cfg_attr(test, derive(schemars::JsonSchema))]
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[cfg_attr(
52    test,
53    schemars(
54        description = "What the file is. A reader checks `rite_transcript` before reading \
55        anything else, and refuses a version it does not know."
56    )
57)]
58pub struct TranscriptHeader {
59    /// Format version of the envelope and chain rule ([`TRANSCRIPT_FORMAT`]).
60    #[cfg_attr(
61        test,
62        schemars(description = "Version of the line format and the chain rule.")
63    )]
64    pub rite_transcript: u32,
65    /// Version of the fact vocabulary ([`FACT_VOCABULARY`]).
66    #[cfg_attr(
67        test,
68        schemars(
69            description = "Version of the fact vocabulary: which fact types and fields the \
70            transcript uses."
71        )
72    )]
73    pub vocabulary: u32,
74    /// Program and version that wrote the transcript, as it reports itself.
75    /// Informational: a binary cannot vouch for its own identity.
76    #[cfg_attr(
77        test,
78        schemars(
79            description = "The program and version that wrote the transcript, as it reports \
80            itself. Informational: a program cannot vouch for its own identity."
81        )
82    )]
83    pub producer: String,
84    /// Random identifier of this run, 32 lowercase hex digits.
85    #[cfg_attr(
86        test,
87        schemars(
88            description = "A random identifier of this run: 32 lowercase hex digits.",
89            extend("pattern" = "^[0-9a-f]{32}$")
90        )
91    )]
92    pub run_id: String,
93    /// Whether the run was a dry run. A dry-run transcript is a rehearsal
94    /// record, never evidence of a ceremony.
95    #[cfg_attr(
96        test,
97        schemars(
98            description = "Whether the run was a dry run. A dry-run transcript is a rehearsal \
99            record, never evidence of a ceremony."
100        )
101    )]
102    pub dry_run: bool,
103    /// Every level this transcript uses, by name. Holds the built-in levels
104    /// at their fixed values, and any level the ceremony declares.
105    #[cfg_attr(
106        test,
107        schemars(
108            description = "Every level the transcript uses, by name. It holds public, \
109            restricted and confidential at their fixed values, and any other level the ceremony \
110            declares. No two names share a value."
111        )
112    )]
113    pub levels: BTreeMap<String, Level>,
114}
115
116impl TranscriptHeader {
117    /// A header for the current format and vocabulary.
118    #[must_use]
119    pub fn new(producer: &str, run_id: &str, dry_run: bool) -> Self {
120        Self {
121            rite_transcript: TRANSCRIPT_FORMAT,
122            vocabulary: FACT_VOCABULARY,
123            producer: producer.to_string(),
124            run_id: run_id.to_string(),
125            dry_run,
126            levels: Level::BUILT_IN
127                .iter()
128                .map(|(name, level)| ((*name).to_string(), *level))
129                .collect(),
130        }
131    }
132
133    /// Whether the header declares `level`.
134    #[must_use]
135    pub fn declares(&self, level: Level) -> bool {
136        self.levels.values().any(|declared| *declared == level)
137    }
138
139    /// A declared level, by the name the header gives it or by its number.
140    #[must_use]
141    pub fn level(&self, name_or_value: &str) -> Option<Level> {
142        if let Some(level) = self.levels.get(name_or_value) {
143            return Some(*level);
144        }
145        let level = Level::new(name_or_value.parse().ok()?);
146        self.declares(level).then_some(level)
147    }
148}
149
150/// Confidentiality level of a transcript line: who may see the fact.
151///
152/// An integer, ordered from widest audience to narrowest, so a disclosure is
153/// a threshold and checking it needs no names. The level is committed in the
154/// chain with the line. Secrets are not a level: a secret value is never
155/// recorded at all.
156///
157/// Three levels are built in, at fixed values that mean the same thing in
158/// every transcript. The gaps leave room for an organisation to declare its
159/// own levels between and beyond them; the header names every level a
160/// transcript uses.
161///
162/// | Level | Value | Audience | TLP 2.0 |
163/// |---|---|---|---|
164/// | `public` | 10 | anyone | CLEAR |
165/// | `restricted` | 20 | auditors, under agreement | AMBER |
166/// | `confidential` | 30 | the ceremony's own organisation | RED |
167#[cfg_attr(test, derive(schemars::JsonSchema))]
168#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
169#[serde(transparent)]
170#[cfg_attr(
171    test,
172    schemars(
173        description = "The confidentiality level of a line: who may see its fact. Levels are \
174        integers ordered from the widest audience to the narrowest, so a disclosure up to a level \
175        withholds every line above it. Three levels are built in at fixed values: public (10, \
176        anyone), restricted (20, auditors under agreement) and confidential (30, the ceremony's \
177        own organisation). Every level a transcript uses is declared in its header."
178    )
179)]
180pub struct Level(u32);
181
182impl Level {
183    /// Anyone.
184    pub const PUBLIC: Level = Level(10);
185    /// Auditors, under agreement.
186    pub const RESTRICTED: Level = Level(20);
187    /// The ceremony's own organisation.
188    pub const CONFIDENTIAL: Level = Level(30);
189
190    /// The built-in levels with their names, widest audience first.
191    pub const BUILT_IN: [(&'static str, Level); 3] = [
192        ("public", Level::PUBLIC),
193        ("restricted", Level::RESTRICTED),
194        ("confidential", Level::CONFIDENTIAL),
195    ];
196
197    /// A level at `value`.
198    #[must_use]
199    pub const fn new(value: u32) -> Self {
200        Self(value)
201    }
202
203    /// The level's value, as written in the transcript and committed in the
204    /// chain.
205    #[must_use]
206    pub const fn value(self) -> u32 {
207        self.0
208    }
209}
210
211impl std::fmt::Display for Level {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        match Level::BUILT_IN.iter().find(|(_, level)| level == self) {
214            Some((name, _)) => f.write_str(name),
215            None => write!(f, "level {}", self.0),
216        }
217    }
218}
219
220/// Validator applied by the runtime to a typed response before it is
221/// accepted.
222///
223/// The rule is part of the prompt, so it is recorded with it: a transcript
224/// says a six-digit secret was entered, never which one. [`check`](Self::check)
225/// is the one place a rule is applied, so `rite check` and the running
226/// ceremony agree on what a pattern accepts.
227#[non_exhaustive]
228#[cfg_attr(test, derive(schemars::JsonSchema))]
229#[derive(Debug, Clone, Serialize, Deserialize)]
230#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
231#[cfg_attr(test, schemars(description = "The check an answer had to pass."))]
232pub enum ValidatorSpec {
233    /// Reject empty or whitespace-only input.
234    #[cfg_attr(test, schemars(description = "The answer is not empty or blank."))]
235    NonEmpty,
236    /// Input must match this regular expression in full.
237    #[cfg_attr(
238        test,
239        schemars(description = "The answer matches a regular expression in full.")
240    )]
241    Regex(#[cfg_attr(test, schemars(description = "The regular expression."))] String),
242    /// Named, runtime-defined predicate (e.g. `serial_number`).
243    #[cfg_attr(
244        test,
245        schemars(description = "The answer passes a check the program defines, by name.")
246    )]
247    Predefined(
248        #[cfg_attr(
249            test,
250            schemars(description = "The name of the check, such as serial_number.")
251        )]
252        String,
253    ),
254    /// A value of one [`Format`], with a length within bounds.
255    ///
256    /// What a PIN or a key component needs, stated without a pattern the
257    /// person writing the ceremony has to get right. The length is counted
258    /// in the format's own units: characters for text, bytes for an
259    /// encoding.
260    #[cfg_attr(
261        test,
262        schemars(
263            description = "The answer is a value of one format, with a length within \
264                                 bounds, counted in characters for text and in bytes for an \
265                                 encoding."
266        )
267    )]
268    Format {
269        /// What kind of value this is.
270        #[cfg_attr(test, schemars(description = "What kind of value the answer is."))]
271        format: Format,
272        /// Fewest units accepted, if bounded.
273        #[cfg_attr(test, schemars(description = "The fewest units accepted, if bounded."))]
274        min_length: Option<usize>,
275        /// Most units accepted, if bounded.
276        #[cfg_attr(test, schemars(description = "The most units accepted, if bounded."))]
277        max_length: Option<usize>,
278    },
279}
280
281/// What kind of value a person types.
282///
283/// Each format has one canonical representation, which is what an entry step
284/// keeps: the text as typed for a text format, the decoded bytes for an
285/// encoding. An encoding is forgiving of the grouping a person types it in,
286/// so whitespace is dropped before decoding.
287#[non_exhaustive]
288#[cfg_attr(test, derive(schemars::JsonSchema))]
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
290#[serde(rename_all = "snake_case")]
291#[cfg_attr(test, schemars(description = "What kind of value a person types."))]
292pub enum Format {
293    /// Anything the person can type.
294    #[cfg_attr(test, schemars(description = "Anything the person can type."))]
295    Text,
296    /// `0` to `9`.
297    #[cfg_attr(test, schemars(description = "Decimal digits only."))]
298    Digits,
299    /// ASCII letters and digits.
300    #[cfg_attr(test, schemars(description = "ASCII letters and digits."))]
301    Alphanumeric,
302    /// Bytes as hexadecimal, in either case, two digits per byte.
303    #[cfg_attr(
304        test,
305        schemars(description = "Bytes as hexadecimal, in either case, two digits per byte.")
306    )]
307    Hex,
308    /// Bytes as standard base64 with padding, as `openssl base64` writes it.
309    #[cfg_attr(test, schemars(description = "Bytes as standard base64 with padding."))]
310    Base64,
311}
312
313impl Format {
314    /// Whether the value stands for bytes, which is what the step then keeps.
315    #[must_use]
316    pub fn is_encoding(self) -> bool {
317        match self {
318            Format::Text | Format::Digits | Format::Alphanumeric => false,
319            Format::Hex | Format::Base64 => true,
320        }
321    }
322
323    /// Whether a text format accepts this character. An encoding accepts
324    /// whatever decodes.
325    fn accepts(self, c: char) -> bool {
326        match self {
327            Format::Text | Format::Hex | Format::Base64 => true,
328            Format::Digits => c.is_ascii_digit(),
329            Format::Alphanumeric => c.is_ascii_alphanumeric(),
330        }
331    }
332
333    /// Decode a typed value of an encoding.
334    ///
335    /// # Errors
336    ///
337    /// Returns why the value is not this encoding, or that the format is not
338    /// one, worded for the person who typed it and naming nothing of what
339    /// they typed.
340    pub fn decode(self, value: &str) -> Result<Vec<u8>, String> {
341        // The value may be a secret, so the copy without its whitespace is
342        // wiped with the call.
343        let compact: Zeroizing<String> =
344            Zeroizing::new(value.chars().filter(|c| !c.is_whitespace()).collect());
345        match self {
346            Format::Hex => base16ct::mixed::decode_vec(compact.as_bytes())
347                .map_err(|_| "value must be hex, two digits per byte".to_string()),
348            Format::Base64 => base64ct::Base64::decode_vec(&compact)
349                .map_err(|_| "value must be standard base64, with padding".to_string()),
350            Format::Text | Format::Digits | Format::Alphanumeric => {
351                Err(format!("{} is text and does not decode", self.describe()))
352            }
353        }
354    }
355
356    /// The unit a length counts, as a person reads it in a hint.
357    #[must_use]
358    pub fn describe(self) -> &'static str {
359        match self {
360            Format::Text => "characters",
361            Format::Digits => "digits",
362            Format::Alphanumeric => "letters or digits",
363            Format::Hex => "bytes as hex",
364            Format::Base64 => "bytes as base64",
365        }
366    }
367
368    /// A stand-in of `length` units that satisfies the format, for a run
369    /// with no one at the keyboard. Never a real value.
370    ///
371    /// `None` above [`PLACEHOLDER_LIMIT`]: a length a ceremony declares is
372    /// not bounded, and a stand-in that size would be allocated for nothing.
373    /// The caller then declines to answer rather than answering with
374    /// something the rule refuses, which would be asked again without end.
375    #[must_use]
376    pub fn placeholder(self, length: usize) -> Option<String> {
377        if length > PLACEHOLDER_LIMIT {
378            return None;
379        }
380        Some(match self {
381            Format::Text | Format::Alphanumeric => "x".repeat(length),
382            Format::Digits => "0".repeat(length),
383            Format::Hex => base16ct::lower::encode_string(&vec![0u8; length]),
384            Format::Base64 => base64ct::Base64::encode_string(&vec![0u8; length]),
385        })
386    }
387}
388
389/// The longest stand-in a [`Format::placeholder`] builds, in the format's
390/// units. Large enough for any value a person would type or paste, and the
391/// entry itself is not bounded by it.
392pub const PLACEHOLDER_LIMIT: usize = 64 * 1024;
393
394impl std::str::FromStr for Format {
395    type Err = String;
396
397    fn from_str(s: &str) -> Result<Self, Self::Err> {
398        match s {
399            "text" => Ok(Format::Text),
400            "digits" => Ok(Format::Digits),
401            "alphanumeric" => Ok(Format::Alphanumeric),
402            "hex" => Ok(Format::Hex),
403            "base64" => Ok(Format::Base64),
404            other => Err(format!(
405                "unknown format '{other}': expected text, digits, alphanumeric, hex or base64"
406            )),
407        }
408    }
409}
410
411impl ValidatorSpec {
412    /// Apply the rule to a typed value.
413    ///
414    /// # Errors
415    ///
416    /// Returns what the value fails to satisfy, worded for the person who
417    /// typed it, or why the rule itself cannot be applied.
418    pub fn check(&self, value: &str) -> Result<(), String> {
419        match self {
420            ValidatorSpec::NonEmpty => {
421                if value.trim().is_empty() {
422                    Err("value must not be empty".to_string())
423                } else {
424                    Ok(())
425                }
426            }
427            ValidatorSpec::Regex(pattern) => {
428                let regex = compile_pattern(pattern)?;
429                if regex.is_match(value) {
430                    Ok(())
431                } else {
432                    Err(format!("value must match {pattern}"))
433                }
434            }
435            // Named predicates will land alongside the actions that need them.
436            ValidatorSpec::Predefined(name) => Err(format!("unknown validator: {name}")),
437            ValidatorSpec::Format {
438                format,
439                min_length,
440                max_length,
441            } => {
442                let hint = self.hint().unwrap_or_default();
443                if value.trim().is_empty() {
444                    return Err("value must not be empty".to_string());
445                }
446                // An encoding is decoded and discarded, wiped on the way out:
447                // the value may be a secret, and this runs before the step
448                // holds it.
449                let length = if format.is_encoding() {
450                    Zeroizing::new(format.decode(value)?).len()
451                } else {
452                    if !value.chars().all(|c| format.accepts(c)) {
453                        return Err(format!("value must be {hint}"));
454                    }
455                    value.chars().count()
456                };
457                if min_length.is_some_and(|min| length < min)
458                    || max_length.is_some_and(|max| length > max)
459                {
460                    return Err(format!("value must be {hint}, this is {length}"));
461                }
462                Ok(())
463            }
464        }
465    }
466
467    /// The rule as a person reads it before typing, if there is one worth
468    /// stating.
469    ///
470    /// `NonEmpty` says nothing: every prompt asks for something. A pattern is
471    /// shown as written, since a regular expression has no better rendering.
472    #[must_use]
473    pub fn hint(&self) -> Option<String> {
474        match self {
475            ValidatorSpec::NonEmpty | ValidatorSpec::Predefined(_) => None,
476            ValidatorSpec::Regex(pattern) => Some(format!("matching {pattern}")),
477            ValidatorSpec::Format {
478                format,
479                min_length,
480                max_length,
481            } => {
482                let what = format.describe();
483                Some(match (min_length, max_length) {
484                    (Some(min), Some(max)) if min == max => format!("{min} {what}"),
485                    (Some(min), Some(max)) => format!("{min} to {max} {what}"),
486                    (Some(min), None) => format!("at least {min} {what}"),
487                    (None, Some(max)) => format!("at most {max} {what}"),
488                    (None, None) if format.is_encoding() => what.to_string(),
489                    (None, None) => format!("{what} only"),
490                })
491            }
492        }
493    }
494}
495
496/// Compile a pattern so that it must match the whole value.
497///
498/// A rule that read `[0-9]+` and accepted `abc123def` would pass what its
499/// author meant to refuse, so the anchors are always supplied here rather
500/// than expected of the author.
501///
502/// # Errors
503///
504/// Returns why the pattern is not a regular expression.
505pub fn compile_pattern(pattern: &str) -> Result<regex_lite::Regex, String> {
506    regex_lite::Regex::new(&format!("^(?:{pattern})$"))
507        .map_err(|e| format!("invalid pattern '{pattern}': {e}"))
508}
509
510/// Request for user input, recorded into the transcript as part of
511/// [`StepFact::PromptAnswered`].
512#[non_exhaustive]
513#[cfg_attr(test, derive(schemars::JsonSchema))]
514#[derive(Debug, Clone, Serialize, Deserialize)]
515#[serde(tag = "type", rename_all = "snake_case")]
516#[cfg_attr(test, schemars(description = "A prompt as it was shown."))]
517pub enum Prompt {
518    /// Yes / no question with an optional default.
519    #[cfg_attr(test, schemars(description = "A yes or no question."))]
520    Confirm {
521        /// Question shown to the user.
522        #[cfg_attr(test, schemars(description = "The question."))]
523        question: String,
524        /// Default selection if the user presses Enter without choosing.
525        #[cfg_attr(
526            test,
527            schemars(
528                description = "The answer given when the person confirms without choosing, if \
529                any."
530            )
531        )]
532        default: Option<bool>,
533    },
534    /// Free-form text input, validated against a [`ValidatorSpec`].
535    #[cfg_attr(
536        test,
537        schemars(description = "A request for free text, checked before it is accepted.")
538    )]
539    Text {
540        /// Label shown to the user.
541        #[cfg_attr(test, schemars(description = "The label shown."))]
542        label: String,
543        /// Validator applied before the response is accepted.
544        #[cfg_attr(test, schemars(description = "The check the answer had to pass."))]
545        validator: ValidatorSpec,
546    },
547    /// Sensitive input (PIN, password). Echo is suppressed; plaintext is
548    /// never serialized to the transcript.
549    #[cfg_attr(
550        test,
551        schemars(
552            description = "A request for a secret, such as a PIN. The answer is never \
553            recorded."
554        )
555    )]
556    Secret {
557        /// Label shown to the user.
558        #[cfg_attr(test, schemars(description = "The label shown."))]
559        label: String,
560        /// Validator applied before the response is accepted.
561        #[cfg_attr(test, schemars(description = "The check the answer had to pass."))]
562        validator: ValidatorSpec,
563    },
564    /// User must type a specific literal string exactly. Validation is
565    /// performed by the runtime against `expected`.
566    #[cfg_attr(
567        test,
568        schemars(
569            description = "A request to type a given text exactly, such as a confirmation \
570            phrase."
571        )
572    )]
573    Literal {
574        /// Label shown to the user.
575        #[cfg_attr(test, schemars(description = "The label shown."))]
576        label: String,
577        /// Exact string the user must type.
578        #[cfg_attr(test, schemars(description = "The text that had to be typed."))]
579        expected: String,
580    },
581    /// Wait for the user to acknowledge before proceeding. Used for pacing.
582    #[cfg_attr(
583        test,
584        schemars(description = "A pause until the person is ready to continue.")
585    )]
586    Continue {
587        /// Optional hint such as "Press Enter when ready".
588        #[cfg_attr(test, schemars(description = "The hint shown, if any."))]
589        hint: Option<String>,
590    },
591}
592
593/// Serializable, redacted form of a user response.
594///
595/// Used inside [`StepFact::PromptAnswered`] so that the transcript records
596/// what was answered without ever persisting plaintext secrets. The
597/// in-flight `Response` type lives next to the channel protocol in
598/// `rite-runtime`; conversion happens at the moment the prompt is accepted.
599#[non_exhaustive]
600#[cfg_attr(test, derive(schemars::JsonSchema))]
601#[derive(Debug, Clone, Serialize, Deserialize)]
602#[serde(tag = "type", rename_all = "snake_case")]
603#[cfg_attr(
604    test,
605    schemars(
606        description = "The answer to a prompt, as recorded. A secret answer is never \
607        recorded, not even as a digest."
608    )
609)]
610pub enum ResponseRecord {
611    /// Yes / no answer.
612    #[cfg_attr(test, schemars(description = "A yes or no answer."))]
613    Bool {
614        /// The answer.
615        #[cfg_attr(test, schemars(description = "The answer."))]
616        value: bool,
617    },
618    /// Free-form text answer.
619    #[cfg_attr(test, schemars(description = "A free-text answer."))]
620    Text {
621        /// The answer.
622        #[cfg_attr(test, schemars(description = "The answer, as typed."))]
623        value: String,
624    },
625    /// Secret answer. The plaintext is never stored, and no digest of it is
626    /// kept either: a hash of a low-entropy secret (such as a 6-8 digit PIV
627    /// PIN) is brute-forceable from a shared transcript. The position of the
628    /// enclosing `PromptAnswered` fact in the chain already records that a
629    /// secret was entered at this point.
630    // Note: a salted, per-run HMAC could later attest that two prompts received
631    // the same secret without reintroducing the low-entropy guessing oracle.
632    // Deferred until a concrete use case needs it.
633    #[cfg_attr(
634        test,
635        schemars(
636            description = "A secret was entered. Only the fact that it was entered is \
637            recorded."
638        )
639    )]
640    SecretRedacted {},
641    /// Acknowledgement of a [`Prompt::Continue`].
642    #[cfg_attr(test, schemars(description = "The person continued past a pause."))]
643    Acknowledged,
644}
645
646/// Audit classification of a bad outcome, recorded so an auditor can tell the
647/// nature of a failure apart without parsing the free-form `message`.
648///
649/// This is the *audit* taxonomy (what an auditor sees), distinct from the
650/// runtime's `Retriability` (whether a step may re-run). For a backend error
651/// the two align: a retriable error is `Environmental`. They are kept separate
652/// because some classes never map cleanly onto retriability (an `Abort` is a
653/// decision, not an error).
654#[non_exhaustive]
655#[cfg_attr(test, derive(schemars::JsonSchema))]
656#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
657#[serde(rename_all = "snake_case")]
658#[cfg_attr(test, schemars(description = "What kind of bad outcome this was."))]
659pub enum ErrorClass {
660    /// The world wasn't ready; the step's work did not happen (token absent,
661    /// loose cable, PIN required).
662    #[cfg_attr(
663        test,
664        schemars(
665            description = "Something around the ceremony was not ready, such as a device not \
666            connected, and the step's work did not happen."
667        )
668    )]
669    Environmental,
670    /// The ceremony's own logic concluded badly (a verification mismatch, a
671    /// refused attestation). A result, not a recoverable condition.
672    #[cfg_attr(
673        test,
674        schemars(
675            description = "The ceremony's own logic concluded badly, such as a verification \
676            that did not match or a refused attestation."
677        )
678    )]
679    Procedural,
680    /// The run itself is compromised or the definition is broken (transcript
681    /// write failed, channel lost, unknown action, invalid params).
682    #[cfg_attr(
683        test,
684        schemars(
685            description = "The run itself cannot be trusted to continue, or the ceremony \
686            definition is broken."
687        )
688    )]
689    Integrity,
690    /// The operator chose to stop. Not an error at all, but recorded on the
691    /// terminal fact so abort is distinguishable from failure.
692    #[cfg_attr(
693        test,
694        schemars(description = "An operator chose to stop the ceremony.")
695    )]
696    Abort,
697}
698
699/// Structured error record for transcript serialization.
700#[non_exhaustive]
701#[cfg_attr(test, derive(schemars::JsonSchema))]
702#[derive(Debug, Clone, Serialize, Deserialize)]
703#[cfg_attr(
704    test,
705    schemars(
706        description = "What went wrong: a class for audit, a stable kind, and a message for \
707        people."
708    )
709)]
710pub struct ErrorRecord {
711    /// Audit classification of this error.
712    #[cfg_attr(test, schemars(description = "The kind of bad outcome, for audit."))]
713    pub class: ErrorClass,
714    /// Stable kind label (e.g. `aborted`, `step_failed`, `material_load_failed`).
715    #[cfg_attr(
716        test,
717        schemars(description = "A stable label for the error, such as aborted or step_failed.")
718    )]
719    pub kind: String,
720    /// Human-readable message.
721    #[cfg_attr(test, schemars(description = "A description of the error for people."))]
722    pub message: String,
723}
724
725impl ErrorRecord {
726    /// Construct an error record.
727    pub fn new(class: ErrorClass, kind: impl Into<String>, message: impl Into<String>) -> Self {
728        Self {
729            class,
730            kind: kind.into(),
731            message: message.into(),
732        }
733    }
734}
735
736/// Outcome of a single step, carried by [`StepFact::StepCompleted`].
737#[non_exhaustive]
738#[cfg_attr(test, derive(schemars::JsonSchema))]
739#[derive(Debug, Clone, Serialize, Deserialize)]
740#[serde(tag = "status", rename_all = "snake_case")]
741#[cfg_attr(test, schemars(description = "How a step ended."))]
742pub enum StepOutcome {
743    /// Step executed successfully.
744    #[cfg_attr(test, schemars(description = "The step did its work."))]
745    Completed {
746        /// Human-readable completion message.
747        #[cfg_attr(
748            test,
749            schemars(description = "A short description of what the step did.")
750        )]
751        message: String,
752    },
753}
754
755/// Durable, transcript-worthy fact.
756///
757/// Every variant is recorded by the runtime's transcript sink synchronously
758/// before being forwarded to the UI. Action handlers emit
759/// [`BackendOperation`] and [`AttestationRecorded`]; all other variants are
760/// emitted by the executor at the corresponding lifecycle boundary.
761///
762/// [`BackendOperation`]: StepFact::BackendOperation
763/// [`AttestationRecorded`]: StepFact::AttestationRecorded
764#[non_exhaustive]
765#[cfg_attr(test, derive(schemars::JsonSchema))]
766#[derive(Debug, Clone, Serialize, Deserialize)]
767#[serde(tag = "type", rename_all = "snake_case")]
768#[cfg_attr(
769    test,
770    schemars(
771        description = "One recorded fact, identified by its `type`. Each fact type has a \
772        fixed set of fields in a given vocabulary."
773    )
774)]
775pub enum StepFact {
776    /// Ceremony has started running.
777    #[cfg_attr(test, schemars(description = "The ceremony started."))]
778    CeremonyStarted {
779        /// Ceremony name from the DSL.
780        #[cfg_attr(test, schemars(description = "The ceremony's name."))]
781        name: String,
782        /// Digest of the ceremony YAML the run was resolved from. The inputs
783        /// supplied for this run are recorded as their own facts, one each,
784        /// so this digest covers the template only.
785        #[cfg_attr(
786            test,
787            schemars(
788                description = "Digest of the ceremony definition file the run was resolved \
789                from. The inputs of the run are recorded as their own facts, so this digest covers \
790                the definition only."
791            )
792        )]
793        template: Sha256Digest,
794    },
795    /// A role the ceremony defines, with its display name.
796    ///
797    /// Recorded once per role at ceremony start, whether or not anyone is
798    /// assigned to it. Later facts name the role by id only.
799    #[cfg_attr(
800        test,
801        schemars(
802            description = "A role the ceremony defines. Recorded once per role at the start, \
803            whether or not anyone is assigned to it; later facts name the role by its identifier."
804        )
805    )]
806    RoleDeclared {
807        /// Role identifier.
808        #[cfg_attr(test, schemars(description = "The role."))]
809        role: RoleId,
810        /// Human-readable role name, from the ceremony's role definition.
811        #[cfg_attr(test, schemars(description = "The role's display name."))]
812        name: String,
813    },
814    /// A person was assigned to a role for this run.
815    ///
816    /// One fact per assignment, so each can be withheld on its own.
817    #[cfg_attr(
818        test,
819        schemars(
820            description = "A person was assigned to a role for this run. One fact per \
821            assignment, so each can be withheld on its own."
822        )
823    )]
824    RoleAssigned {
825        /// Role identifier.
826        #[cfg_attr(test, schemars(description = "The role."))]
827        role: RoleId,
828        /// The person, as supplied at run time. Recorded as given; nothing
829        /// in the ceremony proves who the person is.
830        #[cfg_attr(
831            test,
832            schemars(
833                description = "The person, as supplied when the run started. Recorded as \
834                given: nothing in the transcript proves who the person is."
835            )
836        )]
837        person: String,
838    },
839    /// A parameter took its value for this run, supplied or defaulted.
840    #[cfg_attr(
841        test,
842        schemars(description = "A parameter took its value for this run, supplied or defaulted.")
843    )]
844    ParameterBound {
845        /// Parameter identifier.
846        #[cfg_attr(test, schemars(description = "The parameter."))]
847        name: ParamId,
848        /// The resolved value.
849        #[cfg_attr(test, schemars(description = "The value, as the ceremony used it."))]
850        value: serde_json::Value,
851    },
852    /// A material was loaded at ceremony start.
853    ///
854    /// The digest of a digital material is a separate fact,
855    /// [`MaterialDigest`](StepFact::MaterialDigest), so the two can be
856    /// disclosed to different audiences.
857    #[cfg_attr(
858        test,
859        schemars(
860            description = "A material was loaded at the start of the run. The digest of a \
861            digital material is a separate fact, material_digest, so the two can be disclosed to \
862            different audiences."
863        )
864    )]
865    MaterialLoaded {
866        /// Material identifier.
867        #[cfg_attr(test, schemars(description = "The material."))]
868        name: MaterialId,
869        /// Identifier of a physical or pre-provisioned material, such as a
870        /// serial number, when the ceremony supplied one.
871        #[serde(skip_serializing_if = "Option::is_none")]
872        #[cfg_attr(
873            test,
874            schemars(
875                description = "The identifier of a physical or pre-provisioned material, such \
876                as a serial number, when one was supplied."
877            )
878        )]
879        identifier: Option<String>,
880    },
881    /// The digest of a digital material's bytes.
882    #[cfg_attr(
883        test,
884        schemars(description = "The digest of a digital material's bytes, as loaded.")
885    )]
886    MaterialDigest {
887        /// Material identifier.
888        #[cfg_attr(test, schemars(description = "The material."))]
889        name: MaterialId,
890        /// Digest of the file as loaded.
891        #[cfg_attr(test, schemars(description = "The digest of the material's bytes."))]
892        digest: Sha256Digest,
893    },
894    /// A backend was first acquired in this run.
895    ///
896    /// Recorded once per backend, before the first operation on it. Backend
897    /// operations name the backend and do not repeat its identity, which can
898    /// carry device serials.
899    #[cfg_attr(
900        test,
901        schemars(
902            description = "A backend was used for the first time in this run. Recorded once \
903            per backend, before its first operation; operations name the backend and do not repeat \
904            its identity."
905        )
906    )]
907    BackendBound {
908        /// Backend name from the ceremony.
909        #[cfg_attr(test, schemars(description = "The backend's name in the ceremony."))]
910        name: String,
911        /// Backend provider (e.g. `openssl`, `yubikey`, `pkcs11`).
912        #[cfg_attr(
913            test,
914            schemars(description = "The kind of backend, such as openssl, yubikey or pkcs11.")
915        )]
916        provider: String,
917        /// Identity the backend reports for itself, such as a device serial
918        /// and firmware version.
919        #[cfg_attr(
920            test,
921            schemars(
922                description = "The identity the backend reports for itself, such as a device \
923                serial number and firmware version."
924            )
925        )]
926        identity: String,
927    },
928    /// A snapshot of the machine the ceremony ran on.
929    #[cfg_attr(
930        test,
931        schemars(description = "A description of the machine the ceremony ran on.")
932    )]
933    MachineInfoRecorded {
934        /// Step that captured the snapshot.
935        #[cfg_attr(test, schemars(description = "The step that recorded it."))]
936        step: StepId,
937        /// The parts of the host snapshot the step was asked to record.
938        #[cfg_attr(
939            test,
940            schemars(
941                description = "The parts of the machine description the step was asked to \
942                record."
943            )
944        )]
945        info: serde_json::Value,
946    },
947    /// Beginning of an act (named subdivision of a ceremony).
948    #[cfg_attr(
949        test,
950        schemars(description = "An act, a named part of the ceremony, started.")
951    )]
952    ActStarted {
953        /// Act identifier.
954        #[cfg_attr(test, schemars(description = "The act."))]
955        id: ActId,
956        /// Act label as authored in the DSL.
957        #[cfg_attr(
958            test,
959            schemars(description = "The act's label, as written in the ceremony.")
960        )]
961        label: String,
962    },
963    /// Beginning of a step.
964    #[cfg_attr(test, schemars(description = "A step started."))]
965    StepStarted {
966        /// Step identifier.
967        #[cfg_attr(test, schemars(description = "The step."))]
968        id: StepId,
969        /// Step label as authored in the DSL.
970        #[cfg_attr(
971            test,
972            schemars(description = "The step's label, as written in the ceremony.")
973        )]
974        label: String,
975        /// Role responsible for this step. Its name is on the role's
976        /// [`RoleDeclared`](StepFact::RoleDeclared) fact.
977        #[cfg_attr(
978            test,
979            schemars(
980                description = "The role responsible for the step. Its display name is on the \
981                role's role_declared fact."
982            )
983        )]
984        role: RoleId,
985    },
986    /// A prompt has been answered and validated.
987    #[cfg_attr(
988        test,
989        schemars(description = "Someone answered a prompt, and the answer was accepted.")
990    )]
991    PromptAnswered {
992        /// Step that issued the prompt, if any. `None` for ceremony-level
993        /// prompts emitted before the first step (e.g. the ceremony-start
994        /// confirmation) or after the last step.
995        #[serde(skip_serializing_if = "Option::is_none")]
996        #[cfg_attr(
997            test,
998            schemars(
999                description = "The step that asked, or absent for a prompt asked outside any \
1000                step."
1001            )
1002        )]
1003        step: Option<StepId>,
1004        /// The prompt as issued.
1005        #[cfg_attr(test, schemars(description = "The prompt, as shown."))]
1006        prompt: Prompt,
1007        /// Redacted response record.
1008        #[cfg_attr(test, schemars(description = "The answer, as recorded."))]
1009        response: ResponseRecord,
1010    },
1011    /// A backend operation completed and produced structured evidence.
1012    #[cfg_attr(
1013        test,
1014        schemars(
1015            description = "A backend performed an operation. The inputs and outputs are \
1016            specific to the kind of operation."
1017        )
1018    )]
1019    BackendOperation {
1020        /// Step under which the operation ran.
1021        #[cfg_attr(test, schemars(description = "The step the operation ran in."))]
1022        step: StepId,
1023        /// Stable operation kind (e.g. `generate_key`, `sign_data`).
1024        #[cfg_attr(
1025            test,
1026            schemars(description = "The kind of operation, such as generate_key or sign_data.")
1027        )]
1028        kind: String,
1029        /// The backend the step ran with, by the name the ceremony gives it;
1030        /// its identity is on that backend's [`StepFact::BackendBound`].
1031        /// Absent for an operation done in software.
1032        #[serde(skip_serializing_if = "Option::is_none")]
1033        #[cfg_attr(
1034            test,
1035            schemars(
1036                description = "The backend the step ran with, by the name the ceremony gives it.                 Its identity is on that backend's backend_bound fact. Absent for an operation                 done in software."
1037            )
1038        )]
1039        backend: Option<String>,
1040        /// Structured inputs to the operation (parameters, references).
1041        #[cfg_attr(
1042            test,
1043            schemars(
1044                description = "What the operation was given: parameters and references to \
1045                artifacts or materials."
1046            )
1047        )]
1048        inputs: serde_json::Value,
1049        /// Structured outputs from the operation (artifact ids, hashes).
1050        #[cfg_attr(
1051            test,
1052            schemars(description = "What the operation produced: artifact names and digests.")
1053        )]
1054        outputs: serde_json::Value,
1055        /// Optional fingerprint of the produced material.
1056        #[cfg_attr(
1057            test,
1058            schemars(
1059                description = "A fingerprint of the material the operation produced, when it \
1060                has one."
1061            )
1062        )]
1063        fingerprint: Option<String>,
1064    },
1065    /// A key held outside the ceremony was used as the recipient of a wrap.
1066    ///
1067    /// Its own fact rather than a field on the wrap operation: the method of a
1068    /// wrap is publishable and the identity of a custodian may not be, and a
1069    /// fact carries one confidentiality level.
1070    ///
1071    /// The fingerprint records the bytes that were present. Nothing in the
1072    /// ceremony corroborates that they belong to the intended recipient, and
1073    /// nothing shows the recipient can open the result. The fact states an
1074    /// assumption the ceremony rests on, not something Rite proved.
1075    #[cfg_attr(
1076        test,
1077        schemars(
1078            description = "A key held outside the ceremony received a wrapped key. A separate \
1079            fact from the operation, so the method of a wrap can be published while the recipient \
1080            stays withheld."
1081        )
1082    )]
1083    WrapRecipientRecorded {
1084        /// Step that wrapped to this recipient.
1085        #[cfg_attr(test, schemars(description = "The step that wrapped the key."))]
1086        step: StepId,
1087        /// The artifact or material the recipient key came from, as named in
1088        /// the ceremony.
1089        #[cfg_attr(
1090            test,
1091            schemars(
1092                description = "The artifact or material the recipient's key came from, by its \
1093                name in the ceremony."
1094            )
1095        )]
1096        source: String,
1097        /// `sha256:<hex>` over the recipient's SPKI DER.
1098        #[cfg_attr(
1099            test,
1100            schemars(
1101                description = "The digest of the recipient's public key, as DER-encoded \
1102                SubjectPublicKeyInfo.",
1103                with = "Sha256Digest"
1104            )
1105        )]
1106        fingerprint: String,
1107        /// Whether the ceremony declared this fingerprint in advance, so the
1108        /// runtime checked the key it received against the definition rather
1109        /// than recording whatever arrived.
1110        #[cfg_attr(
1111            test,
1112            schemars(
1113                description = "Whether the ceremony named this fingerprint in advance. When \
1114                it did, the key received was checked against it; otherwise the fingerprint records \
1115                whatever key arrived."
1116            )
1117        )]
1118        declared: bool,
1119    },
1120    /// A human attestation was recorded.
1121    #[cfg_attr(test, schemars(description = "A person made an attestation."))]
1122    AttestationRecorded {
1123        /// Step under which the attestation was recorded.
1124        #[cfg_attr(test, schemars(description = "The step the attestation was made in."))]
1125        step: StepId,
1126        /// Role that issued the attestation.
1127        #[cfg_attr(test, schemars(description = "The role that made the attestation."))]
1128        role: RoleId,
1129        /// Verbatim attestation statement.
1130        #[cfg_attr(test, schemars(description = "The statement, word for word."))]
1131        statement: String,
1132    },
1133    /// An artifact was written to the run's `artifacts/` directory.
1134    #[cfg_attr(
1135        test,
1136        schemars(description = "An artifact was written to the run's artifacts directory.")
1137    )]
1138    ArtifactWritten {
1139        /// Step that produced the artifact.
1140        #[cfg_attr(test, schemars(description = "The step that produced the artifact."))]
1141        step: StepId,
1142        /// Artifact name as declared in the DSL.
1143        #[cfg_attr(
1144            test,
1145            schemars(description = "The artifact's name, as declared in the ceremony.")
1146        )]
1147        name: String,
1148        /// File name under `artifacts/`: one path component, no directory.
1149        #[cfg_attr(
1150            test,
1151            schemars(
1152                description = "The file name in the artifacts directory: a single path \
1153                component."
1154            )
1155        )]
1156        file: String,
1157        /// Digest of the file's bytes. Absent when the artifact is content a
1158        /// step opened: a digest of a secret can be tested against guesses,
1159        /// so none is recorded.
1160        #[serde(skip_serializing_if = "Option::is_none")]
1161        #[cfg_attr(
1162            test,
1163            schemars(
1164                description = "The digest of the file's bytes. Absent for content a step \
1165                opened, since a digest of a secret can be tested against guesses."
1166            )
1167        )]
1168        digest: Option<Sha256Digest>,
1169    },
1170    /// A deviation was logged by the operator.
1171    #[cfg_attr(test, schemars(description = "An operator recorded a deviation."))]
1172    DeviationRecorded {
1173        /// Step in which the deviation was logged, if any. `None` for
1174        /// deviations logged outside of a step (before the first step or
1175        /// while a ceremony-level prompt is pending).
1176        #[serde(skip_serializing_if = "Option::is_none")]
1177        #[cfg_attr(
1178            test,
1179            schemars(
1180                description = "The step during which the deviation was recorded, or absent \
1181                outside any step."
1182            )
1183        )]
1184        step: Option<StepId>,
1185        /// Verbatim deviation text.
1186        #[cfg_attr(test, schemars(description = "The deviation, word for word."))]
1187        text: String,
1188    },
1189    /// A step attempt failed. Recorded per attempt, so a retried step shows
1190    /// `StepAttemptFailed{attempt: 1}` followed by the operator's retry
1191    /// decision and, on success, `StepCompleted`. The final attempt of a step
1192    /// that the run gives up on is followed by the terminal `CeremonyFailed`.
1193    #[cfg_attr(
1194        test,
1195        schemars(
1196            description = "One attempt at a step failed. Recorded per attempt: a step that is \
1197            retried and then succeeds has this fact for each failed attempt, then step_completed."
1198        )
1199    )]
1200    StepAttemptFailed {
1201        /// Step whose attempt failed.
1202        #[cfg_attr(test, schemars(description = "The step."))]
1203        step: StepId,
1204        /// 1-based attempt number within this step.
1205        #[cfg_attr(
1206            test,
1207            schemars(description = "The attempt's number within the step, starting at 1.")
1208        )]
1209        attempt: u32,
1210        /// Structured error record for the failed attempt.
1211        #[cfg_attr(test, schemars(description = "What went wrong in this attempt."))]
1212        error: ErrorRecord,
1213    },
1214    /// Step finished executing.
1215    #[cfg_attr(test, schemars(description = "A step finished."))]
1216    StepCompleted {
1217        /// Step identifier.
1218        #[cfg_attr(test, schemars(description = "The step."))]
1219        id: StepId,
1220        /// How the step ended.
1221        #[cfg_attr(test, schemars(description = "How the step ended."))]
1222        outcome: StepOutcome,
1223    },
1224    /// Ceremony finished successfully.
1225    ///
1226    /// The transcript's fingerprint is the `chain` value of this line, which
1227    /// any reader recomputes, so the fact itself carries no fingerprint
1228    /// field. The runtime forwards the value to frontends through an
1229    /// out-of-band channel event.
1230    #[cfg_attr(
1231        test,
1232        schemars(
1233            description = "The ceremony finished. It is the last line of a transcript whose \
1234            run completed."
1235        )
1236    )]
1237    CeremonyCompleted {},
1238    /// Ceremony failed or was aborted.
1239    #[cfg_attr(
1240        test,
1241        schemars(
1242            description = "The ceremony failed or was aborted. It is the last line of a \
1243            transcript whose run did not complete."
1244        )
1245    )]
1246    CeremonyFailed {
1247        /// Structured error record.
1248        #[cfg_attr(test, schemars(description = "What went wrong."))]
1249        error: ErrorRecord,
1250    },
1251    /// The ceremony entropy source was seeded with machine randomness.
1252    ///
1253    /// Emitted once by the runner at ceremony start (run-metadata). Records
1254    /// the machine contribution `m` and the frozen derivation scheme so any
1255    /// value the ceremony later draws is re-derivable from the transcript
1256    /// alone. Part of the [entropy source](StepFact::EntropyDrawn) family.
1257    ///
1258    /// Like every fact, it is timed by the `at` on its chain envelope.
1259    #[cfg_attr(
1260        test,
1261        schemars(
1262            description = "The ceremony's entropy source was seeded with machine randomness. \
1263            Recorded once, at the start, so every value drawn from the source can be derived again \
1264            from the transcript."
1265        )
1266    )]
1267    EntropySeeded {
1268        /// Lowercase hex of the gathered machine entropy `m`.
1269        #[cfg_attr(
1270            test,
1271            schemars(
1272                description = "The machine randomness, as lowercase hex.",
1273                extend("pattern" = "^(?:[0-9a-f]{2})+$")
1274            )
1275        )]
1276        m: String,
1277        /// Provenance of `m` (e.g. `os`). A single label today; comma-separated
1278        /// if more than one source is ever mixed.
1279        #[cfg_attr(
1280            test,
1281            schemars(description = "Where the machine randomness came from, such as os.")
1282        )]
1283        source: String,
1284        /// Frozen derivation-scheme tag (e.g. `rite-kdf/v1`) that pins the
1285        /// entire construction. A verifier rejects an unrecognised value.
1286        #[cfg_attr(
1287            test,
1288            schemars(
1289                description = "The derivation scheme, such as rite-kdf/v1. A verifier refuses \
1290                a scheme it does not know."
1291            )
1292        )]
1293        derivation: String,
1294    },
1295    /// A human folded additional entropy into the seed, advancing the ratchet.
1296    ///
1297    /// Emitted by the authored `gather_entropy` step. The verbatim operator
1298    /// contribution is recorded so the epoch chain re-folds identically; it is
1299    /// public, witnessed entropy, not a secret. Timed by its enclosing step
1300    /// boundaries (and by the `PromptAnswered` that captured the input).
1301    #[cfg_attr(
1302        test,
1303        schemars(
1304            description = "A person added their own randomness to the entropy source, \
1305            starting a new epoch."
1306        )
1307    )]
1308    EntropyContributed {
1309        /// Step under which the contribution was gathered.
1310        #[cfg_attr(test, schemars(description = "The step the contribution was made in."))]
1311        step: StepId,
1312        /// Epoch index produced by this fold (1 for the first contribution).
1313        #[cfg_attr(
1314            test,
1315            schemars(description = "The epoch this contribution starts, counting from 1.")
1316        )]
1317        epoch: u32,
1318        /// Verbatim operator contribution, fed as UTF-8 into the ratchet.
1319        #[cfg_attr(
1320            test,
1321            schemars(
1322                description = "The contribution, word for word, mixed into the source as \
1323                UTF-8."
1324            )
1325        )]
1326        contribution: String,
1327    },
1328    /// A value was drawn from the entropy source (a nonce, certificate serial,
1329    /// or challenge).
1330    ///
1331    /// Emitted whenever an action draws bytes from the entropy source. The
1332    /// derivation `path` plus the recorded seed let `rite verify` re-derive
1333    /// the value and confirm the right value reached the right consumer. Like
1334    /// other action-emitted evidence, it is timed by its enclosing step.
1335    #[cfg_attr(
1336        test,
1337        schemars(
1338            description = "A value was drawn from the entropy source, such as a nonce, a \
1339            certificate serial number or a challenge. A verifier derives it again from the seed, \
1340            the contributions and the path."
1341        )
1342    )]
1343    EntropyDrawn {
1344        /// Step that drew the value.
1345        #[cfg_attr(test, schemars(description = "The step that drew the value."))]
1346        step: StepId,
1347        /// Derivation path `<epoch>/<step>/<purpose>`.
1348        #[cfg_attr(
1349            test,
1350            schemars(description = "The derivation path: `<epoch>/<step>/<purpose>`.")
1351        )]
1352        path: String,
1353        /// Lowercase hex of the derived bytes. Its length fixes the byte count,
1354        /// so `rite verify` re-derives exactly this many bytes from the seed.
1355        #[cfg_attr(
1356            test,
1357            schemars(
1358                description = "The value, as lowercase hex. Its length is the number of bytes \
1359                drawn.",
1360                extend("pattern" = "^(?:[0-9a-f]{2})*$")
1361            )
1362        )]
1363        value: String,
1364    },
1365}
1366
1367/// The `type` tag of every [`StepFact`] variant in this vocabulary.
1368///
1369/// A reader uses it to tell a fact from a newer vocabulary, which it counts and
1370/// skips, from a known fact that does not parse, which is an error.
1371pub const FACT_TYPES: &[&str] = &[
1372    "ceremony_started",
1373    "role_declared",
1374    "role_assigned",
1375    "parameter_bound",
1376    "material_loaded",
1377    "material_digest",
1378    "backend_bound",
1379    "machine_info_recorded",
1380    "act_started",
1381    "step_started",
1382    "prompt_answered",
1383    "backend_operation",
1384    "wrap_recipient_recorded",
1385    "attestation_recorded",
1386    "artifact_written",
1387    "deviation_recorded",
1388    "step_attempt_failed",
1389    "step_completed",
1390    "ceremony_completed",
1391    "ceremony_failed",
1392    "entropy_seeded",
1393    "entropy_contributed",
1394    "entropy_drawn",
1395];
1396
1397impl StepFact {
1398    /// The level a fact of this type is recorded at.
1399    ///
1400    /// Method facts (what was done, in what order, with what result) are
1401    /// public, and so are attestations, whose statement is authored in the
1402    /// ceremony. Free text, answers typed at prompts, the error message of a
1403    /// failed attempt, and identities of devices and inputs are restricted.
1404    /// Persons, material digests and the host snapshot are confidential.
1405    ///
1406    /// A failed ceremony stays public despite its error message: a public
1407    /// disclosure needs its terminal fact.
1408    #[must_use]
1409    pub fn default_level(&self) -> Level {
1410        match self {
1411            StepFact::CeremonyStarted { .. }
1412            | StepFact::RoleDeclared { .. }
1413            | StepFact::ActStarted { .. }
1414            | StepFact::StepStarted { .. }
1415            | StepFact::BackendOperation { .. }
1416            | StepFact::ArtifactWritten { .. }
1417            | StepFact::AttestationRecorded { .. }
1418            | StepFact::StepCompleted { .. }
1419            | StepFact::CeremonyCompleted {}
1420            | StepFact::CeremonyFailed { .. }
1421            // Re-deriving the entropy source needs all three.
1422            | StepFact::EntropySeeded { .. }
1423            | StepFact::EntropyContributed { .. }
1424            | StepFact::EntropyDrawn { .. } => Level::PUBLIC,
1425            StepFact::ParameterBound { .. }
1426            | StepFact::MaterialLoaded { .. }
1427            | StepFact::BackendBound { .. }
1428            | StepFact::PromptAnswered { .. }
1429            | StepFact::WrapRecipientRecorded { .. }
1430            | StepFact::StepAttemptFailed { .. }
1431            | StepFact::DeviationRecorded { .. } => Level::RESTRICTED,
1432            StepFact::RoleAssigned { .. }
1433            | StepFact::MaterialDigest { .. }
1434            | StepFact::MachineInfoRecorded { .. } => Level::CONFIDENTIAL,
1435        }
1436    }
1437
1438    /// Whether this fact evidences work performed on the world: a backend
1439    /// operation, a written artifact, a captured attestation, or consumed
1440    /// entropy. Interaction records (an answered prompt, an operator
1441    /// deviation note) and lifecycle markers are not side effects: repeating
1442    /// the interaction is safe and simply produces a fresh record.
1443    ///
1444    /// The runtime's retry gate refuses to re-execute a step attempt that
1445    /// already emitted a side-effect fact.
1446    #[must_use]
1447    pub fn is_side_effect(&self) -> bool {
1448        match self {
1449            StepFact::BackendOperation { .. }
1450            | StepFact::AttestationRecorded { .. }
1451            | StepFact::ArtifactWritten { .. }
1452            | StepFact::EntropyContributed { .. }
1453            | StepFact::EntropyDrawn { .. } => true,
1454            StepFact::CeremonyStarted { .. }
1455            // Run inputs and identities, recorded before any step works.
1456            | StepFact::RoleDeclared { .. }
1457            | StepFact::RoleAssigned { .. }
1458            | StepFact::ParameterBound { .. }
1459            | StepFact::MaterialLoaded { .. }
1460            | StepFact::MaterialDigest { .. }
1461            | StepFact::BackendBound { .. }
1462            // Records the host, not work performed on it.
1463            | StepFact::MachineInfoRecorded { .. }
1464            | StepFact::ActStarted { .. }
1465            | StepFact::StepStarted { .. }
1466            | StepFact::PromptAnswered { .. }
1467            // Records an input the step read, not work it performed.
1468            | StepFact::WrapRecipientRecorded { .. }
1469            | StepFact::DeviationRecorded { .. }
1470            | StepFact::StepAttemptFailed { .. }
1471            | StepFact::StepCompleted { .. }
1472            | StepFact::CeremonyCompleted {}
1473            | StepFact::CeremonyFailed { .. }
1474            | StepFact::EntropySeeded { .. } => false,
1475        }
1476    }
1477}
1478
1479/// JSON-shape snapshot tests, the tripwire for accidental wire-format breaks.
1480///
1481/// Every variant of [`StepFact`], [`Prompt`], [`ResponseRecord`], [`StepOutcome`],
1482/// and [`ValidatorSpec`] is serialized once with fixed payloads and compared
1483/// against an inline JSON literal. The on-disk transcript schema is what
1484/// `rite verify`, `rite report`, and any third-party verifier consume; a
1485/// rename, a `serde(tag)` change, a `rename_all` flip, or a timestamp format
1486/// swap must surface here, not in the field.
1487///
1488/// Breaking the format is allowed in early beta, **deliberately**, update
1489/// the fixture in the same commit so the diff documents the wire change.
1490#[cfg(test)]
1491mod validator_tests {
1492    use super::*;
1493
1494    fn shape(format: Format, min: usize, max: usize) -> ValidatorSpec {
1495        ValidatorSpec::Format {
1496            format,
1497            min_length: Some(min),
1498            max_length: Some(max),
1499        }
1500    }
1501
1502    #[test]
1503    fn a_text_format_checks_characters_and_length() {
1504        let pin = shape(Format::Digits, 6, 8);
1505        assert!(pin.check("123456").is_ok());
1506        assert!(pin.check("12345678").is_ok());
1507        assert!(pin.check("12345").unwrap_err().contains("this is 5"));
1508        assert!(pin.check("123456789").unwrap_err().contains("this is 9"));
1509        assert!(pin.check("12345a").unwrap_err().contains("digits"));
1510
1511        assert!(shape(Format::Alphanumeric, 1, 4).check("ab12").is_ok());
1512        assert!(shape(Format::Alphanumeric, 1, 4).check("ab-1").is_err());
1513        assert!(shape(Format::Text, 1, 4).check("ab-1").is_ok());
1514        assert!(
1515            ValidatorSpec::Format {
1516                format: Format::Text,
1517                min_length: None,
1518                max_length: None,
1519            }
1520            .check("  ")
1521            .is_err(),
1522            "a value is still something"
1523        );
1524    }
1525
1526    /// The pattern covers the whole value: a rule that read `[0-9]+` and
1527    /// accepted `abc123` would pass what its author meant to refuse.
1528    #[test]
1529    fn a_pattern_must_match_the_whole_value() {
1530        let digits = ValidatorSpec::Regex("[0-9]+".to_string());
1531        assert!(digits.check("123").is_ok());
1532        assert!(digits.check("abc123").is_err());
1533        assert!(digits.check("123abc").is_err());
1534        // Alternation is grouped before anchoring.
1535        let either = ValidatorSpec::Regex("yes|no".to_string());
1536        assert!(either.check("no").is_ok());
1537        assert!(either.check("nope").is_err());
1538    }
1539
1540    #[test]
1541    fn a_refusal_names_the_rule_and_not_the_value() {
1542        let err = shape(Format::Digits, 6, 6).check("hunter2").unwrap_err();
1543        assert!(!err.contains("hunter2"));
1544        let err = ValidatorSpec::Regex("[0-9]+".to_string())
1545            .check("hunter2")
1546            .unwrap_err();
1547        assert!(!err.contains("hunter2"));
1548    }
1549
1550    #[test]
1551    fn a_hint_reads_as_a_person_would_say_it() {
1552        assert_eq!(shape(Format::Digits, 6, 6).hint().unwrap(), "6 digits");
1553        assert_eq!(
1554            shape(Format::Text, 6, 8).hint().unwrap(),
1555            "6 to 8 characters"
1556        );
1557        assert_eq!(
1558            ValidatorSpec::Format {
1559                format: Format::Hex,
1560                min_length: Some(32),
1561                max_length: None,
1562            }
1563            .hint()
1564            .unwrap(),
1565            "at least 32 bytes as hex"
1566        );
1567        assert_eq!(
1568            ValidatorSpec::Format {
1569                format: Format::Alphanumeric,
1570                min_length: None,
1571                max_length: None,
1572            }
1573            .hint()
1574            .unwrap(),
1575            "letters or digits only"
1576        );
1577        assert_eq!(
1578            ValidatorSpec::Format {
1579                format: Format::Base64,
1580                min_length: None,
1581                max_length: None,
1582            }
1583            .hint()
1584            .unwrap(),
1585            "bytes as base64"
1586        );
1587        assert!(ValidatorSpec::NonEmpty.hint().is_none());
1588    }
1589
1590    /// The string is transport: grouping and case are dropped, and what is
1591    /// checked is the bytes.
1592    #[test]
1593    fn an_encoding_decodes_and_counts_bytes() {
1594        let key = shape(Format::Hex, 4, 4);
1595        assert!(key.check("deadbeef").is_ok());
1596        assert!(key.check("DE AD be ef").is_ok());
1597        assert!(key.check("deadbe").unwrap_err().contains("this is 3"));
1598        assert!(key.check("deadbeefx").unwrap_err().contains("hex"));
1599        assert_eq!(Format::Hex.decode("DE AD").unwrap(), vec![0xde, 0xad]);
1600
1601        let b64 = ValidatorSpec::Format {
1602            format: Format::Base64,
1603            min_length: None,
1604            max_length: None,
1605        };
1606        assert!(b64.check("aGVsbG8=").is_ok());
1607        assert!(b64.check("aGVsbG8").is_err(), "padding is required");
1608        assert_eq!(Format::Base64.decode("aGVs\nbG8=").unwrap(), b"hello");
1609        assert!(Format::Digits.decode("12").is_err(), "text does not decode");
1610    }
1611
1612    #[test]
1613    fn a_placeholder_satisfies_its_own_format() {
1614        for format in [
1615            Format::Text,
1616            Format::Digits,
1617            Format::Alphanumeric,
1618            Format::Hex,
1619            Format::Base64,
1620        ] {
1621            let stand_in = format.placeholder(32).unwrap();
1622            assert!(shape(format, 32, 32).check(&stand_in).is_ok());
1623        }
1624        assert!(Format::Text.placeholder(PLACEHOLDER_LIMIT + 1).is_none());
1625    }
1626}
1627
1628#[cfg(test)]
1629mod schema_snapshot_tests {
1630    use super::*;
1631    use serde_json::json;
1632
1633    fn assert_json(fact: &StepFact, expected: &serde_json::Value) {
1634        let actual = serde_json::to_value(fact).expect("serialize StepFact");
1635        assert_eq!(&actual, expected, "wire-format drift for {fact:?}");
1636        let tag = actual
1637            .get("type")
1638            .and_then(serde_json::Value::as_str)
1639            .expect("a type tag");
1640        assert!(FACT_TYPES.contains(&tag), "{tag} missing from FACT_TYPES");
1641        crate::schema::assert_fact_matches_schema(&actual);
1642    }
1643
1644    #[test]
1645    fn a_level_is_named_or_numbered_and_must_be_declared() {
1646        let header = TranscriptHeader::new("rite test", "0", false);
1647        assert_eq!(header.level("public"), Some(Level::PUBLIC));
1648        assert_eq!(header.level("20"), Some(Level::RESTRICTED));
1649        assert_eq!(header.level("15"), None);
1650        assert_eq!(header.level("partners"), None);
1651    }
1652
1653    #[test]
1654    fn levels_are_ordered_integers() {
1655        assert!(Level::PUBLIC < Level::RESTRICTED);
1656        assert!(Level::RESTRICTED < Level::CONFIDENTIAL);
1657        assert!(Level::new(15) > Level::PUBLIC && Level::new(15) < Level::RESTRICTED);
1658        assert_eq!(
1659            serde_json::to_value(Level::CONFIDENTIAL).expect("serialize"),
1660            json!(30)
1661        );
1662        assert_eq!(Level::RESTRICTED.to_string(), "restricted");
1663        assert_eq!(Level::new(15).to_string(), "level 15");
1664    }
1665
1666    #[test]
1667    fn ceremony_started() {
1668        assert_json(
1669            &StepFact::CeremonyStarted {
1670                name: "Root CA".to_string(),
1671                template: Sha256Digest::of(b"ceremony"),
1672            },
1673            &json!({
1674                "type": "ceremony_started",
1675                "name": "Root CA",
1676                "template": Sha256Digest::of(b"ceremony").as_str(),
1677            }),
1678        );
1679    }
1680
1681    #[test]
1682    fn header() {
1683        let header = TranscriptHeader::new("rite 0.6.0", &"0".repeat(32), false);
1684        assert_eq!(
1685            serde_json::to_value(&header).expect("serialize header"),
1686            json!({
1687                "rite_transcript": 0,
1688                "vocabulary": 0,
1689                "producer": "rite 0.6.0",
1690                "run_id": "0".repeat(32),
1691                "dry_run": false,
1692                "levels": { "public": 10, "restricted": 20, "confidential": 30 },
1693            }),
1694        );
1695    }
1696
1697    #[test]
1698    fn role_assigned() {
1699        assert_json(
1700            &StepFact::RoleAssigned {
1701                role: RoleId::new("crypto_officer"),
1702                person: "Alice Rivera".to_string(),
1703            },
1704            &json!({
1705                "type": "role_assigned",
1706                "role": "crypto_officer",
1707                "person": "Alice Rivera",
1708            }),
1709        );
1710    }
1711
1712    #[test]
1713    fn parameter_bound() {
1714        assert_json(
1715            &StepFact::ParameterBound {
1716                name: ParamId::new("validity_days"),
1717                value: json!(3650),
1718            },
1719            &json!({
1720                "type": "parameter_bound",
1721                "name": "validity_days",
1722                "value": 3650,
1723            }),
1724        );
1725    }
1726
1727    #[test]
1728    fn material_loaded() {
1729        assert_json(
1730            &StepFact::MaterialLoaded {
1731                name: MaterialId::new("usb_drive"),
1732                identifier: Some("SN-1234".to_string()),
1733            },
1734            &json!({
1735                "type": "material_loaded",
1736                "name": "usb_drive",
1737                "identifier": "SN-1234",
1738            }),
1739        );
1740        assert_json(
1741            &StepFact::MaterialLoaded {
1742                name: MaterialId::new("csr"),
1743                identifier: None,
1744            },
1745            &json!({ "type": "material_loaded", "name": "csr" }),
1746        );
1747    }
1748
1749    #[test]
1750    fn material_digest() {
1751        assert_json(
1752            &StepFact::MaterialDigest {
1753                name: MaterialId::new("csr"),
1754                digest: Sha256Digest::of(b"csr"),
1755            },
1756            &json!({
1757                "type": "material_digest",
1758                "name": "csr",
1759                "digest": Sha256Digest::of(b"csr").as_str(),
1760            }),
1761        );
1762    }
1763
1764    #[test]
1765    fn backend_bound() {
1766        assert_json(
1767            &StepFact::BackendBound {
1768                name: "token".to_string(),
1769                provider: "yubikey".to_string(),
1770                identity: "yubikey-serial=12345678+firmware=5.7.1".to_string(),
1771            },
1772            &json!({
1773                "type": "backend_bound",
1774                "name": "token",
1775                "provider": "yubikey",
1776                "identity": "yubikey-serial=12345678+firmware=5.7.1",
1777            }),
1778        );
1779    }
1780
1781    #[test]
1782    fn machine_info_recorded() {
1783        assert_json(
1784            &StepFact::MachineInfoRecorded {
1785                step: StepId::new("capture"),
1786                info: json!({ "arch": "x86_64", "hostname": null }),
1787            },
1788            &json!({
1789                "type": "machine_info_recorded",
1790                "step": "capture",
1791                "info": { "arch": "x86_64", "hostname": null },
1792            }),
1793        );
1794    }
1795
1796    #[test]
1797    fn act_started() {
1798        assert_json(
1799            &StepFact::ActStarted {
1800                id: ActId::new("setup"),
1801                label: "Setup".to_string(),
1802            },
1803            &json!({
1804                "type": "act_started",
1805                "id": "setup",
1806                "label": "Setup",
1807            }),
1808        );
1809    }
1810
1811    #[test]
1812    fn step_started() {
1813        assert_json(
1814            &StepFact::StepStarted {
1815                id: StepId::new("s1"),
1816                label: "2.1".to_string(),
1817                role: RoleId::new("crypto_officer"),
1818            },
1819            &json!({
1820                "type": "step_started",
1821                "id": "s1",
1822                "label": "2.1",
1823                "role": "crypto_officer",
1824            }),
1825        );
1826    }
1827
1828    #[test]
1829    fn role_declared() {
1830        assert_json(
1831            &StepFact::RoleDeclared {
1832                role: RoleId::new("crypto_officer"),
1833                name: "Crypto Officer".to_string(),
1834            },
1835            &json!({
1836                "type": "role_declared",
1837                "role": "crypto_officer",
1838                "name": "Crypto Officer",
1839            }),
1840        );
1841    }
1842
1843    #[test]
1844    fn prompt_answered_confirm_bool() {
1845        assert_json(
1846            &StepFact::PromptAnswered {
1847                step: Some(StepId::new("s1")),
1848                prompt: Prompt::Confirm {
1849                    question: "Proceed?".to_string(),
1850                    default: Some(true),
1851                },
1852                response: ResponseRecord::Bool { value: true },
1853            },
1854            &json!({
1855                "type": "prompt_answered",
1856                "step": "s1",
1857                "prompt": { "type": "confirm", "question": "Proceed?", "default": true },
1858                "response": { "type": "bool", "value": true },
1859            }),
1860        );
1861    }
1862
1863    #[test]
1864    fn prompt_answered_text_nonempty() {
1865        assert_json(
1866            &StepFact::PromptAnswered {
1867                step: Some(StepId::new("s1")),
1868                prompt: Prompt::Text {
1869                    label: "Name".to_string(),
1870                    validator: ValidatorSpec::NonEmpty,
1871                },
1872                response: ResponseRecord::Text {
1873                    value: "Alice".to_string(),
1874                },
1875            },
1876            &json!({
1877                "type": "prompt_answered",
1878                "step": "s1",
1879                "prompt": {
1880                    "type": "text",
1881                    "label": "Name",
1882                    "validator": { "kind": "non_empty" },
1883                },
1884                "response": { "type": "text", "value": "Alice" },
1885            }),
1886        );
1887    }
1888
1889    #[test]
1890    fn prompt_answered_text_regex() {
1891        assert_json(
1892            &StepFact::PromptAnswered {
1893                step: Some(StepId::new("s1")),
1894                prompt: Prompt::Text {
1895                    label: "SN".to_string(),
1896                    validator: ValidatorSpec::Regex(r"^[A-Z0-9]+$".to_string()),
1897                },
1898                response: ResponseRecord::Text {
1899                    value: "AB12".to_string(),
1900                },
1901            },
1902            &json!({
1903                "type": "prompt_answered",
1904                "step": "s1",
1905                "prompt": {
1906                    "type": "text",
1907                    "label": "SN",
1908                    "validator": { "kind": "regex", "value": "^[A-Z0-9]+$" },
1909                },
1910                "response": { "type": "text", "value": "AB12" },
1911            }),
1912        );
1913    }
1914
1915    #[test]
1916    fn prompt_answered_text_predefined() {
1917        assert_json(
1918            &StepFact::PromptAnswered {
1919                step: Some(StepId::new("s1")),
1920                prompt: Prompt::Text {
1921                    label: "SN".to_string(),
1922                    validator: ValidatorSpec::Predefined("serial_number".to_string()),
1923                },
1924                response: ResponseRecord::Text {
1925                    value: "ABCD".to_string(),
1926                },
1927            },
1928            &json!({
1929                "type": "prompt_answered",
1930                "step": "s1",
1931                "prompt": {
1932                    "type": "text",
1933                    "label": "SN",
1934                    "validator": { "kind": "predefined", "value": "serial_number" },
1935                },
1936                "response": { "type": "text", "value": "ABCD" },
1937            }),
1938        );
1939    }
1940
1941    #[test]
1942    fn prompt_answered_secret_redacted() {
1943        assert_json(
1944            &StepFact::PromptAnswered {
1945                step: Some(StepId::new("s1")),
1946                prompt: Prompt::Secret {
1947                    label: "PIN".to_string(),
1948                    validator: ValidatorSpec::NonEmpty,
1949                },
1950                response: ResponseRecord::SecretRedacted {},
1951            },
1952            &json!({
1953                "type": "prompt_answered",
1954                "step": "s1",
1955                "prompt": {
1956                    "type": "secret",
1957                    "label": "PIN",
1958                    "validator": { "kind": "non_empty" },
1959                },
1960                "response": { "type": "secret_redacted" },
1961            }),
1962        );
1963    }
1964
1965    #[test]
1966    fn prompt_answered_literal_text() {
1967        assert_json(
1968            &StepFact::PromptAnswered {
1969                step: Some(StepId::new("s1")),
1970                prompt: Prompt::Literal {
1971                    label: "Type 'attest'".to_string(),
1972                    expected: "attest".to_string(),
1973                },
1974                response: ResponseRecord::Text {
1975                    value: "attest".to_string(),
1976                },
1977            },
1978            &json!({
1979                "type": "prompt_answered",
1980                "step": "s1",
1981                "prompt": { "type": "literal", "label": "Type 'attest'", "expected": "attest" },
1982                "response": { "type": "text", "value": "attest" },
1983            }),
1984        );
1985    }
1986
1987    #[test]
1988    fn prompt_answered_continue_acknowledged() {
1989        assert_json(
1990            &StepFact::PromptAnswered {
1991                step: Some(StepId::new("s1")),
1992                prompt: Prompt::Continue {
1993                    hint: Some("Press Enter".to_string()),
1994                },
1995                response: ResponseRecord::Acknowledged,
1996            },
1997            &json!({
1998                "type": "prompt_answered",
1999                "step": "s1",
2000                "prompt": { "type": "continue", "hint": "Press Enter" },
2001                "response": { "type": "acknowledged" },
2002            }),
2003        );
2004    }
2005
2006    #[test]
2007    fn backend_operation() {
2008        assert_json(
2009            &StepFact::BackendOperation {
2010                step: StepId::new("s1"),
2011                kind: "generate_key".to_string(),
2012                backend: Some("hsm".to_string()),
2013                inputs: json!({ "algorithm": "rsa", "bits": 4096 }),
2014                outputs: json!({ "key_id": "k1" }),
2015                fingerprint: Some("sha256:deadbeef".to_string()),
2016            },
2017            &json!({
2018                "type": "backend_operation",
2019                "step": "s1",
2020                "kind": "generate_key",
2021                "backend": "hsm",
2022                "inputs": { "algorithm": "rsa", "bits": 4096 },
2023                "outputs": { "key_id": "k1" },
2024                "fingerprint": "sha256:deadbeef",
2025            }),
2026        );
2027    }
2028
2029    #[test]
2030    fn wrap_recipient_recorded() {
2031        let fingerprint = Sha256Digest::of(b"escrow spki").to_string();
2032        assert_json(
2033            &StepFact::WrapRecipientRecorded {
2034                step: StepId::new("s1"),
2035                source: "escrow_pubkey".to_string(),
2036                fingerprint: fingerprint.clone(),
2037                declared: true,
2038            },
2039            &json!({
2040                "type": "wrap_recipient_recorded",
2041                "step": "s1",
2042                "source": "escrow_pubkey",
2043                "fingerprint": fingerprint,
2044                "declared": true,
2045            }),
2046        );
2047    }
2048
2049    #[test]
2050    fn attestation_recorded() {
2051        assert_json(
2052            &StepFact::AttestationRecorded {
2053                step: StepId::new("s1"),
2054                role: RoleId::new("crypto_officer"),
2055                statement: "I confirm.".to_string(),
2056            },
2057            &json!({
2058                "type": "attestation_recorded",
2059                "step": "s1",
2060                "role": "crypto_officer",
2061                "statement": "I confirm.",
2062            }),
2063        );
2064    }
2065
2066    #[test]
2067    fn artifact_written() {
2068        assert_json(
2069            &StepFact::ArtifactWritten {
2070                step: StepId::new("s1"),
2071                name: "root_cert".to_string(),
2072                file: "root_cert.crt".to_string(),
2073                digest: Some(Sha256Digest::of(b"cert")),
2074            },
2075            &json!({
2076                "type": "artifact_written",
2077                "step": "s1",
2078                "name": "root_cert",
2079                "file": "root_cert.crt",
2080                "digest": Sha256Digest::of(b"cert").as_str(),
2081            }),
2082        );
2083    }
2084
2085    #[test]
2086    fn artifact_written_secret_has_no_digest() {
2087        assert_json(
2088            &StepFact::ArtifactWritten {
2089                step: StepId::new("s1"),
2090                name: "opened".to_string(),
2091                file: "opened.bin".to_string(),
2092                digest: None,
2093            },
2094            &json!({
2095                "type": "artifact_written",
2096                "step": "s1",
2097                "name": "opened",
2098                "file": "opened.bin",
2099            }),
2100        );
2101    }
2102
2103    #[test]
2104    fn deviation_recorded() {
2105        assert_json(
2106            &StepFact::DeviationRecorded {
2107                step: Some(StepId::new("s1")),
2108                text: "phone rang".to_string(),
2109            },
2110            &json!({
2111                "type": "deviation_recorded",
2112                "step": "s1",
2113                "text": "phone rang",
2114            }),
2115        );
2116    }
2117
2118    #[test]
2119    fn step_completed_completed() {
2120        assert_json(
2121            &StepFact::StepCompleted {
2122                id: StepId::new("s1"),
2123                outcome: StepOutcome::Completed {
2124                    message: "done".to_string(),
2125                },
2126            },
2127            &json!({
2128                "type": "step_completed",
2129                "id": "s1",
2130                "outcome": { "status": "completed", "message": "done" },
2131            }),
2132        );
2133    }
2134
2135    #[test]
2136    fn ceremony_completed() {
2137        assert_json(
2138            &StepFact::CeremonyCompleted {},
2139            &json!({
2140                "type": "ceremony_completed",
2141            }),
2142        );
2143    }
2144
2145    #[test]
2146    fn ceremony_failed() {
2147        assert_json(
2148            &StepFact::CeremonyFailed {
2149                error: ErrorRecord::new(
2150                    ErrorClass::Abort,
2151                    "aborted",
2152                    "ceremony aborted by operator",
2153                ),
2154            },
2155            &json!({
2156                "type": "ceremony_failed",
2157                "error": {
2158                    "class": "abort",
2159                    "kind": "aborted",
2160                    "message": "ceremony aborted by operator",
2161                },
2162            }),
2163        );
2164    }
2165
2166    #[test]
2167    fn step_attempt_failed() {
2168        assert_json(
2169            &StepFact::StepAttemptFailed {
2170                step: StepId::new("import_key"),
2171                attempt: 1,
2172                error: ErrorRecord::new(
2173                    ErrorClass::Environmental,
2174                    "backend_error",
2175                    "Token not present",
2176                ),
2177            },
2178            &json!({
2179                "type": "step_attempt_failed",
2180                "step": "import_key",
2181                "attempt": 1,
2182                "error": {
2183                    "class": "environmental",
2184                    "kind": "backend_error",
2185                    "message": "Token not present",
2186                },
2187            }),
2188        );
2189    }
2190
2191    #[test]
2192    fn entropy_seeded() {
2193        assert_json(
2194            &StepFact::EntropySeeded {
2195                m: "00112233".to_string(),
2196                source: "os".to_string(),
2197                derivation: "rite-kdf/v1".to_string(),
2198            },
2199            &json!({
2200                "type": "entropy_seeded",
2201                "m": "00112233",
2202                "source": "os",
2203                "derivation": "rite-kdf/v1",
2204            }),
2205        );
2206    }
2207
2208    #[test]
2209    fn entropy_contributed() {
2210        assert_json(
2211            &StepFact::EntropyContributed {
2212                step: StepId::new("roll_dice"),
2213                epoch: 1,
2214                contribution: "3 1 6 4 2 5".to_string(),
2215            },
2216            &json!({
2217                "type": "entropy_contributed",
2218                "step": "roll_dice",
2219                "epoch": 1,
2220                "contribution": "3 1 6 4 2 5",
2221            }),
2222        );
2223    }
2224
2225    #[test]
2226    fn entropy_drawn() {
2227        assert_json(
2228            &StepFact::EntropyDrawn {
2229                step: StepId::new("issue"),
2230                path: "0/issue/cert-serial".to_string(),
2231                value: "aabbccddeeff00112233".to_string(),
2232            },
2233            &json!({
2234                "type": "entropy_drawn",
2235                "step": "issue",
2236                "path": "0/issue/cert-serial",
2237                "value": "aabbccddeeff00112233",
2238            }),
2239        );
2240    }
2241}