1use crate::canonical::{canonical_line, hex_lower, is_lower_hex, parse_canonical_line, sha256_hex};
4use crate::model::RequestFileLocator;
5use crate::{FailureCode, PromotionError, PromotionRequest, PromotionResult};
6use rustix::fs::{CWD, Mode, OFlags, ResolveFlags, openat2};
7use rustix::process::geteuid;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest as _, Sha256};
10use std::collections::BTreeSet;
11use std::fs::{File, Metadata};
12use std::os::unix::fs::{FileExt as _, MetadataExt as _};
13use std::path::{Component, Path, PathBuf};
14
15const BURN_SCHEMA: u64 = 1;
16const BURN_CONTRACT: &str = "hashsigs-u280-phase-b-coordination-burn-v1";
17const GENESIS_SHA256: &str = "0000000000000000000000000000000000000000000000000000000000000000";
18const PROJECT_LEDGER: &str = "hashsigs-project-coordination";
19const ED25519_LEDGER: &str = "ed25519-agent-coordination";
20const MAXIMUM_HISTORY_BYTES: usize = 64 * 1024 * 1024;
21const MAXIMUM_RECORD_BYTES: usize = 64 * 1024;
22const MAXIMUM_MANIFEST_BYTES: u64 = 64 * 1024;
23pub(crate) const MAXIMUM_COORDINATION_INTENT_BYTES: u64 = 64 * 1024;
24
25#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
26#[serde(deny_unknown_fields)]
27struct CoordinationGrantWire {
28 ledger_id: String,
29 entry_id: String,
30 entry_bytes: u64,
31 entry_sha256: String,
32}
33
34impl CoordinationGrantWire {
35 fn validate(&self, expected_ledger: &str) -> PromotionResult<()> {
36 let canonical_id = !self.entry_id.is_empty()
37 && self.entry_id.len() <= 128
38 && self.entry_id.bytes().all(|byte| {
39 byte.is_ascii_lowercase()
40 || byte.is_ascii_digit()
41 || matches!(byte, b'.' | b'_' | b':' | b'-')
42 });
43 if self.ledger_id != expected_ledger
44 || !canonical_id
45 || self.entry_bytes == 0
46 || !is_lower_hex(&self.entry_sha256, 64, true)
47 {
48 return Err(PromotionError::new(
49 FailureCode::CoordinationChain,
50 "coordination grant binding is non-canonical or out of order",
51 ));
52 }
53 Ok(())
54 }
55}
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57#[serde(deny_unknown_fields)]
58struct IntentBinding {
59 bytes: u64,
60 sha256: String,
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64#[serde(deny_unknown_fields)]
65struct CoordinationBurnWire {
66 schema: u64,
67 contract: String,
68 sequence: u64,
69 run_id: String,
70 intent: IntentBinding,
71 grants: Vec<CoordinationGrantWire>,
72 previous_sha256: String,
73 record_sha256: String,
74}
75
76impl CoordinationBurnWire {
77 fn validate_shape(&self) -> PromotionResult<()> {
78 if self.schema != BURN_SCHEMA
79 || self.contract != BURN_CONTRACT
80 || !is_lower_hex(&self.run_id, 32, true)
81 || self.intent.bytes == 0
82 || self.intent.bytes > MAXIMUM_COORDINATION_INTENT_BYTES
83 || !is_lower_hex(&self.intent.sha256, 64, false)
84 || self.grants.len() != 2
85 || !is_lower_hex(&self.previous_sha256, 64, false)
86 || !is_lower_hex(&self.record_sha256, 64, false)
87 {
88 return Err(PromotionError::new(
89 FailureCode::CoordinationChain,
90 "coordination burn record has a non-canonical shape",
91 ));
92 }
93 self.grants[0].validate(PROJECT_LEDGER)?;
94 self.grants[1].validate(ED25519_LEDGER)?;
95 if self.grants[0].entry_id == self.grants[1].entry_id {
96 return Err(PromotionError::new(
97 FailureCode::CoordinationChain,
98 "the two coordination grant entry identifiers must differ",
99 ));
100 }
101 Ok(())
102 }
103
104 fn recomputed_record_sha256(&self) -> PromotionResult<String> {
105 let grants = self
106 .grants
107 .iter()
108 .map(|grant| {
109 CoordinationBurnGrantHashInput::new(
110 &grant.ledger_id,
111 &grant.entry_id,
112 grant.entry_bytes,
113 &grant.entry_sha256,
114 )
115 })
116 .collect::<Vec<_>>();
117 recompute_coordination_burn_record_sha256(
118 self.sequence,
119 &self.run_id,
120 self.intent.bytes,
121 &self.intent.sha256,
122 &grants,
123 &self.previous_sha256,
124 )
125 }
126}
127
128#[derive(Serialize)]
129struct CoordinationBurnHashBody<'a> {
130 schema: u64,
131 contract: &'static str,
132 sequence: u64,
133 run_id: &'a str,
134 intent: CoordinationBurnIntentHashInput<'a>,
135 grants: &'a [CoordinationBurnGrantHashInput],
136 previous_sha256: &'a str,
137}
138
139#[derive(Serialize)]
140struct CoordinationBurnIntentHashInput<'a> {
141 bytes: u64,
142 sha256: &'a str,
143}
144
145#[derive(Serialize)]
146pub(crate) struct CoordinationBurnGrantHashInput {
147 ledger_id: String,
148 entry_id: String,
149 entry_bytes: u64,
150 entry_sha256: String,
151}
152
153impl CoordinationBurnGrantHashInput {
154 pub(crate) fn new(
155 ledger_id: &str,
156 entry_id: &str,
157 entry_bytes: u64,
158 entry_sha256: &str,
159 ) -> Self {
160 Self {
161 ledger_id: ledger_id.to_owned(),
162 entry_id: entry_id.to_owned(),
163 entry_bytes,
164 entry_sha256: entry_sha256.to_owned(),
165 }
166 }
167}
168
169pub(crate) fn recompute_coordination_burn_record_sha256(
170 sequence: u64,
171 run_id: &str,
172 intent_bytes: u64,
173 intent_sha256: &str,
174 grants: &[CoordinationBurnGrantHashInput],
175 previous_sha256: &str,
176) -> PromotionResult<String> {
177 let body = CoordinationBurnHashBody {
178 schema: BURN_SCHEMA,
179 contract: BURN_CONTRACT,
180 sequence,
181 run_id,
182 intent: CoordinationBurnIntentHashInput {
183 bytes: intent_bytes,
184 sha256: intent_sha256,
185 },
186 grants,
187 previous_sha256,
188 };
189 let mut canonical = canonical_line(&body)?;
190 let newline = canonical.pop();
191 debug_assert_eq!(newline, Some(b'\n'));
192 Ok(sha256_hex(&canonical))
193}
194
195#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
197pub struct VerifiedCoordinationGrant {
198 ledger_id: String,
199 entry_id: String,
200 entry_bytes: u64,
201 entry_sha256: String,
202}
203
204impl VerifiedCoordinationGrant {
205 #[must_use]
207 pub fn ledger_id(&self) -> &str {
208 &self.ledger_id
209 }
210
211 #[must_use]
213 pub fn entry_id(&self) -> &str {
214 &self.entry_id
215 }
216
217 #[must_use]
219 pub const fn entry_bytes(&self) -> u64 {
220 self.entry_bytes
221 }
222
223 #[must_use]
225 pub fn entry_sha256(&self) -> &str {
226 &self.entry_sha256
227 }
228}
229
230#[derive(Clone, Debug, Eq, PartialEq)]
233pub struct VerifiedCoordinationBurn {
234 sequence: u64,
235 run_id: String,
236 intent_bytes: u64,
237 intent_sha256: String,
238 grants: Vec<VerifiedCoordinationGrant>,
239 previous_sha256: String,
240 record_sha256: String,
241}
242
243impl VerifiedCoordinationBurn {
244 #[must_use]
246 pub const fn sequence(&self) -> u64 {
247 self.sequence
248 }
249
250 #[must_use]
252 pub fn run_id(&self) -> &str {
253 &self.run_id
254 }
255
256 #[must_use]
258 pub const fn intent_bytes(&self) -> u64 {
259 self.intent_bytes
260 }
261
262 #[must_use]
264 pub fn intent_sha256(&self) -> &str {
265 &self.intent_sha256
266 }
267
268 #[must_use]
270 pub fn grants(&self) -> &[VerifiedCoordinationGrant] {
271 &self.grants
272 }
273
274 #[must_use]
276 pub fn previous_sha256(&self) -> &str {
277 &self.previous_sha256
278 }
279
280 #[must_use]
282 pub fn record_sha256(&self) -> &str {
283 &self.record_sha256
284 }
285}
286
287impl From<CoordinationBurnWire> for VerifiedCoordinationBurn {
288 fn from(wire: CoordinationBurnWire) -> Self {
289 Self {
290 sequence: wire.sequence,
291 run_id: wire.run_id,
292 intent_bytes: wire.intent.bytes,
293 intent_sha256: wire.intent.sha256,
294 grants: wire
295 .grants
296 .into_iter()
297 .map(|grant| VerifiedCoordinationGrant {
298 ledger_id: grant.ledger_id,
299 entry_id: grant.entry_id,
300 entry_bytes: grant.entry_bytes,
301 entry_sha256: grant.entry_sha256,
302 })
303 .collect(),
304 previous_sha256: wire.previous_sha256,
305 record_sha256: wire.record_sha256,
306 }
307 }
308}
309
310#[derive(Clone, Debug, Eq, PartialEq)]
312pub struct CoordinationChain {
313 records: u64,
314 selected_burn: VerifiedCoordinationBurn,
315 run_ids: BTreeSet<String>,
316 intent_sha256s: BTreeSet<String>,
317 grant_replay_keys: BTreeSet<(String, String, String)>,
318 exact_grant_bindings: BTreeSet<(String, String, u64, String)>,
319}
320
321impl CoordinationChain {
322 #[must_use]
324 pub const fn records(&self) -> u64 {
325 self.records
326 }
327
328 #[must_use]
330 pub const fn selected_burn(&self) -> &VerifiedCoordinationBurn {
331 &self.selected_burn
332 }
333
334 #[must_use]
336 pub fn contains_run_id(&self, run_id: &str) -> bool {
337 self.run_ids.contains(run_id)
338 }
339
340 #[must_use]
342 pub fn contains_intent_sha256(&self, sha256: &str) -> bool {
343 self.intent_sha256s.contains(sha256)
344 }
345
346 #[must_use]
348 pub fn contains_grant(&self, ledger_id: &str, entry_id: &str, entry_sha256: &str) -> bool {
349 self.grant_replay_keys.contains(&(
350 ledger_id.to_owned(),
351 entry_id.to_owned(),
352 entry_sha256.to_owned(),
353 ))
354 }
355
356 #[must_use]
358 pub fn contains_exact_grant(
359 &self,
360 ledger_id: &str,
361 entry_id: &str,
362 entry_bytes: u64,
363 entry_sha256: &str,
364 ) -> bool {
365 self.exact_grant_bindings.contains(&(
366 ledger_id.to_owned(),
367 entry_id.to_owned(),
368 entry_bytes,
369 entry_sha256.to_owned(),
370 ))
371 }
372}
373
374struct CoordinationChainBuilder {
375 expected_sequence: u64,
376 previous: String,
377 run_ids: BTreeSet<String>,
378 intents: BTreeSet<String>,
379 grant_replay_keys: BTreeSet<(String, String, String)>,
380 exact_grant_bindings: BTreeSet<(String, String, u64, String)>,
381 last_record: Option<CoordinationBurnWire>,
382 last_line: Vec<u8>,
383}
384
385impl CoordinationChainBuilder {
386 fn new() -> Self {
387 Self {
388 expected_sequence: 0,
389 previous: GENESIS_SHA256.to_owned(),
390 run_ids: BTreeSet::new(),
391 intents: BTreeSet::new(),
392 grant_replay_keys: BTreeSet::new(),
393 exact_grant_bindings: BTreeSet::new(),
394 last_record: None,
395 last_line: Vec::new(),
396 }
397 }
398
399 fn push(&mut self, line: &[u8]) -> PromotionResult<()> {
400 let record: CoordinationBurnWire =
401 parse_canonical_line(line, MAXIMUM_RECORD_BYTES, "coordination burn record").map_err(
402 |error| PromotionError::new(FailureCode::CoordinationChain, error.to_string()),
403 )?;
404 record.validate_shape()?;
405 if record.sequence != self.expected_sequence
406 || record.previous_sha256 != self.previous
407 || record.recomputed_record_sha256()? != record.record_sha256
408 {
409 return Err(PromotionError::new(
410 FailureCode::CoordinationChain,
411 "coordination history is not one contiguous recomputed genesis chain",
412 ));
413 }
414 if !self.run_ids.insert(record.run_id.clone())
415 || !self.intents.insert(record.intent.sha256.clone())
416 {
417 return Err(PromotionError::new(
418 FailureCode::CoordinationReplay,
419 "coordination history repeats a run ID or intent digest",
420 ));
421 }
422 for grant in &record.grants {
423 if !self.grant_replay_keys.insert((
424 grant.ledger_id.clone(),
425 grant.entry_id.clone(),
426 grant.entry_sha256.clone(),
427 )) {
428 return Err(PromotionError::new(
429 FailureCode::CoordinationReplay,
430 "coordination history repeats a ledger grant binding",
431 ));
432 }
433 let inserted = self.exact_grant_bindings.insert((
434 grant.ledger_id.clone(),
435 grant.entry_id.clone(),
436 grant.entry_bytes,
437 grant.entry_sha256.clone(),
438 ));
439 debug_assert!(inserted);
440 }
441 self.expected_sequence = self.expected_sequence.checked_add(1).ok_or_else(|| {
442 PromotionError::new(
443 FailureCode::CoordinationChain,
444 "coordination history sequence overflow",
445 )
446 })?;
447 self.previous.clone_from(&record.record_sha256);
448 self.last_line.clear();
449 self.last_line.extend_from_slice(line);
450 self.last_record = Some(record);
451 Ok(())
452 }
453
454 fn finish(
455 self,
456 expected_run_id: &str,
457 selected_wire: CoordinationBurnWire,
458 selected_receipt: &[u8],
459 ) -> PromotionResult<CoordinationChain> {
460 let Some(last_record) = self.last_record else {
461 return Err(PromotionError::new(
462 FailureCode::CoordinationChain,
463 "coordination history contains no canonical burn record",
464 ));
465 };
466 if last_record != selected_wire
467 || last_record.run_id.as_str() != expected_run_id
468 || self.last_line != selected_receipt
469 || canonical_line(&last_record)? != selected_receipt
470 {
471 return Err(PromotionError::new(
472 FailureCode::CoordinationChain,
473 "selected burn receipt is not the exact last record for the requested run",
474 ));
475 }
476 Ok(CoordinationChain {
477 records: self.expected_sequence,
478 selected_burn: last_record.into(),
479 run_ids: self.run_ids,
480 intent_sha256s: self.intents,
481 grant_replay_keys: self.grant_replay_keys,
482 exact_grant_bindings: self.exact_grant_bindings,
483 })
484 }
485}
486
487fn parse_selected_receipt(bytes: &[u8]) -> PromotionResult<CoordinationBurnWire> {
488 let selected: CoordinationBurnWire = parse_canonical_line(
489 bytes,
490 MAXIMUM_RECORD_BYTES,
491 "selected coordination burn receipt",
492 )
493 .map_err(|error| PromotionError::new(FailureCode::CoordinationChain, error.to_string()))?;
494 selected.validate_shape()?;
495 Ok(selected)
496}
497
498#[derive(Debug, Eq, PartialEq)]
503struct CoordinationFileBinding {
504 absolute_path: PathBuf,
505 bytes: u64,
506 sha256: String,
507}
508
509impl CoordinationFileBinding {
510 fn from_request_locator(locator: &RequestFileLocator<'_>) -> Self {
511 Self {
512 absolute_path: PathBuf::from(locator.absolute_path()),
513 bytes: locator.bytes(),
514 sha256: locator.sha256().to_owned(),
515 }
516 }
517}
518
519#[derive(Debug, Eq, PartialEq)]
525struct CoordinationSnapshotBinding {
526 root: PathBuf,
527 history: CoordinationFileBinding,
528 manifest: CoordinationFileBinding,
529 selected_receipt: CoordinationFileBinding,
530}
531
532impl CoordinationSnapshotBinding {
533 fn from_request(request: &PromotionRequest) -> PromotionResult<Self> {
534 let locators = request.coordination_locators();
535 Self::from_direct_children(
536 CoordinationFileBinding::from_request_locator(
537 locators.complete_burn_history_snapshot(),
538 ),
539 CoordinationFileBinding::from_request_locator(locators.history_snapshot_manifest()),
540 CoordinationFileBinding::from_request_locator(locators.selected_burn_receipt()),
541 )
542 }
543
544 fn from_direct_children(
545 history: CoordinationFileBinding,
546 manifest: CoordinationFileBinding,
547 selected_receipt: CoordinationFileBinding,
548 ) -> PromotionResult<Self> {
549 let root = history
550 .absolute_path
551 .parent()
552 .ok_or_else(|| {
553 coordination_error("coordination history has no absolute parent directory")
554 })?
555 .to_owned();
556 if manifest.absolute_path.parent() != Some(root.as_path())
557 || selected_receipt.absolute_path.parent() != Some(root.as_path())
558 {
559 return Err(coordination_error(
560 "coordination snapshot inputs do not share one direct parent",
561 ));
562 }
563 Ok(Self {
564 root,
565 history,
566 manifest,
567 selected_receipt,
568 })
569 }
570}
571
572#[derive(Clone, Debug, Eq, PartialEq)]
573struct SnapshotFingerprint {
574 dev: u64,
575 ino: u64,
576 mode: u32,
577 uid: u32,
578 nlink: u64,
579 bytes: u64,
580 blocks: u64,
581 mtime: i64,
582 mtime_nsec: i64,
583 ctime: i64,
584 ctime_nsec: i64,
585}
586
587impl SnapshotFingerprint {
588 fn from_metadata(metadata: &Metadata) -> Self {
589 Self {
590 dev: metadata.dev(),
591 ino: metadata.ino(),
592 mode: metadata.mode(),
593 uid: metadata.uid(),
594 nlink: metadata.nlink(),
595 bytes: metadata.size(),
596 blocks: metadata.blocks(),
597 mtime: metadata.mtime(),
598 mtime_nsec: metadata.mtime_nsec(),
599 ctime: metadata.ctime(),
600 ctime_nsec: metadata.ctime_nsec(),
601 }
602 }
603
604 fn same_object(&self, other: &Self) -> bool {
605 self.dev == other.dev && self.ino == other.ino
606 }
607}
608
609#[derive(Debug)]
610struct RetainedCoordinationFile {
611 relative: String,
612 file: File,
613 fingerprint: SnapshotFingerprint,
614 binding: CoordinationFileBinding,
615}
616
617#[derive(Debug)]
622pub(crate) struct VerifiedCoordinationSnapshot {
623 expected_run_id: String,
624 root_path: PathBuf,
625 root: File,
626 root_fingerprint: SnapshotFingerprint,
627 history: RetainedCoordinationFile,
628 manifest: RetainedCoordinationFile,
629 selected_receipt: RetainedCoordinationFile,
630 chain: CoordinationChain,
631}
632
633#[allow(dead_code)] impl VerifiedCoordinationSnapshot {
635 #[must_use]
636 pub(crate) const fn chain(&self) -> &CoordinationChain {
637 &self.chain
638 }
639
640 #[must_use]
641 pub(crate) const fn selected_burn(&self) -> &VerifiedCoordinationBurn {
642 self.chain.selected_burn()
643 }
644
645 #[must_use]
646 pub(crate) fn root(&self) -> &Path {
647 &self.root_path
648 }
649
650 #[must_use]
651 pub(crate) fn history_binding(&self) -> (u64, &str) {
652 (self.history.binding.bytes, &self.history.binding.sha256)
653 }
654
655 #[must_use]
656 pub(crate) fn manifest_binding(&self) -> (u64, &str) {
657 (self.manifest.binding.bytes, &self.manifest.binding.sha256)
658 }
659
660 #[must_use]
661 pub(crate) fn selected_receipt_binding(&self) -> (u64, &str) {
662 (
663 self.selected_receipt.binding.bytes,
664 &self.selected_receipt.binding.sha256,
665 )
666 }
667
668 pub(crate) fn revalidate(&self) -> PromotionResult<()> {
671 revalidate_root_path(&self.root_path, &self.root, &self.root_fingerprint)?;
672 revalidate_retained_file(&self.root, &self.history)?;
673 revalidate_retained_file(&self.root, &self.manifest)?;
674 revalidate_retained_file(&self.root, &self.selected_receipt)?;
675
676 let manifest = read_small_retained(&self.manifest, MAXIMUM_MANIFEST_BYTES)?;
677 let selected = read_small_retained(
678 &self.selected_receipt,
679 u64::try_from(MAXIMUM_RECORD_BYTES).expect("record bound fits u64"),
680 )?;
681 verify_history_manifest(&manifest, &self.history)?;
682 let reparsed = verify_streaming_history(&self.history, &selected, &self.expected_run_id)?;
683 if reparsed != self.chain {
684 return Err(coordination_error(
685 "coordination snapshot proof changed during final revalidation",
686 ));
687 }
688
689 revalidate_retained_file(&self.root, &self.history)?;
690 revalidate_retained_file(&self.root, &self.manifest)?;
691 revalidate_retained_file(&self.root, &self.selected_receipt)?;
692 revalidate_root_path(&self.root_path, &self.root, &self.root_fingerprint)
693 }
694}
695
696#[allow(dead_code)] pub(crate) fn retain_verified_coordination_snapshot(
706 request: &PromotionRequest,
707) -> PromotionResult<VerifiedCoordinationSnapshot> {
708 let expected_run_id = request.run_id();
709 let binding = CoordinationSnapshotBinding::from_request(request)?;
710 if !is_lower_hex(expected_run_id, 32, true) {
711 return Err(coordination_error(
712 "coordination snapshot expected run ID is non-canonical",
713 ));
714 }
715 validate_absolute_path(&binding.root, "coordination snapshot root")?;
716 validate_file_binding(
717 &binding.root,
718 &binding.history,
719 u64::try_from(MAXIMUM_HISTORY_BYTES).expect("history bound fits u64"),
720 "coordination history",
721 )?;
722 validate_file_binding(
723 &binding.root,
724 &binding.manifest,
725 MAXIMUM_MANIFEST_BYTES,
726 "coordination history manifest",
727 )?;
728 validate_file_binding(
729 &binding.root,
730 &binding.selected_receipt,
731 u64::try_from(MAXIMUM_RECORD_BYTES).expect("record bound fits u64"),
732 "selected coordination receipt",
733 )?;
734 if binding.history.absolute_path == binding.manifest.absolute_path
735 || binding.history.absolute_path == binding.selected_receipt.absolute_path
736 || binding.manifest.absolute_path == binding.selected_receipt.absolute_path
737 {
738 return Err(coordination_error(
739 "coordination snapshot inputs must have three distinct pathnames",
740 ));
741 }
742
743 let (root, root_fingerprint) = open_immutable_root(&binding.root)?;
744 revalidate_root_path(&binding.root, &root, &root_fingerprint)?;
745 let history = open_retained_file(&root, binding.history, "coordination history")?;
746 let manifest = open_retained_file(&root, binding.manifest, "coordination history manifest")?;
747 let selected_receipt = open_retained_file(
748 &root,
749 binding.selected_receipt,
750 "selected coordination receipt",
751 )?;
752 if history.fingerprint.same_object(&manifest.fingerprint)
753 || history
754 .fingerprint
755 .same_object(&selected_receipt.fingerprint)
756 || manifest
757 .fingerprint
758 .same_object(&selected_receipt.fingerprint)
759 {
760 return Err(coordination_error(
761 "coordination snapshot inputs alias one retained file object",
762 ));
763 }
764
765 let manifest_bytes = read_small_retained(&manifest, MAXIMUM_MANIFEST_BYTES)?;
766 let selected_bytes = read_small_retained(
767 &selected_receipt,
768 u64::try_from(MAXIMUM_RECORD_BYTES).expect("record bound fits u64"),
769 )?;
770 verify_history_manifest(&manifest_bytes, &history)?;
771 let chain = verify_streaming_history(&history, &selected_bytes, expected_run_id)?;
772 revalidate_root_path(&binding.root, &root, &root_fingerprint)?;
773
774 let snapshot = VerifiedCoordinationSnapshot {
775 expected_run_id: expected_run_id.to_owned(),
776 root_path: binding.root,
777 root,
778 root_fingerprint,
779 history,
780 manifest,
781 selected_receipt,
782 chain,
783 };
784 snapshot.revalidate()?;
785 Ok(snapshot)
786}
787
788fn validate_file_binding(
789 root: &Path,
790 binding: &CoordinationFileBinding,
791 maximum_bytes: u64,
792 label: &str,
793) -> PromotionResult<()> {
794 validate_absolute_path(&binding.absolute_path, label)?;
795 let _ = direct_child_name(root, &binding.absolute_path, label)?;
796 if binding.bytes == 0
797 || binding.bytes > maximum_bytes
798 || !is_lower_hex(&binding.sha256, 64, false)
799 {
800 return Err(coordination_error(format!(
801 "{label} binding is empty, oversized, or has a non-canonical SHA-256"
802 )));
803 }
804 Ok(())
805}
806
807fn validate_absolute_path(path: &Path, label: &str) -> PromotionResult<()> {
808 let Some(text) = path.to_str() else {
809 return Err(coordination_error(format!("{label} path is not UTF-8")));
810 };
811 let canonical = text.starts_with('/')
812 && text.len() > 1
813 && !text.ends_with('/')
814 && !text.contains("//")
815 && !text.contains('\0')
816 && path
817 .components()
818 .all(|component| matches!(component, Component::RootDir | Component::Normal(_)));
819 if !canonical {
820 return Err(coordination_error(format!(
821 "{label} is not a traversal-free canonical absolute path"
822 )));
823 }
824 Ok(())
825}
826
827fn direct_child_name(root: &Path, path: &Path, label: &str) -> PromotionResult<String> {
828 let relative = path
829 .strip_prefix(root)
830 .map_err(|_| coordination_error(format!("{label} is not beneath the snapshot root")))?;
831 let mut components = relative.components();
832 let Some(Component::Normal(name)) = components.next() else {
833 return Err(coordination_error(format!(
834 "{label} is not a direct snapshot child"
835 )));
836 };
837 if components.next().is_some() {
838 return Err(coordination_error(format!(
839 "{label} is not a direct snapshot child"
840 )));
841 }
842 let name = name
843 .to_str()
844 .ok_or_else(|| coordination_error(format!("{label} basename is not UTF-8")))?;
845 if name.is_empty()
846 || name.contains('/')
847 || name.contains('\0')
848 || name.contains('\n')
849 || name.contains('\r')
850 {
851 return Err(coordination_error(format!(
852 "{label} basename is non-canonical"
853 )));
854 }
855 Ok(name.to_owned())
856}
857
858fn open_immutable_root(path: &Path) -> PromotionResult<(File, SnapshotFingerprint)> {
859 let descriptor = openat2(
860 CWD,
861 path,
862 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
863 Mode::empty(),
864 ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
865 )
866 .map_err(|error| coordination_error(format!("cannot securely open snapshot root: {error}")))?;
867 let root = File::from(descriptor);
868 let metadata = root
869 .metadata()
870 .map_err(|error| coordination_error(format!("cannot stat snapshot root: {error}")))?;
871 if !metadata.file_type().is_dir()
872 || metadata.uid() != geteuid().as_raw()
873 || metadata.mode() & 0o222 != 0
874 {
875 return Err(coordination_error(
876 "coordination snapshot root must be owner-retained and non-writable",
877 ));
878 }
879 Ok((root, SnapshotFingerprint::from_metadata(&metadata)))
880}
881
882fn open_retained_file(
883 root: &File,
884 binding: CoordinationFileBinding,
885 label: &str,
886) -> PromotionResult<RetainedCoordinationFile> {
887 let root_path = binding
888 .absolute_path
889 .parent()
890 .ok_or_else(|| coordination_error(format!("{label} has no snapshot parent")))?;
891 let relative = direct_child_name(root_path, &binding.absolute_path, label)?;
892 let descriptor = openat2(
893 root,
894 relative.as_str(),
895 OFlags::RDONLY | OFlags::NONBLOCK | OFlags::CLOEXEC | OFlags::NOFOLLOW,
896 Mode::empty(),
897 ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
898 )
899 .map_err(|error| coordination_error(format!("cannot securely open {label}: {error}")))?;
900 let file = File::from(descriptor);
901 let fingerprint = immutable_file_fingerprint(&file, label)?;
902 if fingerprint.bytes != binding.bytes {
903 return Err(coordination_error(format!(
904 "{label} descriptor length differs from its request binding"
905 )));
906 }
907 Ok(RetainedCoordinationFile {
908 relative,
909 file,
910 fingerprint,
911 binding,
912 })
913}
914
915fn immutable_file_fingerprint(file: &File, label: &str) -> PromotionResult<SnapshotFingerprint> {
916 let metadata = file
917 .metadata()
918 .map_err(|error| coordination_error(format!("cannot stat {label}: {error}")))?;
919 let allocated = metadata.blocks().saturating_mul(512);
920 if !metadata.file_type().is_file()
921 || metadata.nlink() != 1
922 || metadata.uid() != geteuid().as_raw()
923 || metadata.mode() & 0o222 != 0
924 || metadata.size() == 0
925 || allocated < metadata.size()
926 {
927 return Err(coordination_error(format!(
928 "{label} must be a nonempty, non-sparse, unaliased, owner-retained, non-writable regular file"
929 )));
930 }
931 Ok(SnapshotFingerprint::from_metadata(&metadata))
932}
933
934fn revalidate_root_path(
935 root_path: &Path,
936 root: &File,
937 expected: &SnapshotFingerprint,
938) -> PromotionResult<()> {
939 let descriptor_metadata = root
940 .metadata()
941 .map_err(|error| coordination_error(format!("cannot restat snapshot root: {error}")))?;
942 let descriptor_fingerprint = SnapshotFingerprint::from_metadata(&descriptor_metadata);
943 if descriptor_fingerprint != *expected {
944 return Err(coordination_error(
945 "coordination snapshot root descriptor changed",
946 ));
947 }
948 let (path_root, path_fingerprint) = open_immutable_root(root_path)?;
949 drop(path_root);
950 if path_fingerprint != *expected {
951 return Err(coordination_error(
952 "coordination snapshot root pathname changed",
953 ));
954 }
955 Ok(())
956}
957
958fn revalidate_retained_file(
959 root: &File,
960 retained: &RetainedCoordinationFile,
961) -> PromotionResult<()> {
962 let descriptor_fingerprint = immutable_file_fingerprint(&retained.file, &retained.relative)?;
963 if descriptor_fingerprint != retained.fingerprint {
964 return Err(coordination_error(format!(
965 "retained coordination file descriptor changed: {}",
966 retained.relative
967 )));
968 }
969 let descriptor = openat2(
970 root,
971 retained.relative.as_str(),
972 OFlags::RDONLY | OFlags::NONBLOCK | OFlags::CLOEXEC | OFlags::NOFOLLOW,
973 Mode::empty(),
974 ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
975 )
976 .map_err(|error| {
977 coordination_error(format!(
978 "cannot reopen retained coordination path {}: {error}",
979 retained.relative
980 ))
981 })?;
982 let path_file = File::from(descriptor);
983 let path_fingerprint = immutable_file_fingerprint(&path_file, &retained.relative)?;
984 if path_fingerprint != retained.fingerprint {
985 return Err(coordination_error(format!(
986 "coordination pathname no longer identifies its retained file: {}",
987 retained.relative
988 )));
989 }
990 Ok(())
991}
992
993fn read_small_retained(
994 retained: &RetainedCoordinationFile,
995 maximum_bytes: u64,
996) -> PromotionResult<Vec<u8>> {
997 if retained.fingerprint.bytes > maximum_bytes {
998 return Err(coordination_error(format!(
999 "retained coordination file exceeds its strict bound: {}",
1000 retained.relative
1001 )));
1002 }
1003 let capacity = usize::try_from(retained.fingerprint.bytes).map_err(|_| {
1004 coordination_error(format!(
1005 "retained coordination file is too large for this host: {}",
1006 retained.relative
1007 ))
1008 })?;
1009 let mut bytes = vec![0_u8; capacity];
1010 retained
1011 .file
1012 .read_exact_at(&mut bytes, 0)
1013 .map_err(|error| {
1014 coordination_error(format!(
1015 "cannot read retained coordination file {}: {error}",
1016 retained.relative
1017 ))
1018 })?;
1019 let mut extra = [0_u8; 1];
1020 if retained
1021 .file
1022 .read_at(&mut extra, retained.fingerprint.bytes)
1023 .map_err(|error| {
1024 coordination_error(format!(
1025 "cannot check retained coordination EOF {}: {error}",
1026 retained.relative
1027 ))
1028 })?
1029 != 0
1030 || immutable_file_fingerprint(&retained.file, &retained.relative)? != retained.fingerprint
1031 || sha256_hex(&bytes) != retained.binding.sha256
1032 {
1033 return Err(coordination_error(format!(
1034 "retained coordination file changed or differs from its binding: {}",
1035 retained.relative
1036 )));
1037 }
1038 Ok(bytes)
1039}
1040
1041fn verify_history_manifest(
1042 bytes: &[u8],
1043 history: &RetainedCoordinationFile,
1044) -> PromotionResult<()> {
1045 if bytes.is_empty() || bytes.last() != Some(&b'\n') || !bytes.is_ascii() {
1046 return Err(coordination_error(
1047 "coordination history manifest is empty, truncated, or non-ASCII",
1048 ));
1049 }
1050 let body = &bytes[..bytes.len() - 1];
1051 if body.contains(&b'\n')
1052 || body.len() != 66_usize.saturating_add(history.relative.len())
1053 || body.get(64..66) != Some(&b" "[..])
1054 || body.get(..64) != Some(history.binding.sha256.as_bytes())
1055 || body.get(66..) != Some(history.relative.as_bytes())
1056 {
1057 return Err(coordination_error(
1058 "coordination history manifest does not contain the exact sole history binding",
1059 ));
1060 }
1061 Ok(())
1062}
1063
1064fn verify_streaming_history(
1065 history: &RetainedCoordinationFile,
1066 selected_receipt: &[u8],
1067 expected_run_id: &str,
1068) -> PromotionResult<CoordinationChain> {
1069 let selected_wire = parse_selected_receipt(selected_receipt)?;
1070 let mut builder = CoordinationChainBuilder::new();
1071 let mut hasher = Sha256::new();
1072 let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();
1073 let mut line = Vec::with_capacity(MAXIMUM_RECORD_BYTES.min(4096));
1074 let mut offset = 0_u64;
1075 while offset < history.fingerprint.bytes {
1076 let remaining = history.fingerprint.bytes - offset;
1077 let requested = usize::try_from(
1078 remaining.min(u64::try_from(buffer.len()).expect("buffer length fits u64")),
1079 )
1080 .expect("bounded history chunk fits usize");
1081 let amount = history
1082 .file
1083 .read_at(&mut buffer[..requested], offset)
1084 .map_err(|error| {
1085 coordination_error(format!("cannot stream coordination history: {error}"))
1086 })?;
1087 if amount == 0 {
1088 return Err(coordination_error(
1089 "coordination history was truncated during streaming",
1090 ));
1091 }
1092 hasher.update(&buffer[..amount]);
1093 for byte in &buffer[..amount] {
1094 line.push(*byte);
1095 if line.len() > MAXIMUM_RECORD_BYTES {
1096 return Err(coordination_error(
1097 "coordination history record exceeds its strict bound",
1098 ));
1099 }
1100 if *byte == b'\n' {
1101 builder.push(&line)?;
1102 line.clear();
1103 }
1104 }
1105 offset = offset
1106 .checked_add(u64::try_from(amount).expect("usize fits u64"))
1107 .ok_or_else(|| coordination_error("coordination history byte-count overflow"))?;
1108 }
1109 let mut extra = [0_u8; 1];
1110 if !line.is_empty()
1111 || history
1112 .file
1113 .read_at(&mut extra, history.fingerprint.bytes)
1114 .map_err(|error| {
1115 coordination_error(format!("cannot check coordination history EOF: {error}"))
1116 })?
1117 != 0
1118 || immutable_file_fingerprint(&history.file, &history.relative)? != history.fingerprint
1119 || offset != history.binding.bytes
1120 || hex_lower(&hasher.finalize()) != history.binding.sha256
1121 {
1122 return Err(coordination_error(
1123 "coordination history is truncated, changed, or differs from its exact binding",
1124 ));
1125 }
1126 builder.finish(expected_run_id, selected_wire, selected_receipt)
1127}
1128
1129fn coordination_error(message: impl Into<String>) -> PromotionError {
1130 PromotionError::new(FailureCode::CoordinationChain, message)
1131}
1132
1133pub fn verify_complete_burn_history(
1145 request: &PromotionRequest,
1146 bytes: &[u8],
1147 selected_receipt: &[u8],
1148) -> PromotionResult<CoordinationChain> {
1149 let (expected_bytes, expected_sha256) = request.coordination_history_binding();
1150 if bytes.is_empty()
1151 || bytes.len() > MAXIMUM_HISTORY_BYTES
1152 || u64::try_from(bytes.len()).ok() != Some(expected_bytes)
1153 || !is_lower_hex(expected_sha256, 64, false)
1154 || sha256_hex(bytes) != expected_sha256
1155 || bytes.last() != Some(&b'\n')
1156 {
1157 return Err(PromotionError::new(
1158 FailureCode::CoordinationChain,
1159 "coordination history is empty, truncated, oversized, or differs from its manifest binding",
1160 ));
1161 }
1162 let (selected_receipt_expected_bytes, selected_receipt_expected_sha256) =
1163 request.selected_burn_binding();
1164 if selected_receipt.is_empty()
1165 || u64::try_from(selected_receipt.len()).ok() != Some(selected_receipt_expected_bytes)
1166 || !is_lower_hex(selected_receipt_expected_sha256, 64, false)
1167 || sha256_hex(selected_receipt) != selected_receipt_expected_sha256
1168 {
1169 return Err(PromotionError::new(
1170 FailureCode::CoordinationChain,
1171 "selected coordination receipt or run binding is non-canonical or differs from its manifest binding",
1172 ));
1173 }
1174 let selected_wire = parse_selected_receipt(selected_receipt)?;
1175 let mut builder = CoordinationChainBuilder::new();
1176 for line in bytes.split_inclusive(|byte| *byte == b'\n') {
1177 builder.push(line)?;
1178 }
1179 builder.finish(request.run_id(), selected_wire, selected_receipt)
1180}
1181
1182#[cfg(test)]
1183fn synthetic_burn_record(sequence: u64, previous_sha256: &str, salt: u8) -> CoordinationBurnWire {
1184 let mut wire = CoordinationBurnWire {
1185 schema: BURN_SCHEMA,
1186 contract: BURN_CONTRACT.to_owned(),
1187 sequence,
1188 run_id: format!("{salt:02x}{}", "1".repeat(30)),
1189 intent: IntentBinding {
1190 bytes: 100,
1191 sha256: format!("{salt:02x}{}", "2".repeat(62)),
1192 },
1193 grants: vec![
1194 CoordinationGrantWire {
1195 ledger_id: PROJECT_LEDGER.to_owned(),
1196 entry_id: format!("project-{salt}"),
1197 entry_bytes: 10,
1198 entry_sha256: format!("{salt:02x}{}", "3".repeat(62)),
1199 },
1200 CoordinationGrantWire {
1201 ledger_id: ED25519_LEDGER.to_owned(),
1202 entry_id: format!("ed25519-{salt}"),
1203 entry_bytes: 11,
1204 entry_sha256: format!("{salt:02x}{}", "4".repeat(62)),
1205 },
1206 ],
1207 previous_sha256: previous_sha256.to_owned(),
1208 record_sha256: String::new(),
1209 };
1210 wire.record_sha256 = wire.recomputed_record_sha256().unwrap();
1211 wire
1212}
1213
1214#[cfg(test)]
1215pub(crate) fn synthetic_chain(salt: u8) -> CoordinationChain {
1216 let wire = synthetic_burn_record(0, GENESIS_SHA256, salt);
1217 let receipt = canonical_line(&wire).unwrap();
1218 let request =
1219 crate::model::synthetic_request_with_coordination(&wire.run_id, &receipt, &receipt);
1220 verify_complete_burn_history(&request, &receipt, &receipt).unwrap()
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225 use super::{
1226 CoordinationBurnWire, CoordinationSnapshotBinding, GENESIS_SHA256, synthetic_burn_record,
1227 verify_complete_burn_history,
1228 };
1229 use crate::FailureCode;
1230 use crate::canonical::{canonical_line, sha256_hex};
1231 use crate::model::{
1232 synthetic_request_with_coordination, synthetic_request_with_coordination_snapshot,
1233 };
1234 use std::fs;
1235 use std::os::unix::fs::PermissionsExt;
1236 use std::path::{Path, PathBuf};
1237 use std::sync::atomic::{AtomicU64, Ordering};
1238
1239 static NEXT_SNAPSHOT_ROOT: AtomicU64 = AtomicU64::new(0);
1240
1241 struct TestSnapshotRoot {
1242 path: PathBuf,
1243 }
1244
1245 impl TestSnapshotRoot {
1246 fn new() -> Self {
1247 let path = std::env::temp_dir().join(format!(
1248 "hashsigs-promoter-coordination-{}-{}",
1249 std::process::id(),
1250 NEXT_SNAPSHOT_ROOT.fetch_add(1, Ordering::Relaxed)
1251 ));
1252 fs::create_dir(&path).unwrap();
1253 set_mode(&path, 0o700);
1254 Self { path }
1255 }
1256
1257 fn path(&self) -> &Path {
1258 &self.path
1259 }
1260
1261 fn child(&self, name: &str) -> PathBuf {
1262 self.path.join(name)
1263 }
1264
1265 fn write_file(&self, name: &str, bytes: &[u8], mode: u32) -> PathBuf {
1266 let path = self.child(name);
1267 fs::write(&path, bytes).unwrap();
1268 set_mode(&path, mode);
1269 path
1270 }
1271
1272 fn seal(&self) {
1273 set_mode(&self.path, 0o500);
1274 }
1275 }
1276
1277 impl Drop for TestSnapshotRoot {
1278 fn drop(&mut self) {
1279 make_writable(&self.path);
1280 let _ = fs::remove_dir_all(&self.path);
1281 }
1282 }
1283
1284 struct SnapshotFixture {
1285 root: TestSnapshotRoot,
1286 request: crate::PromotionRequest,
1287 selected_path: PathBuf,
1288 selected_bytes: Vec<u8>,
1289 }
1290
1291 fn set_mode(path: &Path, mode: u32) {
1292 fs::set_permissions(path, fs::Permissions::from_mode(mode)).unwrap();
1293 }
1294
1295 fn make_writable(path: &Path) {
1296 let Ok(metadata) = fs::symlink_metadata(path) else {
1297 return;
1298 };
1299 if metadata.file_type().is_symlink() {
1300 let _ = fs::remove_file(path);
1301 } else if metadata.is_dir() {
1302 let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o700));
1303 if let Ok(entries) = fs::read_dir(path) {
1304 for entry in entries.flatten() {
1305 make_writable(&entry.path());
1306 }
1307 }
1308 } else {
1309 let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o600));
1310 }
1311 }
1312
1313 fn history_manifest(history: &[u8]) -> Vec<u8> {
1314 format!("{} history.jsonl\n", sha256_hex(history)).into_bytes()
1315 }
1316
1317 fn snapshot_request(
1318 root: &TestSnapshotRoot,
1319 run_id: &str,
1320 history: &[u8],
1321 manifest: &[u8],
1322 selected: &[u8],
1323 ) -> crate::PromotionRequest {
1324 let history_path = root.child("history.jsonl");
1325 let manifest_path = root.child("history.SHA256SUMS");
1326 let selected_path = root.child("selected.json");
1327 synthetic_request_with_coordination_snapshot(
1328 run_id,
1329 [
1330 (history_path.to_str().unwrap(), history),
1331 (manifest_path.to_str().unwrap(), manifest),
1332 (selected_path.to_str().unwrap(), selected),
1333 ],
1334 )
1335 }
1336
1337 fn valid_snapshot_fixture(salt: u8) -> SnapshotFixture {
1338 let burn = record(0, GENESIS_SHA256, salt);
1339 let history = canonical_line(&burn).unwrap();
1340 let selected_bytes = history.clone();
1341 let manifest = history_manifest(&history);
1342 let root = TestSnapshotRoot::new();
1343 root.write_file("history.jsonl", &history, 0o400);
1344 root.write_file("history.SHA256SUMS", &manifest, 0o400);
1345 let selected_path = root.write_file("selected.json", &selected_bytes, 0o400);
1346 let request = snapshot_request(&root, &burn.run_id, &history, &manifest, &selected_bytes);
1347 root.seal();
1348 SnapshotFixture {
1349 root,
1350 request,
1351 selected_path,
1352 selected_bytes,
1353 }
1354 }
1355
1356 fn record(sequence: u64, previous: &str, salt: u8) -> CoordinationBurnWire {
1357 synthetic_burn_record(sequence, previous, salt)
1358 }
1359
1360 fn verify(
1361 history: &[u8],
1362 selected: &CoordinationBurnWire,
1363 ) -> crate::PromotionResult<super::CoordinationChain> {
1364 let selected_bytes = canonical_line(selected).unwrap();
1365 let request =
1366 synthetic_request_with_coordination(&selected.run_id, history, &selected_bytes);
1367 verify_complete_burn_history(&request, history, &selected_bytes)
1368 }
1369
1370 #[test]
1371 fn complete_chain_recomputes_genesis_and_global_uniqueness() {
1372 let first = record(0, GENESIS_SHA256, 1);
1373 let second = record(1, first.record_sha256.as_str(), 2);
1374 let mut history = canonical_line(&first).unwrap();
1375 history.extend(canonical_line(&second).unwrap());
1376 let state = verify(&history, &second).unwrap();
1377 assert_eq!(state.records(), 2);
1378 assert_eq!(state.selected_burn().run_id(), second.run_id.as_str());
1379 assert_eq!(
1380 state.selected_burn().record_sha256(),
1381 second.record_sha256.as_str()
1382 );
1383 assert!(state.contains_run_id(&first.run_id));
1384 assert!(state.contains_grant(
1385 &first.grants[0].ledger_id,
1386 &first.grants[0].entry_id,
1387 &first.grants[0].entry_sha256
1388 ));
1389 }
1390
1391 #[test]
1392 fn gap_and_replay_are_rejected() {
1393 let first = record(0, GENESIS_SHA256, 1);
1394 let gap = record(2, first.record_sha256.as_str(), 2);
1395 let mut gap_history = canonical_line(&first).unwrap();
1396 gap_history.extend(canonical_line(&gap).unwrap());
1397 assert_eq!(
1398 verify(&gap_history, &gap).unwrap_err().code(),
1399 FailureCode::CoordinationChain
1400 );
1401
1402 let mut replay = record(1, first.record_sha256.as_str(), 2);
1403 replay.run_id.clone_from(&first.run_id);
1404 replay.record_sha256 = replay.recomputed_record_sha256().unwrap();
1405 let mut replay_history = canonical_line(&first).unwrap();
1406 replay_history.extend(canonical_line(&replay).unwrap());
1407 assert_eq!(
1408 verify(&replay_history, &replay).unwrap_err().code(),
1409 FailureCode::CoordinationReplay
1410 );
1411 }
1412
1413 #[test]
1414 fn coordination_replay_key_ignores_changed_grant_byte_metadata() {
1415 let first = record(0, GENESIS_SHA256, 1);
1416 let mut second = record(1, &first.record_sha256, 2);
1417 second.grants[0]
1418 .ledger_id
1419 .clone_from(&first.grants[0].ledger_id);
1420 second.grants[0]
1421 .entry_id
1422 .clone_from(&first.grants[0].entry_id);
1423 second.grants[0]
1424 .entry_sha256
1425 .clone_from(&first.grants[0].entry_sha256);
1426 second.grants[0].entry_bytes = first.grants[0].entry_bytes + 1;
1427 second.record_sha256 = second.recomputed_record_sha256().unwrap();
1428 let mut history = canonical_line(&first).unwrap();
1429 history.extend(canonical_line(&second).unwrap());
1430 assert_eq!(
1431 verify(&history, &second).unwrap_err().code(),
1432 FailureCode::CoordinationReplay
1433 );
1434 }
1435
1436 #[test]
1437 fn selected_receipt_and_run_must_bind_the_exact_last_record() {
1438 let first = record(0, GENESIS_SHA256, 1);
1439 let second = record(1, first.record_sha256.as_str(), 2);
1440 let mut history = canonical_line(&first).unwrap();
1441 history.extend(canonical_line(&second).unwrap());
1442 let first_bytes = canonical_line(&first).unwrap();
1443 let request = synthetic_request_with_coordination(&second.run_id, &history, &first_bytes);
1444 assert_eq!(
1445 verify_complete_burn_history(&request, &history, &first_bytes,)
1446 .unwrap_err()
1447 .code(),
1448 FailureCode::CoordinationChain
1449 );
1450 }
1451
1452 #[test]
1453 fn retained_snapshot_binding_is_derived_only_from_request_locators() {
1454 let request =
1455 synthetic_request_with_coordination(&"1".repeat(32), b"history\n", b"receipt\n");
1456 let binding = CoordinationSnapshotBinding::from_request(&request).unwrap();
1457 assert_eq!(binding.root, std::path::Path::new("/evidence"));
1458 assert_eq!(
1459 binding.history.absolute_path,
1460 std::path::Path::new("/evidence/coordination-history.jsonl")
1461 );
1462 assert_eq!(binding.history.bytes, 8);
1463 assert_eq!(
1464 binding.history.sha256,
1465 crate::canonical::sha256_hex(b"history\n")
1466 );
1467 assert_eq!(
1468 binding.manifest.absolute_path,
1469 std::path::Path::new("/evidence/coordination-history.SHA256SUMS")
1470 );
1471 assert_eq!(binding.manifest.bytes, 10);
1472 assert_eq!(binding.manifest.sha256, "8".repeat(64));
1473 assert_eq!(
1474 binding.selected_receipt.absolute_path,
1475 std::path::Path::new("/evidence/coordination-burn.json")
1476 );
1477 assert_eq!(binding.selected_receipt.bytes, 8);
1478 assert_eq!(
1479 binding.selected_receipt.sha256,
1480 crate::canonical::sha256_hex(b"receipt\n")
1481 );
1482 }
1483
1484 #[test]
1485 fn retained_snapshot_rejects_manifest_content_mismatch() {
1486 let burn = record(0, GENESIS_SHA256, 3);
1487 let history = canonical_line(&burn).unwrap();
1488 let manifest = format!("{} history.jsonl\n", "0".repeat(64)).into_bytes();
1489 let root = TestSnapshotRoot::new();
1490 root.write_file("history.jsonl", &history, 0o400);
1491 root.write_file("history.SHA256SUMS", &manifest, 0o400);
1492 root.write_file("selected.json", &history, 0o400);
1493 let request = snapshot_request(&root, &burn.run_id, &history, &manifest, &history);
1494 root.seal();
1495 assert_eq!(
1496 super::retain_verified_coordination_snapshot(&request)
1497 .unwrap_err()
1498 .code(),
1499 FailureCode::CoordinationChain
1500 );
1501 }
1502
1503 #[test]
1504 fn retained_snapshot_rejects_hardlink_alias() {
1505 let burn = record(0, GENESIS_SHA256, 4);
1506 let history = canonical_line(&burn).unwrap();
1507 let manifest = history_manifest(&history);
1508 let root = TestSnapshotRoot::new();
1509 let history_path = root.write_file("history.jsonl", &history, 0o400);
1510 root.write_file("history.SHA256SUMS", &manifest, 0o400);
1511 fs::hard_link(&history_path, root.child("selected.json")).unwrap();
1512 let request = snapshot_request(&root, &burn.run_id, &history, &manifest, &history);
1513 root.seal();
1514 assert_eq!(
1515 super::retain_verified_coordination_snapshot(&request)
1516 .unwrap_err()
1517 .code(),
1518 FailureCode::CoordinationChain
1519 );
1520 }
1521
1522 #[test]
1523 fn retained_snapshot_rejects_writable_inputs() {
1524 let burn = record(0, GENESIS_SHA256, 5);
1525 let history = canonical_line(&burn).unwrap();
1526 let manifest = history_manifest(&history);
1527 let root = TestSnapshotRoot::new();
1528 root.write_file("history.jsonl", &history, 0o600);
1529 root.write_file("history.SHA256SUMS", &manifest, 0o400);
1530 root.write_file("selected.json", &history, 0o400);
1531 let request = snapshot_request(&root, &burn.run_id, &history, &manifest, &history);
1532 root.seal();
1533 assert_eq!(
1534 super::retain_verified_coordination_snapshot(&request)
1535 .unwrap_err()
1536 .code(),
1537 FailureCode::CoordinationChain
1538 );
1539 }
1540
1541 #[test]
1542 fn retained_snapshot_final_revalidation_detects_path_substitution() {
1543 let fixture = valid_snapshot_fixture(6);
1544 let snapshot = super::retain_verified_coordination_snapshot(&fixture.request).unwrap();
1545 snapshot.revalidate().unwrap();
1546
1547 set_mode(fixture.root.path(), 0o700);
1548 fs::rename(&fixture.selected_path, fixture.root.child("selected.old")).unwrap();
1549 fs::write(&fixture.selected_path, &fixture.selected_bytes).unwrap();
1550 set_mode(&fixture.selected_path, 0o400);
1551 fixture.root.seal();
1552 assert_eq!(
1553 snapshot.revalidate().unwrap_err().code(),
1554 FailureCode::CoordinationChain
1555 );
1556 }
1557
1558 #[test]
1559 fn retained_snapshot_final_revalidation_detects_in_place_change() {
1560 let fixture = valid_snapshot_fixture(7);
1561 let snapshot = super::retain_verified_coordination_snapshot(&fixture.request).unwrap();
1562 snapshot.revalidate().unwrap();
1563
1564 let mut changed = fixture.selected_bytes.clone();
1565 changed[0] ^= 1;
1566 set_mode(&fixture.selected_path, 0o600);
1567 fs::write(&fixture.selected_path, changed).unwrap();
1568 set_mode(&fixture.selected_path, 0o400);
1569 assert_eq!(
1570 snapshot.revalidate().unwrap_err().code(),
1571 FailureCode::CoordinationChain
1572 );
1573 }
1574}