1use std::collections::HashSet;
54use std::fs::{File, OpenOptions};
55use std::io::{self, BufRead, BufReader, BufWriter, Write};
56use std::path::{Path, PathBuf};
57
58use chrono::{DateTime, SecondsFormat, Utc};
59use serde::Deserialize;
60use thiserror::Error;
61
62use rite_model::bundle::TRANSCRIPT_FILE;
63use rite_model::commitment::{SALT_LEN, chain_node, fact_leaf, header_node};
64use rite_model::{
65 FACT_TYPES, Level, Sha256Digest, StepFact, TRANSCRIPT_FORMAT, TRANSCRIPT_SCHEMA,
66 TranscriptHeader, canonical_json,
67};
68
69pub type TranscriptFingerprint = Sha256Digest;
71
72pub trait TranscriptSink: Send {
79 fn begin(&mut self, header: &TranscriptHeader) -> io::Result<()>;
86
87 fn record(&mut self, at: DateTime<Utc>, level: Level, fact: &StepFact) -> io::Result<()>;
102
103 fn finalize(&mut self) -> io::Result<TranscriptFingerprint>;
114}
115
116#[derive(Debug, Default)]
122struct Chain {
123 node: Option<[u8; 32]>,
124 levels: Vec<Level>,
125 finalized: bool,
126}
127
128enum Committed {
131 Header {
132 canonical: String,
133 node: [u8; 32],
134 levels: Vec<Level>,
135 },
136 Fact {
137 at: String,
138 level: Level,
139 salt: [u8; SALT_LEN],
140 canonical: String,
141 leaf: [u8; 32],
142 node: [u8; 32],
143 },
144}
145
146impl Committed {
147 fn node(&self) -> [u8; 32] {
148 match self {
149 Committed::Header { node, .. } | Committed::Fact { node, .. } => *node,
150 }
151 }
152
153 fn line(&self) -> String {
155 match self {
156 Committed::Header {
157 canonical, node, ..
158 } => format!(
159 "{{\"$schema\":\"{TRANSCRIPT_SCHEMA}\",\"header\":{canonical},\"chain\":\"{}\"}}",
160 Sha256Digest::from_bytes(node),
161 ),
162 Committed::Fact {
163 at,
164 level,
165 salt,
166 canonical,
167 leaf,
168 node,
169 } => format!(
170 "{{\"at\":\"{at}\",\"level\":{},\"leaf\":\"{}\",\"chain\":\"{}\",\"salt\":\"{}\",\"fact\":{canonical}}}",
171 level.value(),
172 Sha256Digest::from_bytes(leaf),
173 Sha256Digest::from_bytes(node),
174 base16ct::lower::encode_string(salt),
175 ),
176 }
177 }
178}
179
180impl Chain {
181 fn header(&self, header: &TranscriptHeader) -> io::Result<Committed> {
182 if self.node.is_some() {
183 return Err(io::Error::other("transcript header already written"));
184 }
185 let value = serde_json::to_value(header).map_err(io::Error::other)?;
186 let canonical = canonical_json(&value).map_err(io::Error::other)?;
187 let node = header_node(&canonical);
188 Ok(Committed::Header {
189 canonical,
190 node,
191 levels: header.levels.values().copied().collect(),
192 })
193 }
194
195 fn fact(&self, at: DateTime<Utc>, level: Level, fact: &StepFact) -> io::Result<Committed> {
196 let previous = self
197 .node
198 .ok_or_else(|| io::Error::other("transcript header not written"))?;
199 if self.finalized {
200 return Err(io::Error::other("transcript already finalized"));
201 }
202 if !self.levels.contains(&level) {
204 return Err(io::Error::other(format!(
205 "level {} is not declared in the transcript header",
206 level.value()
207 )));
208 }
209 let value = serde_json::to_value(fact).map_err(io::Error::other)?;
210 let canonical = canonical_json(&value).map_err(io::Error::other)?;
211 let salt: [u8; SALT_LEN] = crate::os_random::os_random()?;
212 let leaf = fact_leaf(&salt, &canonical);
213 let at = at.to_rfc3339_opts(SecondsFormat::Micros, true);
214 let node = chain_node(&previous, &at, level, &leaf);
215 Ok(Committed::Fact {
216 at,
217 level,
218 salt,
219 canonical,
220 leaf,
221 node,
222 })
223 }
224
225 fn advance(&mut self, committed: &Committed) {
226 self.node = Some(committed.node());
227 if let Committed::Header { levels, .. } = committed {
228 self.levels.clone_from(levels);
229 }
230 }
231
232 fn finalize(&mut self) -> io::Result<TranscriptFingerprint> {
233 let node = self
234 .node
235 .ok_or_else(|| io::Error::other("transcript header not written"))?;
236 self.finalized = true;
237 Ok(Sha256Digest::from_bytes(&node))
238 }
239}
240
241#[derive(Debug)]
248pub struct JsonlFileSink {
249 jsonl_path: PathBuf,
250 writer: BufWriter<File>,
251 chain: Chain,
252}
253
254impl JsonlFileSink {
255 pub fn create(dir: &Path) -> io::Result<Self> {
261 let jsonl_path = dir.join(TRANSCRIPT_FILE);
262 let file = OpenOptions::new()
263 .create_new(true)
264 .write(true)
265 .open(&jsonl_path)?;
266 Ok(Self {
267 jsonl_path,
268 writer: BufWriter::new(file),
269 chain: Chain::default(),
270 })
271 }
272
273 fn append(&mut self, line: &str) -> io::Result<()> {
275 self.writer.write_all(line.as_bytes())?;
276 self.writer.write_all(b"\n")?;
277 self.writer.flush()?;
284 self.writer.get_ref().sync_data()
285 }
286
287 #[must_use]
289 pub fn jsonl_path(&self) -> &Path {
290 &self.jsonl_path
291 }
292}
293
294impl TranscriptSink for JsonlFileSink {
295 fn begin(&mut self, header: &TranscriptHeader) -> io::Result<()> {
296 let committed = self.chain.header(header)?;
297 self.append(&committed.line())?;
298 self.chain.advance(&committed);
299 Ok(())
300 }
301
302 fn record(&mut self, at: DateTime<Utc>, level: Level, fact: &StepFact) -> io::Result<()> {
303 let committed = self.chain.fact(at, level, fact)?;
304 self.append(&committed.line())?;
305 self.chain.advance(&committed);
306 Ok(())
307 }
308
309 fn finalize(&mut self) -> io::Result<TranscriptFingerprint> {
310 self.chain.finalize()
311 }
312}
313
314#[derive(Debug, Default)]
320pub struct InMemorySink {
321 header: Option<TranscriptHeader>,
322 facts: Vec<StepFact>,
323 chain: Chain,
324}
325
326impl InMemorySink {
327 #[must_use]
329 pub fn new() -> Self {
330 Self::default()
331 }
332
333 #[must_use]
335 pub fn header(&self) -> Option<&TranscriptHeader> {
336 self.header.as_ref()
337 }
338
339 #[must_use]
341 pub fn facts(&self) -> &[StepFact] {
342 &self.facts
343 }
344
345 #[must_use]
347 pub fn len(&self) -> usize {
348 self.facts.len()
349 }
350
351 #[must_use]
353 pub fn is_empty(&self) -> bool {
354 self.facts.is_empty()
355 }
356}
357
358impl TranscriptSink for InMemorySink {
359 fn begin(&mut self, header: &TranscriptHeader) -> io::Result<()> {
360 let committed = self.chain.header(header)?;
361 self.chain.advance(&committed);
362 self.header = Some(header.clone());
363 Ok(())
364 }
365
366 fn record(&mut self, at: DateTime<Utc>, level: Level, fact: &StepFact) -> io::Result<()> {
367 let committed = self.chain.fact(at, level, fact)?;
368 self.chain.advance(&committed);
369 self.facts.push(fact.clone());
370 Ok(())
371 }
372
373 fn finalize(&mut self) -> io::Result<TranscriptFingerprint> {
374 self.chain.finalize()
375 }
376}
377
378#[derive(Deserialize)]
380#[serde(deny_unknown_fields)]
381struct HeaderLine {
382 #[serde(rename = "$schema", default)]
384 _schema: Option<String>,
385 header: serde_json::Value,
386 chain: Sha256Digest,
387}
388
389#[derive(Deserialize)]
391#[serde(deny_unknown_fields)]
392struct FactLine {
393 at: String,
394 level: Level,
395 #[serde(default, deserialize_with = "present")]
396 salt: Option<String>,
397 #[serde(default, deserialize_with = "present")]
398 fact: Option<serde_json::Value>,
399 leaf: Sha256Digest,
400 chain: Sha256Digest,
401}
402
403fn present<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
406where
407 D: serde::Deserializer<'de>,
408 T: Deserialize<'de>,
409{
410 T::deserialize(deserializer).map(Some)
411}
412
413#[derive(Debug, Clone)]
418pub struct TimedFact {
419 pub line: usize,
421 pub at: DateTime<Utc>,
423 pub level: Level,
425 pub fact: StepFact,
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
431pub struct WithheldLine {
432 pub line: usize,
434 pub level: Level,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq)]
441pub struct UnknownFact {
442 pub line: usize,
444 pub level: Level,
446 pub type_name: String,
448}
449
450#[derive(Debug, Clone)]
452pub struct TranscriptVerified {
453 pub header: TranscriptHeader,
455 pub fact_count: usize,
457 pub fingerprint: TranscriptFingerprint,
459 pub terminated: bool,
463}
464
465#[derive(Debug, Error)]
467pub enum VerifyError {
468 #[error("io error: {0}")]
470 Io(#[from] io::Error),
471 #[error("line {line} is not a valid transcript line: {reason}")]
473 InvalidLine {
474 line: usize,
476 reason: String,
478 },
479 #[error(
482 "line {line}: the fact does not match its leaf (computed {computed}, recorded {recorded})"
483 )]
484 LeafMismatch {
485 line: usize,
487 computed: String,
489 recorded: String,
491 },
492 #[error("line {line}: the chain breaks here (computed {computed}, recorded {recorded})")]
495 ChainMismatch {
496 line: usize,
498 computed: String,
500 recorded: String,
502 },
503 #[error("transcript is empty")]
505 Empty,
506 #[error("line 1 is not a transcript header: {0}")]
508 MissingHeader(String),
509 #[error("the header's levels are invalid: {0}")]
512 InvalidLevels(String),
513 #[error("unknown transcript format {0} (this verifier reads format {TRANSCRIPT_FORMAT})")]
516 UnknownFormat(u64),
517 #[error("entropy source used before it was seeded")]
520 SeedMissing,
521 #[error("entropy source seeded more than once")]
526 DuplicateSeed,
527 #[error("unknown entropy derivation scheme: {0}")]
530 UnknownDerivation(String),
531 #[error("malformed entropy value: {0}")]
533 MalformedEntropy(String),
534 #[error("entropy draw at path '{path}' does not match re-derived value")]
537 DrawMismatch {
538 path: String,
540 },
541 #[error(
546 "entropy draw at path '{path}' records {len} bytes, beyond the {max}-byte HKDF-Expand limit"
547 )]
548 DrawTooLong {
549 path: String,
551 len: usize,
553 max: usize,
555 },
556 #[error("entropy draw path '{path}' does not match expected '{expected_prefix}<purpose>'")]
561 PathMismatch {
562 path: String,
564 expected_prefix: String,
566 },
567 #[error("entropy contribution at step '{step}' records epoch {recorded}, expected {expected}")]
570 EpochMismatch {
571 step: String,
573 recorded: u32,
575 expected: u32,
577 },
578 #[error("entropy draw path '{path}' recorded more than once")]
581 DuplicateDrawPath {
582 path: String,
584 },
585}
586
587pub fn verify_transcript(jsonl_path: &Path) -> Result<TranscriptVerified, VerifyError> {
597 let loaded = read_verified_transcript(jsonl_path)?;
598 Ok(TranscriptVerified {
599 header: loaded.header,
600 fact_count: loaded.facts.len(),
601 fingerprint: loaded.fingerprint,
602 terminated: loaded.terminated,
603 })
604}
605
606#[derive(Debug, Clone)]
609pub struct LoadedTranscript {
610 pub header: TranscriptHeader,
612 pub facts: Vec<TimedFact>,
615 pub withheld: Vec<WithheldLine>,
617 pub unknown: Vec<UnknownFact>,
619 pub fingerprint: TranscriptFingerprint,
621 pub terminated: bool,
624}
625
626pub fn read_verified_transcript(jsonl_path: &Path) -> Result<LoadedTranscript, VerifyError> {
636 let file = File::open(jsonl_path)?;
637 verify_lines(BufReader::new(file).lines())
638}
639
640pub fn verify_jsonl(jsonl: &str) -> Result<LoadedTranscript, VerifyError> {
646 verify_lines(jsonl.lines().map(|line| Ok(line.to_string())))
647}
648
649fn verify_lines(
650 mut lines: impl Iterator<Item = io::Result<String>>,
651) -> Result<LoadedTranscript, VerifyError> {
652 let first = lines.next().ok_or(VerifyError::Empty)??;
653 let (header, mut node) = read_header(&first)?;
654
655 let mut loaded = LoadedTranscript {
656 header,
657 facts: Vec::new(),
658 withheld: Vec::new(),
659 unknown: Vec::new(),
660 fingerprint: Sha256Digest::from_bytes(&node),
661 terminated: false,
662 };
663
664 for (idx, line) in lines.enumerate() {
665 let line = line?;
666 let line_no = idx.saturating_add(2);
668 node = read_fact_line(&line, line_no, &node, &mut loaded)?;
669 }
670
671 loaded.fingerprint = Sha256Digest::from_bytes(&node);
672 Ok(loaded)
673}
674
675pub fn disclose_transcript(jsonl: &str, threshold: Level) -> Result<String, VerifyError> {
688 let mut lines = jsonl.lines();
689 let header = lines.next().ok_or(VerifyError::Empty)?;
690 let mut out = String::with_capacity(jsonl.len());
691 out.push_str(header);
692 out.push('\n');
693 for (idx, line) in lines.enumerate() {
694 let line_no = idx.saturating_add(2);
696 let parsed: FactLine = serde_json::from_str(line).map_err(|e| invalid(line_no, &e))?;
697 if parsed.level > threshold && parsed.fact.is_some() {
698 out.push_str(&withheld_line(&parsed)?);
699 } else {
700 out.push_str(line);
701 }
702 out.push('\n');
703 }
704 Ok(out)
705}
706
707fn withheld_line(line: &FactLine) -> Result<String, VerifyError> {
709 let at = serde_json::to_string(&line.at).map_err(|e| invalid(0, &e))?;
710 Ok(format!(
711 "{{\"at\":{at},\"level\":{},\"leaf\":\"{}\",\"chain\":\"{}\"}}",
712 line.level.value(),
713 line.leaf,
714 line.chain,
715 ))
716}
717
718fn read_header(line: &str) -> Result<(TranscriptHeader, [u8; 32]), VerifyError> {
720 let parsed: HeaderLine =
721 serde_json::from_str(line).map_err(|e| VerifyError::MissingHeader(e.to_string()))?;
722 let format = parsed
725 .header
726 .get("rite_transcript")
727 .and_then(serde_json::Value::as_u64)
728 .ok_or_else(|| VerifyError::MissingHeader("no rite_transcript version".to_string()))?;
729 if format != u64::from(TRANSCRIPT_FORMAT) {
730 return Err(VerifyError::UnknownFormat(format));
731 }
732 let canonical = canonical_json(&parsed.header).map_err(|e| invalid(1, &e))?;
733 let node = header_node(&canonical);
734 check_checkpoint(&parsed.chain, &node, |computed, recorded| {
735 VerifyError::ChainMismatch {
736 line: 1,
737 computed,
738 recorded,
739 }
740 })?;
741 let header: TranscriptHeader = serde_json::from_value(parsed.header)
742 .map_err(|e| VerifyError::MissingHeader(e.to_string()))?;
743 check_levels(&header)?;
744 Ok((header, node))
745}
746
747fn check_levels(header: &TranscriptHeader) -> Result<(), VerifyError> {
750 for (name, level) in Level::BUILT_IN {
751 if header.levels.get(name) != Some(&level) {
752 return Err(VerifyError::InvalidLevels(format!(
753 "'{name}' must be {}",
754 level.value()
755 )));
756 }
757 }
758 let mut seen = HashSet::new();
759 for level in header.levels.values() {
760 if !seen.insert(*level) {
761 return Err(VerifyError::InvalidLevels(format!(
762 "more than one name for {}",
763 level.value()
764 )));
765 }
766 }
767 Ok(())
768}
769
770fn read_fact_line(
773 line: &str,
774 line_no: usize,
775 previous: &[u8; 32],
776 loaded: &mut LoadedTranscript,
777) -> Result<[u8; 32], VerifyError> {
778 let parsed: FactLine = serde_json::from_str(line).map_err(|e| invalid(line_no, &e))?;
779 if !loaded.header.declares(parsed.level) {
780 return Err(invalid(
781 line_no,
782 &format!(
783 "level {} is not declared in the header",
784 parsed.level.value()
785 ),
786 ));
787 }
788 let at: DateTime<Utc> = DateTime::parse_from_rfc3339(&parsed.at)
789 .map_err(|e| invalid(line_no, &e))?
790 .with_timezone(&Utc);
791
792 let leaf = match (parsed.salt, parsed.fact) {
793 (Some(salt), Some(fact)) => {
794 let salt = decode_salt(&salt).ok_or_else(|| {
795 invalid(
796 line_no,
797 &format!("salt is not {SALT_LEN} bytes of lowercase hex"),
798 )
799 })?;
800 let canonical = canonical_json(&fact).map_err(|e| invalid(line_no, &e))?;
801 let leaf = fact_leaf(&salt, &canonical);
802 check_checkpoint(&parsed.leaf, &leaf, |computed, recorded| {
803 VerifyError::LeafMismatch {
804 line: line_no,
805 computed,
806 recorded,
807 }
808 })?;
809 loaded.terminated = add_fact(fact, at, parsed.level, line_no, loaded)?;
810 leaf
811 }
812 (None, None) => {
813 loaded.withheld.push(WithheldLine {
814 line: line_no,
815 level: parsed.level,
816 });
817 loaded.terminated = false;
818 parsed.leaf.to_bytes()
819 }
820 _ => {
821 return Err(invalid(
822 line_no,
823 &"a line carries both `salt` and `fact`, or neither",
824 ));
825 }
826 };
827
828 let node = chain_node(previous, &parsed.at, parsed.level, &leaf);
829 check_checkpoint(&parsed.chain, &node, |computed, recorded| {
830 VerifyError::ChainMismatch {
831 line: line_no,
832 computed,
833 recorded,
834 }
835 })?;
836 Ok(node)
837}
838
839fn add_fact(
844 fact: serde_json::Value,
845 at: DateTime<Utc>,
846 level: Level,
847 line_no: usize,
848 loaded: &mut LoadedTranscript,
849) -> Result<bool, VerifyError> {
850 let type_name = fact
851 .get("type")
852 .and_then(serde_json::Value::as_str)
853 .ok_or_else(|| invalid(line_no, &"the fact has no `type`"))?
854 .to_string();
855 if !FACT_TYPES.contains(&type_name.as_str()) {
856 loaded.unknown.push(UnknownFact {
857 line: line_no,
858 level,
859 type_name,
860 });
861 return Ok(false);
862 }
863 let fact: StepFact = serde_json::from_value(fact).map_err(|e| invalid(line_no, &e))?;
864 let terminal = matches!(
865 fact,
866 StepFact::CeremonyCompleted { .. } | StepFact::CeremonyFailed { .. }
867 );
868 loaded.facts.push(TimedFact {
869 line: line_no,
870 at,
871 level,
872 fact,
873 });
874 Ok(terminal)
875}
876
877fn decode_salt(hex: &str) -> Option<[u8; SALT_LEN]> {
878 base16ct::lower::decode_vec(hex).ok()?.try_into().ok()
879}
880
881fn check_checkpoint(
883 recorded: &Sha256Digest,
884 computed: &[u8; 32],
885 mismatch: impl FnOnce(String, String) -> VerifyError,
886) -> Result<(), VerifyError> {
887 if recorded.to_bytes() == *computed {
888 Ok(())
889 } else {
890 Err(mismatch(
891 Sha256Digest::from_bytes(computed).to_string(),
892 recorded.to_string(),
893 ))
894 }
895}
896
897fn invalid(line: usize, reason: &impl std::fmt::Display) -> VerifyError {
898 VerifyError::InvalidLine {
899 line,
900 reason: reason.to_string(),
901 }
902}
903
904#[derive(Debug, Clone, Default)]
906pub struct EntropyVerified {
907 pub derivation: Option<String>,
910 pub source: Option<String>,
915 pub contributions: usize,
917 pub values_verified: usize,
919}
920
921pub fn verify_entropy<'a>(
955 facts: impl IntoIterator<Item = &'a StepFact>,
956) -> Result<EntropyVerified, VerifyError> {
957 let mut seed: Option<[u8; 32]> = None;
958 let mut epoch: u32 = 0;
959 let mut drawn_paths: HashSet<String> = HashSet::new();
960 let mut result = EntropyVerified::default();
961
962 for fact in facts {
963 match fact {
964 StepFact::EntropySeeded {
965 m,
966 source,
967 derivation,
968 ..
969 } => {
970 if seed.is_some() {
971 return Err(VerifyError::DuplicateSeed);
972 }
973 if derivation != crate::entropy::DERIVATION_V1 {
974 return Err(VerifyError::UnknownDerivation(derivation.clone()));
975 }
976 let m_bytes = base16ct::lower::decode_vec(m)
977 .map_err(|e| VerifyError::MalformedEntropy(format!("seed m: {e}")))?;
978 seed = Some(crate::entropy::initial_seed(&m_bytes));
979 result.derivation = Some(derivation.clone());
980 result.source = Some(source.clone());
981 }
982 StepFact::EntropyContributed {
983 step,
984 epoch: recorded_epoch,
985 contribution,
986 ..
987 } => {
988 let current = seed.as_ref().ok_or(VerifyError::SeedMissing)?;
989 let expected = epoch.saturating_add(1);
990 if *recorded_epoch != expected {
991 return Err(VerifyError::EpochMismatch {
992 step: step.as_str().to_string(),
993 recorded: *recorded_epoch,
994 expected,
995 });
996 }
997 seed = Some(crate::entropy::fold_seed(current, contribution.as_bytes()));
998 epoch = expected;
999 result.contributions = result.contributions.saturating_add(1);
1000 }
1001 StepFact::EntropyDrawn {
1002 step, path, value, ..
1003 } => {
1004 let current = seed.as_ref().ok_or(VerifyError::SeedMissing)?;
1005 let expected_prefix = crate::entropy::build_path(epoch, step, "");
1010 if !path.starts_with(&expected_prefix) || path.len() == expected_prefix.len() {
1011 return Err(VerifyError::PathMismatch {
1012 path: path.clone(),
1013 expected_prefix,
1014 });
1015 }
1016 if !drawn_paths.insert(path.clone()) {
1017 return Err(VerifyError::DuplicateDrawPath { path: path.clone() });
1018 }
1019 let recorded = base16ct::lower::decode_vec(value)
1022 .map_err(|e| VerifyError::MalformedEntropy(format!("draw {path}: {e}")))?;
1023 if recorded.len() > crate::entropy::MAX_DRAW_LEN {
1024 return Err(VerifyError::DrawTooLong {
1025 path: path.clone(),
1026 len: recorded.len(),
1027 max: crate::entropy::MAX_DRAW_LEN,
1028 });
1029 }
1030 let expected = crate::entropy::derive_value(current, path, recorded.len());
1031 if expected != recorded {
1032 return Err(VerifyError::DrawMismatch { path: path.clone() });
1033 }
1034 result.values_verified = result.values_verified.saturating_add(1);
1035 }
1036 _ => {}
1037 }
1038 }
1039
1040 Ok(result)
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use super::*;
1046 use crate::test_support::fixed_test_time as test_at;
1047
1048 fn sample_fact() -> StepFact {
1049 crate::test_support::ceremony_started("Test")
1050 }
1051
1052 fn begun_file_sink(dir: &Path) -> JsonlFileSink {
1053 let mut sink = JsonlFileSink::create(dir).expect("create");
1054 sink.begin(&crate::test_support::test_header())
1055 .expect("header");
1056 sink
1057 }
1058
1059 #[test]
1060 fn in_memory_sink_records_and_finalizes() {
1061 let mut sink = crate::test_support::begun_sink();
1062 assert!(sink.is_empty());
1063 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1064 .expect("record");
1065 assert_eq!(sink.len(), 1);
1066 let fp = sink.finalize().expect("finalize");
1067 assert!(fp.as_str().starts_with("sha256:"));
1068 }
1069
1070 #[test]
1071 fn in_memory_sink_rejects_record_after_finalize() {
1072 let mut sink = crate::test_support::begun_sink();
1073 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1074 .expect("record");
1075 sink.finalize().expect("finalize");
1076 let err = sink
1077 .record(test_at(), Level::PUBLIC, &sample_fact())
1078 .expect_err("should fail");
1079 assert!(err.to_string().contains("already finalized"));
1080 }
1081
1082 #[test]
1083 fn in_memory_sink_chain_advances() {
1084 let mut sink = crate::test_support::begun_sink();
1085 let initial = sink.chain.node;
1086 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1087 .expect("record");
1088 assert_ne!(sink.chain.node, initial);
1089 let after_first = sink.chain.node;
1090 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1091 .expect("record");
1092 assert_ne!(sink.chain.node, after_first);
1093 }
1094
1095 #[test]
1096 fn record_persists_the_supplied_time_and_reader_recovers_it() {
1097 let tmp = tempfile::tempdir().expect("tempdir");
1100 let mut sink = begun_file_sink(tmp.path());
1101 let at = test_at();
1102 sink.record(at, Level::PUBLIC, &sample_fact())
1103 .expect("record");
1104 sink.finalize().expect("finalize");
1105
1106 let loaded = read_verified_transcript(sink.jsonl_path()).expect("read");
1107 assert_eq!(loaded.facts.first().expect("one fact").at, at);
1108 }
1109
1110 #[test]
1111 fn jsonl_file_sink_writes_checkpoints_that_fold_to_the_fingerprint() {
1112 let tmp = tempfile::tempdir().expect("tempdir");
1113 let mut sink = begun_file_sink(tmp.path());
1114 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1115 .expect("record");
1116 sink.record(test_at(), Level::RESTRICTED, &sample_fact())
1117 .expect("record");
1118 let fp = sink.finalize().expect("finalize");
1119
1120 let jsonl = std::fs::read_to_string(sink.jsonl_path()).expect("read jsonl");
1121 let lines: Vec<serde_json::Value> = jsonl
1122 .lines()
1123 .map(|l| serde_json::from_str(l).expect("parse line"))
1124 .collect();
1125 assert_eq!(lines.len(), 3, "expected a header and two facts");
1126 assert!(lines.first().and_then(|l| l.get("header")).is_some());
1127 let second = lines.get(2).expect("second fact");
1128 assert_eq!(
1129 second.get("level").and_then(serde_json::Value::as_u64),
1130 Some(20)
1131 );
1132 assert_eq!(
1133 lines
1134 .last()
1135 .and_then(|l| l.get("chain"))
1136 .and_then(serde_json::Value::as_str),
1137 Some(fp.as_str()),
1138 "the last chain value is the fingerprint"
1139 );
1140 }
1141
1142 #[test]
1143 fn two_records_of_the_same_fact_get_different_salts() {
1144 let tmp = tempfile::tempdir().expect("tempdir");
1145 let mut sink = begun_file_sink(tmp.path());
1146 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1147 .expect("record");
1148 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1149 .expect("record");
1150 let jsonl = std::fs::read_to_string(sink.jsonl_path()).expect("read jsonl");
1151 let salts: Vec<String> = jsonl
1152 .lines()
1153 .skip(1)
1154 .map(|l| {
1155 let v: serde_json::Value = serde_json::from_str(l).expect("parse");
1156 v.get("salt")
1157 .and_then(serde_json::Value::as_str)
1158 .expect("salt")
1159 .to_string()
1160 })
1161 .collect();
1162 assert_eq!(salts.len(), 2);
1163 assert_ne!(salts.first(), salts.last());
1164 }
1165
1166 #[test]
1167 fn jsonl_file_sink_refuses_existing_file() {
1168 let tmp = tempfile::tempdir().expect("tempdir");
1169 let _first = JsonlFileSink::create(tmp.path()).expect("first");
1170 let err = JsonlFileSink::create(tmp.path()).expect_err("second should fail");
1171 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
1172 }
1173
1174 #[test]
1175 fn verify_round_trip_succeeds_on_well_formed_transcript() {
1176 let tmp = tempfile::tempdir().expect("tempdir");
1177 let mut sink = begun_file_sink(tmp.path());
1178 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1179 .expect("record");
1180 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1181 .expect("record");
1182 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1183 .expect("record");
1184 let written = sink.finalize().expect("finalize");
1185
1186 let verified = verify_transcript(sink.jsonl_path()).expect("verify");
1187 assert_eq!(verified.fact_count, 3);
1188 assert_eq!(verified.fingerprint, written);
1189 assert!(!verified.terminated);
1191 }
1192
1193 fn three_fact_transcript(dir: &Path) -> (PathBuf, Vec<String>) {
1196 let mut sink = begun_file_sink(dir);
1197 for _ in 0..3 {
1198 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1199 .expect("record");
1200 }
1201 let path = sink.jsonl_path().to_path_buf();
1202 let lines = std::fs::read_to_string(&path)
1203 .expect("read")
1204 .lines()
1205 .map(String::from)
1206 .collect();
1207 (path, lines)
1208 }
1209
1210 fn rewrite(path: &Path, lines: &[String]) {
1211 std::fs::write(path, lines.join("\n") + "\n").expect("write");
1212 }
1213
1214 fn edit_line(lines: &mut [String], index: usize, edit: impl FnOnce(&mut serde_json::Value)) {
1215 let line = lines.get_mut(index).expect("line");
1216 let mut value: serde_json::Value = serde_json::from_str(line).expect("parse");
1217 edit(&mut value);
1218 *line = serde_json::to_string(&value).expect("serialize");
1219 }
1220
1221 #[test]
1222 fn an_edited_fact_is_caught_at_its_leaf() {
1223 let tmp = tempfile::tempdir().expect("tempdir");
1224 let (path, mut lines) = three_fact_transcript(tmp.path());
1225 edit_line(&mut lines, 2, |v| {
1226 if let Some(fact) = v.get_mut("fact").and_then(serde_json::Value::as_object_mut) {
1227 fact.insert("name".to_string(), serde_json::json!("Other"));
1228 }
1229 });
1230 rewrite(&path, &lines);
1231 let err = verify_transcript(&path).expect_err("tampered fact");
1232 assert!(matches!(err, VerifyError::LeafMismatch { line: 3, .. }));
1233 }
1234
1235 #[test]
1236 fn an_edited_level_is_caught_at_its_chain_value() {
1237 let tmp = tempfile::tempdir().expect("tempdir");
1238 let (path, mut lines) = three_fact_transcript(tmp.path());
1239 edit_line(&mut lines, 2, |v| {
1240 if let Some(obj) = v.as_object_mut() {
1241 obj.insert("level".to_string(), serde_json::json!(30));
1242 }
1243 });
1244 rewrite(&path, &lines);
1245 let err = verify_transcript(&path).expect_err("tampered level");
1246 assert!(matches!(err, VerifyError::ChainMismatch { line: 3, .. }));
1247 }
1248
1249 #[test]
1250 fn a_deleted_line_breaks_the_chain_at_the_next_one() {
1251 let tmp = tempfile::tempdir().expect("tempdir");
1252 let (path, mut lines) = three_fact_transcript(tmp.path());
1253 lines.remove(2);
1254 rewrite(&path, &lines);
1255 let err = verify_transcript(&path).expect_err("deleted line");
1256 assert!(matches!(err, VerifyError::ChainMismatch { line: 3, .. }));
1257 }
1258
1259 #[test]
1260 fn a_withheld_line_folds_to_the_same_fingerprint() {
1261 let tmp = tempfile::tempdir().expect("tempdir");
1262 let (path, mut lines) = three_fact_transcript(tmp.path());
1263 let complete = verify_transcript(&path).expect("complete").fingerprint;
1264 edit_line(&mut lines, 2, |v| {
1265 if let Some(obj) = v.as_object_mut() {
1266 obj.remove("salt");
1267 obj.remove("fact");
1268 }
1269 });
1270 rewrite(&path, &lines);
1271 let loaded = read_verified_transcript(&path).expect("withheld");
1272 assert_eq!(loaded.fingerprint, complete);
1273 assert_eq!(
1274 loaded.withheld,
1275 vec![WithheldLine {
1276 line: 3,
1277 level: Level::PUBLIC
1278 }]
1279 );
1280 assert_eq!(loaded.facts.len(), 2);
1281 }
1282
1283 #[test]
1284 fn disclosing_withholds_above_the_threshold_and_keeps_the_fingerprint() {
1285 let tmp = tempfile::tempdir().expect("tempdir");
1286 let mut sink = begun_file_sink(tmp.path());
1287 for level in [Level::PUBLIC, Level::CONFIDENTIAL, Level::RESTRICTED] {
1288 sink.record(test_at(), level, &sample_fact())
1289 .expect("record");
1290 }
1291 let complete = sink.finalize().expect("finalize");
1292 let text = std::fs::read_to_string(sink.jsonl_path()).expect("read");
1293
1294 let disclosed = disclose_transcript(&text, Level::RESTRICTED).expect("disclose");
1295 let path = tmp.path().join("disclosed.jsonl");
1296 std::fs::write(&path, &disclosed).expect("write");
1297 let loaded = read_verified_transcript(&path).expect("verify");
1298
1299 assert_eq!(loaded.fingerprint, complete);
1300 assert_eq!(
1301 loaded.withheld,
1302 vec![WithheldLine {
1303 line: 3,
1304 level: Level::CONFIDENTIAL
1305 }]
1306 );
1307 assert_eq!(loaded.facts.len(), 2);
1308 assert_eq!(
1310 disclose_transcript(&disclosed, Level::RESTRICTED).expect("again"),
1311 disclosed
1312 );
1313 }
1314
1315 #[test]
1316 fn a_line_with_a_fact_and_no_salt_is_invalid() {
1317 let tmp = tempfile::tempdir().expect("tempdir");
1318 let (path, mut lines) = three_fact_transcript(tmp.path());
1319 edit_line(&mut lines, 2, |v| {
1320 if let Some(obj) = v.as_object_mut() {
1321 obj.remove("salt");
1322 }
1323 });
1324 rewrite(&path, &lines);
1325 let err = verify_transcript(&path).expect_err("half-withheld line");
1326 assert!(matches!(err, VerifyError::InvalidLine { line: 3, .. }));
1327 }
1328
1329 #[test]
1330 fn a_line_with_null_salt_and_fact_is_not_withheld() {
1331 let tmp = tempfile::tempdir().expect("tempdir");
1332 let (path, mut lines) = three_fact_transcript(tmp.path());
1333 edit_line(&mut lines, 2, |v| {
1334 if let Some(obj) = v.as_object_mut() {
1335 obj.insert("salt".to_string(), serde_json::Value::Null);
1336 obj.insert("fact".to_string(), serde_json::Value::Null);
1337 }
1338 });
1339 rewrite(&path, &lines);
1340 let err = verify_transcript(&path).expect_err("null members");
1341 assert!(matches!(err, VerifyError::InvalidLine { line: 3, .. }));
1342 }
1343
1344 #[test]
1345 fn an_unknown_fact_type_is_counted_not_refused() {
1346 let tmp = tempfile::tempdir().expect("tempdir");
1347 let mut sink = begun_file_sink(tmp.path());
1348 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1349 .expect("record");
1350 let path = sink.jsonl_path().to_path_buf();
1351 let mut lines: Vec<String> = std::fs::read_to_string(&path)
1352 .expect("read")
1353 .lines()
1354 .map(String::from)
1355 .collect();
1356 let previous: serde_json::Value =
1358 serde_json::from_str(lines.last().expect("last")).expect("parse");
1359 let previous_node = Sha256Digest::parse(
1360 previous
1361 .get("chain")
1362 .and_then(serde_json::Value::as_str)
1363 .expect("chain"),
1364 )
1365 .expect("digest")
1366 .to_bytes();
1367 let fact = serde_json::json!({ "type": "future_fact", "detail": 1 });
1368 let salt = [7u8; SALT_LEN];
1369 let leaf = fact_leaf(&salt, &canonical_json(&fact).expect("canonical"));
1370 let at = "2026-09-22T10:00:00.000000Z";
1371 let node = chain_node(&previous_node, at, Level::PUBLIC, &leaf);
1372 lines.push(
1373 serde_json::json!({
1374 "at": at,
1375 "level": 10,
1376 "salt": base16ct::lower::encode_string(&salt),
1377 "fact": fact,
1378 "leaf": Sha256Digest::from_bytes(&leaf).as_str(),
1379 "chain": Sha256Digest::from_bytes(&node).as_str(),
1380 })
1381 .to_string(),
1382 );
1383 rewrite(&path, &lines);
1384
1385 let loaded = read_verified_transcript(&path).expect("read");
1386 assert_eq!(
1387 loaded.unknown,
1388 vec![UnknownFact {
1389 line: 3,
1390 level: Level::PUBLIC,
1391 type_name: "future_fact".to_string()
1392 }]
1393 );
1394 assert_eq!(loaded.facts.len(), 1);
1395 }
1396
1397 #[test]
1398 fn verify_empty_transcript_returns_empty_error() {
1399 let tmp = tempfile::tempdir().expect("tempdir");
1400 let path = tmp.path().join("transcript.jsonl");
1401 std::fs::write(&path, "").expect("write empty");
1402 let err = verify_transcript(&path).expect_err("empty");
1403 assert!(matches!(err, VerifyError::Empty));
1404 }
1405
1406 #[test]
1407 fn a_fact_before_the_header_is_refused() {
1408 let mut sink = InMemorySink::new();
1409 let err = sink
1410 .record(test_at(), Level::PUBLIC, &sample_fact())
1411 .expect_err("no header yet");
1412 assert!(err.to_string().contains("header not written"));
1413 }
1414
1415 #[test]
1416 fn a_second_header_is_refused() {
1417 let mut sink = crate::test_support::begun_sink();
1418 let err = sink
1419 .begin(&crate::test_support::test_header())
1420 .expect_err("second header");
1421 assert!(err.to_string().contains("already written"));
1422 }
1423
1424 #[test]
1425 fn the_reader_returns_the_header() {
1426 let tmp = tempfile::tempdir().expect("tempdir");
1427 let mut sink = begun_file_sink(tmp.path());
1428 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1429 .expect("record");
1430 let loaded = read_verified_transcript(sink.jsonl_path()).expect("read");
1431 assert_eq!(loaded.header, crate::test_support::test_header());
1432 assert_eq!(loaded.facts.len(), 1);
1433 }
1434
1435 fn transcript_with_header(dir: &Path, header: &TranscriptHeader) -> PathBuf {
1438 let value = serde_json::to_value(header).expect("header json");
1439 let node = header_node(&canonical_json(&value).expect("canonical"));
1440 let line = serde_json::json!({
1441 "header": value,
1442 "chain": Sha256Digest::from_bytes(&node).as_str(),
1443 })
1444 .to_string();
1445 let path = dir.join("transcript.jsonl");
1446 std::fs::write(&path, format!("{line}\n")).expect("write");
1447 path
1448 }
1449
1450 #[test]
1451 fn an_unknown_format_is_refused() {
1452 let tmp = tempfile::tempdir().expect("tempdir");
1453 let mut header = crate::test_support::test_header();
1454 header.rite_transcript = 2;
1455 let path = transcript_with_header(tmp.path(), &header);
1456 let err = verify_transcript(&path).expect_err("unknown format");
1457 assert!(matches!(err, VerifyError::UnknownFormat(2)));
1458 }
1459
1460 #[test]
1461 fn a_newer_vocabulary_is_read() {
1462 let tmp = tempfile::tempdir().expect("tempdir");
1463 let mut header = crate::test_support::test_header();
1464 header.vocabulary = 9;
1465 let path = transcript_with_header(tmp.path(), &header);
1466 let loaded = read_verified_transcript(&path).expect("newer vocabulary");
1467 assert_eq!(loaded.header.vocabulary, 9);
1468 }
1469
1470 #[test]
1471 fn a_moved_built_in_level_is_refused() {
1472 let tmp = tempfile::tempdir().expect("tempdir");
1473 let mut header = crate::test_support::test_header();
1474 header.levels.insert("public".to_string(), Level::new(5));
1475 let path = transcript_with_header(tmp.path(), &header);
1476 let err = verify_transcript(&path).expect_err("moved built-in");
1477 assert!(matches!(err, VerifyError::InvalidLevels(_)));
1478 }
1479
1480 #[test]
1481 fn a_line_at_an_undeclared_level_is_refused() {
1482 let tmp = tempfile::tempdir().expect("tempdir");
1483 let (path, mut lines) = three_fact_transcript(tmp.path());
1484 edit_line(&mut lines, 2, |v| {
1485 if let Some(obj) = v.as_object_mut() {
1486 obj.insert("level".to_string(), 15.into());
1487 }
1488 });
1489 rewrite(&path, &lines);
1490 let err = verify_transcript(&path).expect_err("undeclared level");
1491 assert!(
1492 matches!(err, VerifyError::InvalidLine { line: 3, .. }),
1493 "{err}"
1494 );
1495 }
1496
1497 #[test]
1498 fn the_sink_refuses_a_level_the_header_does_not_declare() {
1499 let tmp = tempfile::tempdir().expect("tempdir");
1500 let mut sink = begun_file_sink(tmp.path());
1501 let err = sink
1502 .record(test_at(), Level::new(15), &sample_fact())
1503 .expect_err("undeclared level");
1504 assert!(err.to_string().contains("not declared"), "{err}");
1505 sink.record(test_at(), Level::PUBLIC, &sample_fact())
1507 .expect("record");
1508 let loaded = read_verified_transcript(sink.jsonl_path()).expect("verify");
1509 assert_eq!(loaded.facts.len(), 1);
1510 }
1511
1512 #[test]
1513 fn a_declared_organisation_level_is_accepted() {
1514 let tmp = tempfile::tempdir().expect("tempdir");
1515 let mut header = crate::test_support::test_header();
1516 header.levels.insert("partners".to_string(), Level::new(15));
1517 let mut sink = JsonlFileSink::create(tmp.path()).expect("create");
1518 sink.begin(&header).expect("header");
1519 sink.record(test_at(), Level::new(15), &sample_fact())
1520 .expect("record");
1521 let loaded = read_verified_transcript(sink.jsonl_path()).expect("read");
1522 assert_eq!(loaded.facts.first().map(|t| t.level), Some(Level::new(15)));
1523 }
1524
1525 #[test]
1526 fn an_edited_header_is_caught() {
1527 let tmp = tempfile::tempdir().expect("tempdir");
1528 let path = transcript_with_header(tmp.path(), &crate::test_support::test_header());
1529 let text = std::fs::read_to_string(&path).expect("read");
1530 std::fs::write(&path, text.replace("\"dry_run\":false", "\"dry_run\":true"))
1531 .expect("write");
1532 let err = verify_transcript(&path).expect_err("edited header");
1533 assert!(matches!(err, VerifyError::ChainMismatch { line: 1, .. }));
1534 }
1535
1536 #[test]
1537 fn a_transcript_starting_with_a_fact_is_refused() {
1538 let tmp = tempfile::tempdir().expect("tempdir");
1539 let (path, lines) = three_fact_transcript(tmp.path());
1540 rewrite(&path, lines.get(1..).expect("facts"));
1541 let err = verify_transcript(&path).expect_err("no header");
1542 assert!(matches!(err, VerifyError::MissingHeader(_)));
1543 }
1544
1545 use rite_model::StepId;
1546
1547 fn seeded_fact(m: &[u8]) -> StepFact {
1548 StepFact::EntropySeeded {
1549 m: base16ct::lower::encode_string(m),
1550 source: "os".to_string(),
1551 derivation: crate::entropy::DERIVATION_V1.to_string(),
1552 }
1553 }
1554
1555 fn drawn_fact(seed: &[u8; 32], step: &str, path: &str, len: usize) -> StepFact {
1556 StepFact::EntropyDrawn {
1557 step: StepId::new(step),
1558 path: path.to_string(),
1559 value: base16ct::lower::encode_string(&crate::entropy::derive_value(seed, path, len)),
1560 }
1561 }
1562
1563 #[test]
1564 fn verify_entropy_re_derives_a_clean_draw() {
1565 let m = b"machine entropy bytes";
1566 let seed = crate::entropy::initial_seed(m);
1567 let facts = [
1568 seeded_fact(m),
1569 drawn_fact(&seed, "issue", "0/issue/cert-serial", 9),
1570 ];
1571 let v = verify_entropy(facts.iter()).expect("verify");
1572 assert_eq!(v.values_verified, 1);
1573 assert_eq!(v.contributions, 0);
1574 assert_eq!(v.derivation.as_deref(), Some("rite-kdf/v1"));
1575 }
1576
1577 #[test]
1578 fn verify_entropy_follows_the_ratchet_through_a_contribution() {
1579 let m = b"machine entropy bytes";
1580 let seed0 = crate::entropy::initial_seed(m);
1581 let seed1 = crate::entropy::fold_seed(&seed0, b"3 1 6 4 2 5");
1582 let facts = [
1583 seeded_fact(m),
1584 StepFact::EntropyContributed {
1585 step: StepId::new("roll"),
1586 epoch: 1,
1587 contribution: "3 1 6 4 2 5".to_string(),
1588 },
1589 drawn_fact(&seed1, "issue", "1/issue/cert-serial", 9),
1591 ];
1592 let v = verify_entropy(facts.iter()).expect("verify");
1593 assert_eq!(v.contributions, 1);
1594 assert_eq!(v.values_verified, 1);
1595 }
1596
1597 #[test]
1598 fn verify_entropy_detects_a_tampered_value() {
1599 let m = b"machine entropy bytes";
1600 let seed = crate::entropy::initial_seed(m);
1601 let mut drawn = drawn_fact(&seed, "issue", "0/issue/cert-serial", 9);
1602 if let StepFact::EntropyDrawn { value, .. } = &mut drawn {
1603 *value = "deadbeefdeadbeefdead".to_string();
1604 }
1605 let facts = [seeded_fact(m), drawn];
1606 let err = verify_entropy(facts.iter()).expect_err("tamper");
1607 assert!(matches!(err, VerifyError::DrawMismatch { .. }));
1608 }
1609
1610 #[test]
1611 fn verify_entropy_rejects_unknown_derivation() {
1612 let facts = [StepFact::EntropySeeded {
1613 m: "00".to_string(),
1614 source: "os".to_string(),
1615 derivation: "rite-kdf/v99".to_string(),
1616 }];
1617 let err = verify_entropy(facts.iter()).expect_err("unknown scheme");
1618 assert!(matches!(err, VerifyError::UnknownDerivation(_)));
1619 }
1620
1621 #[test]
1622 fn verify_entropy_rejects_draw_before_seed() {
1623 let seed = crate::entropy::initial_seed(b"x");
1624 let facts = [drawn_fact(&seed, "issue", "0/issue/cert-serial", 9)];
1625 let err = verify_entropy(facts.iter()).expect_err("unseeded");
1626 assert!(matches!(err, VerifyError::SeedMissing));
1627 }
1628
1629 #[test]
1630 fn verify_entropy_surfaces_the_seed_source() {
1631 let m = b"machine entropy bytes";
1632 let v = verify_entropy([seeded_fact(m)].iter()).expect("verify");
1633 assert_eq!(v.source.as_deref(), Some("os"));
1634 }
1635
1636 #[test]
1637 fn verify_entropy_rejects_a_second_seed() {
1638 let facts = [seeded_fact(b"first"), seeded_fact(b"second")];
1641 let err = verify_entropy(facts.iter()).expect_err("re-seed");
1642 assert!(matches!(err, VerifyError::DuplicateSeed));
1643 }
1644
1645 #[test]
1646 fn verify_entropy_rejects_an_oversized_draw_without_panicking() {
1647 let m = b"machine entropy bytes";
1651 let oversized = "ab".repeat(crate::entropy::MAX_DRAW_LEN.saturating_add(1));
1652 let facts = [
1653 seeded_fact(m),
1654 StepFact::EntropyDrawn {
1655 step: StepId::new("issue"),
1656 path: "0/issue/cert-serial".to_string(),
1657 value: oversized,
1658 },
1659 ];
1660 let err = verify_entropy(facts.iter()).expect_err("oversized draw");
1661 assert!(matches!(err, VerifyError::DrawTooLong { len, .. } if len == 8161));
1662 }
1663
1664 #[test]
1665 fn verify_entropy_rejects_a_draw_path_with_the_wrong_epoch() {
1666 let m = b"machine entropy bytes";
1669 let seed = crate::entropy::initial_seed(m);
1670 let facts = [
1671 seeded_fact(m),
1672 drawn_fact(&seed, "issue", "1/issue/cert-serial", 9),
1673 ];
1674 let err = verify_entropy(facts.iter()).expect_err("wrong epoch");
1675 assert!(matches!(err, VerifyError::PathMismatch { .. }));
1676 }
1677
1678 #[test]
1679 fn verify_entropy_rejects_a_draw_path_with_the_wrong_step() {
1680 let m = b"machine entropy bytes";
1681 let seed = crate::entropy::initial_seed(m);
1682 let facts = [
1683 seeded_fact(m),
1684 drawn_fact(&seed, "issue", "0/other/cert-serial", 9),
1686 ];
1687 let err = verify_entropy(facts.iter()).expect_err("wrong step");
1688 assert!(matches!(err, VerifyError::PathMismatch { .. }));
1689 }
1690
1691 #[test]
1692 fn verify_entropy_rejects_an_empty_purpose() {
1693 let m = b"machine entropy bytes";
1694 let seed = crate::entropy::initial_seed(m);
1695 let facts = [seeded_fact(m), drawn_fact(&seed, "issue", "0/issue/", 9)];
1696 let err = verify_entropy(facts.iter()).expect_err("empty purpose");
1697 assert!(matches!(err, VerifyError::PathMismatch { .. }));
1698 }
1699
1700 #[test]
1701 fn verify_entropy_rejects_a_contribution_epoch_out_of_sequence() {
1702 let m = b"machine entropy bytes";
1703 let facts = [
1704 seeded_fact(m),
1705 StepFact::EntropyContributed {
1706 step: StepId::new("roll"),
1707 epoch: 2,
1709 contribution: "3 1 6 4 2 5".to_string(),
1710 },
1711 ];
1712 let err = verify_entropy(facts.iter()).expect_err("epoch skip");
1713 assert!(matches!(
1714 err,
1715 VerifyError::EpochMismatch {
1716 recorded: 2,
1717 expected: 1,
1718 ..
1719 }
1720 ));
1721 }
1722
1723 #[test]
1724 fn verify_entropy_rejects_a_duplicate_draw_path() {
1725 let m = b"machine entropy bytes";
1728 let seed = crate::entropy::initial_seed(m);
1729 let facts = [
1730 seeded_fact(m),
1731 drawn_fact(&seed, "issue", "0/issue/cert-serial", 9),
1732 drawn_fact(&seed, "issue", "0/issue/cert-serial", 9),
1733 ];
1734 let err = verify_entropy(facts.iter()).expect_err("duplicate path");
1735 assert!(matches!(err, VerifyError::DuplicateDrawPath { .. }));
1736 }
1737}