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