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_key`, `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 key held outside the ceremony was used as the recipient of a wrap.
232 ///
233 /// Its own fact rather than a field on the wrap operation: the method of a
234 /// wrap is publishable and the identity of a custodian may not be, and a
235 /// fact carries one confidentiality level.
236 ///
237 /// The fingerprint records the bytes that were present. Nothing in the
238 /// ceremony corroborates that they belong to the intended recipient, and
239 /// nothing shows the recipient can open the result. The fact states an
240 /// assumption the ceremony rests on, not something Rite proved.
241 WrapRecipientRecorded {
242 /// Step that wrapped to this recipient.
243 step: StepId,
244 /// The artifact or material the recipient key came from, as named in
245 /// the ceremony.
246 source: String,
247 /// `sha256:<hex>` over the recipient's SPKI DER.
248 fingerprint: String,
249 /// Whether the ceremony declared this fingerprint in advance, so the
250 /// runtime checked the key it received against the definition rather
251 /// than recording whatever arrived.
252 declared: bool,
253 },
254 /// A human attestation was recorded.
255 AttestationRecorded {
256 /// Step under which the attestation was recorded.
257 step: StepId,
258 /// Role that issued the attestation.
259 role: RoleId,
260 /// Verbatim attestation statement.
261 statement: String,
262 },
263 /// An artifact was written to disk.
264 ArtifactWritten {
265 /// Step that produced the artifact.
266 step: StepId,
267 /// Artifact name as declared in the DSL.
268 name: String,
269 /// Path on disk.
270 path: PathBuf,
271 /// Lowercase hex SHA-256 of the artifact bytes.
272 sha256: String,
273 },
274 /// A deviation was logged by the operator.
275 DeviationRecorded {
276 /// Step in which the deviation was logged, if any. `None` for
277 /// deviations logged outside of a step (before the first step or
278 /// while a ceremony-level prompt is pending).
279 #[serde(skip_serializing_if = "Option::is_none")]
280 step: Option<StepId>,
281 /// Verbatim deviation text.
282 text: String,
283 },
284 /// A step attempt failed. Recorded per attempt, so a retried step shows
285 /// `StepAttemptFailed{attempt: 1}` followed by the operator's retry
286 /// decision and, on success, `StepCompleted`. The final attempt of a step
287 /// that the run gives up on is followed by the terminal `CeremonyFailed`.
288 StepAttemptFailed {
289 /// Step whose attempt failed.
290 step: StepId,
291 /// 1-based attempt number within this step.
292 attempt: u32,
293 /// Structured error record for the failed attempt.
294 error: ErrorRecord,
295 },
296 /// Step finished executing.
297 StepCompleted {
298 /// Step identifier.
299 id: StepId,
300 /// Outcome (completed or skipped).
301 outcome: StepOutcome,
302 },
303 /// Ceremony finished successfully.
304 ///
305 /// The transcript's cryptographic identity is `SHA-256(line_bytes)`
306 /// for this line, recoverable by any reader, so the fact itself
307 /// carries no fingerprint field. The runtime forwards the value to
308 /// frontends through an out-of-band channel event.
309 CeremonyCompleted {},
310 /// Ceremony failed or was aborted.
311 CeremonyFailed {
312 /// Structured error record.
313 error: ErrorRecord,
314 },
315 /// The ceremony entropy source was seeded with machine randomness.
316 ///
317 /// Emitted once by the runner at ceremony start (run-metadata). Records
318 /// the machine contribution `m` and the frozen derivation scheme so any
319 /// value the ceremony later draws is re-derivable from the transcript
320 /// alone. Part of the [entropy source](StepFact::EntropyDrawn) family.
321 ///
322 /// Like every fact, it is timed by the `at` on its chain envelope.
323 EntropySeeded {
324 /// Lowercase hex of the gathered machine entropy `m`.
325 m: String,
326 /// Provenance of `m` (e.g. `os`). A single label today; comma-separated
327 /// if more than one source is ever mixed.
328 source: String,
329 /// Frozen derivation-scheme tag (e.g. `rite-kdf/v1`) that pins the
330 /// entire construction. A verifier rejects an unrecognised value.
331 derivation: String,
332 },
333 /// A human folded additional entropy into the seed, advancing the ratchet.
334 ///
335 /// Emitted by the authored `gather_entropy` step. The verbatim operator
336 /// contribution is recorded so the epoch chain re-folds identically; it is
337 /// public, witnessed entropy, not a secret. Timed by its enclosing step
338 /// boundaries (and by the `PromptAnswered` that captured the input).
339 EntropyContributed {
340 /// Step under which the contribution was gathered.
341 step: StepId,
342 /// Epoch index produced by this fold (1 for the first contribution).
343 epoch: u32,
344 /// Verbatim operator contribution, fed as UTF-8 into the ratchet.
345 contribution: String,
346 },
347 /// A value was drawn from the entropy source (a nonce, certificate serial,
348 /// or challenge).
349 ///
350 /// Emitted whenever an action draws bytes from the entropy source. The
351 /// derivation `path` plus the recorded seed let `rite verify` re-derive
352 /// the value and confirm the right value reached the right consumer. Like
353 /// other action-emitted evidence, it is timed by its enclosing step.
354 EntropyDrawn {
355 /// Step that drew the value.
356 step: StepId,
357 /// Derivation path `<epoch>/<step>/<purpose>`.
358 path: String,
359 /// Lowercase hex of the derived bytes. Its length fixes the byte count,
360 /// so `rite verify` re-derives exactly this many bytes from the seed.
361 value: String,
362 },
363}
364
365impl StepFact {
366 /// Whether this fact evidences work performed on the world: a backend
367 /// operation, a written artifact, a captured attestation, or consumed
368 /// entropy. Interaction records (an answered prompt, an operator
369 /// deviation note) and lifecycle markers are not side effects: repeating
370 /// the interaction is safe and simply produces a fresh record.
371 ///
372 /// The runtime's retry gate refuses to re-execute a step attempt that
373 /// already emitted a side-effect fact.
374 #[must_use]
375 pub fn is_side_effect(&self) -> bool {
376 match self {
377 StepFact::BackendOperation { .. }
378 | StepFact::AttestationRecorded { .. }
379 | StepFact::ArtifactWritten { .. }
380 | StepFact::EntropyContributed { .. }
381 | StepFact::EntropyDrawn { .. } => true,
382 StepFact::CeremonyStarted { .. }
383 | StepFact::ActStarted { .. }
384 | StepFact::StepStarted { .. }
385 | StepFact::PromptAnswered { .. }
386 // Records an input the step read, not work it performed.
387 | StepFact::WrapRecipientRecorded { .. }
388 | StepFact::DeviationRecorded { .. }
389 | StepFact::StepAttemptFailed { .. }
390 | StepFact::StepCompleted { .. }
391 | StepFact::CeremonyCompleted {}
392 | StepFact::CeremonyFailed { .. }
393 | StepFact::EntropySeeded { .. } => false,
394 }
395 }
396}
397
398/// JSON-shape snapshot tests, the tripwire for accidental wire-format breaks.
399///
400/// Every variant of [`StepFact`], [`Prompt`], [`ResponseRecord`], [`StepOutcome`],
401/// and [`ValidatorSpec`] is serialized once with fixed payloads and compared
402/// against an inline JSON literal. The on-disk transcript schema is what
403/// `rite verify`, `rite report`, and any third-party verifier consume; a
404/// rename, a `serde(tag)` change, a `rename_all` flip, or a timestamp format
405/// swap must surface here, not in the field.
406///
407/// Breaking the format is allowed in early beta, **deliberately**, update
408/// the fixture in the same commit so the diff documents the wire change.
409#[cfg(test)]
410mod schema_snapshot_tests {
411 use super::*;
412 use serde_json::json;
413
414 fn assert_json(fact: &StepFact, expected: &serde_json::Value) {
415 let actual = serde_json::to_value(fact).expect("serialize StepFact");
416 assert_eq!(&actual, expected, "wire-format drift for {fact:?}");
417 }
418
419 #[test]
420 fn ceremony_started() {
421 assert_json(
422 &StepFact::CeremonyStarted {
423 name: "Root CA".to_string(),
424 },
425 &json!({
426 "type": "ceremony_started",
427 "name": "Root CA",
428 }),
429 );
430 }
431
432 #[test]
433 fn act_started() {
434 assert_json(
435 &StepFact::ActStarted {
436 id: ActId::new("setup"),
437 label: "Setup".to_string(),
438 },
439 &json!({
440 "type": "act_started",
441 "id": "setup",
442 "label": "Setup",
443 }),
444 );
445 }
446
447 #[test]
448 fn step_started() {
449 assert_json(
450 &StepFact::StepStarted {
451 id: StepId::new("s1"),
452 label: "2.1".to_string(),
453 role: RoleId::new("crypto_officer"),
454 role_name: "Crypto Officer".to_string(),
455 },
456 &json!({
457 "type": "step_started",
458 "id": "s1",
459 "label": "2.1",
460 "role": "crypto_officer",
461 "role_name": "Crypto Officer",
462 }),
463 );
464 }
465
466 #[test]
467 fn prompt_answered_confirm_bool() {
468 assert_json(
469 &StepFact::PromptAnswered {
470 step: Some(StepId::new("s1")),
471 prompt: Prompt::Confirm {
472 question: "Proceed?".to_string(),
473 default: Some(true),
474 },
475 response: ResponseRecord::Bool { value: true },
476 },
477 &json!({
478 "type": "prompt_answered",
479 "step": "s1",
480 "prompt": { "type": "confirm", "question": "Proceed?", "default": true },
481 "response": { "type": "bool", "value": true },
482 }),
483 );
484 }
485
486 #[test]
487 fn prompt_answered_text_nonempty() {
488 assert_json(
489 &StepFact::PromptAnswered {
490 step: Some(StepId::new("s1")),
491 prompt: Prompt::Text {
492 label: "Name".to_string(),
493 validator: ValidatorSpec::NonEmpty,
494 },
495 response: ResponseRecord::Text {
496 value: "Alice".to_string(),
497 },
498 },
499 &json!({
500 "type": "prompt_answered",
501 "step": "s1",
502 "prompt": {
503 "type": "text",
504 "label": "Name",
505 "validator": { "kind": "non_empty" },
506 },
507 "response": { "type": "text", "value": "Alice" },
508 }),
509 );
510 }
511
512 #[test]
513 fn prompt_answered_text_regex() {
514 assert_json(
515 &StepFact::PromptAnswered {
516 step: Some(StepId::new("s1")),
517 prompt: Prompt::Text {
518 label: "SN".to_string(),
519 validator: ValidatorSpec::Regex(r"^[A-Z0-9]+$".to_string()),
520 },
521 response: ResponseRecord::Text {
522 value: "AB12".to_string(),
523 },
524 },
525 &json!({
526 "type": "prompt_answered",
527 "step": "s1",
528 "prompt": {
529 "type": "text",
530 "label": "SN",
531 "validator": { "kind": "regex", "value": "^[A-Z0-9]+$" },
532 },
533 "response": { "type": "text", "value": "AB12" },
534 }),
535 );
536 }
537
538 #[test]
539 fn prompt_answered_text_predefined() {
540 assert_json(
541 &StepFact::PromptAnswered {
542 step: Some(StepId::new("s1")),
543 prompt: Prompt::Text {
544 label: "SN".to_string(),
545 validator: ValidatorSpec::Predefined("serial_number".to_string()),
546 },
547 response: ResponseRecord::Text {
548 value: "ABCD".to_string(),
549 },
550 },
551 &json!({
552 "type": "prompt_answered",
553 "step": "s1",
554 "prompt": {
555 "type": "text",
556 "label": "SN",
557 "validator": { "kind": "predefined", "value": "serial_number" },
558 },
559 "response": { "type": "text", "value": "ABCD" },
560 }),
561 );
562 }
563
564 #[test]
565 fn prompt_answered_secret_redacted() {
566 assert_json(
567 &StepFact::PromptAnswered {
568 step: Some(StepId::new("s1")),
569 prompt: Prompt::Secret {
570 label: "PIN".to_string(),
571 },
572 response: ResponseRecord::SecretRedacted {},
573 },
574 &json!({
575 "type": "prompt_answered",
576 "step": "s1",
577 "prompt": { "type": "secret", "label": "PIN" },
578 "response": { "type": "secret_redacted" },
579 }),
580 );
581 }
582
583 #[test]
584 fn prompt_answered_literal_text() {
585 assert_json(
586 &StepFact::PromptAnswered {
587 step: Some(StepId::new("s1")),
588 prompt: Prompt::Literal {
589 label: "Type 'attest'".to_string(),
590 expected: "attest".to_string(),
591 },
592 response: ResponseRecord::Text {
593 value: "attest".to_string(),
594 },
595 },
596 &json!({
597 "type": "prompt_answered",
598 "step": "s1",
599 "prompt": { "type": "literal", "label": "Type 'attest'", "expected": "attest" },
600 "response": { "type": "text", "value": "attest" },
601 }),
602 );
603 }
604
605 #[test]
606 fn prompt_answered_continue_acknowledged() {
607 assert_json(
608 &StepFact::PromptAnswered {
609 step: Some(StepId::new("s1")),
610 prompt: Prompt::Continue {
611 hint: Some("Press Enter".to_string()),
612 },
613 response: ResponseRecord::Acknowledged,
614 },
615 &json!({
616 "type": "prompt_answered",
617 "step": "s1",
618 "prompt": { "type": "continue", "hint": "Press Enter" },
619 "response": { "type": "acknowledged" },
620 }),
621 );
622 }
623
624 #[test]
625 fn backend_operation() {
626 assert_json(
627 &StepFact::BackendOperation {
628 step: StepId::new("s1"),
629 kind: "generate_key".to_string(),
630 inputs: json!({ "algorithm": "rsa", "bits": 4096 }),
631 outputs: json!({ "key_id": "k1" }),
632 fingerprint: Some("sha256:deadbeef".to_string()),
633 },
634 &json!({
635 "type": "backend_operation",
636 "step": "s1",
637 "kind": "generate_key",
638 "inputs": { "algorithm": "rsa", "bits": 4096 },
639 "outputs": { "key_id": "k1" },
640 "fingerprint": "sha256:deadbeef",
641 }),
642 );
643 }
644
645 #[test]
646 fn wrap_recipient_recorded() {
647 assert_json(
648 &StepFact::WrapRecipientRecorded {
649 step: StepId::new("s1"),
650 source: "escrow_pubkey".to_string(),
651 fingerprint: "sha256:deadbeef".to_string(),
652 declared: true,
653 },
654 &json!({
655 "type": "wrap_recipient_recorded",
656 "step": "s1",
657 "source": "escrow_pubkey",
658 "fingerprint": "sha256:deadbeef",
659 "declared": true,
660 }),
661 );
662 }
663
664 #[test]
665 fn attestation_recorded() {
666 assert_json(
667 &StepFact::AttestationRecorded {
668 step: StepId::new("s1"),
669 role: RoleId::new("crypto_officer"),
670 statement: "I confirm.".to_string(),
671 },
672 &json!({
673 "type": "attestation_recorded",
674 "step": "s1",
675 "role": "crypto_officer",
676 "statement": "I confirm.",
677 }),
678 );
679 }
680
681 #[test]
682 fn artifact_written() {
683 assert_json(
684 &StepFact::ArtifactWritten {
685 step: StepId::new("s1"),
686 name: "root.crt".to_string(),
687 path: "artifacts/root.crt".into(),
688 sha256: "a".repeat(64),
689 },
690 &json!({
691 "type": "artifact_written",
692 "step": "s1",
693 "name": "root.crt",
694 "path": "artifacts/root.crt",
695 "sha256": "a".repeat(64),
696 }),
697 );
698 }
699
700 #[test]
701 fn deviation_recorded() {
702 assert_json(
703 &StepFact::DeviationRecorded {
704 step: Some(StepId::new("s1")),
705 text: "phone rang".to_string(),
706 },
707 &json!({
708 "type": "deviation_recorded",
709 "step": "s1",
710 "text": "phone rang",
711 }),
712 );
713 }
714
715 #[test]
716 fn step_completed_completed() {
717 assert_json(
718 &StepFact::StepCompleted {
719 id: StepId::new("s1"),
720 outcome: StepOutcome::Completed {
721 message: "done".to_string(),
722 },
723 },
724 &json!({
725 "type": "step_completed",
726 "id": "s1",
727 "outcome": { "status": "completed", "message": "done" },
728 }),
729 );
730 }
731
732 #[test]
733 fn ceremony_completed() {
734 assert_json(
735 &StepFact::CeremonyCompleted {},
736 &json!({
737 "type": "ceremony_completed",
738 }),
739 );
740 }
741
742 #[test]
743 fn ceremony_failed() {
744 assert_json(
745 &StepFact::CeremonyFailed {
746 error: ErrorRecord::new(
747 ErrorClass::Abort,
748 "aborted",
749 "ceremony aborted by operator",
750 ),
751 },
752 &json!({
753 "type": "ceremony_failed",
754 "error": {
755 "class": "abort",
756 "kind": "aborted",
757 "message": "ceremony aborted by operator",
758 },
759 }),
760 );
761 }
762
763 #[test]
764 fn step_attempt_failed() {
765 assert_json(
766 &StepFact::StepAttemptFailed {
767 step: StepId::new("import_key"),
768 attempt: 1,
769 error: ErrorRecord::new(
770 ErrorClass::Environmental,
771 "backend_error",
772 "Token not present",
773 ),
774 },
775 &json!({
776 "type": "step_attempt_failed",
777 "step": "import_key",
778 "attempt": 1,
779 "error": {
780 "class": "environmental",
781 "kind": "backend_error",
782 "message": "Token not present",
783 },
784 }),
785 );
786 }
787
788 #[test]
789 fn entropy_seeded() {
790 assert_json(
791 &StepFact::EntropySeeded {
792 m: "00112233".to_string(),
793 source: "os".to_string(),
794 derivation: "rite-kdf/v1".to_string(),
795 },
796 &json!({
797 "type": "entropy_seeded",
798 "m": "00112233",
799 "source": "os",
800 "derivation": "rite-kdf/v1",
801 }),
802 );
803 }
804
805 #[test]
806 fn entropy_contributed() {
807 assert_json(
808 &StepFact::EntropyContributed {
809 step: StepId::new("roll_dice"),
810 epoch: 1,
811 contribution: "3 1 6 4 2 5".to_string(),
812 },
813 &json!({
814 "type": "entropy_contributed",
815 "step": "roll_dice",
816 "epoch": 1,
817 "contribution": "3 1 6 4 2 5",
818 }),
819 );
820 }
821
822 #[test]
823 fn entropy_drawn() {
824 assert_json(
825 &StepFact::EntropyDrawn {
826 step: StepId::new("issue"),
827 path: "0/issue/cert-serial".to_string(),
828 value: "aabbccddeeff00112233".to_string(),
829 },
830 &json!({
831 "type": "entropy_drawn",
832 "step": "issue",
833 "path": "0/issue/cert-serial",
834 "value": "aabbccddeeff00112233",
835 }),
836 );
837 }
838}