1use std::collections::{BTreeMap, BTreeSet};
23use std::fmt;
24use std::net::SocketAddr;
25
26use serde::{Deserialize, Serialize};
27use sha2::{Digest, Sha256};
28
29use crate::atp::object::{ContentId, ObjectId as AtpObjectId};
30use crate::net::atp::transport_common::{EntryDigest, flat_merkle_root_from_digests, hex_encode};
31use crate::types::symbol::ObjectId as RaptorqObjectId;
32
33pub const BONDING_PROTOCOL_VERSION: u16 = 1;
35
36pub const MAX_STATIC_RESIDUE_DONORS: u32 = 1024;
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct BondTransferDescriptor {
42 pub transfer_id: String,
44 pub root_name: String,
46 pub is_directory: bool,
48 pub total_bytes: u64,
50 pub merkle_root_hex: String,
52 pub entries: Vec<BondDescriptorEntry>,
54 pub symbol_size: u16,
56 pub max_block_size: usize,
58 pub auth_key_ref: Option<String>,
60}
61
62impl BondTransferDescriptor {
63 pub fn from_donor_proof(
65 root_name: impl Into<String>,
66 is_directory: bool,
67 symbol_size: u16,
68 max_block_size: usize,
69 auth_key_ref: Option<String>,
70 proof_entries: Vec<BondDonorProofEntry>,
71 ) -> Result<Self, ChannelBondingError> {
72 validate_proof_entries(&proof_entries)?;
73 let total_bytes = total_bytes_for_proof(&proof_entries)?;
74 let merkle_root_hex = merkle_root_for_proof(&proof_entries)?;
75 let transfer_id = transfer_id_hex(&merkle_root_hex, total_bytes, proof_entries.len());
76 let entries = proof_entries
77 .iter()
78 .map(BondDescriptorEntry::from)
79 .collect();
80
81 let descriptor = Self {
82 transfer_id,
83 root_name: root_name.into(),
84 is_directory,
85 total_bytes,
86 merkle_root_hex,
87 entries,
88 symbol_size,
89 max_block_size,
90 auth_key_ref,
91 };
92 descriptor.validate()?;
93 Ok(descriptor)
94 }
95
96 pub fn validate(&self) -> Result<(), ChannelBondingError> {
98 validate_hex_32("merkle_root_hex", None, &self.merkle_root_hex)?;
99 if self.transfer_id
100 != transfer_id_hex(&self.merkle_root_hex, self.total_bytes, self.entries.len())
101 {
102 return Err(ChannelBondingError::TransferIdMismatch {
103 expected: transfer_id_hex(
104 &self.merkle_root_hex,
105 self.total_bytes,
106 self.entries.len(),
107 ),
108 actual: self.transfer_id.clone(),
109 });
110 }
111 validate_descriptor_entries(&self.entries)?;
112 let computed_total = total_bytes_for_descriptor(&self.entries)?;
113 if computed_total != self.total_bytes {
114 return Err(ChannelBondingError::TotalBytesMismatch {
115 expected: self.total_bytes,
116 actual: computed_total,
117 });
118 }
119 Ok(())
120 }
121
122 pub fn verify_donor_byte_match(
129 &self,
130 proof: &BondDonorByteProof,
131 ) -> Result<(), ChannelBondingError> {
132 self.validate()?;
133 proof.validate()?;
134
135 if proof.transfer_id != self.transfer_id {
136 return Err(ChannelBondingError::TransferIdMismatch {
137 expected: self.transfer_id.clone(),
138 actual: proof.transfer_id.clone(),
139 });
140 }
141 if proof.total_bytes != self.total_bytes {
142 return Err(ChannelBondingError::TotalBytesMismatch {
143 expected: self.total_bytes,
144 actual: proof.total_bytes,
145 });
146 }
147 if proof.merkle_root_hex != self.merkle_root_hex {
148 return Err(ChannelBondingError::MerkleRootMismatch {
149 expected: self.merkle_root_hex.clone(),
150 actual: proof.merkle_root_hex.clone(),
151 });
152 }
153
154 let expected = descriptor_entries_by_index(&self.entries)?;
155 let actual = proof_entries_by_index(&proof.entries)?;
156 if expected.len() != actual.len() {
157 return Err(ChannelBondingError::EntryCountMismatch {
158 expected: expected.len(),
159 actual: actual.len(),
160 });
161 }
162
163 for (index, descriptor_entry) in expected {
164 let proof_entry = actual
165 .get(&index)
166 .ok_or(ChannelBondingError::MissingProofEntry { index })?;
167 compare_entry_identity(descriptor_entry, proof_entry)?;
168 }
169
170 let computed_merkle = merkle_root_for_proof(&proof.entries)?;
171 if computed_merkle != self.merkle_root_hex {
172 return Err(ChannelBondingError::MerkleRootMismatch {
173 expected: self.merkle_root_hex.clone(),
174 actual: computed_merkle,
175 });
176 }
177
178 Ok(())
179 }
180
181 #[must_use]
183 pub fn entry_object_id(&self, entry_index: u32) -> RaptorqObjectId {
184 entry_object_id(&self.transfer_id, entry_index)
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub struct BondDescriptorEntry {
191 pub index: u32,
193 pub rel_path: String,
195 pub size: u64,
197 pub sha256_hex: String,
199}
200
201impl From<&BondDonorProofEntry> for BondDescriptorEntry {
202 fn from(entry: &BondDonorProofEntry) -> Self {
203 Self {
204 index: entry.index,
205 rel_path: entry.rel_path.clone(),
206 size: entry.size,
207 sha256_hex: entry.sha256_hex.clone(),
208 }
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct BondDonorByteProof {
215 pub transfer_id: String,
217 pub total_bytes: u64,
219 pub merkle_root_hex: String,
221 pub entries: Vec<BondDonorProofEntry>,
223}
224
225impl BondDonorByteProof {
226 pub fn from_entries(entries: Vec<BondDonorProofEntry>) -> Result<Self, ChannelBondingError> {
228 validate_proof_entries(&entries)?;
229 let total_bytes = total_bytes_for_proof(&entries)?;
230 let merkle_root_hex = merkle_root_for_proof(&entries)?;
231 let transfer_id = transfer_id_hex(&merkle_root_hex, total_bytes, entries.len());
232 Ok(Self {
233 transfer_id,
234 total_bytes,
235 merkle_root_hex,
236 entries,
237 })
238 }
239
240 pub fn validate(&self) -> Result<(), ChannelBondingError> {
242 validate_hex_32("merkle_root_hex", None, &self.merkle_root_hex)?;
243 validate_proof_entries(&self.entries)?;
244 let computed_total = total_bytes_for_proof(&self.entries)?;
245 if computed_total != self.total_bytes {
246 return Err(ChannelBondingError::TotalBytesMismatch {
247 expected: self.total_bytes,
248 actual: computed_total,
249 });
250 }
251 let computed_merkle = merkle_root_for_proof(&self.entries)?;
252 if computed_merkle != self.merkle_root_hex {
253 return Err(ChannelBondingError::MerkleRootMismatch {
254 expected: self.merkle_root_hex.clone(),
255 actual: computed_merkle,
256 });
257 }
258 let computed_transfer_id =
259 transfer_id_hex(&self.merkle_root_hex, self.total_bytes, self.entries.len());
260 if computed_transfer_id != self.transfer_id {
261 return Err(ChannelBondingError::TransferIdMismatch {
262 expected: computed_transfer_id,
263 actual: self.transfer_id.clone(),
264 });
265 }
266 Ok(())
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272pub struct BondDonorProofEntry {
273 pub index: u32,
275 pub rel_path: String,
277 pub size: u64,
279 pub content_id_hex: String,
281 pub sha256_hex: String,
283}
284
285impl BondDonorProofEntry {
286 #[must_use]
292 pub fn from_bytes(index: u32, rel_path: impl Into<String>, bytes: &[u8]) -> Self {
293 let content_id = ContentId::from_bytes(bytes);
294 let sha256 = Sha256::digest(bytes);
295 Self {
296 index,
297 rel_path: rel_path.into(),
298 size: bytes.len() as u64,
299 content_id_hex: content_id.to_hex(),
300 sha256_hex: hex_encode(&sha256),
301 }
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
307pub struct DonorAssignment {
308 pub donor_index: u32,
310 pub donor_count: u32,
312 pub esi_windows: Option<Vec<EsiWindow>>,
314 pub receiver_udp_endpoints: Vec<SocketAddr>,
316 pub auth_key_ref: Option<String>,
318}
319
320impl DonorAssignment {
321 pub fn validate(&self) -> Result<(), ChannelBondingError> {
323 validate_donor_index(self.donor_index, self.donor_count)?;
324 if self.donor_count > MAX_STATIC_RESIDUE_DONORS {
325 return Err(ChannelBondingError::TooManyDonors {
326 donor_count: self.donor_count,
327 max: MAX_STATIC_RESIDUE_DONORS,
328 });
329 }
330 if let Some(windows) = &self.esi_windows {
331 for window in windows {
332 window.validate()?;
333 }
334 }
335 Ok(())
336 }
337
338 #[must_use]
340 pub fn owns_esi(&self, esi: u32) -> bool {
341 if !owns_esi(self.donor_index, self.donor_count, esi) {
342 return false;
343 }
344 self.esi_windows
345 .as_ref()
346 .is_none_or(|windows| windows.iter().any(|window| window.contains(esi)))
347 }
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
352pub enum BondAuthControlPlane {
353 Ssh,
355 Tailscale,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
361pub enum BondAuthKeyLocation {
362 EnvVar(String),
364 KeyFile(String),
366 Argv(String),
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct BondAuthKeyRef {
373 pub key_id: String,
375 pub control_plane: BondAuthControlPlane,
377 pub location: BondAuthKeyLocation,
379}
380
381impl BondAuthKeyRef {
382 pub fn validate(&self) -> Result<(), ChannelBondingError> {
384 if self.key_id.trim().is_empty() {
385 return Err(ChannelBondingError::MissingAuthKeyId);
386 }
387 match &self.location {
388 BondAuthKeyLocation::EnvVar(name) => {
389 if name.trim().is_empty() {
390 return Err(ChannelBondingError::MissingAuthKeyLocation);
391 }
392 }
393 BondAuthKeyLocation::KeyFile(path) => {
394 if path.trim().is_empty() {
395 return Err(ChannelBondingError::MissingAuthKeyLocation);
396 }
397 }
398 BondAuthKeyLocation::Argv(_) => {
399 return Err(ChannelBondingError::InsecureAuthKeyDelivery);
400 }
401 }
402 Ok(())
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
408pub struct BondingSecurityModel {
409 pub auth_key: BondAuthKeyRef,
411 pub auth_before_decode: bool,
413 pub fail_closed_on_merkle_mismatch: bool,
415}
416
417impl BondingSecurityModel {
418 pub fn validate(&self) -> Result<(), ChannelBondingError> {
420 self.auth_key.validate()?;
421 if !self.auth_before_decode {
422 return Err(ChannelBondingError::AuthBeforeDecodeDisabled);
423 }
424 if !self.fail_closed_on_merkle_mismatch {
425 return Err(ChannelBondingError::MerkleFailClosedDisabled);
426 }
427 Ok(())
428 }
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
433pub struct EsiWindow {
434 pub start_inclusive: u32,
436 pub end_exclusive: u32,
438}
439
440impl EsiWindow {
441 #[must_use]
443 pub const fn new(start_inclusive: u32, end_exclusive: u32) -> Self {
444 Self {
445 start_inclusive,
446 end_exclusive,
447 }
448 }
449
450 pub fn validate(&self) -> Result<(), ChannelBondingError> {
452 if self.start_inclusive >= self.end_exclusive {
453 return Err(ChannelBondingError::InvalidEsiWindow {
454 start_inclusive: self.start_inclusive,
455 end_exclusive: self.end_exclusive,
456 });
457 }
458 Ok(())
459 }
460
461 #[must_use]
463 pub const fn contains(&self, esi: u32) -> bool {
464 self.start_inclusive <= esi && esi < self.end_exclusive
465 }
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
470pub enum BondTransport {
471 DirectIp,
473 Ssh,
475 Tailscale,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
481pub struct BondingHandshake {
482 pub min_protocol_version: u16,
484 pub max_protocol_version: u16,
486 pub supported_transports: BTreeSet<BondTransport>,
488 pub supports_dynamic_windows: bool,
490 pub supports_resume: bool,
492 pub auth_required: bool,
494 pub max_donor_count: u32,
496 pub extension_capabilities: BTreeSet<String>,
498}
499
500impl BondingHandshake {
501 #[must_use]
503 pub fn v1_static(
504 transports: impl IntoIterator<Item = BondTransport>,
505 max_donor_count: u32,
506 auth_required: bool,
507 ) -> Self {
508 Self {
509 min_protocol_version: BONDING_PROTOCOL_VERSION,
510 max_protocol_version: BONDING_PROTOCOL_VERSION,
511 supported_transports: transports.into_iter().collect(),
512 supports_dynamic_windows: false,
513 supports_resume: false,
514 auth_required,
515 max_donor_count,
516 extension_capabilities: BTreeSet::new(),
517 }
518 }
519
520 #[must_use]
522 pub const fn with_protocol_range(
523 mut self,
524 min_protocol_version: u16,
525 max_protocol_version: u16,
526 ) -> Self {
527 self.min_protocol_version = min_protocol_version;
528 self.max_protocol_version = max_protocol_version;
529 self
530 }
531
532 #[must_use]
534 pub const fn with_dynamic_windows(mut self, supported: bool) -> Self {
535 self.supports_dynamic_windows = supported;
536 self
537 }
538
539 #[must_use]
541 pub const fn with_resume(mut self, supported: bool) -> Self {
542 self.supports_resume = supported;
543 self
544 }
545
546 #[must_use]
548 pub fn with_extension_capability(mut self, capability: impl Into<String>) -> Self {
549 self.extension_capabilities.insert(capability.into());
550 self
551 }
552
553 pub fn negotiate(&self, peer: &Self) -> Result<BondingAgreement, ChannelBondingError> {
555 validate_handshake_offer(self)?;
556 validate_handshake_offer(peer)?;
557 if self.supported_transports.is_empty() || peer.supported_transports.is_empty() {
558 return Err(ChannelBondingError::NoCommonTransport);
559 }
560
561 let selected_version = self.max_protocol_version.min(peer.max_protocol_version);
562 let required_min = self.min_protocol_version.max(peer.min_protocol_version);
563 if selected_version < required_min {
564 return Err(ChannelBondingError::IncompatibleProtocolVersion {
565 local_min: self.min_protocol_version,
566 local_max: self.max_protocol_version,
567 peer_min: peer.min_protocol_version,
568 peer_max: peer.max_protocol_version,
569 });
570 }
571
572 let supported_transports = self
573 .supported_transports
574 .intersection(&peer.supported_transports)
575 .copied()
576 .collect::<BTreeSet<_>>();
577 if supported_transports.is_empty() {
578 return Err(ChannelBondingError::NoCommonTransport);
579 }
580
581 let max_donor_count = self.max_donor_count.min(peer.max_donor_count);
582 if max_donor_count == 0 {
583 return Err(ChannelBondingError::InvalidDonorCount { donor_count: 0 });
584 }
585
586 Ok(BondingAgreement {
587 protocol_version: selected_version,
588 supported_transports,
589 assignment_mode: if self.supports_dynamic_windows && peer.supports_dynamic_windows {
590 BondingAssignmentMode::DynamicWindows
591 } else {
592 BondingAssignmentMode::StaticResidue
593 },
594 resume_supported: self.supports_resume && peer.supports_resume,
595 auth_required: self.auth_required || peer.auth_required,
596 max_donor_count,
597 })
598 }
599}
600
601#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
603pub struct BondingAgreement {
604 pub protocol_version: u16,
606 pub supported_transports: BTreeSet<BondTransport>,
608 pub assignment_mode: BondingAssignmentMode,
610 pub resume_supported: bool,
612 pub auth_required: bool,
614 pub max_donor_count: u32,
616}
617
618impl BondingAgreement {
619 #[must_use]
621 pub fn supports_transport(&self, transport: BondTransport) -> bool {
622 self.supported_transports.contains(&transport)
623 }
624}
625
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
628pub enum BondingAssignmentMode {
629 StaticResidue,
631 DynamicWindows,
633}
634
635#[derive(Debug, Clone)]
637pub struct DonorEsiStream {
638 donor_index: u32,
639 donor_count: u32,
640 next_seq: Option<u32>,
641}
642
643impl DonorEsiStream {
644 pub fn new(donor_index: u32, donor_count: u32) -> Result<Self, ChannelBondingError> {
646 validate_donor_index(donor_index, donor_count)?;
647 Ok(Self {
648 donor_index,
649 donor_count,
650 next_seq: Some(0),
651 })
652 }
653}
654
655impl Iterator for DonorEsiStream {
656 type Item = u32;
657
658 fn next(&mut self) -> Option<Self::Item> {
659 let seq = self.next_seq?;
660 match esi_for_donor(self.donor_index, self.donor_count, seq) {
661 Ok(esi) => {
662 self.next_seq = seq.checked_add(1);
663 Some(esi)
664 }
665 Err(_) => {
666 self.next_seq = None;
667 None
668 }
669 }
670 }
671}
672
673pub fn esi_for_donor(
675 donor_index: u32,
676 donor_count: u32,
677 seq: u32,
678) -> Result<u32, ChannelBondingError> {
679 validate_donor_index(donor_index, donor_count)?;
680 seq.checked_mul(donor_count)
681 .and_then(|base| base.checked_add(donor_index))
682 .ok_or(ChannelBondingError::EsiOverflow)
683}
684
685#[must_use]
687pub const fn owns_esi(donor_index: u32, donor_count: u32, esi: u32) -> bool {
688 donor_count != 0 && donor_index < donor_count && esi % donor_count == donor_index
689}
690
691#[must_use]
693pub fn entry_object_id(transfer_id: &str, index: u32) -> RaptorqObjectId {
694 let mut hasher = Sha256::new();
695 hasher.update(b"asupersync.atp.rq.entry-object-id.v1\0");
696 hasher.update(transfer_id.as_bytes());
697 hasher.update(index.to_be_bytes());
698 let digest = hasher.finalize();
699 let high = u64::from_be_bytes([
700 digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7],
701 ]);
702 let low = u64::from_be_bytes([
703 digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14],
704 digest[15],
705 ]);
706 RaptorqObjectId::new(high, low)
707}
708
709#[must_use]
711pub fn transfer_id_hex(merkle_root_hex: &str, total_bytes: u64, file_count: usize) -> String {
712 let mut hasher = Sha256::new();
713 hasher.update(b"asupersync.atp.rq.transfer-id.v1\0");
714 hasher.update(merkle_root_hex.as_bytes());
715 hasher.update(total_bytes.to_be_bytes());
716 hasher.update(u64::try_from(file_count).unwrap_or(u64::MAX).to_be_bytes());
717 hex_encode(&hasher.finalize()[..16])
718}
719
720#[derive(Debug, Clone, PartialEq, Eq)]
722pub enum ChannelBondingError {
723 InvalidDonorCount {
725 donor_count: u32,
727 },
728 InvalidDonorIndex {
730 donor_index: u32,
732 donor_count: u32,
734 },
735 TooManyDonors {
737 donor_count: u32,
739 max: u32,
741 },
742 EsiOverflow,
744 InvalidEsiWindow {
746 start_inclusive: u32,
748 end_exclusive: u32,
750 },
751 InvalidProtocolRange {
753 min: u16,
755 max: u16,
757 },
758 IncompatibleProtocolVersion {
760 local_min: u16,
762 local_max: u16,
764 peer_min: u16,
766 peer_max: u16,
768 },
769 NoCommonTransport,
771 InvalidHexDigest {
773 field: &'static str,
775 index: Option<u32>,
777 value: String,
779 },
780 DuplicateEntryIndex {
782 index: u32,
784 },
785 DuplicateEntryPath {
787 rel_path: String,
789 },
790 EntryCountMismatch {
792 expected: usize,
794 actual: usize,
796 },
797 MissingProofEntry {
799 index: u32,
801 },
802 EntryMismatch {
804 index: u32,
806 field: &'static str,
808 expected: String,
810 actual: String,
812 },
813 TotalBytesMismatch {
815 expected: u64,
817 actual: u64,
819 },
820 TotalBytesOverflow,
822 TransferIdMismatch {
824 expected: String,
826 actual: String,
828 },
829 MerkleRootMismatch {
831 expected: String,
833 actual: String,
835 },
836 MissingAuthKeyId,
838 MissingAuthKeyLocation,
840 InsecureAuthKeyDelivery,
842 AuthBeforeDecodeDisabled,
844 MerkleFailClosedDisabled,
846}
847
848impl fmt::Display for ChannelBondingError {
849 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
850 match self {
851 Self::InvalidDonorCount { donor_count } => {
852 write!(f, "invalid donor_count {donor_count}; must be nonzero")
853 }
854 Self::InvalidDonorIndex {
855 donor_index,
856 donor_count,
857 } => write!(
858 f,
859 "invalid donor_index {donor_index}; donor_count is {donor_count}"
860 ),
861 Self::TooManyDonors { donor_count, max } => {
862 write!(f, "donor_count {donor_count} exceeds max {max}")
863 }
864 Self::EsiOverflow => f.write_str("ESI arithmetic overflow"),
865 Self::InvalidEsiWindow {
866 start_inclusive,
867 end_exclusive,
868 } => write!(f, "invalid ESI window [{start_inclusive}, {end_exclusive})"),
869 Self::InvalidProtocolRange { min, max } => {
870 write!(f, "invalid bonding protocol range {min}..={max}")
871 }
872 Self::IncompatibleProtocolVersion {
873 local_min,
874 local_max,
875 peer_min,
876 peer_max,
877 } => write!(
878 f,
879 "incompatible bonding protocol versions: local {local_min}..={local_max}, peer {peer_min}..={peer_max}"
880 ),
881 Self::NoCommonTransport => f.write_str("no common bonding transport"),
882 Self::InvalidHexDigest {
883 field,
884 index,
885 value,
886 } => write!(f, "invalid {field} digest for entry {index:?}: {value:?}"),
887 Self::DuplicateEntryIndex { index } => {
888 write!(f, "duplicate bonded entry index {index}")
889 }
890 Self::DuplicateEntryPath { rel_path } => {
891 write!(f, "duplicate bonded entry path {rel_path:?}")
892 }
893 Self::EntryCountMismatch { expected, actual } => {
894 write!(
895 f,
896 "entry count mismatch: expected {expected}, actual {actual}"
897 )
898 }
899 Self::MissingProofEntry { index } => {
900 write!(f, "donor proof missing entry index {index}")
901 }
902 Self::EntryMismatch {
903 index,
904 field,
905 expected,
906 actual,
907 } => write!(
908 f,
909 "entry {index} {field} mismatch: expected {expected:?}, actual {actual:?}"
910 ),
911 Self::TotalBytesMismatch { expected, actual } => {
912 write!(
913 f,
914 "total bytes mismatch: expected {expected}, actual {actual}"
915 )
916 }
917 Self::TotalBytesOverflow => f.write_str("total bytes overflow"),
918 Self::TransferIdMismatch { expected, actual } => write!(
919 f,
920 "transfer id mismatch: expected {expected:?}, actual {actual:?}"
921 ),
922 Self::MerkleRootMismatch { expected, actual } => write!(
923 f,
924 "merkle root mismatch: expected {expected:?}, actual {actual:?}"
925 ),
926 Self::MissingAuthKeyId => f.write_str("bonding auth key id must not be empty"),
927 Self::MissingAuthKeyLocation => {
928 f.write_str("bonding auth key location must not be empty")
929 }
930 Self::InsecureAuthKeyDelivery => {
931 f.write_str("bonding auth key must not be delivered through argv")
932 }
933 Self::AuthBeforeDecodeDisabled => {
934 f.write_str("channel bonding requires auth-before-decode")
935 }
936 Self::MerkleFailClosedDisabled => {
937 f.write_str("channel bonding requires fail-closed Merkle/SHA verification")
938 }
939 }
940 }
941}
942
943impl std::error::Error for ChannelBondingError {}
944
945fn validate_donor_index(donor_index: u32, donor_count: u32) -> Result<(), ChannelBondingError> {
946 validate_donor_count(donor_count)?;
947 if donor_index >= donor_count {
948 return Err(ChannelBondingError::InvalidDonorIndex {
949 donor_index,
950 donor_count,
951 });
952 }
953 Ok(())
954}
955
956fn validate_donor_count(donor_count: u32) -> Result<(), ChannelBondingError> {
957 if donor_count == 0 {
958 return Err(ChannelBondingError::InvalidDonorCount { donor_count });
959 }
960 Ok(())
961}
962
963fn validate_protocol_range(min: u16, max: u16) -> Result<(), ChannelBondingError> {
964 if min == 0 || min > max {
965 return Err(ChannelBondingError::InvalidProtocolRange { min, max });
966 }
967 Ok(())
968}
969
970fn validate_handshake_offer(offer: &BondingHandshake) -> Result<(), ChannelBondingError> {
971 validate_protocol_range(offer.min_protocol_version, offer.max_protocol_version)?;
972 validate_donor_count(offer.max_donor_count)?;
973 if offer.max_donor_count > MAX_STATIC_RESIDUE_DONORS {
974 return Err(ChannelBondingError::TooManyDonors {
975 donor_count: offer.max_donor_count,
976 max: MAX_STATIC_RESIDUE_DONORS,
977 });
978 }
979 Ok(())
980}
981
982fn validate_descriptor_entries(entries: &[BondDescriptorEntry]) -> Result<(), ChannelBondingError> {
983 let mut indices = BTreeSet::new();
984 let mut paths = BTreeSet::new();
985 for entry in entries {
986 if !indices.insert(entry.index) {
987 return Err(ChannelBondingError::DuplicateEntryIndex { index: entry.index });
988 }
989 if !paths.insert(entry.rel_path.clone()) {
990 return Err(ChannelBondingError::DuplicateEntryPath {
991 rel_path: entry.rel_path.clone(),
992 });
993 }
994 validate_hex_32("sha256_hex", Some(entry.index), &entry.sha256_hex)?;
995 }
996 Ok(())
997}
998
999fn validate_proof_entries(entries: &[BondDonorProofEntry]) -> Result<(), ChannelBondingError> {
1000 let mut indices = BTreeSet::new();
1001 let mut paths = BTreeSet::new();
1002 for entry in entries {
1003 if !indices.insert(entry.index) {
1004 return Err(ChannelBondingError::DuplicateEntryIndex { index: entry.index });
1005 }
1006 if !paths.insert(entry.rel_path.clone()) {
1007 return Err(ChannelBondingError::DuplicateEntryPath {
1008 rel_path: entry.rel_path.clone(),
1009 });
1010 }
1011 validate_hex_32("content_id_hex", Some(entry.index), &entry.content_id_hex)?;
1012 validate_hex_32("sha256_hex", Some(entry.index), &entry.sha256_hex)?;
1013 }
1014 Ok(())
1015}
1016
1017fn validate_hex_32(
1018 field: &'static str,
1019 index: Option<u32>,
1020 value: &str,
1021) -> Result<(), ChannelBondingError> {
1022 if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
1023 return Err(ChannelBondingError::InvalidHexDigest {
1024 field,
1025 index,
1026 value: value.to_string(),
1027 });
1028 }
1029 Ok(())
1030}
1031
1032fn descriptor_entries_by_index(
1033 entries: &[BondDescriptorEntry],
1034) -> Result<BTreeMap<u32, &BondDescriptorEntry>, ChannelBondingError> {
1035 let mut by_index = BTreeMap::new();
1036 for entry in entries {
1037 if by_index.insert(entry.index, entry).is_some() {
1038 return Err(ChannelBondingError::DuplicateEntryIndex { index: entry.index });
1039 }
1040 }
1041 Ok(by_index)
1042}
1043
1044fn proof_entries_by_index(
1045 entries: &[BondDonorProofEntry],
1046) -> Result<BTreeMap<u32, &BondDonorProofEntry>, ChannelBondingError> {
1047 let mut by_index = BTreeMap::new();
1048 for entry in entries {
1049 if by_index.insert(entry.index, entry).is_some() {
1050 return Err(ChannelBondingError::DuplicateEntryIndex { index: entry.index });
1051 }
1052 }
1053 Ok(by_index)
1054}
1055
1056fn compare_entry_identity(
1057 descriptor: &BondDescriptorEntry,
1058 proof: &BondDonorProofEntry,
1059) -> Result<(), ChannelBondingError> {
1060 if descriptor.rel_path != proof.rel_path {
1061 return Err(ChannelBondingError::EntryMismatch {
1062 index: descriptor.index,
1063 field: "rel_path",
1064 expected: descriptor.rel_path.clone(),
1065 actual: proof.rel_path.clone(),
1066 });
1067 }
1068 if descriptor.size != proof.size {
1069 return Err(ChannelBondingError::EntryMismatch {
1070 index: descriptor.index,
1071 field: "size",
1072 expected: descriptor.size.to_string(),
1073 actual: proof.size.to_string(),
1074 });
1075 }
1076 if descriptor.sha256_hex != proof.sha256_hex {
1077 return Err(ChannelBondingError::EntryMismatch {
1078 index: descriptor.index,
1079 field: "sha256_hex",
1080 expected: descriptor.sha256_hex.clone(),
1081 actual: proof.sha256_hex.clone(),
1082 });
1083 }
1084 Ok(())
1085}
1086
1087fn total_bytes_for_descriptor(entries: &[BondDescriptorEntry]) -> Result<u64, ChannelBondingError> {
1088 entries.iter().try_fold(0u64, |acc, entry| {
1089 acc.checked_add(entry.size)
1090 .ok_or(ChannelBondingError::TotalBytesOverflow)
1091 })
1092}
1093
1094fn total_bytes_for_proof(entries: &[BondDonorProofEntry]) -> Result<u64, ChannelBondingError> {
1095 entries.iter().try_fold(0u64, |acc, entry| {
1096 acc.checked_add(entry.size)
1097 .ok_or(ChannelBondingError::TotalBytesOverflow)
1098 })
1099}
1100
1101fn merkle_root_for_proof(entries: &[BondDonorProofEntry]) -> Result<String, ChannelBondingError> {
1102 let digests = entries
1103 .iter()
1104 .map(|entry| {
1105 let content_id = ContentId::new(parse_hex_32(
1106 "content_id_hex",
1107 Some(entry.index),
1108 &entry.content_id_hex,
1109 )?);
1110 Ok(EntryDigest {
1111 rel_path: entry.rel_path.clone(),
1112 size: entry.size,
1113 content_id: AtpObjectId::content(content_id),
1114 content_sha256: parse_hex_32("sha256_hex", Some(entry.index), &entry.sha256_hex)?,
1115 })
1116 })
1117 .collect::<Result<Vec<_>, ChannelBondingError>>()?;
1118 Ok(flat_merkle_root_from_digests(&digests))
1119}
1120
1121fn parse_hex_32(
1122 field: &'static str,
1123 index: Option<u32>,
1124 value: &str,
1125) -> Result<[u8; 32], ChannelBondingError> {
1126 validate_hex_32(field, index, value)?;
1127 let mut out = [0u8; 32];
1128 hex::decode_to_slice(value, &mut out).map_err(|_| ChannelBondingError::InvalidHexDigest {
1129 field,
1130 index,
1131 value: value.to_string(),
1132 })?;
1133 Ok(out)
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138 use super::*;
1139 use crate::security::{AuthenticatedSymbol, SecurityContext};
1140 use crate::types::{Symbol, SymbolId, SymbolKind};
1141
1142 fn sample_proof() -> BondDonorByteProof {
1143 BondDonorByteProof::from_entries(vec![
1144 BondDonorProofEntry::from_bytes(0, "alpha.txt", b"alpha"),
1145 BondDonorProofEntry::from_bytes(1, "dir/beta.txt", b"beta"),
1146 ])
1147 .expect("valid proof")
1148 }
1149
1150 #[test]
1151 fn donor_esi_partition_covers_without_overlap() {
1152 for donor_count in [1, 2, 3, 5, 8] {
1153 let limit = 257u32;
1154 let mut owners = vec![0u32; limit as usize];
1155 for donor_index in 0..donor_count {
1156 for esi in 0..limit {
1157 if owns_esi(donor_index, donor_count, esi) {
1158 owners[esi as usize] += 1;
1159 }
1160 }
1161 let stream = DonorEsiStream::new(donor_index, donor_count).expect("valid stream");
1162 for (seq, esi) in stream.take(32).enumerate() {
1163 assert_eq!(
1164 esi,
1165 esi_for_donor(donor_index, donor_count, seq as u32).expect("esi")
1166 );
1167 assert!(owns_esi(donor_index, donor_count, esi));
1168 }
1169 }
1170 assert!(owners.iter().all(|owner_count| *owner_count == 1));
1171 }
1172 }
1173
1174 #[test]
1175 fn n_one_owns_every_esi() {
1176 for esi in 0..512 {
1177 assert!(owns_esi(0, 1, esi));
1178 assert_eq!(esi_for_donor(0, 1, esi).expect("esi"), esi);
1179 }
1180 }
1181
1182 #[test]
1183 fn donor_esi_partition_rejects_invalid_or_overflowing_inputs() {
1184 assert_eq!(
1185 DonorEsiStream::new(0, 0).unwrap_err(),
1186 ChannelBondingError::InvalidDonorCount { donor_count: 0 }
1187 );
1188 assert_eq!(
1189 esi_for_donor(2, 2, 0).unwrap_err(),
1190 ChannelBondingError::InvalidDonorIndex {
1191 donor_index: 2,
1192 donor_count: 2,
1193 }
1194 );
1195 assert_eq!(
1196 esi_for_donor(u32::MAX - 1, u32::MAX, 1).unwrap_err(),
1197 ChannelBondingError::EsiOverflow
1198 );
1199 }
1200
1201 #[test]
1202 fn donor_esi_stream_yields_final_u32_esi_once() {
1203 let mut stream = DonorEsiStream::new(0, 1).expect("single donor stream");
1204 stream.next_seq = Some(u32::MAX);
1205
1206 assert_eq!(stream.next(), Some(u32::MAX));
1207 assert_eq!(stream.next(), None);
1208 assert_eq!(stream.next(), None);
1209 }
1210
1211 #[test]
1212 fn descriptor_accepts_matching_donor_proof() {
1213 let proof = sample_proof();
1214 let descriptor = BondTransferDescriptor::from_donor_proof(
1215 "sample",
1216 true,
1217 1200,
1218 256 * 1024,
1219 Some("env:ATP_BOND_AUTH_KEY".to_string()),
1220 proof.entries.clone(),
1221 )
1222 .expect("descriptor");
1223
1224 descriptor
1225 .verify_donor_byte_match(&proof)
1226 .expect("matching donor proof accepted");
1227 }
1228
1229 #[test]
1230 fn descriptor_rejects_content_mismatch_fail_closed() {
1231 let proof = sample_proof();
1232 let descriptor = BondTransferDescriptor::from_donor_proof(
1233 "sample",
1234 true,
1235 1200,
1236 256 * 1024,
1237 None,
1238 proof.entries.clone(),
1239 )
1240 .expect("descriptor");
1241 let tampered = BondDonorByteProof::from_entries(vec![
1242 BondDonorProofEntry::from_bytes(0, "alpha.txt", b"alpha"),
1243 BondDonorProofEntry::from_bytes(1, "dir/beta.txt", b"tampered"),
1244 ])
1245 .expect("tampered proof is internally consistent");
1246
1247 let err = descriptor
1248 .verify_donor_byte_match(&tampered)
1249 .expect_err("mismatch rejected");
1250 assert!(matches!(
1251 err,
1252 ChannelBondingError::TransferIdMismatch { .. }
1253 | ChannelBondingError::MerkleRootMismatch { .. }
1254 | ChannelBondingError::EntryMismatch { .. }
1255 ));
1256 }
1257
1258 #[test]
1259 fn descriptor_rejects_same_sha_with_wrong_content_id_merkle() {
1260 let proof = sample_proof();
1261 let descriptor = BondTransferDescriptor::from_donor_proof(
1262 "sample",
1263 true,
1264 1200,
1265 256 * 1024,
1266 None,
1267 proof.entries.clone(),
1268 )
1269 .expect("descriptor");
1270 let mut forged_entries = proof.entries.clone();
1271 forged_entries[0].content_id_hex = "11".repeat(32);
1272 let forged = BondDonorByteProof {
1273 transfer_id: proof.transfer_id,
1274 total_bytes: proof.total_bytes,
1275 merkle_root_hex: proof.merkle_root_hex,
1276 entries: forged_entries,
1277 };
1278
1279 let err = descriptor
1280 .verify_donor_byte_match(&forged)
1281 .expect_err("wrong content id rejected");
1282 assert!(matches!(
1283 err,
1284 ChannelBondingError::MerkleRootMismatch { .. }
1285 ));
1286 }
1287
1288 #[test]
1289 fn entry_object_id_is_deterministic_and_entry_specific() {
1290 let transfer = "0123456789abcdef0123456789abcdef";
1291 let first = entry_object_id(transfer, 0);
1292 assert_eq!(first, entry_object_id(transfer, 0));
1293 assert_ne!(first, entry_object_id(transfer, 1));
1294 assert_ne!(first, entry_object_id("different", 0));
1295 }
1296
1297 #[test]
1298 fn assignment_windows_restrict_static_residue() {
1299 let assignment = DonorAssignment {
1300 donor_index: 1,
1301 donor_count: 3,
1302 esi_windows: Some(vec![EsiWindow::new(0, 10)]),
1303 receiver_udp_endpoints: Vec::new(),
1304 auth_key_ref: Some("keyfile:/run/asupersync/bond.key".to_string()),
1305 };
1306 assignment.validate().expect("valid assignment");
1307 assert!(assignment.owns_esi(1));
1308 assert!(assignment.owns_esi(4));
1309 assert!(!assignment.owns_esi(10));
1310 assert!(!assignment.owns_esi(2));
1311 }
1312
1313 #[test]
1314 fn assignment_windows_are_exact_static_residue_intersections() {
1315 let windows = vec![EsiWindow::new(2, 11), EsiWindow::new(15, 22)];
1316 for donor_count in [2, 3, 5, 8] {
1317 for donor_index in 0..donor_count {
1318 let assignment = DonorAssignment {
1319 donor_index,
1320 donor_count,
1321 esi_windows: Some(windows.clone()),
1322 receiver_udp_endpoints: Vec::new(),
1323 auth_key_ref: None,
1324 };
1325 assignment.validate().expect("valid assignment");
1326
1327 for esi in 0..25 {
1328 let expected = owns_esi(donor_index, donor_count, esi)
1329 && windows.iter().any(|window| window.contains(esi));
1330 assert_eq!(
1331 assignment.owns_esi(esi),
1332 expected,
1333 "donor {donor_index}/{donor_count} ownership mismatch for esi {esi}"
1334 );
1335 }
1336 }
1337 }
1338 }
1339
1340 #[test]
1341 fn assignment_validation_rejects_invalid_window_and_donor_ceiling() {
1342 let invalid_window = DonorAssignment {
1343 donor_index: 0,
1344 donor_count: 1,
1345 esi_windows: Some(vec![EsiWindow::new(5, 5)]),
1346 receiver_udp_endpoints: Vec::new(),
1347 auth_key_ref: None,
1348 };
1349 assert_eq!(
1350 invalid_window.validate().unwrap_err(),
1351 ChannelBondingError::InvalidEsiWindow {
1352 start_inclusive: 5,
1353 end_exclusive: 5,
1354 }
1355 );
1356
1357 let too_many_donors = DonorAssignment {
1358 donor_index: 0,
1359 donor_count: MAX_STATIC_RESIDUE_DONORS + 1,
1360 esi_windows: None,
1361 receiver_udp_endpoints: Vec::new(),
1362 auth_key_ref: None,
1363 };
1364 assert_eq!(
1365 too_many_donors.validate().unwrap_err(),
1366 ChannelBondingError::TooManyDonors {
1367 donor_count: MAX_STATIC_RESIDUE_DONORS + 1,
1368 max: MAX_STATIC_RESIDUE_DONORS,
1369 }
1370 );
1371 }
1372
1373 #[test]
1374 fn handshake_degrades_to_static_residue_when_dynamic_missing() {
1375 let receiver = BondingHandshake::v1_static(
1376 [BondTransport::DirectIp, BondTransport::Tailscale],
1377 16,
1378 true,
1379 )
1380 .with_dynamic_windows(true)
1381 .with_resume(true)
1382 .with_extension_capability("phase-e.dynamic-window-v1");
1383 let donor = BondingHandshake::v1_static([BondTransport::Tailscale], 8, false);
1384
1385 let agreement = receiver.negotiate(&donor).expect("compatible");
1386 assert_eq!(agreement.protocol_version, BONDING_PROTOCOL_VERSION);
1387 assert_eq!(
1388 agreement.assignment_mode,
1389 BondingAssignmentMode::StaticResidue
1390 );
1391 assert!(!agreement.resume_supported);
1392 assert!(agreement.auth_required);
1393 assert_eq!(agreement.max_donor_count, 8);
1394 assert_eq!(
1395 agreement.supported_transports,
1396 BTreeSet::from([BondTransport::Tailscale])
1397 );
1398 assert!(agreement.supports_transport(BondTransport::Tailscale));
1399 assert!(!agreement.supports_transport(BondTransport::DirectIp));
1400 }
1401
1402 #[test]
1403 fn handshake_selects_dynamic_windows_when_both_peers_support_them() {
1404 let receiver = BondingHandshake::v1_static([BondTransport::DirectIp], 16, true)
1405 .with_dynamic_windows(true)
1406 .with_resume(true)
1407 .with_extension_capability("receiver-private-future");
1408 let donor = BondingHandshake::v1_static([BondTransport::DirectIp], 12, true)
1409 .with_dynamic_windows(true)
1410 .with_resume(true)
1411 .with_extension_capability("donor-private-future");
1412
1413 let agreement = receiver.negotiate(&donor).expect("compatible");
1414
1415 assert_eq!(
1416 agreement.assignment_mode,
1417 BondingAssignmentMode::DynamicWindows
1418 );
1419 assert!(agreement.resume_supported);
1420 assert_eq!(agreement.max_donor_count, 12);
1421 assert!(agreement.supports_transport(BondTransport::DirectIp));
1422 }
1423
1424 #[test]
1425 fn handshake_ignores_unknown_extensions_but_refuses_no_common_transport() {
1426 let receiver = BondingHandshake::v1_static([BondTransport::DirectIp], 16, true)
1427 .with_extension_capability("unknown.receiver.future");
1428 let donor = BondingHandshake::v1_static([BondTransport::Tailscale], 16, true)
1429 .with_extension_capability("unknown.donor.future");
1430
1431 let err = receiver.negotiate(&donor).expect_err("no common transport");
1432 assert_eq!(err, ChannelBondingError::NoCommonTransport);
1433 }
1434
1435 #[test]
1436 fn handshake_refuses_incompatible_versions() {
1437 let receiver = BondingHandshake::v1_static([BondTransport::DirectIp], 16, true)
1438 .with_protocol_range(2, 2);
1439 let donor = BondingHandshake::v1_static([BondTransport::DirectIp], 16, true);
1440
1441 let err = receiver.negotiate(&donor).expect_err("version mismatch");
1442 assert!(matches!(
1443 err,
1444 ChannelBondingError::IncompatibleProtocolVersion { .. }
1445 ));
1446 }
1447
1448 #[test]
1449 fn handshake_refuses_invalid_version_zero_and_donor_ceiling() {
1450 let donor = BondingHandshake::v1_static([BondTransport::DirectIp], 16, true);
1451 let version_zero = BondingHandshake::v1_static([BondTransport::DirectIp], 16, true)
1452 .with_protocol_range(0, BONDING_PROTOCOL_VERSION);
1453 let err = version_zero.negotiate(&donor).expect_err("version zero");
1454 assert_eq!(
1455 err,
1456 ChannelBondingError::InvalidProtocolRange {
1457 min: 0,
1458 max: BONDING_PROTOCOL_VERSION,
1459 }
1460 );
1461
1462 let zero_donors = BondingHandshake::v1_static([BondTransport::DirectIp], 0, true);
1463 let err = zero_donors.negotiate(&donor).expect_err("zero donors");
1464 assert_eq!(
1465 err,
1466 ChannelBondingError::InvalidDonorCount { donor_count: 0 }
1467 );
1468
1469 let too_many_donors = BondingHandshake::v1_static(
1470 [BondTransport::DirectIp],
1471 MAX_STATIC_RESIDUE_DONORS + 1,
1472 true,
1473 );
1474 let err = too_many_donors
1475 .negotiate(&donor)
1476 .expect_err("donor ceiling exceeds phase-a cap");
1477 assert_eq!(
1478 err,
1479 ChannelBondingError::TooManyDonors {
1480 donor_count: MAX_STATIC_RESIDUE_DONORS + 1,
1481 max: MAX_STATIC_RESIDUE_DONORS,
1482 }
1483 );
1484 }
1485
1486 #[test]
1487 fn security_model_rejects_argv_and_disabled_fail_closed_layers() {
1488 let mut model = BondingSecurityModel {
1489 auth_key: BondAuthKeyRef {
1490 key_id: "bond-key-1".to_string(),
1491 control_plane: BondAuthControlPlane::Ssh,
1492 location: BondAuthKeyLocation::EnvVar("ATP_BOND_AUTH_KEY".to_string()),
1493 },
1494 auth_before_decode: true,
1495 fail_closed_on_merkle_mismatch: true,
1496 };
1497 model.validate().expect("valid security model");
1498
1499 model.auth_key.location = BondAuthKeyLocation::Argv("--bond-key=secret".to_string());
1500 assert_eq!(
1501 model.validate().unwrap_err(),
1502 ChannelBondingError::InsecureAuthKeyDelivery
1503 );
1504
1505 model.auth_key.location = BondAuthKeyLocation::EnvVar("ATP_BOND_AUTH_KEY".to_string());
1506 model.auth_before_decode = false;
1507 assert_eq!(
1508 model.validate().unwrap_err(),
1509 ChannelBondingError::AuthBeforeDecodeDisabled
1510 );
1511
1512 model.auth_before_decode = true;
1513 model.fail_closed_on_merkle_mismatch = false;
1514 assert_eq!(
1515 model.validate().unwrap_err(),
1516 ChannelBondingError::MerkleFailClosedDisabled
1517 );
1518 }
1519
1520 #[test]
1521 fn wrong_donor_auth_key_rejects_symbol_before_decode() {
1522 let signer = SecurityContext::for_testing(0xA11CE);
1523 let verifier = SecurityContext::for_testing(0xB0B);
1524 let symbol = Symbol::new(
1525 SymbolId::new_for_test(7, 0, 3),
1526 b"bonded repair".to_vec(),
1527 SymbolKind::Repair,
1528 );
1529 let signed = signer.sign_symbol(&symbol);
1530 let tag = *signed.tag();
1531 let mut received = AuthenticatedSymbol::from_parts(signed.into_symbol(), tag);
1532
1533 let err = verifier
1534 .verify_authenticated_symbol(&mut received)
1535 .expect_err("wrong shared donor key must reject symbol");
1536 assert!(err.is_invalid_tag());
1537 assert!(!received.is_verified());
1538 }
1539}