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::path::PathBuf;
14
15use serde::{Deserialize, Serialize};
16
17use crate::ir::{ActId, RoleId, StepId};
18
19/// Validator applied by the runtime to a free-form text or literal response
20/// before it is accepted.
21#[non_exhaustive]
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
24pub enum ValidatorSpec {
25    /// Reject empty or whitespace-only input.
26    NonEmpty,
27    /// Input must match this regular expression.
28    Regex(String),
29    /// Named, runtime-defined predicate (e.g. `serial_number`).
30    Predefined(String),
31}
32
33/// Request for user input, recorded into the transcript as part of
34/// [`StepFact::PromptAnswered`].
35#[non_exhaustive]
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(tag = "type", rename_all = "snake_case")]
38pub enum Prompt {
39    /// Yes / no question with an optional default.
40    Confirm {
41        /// Question shown to the user.
42        question: String,
43        /// Default selection if the user presses Enter without choosing.
44        default: Option<bool>,
45    },
46    /// Free-form text input, validated against a [`ValidatorSpec`].
47    Text {
48        /// Label shown to the user.
49        label: String,
50        /// Validator applied before the response is accepted.
51        validator: ValidatorSpec,
52    },
53    /// Sensitive input (PIN, password). Echo is suppressed; plaintext is
54    /// never serialized to the transcript.
55    Secret {
56        /// Label shown to the user.
57        label: String,
58    },
59    /// User must type a specific literal string exactly. Validation is
60    /// performed by the runtime against `expected`.
61    Literal {
62        /// Label shown to the user.
63        label: String,
64        /// Exact string the user must type.
65        expected: String,
66    },
67    /// Wait for the user to acknowledge before proceeding. Used for pacing.
68    Continue {
69        /// Optional hint such as "Press Enter when ready".
70        hint: Option<String>,
71    },
72}
73
74/// Serializable, redacted form of a user response.
75///
76/// Used inside [`StepFact::PromptAnswered`] so that the transcript records
77/// what was answered without ever persisting plaintext secrets. The
78/// in-flight `Response` type lives next to the channel protocol in
79/// `rite-runtime`; conversion happens at the moment the prompt is accepted.
80#[non_exhaustive]
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(tag = "type", rename_all = "snake_case")]
83pub enum ResponseRecord {
84    /// Yes / no answer.
85    Bool {
86        /// The answer.
87        value: bool,
88    },
89    /// Free-form text answer.
90    Text {
91        /// The answer.
92        value: String,
93    },
94    /// Secret answer. The plaintext is never stored, and no digest of it is
95    /// kept either: a hash of a low-entropy secret (such as a 6-8 digit PIV
96    /// PIN) is brute-forceable from a shared transcript. The position of the
97    /// enclosing `PromptAnswered` fact in the chain already records that a
98    /// secret was entered at this point.
99    // Note: a salted, per-run HMAC could later attest that two prompts received
100    // the same secret without reintroducing the low-entropy guessing oracle.
101    // Deferred until a concrete use case needs it.
102    SecretRedacted {},
103    /// Acknowledgement of a [`Prompt::Continue`].
104    Acknowledged,
105}
106
107/// Audit classification of a bad outcome, recorded so an auditor can tell the
108/// nature of a failure apart without parsing the free-form `message`.
109///
110/// This is the *audit* taxonomy (what an auditor sees), distinct from the
111/// runtime's `Retriability` (whether a step may re-run). For a backend error
112/// the two align: a retriable error is `Environmental`. They are kept separate
113/// because some classes never map cleanly onto retriability (an `Abort` is a
114/// decision, not an error).
115#[non_exhaustive]
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum ErrorClass {
119    /// The world wasn't ready; the step's work did not happen (token absent,
120    /// loose cable, PIN required).
121    Environmental,
122    /// The ceremony's own logic concluded badly (a verification mismatch, a
123    /// refused attestation). A result, not a recoverable condition.
124    Procedural,
125    /// The run itself is compromised or the definition is broken (transcript
126    /// write failed, channel lost, unknown action, invalid params).
127    Integrity,
128    /// The operator chose to stop. Not an error at all, but recorded on the
129    /// terminal fact so abort is distinguishable from failure.
130    Abort,
131}
132
133/// Structured error record for transcript serialization.
134#[non_exhaustive]
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ErrorRecord {
137    /// Audit classification of this error.
138    pub class: ErrorClass,
139    /// Stable kind label (e.g. `aborted`, `step_failed`, `material_load_failed`).
140    pub kind: String,
141    /// Human-readable message.
142    pub message: String,
143}
144
145impl ErrorRecord {
146    /// Construct an error record.
147    pub fn new(class: ErrorClass, kind: impl Into<String>, message: impl Into<String>) -> Self {
148        Self {
149            class,
150            kind: kind.into(),
151            message: message.into(),
152        }
153    }
154}
155
156/// Outcome of a single step, carried by [`StepFact::StepCompleted`].
157#[non_exhaustive]
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(tag = "status", rename_all = "snake_case")]
160pub enum StepOutcome {
161    /// Step executed successfully.
162    Completed {
163        /// Human-readable completion message.
164        message: String,
165    },
166}
167
168/// Durable, transcript-worthy fact.
169///
170/// Every variant is recorded by the runtime's transcript sink synchronously
171/// before being forwarded to the UI. Action handlers emit
172/// [`BackendOperation`] and [`AttestationRecorded`]; all other variants are
173/// emitted by the executor at the corresponding lifecycle boundary.
174///
175/// [`BackendOperation`]: StepFact::BackendOperation
176/// [`AttestationRecorded`]: StepFact::AttestationRecorded
177#[non_exhaustive]
178#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(tag = "type", rename_all = "snake_case")]
180pub enum StepFact {
181    /// Ceremony has started running.
182    CeremonyStarted {
183        /// Ceremony name from the DSL.
184        name: String,
185    },
186    /// Beginning of an act (named subdivision of a ceremony).
187    ActStarted {
188        /// Act identifier.
189        id: ActId,
190        /// Act label as authored in the DSL.
191        label: String,
192    },
193    /// Beginning of a step.
194    StepStarted {
195        /// Step identifier.
196        id: StepId,
197        /// Step label as authored in the DSL.
198        label: String,
199        /// Role responsible for this step (stable id).
200        role: RoleId,
201        /// Human-readable role name, from the ceremony's role definition.
202        /// Carried alongside the id so transcripts stay self-contained for
203        /// reports and verifiers without re-reading the ceremony YAML.
204        role_name: String,
205    },
206    /// A prompt has been answered and validated.
207    PromptAnswered {
208        /// Step that issued the prompt, if any. `None` for ceremony-level
209        /// prompts emitted before the first step (e.g. the ceremony-start
210        /// confirmation) or after the last step.
211        #[serde(skip_serializing_if = "Option::is_none")]
212        step: Option<StepId>,
213        /// The prompt as issued.
214        prompt: Prompt,
215        /// Redacted response record.
216        response: ResponseRecord,
217    },
218    /// A backend operation completed and produced structured evidence.
219    BackendOperation {
220        /// Step under which the operation ran.
221        step: StepId,
222        /// Stable operation kind (e.g. `generate_keypair`, `sign_data`).
223        kind: String,
224        /// Structured inputs to the operation (parameters, references).
225        inputs: serde_json::Value,
226        /// Structured outputs from the operation (artifact ids, hashes).
227        outputs: serde_json::Value,
228        /// Optional fingerprint of the produced material.
229        fingerprint: Option<String>,
230    },
231    /// A human attestation was recorded.
232    AttestationRecorded {
233        /// Step under which the attestation was recorded.
234        step: StepId,
235        /// Role that issued the attestation.
236        role: RoleId,
237        /// Verbatim attestation statement.
238        statement: String,
239    },
240    /// An artifact was written to disk.
241    ArtifactWritten {
242        /// Step that produced the artifact.
243        step: StepId,
244        /// Artifact name as declared in the DSL.
245        name: String,
246        /// Path on disk.
247        path: PathBuf,
248        /// Lowercase hex SHA-256 of the artifact bytes.
249        sha256: String,
250    },
251    /// A deviation was logged by the operator.
252    DeviationRecorded {
253        /// Step in which the deviation was logged, if any. `None` for
254        /// deviations logged outside of a step (before the first step or
255        /// while a ceremony-level prompt is pending).
256        #[serde(skip_serializing_if = "Option::is_none")]
257        step: Option<StepId>,
258        /// Verbatim deviation text.
259        text: String,
260    },
261    /// A step attempt failed. Recorded per attempt, so a retried step shows
262    /// `StepAttemptFailed{attempt: 1}` followed by the operator's retry
263    /// decision and, on success, `StepCompleted`. The final attempt of a step
264    /// that the run gives up on is followed by the terminal `CeremonyFailed`.
265    StepAttemptFailed {
266        /// Step whose attempt failed.
267        step: StepId,
268        /// 1-based attempt number within this step.
269        attempt: u32,
270        /// Structured error record for the failed attempt.
271        error: ErrorRecord,
272    },
273    /// Step finished executing.
274    StepCompleted {
275        /// Step identifier.
276        id: StepId,
277        /// Outcome (completed or skipped).
278        outcome: StepOutcome,
279    },
280    /// Ceremony finished successfully.
281    ///
282    /// The transcript's cryptographic identity is `SHA-256(line_bytes)`
283    /// for this line, recoverable by any reader, so the fact itself
284    /// carries no fingerprint field. The runtime forwards the value to
285    /// frontends through an out-of-band channel event.
286    CeremonyCompleted {},
287    /// Ceremony failed or was aborted.
288    CeremonyFailed {
289        /// Structured error record.
290        error: ErrorRecord,
291    },
292    /// The ceremony entropy source was seeded with machine randomness.
293    ///
294    /// Emitted once by the runner at ceremony start (run-metadata). Records
295    /// the machine contribution `m` and the frozen derivation scheme so any
296    /// value the ceremony later draws is re-derivable from the transcript
297    /// alone. Part of the [entropy source](StepFact::EntropyDrawn) family.
298    ///
299    /// Like every fact, it is timed by the `at` on its chain envelope.
300    EntropySeeded {
301        /// Lowercase hex of the gathered machine entropy `m`.
302        m: String,
303        /// Provenance of `m` (e.g. `os`). A single label today; comma-separated
304        /// if more than one source is ever mixed.
305        source: String,
306        /// Frozen derivation-scheme tag (e.g. `rite-kdf/v1`) that pins the
307        /// entire construction. A verifier rejects an unrecognised value.
308        derivation: String,
309    },
310    /// A human folded additional entropy into the seed, advancing the ratchet.
311    ///
312    /// Emitted by the authored `gather_entropy` step. The verbatim operator
313    /// contribution is recorded so the epoch chain re-folds identically; it is
314    /// public, witnessed entropy, not a secret. Timed by its enclosing step
315    /// boundaries (and by the `PromptAnswered` that captured the input).
316    EntropyContributed {
317        /// Step under which the contribution was gathered.
318        step: StepId,
319        /// Epoch index produced by this fold (1 for the first contribution).
320        epoch: u32,
321        /// Verbatim operator contribution, fed as UTF-8 into the ratchet.
322        contribution: String,
323    },
324    /// A value was drawn from the entropy source (a nonce, certificate serial,
325    /// or challenge).
326    ///
327    /// Emitted whenever an action draws bytes from the entropy source. The
328    /// derivation `path` plus the recorded seed let `rite verify` re-derive
329    /// the value and confirm the right value reached the right consumer. Like
330    /// other action-emitted evidence, it is timed by its enclosing step.
331    EntropyDrawn {
332        /// Step that drew the value.
333        step: StepId,
334        /// Derivation path `<epoch>/<step>/<purpose>`.
335        path: String,
336        /// Lowercase hex of the derived bytes. Its length fixes the byte count,
337        /// so `rite verify` re-derives exactly this many bytes from the seed.
338        value: String,
339    },
340}
341
342impl StepFact {
343    /// Whether this fact evidences work performed on the world: a backend
344    /// operation, a written artifact, a captured attestation, or consumed
345    /// entropy. Interaction records (an answered prompt, an operator
346    /// deviation note) and lifecycle markers are not side effects: repeating
347    /// the interaction is safe and simply produces a fresh record.
348    ///
349    /// The runtime's retry gate refuses to re-execute a step attempt that
350    /// already emitted a side-effect fact.
351    #[must_use]
352    pub fn is_side_effect(&self) -> bool {
353        match self {
354            StepFact::BackendOperation { .. }
355            | StepFact::AttestationRecorded { .. }
356            | StepFact::ArtifactWritten { .. }
357            | StepFact::EntropyContributed { .. }
358            | StepFact::EntropyDrawn { .. } => true,
359            StepFact::CeremonyStarted { .. }
360            | StepFact::ActStarted { .. }
361            | StepFact::StepStarted { .. }
362            | StepFact::PromptAnswered { .. }
363            | StepFact::DeviationRecorded { .. }
364            | StepFact::StepAttemptFailed { .. }
365            | StepFact::StepCompleted { .. }
366            | StepFact::CeremonyCompleted {}
367            | StepFact::CeremonyFailed { .. }
368            | StepFact::EntropySeeded { .. } => false,
369        }
370    }
371}
372
373/// JSON-shape snapshot tests, the tripwire for accidental wire-format breaks.
374///
375/// Every variant of [`StepFact`], [`Prompt`], [`ResponseRecord`], [`StepOutcome`],
376/// and [`ValidatorSpec`] is serialized once with fixed payloads and compared
377/// against an inline JSON literal. The on-disk transcript schema is what
378/// `rite verify`, `rite report`, and any third-party verifier consume; a
379/// rename, a `serde(tag)` change, a `rename_all` flip, or a timestamp format
380/// swap must surface here, not in the field.
381///
382/// Breaking the format is allowed in early beta, **deliberately**, update
383/// the fixture in the same commit so the diff documents the wire change.
384#[cfg(test)]
385mod schema_snapshot_tests {
386    use super::*;
387    use serde_json::json;
388
389    fn assert_json(fact: &StepFact, expected: &serde_json::Value) {
390        let actual = serde_json::to_value(fact).expect("serialize StepFact");
391        assert_eq!(&actual, expected, "wire-format drift for {fact:?}");
392    }
393
394    #[test]
395    fn ceremony_started() {
396        assert_json(
397            &StepFact::CeremonyStarted {
398                name: "Root CA".to_string(),
399            },
400            &json!({
401                "type": "ceremony_started",
402                "name": "Root CA",
403            }),
404        );
405    }
406
407    #[test]
408    fn act_started() {
409        assert_json(
410            &StepFact::ActStarted {
411                id: ActId::new("setup"),
412                label: "Setup".to_string(),
413            },
414            &json!({
415                "type": "act_started",
416                "id": "setup",
417                "label": "Setup",
418            }),
419        );
420    }
421
422    #[test]
423    fn step_started() {
424        assert_json(
425            &StepFact::StepStarted {
426                id: StepId::new("s1"),
427                label: "2.1".to_string(),
428                role: RoleId::new("crypto_officer"),
429                role_name: "Crypto Officer".to_string(),
430            },
431            &json!({
432                "type": "step_started",
433                "id": "s1",
434                "label": "2.1",
435                "role": "crypto_officer",
436                "role_name": "Crypto Officer",
437            }),
438        );
439    }
440
441    #[test]
442    fn prompt_answered_confirm_bool() {
443        assert_json(
444            &StepFact::PromptAnswered {
445                step: Some(StepId::new("s1")),
446                prompt: Prompt::Confirm {
447                    question: "Proceed?".to_string(),
448                    default: Some(true),
449                },
450                response: ResponseRecord::Bool { value: true },
451            },
452            &json!({
453                "type": "prompt_answered",
454                "step": "s1",
455                "prompt": { "type": "confirm", "question": "Proceed?", "default": true },
456                "response": { "type": "bool", "value": true },
457            }),
458        );
459    }
460
461    #[test]
462    fn prompt_answered_text_nonempty() {
463        assert_json(
464            &StepFact::PromptAnswered {
465                step: Some(StepId::new("s1")),
466                prompt: Prompt::Text {
467                    label: "Name".to_string(),
468                    validator: ValidatorSpec::NonEmpty,
469                },
470                response: ResponseRecord::Text {
471                    value: "Alice".to_string(),
472                },
473            },
474            &json!({
475                "type": "prompt_answered",
476                "step": "s1",
477                "prompt": {
478                    "type": "text",
479                    "label": "Name",
480                    "validator": { "kind": "non_empty" },
481                },
482                "response": { "type": "text", "value": "Alice" },
483            }),
484        );
485    }
486
487    #[test]
488    fn prompt_answered_text_regex() {
489        assert_json(
490            &StepFact::PromptAnswered {
491                step: Some(StepId::new("s1")),
492                prompt: Prompt::Text {
493                    label: "SN".to_string(),
494                    validator: ValidatorSpec::Regex(r"^[A-Z0-9]+$".to_string()),
495                },
496                response: ResponseRecord::Text {
497                    value: "AB12".to_string(),
498                },
499            },
500            &json!({
501                "type": "prompt_answered",
502                "step": "s1",
503                "prompt": {
504                    "type": "text",
505                    "label": "SN",
506                    "validator": { "kind": "regex", "value": "^[A-Z0-9]+$" },
507                },
508                "response": { "type": "text", "value": "AB12" },
509            }),
510        );
511    }
512
513    #[test]
514    fn prompt_answered_text_predefined() {
515        assert_json(
516            &StepFact::PromptAnswered {
517                step: Some(StepId::new("s1")),
518                prompt: Prompt::Text {
519                    label: "SN".to_string(),
520                    validator: ValidatorSpec::Predefined("serial_number".to_string()),
521                },
522                response: ResponseRecord::Text {
523                    value: "ABCD".to_string(),
524                },
525            },
526            &json!({
527                "type": "prompt_answered",
528                "step": "s1",
529                "prompt": {
530                    "type": "text",
531                    "label": "SN",
532                    "validator": { "kind": "predefined", "value": "serial_number" },
533                },
534                "response": { "type": "text", "value": "ABCD" },
535            }),
536        );
537    }
538
539    #[test]
540    fn prompt_answered_secret_redacted() {
541        assert_json(
542            &StepFact::PromptAnswered {
543                step: Some(StepId::new("s1")),
544                prompt: Prompt::Secret {
545                    label: "PIN".to_string(),
546                },
547                response: ResponseRecord::SecretRedacted {},
548            },
549            &json!({
550                "type": "prompt_answered",
551                "step": "s1",
552                "prompt": { "type": "secret", "label": "PIN" },
553                "response": { "type": "secret_redacted" },
554            }),
555        );
556    }
557
558    #[test]
559    fn prompt_answered_literal_text() {
560        assert_json(
561            &StepFact::PromptAnswered {
562                step: Some(StepId::new("s1")),
563                prompt: Prompt::Literal {
564                    label: "Type 'attest'".to_string(),
565                    expected: "attest".to_string(),
566                },
567                response: ResponseRecord::Text {
568                    value: "attest".to_string(),
569                },
570            },
571            &json!({
572                "type": "prompt_answered",
573                "step": "s1",
574                "prompt": { "type": "literal", "label": "Type 'attest'", "expected": "attest" },
575                "response": { "type": "text", "value": "attest" },
576            }),
577        );
578    }
579
580    #[test]
581    fn prompt_answered_continue_acknowledged() {
582        assert_json(
583            &StepFact::PromptAnswered {
584                step: Some(StepId::new("s1")),
585                prompt: Prompt::Continue {
586                    hint: Some("Press Enter".to_string()),
587                },
588                response: ResponseRecord::Acknowledged,
589            },
590            &json!({
591                "type": "prompt_answered",
592                "step": "s1",
593                "prompt": { "type": "continue", "hint": "Press Enter" },
594                "response": { "type": "acknowledged" },
595            }),
596        );
597    }
598
599    #[test]
600    fn backend_operation() {
601        assert_json(
602            &StepFact::BackendOperation {
603                step: StepId::new("s1"),
604                kind: "generate_keypair".to_string(),
605                inputs: json!({ "algorithm": "rsa", "bits": 4096 }),
606                outputs: json!({ "key_id": "k1" }),
607                fingerprint: Some("sha256:deadbeef".to_string()),
608            },
609            &json!({
610                "type": "backend_operation",
611                "step": "s1",
612                "kind": "generate_keypair",
613                "inputs": { "algorithm": "rsa", "bits": 4096 },
614                "outputs": { "key_id": "k1" },
615                "fingerprint": "sha256:deadbeef",
616            }),
617        );
618    }
619
620    #[test]
621    fn attestation_recorded() {
622        assert_json(
623            &StepFact::AttestationRecorded {
624                step: StepId::new("s1"),
625                role: RoleId::new("crypto_officer"),
626                statement: "I confirm.".to_string(),
627            },
628            &json!({
629                "type": "attestation_recorded",
630                "step": "s1",
631                "role": "crypto_officer",
632                "statement": "I confirm.",
633            }),
634        );
635    }
636
637    #[test]
638    fn artifact_written() {
639        assert_json(
640            &StepFact::ArtifactWritten {
641                step: StepId::new("s1"),
642                name: "root.crt".to_string(),
643                path: "/out/root.crt".into(),
644                sha256: "a".repeat(64),
645            },
646            &json!({
647                "type": "artifact_written",
648                "step": "s1",
649                "name": "root.crt",
650                "path": "/out/root.crt",
651                "sha256": "a".repeat(64),
652            }),
653        );
654    }
655
656    #[test]
657    fn deviation_recorded() {
658        assert_json(
659            &StepFact::DeviationRecorded {
660                step: Some(StepId::new("s1")),
661                text: "phone rang".to_string(),
662            },
663            &json!({
664                "type": "deviation_recorded",
665                "step": "s1",
666                "text": "phone rang",
667            }),
668        );
669    }
670
671    #[test]
672    fn step_completed_completed() {
673        assert_json(
674            &StepFact::StepCompleted {
675                id: StepId::new("s1"),
676                outcome: StepOutcome::Completed {
677                    message: "done".to_string(),
678                },
679            },
680            &json!({
681                "type": "step_completed",
682                "id": "s1",
683                "outcome": { "status": "completed", "message": "done" },
684            }),
685        );
686    }
687
688    #[test]
689    fn ceremony_completed() {
690        assert_json(
691            &StepFact::CeremonyCompleted {},
692            &json!({
693                "type": "ceremony_completed",
694            }),
695        );
696    }
697
698    #[test]
699    fn ceremony_failed() {
700        assert_json(
701            &StepFact::CeremonyFailed {
702                error: ErrorRecord::new(
703                    ErrorClass::Abort,
704                    "aborted",
705                    "ceremony aborted by operator",
706                ),
707            },
708            &json!({
709                "type": "ceremony_failed",
710                "error": {
711                    "class": "abort",
712                    "kind": "aborted",
713                    "message": "ceremony aborted by operator",
714                },
715            }),
716        );
717    }
718
719    #[test]
720    fn step_attempt_failed() {
721        assert_json(
722            &StepFact::StepAttemptFailed {
723                step: StepId::new("import_key"),
724                attempt: 1,
725                error: ErrorRecord::new(
726                    ErrorClass::Environmental,
727                    "backend_error",
728                    "Token not present",
729                ),
730            },
731            &json!({
732                "type": "step_attempt_failed",
733                "step": "import_key",
734                "attempt": 1,
735                "error": {
736                    "class": "environmental",
737                    "kind": "backend_error",
738                    "message": "Token not present",
739                },
740            }),
741        );
742    }
743
744    #[test]
745    fn entropy_seeded() {
746        assert_json(
747            &StepFact::EntropySeeded {
748                m: "00112233".to_string(),
749                source: "os".to_string(),
750                derivation: "rite-kdf/v1".to_string(),
751            },
752            &json!({
753                "type": "entropy_seeded",
754                "m": "00112233",
755                "source": "os",
756                "derivation": "rite-kdf/v1",
757            }),
758        );
759    }
760
761    #[test]
762    fn entropy_contributed() {
763        assert_json(
764            &StepFact::EntropyContributed {
765                step: StepId::new("roll_dice"),
766                epoch: 1,
767                contribution: "3 1 6 4 2 5".to_string(),
768            },
769            &json!({
770                "type": "entropy_contributed",
771                "step": "roll_dice",
772                "epoch": 1,
773                "contribution": "3 1 6 4 2 5",
774            }),
775        );
776    }
777
778    #[test]
779    fn entropy_drawn() {
780        assert_json(
781            &StepFact::EntropyDrawn {
782                step: StepId::new("issue"),
783                path: "0/issue/cert-serial".to_string(),
784                value: "aabbccddeeff00112233".to_string(),
785            },
786            &json!({
787                "type": "entropy_drawn",
788                "step": "issue",
789                "path": "0/issue/cert-serial",
790                "value": "aabbccddeeff00112233",
791            }),
792        );
793    }
794}