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: "artifacts/root.crt".into(),
644 sha256: "a".repeat(64),
645 },
646 &json!({
647 "type": "artifact_written",
648 "step": "s1",
649 "name": "root.crt",
650 "path": "artifacts/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}