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]
288#[cfg_attr(test, derive(schemars::JsonSchema))]
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
290#[serde(rename_all = "snake_case")]
291#[cfg_attr(test, schemars(description = "What kind of value a person types."))]
292pub enum Format {
293 #[cfg_attr(test, schemars(description = "Anything the person can type."))]
295 Text,
296 #[cfg_attr(test, schemars(description = "Decimal digits only."))]
298 Digits,
299 #[cfg_attr(test, schemars(description = "ASCII letters and digits."))]
301 Alphanumeric,
302 #[cfg_attr(
304 test,
305 schemars(description = "Bytes as hexadecimal, in either case, two digits per byte.")
306 )]
307 Hex,
308 #[cfg_attr(test, schemars(description = "Bytes as standard base64 with padding."))]
310 Base64,
311}
312
313impl Format {
314 #[must_use]
316 pub fn is_encoding(self) -> bool {
317 match self {
318 Format::Text | Format::Digits | Format::Alphanumeric => false,
319 Format::Hex | Format::Base64 => true,
320 }
321 }
322
323 fn accepts(self, c: char) -> bool {
326 match self {
327 Format::Text | Format::Hex | Format::Base64 => true,
328 Format::Digits => c.is_ascii_digit(),
329 Format::Alphanumeric => c.is_ascii_alphanumeric(),
330 }
331 }
332
333 pub fn decode(self, value: &str) -> Result<Vec<u8>, String> {
341 let compact: Zeroizing<String> =
344 Zeroizing::new(value.chars().filter(|c| !c.is_whitespace()).collect());
345 match self {
346 Format::Hex => base16ct::mixed::decode_vec(compact.as_bytes())
347 .map_err(|_| "value must be hex, two digits per byte".to_string()),
348 Format::Base64 => base64ct::Base64::decode_vec(&compact)
349 .map_err(|_| "value must be standard base64, with padding".to_string()),
350 Format::Text | Format::Digits | Format::Alphanumeric => {
351 Err(format!("{} is text and does not decode", self.describe()))
352 }
353 }
354 }
355
356 #[must_use]
358 pub fn describe(self) -> &'static str {
359 match self {
360 Format::Text => "characters",
361 Format::Digits => "digits",
362 Format::Alphanumeric => "letters or digits",
363 Format::Hex => "bytes as hex",
364 Format::Base64 => "bytes as base64",
365 }
366 }
367
368 #[must_use]
376 pub fn placeholder(self, length: usize) -> Option<String> {
377 if length > PLACEHOLDER_LIMIT {
378 return None;
379 }
380 Some(match self {
381 Format::Text | Format::Alphanumeric => "x".repeat(length),
382 Format::Digits => "0".repeat(length),
383 Format::Hex => base16ct::lower::encode_string(&vec![0u8; length]),
384 Format::Base64 => base64ct::Base64::encode_string(&vec![0u8; length]),
385 })
386 }
387}
388
389pub const PLACEHOLDER_LIMIT: usize = 64 * 1024;
393
394impl std::str::FromStr for Format {
395 type Err = String;
396
397 fn from_str(s: &str) -> Result<Self, Self::Err> {
398 match s {
399 "text" => Ok(Format::Text),
400 "digits" => Ok(Format::Digits),
401 "alphanumeric" => Ok(Format::Alphanumeric),
402 "hex" => Ok(Format::Hex),
403 "base64" => Ok(Format::Base64),
404 other => Err(format!(
405 "unknown format '{other}': expected text, digits, alphanumeric, hex or base64"
406 )),
407 }
408 }
409}
410
411impl ValidatorSpec {
412 pub fn check(&self, value: &str) -> Result<(), String> {
419 match self {
420 ValidatorSpec::NonEmpty => {
421 if value.trim().is_empty() {
422 Err("value must not be empty".to_string())
423 } else {
424 Ok(())
425 }
426 }
427 ValidatorSpec::Regex(pattern) => {
428 let regex = compile_pattern(pattern)?;
429 if regex.is_match(value) {
430 Ok(())
431 } else {
432 Err(format!("value must match {pattern}"))
433 }
434 }
435 ValidatorSpec::Predefined(name) => Err(format!("unknown validator: {name}")),
437 ValidatorSpec::Format {
438 format,
439 min_length,
440 max_length,
441 } => {
442 let hint = self.hint().unwrap_or_default();
443 if value.trim().is_empty() {
444 return Err("value must not be empty".to_string());
445 }
446 let length = if format.is_encoding() {
450 Zeroizing::new(format.decode(value)?).len()
451 } else {
452 if !value.chars().all(|c| format.accepts(c)) {
453 return Err(format!("value must be {hint}"));
454 }
455 value.chars().count()
456 };
457 if min_length.is_some_and(|min| length < min)
458 || max_length.is_some_and(|max| length > max)
459 {
460 return Err(format!("value must be {hint}, this is {length}"));
461 }
462 Ok(())
463 }
464 }
465 }
466
467 #[must_use]
473 pub fn hint(&self) -> Option<String> {
474 match self {
475 ValidatorSpec::NonEmpty | ValidatorSpec::Predefined(_) => None,
476 ValidatorSpec::Regex(pattern) => Some(format!("matching {pattern}")),
477 ValidatorSpec::Format {
478 format,
479 min_length,
480 max_length,
481 } => {
482 let what = format.describe();
483 Some(match (min_length, max_length) {
484 (Some(min), Some(max)) if min == max => format!("{min} {what}"),
485 (Some(min), Some(max)) => format!("{min} to {max} {what}"),
486 (Some(min), None) => format!("at least {min} {what}"),
487 (None, Some(max)) => format!("at most {max} {what}"),
488 (None, None) if format.is_encoding() => what.to_string(),
489 (None, None) => format!("{what} only"),
490 })
491 }
492 }
493 }
494}
495
496pub fn compile_pattern(pattern: &str) -> Result<regex_lite::Regex, String> {
506 regex_lite::Regex::new(&format!("^(?:{pattern})$"))
507 .map_err(|e| format!("invalid pattern '{pattern}': {e}"))
508}
509
510#[non_exhaustive]
513#[cfg_attr(test, derive(schemars::JsonSchema))]
514#[derive(Debug, Clone, Serialize, Deserialize)]
515#[serde(tag = "type", rename_all = "snake_case")]
516#[cfg_attr(test, schemars(description = "A prompt as it was shown."))]
517pub enum Prompt {
518 #[cfg_attr(test, schemars(description = "A yes or no question."))]
520 Confirm {
521 #[cfg_attr(test, schemars(description = "The question."))]
523 question: String,
524 #[cfg_attr(
526 test,
527 schemars(
528 description = "The answer given when the person confirms without choosing, if \
529 any."
530 )
531 )]
532 default: Option<bool>,
533 },
534 #[cfg_attr(
536 test,
537 schemars(description = "A request for free text, checked before it is accepted.")
538 )]
539 Text {
540 #[cfg_attr(test, schemars(description = "The label shown."))]
542 label: String,
543 #[cfg_attr(test, schemars(description = "The check the answer had to pass."))]
545 validator: ValidatorSpec,
546 },
547 #[cfg_attr(
550 test,
551 schemars(
552 description = "A request for a secret, such as a PIN. The answer is never \
553 recorded."
554 )
555 )]
556 Secret {
557 #[cfg_attr(test, schemars(description = "The label shown."))]
559 label: String,
560 #[cfg_attr(test, schemars(description = "The check the answer had to pass."))]
562 validator: ValidatorSpec,
563 },
564 #[cfg_attr(
567 test,
568 schemars(
569 description = "A request to type a given text exactly, such as a confirmation \
570 phrase."
571 )
572 )]
573 Literal {
574 #[cfg_attr(test, schemars(description = "The label shown."))]
576 label: String,
577 #[cfg_attr(test, schemars(description = "The text that had to be typed."))]
579 expected: String,
580 },
581 #[cfg_attr(
583 test,
584 schemars(description = "A pause until the person is ready to continue.")
585 )]
586 Continue {
587 #[cfg_attr(test, schemars(description = "The hint shown, if any."))]
589 hint: Option<String>,
590 },
591}
592
593#[non_exhaustive]
600#[cfg_attr(test, derive(schemars::JsonSchema))]
601#[derive(Debug, Clone, Serialize, Deserialize)]
602#[serde(tag = "type", rename_all = "snake_case")]
603#[cfg_attr(
604 test,
605 schemars(
606 description = "The answer to a prompt, as recorded. A secret answer is never \
607 recorded, not even as a digest."
608 )
609)]
610pub enum ResponseRecord {
611 #[cfg_attr(test, schemars(description = "A yes or no answer."))]
613 Bool {
614 #[cfg_attr(test, schemars(description = "The answer."))]
616 value: bool,
617 },
618 #[cfg_attr(test, schemars(description = "A free-text answer."))]
620 Text {
621 #[cfg_attr(test, schemars(description = "The answer, as typed."))]
623 value: String,
624 },
625 #[cfg_attr(
634 test,
635 schemars(
636 description = "A secret was entered. Only the fact that it was entered is \
637 recorded."
638 )
639 )]
640 SecretRedacted {},
641 #[cfg_attr(test, schemars(description = "The person continued past a pause."))]
643 Acknowledged,
644}
645
646#[non_exhaustive]
655#[cfg_attr(test, derive(schemars::JsonSchema))]
656#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
657#[serde(rename_all = "snake_case")]
658#[cfg_attr(test, schemars(description = "What kind of bad outcome this was."))]
659pub enum ErrorClass {
660 #[cfg_attr(
663 test,
664 schemars(
665 description = "Something around the ceremony was not ready, such as a device not \
666 connected, and the step's work did not happen."
667 )
668 )]
669 Environmental,
670 #[cfg_attr(
673 test,
674 schemars(
675 description = "The ceremony's own logic concluded badly, such as a verification \
676 that did not match or a refused attestation."
677 )
678 )]
679 Procedural,
680 #[cfg_attr(
683 test,
684 schemars(
685 description = "The run itself cannot be trusted to continue, or the ceremony \
686 definition is broken."
687 )
688 )]
689 Integrity,
690 #[cfg_attr(
693 test,
694 schemars(description = "An operator chose to stop the ceremony.")
695 )]
696 Abort,
697}
698
699#[non_exhaustive]
701#[cfg_attr(test, derive(schemars::JsonSchema))]
702#[derive(Debug, Clone, Serialize, Deserialize)]
703#[cfg_attr(
704 test,
705 schemars(
706 description = "What went wrong: a class for audit, a stable kind, and a message for \
707 people."
708 )
709)]
710pub struct ErrorRecord {
711 #[cfg_attr(test, schemars(description = "The kind of bad outcome, for audit."))]
713 pub class: ErrorClass,
714 #[cfg_attr(
716 test,
717 schemars(description = "A stable label for the error, such as aborted or step_failed.")
718 )]
719 pub kind: String,
720 #[cfg_attr(test, schemars(description = "A description of the error for people."))]
722 pub message: String,
723}
724
725impl ErrorRecord {
726 pub fn new(class: ErrorClass, kind: impl Into<String>, message: impl Into<String>) -> Self {
728 Self {
729 class,
730 kind: kind.into(),
731 message: message.into(),
732 }
733 }
734}
735
736#[non_exhaustive]
738#[cfg_attr(test, derive(schemars::JsonSchema))]
739#[derive(Debug, Clone, Serialize, Deserialize)]
740#[serde(tag = "status", rename_all = "snake_case")]
741#[cfg_attr(test, schemars(description = "How a step ended."))]
742pub enum StepOutcome {
743 #[cfg_attr(test, schemars(description = "The step did its work."))]
745 Completed {
746 #[cfg_attr(
748 test,
749 schemars(description = "A short description of what the step did.")
750 )]
751 message: String,
752 },
753}
754
755#[non_exhaustive]
765#[cfg_attr(test, derive(schemars::JsonSchema))]
766#[derive(Debug, Clone, Serialize, Deserialize)]
767#[serde(tag = "type", rename_all = "snake_case")]
768#[cfg_attr(
769 test,
770 schemars(
771 description = "One recorded fact, identified by its `type`. Each fact type has a \
772 fixed set of fields in a given vocabulary."
773 )
774)]
775pub enum StepFact {
776 #[cfg_attr(test, schemars(description = "The ceremony started."))]
778 CeremonyStarted {
779 #[cfg_attr(test, schemars(description = "The ceremony's name."))]
781 name: String,
782 #[cfg_attr(
786 test,
787 schemars(
788 description = "Digest of the ceremony definition file the run was resolved \
789 from. The inputs of the run are recorded as their own facts, so this digest covers \
790 the definition only."
791 )
792 )]
793 template: Sha256Digest,
794 },
795 #[cfg_attr(
800 test,
801 schemars(
802 description = "A role the ceremony defines. Recorded once per role at the start, \
803 whether or not anyone is assigned to it; later facts name the role by its identifier."
804 )
805 )]
806 RoleDeclared {
807 #[cfg_attr(test, schemars(description = "The role."))]
809 role: RoleId,
810 #[cfg_attr(test, schemars(description = "The role's display name."))]
812 name: String,
813 },
814 #[cfg_attr(
818 test,
819 schemars(
820 description = "A person was assigned to a role for this run. One fact per \
821 assignment, so each can be withheld on its own."
822 )
823 )]
824 RoleAssigned {
825 #[cfg_attr(test, schemars(description = "The role."))]
827 role: RoleId,
828 #[cfg_attr(
831 test,
832 schemars(
833 description = "The person, as supplied when the run started. Recorded as \
834 given: nothing in the transcript proves who the person is."
835 )
836 )]
837 person: String,
838 },
839 #[cfg_attr(
841 test,
842 schemars(description = "A parameter took its value for this run, supplied or defaulted.")
843 )]
844 ParameterBound {
845 #[cfg_attr(test, schemars(description = "The parameter."))]
847 name: ParamId,
848 #[cfg_attr(test, schemars(description = "The value, as the ceremony used it."))]
850 value: serde_json::Value,
851 },
852 #[cfg_attr(
858 test,
859 schemars(
860 description = "A material was loaded at the start of the run. The digest of a \
861 digital material is a separate fact, material_digest, so the two can be disclosed to \
862 different audiences."
863 )
864 )]
865 MaterialLoaded {
866 #[cfg_attr(test, schemars(description = "The material."))]
868 name: MaterialId,
869 #[serde(skip_serializing_if = "Option::is_none")]
872 #[cfg_attr(
873 test,
874 schemars(
875 description = "The identifier of a physical or pre-provisioned material, such \
876 as a serial number, when one was supplied."
877 )
878 )]
879 identifier: Option<String>,
880 },
881 #[cfg_attr(
883 test,
884 schemars(description = "The digest of a digital material's bytes, as loaded.")
885 )]
886 MaterialDigest {
887 #[cfg_attr(test, schemars(description = "The material."))]
889 name: MaterialId,
890 #[cfg_attr(test, schemars(description = "The digest of the material's bytes."))]
892 digest: Sha256Digest,
893 },
894 #[cfg_attr(
900 test,
901 schemars(
902 description = "A backend was used for the first time in this run. Recorded once \
903 per backend, before its first operation; operations name the backend and do not repeat \
904 its identity."
905 )
906 )]
907 BackendBound {
908 #[cfg_attr(test, schemars(description = "The backend's name in the ceremony."))]
910 name: String,
911 #[cfg_attr(
913 test,
914 schemars(description = "The kind of backend, such as openssl, yubikey or pkcs11.")
915 )]
916 provider: String,
917 #[cfg_attr(
920 test,
921 schemars(
922 description = "The identity the backend reports for itself, such as a device \
923 serial number and firmware version."
924 )
925 )]
926 identity: String,
927 },
928 #[cfg_attr(
930 test,
931 schemars(description = "A description of the machine the ceremony ran on.")
932 )]
933 MachineInfoRecorded {
934 #[cfg_attr(test, schemars(description = "The step that recorded it."))]
936 step: StepId,
937 #[cfg_attr(
939 test,
940 schemars(
941 description = "The parts of the machine description the step was asked to \
942 record."
943 )
944 )]
945 info: serde_json::Value,
946 },
947 #[cfg_attr(
949 test,
950 schemars(description = "An act, a named part of the ceremony, started.")
951 )]
952 ActStarted {
953 #[cfg_attr(test, schemars(description = "The act."))]
955 id: ActId,
956 #[cfg_attr(
958 test,
959 schemars(description = "The act's label, as written in the ceremony.")
960 )]
961 label: String,
962 },
963 #[cfg_attr(test, schemars(description = "A step started."))]
965 StepStarted {
966 #[cfg_attr(test, schemars(description = "The step."))]
968 id: StepId,
969 #[cfg_attr(
971 test,
972 schemars(description = "The step's label, as written in the ceremony.")
973 )]
974 label: String,
975 #[cfg_attr(
978 test,
979 schemars(
980 description = "The role responsible for the step. Its display name is on the \
981 role's role_declared fact."
982 )
983 )]
984 role: RoleId,
985 },
986 #[cfg_attr(
988 test,
989 schemars(description = "Someone answered a prompt, and the answer was accepted.")
990 )]
991 PromptAnswered {
992 #[serde(skip_serializing_if = "Option::is_none")]
996 #[cfg_attr(
997 test,
998 schemars(
999 description = "The step that asked, or absent for a prompt asked outside any \
1000 step."
1001 )
1002 )]
1003 step: Option<StepId>,
1004 #[cfg_attr(test, schemars(description = "The prompt, as shown."))]
1006 prompt: Prompt,
1007 #[cfg_attr(test, schemars(description = "The answer, as recorded."))]
1009 response: ResponseRecord,
1010 },
1011 #[cfg_attr(
1013 test,
1014 schemars(
1015 description = "A backend performed an operation. The inputs and outputs are \
1016 specific to the kind of operation."
1017 )
1018 )]
1019 BackendOperation {
1020 #[cfg_attr(test, schemars(description = "The step the operation ran in."))]
1022 step: StepId,
1023 #[cfg_attr(
1025 test,
1026 schemars(description = "The kind of operation, such as generate_key or sign_data.")
1027 )]
1028 kind: String,
1029 #[serde(skip_serializing_if = "Option::is_none")]
1033 #[cfg_attr(
1034 test,
1035 schemars(
1036 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."
1037 )
1038 )]
1039 backend: Option<String>,
1040 #[cfg_attr(
1042 test,
1043 schemars(
1044 description = "What the operation was given: parameters and references to \
1045 artifacts or materials."
1046 )
1047 )]
1048 inputs: serde_json::Value,
1049 #[cfg_attr(
1051 test,
1052 schemars(description = "What the operation produced: artifact names and digests.")
1053 )]
1054 outputs: serde_json::Value,
1055 #[cfg_attr(
1057 test,
1058 schemars(
1059 description = "A fingerprint of the material the operation produced, when it \
1060 has one."
1061 )
1062 )]
1063 fingerprint: Option<String>,
1064 },
1065 #[cfg_attr(
1076 test,
1077 schemars(
1078 description = "A key held outside the ceremony received a wrapped key. A separate \
1079 fact from the operation, so the method of a wrap can be published while the recipient \
1080 stays withheld."
1081 )
1082 )]
1083 WrapRecipientRecorded {
1084 #[cfg_attr(test, schemars(description = "The step that wrapped the key."))]
1086 step: StepId,
1087 #[cfg_attr(
1090 test,
1091 schemars(
1092 description = "The artifact or material the recipient's key came from, by its \
1093 name in the ceremony."
1094 )
1095 )]
1096 source: String,
1097 #[cfg_attr(
1099 test,
1100 schemars(
1101 description = "The digest of the recipient's public key, as DER-encoded \
1102 SubjectPublicKeyInfo.",
1103 with = "Sha256Digest"
1104 )
1105 )]
1106 fingerprint: String,
1107 #[cfg_attr(
1111 test,
1112 schemars(
1113 description = "Whether the ceremony named this fingerprint in advance. When \
1114 it did, the key received was checked against it; otherwise the fingerprint records \
1115 whatever key arrived."
1116 )
1117 )]
1118 declared: bool,
1119 },
1120 #[cfg_attr(test, schemars(description = "A person made an attestation."))]
1122 AttestationRecorded {
1123 #[cfg_attr(test, schemars(description = "The step the attestation was made in."))]
1125 step: StepId,
1126 #[cfg_attr(test, schemars(description = "The role that made the attestation."))]
1128 role: RoleId,
1129 #[cfg_attr(test, schemars(description = "The statement, word for word."))]
1131 statement: String,
1132 },
1133 #[cfg_attr(
1135 test,
1136 schemars(description = "An artifact was written to the run's artifacts directory.")
1137 )]
1138 ArtifactWritten {
1139 #[cfg_attr(test, schemars(description = "The step that produced the artifact."))]
1141 step: StepId,
1142 #[cfg_attr(
1144 test,
1145 schemars(description = "The artifact's name, as declared in the ceremony.")
1146 )]
1147 name: String,
1148 #[cfg_attr(
1150 test,
1151 schemars(
1152 description = "The file name in the artifacts directory: a single path \
1153 component."
1154 )
1155 )]
1156 file: String,
1157 #[serde(skip_serializing_if = "Option::is_none")]
1161 #[cfg_attr(
1162 test,
1163 schemars(
1164 description = "The digest of the file's bytes. Absent for content a step \
1165 opened, since a digest of a secret can be tested against guesses."
1166 )
1167 )]
1168 digest: Option<Sha256Digest>,
1169 },
1170 #[cfg_attr(test, schemars(description = "An operator recorded a deviation."))]
1172 DeviationRecorded {
1173 #[serde(skip_serializing_if = "Option::is_none")]
1177 #[cfg_attr(
1178 test,
1179 schemars(
1180 description = "The step during which the deviation was recorded, or absent \
1181 outside any step."
1182 )
1183 )]
1184 step: Option<StepId>,
1185 #[cfg_attr(test, schemars(description = "The deviation, word for word."))]
1187 text: String,
1188 },
1189 #[cfg_attr(
1194 test,
1195 schemars(
1196 description = "One attempt at a step failed. Recorded per attempt: a step that is \
1197 retried and then succeeds has this fact for each failed attempt, then step_completed."
1198 )
1199 )]
1200 StepAttemptFailed {
1201 #[cfg_attr(test, schemars(description = "The step."))]
1203 step: StepId,
1204 #[cfg_attr(
1206 test,
1207 schemars(description = "The attempt's number within the step, starting at 1.")
1208 )]
1209 attempt: u32,
1210 #[cfg_attr(test, schemars(description = "What went wrong in this attempt."))]
1212 error: ErrorRecord,
1213 },
1214 #[cfg_attr(test, schemars(description = "A step finished."))]
1216 StepCompleted {
1217 #[cfg_attr(test, schemars(description = "The step."))]
1219 id: StepId,
1220 #[cfg_attr(test, schemars(description = "How the step ended."))]
1222 outcome: StepOutcome,
1223 },
1224 #[cfg_attr(
1231 test,
1232 schemars(
1233 description = "The ceremony finished. It is the last line of a transcript whose \
1234 run completed."
1235 )
1236 )]
1237 CeremonyCompleted {},
1238 #[cfg_attr(
1240 test,
1241 schemars(
1242 description = "The ceremony failed or was aborted. It is the last line of a \
1243 transcript whose run did not complete."
1244 )
1245 )]
1246 CeremonyFailed {
1247 #[cfg_attr(test, schemars(description = "What went wrong."))]
1249 error: ErrorRecord,
1250 },
1251 #[cfg_attr(
1260 test,
1261 schemars(
1262 description = "The ceremony's entropy source was seeded with machine randomness. \
1263 Recorded once, at the start, so every value drawn from the source can be derived again \
1264 from the transcript."
1265 )
1266 )]
1267 EntropySeeded {
1268 #[cfg_attr(
1270 test,
1271 schemars(
1272 description = "The machine randomness, as lowercase hex.",
1273 extend("pattern" = "^(?:[0-9a-f]{2})+$")
1274 )
1275 )]
1276 m: String,
1277 #[cfg_attr(
1280 test,
1281 schemars(description = "Where the machine randomness came from, such as os.")
1282 )]
1283 source: String,
1284 #[cfg_attr(
1287 test,
1288 schemars(
1289 description = "The derivation scheme, such as rite-kdf/v1. A verifier refuses \
1290 a scheme it does not know."
1291 )
1292 )]
1293 derivation: String,
1294 },
1295 #[cfg_attr(
1302 test,
1303 schemars(
1304 description = "A person added their own randomness to the entropy source, \
1305 starting a new epoch."
1306 )
1307 )]
1308 EntropyContributed {
1309 #[cfg_attr(test, schemars(description = "The step the contribution was made in."))]
1311 step: StepId,
1312 #[cfg_attr(
1314 test,
1315 schemars(description = "The epoch this contribution starts, counting from 1.")
1316 )]
1317 epoch: u32,
1318 #[cfg_attr(
1320 test,
1321 schemars(
1322 description = "The contribution, word for word, mixed into the source as \
1323 UTF-8."
1324 )
1325 )]
1326 contribution: String,
1327 },
1328 #[cfg_attr(
1336 test,
1337 schemars(
1338 description = "A value was drawn from the entropy source, such as a nonce, a \
1339 certificate serial number or a challenge. A verifier derives it again from the seed, \
1340 the contributions and the path."
1341 )
1342 )]
1343 EntropyDrawn {
1344 #[cfg_attr(test, schemars(description = "The step that drew the value."))]
1346 step: StepId,
1347 #[cfg_attr(
1349 test,
1350 schemars(description = "The derivation path: `<epoch>/<step>/<purpose>`.")
1351 )]
1352 path: String,
1353 #[cfg_attr(
1356 test,
1357 schemars(
1358 description = "The value, as lowercase hex. Its length is the number of bytes \
1359 drawn.",
1360 extend("pattern" = "^(?:[0-9a-f]{2})*$")
1361 )
1362 )]
1363 value: String,
1364 },
1365}
1366
1367pub const FACT_TYPES: &[&str] = &[
1372 "ceremony_started",
1373 "role_declared",
1374 "role_assigned",
1375 "parameter_bound",
1376 "material_loaded",
1377 "material_digest",
1378 "backend_bound",
1379 "machine_info_recorded",
1380 "act_started",
1381 "step_started",
1382 "prompt_answered",
1383 "backend_operation",
1384 "wrap_recipient_recorded",
1385 "attestation_recorded",
1386 "artifact_written",
1387 "deviation_recorded",
1388 "step_attempt_failed",
1389 "step_completed",
1390 "ceremony_completed",
1391 "ceremony_failed",
1392 "entropy_seeded",
1393 "entropy_contributed",
1394 "entropy_drawn",
1395];
1396
1397impl StepFact {
1398 #[must_use]
1409 pub fn default_level(&self) -> Level {
1410 match self {
1411 StepFact::CeremonyStarted { .. }
1412 | StepFact::RoleDeclared { .. }
1413 | StepFact::ActStarted { .. }
1414 | StepFact::StepStarted { .. }
1415 | StepFact::BackendOperation { .. }
1416 | StepFact::ArtifactWritten { .. }
1417 | StepFact::AttestationRecorded { .. }
1418 | StepFact::StepCompleted { .. }
1419 | StepFact::CeremonyCompleted {}
1420 | StepFact::CeremonyFailed { .. }
1421 | StepFact::EntropySeeded { .. }
1423 | StepFact::EntropyContributed { .. }
1424 | StepFact::EntropyDrawn { .. } => Level::PUBLIC,
1425 StepFact::ParameterBound { .. }
1426 | StepFact::MaterialLoaded { .. }
1427 | StepFact::BackendBound { .. }
1428 | StepFact::PromptAnswered { .. }
1429 | StepFact::WrapRecipientRecorded { .. }
1430 | StepFact::StepAttemptFailed { .. }
1431 | StepFact::DeviationRecorded { .. } => Level::RESTRICTED,
1432 StepFact::RoleAssigned { .. }
1433 | StepFact::MaterialDigest { .. }
1434 | StepFact::MachineInfoRecorded { .. } => Level::CONFIDENTIAL,
1435 }
1436 }
1437
1438 #[must_use]
1447 pub fn is_side_effect(&self) -> bool {
1448 match self {
1449 StepFact::BackendOperation { .. }
1450 | StepFact::AttestationRecorded { .. }
1451 | StepFact::ArtifactWritten { .. }
1452 | StepFact::EntropyContributed { .. }
1453 | StepFact::EntropyDrawn { .. } => true,
1454 StepFact::CeremonyStarted { .. }
1455 | StepFact::RoleDeclared { .. }
1457 | StepFact::RoleAssigned { .. }
1458 | StepFact::ParameterBound { .. }
1459 | StepFact::MaterialLoaded { .. }
1460 | StepFact::MaterialDigest { .. }
1461 | StepFact::BackendBound { .. }
1462 | StepFact::MachineInfoRecorded { .. }
1464 | StepFact::ActStarted { .. }
1465 | StepFact::StepStarted { .. }
1466 | StepFact::PromptAnswered { .. }
1467 | StepFact::WrapRecipientRecorded { .. }
1469 | StepFact::DeviationRecorded { .. }
1470 | StepFact::StepAttemptFailed { .. }
1471 | StepFact::StepCompleted { .. }
1472 | StepFact::CeremonyCompleted {}
1473 | StepFact::CeremonyFailed { .. }
1474 | StepFact::EntropySeeded { .. } => false,
1475 }
1476 }
1477}
1478
1479#[cfg(test)]
1491mod validator_tests {
1492 use super::*;
1493
1494 fn shape(format: Format, min: usize, max: usize) -> ValidatorSpec {
1495 ValidatorSpec::Format {
1496 format,
1497 min_length: Some(min),
1498 max_length: Some(max),
1499 }
1500 }
1501
1502 #[test]
1503 fn a_text_format_checks_characters_and_length() {
1504 let pin = shape(Format::Digits, 6, 8);
1505 assert!(pin.check("123456").is_ok());
1506 assert!(pin.check("12345678").is_ok());
1507 assert!(pin.check("12345").unwrap_err().contains("this is 5"));
1508 assert!(pin.check("123456789").unwrap_err().contains("this is 9"));
1509 assert!(pin.check("12345a").unwrap_err().contains("digits"));
1510
1511 assert!(shape(Format::Alphanumeric, 1, 4).check("ab12").is_ok());
1512 assert!(shape(Format::Alphanumeric, 1, 4).check("ab-1").is_err());
1513 assert!(shape(Format::Text, 1, 4).check("ab-1").is_ok());
1514 assert!(
1515 ValidatorSpec::Format {
1516 format: Format::Text,
1517 min_length: None,
1518 max_length: None,
1519 }
1520 .check(" ")
1521 .is_err(),
1522 "a value is still something"
1523 );
1524 }
1525
1526 #[test]
1529 fn a_pattern_must_match_the_whole_value() {
1530 let digits = ValidatorSpec::Regex("[0-9]+".to_string());
1531 assert!(digits.check("123").is_ok());
1532 assert!(digits.check("abc123").is_err());
1533 assert!(digits.check("123abc").is_err());
1534 let either = ValidatorSpec::Regex("yes|no".to_string());
1536 assert!(either.check("no").is_ok());
1537 assert!(either.check("nope").is_err());
1538 }
1539
1540 #[test]
1541 fn a_refusal_names_the_rule_and_not_the_value() {
1542 let err = shape(Format::Digits, 6, 6).check("hunter2").unwrap_err();
1543 assert!(!err.contains("hunter2"));
1544 let err = ValidatorSpec::Regex("[0-9]+".to_string())
1545 .check("hunter2")
1546 .unwrap_err();
1547 assert!(!err.contains("hunter2"));
1548 }
1549
1550 #[test]
1551 fn a_hint_reads_as_a_person_would_say_it() {
1552 assert_eq!(shape(Format::Digits, 6, 6).hint().unwrap(), "6 digits");
1553 assert_eq!(
1554 shape(Format::Text, 6, 8).hint().unwrap(),
1555 "6 to 8 characters"
1556 );
1557 assert_eq!(
1558 ValidatorSpec::Format {
1559 format: Format::Hex,
1560 min_length: Some(32),
1561 max_length: None,
1562 }
1563 .hint()
1564 .unwrap(),
1565 "at least 32 bytes as hex"
1566 );
1567 assert_eq!(
1568 ValidatorSpec::Format {
1569 format: Format::Alphanumeric,
1570 min_length: None,
1571 max_length: None,
1572 }
1573 .hint()
1574 .unwrap(),
1575 "letters or digits only"
1576 );
1577 assert_eq!(
1578 ValidatorSpec::Format {
1579 format: Format::Base64,
1580 min_length: None,
1581 max_length: None,
1582 }
1583 .hint()
1584 .unwrap(),
1585 "bytes as base64"
1586 );
1587 assert!(ValidatorSpec::NonEmpty.hint().is_none());
1588 }
1589
1590 #[test]
1593 fn an_encoding_decodes_and_counts_bytes() {
1594 let key = shape(Format::Hex, 4, 4);
1595 assert!(key.check("deadbeef").is_ok());
1596 assert!(key.check("DE AD be ef").is_ok());
1597 assert!(key.check("deadbe").unwrap_err().contains("this is 3"));
1598 assert!(key.check("deadbeefx").unwrap_err().contains("hex"));
1599 assert_eq!(Format::Hex.decode("DE AD").unwrap(), vec![0xde, 0xad]);
1600
1601 let b64 = ValidatorSpec::Format {
1602 format: Format::Base64,
1603 min_length: None,
1604 max_length: None,
1605 };
1606 assert!(b64.check("aGVsbG8=").is_ok());
1607 assert!(b64.check("aGVsbG8").is_err(), "padding is required");
1608 assert_eq!(Format::Base64.decode("aGVs\nbG8=").unwrap(), b"hello");
1609 assert!(Format::Digits.decode("12").is_err(), "text does not decode");
1610 }
1611
1612 #[test]
1613 fn a_placeholder_satisfies_its_own_format() {
1614 for format in [
1615 Format::Text,
1616 Format::Digits,
1617 Format::Alphanumeric,
1618 Format::Hex,
1619 Format::Base64,
1620 ] {
1621 let stand_in = format.placeholder(32).unwrap();
1622 assert!(shape(format, 32, 32).check(&stand_in).is_ok());
1623 }
1624 assert!(Format::Text.placeholder(PLACEHOLDER_LIMIT + 1).is_none());
1625 }
1626}
1627
1628#[cfg(test)]
1629mod schema_snapshot_tests {
1630 use super::*;
1631 use serde_json::json;
1632
1633 fn assert_json(fact: &StepFact, expected: &serde_json::Value) {
1634 let actual = serde_json::to_value(fact).expect("serialize StepFact");
1635 assert_eq!(&actual, expected, "wire-format drift for {fact:?}");
1636 let tag = actual
1637 .get("type")
1638 .and_then(serde_json::Value::as_str)
1639 .expect("a type tag");
1640 assert!(FACT_TYPES.contains(&tag), "{tag} missing from FACT_TYPES");
1641 crate::schema::assert_fact_matches_schema(&actual);
1642 }
1643
1644 #[test]
1645 fn a_level_is_named_or_numbered_and_must_be_declared() {
1646 let header = TranscriptHeader::new("rite test", "0", false);
1647 assert_eq!(header.level("public"), Some(Level::PUBLIC));
1648 assert_eq!(header.level("20"), Some(Level::RESTRICTED));
1649 assert_eq!(header.level("15"), None);
1650 assert_eq!(header.level("partners"), None);
1651 }
1652
1653 #[test]
1654 fn levels_are_ordered_integers() {
1655 assert!(Level::PUBLIC < Level::RESTRICTED);
1656 assert!(Level::RESTRICTED < Level::CONFIDENTIAL);
1657 assert!(Level::new(15) > Level::PUBLIC && Level::new(15) < Level::RESTRICTED);
1658 assert_eq!(
1659 serde_json::to_value(Level::CONFIDENTIAL).expect("serialize"),
1660 json!(30)
1661 );
1662 assert_eq!(Level::RESTRICTED.to_string(), "restricted");
1663 assert_eq!(Level::new(15).to_string(), "level 15");
1664 }
1665
1666 #[test]
1667 fn ceremony_started() {
1668 assert_json(
1669 &StepFact::CeremonyStarted {
1670 name: "Root CA".to_string(),
1671 template: Sha256Digest::of(b"ceremony"),
1672 },
1673 &json!({
1674 "type": "ceremony_started",
1675 "name": "Root CA",
1676 "template": Sha256Digest::of(b"ceremony").as_str(),
1677 }),
1678 );
1679 }
1680
1681 #[test]
1682 fn header() {
1683 let header = TranscriptHeader::new("rite 0.6.0", &"0".repeat(32), false);
1684 assert_eq!(
1685 serde_json::to_value(&header).expect("serialize header"),
1686 json!({
1687 "rite_transcript": 0,
1688 "vocabulary": 0,
1689 "producer": "rite 0.6.0",
1690 "run_id": "0".repeat(32),
1691 "dry_run": false,
1692 "levels": { "public": 10, "restricted": 20, "confidential": 30 },
1693 }),
1694 );
1695 }
1696
1697 #[test]
1698 fn role_assigned() {
1699 assert_json(
1700 &StepFact::RoleAssigned {
1701 role: RoleId::new("crypto_officer"),
1702 person: "Alice Rivera".to_string(),
1703 },
1704 &json!({
1705 "type": "role_assigned",
1706 "role": "crypto_officer",
1707 "person": "Alice Rivera",
1708 }),
1709 );
1710 }
1711
1712 #[test]
1713 fn parameter_bound() {
1714 assert_json(
1715 &StepFact::ParameterBound {
1716 name: ParamId::new("validity_days"),
1717 value: json!(3650),
1718 },
1719 &json!({
1720 "type": "parameter_bound",
1721 "name": "validity_days",
1722 "value": 3650,
1723 }),
1724 );
1725 }
1726
1727 #[test]
1728 fn material_loaded() {
1729 assert_json(
1730 &StepFact::MaterialLoaded {
1731 name: MaterialId::new("usb_drive"),
1732 identifier: Some("SN-1234".to_string()),
1733 },
1734 &json!({
1735 "type": "material_loaded",
1736 "name": "usb_drive",
1737 "identifier": "SN-1234",
1738 }),
1739 );
1740 assert_json(
1741 &StepFact::MaterialLoaded {
1742 name: MaterialId::new("csr"),
1743 identifier: None,
1744 },
1745 &json!({ "type": "material_loaded", "name": "csr" }),
1746 );
1747 }
1748
1749 #[test]
1750 fn material_digest() {
1751 assert_json(
1752 &StepFact::MaterialDigest {
1753 name: MaterialId::new("csr"),
1754 digest: Sha256Digest::of(b"csr"),
1755 },
1756 &json!({
1757 "type": "material_digest",
1758 "name": "csr",
1759 "digest": Sha256Digest::of(b"csr").as_str(),
1760 }),
1761 );
1762 }
1763
1764 #[test]
1765 fn backend_bound() {
1766 assert_json(
1767 &StepFact::BackendBound {
1768 name: "token".to_string(),
1769 provider: "yubikey".to_string(),
1770 identity: "yubikey-serial=12345678+firmware=5.7.1".to_string(),
1771 },
1772 &json!({
1773 "type": "backend_bound",
1774 "name": "token",
1775 "provider": "yubikey",
1776 "identity": "yubikey-serial=12345678+firmware=5.7.1",
1777 }),
1778 );
1779 }
1780
1781 #[test]
1782 fn machine_info_recorded() {
1783 assert_json(
1784 &StepFact::MachineInfoRecorded {
1785 step: StepId::new("capture"),
1786 info: json!({ "arch": "x86_64", "hostname": null }),
1787 },
1788 &json!({
1789 "type": "machine_info_recorded",
1790 "step": "capture",
1791 "info": { "arch": "x86_64", "hostname": null },
1792 }),
1793 );
1794 }
1795
1796 #[test]
1797 fn act_started() {
1798 assert_json(
1799 &StepFact::ActStarted {
1800 id: ActId::new("setup"),
1801 label: "Setup".to_string(),
1802 },
1803 &json!({
1804 "type": "act_started",
1805 "id": "setup",
1806 "label": "Setup",
1807 }),
1808 );
1809 }
1810
1811 #[test]
1812 fn step_started() {
1813 assert_json(
1814 &StepFact::StepStarted {
1815 id: StepId::new("s1"),
1816 label: "2.1".to_string(),
1817 role: RoleId::new("crypto_officer"),
1818 },
1819 &json!({
1820 "type": "step_started",
1821 "id": "s1",
1822 "label": "2.1",
1823 "role": "crypto_officer",
1824 }),
1825 );
1826 }
1827
1828 #[test]
1829 fn role_declared() {
1830 assert_json(
1831 &StepFact::RoleDeclared {
1832 role: RoleId::new("crypto_officer"),
1833 name: "Crypto Officer".to_string(),
1834 },
1835 &json!({
1836 "type": "role_declared",
1837 "role": "crypto_officer",
1838 "name": "Crypto Officer",
1839 }),
1840 );
1841 }
1842
1843 #[test]
1844 fn prompt_answered_confirm_bool() {
1845 assert_json(
1846 &StepFact::PromptAnswered {
1847 step: Some(StepId::new("s1")),
1848 prompt: Prompt::Confirm {
1849 question: "Proceed?".to_string(),
1850 default: Some(true),
1851 },
1852 response: ResponseRecord::Bool { value: true },
1853 },
1854 &json!({
1855 "type": "prompt_answered",
1856 "step": "s1",
1857 "prompt": { "type": "confirm", "question": "Proceed?", "default": true },
1858 "response": { "type": "bool", "value": true },
1859 }),
1860 );
1861 }
1862
1863 #[test]
1864 fn prompt_answered_text_nonempty() {
1865 assert_json(
1866 &StepFact::PromptAnswered {
1867 step: Some(StepId::new("s1")),
1868 prompt: Prompt::Text {
1869 label: "Name".to_string(),
1870 validator: ValidatorSpec::NonEmpty,
1871 },
1872 response: ResponseRecord::Text {
1873 value: "Alice".to_string(),
1874 },
1875 },
1876 &json!({
1877 "type": "prompt_answered",
1878 "step": "s1",
1879 "prompt": {
1880 "type": "text",
1881 "label": "Name",
1882 "validator": { "kind": "non_empty" },
1883 },
1884 "response": { "type": "text", "value": "Alice" },
1885 }),
1886 );
1887 }
1888
1889 #[test]
1890 fn prompt_answered_text_regex() {
1891 assert_json(
1892 &StepFact::PromptAnswered {
1893 step: Some(StepId::new("s1")),
1894 prompt: Prompt::Text {
1895 label: "SN".to_string(),
1896 validator: ValidatorSpec::Regex(r"^[A-Z0-9]+$".to_string()),
1897 },
1898 response: ResponseRecord::Text {
1899 value: "AB12".to_string(),
1900 },
1901 },
1902 &json!({
1903 "type": "prompt_answered",
1904 "step": "s1",
1905 "prompt": {
1906 "type": "text",
1907 "label": "SN",
1908 "validator": { "kind": "regex", "value": "^[A-Z0-9]+$" },
1909 },
1910 "response": { "type": "text", "value": "AB12" },
1911 }),
1912 );
1913 }
1914
1915 #[test]
1916 fn prompt_answered_text_predefined() {
1917 assert_json(
1918 &StepFact::PromptAnswered {
1919 step: Some(StepId::new("s1")),
1920 prompt: Prompt::Text {
1921 label: "SN".to_string(),
1922 validator: ValidatorSpec::Predefined("serial_number".to_string()),
1923 },
1924 response: ResponseRecord::Text {
1925 value: "ABCD".to_string(),
1926 },
1927 },
1928 &json!({
1929 "type": "prompt_answered",
1930 "step": "s1",
1931 "prompt": {
1932 "type": "text",
1933 "label": "SN",
1934 "validator": { "kind": "predefined", "value": "serial_number" },
1935 },
1936 "response": { "type": "text", "value": "ABCD" },
1937 }),
1938 );
1939 }
1940
1941 #[test]
1942 fn prompt_answered_secret_redacted() {
1943 assert_json(
1944 &StepFact::PromptAnswered {
1945 step: Some(StepId::new("s1")),
1946 prompt: Prompt::Secret {
1947 label: "PIN".to_string(),
1948 validator: ValidatorSpec::NonEmpty,
1949 },
1950 response: ResponseRecord::SecretRedacted {},
1951 },
1952 &json!({
1953 "type": "prompt_answered",
1954 "step": "s1",
1955 "prompt": {
1956 "type": "secret",
1957 "label": "PIN",
1958 "validator": { "kind": "non_empty" },
1959 },
1960 "response": { "type": "secret_redacted" },
1961 }),
1962 );
1963 }
1964
1965 #[test]
1966 fn prompt_answered_literal_text() {
1967 assert_json(
1968 &StepFact::PromptAnswered {
1969 step: Some(StepId::new("s1")),
1970 prompt: Prompt::Literal {
1971 label: "Type 'attest'".to_string(),
1972 expected: "attest".to_string(),
1973 },
1974 response: ResponseRecord::Text {
1975 value: "attest".to_string(),
1976 },
1977 },
1978 &json!({
1979 "type": "prompt_answered",
1980 "step": "s1",
1981 "prompt": { "type": "literal", "label": "Type 'attest'", "expected": "attest" },
1982 "response": { "type": "text", "value": "attest" },
1983 }),
1984 );
1985 }
1986
1987 #[test]
1988 fn prompt_answered_continue_acknowledged() {
1989 assert_json(
1990 &StepFact::PromptAnswered {
1991 step: Some(StepId::new("s1")),
1992 prompt: Prompt::Continue {
1993 hint: Some("Press Enter".to_string()),
1994 },
1995 response: ResponseRecord::Acknowledged,
1996 },
1997 &json!({
1998 "type": "prompt_answered",
1999 "step": "s1",
2000 "prompt": { "type": "continue", "hint": "Press Enter" },
2001 "response": { "type": "acknowledged" },
2002 }),
2003 );
2004 }
2005
2006 #[test]
2007 fn backend_operation() {
2008 assert_json(
2009 &StepFact::BackendOperation {
2010 step: StepId::new("s1"),
2011 kind: "generate_key".to_string(),
2012 backend: Some("hsm".to_string()),
2013 inputs: json!({ "algorithm": "rsa", "bits": 4096 }),
2014 outputs: json!({ "key_id": "k1" }),
2015 fingerprint: Some("sha256:deadbeef".to_string()),
2016 },
2017 &json!({
2018 "type": "backend_operation",
2019 "step": "s1",
2020 "kind": "generate_key",
2021 "backend": "hsm",
2022 "inputs": { "algorithm": "rsa", "bits": 4096 },
2023 "outputs": { "key_id": "k1" },
2024 "fingerprint": "sha256:deadbeef",
2025 }),
2026 );
2027 }
2028
2029 #[test]
2030 fn wrap_recipient_recorded() {
2031 let fingerprint = Sha256Digest::of(b"escrow spki").to_string();
2032 assert_json(
2033 &StepFact::WrapRecipientRecorded {
2034 step: StepId::new("s1"),
2035 source: "escrow_pubkey".to_string(),
2036 fingerprint: fingerprint.clone(),
2037 declared: true,
2038 },
2039 &json!({
2040 "type": "wrap_recipient_recorded",
2041 "step": "s1",
2042 "source": "escrow_pubkey",
2043 "fingerprint": fingerprint,
2044 "declared": true,
2045 }),
2046 );
2047 }
2048
2049 #[test]
2050 fn attestation_recorded() {
2051 assert_json(
2052 &StepFact::AttestationRecorded {
2053 step: StepId::new("s1"),
2054 role: RoleId::new("crypto_officer"),
2055 statement: "I confirm.".to_string(),
2056 },
2057 &json!({
2058 "type": "attestation_recorded",
2059 "step": "s1",
2060 "role": "crypto_officer",
2061 "statement": "I confirm.",
2062 }),
2063 );
2064 }
2065
2066 #[test]
2067 fn artifact_written() {
2068 assert_json(
2069 &StepFact::ArtifactWritten {
2070 step: StepId::new("s1"),
2071 name: "root_cert".to_string(),
2072 file: "root_cert.crt".to_string(),
2073 digest: Some(Sha256Digest::of(b"cert")),
2074 },
2075 &json!({
2076 "type": "artifact_written",
2077 "step": "s1",
2078 "name": "root_cert",
2079 "file": "root_cert.crt",
2080 "digest": Sha256Digest::of(b"cert").as_str(),
2081 }),
2082 );
2083 }
2084
2085 #[test]
2086 fn artifact_written_secret_has_no_digest() {
2087 assert_json(
2088 &StepFact::ArtifactWritten {
2089 step: StepId::new("s1"),
2090 name: "opened".to_string(),
2091 file: "opened.bin".to_string(),
2092 digest: None,
2093 },
2094 &json!({
2095 "type": "artifact_written",
2096 "step": "s1",
2097 "name": "opened",
2098 "file": "opened.bin",
2099 }),
2100 );
2101 }
2102
2103 #[test]
2104 fn deviation_recorded() {
2105 assert_json(
2106 &StepFact::DeviationRecorded {
2107 step: Some(StepId::new("s1")),
2108 text: "phone rang".to_string(),
2109 },
2110 &json!({
2111 "type": "deviation_recorded",
2112 "step": "s1",
2113 "text": "phone rang",
2114 }),
2115 );
2116 }
2117
2118 #[test]
2119 fn step_completed_completed() {
2120 assert_json(
2121 &StepFact::StepCompleted {
2122 id: StepId::new("s1"),
2123 outcome: StepOutcome::Completed {
2124 message: "done".to_string(),
2125 },
2126 },
2127 &json!({
2128 "type": "step_completed",
2129 "id": "s1",
2130 "outcome": { "status": "completed", "message": "done" },
2131 }),
2132 );
2133 }
2134
2135 #[test]
2136 fn ceremony_completed() {
2137 assert_json(
2138 &StepFact::CeremonyCompleted {},
2139 &json!({
2140 "type": "ceremony_completed",
2141 }),
2142 );
2143 }
2144
2145 #[test]
2146 fn ceremony_failed() {
2147 assert_json(
2148 &StepFact::CeremonyFailed {
2149 error: ErrorRecord::new(
2150 ErrorClass::Abort,
2151 "aborted",
2152 "ceremony aborted by operator",
2153 ),
2154 },
2155 &json!({
2156 "type": "ceremony_failed",
2157 "error": {
2158 "class": "abort",
2159 "kind": "aborted",
2160 "message": "ceremony aborted by operator",
2161 },
2162 }),
2163 );
2164 }
2165
2166 #[test]
2167 fn step_attempt_failed() {
2168 assert_json(
2169 &StepFact::StepAttemptFailed {
2170 step: StepId::new("import_key"),
2171 attempt: 1,
2172 error: ErrorRecord::new(
2173 ErrorClass::Environmental,
2174 "backend_error",
2175 "Token not present",
2176 ),
2177 },
2178 &json!({
2179 "type": "step_attempt_failed",
2180 "step": "import_key",
2181 "attempt": 1,
2182 "error": {
2183 "class": "environmental",
2184 "kind": "backend_error",
2185 "message": "Token not present",
2186 },
2187 }),
2188 );
2189 }
2190
2191 #[test]
2192 fn entropy_seeded() {
2193 assert_json(
2194 &StepFact::EntropySeeded {
2195 m: "00112233".to_string(),
2196 source: "os".to_string(),
2197 derivation: "rite-kdf/v1".to_string(),
2198 },
2199 &json!({
2200 "type": "entropy_seeded",
2201 "m": "00112233",
2202 "source": "os",
2203 "derivation": "rite-kdf/v1",
2204 }),
2205 );
2206 }
2207
2208 #[test]
2209 fn entropy_contributed() {
2210 assert_json(
2211 &StepFact::EntropyContributed {
2212 step: StepId::new("roll_dice"),
2213 epoch: 1,
2214 contribution: "3 1 6 4 2 5".to_string(),
2215 },
2216 &json!({
2217 "type": "entropy_contributed",
2218 "step": "roll_dice",
2219 "epoch": 1,
2220 "contribution": "3 1 6 4 2 5",
2221 }),
2222 );
2223 }
2224
2225 #[test]
2226 fn entropy_drawn() {
2227 assert_json(
2228 &StepFact::EntropyDrawn {
2229 step: StepId::new("issue"),
2230 path: "0/issue/cert-serial".to_string(),
2231 value: "aabbccddeeff00112233".to_string(),
2232 },
2233 &json!({
2234 "type": "entropy_drawn",
2235 "step": "issue",
2236 "path": "0/issue/cert-serial",
2237 "value": "aabbccddeeff00112233",
2238 }),
2239 );
2240 }
2241}