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(crate) fn seal_with_ephemeral(
119 params: &SealParams,
120 esk: &SecretKey,
121) -> Result<DigMessageEnvelope> {
122 let compressed = compress_payload(params.payload)?;
124
125 let sender_pub = public_key_bytes(params.sender_sk);
126 let kem_enc = public_key_bytes(esk);
127
128 let z = auth_encap_secret(esk, params.sender_sk, params.recipient_pub)?;
130 let okm = kdf(&z, &kem_enc, &sender_pub, params.recipient_pub);
131
132 let transcript = TranscriptFields {
134 version: VERSION,
135 message_type: params.message_type,
136 flags: params.shape.as_bits() | FLAG_SEALED,
137 correlation_id: params.correlation_id,
138 sender: params.sender,
139 recipient: params.recipient,
140 sender_epoch: params.sender_epoch,
141 counter: params.counter,
142 timestamp_ms: params.timestamp_ms,
143 expires_at: params.expires_at,
144 stream: params.stream,
145 kem_enc: &kem_enc,
146 compression: compressed.compression,
147 uncompressed_len: compressed.uncompressed_len,
148 compressed_payload: &compressed.bytes,
149 };
150 let sender_sig = transcript.sign(params.sender_sk);
151
152 let inner = InnerMessage {
153 message_type: params.message_type,
154 correlation_id: params.correlation_id,
155 compression: compressed.compression,
156 uncompressed_len: compressed.uncompressed_len,
157 counter: params.counter,
158 timestamp_ms: params.timestamp_ms,
159 expires_at: params.expires_at,
160 payload: compressed.bytes,
161 sender_sig: Bytes96::new(sender_sig),
162 };
163 let inner_bytes = inner
164 .to_bytes()
165 .map_err(|e| MessageError::SealFailed(e.to_string()))?;
166
167 let mut envelope = DigMessageEnvelope {
171 version: VERSION,
172 message_type: params.message_type,
173 flags: params.shape.as_bits() | FLAG_SEALED,
174 correlation_id: params.correlation_id,
175 sender: params.sender,
176 recipient: params.recipient,
177 sender_epoch: params.sender_epoch,
178 stream: params.stream,
179 sealed: SealedPayload {
180 kem_enc: Bytes48::new(kem_enc),
181 ciphertext: Vec::new(),
182 },
183 };
184 let aad = envelope.header_bytes()?;
185 envelope.sealed.ciphertext = aead_seal(&okm, &aad, &inner_bytes)?;
186 Ok(envelope)
187}
188
189pub fn open_message(
202 recipient_sk: &SecretKey,
203 envelope: &DigMessageEnvelope,
204 resolve_sender_pub: impl Fn(Bytes32, u32) -> Option<[u8; 48]>,
205 guard: &mut ReplayGuard,
206 now_ms: u64,
207) -> Result<OpenedMessage> {
208 let shape = InteractionShape::from_flags(envelope.flags).ok_or(MessageError::HeaderMismatch)?;
209
210 let sender_pub = resolve_sender_pub(envelope.sender, envelope.sender_epoch)
212 .ok_or(MessageError::UnresolvableSender)?;
213
214 let kem_enc: [u8; 48] = envelope
215 .sealed
216 .kem_enc
217 .as_ref()
218 .try_into()
219 .expect("Bytes48 is exactly 48 bytes");
220
221 if !g1_subgroup_check(&kem_enc) || !g1_subgroup_check(&sender_pub) {
223 return Err(MessageError::InvalidPoint);
224 }
225
226 let z = auth_decap_secret(recipient_sk, &sender_pub, &kem_enc)?;
228 let recipient_pub = public_key_bytes(recipient_sk);
229 let okm = kdf(&z, &kem_enc, &sender_pub, &recipient_pub);
230
231 let aad = envelope.header_bytes()?;
232 let inner_bytes = aead_open(&okm, &aad, &envelope.sealed.ciphertext)?;
233 let inner =
234 InnerMessage::from_bytes(&inner_bytes).map_err(|e| MessageError::Codec(e.to_string()))?;
235
236 let transcript = TranscriptFields {
238 version: envelope.version,
239 message_type: envelope.message_type,
240 flags: envelope.flags,
241 correlation_id: envelope.correlation_id,
242 sender: envelope.sender,
243 recipient: envelope.recipient,
244 sender_epoch: envelope.sender_epoch,
245 counter: inner.counter,
246 timestamp_ms: inner.timestamp_ms,
247 expires_at: inner.expires_at,
248 stream: envelope.stream,
249 kem_enc: &kem_enc,
250 compression: inner.compression,
251 uncompressed_len: inner.uncompressed_len,
252 compressed_payload: &inner.payload,
253 };
254 let sig: [u8; 96] = inner
255 .sender_sig
256 .as_ref()
257 .try_into()
258 .expect("Bytes96 is exactly 96 bytes");
259 if !transcript.verify(&sender_pub, &sig) {
260 return Err(MessageError::BadSignature);
261 }
262
263 if inner.message_type != envelope.message_type
265 || inner.correlation_id != envelope.correlation_id
266 {
267 return Err(MessageError::HeaderMismatch);
268 }
269
270 check_expiry(inner.timestamp_ms, inner.expires_at, now_ms)?;
272
273 if !guard.check_and_admit(
275 envelope.sender,
276 envelope.sender_epoch,
277 inner.counter,
278 inner.timestamp_ms,
279 now_ms,
280 ) {
281 return Err(MessageError::Replay);
282 }
283
284 if inner.uncompressed_len as usize > MAX_DECOMPRESSED_BYTES {
286 return Err(MessageError::DecompressionBomb {
287 declared: inner.uncompressed_len as usize,
288 max: MAX_DECOMPRESSED_BYTES,
289 });
290 }
291 let payload = decompress_payload(inner.compression, &inner.payload, inner.uncompressed_len)?;
292
293 Ok(OpenedMessage {
294 message_type: inner.message_type,
295 correlation_id: inner.correlation_id,
296 shape,
297 sender: envelope.sender,
298 sender_epoch: envelope.sender_epoch,
299 counter: inner.counter,
300 timestamp_ms: inner.timestamp_ms,
301 expires_at: inner.expires_at,
302 payload,
303 })
304}
305
306fn check_expiry(timestamp_ms: u64, expires_at: u64, now_ms: u64) -> Result<()> {
309 if expires_at == 0 {
310 return Ok(());
311 }
312 if expires_at > timestamp_ms.saturating_add(crate::constants::MAX_MESSAGE_TTL_MS) {
313 return Err(MessageError::TtlTooLong);
314 }
315 if now_ms > expires_at {
316 return Err(MessageError::Expired);
317 }
318 Ok(())
319}
320
321fn auth_encap_secret(
323 esk: &SecretKey,
324 sender_sk: &SecretKey,
325 recipient_pub: &[u8; 48],
326) -> Result<[u8; 96]> {
327 let ephemeral = g1_dh(esk, recipient_pub).ok_or(MessageError::InvalidPoint)?;
328 let static_term = g1_dh(sender_sk, recipient_pub).ok_or(MessageError::InvalidPoint)?;
329 Ok(concat_dh(&ephemeral, &static_term))
330}
331
332fn auth_decap_secret(
334 recipient_sk: &SecretKey,
335 sender_pub: &[u8; 48],
336 kem_enc: &[u8; 48],
337) -> Result<[u8; 96]> {
338 let ephemeral = g1_dh(recipient_sk, kem_enc).ok_or(MessageError::InvalidPoint)?;
339 let static_term = g1_dh(recipient_sk, sender_pub).ok_or(MessageError::InvalidPoint)?;
340 Ok(concat_dh(&ephemeral, &static_term))
341}
342
343fn concat_dh(a: &[u8; 48], b: &[u8; 48]) -> [u8; 96] {
345 let mut z = [0u8; 96];
346 z[..48].copy_from_slice(a);
347 z[48..].copy_from_slice(b);
348 z
349}
350
351fn kdf(
354 z: &[u8; 96],
355 kem_enc: &[u8; 48],
356 sender_pub: &[u8; 48],
357 recipient_pub: &[u8; 48],
358) -> [u8; OKM_LEN] {
359 let hk = Hkdf::<Sha256>::new(None, z);
360 let mut info = Vec::with_capacity(KDF_INFO_LABEL.len() + 48 * 3);
361 info.extend_from_slice(KDF_INFO_LABEL);
362 info.extend_from_slice(kem_enc);
363 info.extend_from_slice(sender_pub);
364 info.extend_from_slice(recipient_pub);
365 let mut okm = [0u8; OKM_LEN];
366 hk.expand(&info, &mut okm)
367 .expect("OKM_LEN is well within the HKDF-SHA256 output limit");
368 okm
369}
370
371fn aead_seal(okm: &[u8; OKM_LEN], aad: &[u8], plaintext: &[u8]) -> Result<Vec<u8>> {
373 let cipher = ChaCha20Poly1305::new_from_slice(&okm[..32])
374 .map_err(|_| MessageError::SealFailed("invalid AEAD key length".into()))?;
375 let nonce: [u8; 12] = okm[32..].try_into().expect("OKM tail is exactly 12 bytes");
376 cipher
377 .encrypt(
378 (&nonce).into(),
379 Payload {
380 msg: plaintext,
381 aad,
382 },
383 )
384 .map_err(|_| MessageError::SealFailed("AEAD encrypt failed".into()))
385}
386
387fn aead_open(okm: &[u8; OKM_LEN], aad: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
390 let cipher =
391 ChaCha20Poly1305::new_from_slice(&okm[..32]).map_err(|_| MessageError::OpenFailed)?;
392 let nonce: [u8; 12] = okm[32..].try_into().expect("OKM tail is exactly 12 bytes");
393 cipher
394 .decrypt(
395 (&nonce).into(),
396 Payload {
397 msg: ciphertext,
398 aad,
399 },
400 )
401 .map_err(|_| MessageError::OpenFailed)
402}
403
404fn random_ephemeral() -> Result<SecretKey> {
406 let mut ikm = [0u8; 32];
407 getrandom::getrandom(&mut ikm)
408 .map_err(|e| MessageError::SealFailed(format!("ephemeral key entropy: {e}")))?;
409 Ok(SecretKey::from_seed(&ikm))
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use dig_identity::{derive_identity_sk, master_secret_key_from_seed};
416 use sha2::Digest;
417
418 fn sk(label: &str) -> SecretKey {
419 let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
420 derive_identity_sk(&master_secret_key_from_seed(&seed))
421 }
422
423 fn ephemeral(label: &str) -> SecretKey {
425 let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
426 SecretKey::from_seed(&seed)
427 }
428
429 fn did(n: u8) -> Bytes32 {
430 Bytes32::new([n; 32])
431 }
432
433 struct Party {
434 sk: SecretKey,
435 did: Bytes32,
436 pub_key: [u8; 48],
437 }
438 fn party(label: &str, did_byte: u8) -> Party {
439 let sk = sk(label);
440 let pub_key = public_key_bytes(&sk);
441 Party {
442 sk,
443 did: did(did_byte),
444 pub_key,
445 }
446 }
447
448 fn params<'a>(
449 sender: &'a Party,
450 recipient: &'a Party,
451 payload: &'a [u8],
452 counter: u64,
453 timestamp_ms: u64,
454 expires_at: u64,
455 ) -> SealParams<'a> {
456 SealParams {
457 sender_sk: &sender.sk,
458 sender: sender.did,
459 sender_epoch: 0,
460 recipient: recipient.did,
461 recipient_pub: &recipient.pub_key,
462 message_type: 0x0000_0201,
463 shape: InteractionShape::OneShot,
464 correlation_id: did(0xAB),
465 stream: None,
466 counter,
467 timestamp_ms,
468 expires_at,
469 payload,
470 }
471 }
472
473 fn resolver(pk: [u8; 48]) -> impl Fn(Bytes32, u32) -> Option<[u8; 48]> {
475 move |_did, _epoch| Some(pk)
476 }
477
478 const NOW: u64 = 1_700_000_000_000;
479
480 #[test]
481 fn seal_open_round_trip_raw() {
482 let alice = party("seal/alice", 1);
483 let bob = party("seal/bob", 2);
484 let msg = b"hello bob";
485 let env =
486 seal_with_ephemeral(¶ms(&alice, &bob, msg, 0, NOW, 0), &ephemeral("e1")).unwrap();
487 let mut guard = ReplayGuard::new();
488 let opened = open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap();
489 assert_eq!(opened.payload, msg);
490 assert_eq!(opened.sender, alice.did);
491 assert_eq!(opened.counter, 0);
492 }
493
494 #[test]
495 fn seal_open_round_trip_compressed() {
496 let alice = party("seal/alice2", 1);
497 let bob = party("seal/bob2", 2);
498 let msg: Vec<u8> = (0..4096).map(|i| (i % 7) as u8).collect();
499 let env =
500 seal_with_ephemeral(¶ms(&alice, &bob, &msg, 0, NOW, 0), &ephemeral("e2")).unwrap();
501 assert!(env.sealed.ciphertext.len() < msg.len());
503 let mut guard = ReplayGuard::new();
504 let opened = open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap();
505 assert_eq!(opened.payload, msg);
506 }
507
508 #[test]
509 fn relay_sees_only_ciphertext_no_plaintext_substring() {
510 let alice = party("seal/leak-a", 1);
511 let bob = party("seal/leak-b", 2);
512 let secret = b"TOP-SECRET-PLAINTEXT-MARKER";
513 let env = seal_with_ephemeral(
514 ¶ms(&alice, &bob, secret, 0, NOW, 0),
515 &ephemeral("eleak"),
516 )
517 .unwrap();
518 let wire = crate::envelope::encode_envelope(&env).unwrap();
519 assert!(
520 wire.windows(secret.len()).all(|w| w != secret),
521 "the plaintext must never appear on the wire"
522 );
523 }
524
525 #[test]
526 fn wrong_recipient_fails() {
527 let alice = party("seal/a3", 1);
528 let bob = party("seal/b3", 2);
529 let eve = party("seal/eve3", 9);
530 let env = seal_with_ephemeral(
531 ¶ms(&alice, &bob, b"secret", 0, NOW, 0),
532 &ephemeral("e3"),
533 )
534 .unwrap();
535 let mut guard = ReplayGuard::new();
536 let err =
537 open_message(&eve.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
538 assert_eq!(err, MessageError::OpenFailed);
539 }
540
541 #[test]
542 fn wrong_sender_pub_fails() {
543 let alice = party("seal/a4", 1);
544 let bob = party("seal/b4", 2);
545 let mallory = party("seal/m4", 9);
546 let env = seal_with_ephemeral(
547 ¶ms(&alice, &bob, b"secret", 0, NOW, 0),
548 &ephemeral("e4"),
549 )
550 .unwrap();
551 let mut guard = ReplayGuard::new();
552 let err =
554 open_message(&bob.sk, &env, resolver(mallory.pub_key), &mut guard, NOW).unwrap_err();
555 assert_eq!(err, MessageError::OpenFailed);
556 }
557
558 #[test]
559 fn unresolvable_sender_fails_closed() {
560 let alice = party("seal/a5", 1);
561 let bob = party("seal/b5", 2);
562 let env =
563 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e5")).unwrap();
564 let mut guard = ReplayGuard::new();
565 let err = open_message(&bob.sk, &env, |_d, _e| None, &mut guard, NOW).unwrap_err();
566 assert_eq!(err, MessageError::UnresolvableSender);
567 }
568
569 #[test]
570 fn tampered_ciphertext_rejected() {
571 let alice = party("seal/a6", 1);
572 let bob = party("seal/b6", 2);
573 let mut env =
574 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e6")).unwrap();
575 let last = env.sealed.ciphertext.len() - 1;
576 env.sealed.ciphertext[last] ^= 0x01;
577 let mut guard = ReplayGuard::new();
578 let err =
579 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
580 assert_eq!(err, MessageError::OpenFailed);
581 }
582
583 #[test]
584 fn tampered_header_aad_rejected() {
585 let alice = party("seal/a7", 1);
586 let bob = party("seal/b7", 2);
587 let mut env =
588 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e7")).unwrap();
589 env.message_type ^= 0xFF;
591 let mut guard = ReplayGuard::new();
592 let err =
593 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
594 assert_eq!(err, MessageError::OpenFailed);
595 }
596
597 #[test]
598 fn non_subgroup_kem_enc_rejected() {
599 let alice = party("seal/a8", 1);
600 let bob = party("seal/b8", 2);
601 let mut env =
602 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e8")).unwrap();
603 env.sealed.kem_enc = Bytes48::new([0xFFu8; 48]);
605 let mut guard = ReplayGuard::new();
606 let err =
607 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
608 assert_eq!(err, MessageError::InvalidPoint);
609 }
610
611 #[test]
612 fn bad_signature_rejected() {
613 let alice = party("seal/sigA", 1);
616 let bob = party("seal/sigB", 2);
617 let esk = ephemeral("esig");
618 let compressed = compress_payload(b"x").unwrap();
619 let sender_pub = public_key_bytes(&alice.sk);
620 let kem_enc = public_key_bytes(&esk);
621 let z = auth_encap_secret(&esk, &alice.sk, &bob.pub_key).unwrap();
622 let okm = kdf(&z, &kem_enc, &sender_pub, &bob.pub_key);
623
624 let inner = InnerMessage {
625 message_type: 0x0000_0201,
626 correlation_id: did(0xAB),
627 compression: compressed.compression,
628 uncompressed_len: compressed.uncompressed_len,
629 counter: 0,
630 timestamp_ms: NOW,
631 expires_at: 0,
632 payload: compressed.bytes,
633 sender_sig: Bytes96::new([0u8; 96]), };
635 let inner_bytes = inner.to_bytes().unwrap();
636 let mut env = DigMessageEnvelope {
637 version: VERSION,
638 message_type: 0x0000_0201,
639 flags: InteractionShape::OneShot.as_bits() | FLAG_SEALED,
640 correlation_id: did(0xAB),
641 sender: alice.did,
642 recipient: bob.did,
643 sender_epoch: 0,
644 stream: None,
645 sealed: SealedPayload {
646 kem_enc: Bytes48::new(kem_enc),
647 ciphertext: Vec::new(),
648 },
649 };
650 let aad = env.header_bytes().unwrap();
651 env.sealed.ciphertext = aead_seal(&okm, &aad, &inner_bytes).unwrap();
652
653 let mut guard = ReplayGuard::new();
654 let err =
655 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
656 assert_eq!(err, MessageError::BadSignature);
657 }
658
659 #[test]
660 fn replay_rejected() {
661 let alice = party("seal/a9", 1);
662 let bob = party("seal/b9", 2);
663 let env =
664 seal_with_ephemeral(¶ms(&alice, &bob, b"x", 0, NOW, 0), &ephemeral("e9")).unwrap();
665 let mut guard = ReplayGuard::new();
666 assert!(open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).is_ok());
667 let err =
668 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
669 assert_eq!(err, MessageError::Replay);
670 }
671
672 #[test]
673 fn stale_timestamp_rejected() {
674 let alice = party("seal/a10", 1);
675 let bob = party("seal/b10", 2);
676 let env = seal_with_ephemeral(
677 ¶ms(&alice, &bob, b"x", 0, NOW - 3_600_000, 0),
678 &ephemeral("e10"),
679 )
680 .unwrap();
681 let mut guard = ReplayGuard::new();
682 let err =
683 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
684 assert_eq!(err, MessageError::Replay);
685 }
686
687 #[test]
688 fn in_window_reorder_accepted() {
689 let alice = party("seal/a11", 1);
690 let bob = party("seal/b11", 2);
691 let mk = |c: u64| {
692 seal_with_ephemeral(
693 ¶ms(&alice, &bob, b"x", c, NOW, 0),
694 &ephemeral(&format!("e11-{c}")),
695 )
696 .unwrap()
697 };
698 let mut guard = ReplayGuard::new();
699 assert!(open_message(&bob.sk, &mk(5), resolver(alice.pub_key), &mut guard, NOW).is_ok());
700 assert!(open_message(&bob.sk, &mk(3), resolver(alice.pub_key), &mut guard, NOW).is_ok());
701 assert_eq!(
702 open_message(&bob.sk, &mk(3), resolver(alice.pub_key), &mut guard, NOW).unwrap_err(),
703 MessageError::Replay
704 );
705 }
706
707 #[test]
708 fn expired_message_discarded() {
709 let alice = party("seal/a12", 1);
710 let bob = party("seal/b12", 2);
711 let env = seal_with_ephemeral(
712 ¶ms(&alice, &bob, b"x", 0, NOW, NOW + 1000),
713 &ephemeral("e12"),
714 )
715 .unwrap();
716 let mut guard = ReplayGuard::new();
717 let err = open_message(
718 &bob.sk,
719 &env,
720 resolver(alice.pub_key),
721 &mut guard,
722 NOW + 5000,
723 )
724 .unwrap_err();
725 assert_eq!(err, MessageError::Expired);
726 }
727
728 #[test]
729 fn within_expiry_accepted() {
730 let alice = party("seal/a13", 1);
731 let bob = party("seal/b13", 2);
732 let env = seal_with_ephemeral(
733 ¶ms(&alice, &bob, b"x", 0, NOW, NOW + 60_000),
734 &ephemeral("e13"),
735 )
736 .unwrap();
737 let mut guard = ReplayGuard::new();
738 assert!(open_message(
739 &bob.sk,
740 &env,
741 resolver(alice.pub_key),
742 &mut guard,
743 NOW + 1000
744 )
745 .is_ok());
746 }
747
748 #[test]
749 fn over_long_ttl_rejected() {
750 let alice = party("seal/a14", 1);
751 let bob = party("seal/b14", 2);
752 let expires = NOW + crate::constants::MAX_MESSAGE_TTL_MS + 10_000;
753 let env = seal_with_ephemeral(
754 ¶ms(&alice, &bob, b"x", 0, NOW, expires),
755 &ephemeral("e14"),
756 )
757 .unwrap();
758 let mut guard = ReplayGuard::new();
759 let err =
760 open_message(&bob.sk, &env, resolver(alice.pub_key), &mut guard, NOW).unwrap_err();
761 assert_eq!(err, MessageError::TtlTooLong);
762 }
763
764 #[test]
765 fn self_addressed_round_trips() {
766 let me = party("seal/self", 7);
768 let env = seal_with_ephemeral(
769 &SealParams {
770 sender_sk: &me.sk,
771 sender: me.did,
772 sender_epoch: 0,
773 recipient: me.did,
774 recipient_pub: &me.pub_key,
775 message_type: 0x0000_0601,
776 shape: InteractionShape::OneShot,
777 correlation_id: did(0xCD),
778 stream: None,
779 counter: 0,
780 timestamp_ms: NOW,
781 expires_at: 0,
782 payload: b"note to self",
783 },
784 &ephemeral("eself"),
785 )
786 .unwrap();
787 let mut guard = ReplayGuard::new();
788 let opened = open_message(&me.sk, &env, resolver(me.pub_key), &mut guard, NOW).unwrap();
789 assert_eq!(opened.payload, b"note to self");
790 assert_eq!(
792 open_message(&me.sk, &env, resolver(me.pub_key), &mut guard, NOW).unwrap_err(),
793 MessageError::Replay
794 );
795 }
796
797 #[test]
798 fn seal_message_uses_random_ephemeral_and_round_trips() {
799 let alice = party("seal/rand-a", 1);
802 let bob = party("seal/rand-b", 2);
803 let p = params(&alice, &bob, b"random-ephemeral", 0, NOW, 0);
804 let env1 = seal_message(&p).unwrap();
805 let env2 = seal_message(&p).unwrap();
806 assert_ne!(
807 env1.sealed.kem_enc, env2.sealed.kem_enc,
808 "fresh ephemeral per seal"
809 );
810 let mut guard = ReplayGuard::new();
811 let opened =
812 open_message(&bob.sk, &env1, resolver(alice.pub_key), &mut guard, NOW).unwrap();
813 assert_eq!(opened.payload, b"random-ephemeral");
814 }
815
816 const KAT_NOW: u64 = 1_700_000_000_000;
821
822 fn b32_from_seed(tag: &[u8]) -> Bytes32 {
823 let seed: [u8; 32] = sha2::Sha256::digest(tag).into();
824 Bytes32::new(seed)
825 }
826
827 fn kat_sk(label: &str) -> SecretKey {
828 let seed: [u8; 32] = sha2::Sha256::digest(label.as_bytes()).into();
829 let msk = dig_identity::master_secret_key_from_seed(&seed);
830 dig_identity::derive_identity_sk(&msk)
831 }
832
833 fn kat_ephemeral(label: &str) -> SecretKey {
834 let seed: [u8; 32] = sha2::Sha256::digest(label.as_bytes()).into();
835 SecretKey::from_seed(&seed)
836 }
837
838 fn kat_resolver(pk: [u8; 48]) -> impl Fn(Bytes32, u32) -> Option<[u8; 48]> {
839 move |_did, _epoch| Some(pk)
840 }
841
842 fn kat_params<'a>(
843 sender_sk: &'a SecretKey,
844 sender: Bytes32,
845 recipient: Bytes32,
846 recipient_pub: &'a [u8; 48],
847 payload: &'a [u8],
848 ) -> SealParams<'a> {
849 SealParams {
850 sender_sk,
851 sender,
852 sender_epoch: 0,
853 recipient,
854 recipient_pub,
855 message_type: 0x0001_0101,
856 shape: InteractionShape::OneShot,
857 correlation_id: b32_from_seed(b"kat/corr"),
858 stream: None,
859 counter: 0,
860 timestamp_ms: KAT_NOW,
861 expires_at: 0,
862 payload,
863 }
864 }
865
866 #[test]
867 fn kat_seal_open_round_trip_raw_and_compressed() {
868 let alice = kat_sk("kat/seal/alice");
869 let bob = kat_sk("kat/seal/bob");
870 let (a_did, b_did) = (b32_from_seed(b"kat/a"), b32_from_seed(b"kat/b"));
871 let b_pub = public_key_bytes(&bob);
872
873 for (label, msg) in [
874 ("raw", b"hi".to_vec()),
875 (
876 "zstd",
877 (0..2048).map(|i| (i % 5) as u8).collect::<Vec<u8>>(),
878 ),
879 ] {
880 let params = kat_params(&alice, a_did, b_did, &b_pub, &msg);
881 let env = seal_with_ephemeral(¶ms, &kat_ephemeral(label)).unwrap();
882 let mut guard = ReplayGuard::new();
883 let opened = open_message(
884 &bob,
885 &env,
886 kat_resolver(public_key_bytes(&alice)),
887 &mut guard,
888 KAT_NOW,
889 )
890 .unwrap();
891 assert_eq!(opened.payload, msg, "{label} payload round-trips");
892 }
893 }
894
895 #[test]
896 fn kat_relay_sees_only_ciphertext() {
897 let alice = kat_sk("kat/leak/a");
898 let bob = kat_sk("kat/leak/b");
899 let secret = b"UNIQUE-PLAINTEXT-NEEDLE-XYZ";
900 let b_pub = public_key_bytes(&bob);
901 let params = kat_params(
902 &alice,
903 b32_from_seed(b"a"),
904 b32_from_seed(b"b"),
905 &b_pub,
906 secret,
907 );
908 let env = seal_with_ephemeral(¶ms, &kat_ephemeral("leak")).unwrap();
909 let wire = crate::envelope::encode_envelope(&env).unwrap();
910 assert!(wire.windows(secret.len()).all(|w| w != secret));
911 }
912
913 #[test]
914 fn kat_wrong_recipient_and_wrong_sender_reject() {
915 let alice = kat_sk("kat/wr/a");
916 let bob = kat_sk("kat/wr/b");
917 let eve = kat_sk("kat/wr/eve");
918 let b_pub = public_key_bytes(&bob);
919 let params = kat_params(
920 &alice,
921 b32_from_seed(b"a"),
922 b32_from_seed(b"b"),
923 &b_pub,
924 b"secret",
925 );
926 let env = seal_with_ephemeral(¶ms, &kat_ephemeral("wr")).unwrap();
927
928 let mut g1 = ReplayGuard::new();
929 assert_eq!(
930 open_message(
931 &eve,
932 &env,
933 kat_resolver(public_key_bytes(&alice)),
934 &mut g1,
935 KAT_NOW
936 )
937 .unwrap_err(),
938 MessageError::OpenFailed,
939 "wrong recipient key cannot open"
940 );
941 let mut g2 = ReplayGuard::new();
942 assert_eq!(
943 open_message(
944 &bob,
945 &env,
946 kat_resolver(public_key_bytes(&eve)),
947 &mut g2,
948 KAT_NOW
949 )
950 .unwrap_err(),
951 MessageError::OpenFailed,
952 "wrong sender key cannot open"
953 );
954 }
955
956 #[test]
957 fn kat_non_subgroup_kem_enc_rejected() {
958 let alice = kat_sk("kat/sg/a");
959 let bob = kat_sk("kat/sg/b");
960 let b_pub = public_key_bytes(&bob);
961 let params = kat_params(
962 &alice,
963 b32_from_seed(b"a"),
964 b32_from_seed(b"b"),
965 &b_pub,
966 b"x",
967 );
968 let mut env = seal_with_ephemeral(¶ms, &kat_ephemeral("sg")).unwrap();
969 env.sealed.kem_enc = Bytes48::new([0xFFu8; 48]);
970 let mut guard = ReplayGuard::new();
971 assert_eq!(
972 open_message(
973 &bob,
974 &env,
975 kat_resolver(public_key_bytes(&alice)),
976 &mut guard,
977 KAT_NOW
978 )
979 .unwrap_err(),
980 MessageError::InvalidPoint
981 );
982 }
983
984 #[test]
985 fn kat_replay_and_expiry() {
986 let alice = kat_sk("kat/re/a");
987 let bob = kat_sk("kat/re/b");
988 let a_pub = public_key_bytes(&alice);
989 let b_pub = public_key_bytes(&bob);
990 let (a_did, b_did) = (b32_from_seed(b"a"), b32_from_seed(b"b"));
991
992 let env = seal_with_ephemeral(
994 &kat_params(&alice, a_did, b_did, &b_pub, b"x"),
995 &kat_ephemeral("re0"),
996 )
997 .unwrap();
998 let mut guard = ReplayGuard::new();
999 assert!(open_message(&bob, &env, kat_resolver(a_pub), &mut guard, KAT_NOW).is_ok());
1000 assert_eq!(
1001 open_message(&bob, &env, kat_resolver(a_pub), &mut guard, KAT_NOW).unwrap_err(),
1002 MessageError::Replay
1003 );
1004
1005 let mut expiring_params = kat_params(&alice, a_did, b_did, &b_pub, b"x");
1007 expiring_params.counter = 1;
1008 expiring_params.expires_at = KAT_NOW + 1000;
1009 let expiring = seal_with_ephemeral(&expiring_params, &kat_ephemeral("re1")).unwrap();
1010 let mut g2 = ReplayGuard::new();
1011 assert_eq!(
1012 open_message(
1013 &bob,
1014 &expiring,
1015 kat_resolver(a_pub),
1016 &mut g2,
1017 KAT_NOW + 5000
1018 )
1019 .unwrap_err(),
1020 MessageError::Expired
1021 );
1022 }
1023
1024 #[test]
1025 fn kat_self_addressed_round_trip() {
1026 let me = kat_sk("kat/self");
1027 let me_pub = public_key_bytes(&me);
1028 let did = b32_from_seed(b"kat/self/did");
1029 let params = kat_params(&me, did, did, &me_pub, b"note to self");
1030 let env = seal_with_ephemeral(¶ms, &kat_ephemeral("self")).unwrap();
1031 let mut guard = ReplayGuard::new();
1032 let opened = open_message(&me, &env, kat_resolver(me_pub), &mut guard, KAT_NOW).unwrap();
1033 assert_eq!(opened.payload, b"note to self");
1034 }
1035}