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