1use chacha20poly1305::aead::{Aead, Payload};
22use chacha20poly1305::{ChaCha20Poly1305, KeyInit};
23use chia_bls::SecretKey;
24use chia_protocol::{Bytes32, Bytes48, Bytes96};
25use chia_traits::Streamable as _;
26use hkdf::Hkdf;
27use sha2::Sha256;
28
29use crate::compression::{compress_payload, decompress_payload};
30use crate::constants::MAX_DECOMPRESSED_BYTES;
31use crate::envelope::{
32 DigMessageEnvelope, InnerMessage, InteractionShape, SealedPayload, StreamHeader, FLAG_SEALED,
33};
34use crate::error::{MessageError, Result};
35use crate::replay::ReplayGuard;
36use crate::transcript::TranscriptFields;
37use dig_identity::{g1_dh, g1_subgroup_check, public_key_bytes};
38
39const KDF_INFO_LABEL: &[u8] = b"dig-message/dhkem-g1/v1";
42
43const OKM_LEN: usize = 32 + 12;
45
46const VERSION: u8 = crate::constants::ENVELOPE_VERSION;
48
49pub struct SealParams<'a> {
52 pub sender_sk: &'a SecretKey,
54 pub sender: Bytes32,
56 pub sender_epoch: u32,
58 pub recipient: Bytes32,
60 pub recipient_pub: &'a [u8; 48],
62 pub message_type: u32,
64 pub shape: InteractionShape,
66 pub correlation_id: Bytes32,
68 pub stream: Option<StreamHeader>,
70 pub counter: u64,
72 pub timestamp_ms: u64,
74 pub expires_at: u64,
76 pub payload: &'a [u8],
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct OpenedMessage {
84 pub message_type: u32,
85 pub correlation_id: Bytes32,
86 pub shape: InteractionShape,
87 pub sender: Bytes32,
88 pub sender_epoch: u32,
89 pub counter: u64,
90 pub timestamp_ms: u64,
91 pub expires_at: u64,
92 pub payload: Vec<u8>,
94}
95
96pub fn seal_message(params: &SealParams) -> Result<DigMessageEnvelope> {
103 let esk = random_ephemeral()?;
104 seal_with_ephemeral(params, &esk)
105}
106
107pub fn seal_with_ephemeral(params: &SealParams, esk: &SecretKey) -> Result<DigMessageEnvelope> {
113 let compressed = compress_payload(params.payload)?;
115
116 let sender_pub = public_key_bytes(params.sender_sk);
117 let kem_enc = public_key_bytes(esk);
118
119 let z = auth_encap_secret(esk, params.sender_sk, params.recipient_pub)?;
121 let okm = kdf(&z, &kem_enc, &sender_pub, params.recipient_pub);
122
123 let transcript = TranscriptFields {
125 version: VERSION,
126 message_type: params.message_type,
127 flags: params.shape.as_bits() | FLAG_SEALED,
128 correlation_id: params.correlation_id,
129 sender: params.sender,
130 recipient: params.recipient,
131 sender_epoch: params.sender_epoch,
132 counter: params.counter,
133 timestamp_ms: params.timestamp_ms,
134 expires_at: params.expires_at,
135 stream: params.stream,
136 kem_enc: &kem_enc,
137 compression: compressed.compression,
138 uncompressed_len: compressed.uncompressed_len,
139 compressed_payload: &compressed.bytes,
140 };
141 let sender_sig = transcript.sign(params.sender_sk);
142
143 let inner = InnerMessage {
144 message_type: params.message_type,
145 correlation_id: params.correlation_id,
146 compression: compressed.compression,
147 uncompressed_len: compressed.uncompressed_len,
148 counter: params.counter,
149 timestamp_ms: params.timestamp_ms,
150 expires_at: params.expires_at,
151 payload: compressed.bytes,
152 sender_sig: Bytes96::new(sender_sig),
153 };
154 let inner_bytes = inner
155 .to_bytes()
156 .map_err(|e| MessageError::SealFailed(e.to_string()))?;
157
158 let mut envelope = DigMessageEnvelope {
162 version: VERSION,
163 message_type: params.message_type,
164 flags: params.shape.as_bits() | FLAG_SEALED,
165 correlation_id: params.correlation_id,
166 sender: params.sender,
167 recipient: params.recipient,
168 sender_epoch: params.sender_epoch,
169 stream: params.stream,
170 sealed: SealedPayload {
171 kem_enc: Bytes48::new(kem_enc),
172 ciphertext: Vec::new(),
173 },
174 };
175 let aad = envelope.header_bytes()?;
176 envelope.sealed.ciphertext = aead_seal(&okm, &aad, &inner_bytes)?;
177 Ok(envelope)
178}
179
180pub fn open_message(
193 recipient_sk: &SecretKey,
194 envelope: &DigMessageEnvelope,
195 resolve_sender_pub: impl Fn(Bytes32, u32) -> Option<[u8; 48]>,
196 guard: &mut ReplayGuard,
197 now_ms: u64,
198) -> Result<OpenedMessage> {
199 let shape = InteractionShape::from_flags(envelope.flags).ok_or(MessageError::HeaderMismatch)?;
200
201 let sender_pub = resolve_sender_pub(envelope.sender, envelope.sender_epoch)
203 .ok_or(MessageError::UnresolvableSender)?;
204
205 let kem_enc: [u8; 48] = envelope
206 .sealed
207 .kem_enc
208 .as_ref()
209 .try_into()
210 .expect("Bytes48 is exactly 48 bytes");
211
212 if !g1_subgroup_check(&kem_enc) || !g1_subgroup_check(&sender_pub) {
214 return Err(MessageError::InvalidPoint);
215 }
216
217 let z = auth_decap_secret(recipient_sk, &sender_pub, &kem_enc)?;
219 let recipient_pub = public_key_bytes(recipient_sk);
220 let okm = kdf(&z, &kem_enc, &sender_pub, &recipient_pub);
221
222 let aad = envelope.header_bytes()?;
223 let inner_bytes = aead_open(&okm, &aad, &envelope.sealed.ciphertext)?;
224 let inner =
225 InnerMessage::from_bytes(&inner_bytes).map_err(|e| MessageError::Codec(e.to_string()))?;
226
227 let transcript = TranscriptFields {
229 version: envelope.version,
230 message_type: envelope.message_type,
231 flags: envelope.flags,
232 correlation_id: envelope.correlation_id,
233 sender: envelope.sender,
234 recipient: envelope.recipient,
235 sender_epoch: envelope.sender_epoch,
236 counter: inner.counter,
237 timestamp_ms: inner.timestamp_ms,
238 expires_at: inner.expires_at,
239 stream: envelope.stream,
240 kem_enc: &kem_enc,
241 compression: inner.compression,
242 uncompressed_len: inner.uncompressed_len,
243 compressed_payload: &inner.payload,
244 };
245 let sig: [u8; 96] = inner
246 .sender_sig
247 .as_ref()
248 .try_into()
249 .expect("Bytes96 is exactly 96 bytes");
250 if !transcript.verify(&sender_pub, &sig) {
251 return Err(MessageError::BadSignature);
252 }
253
254 if inner.message_type != envelope.message_type
256 || inner.correlation_id != envelope.correlation_id
257 {
258 return Err(MessageError::HeaderMismatch);
259 }
260
261 check_expiry(inner.timestamp_ms, inner.expires_at, now_ms)?;
263
264 if !guard.check_and_admit(
266 envelope.sender,
267 envelope.sender_epoch,
268 inner.counter,
269 inner.timestamp_ms,
270 now_ms,
271 ) {
272 return Err(MessageError::Replay);
273 }
274
275 if inner.uncompressed_len as usize > MAX_DECOMPRESSED_BYTES {
277 return Err(MessageError::DecompressionBomb {
278 declared: inner.uncompressed_len as usize,
279 max: MAX_DECOMPRESSED_BYTES,
280 });
281 }
282 let payload = decompress_payload(inner.compression, &inner.payload, inner.uncompressed_len)?;
283
284 Ok(OpenedMessage {
285 message_type: inner.message_type,
286 correlation_id: inner.correlation_id,
287 shape,
288 sender: envelope.sender,
289 sender_epoch: envelope.sender_epoch,
290 counter: inner.counter,
291 timestamp_ms: inner.timestamp_ms,
292 expires_at: inner.expires_at,
293 payload,
294 })
295}
296
297fn check_expiry(timestamp_ms: u64, expires_at: u64, now_ms: u64) -> Result<()> {
300 if expires_at == 0 {
301 return Ok(());
302 }
303 if expires_at > timestamp_ms.saturating_add(crate::constants::MAX_MESSAGE_TTL_MS) {
304 return Err(MessageError::TtlTooLong);
305 }
306 if now_ms > expires_at {
307 return Err(MessageError::Expired);
308 }
309 Ok(())
310}
311
312fn auth_encap_secret(
314 esk: &SecretKey,
315 sender_sk: &SecretKey,
316 recipient_pub: &[u8; 48],
317) -> Result<[u8; 96]> {
318 let ephemeral = g1_dh(esk, recipient_pub).ok_or(MessageError::InvalidPoint)?;
319 let static_term = g1_dh(sender_sk, recipient_pub).ok_or(MessageError::InvalidPoint)?;
320 Ok(concat_dh(&ephemeral, &static_term))
321}
322
323fn auth_decap_secret(
325 recipient_sk: &SecretKey,
326 sender_pub: &[u8; 48],
327 kem_enc: &[u8; 48],
328) -> Result<[u8; 96]> {
329 let ephemeral = g1_dh(recipient_sk, kem_enc).ok_or(MessageError::InvalidPoint)?;
330 let static_term = g1_dh(recipient_sk, sender_pub).ok_or(MessageError::InvalidPoint)?;
331 Ok(concat_dh(&ephemeral, &static_term))
332}
333
334fn concat_dh(a: &[u8; 48], b: &[u8; 48]) -> [u8; 96] {
336 let mut z = [0u8; 96];
337 z[..48].copy_from_slice(a);
338 z[48..].copy_from_slice(b);
339 z
340}
341
342fn kdf(
345 z: &[u8; 96],
346 kem_enc: &[u8; 48],
347 sender_pub: &[u8; 48],
348 recipient_pub: &[u8; 48],
349) -> [u8; OKM_LEN] {
350 let hk = Hkdf::<Sha256>::new(None, z);
351 let mut info = Vec::with_capacity(KDF_INFO_LABEL.len() + 48 * 3);
352 info.extend_from_slice(KDF_INFO_LABEL);
353 info.extend_from_slice(kem_enc);
354 info.extend_from_slice(sender_pub);
355 info.extend_from_slice(recipient_pub);
356 let mut okm = [0u8; OKM_LEN];
357 hk.expand(&info, &mut okm)
358 .expect("OKM_LEN is well within the HKDF-SHA256 output limit");
359 okm
360}
361
362fn aead_seal(okm: &[u8; OKM_LEN], aad: &[u8], plaintext: &[u8]) -> Result<Vec<u8>> {
364 let cipher = ChaCha20Poly1305::new_from_slice(&okm[..32])
365 .map_err(|_| MessageError::SealFailed("invalid AEAD key length".into()))?;
366 let nonce: [u8; 12] = okm[32..].try_into().expect("OKM tail is exactly 12 bytes");
367 cipher
368 .encrypt(
369 (&nonce).into(),
370 Payload {
371 msg: plaintext,
372 aad,
373 },
374 )
375 .map_err(|_| MessageError::SealFailed("AEAD encrypt failed".into()))
376}
377
378fn aead_open(okm: &[u8; OKM_LEN], aad: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
381 let cipher =
382 ChaCha20Poly1305::new_from_slice(&okm[..32]).map_err(|_| MessageError::OpenFailed)?;
383 let nonce: [u8; 12] = okm[32..].try_into().expect("OKM tail is exactly 12 bytes");
384 cipher
385 .decrypt(
386 (&nonce).into(),
387 Payload {
388 msg: ciphertext,
389 aad,
390 },
391 )
392 .map_err(|_| MessageError::OpenFailed)
393}
394
395fn random_ephemeral() -> Result<SecretKey> {
397 let mut ikm = [0u8; 32];
398 getrandom::getrandom(&mut ikm)
399 .map_err(|e| MessageError::SealFailed(format!("ephemeral key entropy: {e}")))?;
400 Ok(SecretKey::from_seed(&ikm))
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use dig_identity::{derive_identity_sk, master_secret_key_from_seed};
407 use sha2::Digest;
408
409 fn sk(label: &str) -> SecretKey {
410 let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
411 derive_identity_sk(&master_secret_key_from_seed(&seed))
412 }
413
414 fn ephemeral(label: &str) -> SecretKey {
416 let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
417 SecretKey::from_seed(&seed)
418 }
419
420 fn did(n: u8) -> Bytes32 {
421 Bytes32::new([n; 32])
422 }
423
424 struct Party {
425 sk: SecretKey,
426 did: Bytes32,
427 pub_key: [u8; 48],
428 }
429 fn party(label: &str, did_byte: u8) -> Party {
430 let sk = sk(label);
431 let pub_key = public_key_bytes(&sk);
432 Party {
433 sk,
434 did: did(did_byte),
435 pub_key,
436 }
437 }
438
439 fn params<'a>(
440 sender: &'a Party,
441 recipient: &'a Party,
442 payload: &'a [u8],
443 counter: u64,
444 timestamp_ms: u64,
445 expires_at: u64,
446 ) -> SealParams<'a> {
447 SealParams {
448 sender_sk: &sender.sk,
449 sender: sender.did,
450 sender_epoch: 0,
451 recipient: recipient.did,
452 recipient_pub: &recipient.pub_key,
453 message_type: 0x0000_0201,
454 shape: InteractionShape::OneShot,
455 correlation_id: did(0xAB),
456 stream: None,
457 counter,
458 timestamp_ms,
459 expires_at,
460 payload,
461 }
462 }
463
464 fn resolver(pk: [u8; 48]) -> impl Fn(Bytes32, u32) -> Option<[u8; 48]> {
466 move |_did, _epoch| Some(pk)
467 }
468
469 const NOW: u64 = 1_700_000_000_000;
470
471 #[test]
472 fn seal_open_round_trip_raw() {
473 let alice = party("seal/alice", 1);
474 let bob = party("seal/bob", 2);
475 let msg = b"hello bob";
476 let env =
477 seal_with_ephemeral(¶ms(&alice, &bob, msg, 0, NOW, 0), &ephemeral("e1")).unwrap();
478 let mut guard = ReplayGuard::new();
479 let opened = open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap();
480 assert_eq!(opened.payload, msg);
481 assert_eq!(opened.sender, alice.did);
482 assert_eq!(opened.counter, 0);
483 }
484
485 #[test]
486 fn seal_open_round_trip_compressed() {
487 let alice = party("seal/alice2", 1);
488 let bob = party("seal/bob2", 2);
489 let msg: Vec<u8> = (0..4096).map(|i| (i % 7) as u8).collect();
490 let env =
491 seal_with_ephemeral(¶ms(&alice, &bob, &msg, 0, NOW, 0), &ephemeral("e2")).unwrap();
492 assert!(env.sealed.ciphertext.len() < msg.len());
494 let mut guard = ReplayGuard::new();
495 let opened = open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap();
496 assert_eq!(opened.payload, msg);
497 }
498
499 #[test]
500 fn relay_sees_only_ciphertext_no_plaintext_substring() {
501 let alice = party("seal/leak-a", 1);
502 let bob = party("seal/leak-b", 2);
503 let secret = b"TOP-SECRET-PLAINTEXT-MARKER";
504 let env = seal_with_ephemeral(
505 ¶ms(&alice, &bob, secret, 0, NOW, 0),
506 &ephemeral("eleak"),
507 )
508 .unwrap();
509 let wire = crate::envelope::encode_envelope(&env).unwrap();
510 assert!(
511 wire.windows(secret.len()).all(|w| w != secret),
512 "the plaintext must never appear on the wire"
513 );
514 }
515
516 #[test]
517 fn wrong_recipient_fails() {
518 let alice = party("seal/a3", 1);
519 let bob = party("seal/b3", 2);
520 let eve = party("seal/eve3", 9);
521 let env = seal_with_ephemeral(
522 ¶ms(&alice, &bob, b"secret", 0, NOW, 0),
523 &ephemeral("e3"),
524 )
525 .unwrap();
526 let mut guard = ReplayGuard::new();
527 let err =
528 open_message(&eve.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
529 assert_eq!(err, MessageError::OpenFailed);
530 }
531
532 #[test]
533 fn wrong_sender_pub_fails() {
534 let alice = party("seal/a4", 1);
535 let bob = party("seal/b4", 2);
536 let mallory = party("seal/m4", 9);
537 let env = seal_with_ephemeral(
538 ¶ms(&alice, &bob, b"secret", 0, NOW, 0),
539 &ephemeral("e4"),
540 )
541 .unwrap();
542 let mut guard = ReplayGuard::new();
543 let err =
545 open_message(&bob.sk, &env, resolver(mallory.pub_key), &mut guard, NOW).unwrap_err();
546 assert_eq!(err, MessageError::OpenFailed);
547 }
548
549 #[test]
550 fn unresolvable_sender_fails_closed() {
551 let alice = party("seal/a5", 1);
552 let bob = party("seal/b5", 2);
553 let env =
554 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e5")).unwrap();
555 let mut guard = ReplayGuard::new();
556 let err = open_message(&bob.sk, &env, |_d, _e| None, &mut guard, NOW).unwrap_err();
557 assert_eq!(err, MessageError::UnresolvableSender);
558 }
559
560 #[test]
561 fn tampered_ciphertext_rejected() {
562 let alice = party("seal/a6", 1);
563 let bob = party("seal/b6", 2);
564 let mut env =
565 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e6")).unwrap();
566 let last = env.sealed.ciphertext.len() - 1;
567 env.sealed.ciphertext[last] ^= 0x01;
568 let mut guard = ReplayGuard::new();
569 let err =
570 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
571 assert_eq!(err, MessageError::OpenFailed);
572 }
573
574 #[test]
575 fn tampered_header_aad_rejected() {
576 let alice = party("seal/a7", 1);
577 let bob = party("seal/b7", 2);
578 let mut env =
579 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e7")).unwrap();
580 env.message_type ^= 0xFF;
582 let mut guard = ReplayGuard::new();
583 let err =
584 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
585 assert_eq!(err, MessageError::OpenFailed);
586 }
587
588 #[test]
589 fn non_subgroup_kem_enc_rejected() {
590 let alice = party("seal/a8", 1);
591 let bob = party("seal/b8", 2);
592 let mut env =
593 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e8")).unwrap();
594 env.sealed.kem_enc = Bytes48::new([0xFFu8; 48]);
596 let mut guard = ReplayGuard::new();
597 let err =
598 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
599 assert_eq!(err, MessageError::InvalidPoint);
600 }
601
602 #[test]
603 fn bad_signature_rejected() {
604 let alice = party("seal/sigA", 1);
607 let bob = party("seal/sigB", 2);
608 let esk = ephemeral("esig");
609 let compressed = compress_payload(b"x").unwrap();
610 let sender_pub = public_key_bytes(&alice.sk);
611 let kem_enc = public_key_bytes(&esk);
612 let z = auth_encap_secret(&esk, &alice.sk, &bob.pub_key).unwrap();
613 let okm = kdf(&z, &kem_enc, &sender_pub, &bob.pub_key);
614
615 let inner = InnerMessage {
616 message_type: 0x0000_0201,
617 correlation_id: did(0xAB),
618 compression: compressed.compression,
619 uncompressed_len: compressed.uncompressed_len,
620 counter: 0,
621 timestamp_ms: NOW,
622 expires_at: 0,
623 payload: compressed.bytes,
624 sender_sig: Bytes96::new([0u8; 96]), };
626 let inner_bytes = inner.to_bytes().unwrap();
627 let mut env = DigMessageEnvelope {
628 version: VERSION,
629 message_type: 0x0000_0201,
630 flags: InteractionShape::OneShot.as_bits() | FLAG_SEALED,
631 correlation_id: did(0xAB),
632 sender: alice.did,
633 recipient: bob.did,
634 sender_epoch: 0,
635 stream: None,
636 sealed: SealedPayload {
637 kem_enc: Bytes48::new(kem_enc),
638 ciphertext: Vec::new(),
639 },
640 };
641 let aad = env.header_bytes().unwrap();
642 env.sealed.ciphertext = aead_seal(&okm, &aad, &inner_bytes).unwrap();
643
644 let mut guard = ReplayGuard::new();
645 let err =
646 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
647 assert_eq!(err, MessageError::BadSignature);
648 }
649
650 #[test]
651 fn replay_rejected() {
652 let alice = party("seal/a9", 1);
653 let bob = party("seal/b9", 2);
654 let env =
655 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e9")).unwrap();
656 let mut guard = ReplayGuard::new();
657 assert!(open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).is_ok());
658 let err =
659 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
660 assert_eq!(err, MessageError::Replay);
661 }
662
663 #[test]
664 fn stale_timestamp_rejected() {
665 let alice = party("seal/a10", 1);
666 let bob = party("seal/b10", 2);
667 let env = seal_with_ephemeral(
668 ¶ms(&alice, &bob, b"x", 0, NOW - 3_600_000, 0),
669 &ephemeral("e10"),
670 )
671 .unwrap();
672 let mut guard = ReplayGuard::new();
673 let err =
674 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
675 assert_eq!(err, MessageError::Replay);
676 }
677
678 #[test]
679 fn in_window_reorder_accepted() {
680 let alice = party("seal/a11", 1);
681 let bob = party("seal/b11", 2);
682 let mk = |c: u64| {
683 seal_with_ephemeral(
684 ¶ms(&alice, &bob, b"x", c, NOW, 0),
685 &ephemeral(&format!("e11-{c}")),
686 )
687 .unwrap()
688 };
689 let mut guard = ReplayGuard::new();
690 assert!(open_message(&bob.sk, &mk(5), resolver(alice.pub_key), &mut guard, NOW).is_ok());
691 assert!(open_message(&bob.sk, &mk(3), resolver(alice.pub_key), &mut guard, NOW).is_ok());
692 assert_eq!(
693 open_message(&bob.sk, &mk(3), resolver(alice.pub_key), &mut guard, NOW).unwrap_err(),
694 MessageError::Replay
695 );
696 }
697
698 #[test]
699 fn expired_message_discarded() {
700 let alice = party("seal/a12", 1);
701 let bob = party("seal/b12", 2);
702 let env = seal_with_ephemeral(
703 ¶ms(&alice, &bob, b"x", 0, NOW, NOW + 1000),
704 &ephemeral("e12"),
705 )
706 .unwrap();
707 let mut guard = ReplayGuard::new();
708 let err = open_message(
709 &bob.sk,
710 &env,
711 resolver(alice.pub_key),
712 &mut guard,
713 NOW + 5000,
714 )
715 .unwrap_err();
716 assert_eq!(err, MessageError::Expired);
717 }
718
719 #[test]
720 fn within_expiry_accepted() {
721 let alice = party("seal/a13", 1);
722 let bob = party("seal/b13", 2);
723 let env = seal_with_ephemeral(
724 ¶ms(&alice, &bob, b"x", 0, NOW, NOW + 60_000),
725 &ephemeral("e13"),
726 )
727 .unwrap();
728 let mut guard = ReplayGuard::new();
729 assert!(open_message(
730 &bob.sk,
731 &env,
732 resolver(alice.pub_key),
733 &mut guard,
734 NOW + 1000
735 )
736 .is_ok());
737 }
738
739 #[test]
740 fn over_long_ttl_rejected() {
741 let alice = party("seal/a14", 1);
742 let bob = party("seal/b14", 2);
743 let expires = NOW + crate::constants::MAX_MESSAGE_TTL_MS + 10_000;
744 let env = seal_with_ephemeral(
745 ¶ms(&alice, &bob, b"x", 0, NOW, expires),
746 &ephemeral("e14"),
747 )
748 .unwrap();
749 let mut guard = ReplayGuard::new();
750 let err =
751 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
752 assert_eq!(err, MessageError::TtlTooLong);
753 }
754
755 #[test]
756 fn self_addressed_round_trips() {
757 let me = party("seal/self", 7);
759 let env = seal_with_ephemeral(
760 &SealParams {
761 sender_sk: &me.sk,
762 sender: me.did,
763 sender_epoch: 0,
764 recipient: me.did,
765 recipient_pub: &me.pub_key,
766 message_type: 0x0000_0601,
767 shape: InteractionShape::OneShot,
768 correlation_id: did(0xCD),
769 stream: None,
770 counter: 0,
771 timestamp_ms: NOW,
772 expires_at: 0,
773 payload: b"note to self",
774 },
775 &ephemeral("eself"),
776 )
777 .unwrap();
778 let mut guard = ReplayGuard::new();
779 let opened = open_message(&me.sk, &env, resolver(me.pub_key), &mut guard, NOW).unwrap();
780 assert_eq!(opened.payload, b"note to self");
781 assert_eq!(
783 open_message(&me.sk, &env, resolver(me.pub_key), &mut guard, NOW).unwrap_err(),
784 MessageError::Replay
785 );
786 }
787
788 #[test]
789 fn seal_message_uses_random_ephemeral_and_round_trips() {
790 let alice = party("seal/rand-a", 1);
793 let bob = party("seal/rand-b", 2);
794 let p = params(&alice, &bob, b"random-ephemeral", 0, NOW, 0);
795 let env1 = seal_message(&p).unwrap();
796 let env2 = seal_message(&p).unwrap();
797 assert_ne!(
798 env1.sealed.kem_enc, env2.sealed.kem_enc,
799 "fresh ephemeral per seal"
800 );
801 let mut guard = ReplayGuard::new();
802 let opened =
803 open_message(&bob.sk, &env1, resolver(alice.pub_key), &mut guard, NOW).unwrap();
804 assert_eq!(opened.payload, b"random-ephemeral");
805 }
806}