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