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