1#![allow(clippy::let_and_return)]
2use std::collections::{HashMap, VecDeque};
13
14use crate::encoding::STANDARD;
15use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18
19use crate::canonicalize;
20use crate::generated::offline_revocation_list::{
21 OfflineRevocationList, OfflineRevocationList_ListVersion, RevokedEntry, RevokedEntry_Kind,
22};
23use crate::packet::Packet;
24
25#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "kebab-case")]
31pub enum PacketRejectReason {
32 Replay,
33 Expired,
34 FutureDated,
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
38#[serde(tag = "kind", rename_all = "kebab-case")]
39pub enum PacketReceiverDecision {
40 Accept,
41 Reject { reason: PacketRejectReason },
42}
43
44#[derive(Debug)]
45pub struct PacketReceiver {
46 seen: VecDeque<(String, String)>,
49 index: HashMap<String, ()>,
51 window_size: usize,
52}
53
54impl PacketReceiver {
55 pub fn new(window_size: Option<usize>) -> Self {
56 let window_size = window_size.unwrap_or(4096);
57 PacketReceiver {
58 seen: VecDeque::with_capacity(window_size),
59 index: HashMap::with_capacity(window_size),
60 window_size,
61 }
62 }
63
64 pub fn observe(&mut self, packet: &Packet, now: &str) -> PacketReceiverDecision {
67 if let Some(exp) = packet.expires_at.as_deref() {
68 if exp < now {
69 return PacketReceiverDecision::Reject {
70 reason: PacketRejectReason::Expired,
71 };
72 }
73 }
74 if packet.created_at.as_str() > now {
75 return PacketReceiverDecision::Reject {
76 reason: PacketRejectReason::FutureDated,
77 };
78 }
79 if self.index.contains_key(&packet.packet_id) {
80 return PacketReceiverDecision::Reject {
81 reason: PacketRejectReason::Replay,
82 };
83 }
84 if self.seen.len() >= self.window_size {
85 if let Some((oldest, _)) = self.seen.pop_front() {
86 self.index.remove(&oldest);
87 }
88 }
89 self.seen.push_back((
90 packet.packet_id.clone(),
91 packet.expires_at.clone().unwrap_or_default(),
92 ));
93 self.index.insert(packet.packet_id.clone(), ());
94 PacketReceiverDecision::Accept
95 }
96
97 pub fn expire_older_than(&mut self, before: &str) -> usize {
101 let mut removed = 0usize;
102 let mut survivors: VecDeque<(String, String)> = VecDeque::with_capacity(self.seen.len());
107 let mut new_index: HashMap<String, ()> = HashMap::with_capacity(self.seen.len());
108 for entry in self.seen.drain(..) {
109 let drop = !entry.1.is_empty() && entry.1.as_str() < before;
110 if drop {
111 removed += 1;
112 } else {
113 new_index.insert(entry.0.clone(), ());
114 survivors.push_back(entry);
115 }
116 }
117 self.seen = survivors;
118 self.index = new_index;
119 removed
120 }
121
122 pub fn size(&self) -> usize {
123 self.seen.len()
124 }
125}
126
127#[derive(Debug, thiserror::Error)]
132pub enum OrlError {
133 #[error("offline revocation list version unsupported")]
134 UnsupportedVersion,
135 #[error("offline revocation list expired at {0}")]
136 Expired(String),
137 #[error("offline revocation list dated in the future: {0}")]
138 FutureDated(String),
139 #[error("offline revocation list signature did not verify")]
140 BadSignature,
141 #[error("signature decode: {0}")]
142 SignatureDecode(String),
143 #[error("verifying key: {0}")]
144 VerifyingKey(String),
145 #[error("canonicalize: {0}")]
146 Canonicalize(String),
147}
148
149#[derive(Debug)]
150pub struct OfflineRevocationListRuntime {
151 list: OfflineRevocationList,
152 index: HashMap<String, RevokedEntry>,
156}
157
158fn revoked_entry_kind_str(k: &RevokedEntry_Kind) -> &'static str {
159 match k {
160 RevokedEntry_Kind::Actor => "actor",
161 RevokedEntry_Kind::Instance => "instance",
162 RevokedEntry_Kind::Capability => "capability",
163 RevokedEntry_Kind::Delegation => "delegation",
164 RevokedEntry_Kind::Key => "key",
165 }
166}
167
168impl OfflineRevocationListRuntime {
169 pub fn load(
173 list: OfflineRevocationList,
174 issuer_public_key: &[u8; 32],
175 now: &str,
176 ) -> Result<Self, OrlError> {
177 if list.list_version != OfflineRevocationList_ListVersion::V1 {
178 return Err(OrlError::UnsupportedVersion);
179 }
180 if list.valid_until.as_str() < now {
181 return Err(OrlError::Expired(list.valid_until.clone()));
182 }
183 if list.issued_at.as_str() > now {
184 return Err(OrlError::FutureDated(list.issued_at.clone()));
185 }
186 if !verify_offline_revocation_list_signature(&list, issuer_public_key)? {
187 return Err(OrlError::BadSignature);
188 }
189 let mut index: HashMap<String, RevokedEntry> = HashMap::new();
190 for e in &list.revoked {
191 index.insert(
192 format!("{}:{}", revoked_entry_kind_str(&e.kind), e.id),
193 e.clone(),
194 );
195 }
196 Ok(OfflineRevocationListRuntime { list, index })
197 }
198
199 pub fn is_revoked(&self, kind: &RevokedEntry_Kind, id: &str) -> Option<&RevokedEntry> {
201 self.index
202 .get(&format!("{}:{}", revoked_entry_kind_str(kind), id))
203 }
204
205 pub fn metadata(&self) -> OrlMetadata<'_> {
206 OrlMetadata {
207 issuer: &self.list.issuer,
208 trust_domain: &self.list.trust_domain,
209 issued_at: &self.list.issued_at,
210 valid_until: &self.list.valid_until,
211 }
212 }
213}
214
215#[derive(Debug)]
216pub struct OrlMetadata<'a> {
217 pub issuer: &'a str,
218 pub trust_domain: &'a str,
219 pub issued_at: &'a str,
220 pub valid_until: &'a str,
221}
222
223pub fn verify_offline_revocation_list_signature(
224 list: &OfflineRevocationList,
225 public_key: &[u8; 32],
226) -> Result<bool, OrlError> {
227 let mut value = serde_json::to_value(list).unwrap_or(serde_json::Value::Null);
228 if let serde_json::Value::Object(map) = &mut value {
229 map.remove("signature");
230 }
231 let canonical = canonicalize(&value).map_err(|e| OrlError::Canonicalize(e.to_string()))?;
232 let sig_bytes = STANDARD
233 .decode(&list.signature.signature)
234 .map_err(|e| OrlError::SignatureDecode(e.to_string()))?;
235 let sig = match Signature::from_slice(&sig_bytes) {
236 Ok(s) => s,
237 Err(_) => return Ok(false),
238 };
239 let vk =
240 VerifyingKey::from_bytes(public_key).map_err(|e| OrlError::VerifyingKey(e.to_string()))?;
241 Ok(vk.verify(canonical.as_bytes(), &sig).is_ok())
242}
243
244pub fn sign_offline_revocation_list(
245 mut list: OfflineRevocationList,
246 private_key: &[u8; 32],
247) -> Result<OfflineRevocationList, OrlError> {
248 list.signature = crate::generated::common::SignatureEnvelope {
250 algorithm: list.signature.algorithm.clone(),
251 signer: list.signature.signer.clone(),
252 signature: String::new(),
253 hash_alg: None,
254 alt_algorithm: None,
255 alt_signature: None,
256 };
257 let mut value = serde_json::to_value(&list).unwrap_or(serde_json::Value::Null);
258 if let serde_json::Value::Object(map) = &mut value {
259 map.remove("signature");
260 }
261 let canonical = canonicalize(&value).map_err(|e| OrlError::Canonicalize(e.to_string()))?;
262 let signing = SigningKey::from_bytes(private_key);
263 let sig: Signature = signing.sign(canonical.as_bytes());
264 list.signature.signature = STANDARD.encode(sig.to_bytes());
265 Ok(list)
266}
267
268#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
273pub struct DeliveryReceipt {
274 pub receipt_version: String,
275 pub packet_id: String,
276 pub packet_hash: String,
279 pub receiver: String,
280 pub received_at: String,
281 pub signature: ReceiptSignature,
282}
283
284#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
285pub struct ReceiptSignature {
286 pub algorithm: String,
287 pub signer: String,
288 pub signature: String,
289}
290
291#[derive(Debug)]
292pub struct VerifyResult {
293 pub ok: bool,
294 pub reason: Option<String>,
295}
296
297fn sha256_hashref(bytes: &[u8]) -> String {
298 let digest = Sha256::digest(bytes);
299 let hex: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
300 format!("sha256:{}", hex)
301}
302
303pub fn sign_delivery_receipt(
304 packet: &Packet,
305 receiver: &str,
306 received_at: &str,
307 private_key: &[u8; 32],
308) -> Result<DeliveryReceipt, OrlError> {
309 let canonical_packet =
310 canonicalize(&serde_json::to_value(packet).unwrap_or(serde_json::Value::Null))
311 .map_err(|e| OrlError::Canonicalize(e.to_string()))?;
312 let packet_hash = sha256_hashref(canonical_packet.as_bytes());
313 let mut draft = DeliveryReceipt {
314 receipt_version: "1".into(),
315 packet_id: packet.packet_id.clone(),
316 packet_hash,
317 receiver: receiver.into(),
318 received_at: received_at.into(),
319 signature: ReceiptSignature {
320 algorithm: "ed25519".into(),
321 signer: receiver.into(),
322 signature: String::new(),
323 },
324 };
325 let mut sig_value =
326 serde_json::to_value(&draft).map_err(|e| OrlError::Canonicalize(e.to_string()))?;
327 if let serde_json::Value::Object(map) = &mut sig_value {
328 map.remove("signature");
329 }
330 let sig_canonical =
331 canonicalize(&sig_value).map_err(|e| OrlError::Canonicalize(e.to_string()))?;
332 let signing = SigningKey::from_bytes(private_key);
333 let sig: Signature = signing.sign(sig_canonical.as_bytes());
334 draft.signature.signature = STANDARD.encode(sig.to_bytes());
335 Ok(draft)
336}
337
338pub fn verify_delivery_receipt(
339 receipt: &DeliveryReceipt,
340 packet: &Packet,
341 receiver_public_key: &[u8; 32],
342) -> VerifyResult {
343 if receipt.receipt_version != "1" {
344 return reject(format!(
345 "receipt_version {} unsupported",
346 receipt.receipt_version
347 ));
348 }
349 if receipt.packet_id != packet.packet_id {
350 return reject("packet_id mismatch".into());
351 }
352 let canonical_packet = match canonicalize(&serde_json::to_value(packet).unwrap_or_default()) {
353 Ok(c) => c,
354 Err(e) => return reject(format!("canonicalize: {}", e)),
355 };
356 let expected = sha256_hashref(canonical_packet.as_bytes());
357 if expected != receipt.packet_hash {
358 return reject("packet_hash mismatch".into());
359 }
360 if receipt.signature.signer != receipt.receiver {
361 return reject("receipt signer != receiver".into());
362 }
363 let mut sig_value = match serde_json::to_value(receipt) {
364 Ok(v) => v,
365 Err(e) => return reject(format!("serde: {}", e)),
366 };
367 if let serde_json::Value::Object(map) = &mut sig_value {
368 map.remove("signature");
369 }
370 let sig_canonical = match canonicalize(&sig_value) {
371 Ok(c) => c,
372 Err(e) => return reject(format!("canonicalize: {}", e)),
373 };
374 let sig_bytes = match STANDARD.decode(&receipt.signature.signature) {
375 Ok(b) => b,
376 Err(e) => return reject(format!("signature base64: {}", e)),
377 };
378 let sig = match Signature::from_slice(&sig_bytes) {
379 Ok(s) => s,
380 Err(e) => return reject(format!("signature parse: {}", e)),
381 };
382 let vk = match VerifyingKey::from_bytes(receiver_public_key) {
383 Ok(v) => v,
384 Err(e) => return reject(format!("verifying key: {}", e)),
385 };
386 if vk.verify(sig_canonical.as_bytes(), &sig).is_err() {
387 return reject("receipt signature did not verify".into());
388 }
389 VerifyResult {
390 ok: true,
391 reason: None,
392 }
393}
394
395#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
400pub struct ProofOfForwarding {
401 pub proof_version: String,
402 pub packet_id: String,
403 pub packet_hash: String,
404 pub relay: String,
405 pub forwarded_at: String,
406 pub hop_count: u32,
407 pub signature: ReceiptSignature,
408}
409
410pub fn sign_proof_of_forwarding(
411 packet: &Packet,
412 relay: &str,
413 forwarded_at: &str,
414 hop_count: u32,
415 private_key: &[u8; 32],
416) -> Result<ProofOfForwarding, OrlError> {
417 let canonical_packet =
418 canonicalize(&serde_json::to_value(packet).unwrap_or(serde_json::Value::Null))
419 .map_err(|e| OrlError::Canonicalize(e.to_string()))?;
420 let packet_hash = sha256_hashref(canonical_packet.as_bytes());
421 let mut draft = ProofOfForwarding {
422 proof_version: "1".into(),
423 packet_id: packet.packet_id.clone(),
424 packet_hash,
425 relay: relay.into(),
426 forwarded_at: forwarded_at.into(),
427 hop_count,
428 signature: ReceiptSignature {
429 algorithm: "ed25519".into(),
430 signer: relay.into(),
431 signature: String::new(),
432 },
433 };
434 let mut sig_value =
435 serde_json::to_value(&draft).map_err(|e| OrlError::Canonicalize(e.to_string()))?;
436 if let serde_json::Value::Object(map) = &mut sig_value {
437 map.remove("signature");
438 }
439 let sig_canonical =
440 canonicalize(&sig_value).map_err(|e| OrlError::Canonicalize(e.to_string()))?;
441 let signing = SigningKey::from_bytes(private_key);
442 let sig: Signature = signing.sign(sig_canonical.as_bytes());
443 draft.signature.signature = STANDARD.encode(sig.to_bytes());
444 Ok(draft)
445}
446
447pub fn verify_proof_of_forwarding(
448 proof: &ProofOfForwarding,
449 packet: &Packet,
450 relay_public_key: &[u8; 32],
451) -> VerifyResult {
452 if proof.proof_version != "1" {
453 return reject(format!("proof_version {} unsupported", proof.proof_version));
454 }
455 if proof.packet_id != packet.packet_id {
456 return reject("packet_id mismatch".into());
457 }
458 let canonical_packet = match canonicalize(&serde_json::to_value(packet).unwrap_or_default()) {
459 Ok(c) => c,
460 Err(e) => return reject(format!("canonicalize: {}", e)),
461 };
462 let expected = sha256_hashref(canonical_packet.as_bytes());
463 if expected != proof.packet_hash {
464 return reject("packet_hash mismatch".into());
465 }
466 if proof.signature.signer != proof.relay {
467 return reject("proof signer != relay".into());
468 }
469 let mut sig_value = match serde_json::to_value(proof) {
470 Ok(v) => v,
471 Err(e) => return reject(format!("serde: {}", e)),
472 };
473 if let serde_json::Value::Object(map) = &mut sig_value {
474 map.remove("signature");
475 }
476 let sig_canonical = match canonicalize(&sig_value) {
477 Ok(c) => c,
478 Err(e) => return reject(format!("canonicalize: {}", e)),
479 };
480 let sig_bytes = match STANDARD.decode(&proof.signature.signature) {
481 Ok(b) => b,
482 Err(e) => return reject(format!("signature base64: {}", e)),
483 };
484 let sig = match Signature::from_slice(&sig_bytes) {
485 Ok(s) => s,
486 Err(e) => return reject(format!("signature parse: {}", e)),
487 };
488 let vk = match VerifyingKey::from_bytes(relay_public_key) {
489 Ok(v) => v,
490 Err(e) => return reject(format!("verifying key: {}", e)),
491 };
492 if vk.verify(sig_canonical.as_bytes(), &sig).is_err() {
493 return reject("forwarding signature did not verify".into());
494 }
495 VerifyResult {
496 ok: true,
497 reason: None,
498 }
499}
500
501fn reject(reason: String) -> VerifyResult {
502 VerifyResult {
503 ok: false,
504 reason: Some(reason),
505 }
506}
507
508#[cfg(test)]
513mod tests {
514 use super::*;
515 use crate::crypto::Ed25519Signer;
516 use crate::packet::{sign_packet, SignPacketArgs};
517 use rand::rngs::OsRng;
518 use rand::RngCore;
519
520 fn fresh_seed() -> [u8; 32] {
521 let mut seed = [0u8; 32];
522 OsRng.fill_bytes(&mut seed);
523 seed
524 }
525
526 fn fixture_packet(packet_id: &str, expires_at: Option<&str>, created_at: &str) -> Packet {
527 let signer_seed = fresh_seed();
528 sign_packet(SignPacketArgs {
529 packet_id: packet_id.into(),
530 source: "tf:actor:agent:example.com/x".into(),
531 destination: "tf:actor:service:example.com/d".into(),
532 priority: "P3".into(),
533 payload: b"hi",
534 encoding: None,
535 compression: None,
536 emergency: false,
537 expires_at: expires_at.map(str::to_string),
538 ttl_hops: None,
539 route_constraints: None,
540 session_ref: None,
541 private_key: signer_seed,
542 signer: "tf:actor:agent:example.com/x".into(),
543 created_at: Some(created_at.into()),
544 })
545 .expect("sign")
546 }
547
548 #[test]
549 fn packet_receiver_accepts_then_rejects_replay() {
550 let mut recv = PacketReceiver::new(None);
551 let p = fixture_packet("pkt-A", None, "2026-04-24T12:00:00Z");
552 assert_eq!(
553 recv.observe(&p, "2026-04-24T13:00:00Z"),
554 PacketReceiverDecision::Accept
555 );
556 assert!(matches!(
557 recv.observe(&p, "2026-04-24T13:00:00Z"),
558 PacketReceiverDecision::Reject {
559 reason: PacketRejectReason::Replay
560 }
561 ));
562 }
563
564 #[test]
565 fn packet_receiver_rejects_expired() {
566 let mut recv = PacketReceiver::new(None);
567 let p = fixture_packet(
568 "pkt-old",
569 Some("2026-04-23T00:00:00Z"),
570 "2026-04-22T00:00:00Z",
571 );
572 assert!(matches!(
573 recv.observe(&p, "2026-04-24T12:00:00Z"),
574 PacketReceiverDecision::Reject {
575 reason: PacketRejectReason::Expired
576 }
577 ));
578 }
579
580 #[test]
581 fn packet_receiver_rejects_future_dated() {
582 let mut recv = PacketReceiver::new(None);
583 let p = fixture_packet("pkt-future", None, "2099-04-24T12:00:00Z");
584 assert!(matches!(
585 recv.observe(&p, "2026-04-24T12:00:00Z"),
586 PacketReceiverDecision::Reject {
587 reason: PacketRejectReason::FutureDated
588 }
589 ));
590 }
591
592 #[test]
593 fn packet_receiver_lru_evicts_oldest() {
594 let mut recv = PacketReceiver::new(Some(2));
595 for i in 0..3 {
596 let p = fixture_packet(&format!("pkt-{}", i), None, "2026-04-24T11:00:00Z");
597 assert_eq!(
598 recv.observe(&p, "2026-04-24T12:00:00Z"),
599 PacketReceiverDecision::Accept
600 );
601 }
602 assert_eq!(recv.size(), 2);
603 }
604
605 #[test]
606 fn orl_runtime_load_and_lookup() {
607 let issuer = Ed25519Signer::from_bytes(&fresh_seed());
608 let issuer_pub = issuer.public_key_bytes();
609 let issuer_priv = {
610 let seed = fresh_seed();
614 seed
615 };
616 let issuer_signer = Ed25519Signer::from_bytes(&issuer_priv);
617 let issuer_pub2 = issuer_signer.public_key_bytes();
618 let draft = OfflineRevocationList {
619 list_version: OfflineRevocationList_ListVersion::V1,
620 trust_domain: "example.com".into(),
621 issued_at: "2026-04-24T00:00:00Z".into(),
622 valid_until: "2026-04-30T00:00:00Z".into(),
623 issuer: "tf:actor:service:example.com/tf-daemon".into(),
624 revoked: vec![
625 RevokedEntry {
626 kind: RevokedEntry_Kind::Actor,
627 id: "tf:actor:agent:example.com/bad".into(),
628 reason: Some("compromised".into()),
629 revoked_at: None,
630 },
631 RevokedEntry {
632 kind: RevokedEntry_Kind::Key,
633 id: "kid-42".into(),
634 reason: None,
635 revoked_at: None,
636 },
637 ],
638 signature: crate::generated::common::SignatureEnvelope {
639 algorithm: "ed25519".to_string(),
640 signer: "tf:actor:service:example.com/tf-daemon".into(),
641 signature: String::new(),
642 hash_alg: None,
643 alt_algorithm: None,
644 alt_signature: None,
645 },
646 };
647 let _ = issuer_pub; let signed = sign_offline_revocation_list(draft, &issuer_priv).expect("sign");
649 let runtime = OfflineRevocationListRuntime::load(
650 signed.clone(),
651 &issuer_pub2,
652 "2026-04-25T00:00:00Z",
653 )
654 .expect("load");
655 assert!(runtime
656 .is_revoked(&RevokedEntry_Kind::Actor, "tf:actor:agent:example.com/bad")
657 .is_some());
658 assert!(runtime
659 .is_revoked(&RevokedEntry_Kind::Key, "kid-42")
660 .is_some());
661 assert!(runtime
662 .is_revoked(&RevokedEntry_Kind::Actor, "tf:actor:agent:example.com/ok")
663 .is_none());
664 }
665
666 #[test]
667 fn orl_runtime_rejects_expired() {
668 let issuer_priv = fresh_seed();
669 let issuer_pub = Ed25519Signer::from_bytes(&issuer_priv).public_key_bytes();
670 let draft = OfflineRevocationList {
671 list_version: OfflineRevocationList_ListVersion::V1,
672 trust_domain: "example.com".into(),
673 issued_at: "2026-04-24T00:00:00Z".into(),
674 valid_until: "2026-04-30T00:00:00Z".into(),
675 issuer: "tf:actor:service:example.com/tf-daemon".into(),
676 revoked: Vec::new(),
677 signature: crate::generated::common::SignatureEnvelope {
678 algorithm: "ed25519".to_string(),
679 signer: "tf:actor:service:example.com/tf-daemon".into(),
680 signature: String::new(),
681 hash_alg: None,
682 alt_algorithm: None,
683 alt_signature: None,
684 },
685 };
686 let signed = sign_offline_revocation_list(draft, &issuer_priv).expect("sign");
687 let r = OfflineRevocationListRuntime::load(signed, &issuer_pub, "2026-05-15T00:00:00Z");
688 assert!(matches!(r, Err(OrlError::Expired(_))));
689 }
690
691 #[test]
692 fn orl_runtime_rejects_forged_signature() {
693 let issuer_priv = fresh_seed();
694 let other_pub = Ed25519Signer::from_bytes(&fresh_seed()).public_key_bytes();
695 let draft = OfflineRevocationList {
696 list_version: OfflineRevocationList_ListVersion::V1,
697 trust_domain: "example.com".into(),
698 issued_at: "2026-04-24T00:00:00Z".into(),
699 valid_until: "2026-04-30T00:00:00Z".into(),
700 issuer: "tf:actor:service:example.com/tf-daemon".into(),
701 revoked: Vec::new(),
702 signature: crate::generated::common::SignatureEnvelope {
703 algorithm: "ed25519".to_string(),
704 signer: "tf:actor:service:example.com/tf-daemon".into(),
705 signature: String::new(),
706 hash_alg: None,
707 alt_algorithm: None,
708 alt_signature: None,
709 },
710 };
711 let signed = sign_offline_revocation_list(draft, &issuer_priv).expect("sign");
712 let r = OfflineRevocationListRuntime::load(signed, &other_pub, "2026-04-25T00:00:00Z");
713 assert!(matches!(r, Err(OrlError::BadSignature)));
714 }
715
716 #[test]
717 fn delivery_receipt_round_trip() {
718 let receiver_priv = fresh_seed();
719 let receiver_pub = Ed25519Signer::from_bytes(&receiver_priv).public_key_bytes();
720 let p = fixture_packet("pkt-deliver-1", None, "2026-04-24T12:00:00Z");
721 let receipt = sign_delivery_receipt(
722 &p,
723 "tf:actor:agent:example.com/receiver",
724 "2026-04-24T12:01:00Z",
725 &receiver_priv,
726 )
727 .expect("sign");
728 let v = verify_delivery_receipt(&receipt, &p, &receiver_pub);
729 assert!(v.ok);
730 }
731
732 #[test]
733 fn delivery_receipt_rejects_packet_mismatch() {
734 let receiver_priv = fresh_seed();
735 let receiver_pub = Ed25519Signer::from_bytes(&receiver_priv).public_key_bytes();
736 let p1 = fixture_packet("pkt-1", None, "2026-04-24T12:00:00Z");
737 let p2 = fixture_packet("pkt-2", None, "2026-04-24T12:00:00Z");
738 let receipt = sign_delivery_receipt(
739 &p1,
740 "tf:actor:agent:example.com/receiver",
741 "2026-04-24T12:01:00Z",
742 &receiver_priv,
743 )
744 .expect("sign");
745 let v = verify_delivery_receipt(&receipt, &p2, &receiver_pub);
746 assert!(!v.ok);
747 assert_eq!(v.reason.as_deref(), Some("packet_id mismatch"));
748 }
749
750 #[test]
751 fn proof_of_forwarding_round_trip_and_tamper() {
752 let relay_priv = fresh_seed();
753 let relay_pub = Ed25519Signer::from_bytes(&relay_priv).public_key_bytes();
754 let p = fixture_packet("pkt-relay-1", None, "2026-04-24T12:00:00Z");
755 let proof = sign_proof_of_forwarding(
756 &p,
757 "tf:actor:relay:example.com/edge",
758 "2026-04-24T12:01:00Z",
759 1,
760 &relay_priv,
761 )
762 .expect("sign");
763 let v = verify_proof_of_forwarding(&proof, &p, &relay_pub);
764 assert!(v.ok);
765 let tampered = ProofOfForwarding {
768 forwarded_at: "2027-01-01T00:00:00Z".into(),
769 ..proof.clone()
770 };
771 let v2 = verify_proof_of_forwarding(&tampered, &p, &relay_pub);
772 assert!(!v2.ok);
773 }
774}