1use 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
22pub const TRANSCRIPT_FORMAT: u32 = 0;
29
30pub const FACT_VOCABULARY: u32 = 0;
33
34pub const TRANSCRIPT_SCHEMA: &str = concat!(
38 "https://ritely.io/schemas/",
39 env!("CARGO_PKG_VERSION"),
40 "/transcript.schema.json"
41);
42
43#[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 #[cfg_attr(
61 test,
62 schemars(description = "Version of the line format and the chain rule.")
63 )]
64 pub rite_transcript: u32,
65 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
135 pub fn declares(&self, level: Level) -> bool {
136 self.levels.values().any(|declared| *declared == level)
137 }
138
139 #[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#[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 pub const PUBLIC: Level = Level(10);
185 pub const RESTRICTED: Level = Level(20);
187 pub const CONFIDENTIAL: Level = Level(30);
189
190 pub const BUILT_IN: [(&'static str, Level); 3] = [
192 ("public", Level::PUBLIC),
193 ("restricted", Level::RESTRICTED),
194 ("confidential", Level::CONFIDENTIAL),
195 ];
196
197 #[must_use]
199 pub const fn new(value: u32) -> Self {
200 Self(value)
201 }
202
203 #[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#[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 #[cfg_attr(test, schemars(description = "The answer is not empty or blank."))]
235 NonEmpty,
236 #[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 #[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 #[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 #[cfg_attr(test, schemars(description = "What kind of value the answer is."))]
271 format: Format,
272 #[cfg_attr(test, schemars(description = "The fewest units accepted, if bounded."))]
274 min_length: Option<usize>,
275 #[cfg_attr(test, schemars(description = "The most units accepted, if bounded."))]
277 max_length: Option<usize>,
278 },
279}
280
281#[non_exhaustive]
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
290#[serde(rename_all = "snake_case")]
291#[schemars(description = "What kind of value a person types.")]
292pub enum Format {
293 #[schemars(description = "Anything the person can type.")]
295 Text,
296 #[schemars(description = "Decimal digits only.")]
298 Digits,
299 #[schemars(description = "ASCII letters and digits.")]
301 Alphanumeric,
302 #[schemars(description = "Bytes as hexadecimal, in either case, two digits per byte.")]
304 Hex,
305 #[schemars(description = "Bytes as standard base64 with padding.")]
307 Base64,
308 #[schemars(
312 description = "Bytes as paper32 rows, typed a row at a time from a sheet, a wrong \
313 character in a row corrected."
314 )]
315 Paper32,
316 #[schemars(
320 description = "A BIP-39 recovery phrase in the English list, typed a word at a \
321 time; the value is its entropy, and a length counts words."
322 )]
323 Bip39,
324}
325
326crate::types::vocabulary!(Format {
327 Text => "text",
328 Digits => "digits",
329 Alphanumeric => "alphanumeric",
330 Hex => "hex",
331 Base64 => "base64",
332 Paper32 => "paper32",
333 Bip39 => "bip39",
334});
335
336impl Format {
337 #[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 #[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 #[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 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 pub fn decode(self, value: &str) -> Result<Vec<u8>, String> {
385 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 Format::Paper32 => crate::paper32::decode(value)
397 .map(|mut decoded| std::mem::take(&mut *decoded.bytes))
398 .map_err(|e| e.to_string()),
399 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 #[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 #[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 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
452pub const PLACEHOLDER_LIMIT: usize = 64 * 1024;
456
457impl std::fmt::Display for Format {
458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459 f.write_str(self.as_str())
460 }
461}
462
463impl std::str::FromStr for Format {
464 type Err = String;
465
466 fn from_str(s: &str) -> Result<Self, Self::Err> {
467 <Self as crate::types::Vocabulary>::from_name(s).ok_or_else(|| {
468 format!(
469 "unknown format '{s}': expected {}",
470 crate::types::either(Format::ALL.iter().map(|format| format.as_str()))
471 )
472 })
473 }
474}
475
476impl ValidatorSpec {
477 pub fn check(&self, value: &str) -> Result<(), String> {
484 match self {
485 ValidatorSpec::NonEmpty => {
486 if value.trim().is_empty() {
487 Err("value must not be empty".to_string())
488 } else {
489 Ok(())
490 }
491 }
492 ValidatorSpec::Regex(pattern) => {
493 let regex = compile_pattern(pattern)?;
494 if regex.is_match(value) {
495 Ok(())
496 } else {
497 Err(format!("value must match {pattern}"))
498 }
499 }
500 ValidatorSpec::Predefined(name) => Err(format!("unknown validator: {name}")),
502 ValidatorSpec::Format {
503 format,
504 min_length,
505 max_length,
506 } => {
507 let hint = self.hint().unwrap_or_default();
508 if value.trim().is_empty() {
509 return Err("value must not be empty".to_string());
510 }
511 let length = if format.is_encoding() {
515 format.length_of(Zeroizing::new(format.decode(value)?).len())
516 } else {
517 if !value.chars().all(|c| format.accepts(c)) {
518 return Err(format!("value must be {hint}"));
519 }
520 value.chars().count()
521 };
522 if min_length.is_some_and(|min| length < min)
523 || max_length.is_some_and(|max| length > max)
524 {
525 return Err(format!("value must be {hint}, this is {length}"));
526 }
527 Ok(())
528 }
529 }
530 }
531
532 #[must_use]
538 pub fn hint(&self) -> Option<String> {
539 match self {
540 ValidatorSpec::NonEmpty | ValidatorSpec::Predefined(_) => None,
541 ValidatorSpec::Regex(pattern) => Some(format!("matching {pattern}")),
542 ValidatorSpec::Format {
543 format,
544 min_length,
545 max_length,
546 } => {
547 let what = format.describe();
548 Some(match (min_length, max_length) {
549 (Some(min), Some(max)) if min == max => format!("{min} {what}"),
550 (Some(min), Some(max)) => format!("{min} to {max} {what}"),
551 (Some(min), None) => format!("at least {min} {what}"),
552 (None, Some(max)) => format!("at most {max} {what}"),
553 (None, None) if format.is_encoding() => what.to_string(),
554 (None, None) => format!("{what} only"),
555 })
556 }
557 }
558 }
559}
560
561pub fn compile_pattern(pattern: &str) -> Result<regex_lite::Regex, String> {
571 regex_lite::Regex::new(&format!("^(?:{pattern})$"))
572 .map_err(|e| format!("invalid pattern '{pattern}': {e}"))
573}
574
575#[non_exhaustive]
578#[cfg_attr(test, derive(schemars::JsonSchema))]
579#[derive(Debug, Clone, Serialize, Deserialize)]
580#[serde(tag = "type", rename_all = "snake_case")]
581#[cfg_attr(test, schemars(description = "A prompt as it was shown."))]
582pub enum Prompt {
583 #[cfg_attr(test, schemars(description = "A yes or no question."))]
585 Confirm {
586 #[cfg_attr(test, schemars(description = "The question."))]
588 question: String,
589 #[cfg_attr(
591 test,
592 schemars(
593 description = "The answer given when the person confirms without choosing, if \
594 any."
595 )
596 )]
597 default: Option<bool>,
598 },
599 #[cfg_attr(
601 test,
602 schemars(description = "A request for free text, checked before it is accepted.")
603 )]
604 Text {
605 #[cfg_attr(test, schemars(description = "The label shown."))]
607 label: String,
608 #[cfg_attr(test, schemars(description = "The check the answer had to pass."))]
610 validator: ValidatorSpec,
611 },
612 #[cfg_attr(
615 test,
616 schemars(
617 description = "A request for a secret, such as a PIN. The answer is never \
618 recorded."
619 )
620 )]
621 Secret {
622 #[cfg_attr(test, schemars(description = "The label shown."))]
624 label: String,
625 #[cfg_attr(test, schemars(description = "The check the answer had to pass."))]
627 validator: ValidatorSpec,
628 },
629 #[cfg_attr(
632 test,
633 schemars(
634 description = "A request to type a given text exactly, such as a confirmation \
635 phrase."
636 )
637 )]
638 Literal {
639 #[cfg_attr(test, schemars(description = "The label shown."))]
641 label: String,
642 #[cfg_attr(test, schemars(description = "The text that had to be typed."))]
644 expected: String,
645 },
646 #[cfg_attr(
648 test,
649 schemars(description = "A pause until the person is ready to continue.")
650 )]
651 Continue {
652 #[cfg_attr(test, schemars(description = "The hint shown, if any."))]
654 hint: Option<String>,
655 },
656 #[cfg_attr(
660 test,
661 schemars(
662 description = "A value shown for a person to write down, withdrawn when they \
663 acknowledge. The value is never recorded."
664 )
665 )]
666 Reveal {
667 #[cfg_attr(test, schemars(description = "The label shown above the value."))]
669 label: String,
670 #[serde(default, skip_serializing_if = "Option::is_none")]
672 #[cfg_attr(test, schemars(description = "The note shown with the value, if any."))]
673 note: Option<String>,
674 #[serde(skip)]
677 shown: Shown,
678 },
679 #[cfg_attr(
683 test,
684 schemars(
685 description = "A request for a value typed row by row from a sheet, each row \
686 checked as it is entered. The answer is never recorded."
687 )
688 )]
689 EnterRows {
690 #[cfg_attr(test, schemars(description = "The label shown above the rows."))]
692 label: String,
693 #[serde(default, skip_serializing_if = "Option::is_none")]
695 #[cfg_attr(test, schemars(description = "The note shown with the rows, if any."))]
696 note: Option<String>,
697 #[cfg_attr(test, schemars(description = "The encoding the sheet was written in."))]
699 format: crate::display::RevealFormat,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
703 #[cfg_attr(
704 test,
705 schemars(
706 description = "The number of rows asked for, when the value's length is known. \
707 Without it a short row, or an empty row after full ones, ends the entry."
708 )
709 )]
710 rows: Option<usize>,
711 #[serde(default, skip_serializing_if = "Option::is_none")]
716 #[cfg_attr(
717 test,
718 schemars(
719 description = "The check the whole value had to pass. Absent when the step \
720 checks the value itself, as for a share."
721 )
722 )]
723 validator: Option<ValidatorSpec>,
724 },
725}
726
727impl Prompt {
728 #[must_use]
732 pub fn for_record(&self) -> Self {
733 match self {
734 Prompt::Reveal { label, note, .. } => Prompt::Reveal {
735 label: label.clone(),
736 note: note.clone(),
737 shown: Shown::default(),
738 },
739 other => other.clone(),
740 }
741 }
742}
743
744#[derive(Clone, Default)]
748pub struct Shown {
749 text: zeroize::Zeroizing<String>,
750 layout: crate::display::Layout,
751}
752
753impl Shown {
754 pub fn new(text: String, layout: crate::display::Layout) -> Self {
756 Self {
757 text: zeroize::Zeroizing::new(text),
758 layout,
759 }
760 }
761
762 pub fn text(&self) -> &str {
764 &self.text
765 }
766
767 pub fn layout(&self) -> &crate::display::Layout {
769 &self.layout
770 }
771}
772
773impl std::fmt::Debug for Shown {
774 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
775 write!(
776 f,
777 "Shown({} characters, {})",
778 self.text.len(),
779 self.layout.format
780 )
781 }
782}
783
784#[non_exhaustive]
791#[cfg_attr(test, derive(schemars::JsonSchema))]
792#[derive(Debug, Clone, Serialize, Deserialize)]
793#[serde(tag = "type", rename_all = "snake_case")]
794#[cfg_attr(
795 test,
796 schemars(
797 description = "The answer to a prompt, as recorded. A secret answer is never \
798 recorded, not even as a digest."
799 )
800)]
801pub enum ResponseRecord {
802 #[cfg_attr(test, schemars(description = "A yes or no answer."))]
804 Bool {
805 #[cfg_attr(test, schemars(description = "The answer."))]
807 value: bool,
808 },
809 #[cfg_attr(test, schemars(description = "A free-text answer."))]
811 Text {
812 #[cfg_attr(test, schemars(description = "The answer, as typed."))]
814 value: String,
815 },
816 #[cfg_attr(
825 test,
826 schemars(
827 description = "A secret was entered. Only the fact that it was entered is \
828 recorded."
829 )
830 )]
831 SecretRedacted {},
832 #[cfg_attr(test, schemars(description = "The person continued past a pause."))]
834 Acknowledged,
835}
836
837#[non_exhaustive]
846#[cfg_attr(test, derive(schemars::JsonSchema))]
847#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
848#[serde(rename_all = "snake_case")]
849#[cfg_attr(test, schemars(description = "What kind of bad outcome this was."))]
850pub enum ErrorClass {
851 #[cfg_attr(
854 test,
855 schemars(
856 description = "Something around the ceremony was not ready, such as a device not \
857 connected, and the step's work did not happen."
858 )
859 )]
860 Environmental,
861 #[cfg_attr(
864 test,
865 schemars(
866 description = "The ceremony's own logic concluded badly, such as a verification \
867 that did not match or a refused attestation."
868 )
869 )]
870 Procedural,
871 #[cfg_attr(
874 test,
875 schemars(
876 description = "The run itself cannot be trusted to continue, or the ceremony \
877 definition is broken."
878 )
879 )]
880 Integrity,
881 #[cfg_attr(
884 test,
885 schemars(description = "An operator chose to stop the ceremony.")
886 )]
887 Abort,
888}
889
890#[non_exhaustive]
892#[cfg_attr(test, derive(schemars::JsonSchema))]
893#[derive(Debug, Clone, Serialize, Deserialize)]
894#[cfg_attr(
895 test,
896 schemars(
897 description = "What went wrong: a class for audit, a stable kind, and a message for \
898 people."
899 )
900)]
901pub struct ErrorRecord {
902 #[cfg_attr(test, schemars(description = "The kind of bad outcome, for audit."))]
904 pub class: ErrorClass,
905 #[cfg_attr(
907 test,
908 schemars(description = "A stable label for the error, such as aborted or step_failed.")
909 )]
910 pub kind: String,
911 #[cfg_attr(test, schemars(description = "A description of the error for people."))]
913 pub message: String,
914}
915
916impl ErrorRecord {
917 pub fn new(class: ErrorClass, kind: impl Into<String>, message: impl Into<String>) -> Self {
919 Self {
920 class,
921 kind: kind.into(),
922 message: message.into(),
923 }
924 }
925}
926
927#[non_exhaustive]
929#[cfg_attr(test, derive(schemars::JsonSchema))]
930#[derive(Debug, Clone, Serialize, Deserialize)]
931#[serde(tag = "status", rename_all = "snake_case")]
932#[cfg_attr(test, schemars(description = "How a step ended."))]
933pub enum StepOutcome {
934 #[cfg_attr(test, schemars(description = "The step did its work."))]
936 Completed {
937 #[cfg_attr(
939 test,
940 schemars(description = "A short description of what the step did.")
941 )]
942 message: String,
943 },
944}
945
946#[non_exhaustive]
956#[cfg_attr(test, derive(schemars::JsonSchema))]
957#[derive(Debug, Clone, Serialize, Deserialize)]
958#[serde(tag = "type", rename_all = "snake_case")]
959#[cfg_attr(
960 test,
961 schemars(
962 description = "One recorded fact, identified by its `type`. Each fact type has a \
963 fixed set of fields in a given vocabulary."
964 )
965)]
966pub enum StepFact {
967 #[cfg_attr(test, schemars(description = "The ceremony started."))]
969 CeremonyStarted {
970 #[cfg_attr(test, schemars(description = "The ceremony's name."))]
972 name: String,
973 #[cfg_attr(
977 test,
978 schemars(
979 description = "Digest of the ceremony definition file the run was resolved \
980 from. The inputs of the run are recorded as their own facts, so this digest covers \
981 the definition only."
982 )
983 )]
984 template: Sha256Digest,
985 },
986 #[cfg_attr(
991 test,
992 schemars(
993 description = "A role the ceremony defines. Recorded once per role at the start, \
994 whether or not anyone is assigned to it; later facts name the role by its identifier."
995 )
996 )]
997 RoleDeclared {
998 #[cfg_attr(test, schemars(description = "The role."))]
1000 role: RoleId,
1001 #[cfg_attr(test, schemars(description = "The role's display name."))]
1003 name: String,
1004 },
1005 #[cfg_attr(
1009 test,
1010 schemars(
1011 description = "A person was assigned to a role for this run. One fact per \
1012 assignment, so each can be withheld on its own."
1013 )
1014 )]
1015 RoleAssigned {
1016 #[cfg_attr(test, schemars(description = "The role."))]
1018 role: RoleId,
1019 #[cfg_attr(
1022 test,
1023 schemars(
1024 description = "The person, as supplied when the run started. Recorded as \
1025 given: nothing in the transcript proves who the person is."
1026 )
1027 )]
1028 person: String,
1029 },
1030 #[cfg_attr(
1032 test,
1033 schemars(description = "A parameter took its value for this run, supplied or defaulted.")
1034 )]
1035 ParameterBound {
1036 #[cfg_attr(test, schemars(description = "The parameter."))]
1038 name: ParamId,
1039 #[cfg_attr(test, schemars(description = "The value, as the ceremony used it."))]
1041 value: serde_json::Value,
1042 },
1043 #[cfg_attr(
1049 test,
1050 schemars(
1051 description = "A material was loaded at the start of the run. The digest of a \
1052 digital material is a separate fact, material_digest, so the two can be disclosed to \
1053 different audiences."
1054 )
1055 )]
1056 MaterialLoaded {
1057 #[cfg_attr(test, schemars(description = "The material."))]
1059 name: MaterialId,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1063 #[cfg_attr(
1064 test,
1065 schemars(
1066 description = "The identifier of a physical or pre-provisioned material, such \
1067 as a serial number, when one was supplied."
1068 )
1069 )]
1070 identifier: Option<String>,
1071 },
1072 #[cfg_attr(
1074 test,
1075 schemars(description = "The digest of a digital material's bytes, as loaded.")
1076 )]
1077 MaterialDigest {
1078 #[cfg_attr(test, schemars(description = "The material."))]
1080 name: MaterialId,
1081 #[cfg_attr(test, schemars(description = "The digest of the material's bytes."))]
1083 digest: Sha256Digest,
1084 },
1085 #[cfg_attr(
1091 test,
1092 schemars(
1093 description = "A backend was used for the first time in this run. Recorded once \
1094 per backend, before its first operation; operations name the backend and do not repeat \
1095 its identity."
1096 )
1097 )]
1098 BackendBound {
1099 #[cfg_attr(test, schemars(description = "The backend's name in the ceremony."))]
1101 name: String,
1102 #[cfg_attr(
1104 test,
1105 schemars(description = "The kind of backend, such as openssl, yubikey or pkcs11.")
1106 )]
1107 provider: String,
1108 #[cfg_attr(
1111 test,
1112 schemars(
1113 description = "The identity the backend reports for itself, such as a device \
1114 serial number and firmware version."
1115 )
1116 )]
1117 identity: String,
1118 },
1119 #[cfg_attr(
1121 test,
1122 schemars(description = "A description of the machine the ceremony ran on.")
1123 )]
1124 MachineInfoRecorded {
1125 #[cfg_attr(test, schemars(description = "The step that recorded it."))]
1127 step: StepId,
1128 #[cfg_attr(
1130 test,
1131 schemars(
1132 description = "The parts of the machine description the step was asked to \
1133 record."
1134 )
1135 )]
1136 info: serde_json::Value,
1137 },
1138 #[cfg_attr(
1140 test,
1141 schemars(description = "An act, a named part of the ceremony, started.")
1142 )]
1143 ActStarted {
1144 #[cfg_attr(test, schemars(description = "The act."))]
1146 id: ActId,
1147 #[cfg_attr(
1149 test,
1150 schemars(description = "The act's label, as written in the ceremony.")
1151 )]
1152 label: String,
1153 },
1154 #[cfg_attr(test, schemars(description = "A step started."))]
1156 StepStarted {
1157 #[cfg_attr(test, schemars(description = "The step."))]
1159 id: StepId,
1160 #[cfg_attr(
1162 test,
1163 schemars(description = "The step's label, as written in the ceremony.")
1164 )]
1165 label: String,
1166 #[cfg_attr(
1169 test,
1170 schemars(
1171 description = "The role responsible for the step. Its display name is on the \
1172 role's role_declared fact."
1173 )
1174 )]
1175 role: RoleId,
1176 },
1177 #[cfg_attr(
1179 test,
1180 schemars(description = "Someone answered a prompt, and the answer was accepted.")
1181 )]
1182 PromptAnswered {
1183 #[serde(skip_serializing_if = "Option::is_none")]
1187 #[cfg_attr(
1188 test,
1189 schemars(
1190 description = "The step that asked, or absent for a prompt asked outside any \
1191 step."
1192 )
1193 )]
1194 step: Option<StepId>,
1195 #[cfg_attr(test, schemars(description = "The prompt, as shown."))]
1197 prompt: Prompt,
1198 #[cfg_attr(test, schemars(description = "The answer, as recorded."))]
1200 response: ResponseRecord,
1201 },
1202 #[cfg_attr(
1204 test,
1205 schemars(
1206 description = "A backend performed an operation. The inputs and outputs are \
1207 specific to the kind of operation."
1208 )
1209 )]
1210 BackendOperation {
1211 #[cfg_attr(test, schemars(description = "The step the operation ran in."))]
1213 step: StepId,
1214 #[cfg_attr(
1216 test,
1217 schemars(description = "The kind of operation, such as generate_key or sign_data.")
1218 )]
1219 kind: String,
1220 #[serde(skip_serializing_if = "Option::is_none")]
1224 #[cfg_attr(
1225 test,
1226 schemars(
1227 description = "The backend the step ran with, by the name the ceremony gives it. Its identity is on that backend's backend_bound fact. Absent for an operation done in software."
1228 )
1229 )]
1230 backend: Option<String>,
1231 #[cfg_attr(
1233 test,
1234 schemars(
1235 description = "What the operation was given: parameters and references to \
1236 artifacts or materials."
1237 )
1238 )]
1239 inputs: serde_json::Value,
1240 #[cfg_attr(
1242 test,
1243 schemars(description = "What the operation produced: artifact names and digests.")
1244 )]
1245 outputs: serde_json::Value,
1246 #[cfg_attr(
1248 test,
1249 schemars(
1250 description = "A fingerprint of the material the operation produced, when it \
1251 has one."
1252 )
1253 )]
1254 fingerprint: Option<String>,
1255 },
1256 #[cfg_attr(
1267 test,
1268 schemars(
1269 description = "A key held outside the ceremony received a wrapped key. A separate \
1270 fact from the operation, so the method of a wrap can be published while the recipient \
1271 stays withheld."
1272 )
1273 )]
1274 WrapRecipientRecorded {
1275 #[cfg_attr(test, schemars(description = "The step that wrapped the key."))]
1277 step: StepId,
1278 #[cfg_attr(
1281 test,
1282 schemars(
1283 description = "The artifact or material the recipient's key came from, by its \
1284 name in the ceremony."
1285 )
1286 )]
1287 source: String,
1288 #[cfg_attr(
1290 test,
1291 schemars(
1292 description = "The digest of the recipient's public key, as DER-encoded \
1293 SubjectPublicKeyInfo.",
1294 with = "Sha256Digest"
1295 )
1296 )]
1297 fingerprint: String,
1298 #[cfg_attr(
1302 test,
1303 schemars(
1304 description = "Whether the ceremony named this fingerprint in advance. When \
1305 it did, the key received was checked against it; otherwise the fingerprint records \
1306 whatever key arrived."
1307 )
1308 )]
1309 declared: bool,
1310 },
1311 #[cfg_attr(test, schemars(description = "A person made an attestation."))]
1313 AttestationRecorded {
1314 #[cfg_attr(test, schemars(description = "The step the attestation was made in."))]
1316 step: StepId,
1317 #[cfg_attr(test, schemars(description = "The role that made the attestation."))]
1319 role: RoleId,
1320 #[cfg_attr(test, schemars(description = "The statement, word for word."))]
1322 statement: String,
1323 },
1324 #[cfg_attr(
1326 test,
1327 schemars(description = "An artifact was written to the run's artifacts directory.")
1328 )]
1329 ArtifactWritten {
1330 #[cfg_attr(test, schemars(description = "The step that produced the artifact."))]
1332 step: StepId,
1333 #[cfg_attr(
1335 test,
1336 schemars(description = "The artifact's name, as declared in the ceremony.")
1337 )]
1338 name: String,
1339 #[cfg_attr(
1341 test,
1342 schemars(
1343 description = "The file name in the artifacts directory: a single path \
1344 component."
1345 )
1346 )]
1347 file: String,
1348 #[serde(skip_serializing_if = "Option::is_none")]
1352 #[cfg_attr(
1353 test,
1354 schemars(
1355 description = "The digest of the file's bytes. Absent for content a step \
1356 opened, since a digest of a secret can be tested against guesses."
1357 )
1358 )]
1359 digest: Option<Sha256Digest>,
1360 },
1361 #[cfg_attr(test, schemars(description = "An operator recorded a deviation."))]
1363 DeviationRecorded {
1364 #[serde(skip_serializing_if = "Option::is_none")]
1368 #[cfg_attr(
1369 test,
1370 schemars(
1371 description = "The step during which the deviation was recorded, or absent \
1372 outside any step."
1373 )
1374 )]
1375 step: Option<StepId>,
1376 #[cfg_attr(test, schemars(description = "The deviation, word for word."))]
1378 text: String,
1379 },
1380 #[cfg_attr(
1385 test,
1386 schemars(
1387 description = "One attempt at a step failed. Recorded per attempt: a step that is \
1388 retried and then succeeds has this fact for each failed attempt, then step_completed."
1389 )
1390 )]
1391 StepAttemptFailed {
1392 #[cfg_attr(test, schemars(description = "The step."))]
1394 step: StepId,
1395 #[cfg_attr(
1397 test,
1398 schemars(description = "The attempt's number within the step, starting at 1.")
1399 )]
1400 attempt: u32,
1401 #[cfg_attr(test, schemars(description = "What went wrong in this attempt."))]
1403 error: ErrorRecord,
1404 },
1405 #[cfg_attr(test, schemars(description = "A step finished."))]
1407 StepCompleted {
1408 #[cfg_attr(test, schemars(description = "The step."))]
1410 id: StepId,
1411 #[cfg_attr(test, schemars(description = "How the step ended."))]
1413 outcome: StepOutcome,
1414 },
1415 #[cfg_attr(
1422 test,
1423 schemars(
1424 description = "The ceremony finished. It is the last line of a transcript whose \
1425 run completed."
1426 )
1427 )]
1428 CeremonyCompleted {},
1429 #[cfg_attr(
1431 test,
1432 schemars(
1433 description = "The ceremony failed or was aborted. It is the last line of a \
1434 transcript whose run did not complete."
1435 )
1436 )]
1437 CeremonyFailed {
1438 #[cfg_attr(test, schemars(description = "What went wrong."))]
1440 error: ErrorRecord,
1441 },
1442 #[cfg_attr(
1451 test,
1452 schemars(
1453 description = "The ceremony's entropy source was seeded with machine randomness. \
1454 Recorded once, at the start, so every value drawn from the source can be derived again \
1455 from the transcript."
1456 )
1457 )]
1458 EntropySeeded {
1459 #[cfg_attr(
1461 test,
1462 schemars(
1463 description = "The machine randomness, as lowercase hex.",
1464 extend("pattern" = "^(?:[0-9a-f]{2})+$")
1465 )
1466 )]
1467 m: String,
1468 #[cfg_attr(
1471 test,
1472 schemars(description = "Where the machine randomness came from, such as os.")
1473 )]
1474 source: String,
1475 #[cfg_attr(
1478 test,
1479 schemars(
1480 description = "The derivation scheme, such as rite-kdf/v1. A verifier refuses \
1481 a scheme it does not know."
1482 )
1483 )]
1484 derivation: String,
1485 },
1486 #[cfg_attr(
1493 test,
1494 schemars(
1495 description = "A person added their own randomness to the entropy source, \
1496 starting a new epoch."
1497 )
1498 )]
1499 EntropyContributed {
1500 #[cfg_attr(test, schemars(description = "The step the contribution was made in."))]
1502 step: StepId,
1503 #[cfg_attr(
1505 test,
1506 schemars(description = "The epoch this contribution starts, counting from 1.")
1507 )]
1508 epoch: u32,
1509 #[cfg_attr(
1511 test,
1512 schemars(
1513 description = "The contribution, word for word, mixed into the source as \
1514 UTF-8."
1515 )
1516 )]
1517 contribution: String,
1518 },
1519 #[cfg_attr(
1527 test,
1528 schemars(
1529 description = "A value was drawn from the entropy source, such as a nonce, a \
1530 certificate serial number or a challenge. A verifier derives it again from the seed, \
1531 the contributions and the path."
1532 )
1533 )]
1534 EntropyDrawn {
1535 #[cfg_attr(test, schemars(description = "The step that drew the value."))]
1537 step: StepId,
1538 #[cfg_attr(
1540 test,
1541 schemars(description = "The derivation path: `<epoch>/<step>/<purpose>`.")
1542 )]
1543 path: String,
1544 #[cfg_attr(
1547 test,
1548 schemars(
1549 description = "The value, as lowercase hex. Its length is the number of bytes \
1550 drawn.",
1551 extend("pattern" = "^(?:[0-9a-f]{2})*$")
1552 )
1553 )]
1554 value: String,
1555 },
1556}
1557
1558pub const FACT_TYPES: &[&str] = &[
1563 "ceremony_started",
1564 "role_declared",
1565 "role_assigned",
1566 "parameter_bound",
1567 "material_loaded",
1568 "material_digest",
1569 "backend_bound",
1570 "machine_info_recorded",
1571 "act_started",
1572 "step_started",
1573 "prompt_answered",
1574 "backend_operation",
1575 "wrap_recipient_recorded",
1576 "attestation_recorded",
1577 "artifact_written",
1578 "deviation_recorded",
1579 "step_attempt_failed",
1580 "step_completed",
1581 "ceremony_completed",
1582 "ceremony_failed",
1583 "entropy_seeded",
1584 "entropy_contributed",
1585 "entropy_drawn",
1586];
1587
1588impl StepFact {
1589 #[must_use]
1600 pub fn default_level(&self) -> Level {
1601 match self {
1602 StepFact::CeremonyStarted { .. }
1603 | StepFact::RoleDeclared { .. }
1604 | StepFact::ActStarted { .. }
1605 | StepFact::StepStarted { .. }
1606 | StepFact::BackendOperation { .. }
1607 | StepFact::ArtifactWritten { .. }
1608 | StepFact::AttestationRecorded { .. }
1609 | StepFact::StepCompleted { .. }
1610 | StepFact::CeremonyCompleted {}
1611 | StepFact::CeremonyFailed { .. }
1612 | StepFact::EntropySeeded { .. }
1614 | StepFact::EntropyContributed { .. }
1615 | StepFact::EntropyDrawn { .. } => Level::PUBLIC,
1616 StepFact::ParameterBound { .. }
1617 | StepFact::MaterialLoaded { .. }
1618 | StepFact::BackendBound { .. }
1619 | StepFact::PromptAnswered { .. }
1620 | StepFact::WrapRecipientRecorded { .. }
1621 | StepFact::StepAttemptFailed { .. }
1622 | StepFact::DeviationRecorded { .. } => Level::RESTRICTED,
1623 StepFact::RoleAssigned { .. }
1624 | StepFact::MaterialDigest { .. }
1625 | StepFact::MachineInfoRecorded { .. } => Level::CONFIDENTIAL,
1626 }
1627 }
1628
1629 #[must_use]
1638 pub fn is_side_effect(&self) -> bool {
1639 match self {
1640 StepFact::BackendOperation { .. }
1641 | StepFact::AttestationRecorded { .. }
1642 | StepFact::ArtifactWritten { .. }
1643 | StepFact::EntropyContributed { .. }
1644 | StepFact::EntropyDrawn { .. } => true,
1645 StepFact::CeremonyStarted { .. }
1646 | StepFact::RoleDeclared { .. }
1648 | StepFact::RoleAssigned { .. }
1649 | StepFact::ParameterBound { .. }
1650 | StepFact::MaterialLoaded { .. }
1651 | StepFact::MaterialDigest { .. }
1652 | StepFact::BackendBound { .. }
1653 | StepFact::MachineInfoRecorded { .. }
1655 | StepFact::ActStarted { .. }
1656 | StepFact::StepStarted { .. }
1657 | StepFact::PromptAnswered { .. }
1658 | StepFact::WrapRecipientRecorded { .. }
1660 | StepFact::DeviationRecorded { .. }
1661 | StepFact::StepAttemptFailed { .. }
1662 | StepFact::StepCompleted { .. }
1663 | StepFact::CeremonyCompleted {}
1664 | StepFact::CeremonyFailed { .. }
1665 | StepFact::EntropySeeded { .. } => false,
1666 }
1667 }
1668}
1669
1670#[cfg(test)]
1682mod validator_tests {
1683 use super::*;
1684
1685 fn shape(format: Format, min: usize, max: usize) -> ValidatorSpec {
1686 ValidatorSpec::Format {
1687 format,
1688 min_length: Some(min),
1689 max_length: Some(max),
1690 }
1691 }
1692
1693 #[test]
1694 fn a_text_format_checks_characters_and_length() {
1695 let pin = shape(Format::Digits, 6, 8);
1696 assert!(pin.check("123456").is_ok());
1697 assert!(pin.check("12345678").is_ok());
1698 assert!(pin.check("12345").unwrap_err().contains("this is 5"));
1699 assert!(pin.check("123456789").unwrap_err().contains("this is 9"));
1700 assert!(pin.check("12345a").unwrap_err().contains("digits"));
1701
1702 assert!(shape(Format::Alphanumeric, 1, 4).check("ab12").is_ok());
1703 assert!(shape(Format::Alphanumeric, 1, 4).check("ab-1").is_err());
1704 assert!(shape(Format::Text, 1, 4).check("ab-1").is_ok());
1705 assert!(
1706 ValidatorSpec::Format {
1707 format: Format::Text,
1708 min_length: None,
1709 max_length: None,
1710 }
1711 .check(" ")
1712 .is_err(),
1713 "a value is still something"
1714 );
1715 }
1716
1717 #[test]
1720 fn a_pattern_must_match_the_whole_value() {
1721 let digits = ValidatorSpec::Regex("[0-9]+".to_string());
1722 assert!(digits.check("123").is_ok());
1723 assert!(digits.check("abc123").is_err());
1724 assert!(digits.check("123abc").is_err());
1725 let either = ValidatorSpec::Regex("yes|no".to_string());
1727 assert!(either.check("no").is_ok());
1728 assert!(either.check("nope").is_err());
1729 }
1730
1731 #[test]
1732 fn a_refusal_names_the_rule_and_not_the_value() {
1733 let err = shape(Format::Digits, 6, 6).check("hunter2").unwrap_err();
1734 assert!(!err.contains("hunter2"));
1735 let err = ValidatorSpec::Regex("[0-9]+".to_string())
1736 .check("hunter2")
1737 .unwrap_err();
1738 assert!(!err.contains("hunter2"));
1739 }
1740
1741 #[test]
1742 fn a_hint_reads_as_a_person_would_say_it() {
1743 assert_eq!(shape(Format::Digits, 6, 6).hint().unwrap(), "6 digits");
1744 assert_eq!(
1745 shape(Format::Text, 6, 8).hint().unwrap(),
1746 "6 to 8 characters"
1747 );
1748 assert_eq!(
1749 ValidatorSpec::Format {
1750 format: Format::Hex,
1751 min_length: Some(32),
1752 max_length: None,
1753 }
1754 .hint()
1755 .unwrap(),
1756 "at least 32 bytes as hex"
1757 );
1758 assert_eq!(
1759 ValidatorSpec::Format {
1760 format: Format::Alphanumeric,
1761 min_length: None,
1762 max_length: None,
1763 }
1764 .hint()
1765 .unwrap(),
1766 "letters or digits only"
1767 );
1768 assert_eq!(
1769 ValidatorSpec::Format {
1770 format: Format::Base64,
1771 min_length: None,
1772 max_length: None,
1773 }
1774 .hint()
1775 .unwrap(),
1776 "bytes as base64"
1777 );
1778 assert!(ValidatorSpec::NonEmpty.hint().is_none());
1779 }
1780
1781 #[test]
1784 fn an_encoding_decodes_and_counts_bytes() {
1785 let key = shape(Format::Hex, 4, 4);
1786 assert!(key.check("deadbeef").is_ok());
1787 assert!(key.check("DE AD be ef").is_ok());
1788 assert!(key.check("deadbe").unwrap_err().contains("this is 3"));
1789 assert!(key.check("deadbeefx").unwrap_err().contains("hex"));
1790 assert_eq!(Format::Hex.decode("DE AD").unwrap(), vec![0xde, 0xad]);
1791
1792 let b64 = ValidatorSpec::Format {
1793 format: Format::Base64,
1794 min_length: None,
1795 max_length: None,
1796 };
1797 assert!(b64.check("aGVsbG8=").is_ok());
1798 assert!(b64.check("aGVsbG8").is_err(), "padding is required");
1799 assert_eq!(Format::Base64.decode("aGVs\nbG8=").unwrap(), b"hello");
1800 assert!(Format::Digits.decode("12").is_err(), "text does not decode");
1801 }
1802
1803 #[test]
1804 fn a_placeholder_satisfies_its_own_format() {
1805 for format in [
1806 Format::Text,
1807 Format::Digits,
1808 Format::Alphanumeric,
1809 Format::Hex,
1810 Format::Base64,
1811 ] {
1812 let stand_in = format.placeholder(32).unwrap();
1813 assert!(shape(format, 32, 32).check(&stand_in).is_ok());
1814 }
1815 assert!(Format::Text.placeholder(PLACEHOLDER_LIMIT + 1).is_none());
1816 }
1817}
1818
1819#[cfg(test)]
1820mod schema_snapshot_tests {
1821 use super::*;
1822 use serde_json::json;
1823
1824 fn assert_json(fact: &StepFact, expected: &serde_json::Value) {
1825 let actual = serde_json::to_value(fact).expect("serialize StepFact");
1826 assert_eq!(&actual, expected, "wire-format drift for {fact:?}");
1827 let tag = actual
1828 .get("type")
1829 .and_then(serde_json::Value::as_str)
1830 .expect("a type tag");
1831 assert!(FACT_TYPES.contains(&tag), "{tag} missing from FACT_TYPES");
1832 crate::schema::assert_fact_matches_schema(&actual);
1833 }
1834
1835 #[test]
1836 fn a_level_is_named_or_numbered_and_must_be_declared() {
1837 let header = TranscriptHeader::new("rite test", "0", false);
1838 assert_eq!(header.level("public"), Some(Level::PUBLIC));
1839 assert_eq!(header.level("20"), Some(Level::RESTRICTED));
1840 assert_eq!(header.level("15"), None);
1841 assert_eq!(header.level("partners"), None);
1842 }
1843
1844 #[test]
1845 fn levels_are_ordered_integers() {
1846 assert!(Level::PUBLIC < Level::RESTRICTED);
1847 assert!(Level::RESTRICTED < Level::CONFIDENTIAL);
1848 assert!(Level::new(15) > Level::PUBLIC && Level::new(15) < Level::RESTRICTED);
1849 assert_eq!(
1850 serde_json::to_value(Level::CONFIDENTIAL).expect("serialize"),
1851 json!(30)
1852 );
1853 assert_eq!(Level::RESTRICTED.to_string(), "restricted");
1854 assert_eq!(Level::new(15).to_string(), "level 15");
1855 }
1856
1857 #[test]
1858 fn ceremony_started() {
1859 assert_json(
1860 &StepFact::CeremonyStarted {
1861 name: "Root CA".to_string(),
1862 template: Sha256Digest::of(b"ceremony"),
1863 },
1864 &json!({
1865 "type": "ceremony_started",
1866 "name": "Root CA",
1867 "template": Sha256Digest::of(b"ceremony").as_str(),
1868 }),
1869 );
1870 }
1871
1872 #[test]
1873 fn header() {
1874 let header = TranscriptHeader::new("rite 0.6.0", &"0".repeat(32), false);
1875 assert_eq!(
1876 serde_json::to_value(&header).expect("serialize header"),
1877 json!({
1878 "rite_transcript": 0,
1879 "vocabulary": 0,
1880 "producer": "rite 0.6.0",
1881 "run_id": "0".repeat(32),
1882 "dry_run": false,
1883 "levels": { "public": 10, "restricted": 20, "confidential": 30 },
1884 }),
1885 );
1886 }
1887
1888 #[test]
1889 fn role_assigned() {
1890 assert_json(
1891 &StepFact::RoleAssigned {
1892 role: RoleId::new("crypto_officer"),
1893 person: "Alice Rivera".to_string(),
1894 },
1895 &json!({
1896 "type": "role_assigned",
1897 "role": "crypto_officer",
1898 "person": "Alice Rivera",
1899 }),
1900 );
1901 }
1902
1903 #[test]
1904 fn parameter_bound() {
1905 assert_json(
1906 &StepFact::ParameterBound {
1907 name: ParamId::new("validity_days"),
1908 value: json!(3650),
1909 },
1910 &json!({
1911 "type": "parameter_bound",
1912 "name": "validity_days",
1913 "value": 3650,
1914 }),
1915 );
1916 }
1917
1918 #[test]
1919 fn material_loaded() {
1920 assert_json(
1921 &StepFact::MaterialLoaded {
1922 name: MaterialId::new("usb_drive"),
1923 identifier: Some("SN-1234".to_string()),
1924 },
1925 &json!({
1926 "type": "material_loaded",
1927 "name": "usb_drive",
1928 "identifier": "SN-1234",
1929 }),
1930 );
1931 assert_json(
1932 &StepFact::MaterialLoaded {
1933 name: MaterialId::new("csr"),
1934 identifier: None,
1935 },
1936 &json!({ "type": "material_loaded", "name": "csr" }),
1937 );
1938 }
1939
1940 #[test]
1941 fn material_digest() {
1942 assert_json(
1943 &StepFact::MaterialDigest {
1944 name: MaterialId::new("csr"),
1945 digest: Sha256Digest::of(b"csr"),
1946 },
1947 &json!({
1948 "type": "material_digest",
1949 "name": "csr",
1950 "digest": Sha256Digest::of(b"csr").as_str(),
1951 }),
1952 );
1953 }
1954
1955 #[test]
1956 fn backend_bound() {
1957 assert_json(
1958 &StepFact::BackendBound {
1959 name: "token".to_string(),
1960 provider: "yubikey".to_string(),
1961 identity: "yubikey-serial=12345678+firmware=5.7.1".to_string(),
1962 },
1963 &json!({
1964 "type": "backend_bound",
1965 "name": "token",
1966 "provider": "yubikey",
1967 "identity": "yubikey-serial=12345678+firmware=5.7.1",
1968 }),
1969 );
1970 }
1971
1972 #[test]
1973 fn machine_info_recorded() {
1974 assert_json(
1975 &StepFact::MachineInfoRecorded {
1976 step: StepId::new("capture"),
1977 info: json!({ "arch": "x86_64", "hostname": null }),
1978 },
1979 &json!({
1980 "type": "machine_info_recorded",
1981 "step": "capture",
1982 "info": { "arch": "x86_64", "hostname": null },
1983 }),
1984 );
1985 }
1986
1987 #[test]
1988 fn act_started() {
1989 assert_json(
1990 &StepFact::ActStarted {
1991 id: ActId::new("setup"),
1992 label: "Setup".to_string(),
1993 },
1994 &json!({
1995 "type": "act_started",
1996 "id": "setup",
1997 "label": "Setup",
1998 }),
1999 );
2000 }
2001
2002 #[test]
2003 fn step_started() {
2004 assert_json(
2005 &StepFact::StepStarted {
2006 id: StepId::new("s1"),
2007 label: "2.1".to_string(),
2008 role: RoleId::new("crypto_officer"),
2009 },
2010 &json!({
2011 "type": "step_started",
2012 "id": "s1",
2013 "label": "2.1",
2014 "role": "crypto_officer",
2015 }),
2016 );
2017 }
2018
2019 #[test]
2020 fn role_declared() {
2021 assert_json(
2022 &StepFact::RoleDeclared {
2023 role: RoleId::new("crypto_officer"),
2024 name: "Crypto Officer".to_string(),
2025 },
2026 &json!({
2027 "type": "role_declared",
2028 "role": "crypto_officer",
2029 "name": "Crypto Officer",
2030 }),
2031 );
2032 }
2033
2034 #[test]
2035 fn prompt_answered_confirm_bool() {
2036 assert_json(
2037 &StepFact::PromptAnswered {
2038 step: Some(StepId::new("s1")),
2039 prompt: Prompt::Confirm {
2040 question: "Proceed?".to_string(),
2041 default: Some(true),
2042 },
2043 response: ResponseRecord::Bool { value: true },
2044 },
2045 &json!({
2046 "type": "prompt_answered",
2047 "step": "s1",
2048 "prompt": { "type": "confirm", "question": "Proceed?", "default": true },
2049 "response": { "type": "bool", "value": true },
2050 }),
2051 );
2052 }
2053
2054 #[test]
2055 fn prompt_answered_text_nonempty() {
2056 assert_json(
2057 &StepFact::PromptAnswered {
2058 step: Some(StepId::new("s1")),
2059 prompt: Prompt::Text {
2060 label: "Name".to_string(),
2061 validator: ValidatorSpec::NonEmpty,
2062 },
2063 response: ResponseRecord::Text {
2064 value: "Alice".to_string(),
2065 },
2066 },
2067 &json!({
2068 "type": "prompt_answered",
2069 "step": "s1",
2070 "prompt": {
2071 "type": "text",
2072 "label": "Name",
2073 "validator": { "kind": "non_empty" },
2074 },
2075 "response": { "type": "text", "value": "Alice" },
2076 }),
2077 );
2078 }
2079
2080 #[test]
2081 fn prompt_answered_text_regex() {
2082 assert_json(
2083 &StepFact::PromptAnswered {
2084 step: Some(StepId::new("s1")),
2085 prompt: Prompt::Text {
2086 label: "SN".to_string(),
2087 validator: ValidatorSpec::Regex(r"^[A-Z0-9]+$".to_string()),
2088 },
2089 response: ResponseRecord::Text {
2090 value: "AB12".to_string(),
2091 },
2092 },
2093 &json!({
2094 "type": "prompt_answered",
2095 "step": "s1",
2096 "prompt": {
2097 "type": "text",
2098 "label": "SN",
2099 "validator": { "kind": "regex", "value": "^[A-Z0-9]+$" },
2100 },
2101 "response": { "type": "text", "value": "AB12" },
2102 }),
2103 );
2104 }
2105
2106 #[test]
2107 fn prompt_answered_text_predefined() {
2108 assert_json(
2109 &StepFact::PromptAnswered {
2110 step: Some(StepId::new("s1")),
2111 prompt: Prompt::Text {
2112 label: "SN".to_string(),
2113 validator: ValidatorSpec::Predefined("serial_number".to_string()),
2114 },
2115 response: ResponseRecord::Text {
2116 value: "ABCD".to_string(),
2117 },
2118 },
2119 &json!({
2120 "type": "prompt_answered",
2121 "step": "s1",
2122 "prompt": {
2123 "type": "text",
2124 "label": "SN",
2125 "validator": { "kind": "predefined", "value": "serial_number" },
2126 },
2127 "response": { "type": "text", "value": "ABCD" },
2128 }),
2129 );
2130 }
2131
2132 #[test]
2133 fn prompt_answered_secret_redacted() {
2134 assert_json(
2135 &StepFact::PromptAnswered {
2136 step: Some(StepId::new("s1")),
2137 prompt: Prompt::Secret {
2138 label: "PIN".to_string(),
2139 validator: ValidatorSpec::NonEmpty,
2140 },
2141 response: ResponseRecord::SecretRedacted {},
2142 },
2143 &json!({
2144 "type": "prompt_answered",
2145 "step": "s1",
2146 "prompt": {
2147 "type": "secret",
2148 "label": "PIN",
2149 "validator": { "kind": "non_empty" },
2150 },
2151 "response": { "type": "secret_redacted" },
2152 }),
2153 );
2154 }
2155
2156 #[test]
2157 fn prompt_answered_literal_text() {
2158 assert_json(
2159 &StepFact::PromptAnswered {
2160 step: Some(StepId::new("s1")),
2161 prompt: Prompt::Literal {
2162 label: "Type 'attest'".to_string(),
2163 expected: "attest".to_string(),
2164 },
2165 response: ResponseRecord::Text {
2166 value: "attest".to_string(),
2167 },
2168 },
2169 &json!({
2170 "type": "prompt_answered",
2171 "step": "s1",
2172 "prompt": { "type": "literal", "label": "Type 'attest'", "expected": "attest" },
2173 "response": { "type": "text", "value": "attest" },
2174 }),
2175 );
2176 }
2177
2178 #[test]
2179 fn prompt_answered_continue_acknowledged() {
2180 assert_json(
2181 &StepFact::PromptAnswered {
2182 step: Some(StepId::new("s1")),
2183 prompt: Prompt::Continue {
2184 hint: Some("Press Enter".to_string()),
2185 },
2186 response: ResponseRecord::Acknowledged,
2187 },
2188 &json!({
2189 "type": "prompt_answered",
2190 "step": "s1",
2191 "prompt": { "type": "continue", "hint": "Press Enter" },
2192 "response": { "type": "acknowledged" },
2193 }),
2194 );
2195 }
2196
2197 #[test]
2198 fn backend_operation() {
2199 assert_json(
2200 &StepFact::BackendOperation {
2201 step: StepId::new("s1"),
2202 kind: "generate_key".to_string(),
2203 backend: Some("hsm".to_string()),
2204 inputs: json!({ "algorithm": "rsa", "bits": 4096 }),
2205 outputs: json!({ "key_id": "k1" }),
2206 fingerprint: Some("sha256:deadbeef".to_string()),
2207 },
2208 &json!({
2209 "type": "backend_operation",
2210 "step": "s1",
2211 "kind": "generate_key",
2212 "backend": "hsm",
2213 "inputs": { "algorithm": "rsa", "bits": 4096 },
2214 "outputs": { "key_id": "k1" },
2215 "fingerprint": "sha256:deadbeef",
2216 }),
2217 );
2218 }
2219
2220 #[test]
2221 fn wrap_recipient_recorded() {
2222 let fingerprint = Sha256Digest::of(b"escrow spki").to_string();
2223 assert_json(
2224 &StepFact::WrapRecipientRecorded {
2225 step: StepId::new("s1"),
2226 source: "escrow_pubkey".to_string(),
2227 fingerprint: fingerprint.clone(),
2228 declared: true,
2229 },
2230 &json!({
2231 "type": "wrap_recipient_recorded",
2232 "step": "s1",
2233 "source": "escrow_pubkey",
2234 "fingerprint": fingerprint,
2235 "declared": true,
2236 }),
2237 );
2238 }
2239
2240 #[test]
2241 fn attestation_recorded() {
2242 assert_json(
2243 &StepFact::AttestationRecorded {
2244 step: StepId::new("s1"),
2245 role: RoleId::new("crypto_officer"),
2246 statement: "I confirm.".to_string(),
2247 },
2248 &json!({
2249 "type": "attestation_recorded",
2250 "step": "s1",
2251 "role": "crypto_officer",
2252 "statement": "I confirm.",
2253 }),
2254 );
2255 }
2256
2257 #[test]
2258 fn artifact_written() {
2259 assert_json(
2260 &StepFact::ArtifactWritten {
2261 step: StepId::new("s1"),
2262 name: "root_cert".to_string(),
2263 file: "root_cert.crt".to_string(),
2264 digest: Some(Sha256Digest::of(b"cert")),
2265 },
2266 &json!({
2267 "type": "artifact_written",
2268 "step": "s1",
2269 "name": "root_cert",
2270 "file": "root_cert.crt",
2271 "digest": Sha256Digest::of(b"cert").as_str(),
2272 }),
2273 );
2274 }
2275
2276 #[test]
2277 fn artifact_written_secret_has_no_digest() {
2278 assert_json(
2279 &StepFact::ArtifactWritten {
2280 step: StepId::new("s1"),
2281 name: "opened".to_string(),
2282 file: "opened.bin".to_string(),
2283 digest: None,
2284 },
2285 &json!({
2286 "type": "artifact_written",
2287 "step": "s1",
2288 "name": "opened",
2289 "file": "opened.bin",
2290 }),
2291 );
2292 }
2293
2294 #[test]
2295 fn deviation_recorded() {
2296 assert_json(
2297 &StepFact::DeviationRecorded {
2298 step: Some(StepId::new("s1")),
2299 text: "phone rang".to_string(),
2300 },
2301 &json!({
2302 "type": "deviation_recorded",
2303 "step": "s1",
2304 "text": "phone rang",
2305 }),
2306 );
2307 }
2308
2309 #[test]
2310 fn step_completed_completed() {
2311 assert_json(
2312 &StepFact::StepCompleted {
2313 id: StepId::new("s1"),
2314 outcome: StepOutcome::Completed {
2315 message: "done".to_string(),
2316 },
2317 },
2318 &json!({
2319 "type": "step_completed",
2320 "id": "s1",
2321 "outcome": { "status": "completed", "message": "done" },
2322 }),
2323 );
2324 }
2325
2326 #[test]
2327 fn ceremony_completed() {
2328 assert_json(
2329 &StepFact::CeremonyCompleted {},
2330 &json!({
2331 "type": "ceremony_completed",
2332 }),
2333 );
2334 }
2335
2336 #[test]
2337 fn ceremony_failed() {
2338 assert_json(
2339 &StepFact::CeremonyFailed {
2340 error: ErrorRecord::new(
2341 ErrorClass::Abort,
2342 "aborted",
2343 "ceremony aborted by operator",
2344 ),
2345 },
2346 &json!({
2347 "type": "ceremony_failed",
2348 "error": {
2349 "class": "abort",
2350 "kind": "aborted",
2351 "message": "ceremony aborted by operator",
2352 },
2353 }),
2354 );
2355 }
2356
2357 #[test]
2358 fn step_attempt_failed() {
2359 assert_json(
2360 &StepFact::StepAttemptFailed {
2361 step: StepId::new("import_key"),
2362 attempt: 1,
2363 error: ErrorRecord::new(
2364 ErrorClass::Environmental,
2365 "backend_error",
2366 "Token not present",
2367 ),
2368 },
2369 &json!({
2370 "type": "step_attempt_failed",
2371 "step": "import_key",
2372 "attempt": 1,
2373 "error": {
2374 "class": "environmental",
2375 "kind": "backend_error",
2376 "message": "Token not present",
2377 },
2378 }),
2379 );
2380 }
2381
2382 #[test]
2383 fn entropy_seeded() {
2384 assert_json(
2385 &StepFact::EntropySeeded {
2386 m: "00112233".to_string(),
2387 source: "os".to_string(),
2388 derivation: "rite-kdf/v1".to_string(),
2389 },
2390 &json!({
2391 "type": "entropy_seeded",
2392 "m": "00112233",
2393 "source": "os",
2394 "derivation": "rite-kdf/v1",
2395 }),
2396 );
2397 }
2398
2399 #[test]
2400 fn entropy_contributed() {
2401 assert_json(
2402 &StepFact::EntropyContributed {
2403 step: StepId::new("roll_dice"),
2404 epoch: 1,
2405 contribution: "3 1 6 4 2 5".to_string(),
2406 },
2407 &json!({
2408 "type": "entropy_contributed",
2409 "step": "roll_dice",
2410 "epoch": 1,
2411 "contribution": "3 1 6 4 2 5",
2412 }),
2413 );
2414 }
2415
2416 #[test]
2417 fn entropy_drawn() {
2418 assert_json(
2419 &StepFact::EntropyDrawn {
2420 step: StepId::new("issue"),
2421 path: "0/issue/cert-serial".to_string(),
2422 value: "aabbccddeeff00112233".to_string(),
2423 },
2424 &json!({
2425 "type": "entropy_drawn",
2426 "step": "issue",
2427 "path": "0/issue/cert-serial",
2428 "value": "aabbccddeeff00112233",
2429 }),
2430 );
2431 }
2432}