1use chacha20poly1305::{
2 Key, XChaCha20Poly1305, XNonce,
3 aead::{Aead, AeadCore, KeyInit},
4};
5use ed25519_dalek::{Signature, Verifier};
6use nanoid::nanoid;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9#[cfg(not(target_arch = "wasm32"))]
10use std::time::{SystemTime, UNIX_EPOCH};
11use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
12
13use crate::{
14 constants,
15 did::Did,
16 doc::Document,
17 error::{MaError, Result},
18 key::{EncryptionKey, SigningKey},
19};
20
21pub const MESSAGE_PREFIX: &str = "/ma/";
22
23pub const DEFAULT_REPLAY_WINDOW_SECS: u64 = 120;
24pub const DEFAULT_MAX_CLOCK_SKEW_SECS: u64 = 30;
25pub const DEFAULT_MESSAGE_TTL_SECS: u64 = 3600;
26
27fn default_message_ttl_secs() -> u64 {
28 DEFAULT_MESSAGE_TTL_SECS
29}
30
31pub fn message_type() -> String {
32 format!("{MESSAGE_PREFIX}{}", constants::VERSION)
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Headers {
41 pub id: String,
42 #[serde(rename = "type")]
43 pub message_type: String,
44 pub from: String,
45 pub to: String,
46 #[serde(rename = "createdAt")]
47 pub created_at: u64,
48 #[serde(default = "default_message_ttl_secs")]
49 pub ttl: u64,
50 #[serde(rename = "contentType")]
51 pub content_type: String,
52 #[serde(default, skip_serializing_if = "Option::is_none", rename = "replyTo")]
53 pub reply_to: Option<String>,
54 #[serde(rename = "contentHash")]
55 pub content_hash: [u8; 32],
56 pub signature: Vec<u8>,
57}
58
59impl Headers {
60 pub fn validate(&self) -> Result<()> {
61 validate_message_id(&self.id)?;
62 validate_message_type(&self.message_type)?;
63 if let Some(reply_to) = &self.reply_to {
64 validate_message_id(reply_to)?;
65 }
66
67 if self.content_type.is_empty() {
68 return Err(MaError::MissingContentType);
69 }
70
71 Did::validate(&self.from)?;
72 let recipient_is_empty = self.to.trim().is_empty();
73 if recipient_is_empty {
74 if self.content_type != "application/x-ma-chat" {
75 return Err(MaError::InvalidRecipient);
76 }
77 } else {
78 Did::validate(&self.to).map_err(|_| MaError::InvalidRecipient)?;
79 if self.from == self.to {
80 return Err(MaError::SameActor);
81 }
82 }
83 validate_message_freshness(self.created_at, self.ttl)?;
84
85 Ok(())
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct Message {
128 pub id: String,
129 #[serde(rename = "type")]
130 pub message_type: String,
131 pub from: String,
132 pub to: String,
133 #[serde(rename = "createdAt")]
134 pub created_at: u64,
135 #[serde(default = "default_message_ttl_secs")]
136 pub ttl: u64,
137 #[serde(rename = "contentType")]
138 pub content_type: String,
139 #[serde(default, skip_serializing_if = "Option::is_none", rename = "replyTo")]
140 pub reply_to: Option<String>,
141 pub content: Vec<u8>,
142 pub signature: Vec<u8>,
143}
144
145impl Message {
146 pub fn new(
147 from: impl Into<String>,
148 to: impl Into<String>,
149 content_type: impl Into<String>,
150 content: Vec<u8>,
151 signing_key: &SigningKey,
152 ) -> Result<Self> {
153 Self::new_with_ttl(
154 from,
155 to,
156 content_type,
157 content,
158 DEFAULT_MESSAGE_TTL_SECS,
159 signing_key,
160 )
161 }
162
163 pub fn new_with_ttl(
164 from: impl Into<String>,
165 to: impl Into<String>,
166 content_type: impl Into<String>,
167 content: Vec<u8>,
168 ttl: u64,
169 signing_key: &SigningKey,
170 ) -> Result<Self> {
171 let mut message = Self {
172 id: nanoid!(),
173 message_type: message_type(),
174 from: from.into(),
175 to: to.into(),
176 created_at: now_unix_secs()?,
177 ttl,
178 content_type: content_type.into(),
179 reply_to: None,
180 content,
181 signature: Vec::new(),
182 };
183
184 message.validate_content()?;
185 message.sign(signing_key)?;
186 Ok(message)
187 }
188
189 pub fn to_cbor(&self) -> Result<Vec<u8>> {
190 let mut out = Vec::new();
191 ciborium::ser::into_writer(self, &mut out)
192 .map_err(|error| MaError::CborEncode(error.to_string()))?;
193 Ok(out)
194 }
195
196 pub fn from_cbor(bytes: &[u8]) -> Result<Self> {
197 ciborium::de::from_reader(bytes).map_err(|error| MaError::CborDecode(error.to_string()))
198 }
199
200 pub fn unsigned_headers(&self) -> Headers {
201 Headers {
202 id: self.id.clone(),
203 message_type: self.message_type.clone(),
204 from: self.from.clone(),
205 to: self.to.clone(),
206 created_at: self.created_at,
207 ttl: self.ttl,
208 content_type: self.content_type.clone(),
209 reply_to: self.reply_to.clone(),
210 content_hash: content_hash(&self.content),
211 signature: Vec::new(),
212 }
213 }
214
215 pub fn headers(&self) -> Headers {
216 let mut headers = self.unsigned_headers();
217 headers.signature = self.signature.clone();
218 headers
219 }
220
221 pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
222 let bytes = self.unsigned_headers_cbor()?;
223 self.signature = signing_key.sign(&bytes);
224 Ok(())
225 }
226
227 pub fn verify_with_document(&self, sender_document: &Document) -> Result<()> {
228 if self.from.is_empty() {
229 return Err(MaError::MissingSender);
230 }
231
232 if self.signature.is_empty() {
233 return Err(MaError::MissingSignature);
234 }
235
236 let sender_did = Did::try_from(self.from.as_str())?;
237 if sender_document.id != sender_did.base_id() {
238 return Err(MaError::InvalidRecipient);
239 }
240
241 self.headers().validate()?;
242 let bytes = self.unsigned_headers_cbor()?;
243 let signature =
244 Signature::from_slice(&self.signature).map_err(|_| MaError::InvalidMessageSignature)?;
245 sender_document
246 .assertion_method_public_key()?
247 .verify(&bytes, &signature)
248 .map_err(|_| MaError::InvalidMessageSignature)
249 }
250
251 pub fn enclose_for(&self, recipient_document: &Document) -> Result<Envelope> {
252 self.headers().validate()?;
253
254 let recipient_public_key =
255 X25519PublicKey::from(recipient_document.key_agreement_public_key_bytes()?);
256 let ephemeral_secret = StaticSecret::random_from_rng(rand_core::OsRng);
257 let ephemeral_public = X25519PublicKey::from(&ephemeral_secret);
258 let shared_secret = ephemeral_secret
259 .diffie_hellman(&recipient_public_key)
260 .to_bytes();
261
262 let encrypted_headers = encrypt(
263 &self.headers_cbor()?,
264 derive_symmetric_key(&shared_secret, constants::BLAKE3_HEADERS_LABEL),
265 )?;
266
267 let encrypted_content = encrypt(
268 &self.content,
269 derive_symmetric_key(&shared_secret, constants::blake3_content_label()),
270 )?;
271
272 Ok(Envelope {
273 ephemeral_key: ephemeral_public.as_bytes().to_vec(),
274 encrypted_content,
275 encrypted_headers,
276 })
277 }
278
279 fn headers_cbor(&self) -> Result<Vec<u8>> {
280 let mut out = Vec::new();
281 ciborium::ser::into_writer(&self.headers(), &mut out)
282 .map_err(|error| MaError::CborEncode(error.to_string()))?;
283 Ok(out)
284 }
285
286 fn unsigned_headers_cbor(&self) -> Result<Vec<u8>> {
287 let mut out = Vec::new();
288 ciborium::ser::into_writer(&self.unsigned_headers(), &mut out)
289 .map_err(|error| MaError::CborEncode(error.to_string()))?;
290 Ok(out)
291 }
292
293 fn validate_content(&self) -> Result<()> {
294 if self.content.is_empty() {
295 return Err(MaError::MissingContent);
296 }
297 Ok(())
298 }
299
300 fn from_headers(headers: Headers) -> Result<Self> {
301 headers.validate()?;
302 Ok(Self {
303 id: headers.id,
304 message_type: headers.message_type,
305 from: headers.from,
306 to: headers.to,
307 created_at: headers.created_at,
308 ttl: headers.ttl,
309 content_type: headers.content_type,
310 reply_to: headers.reply_to,
311 content: Vec::new(),
312 signature: headers.signature,
313 })
314 }
315}
316
317#[derive(Debug, Clone)]
333pub struct ReplayGuard {
334 seen: HashMap<String, u64>,
335 window_secs: u64,
336}
337
338impl Default for ReplayGuard {
339 fn default() -> Self {
340 Self::new(DEFAULT_REPLAY_WINDOW_SECS)
341 }
342}
343
344impl ReplayGuard {
345 pub fn new(window_secs: u64) -> Self {
346 Self {
347 seen: HashMap::new(),
348 window_secs,
349 }
350 }
351
352 pub fn check_and_insert(&mut self, headers: &Headers) -> Result<()> {
353 headers.validate()?;
354 self.prune_old()?;
355 if self.seen.contains_key(&headers.id) {
356 return Err(MaError::ReplayDetected);
357 }
358 self.seen.insert(headers.id.clone(), now_unix_secs()?);
359 Ok(())
360 }
361
362 fn prune_old(&mut self) -> Result<()> {
363 let now = now_unix_secs()?;
364 self.seen
365 .retain(|_, seen_at| now.saturating_sub(*seen_at) <= self.window_secs);
366 Ok(())
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
411pub struct Envelope {
412 #[serde(rename = "ephemeralKey")]
413 pub ephemeral_key: Vec<u8>,
414 #[serde(rename = "encryptedContent")]
415 pub encrypted_content: Vec<u8>,
416 #[serde(rename = "encryptedHeaders")]
417 pub encrypted_headers: Vec<u8>,
418}
419
420impl Envelope {
421 pub fn verify(&self) -> Result<()> {
422 if self.ephemeral_key.is_empty() {
423 return Err(MaError::MissingEnvelopeField("ephemeralKey"));
424 }
425 if self.ephemeral_key.len() != 32 {
426 return Err(MaError::InvalidEphemeralKeyLength);
427 }
428 if self.encrypted_content.is_empty() {
429 return Err(MaError::MissingEnvelopeField("encryptedContent"));
430 }
431 if self.encrypted_headers.is_empty() {
432 return Err(MaError::MissingEnvelopeField("encryptedHeaders"));
433 }
434 Ok(())
435 }
436
437 pub fn to_cbor(&self) -> Result<Vec<u8>> {
438 let mut out = Vec::new();
439 ciborium::ser::into_writer(self, &mut out)
440 .map_err(|error| MaError::CborEncode(error.to_string()))?;
441 Ok(out)
442 }
443
444 pub fn from_cbor(bytes: &[u8]) -> Result<Self> {
445 ciborium::de::from_reader(bytes).map_err(|error| MaError::CborDecode(error.to_string()))
446 }
447
448 pub fn open(
449 &self,
450 recipient_document: &Document,
451 recipient_key: &EncryptionKey,
452 sender_document: &Document,
453 ) -> Result<Message> {
454 self.verify()?;
455
456 if recipient_document.id == sender_document.id {
457 return Err(MaError::SameActor);
458 }
459
460 let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
461 let headers = self.decrypt_headers(&shared_secret)?;
462 headers.validate()?;
463 let content = self.decrypt_content(&shared_secret)?;
464
465 let mut message = Message::from_headers(headers)?;
466 message.content = content;
467 message.verify_with_document(sender_document)?;
468 Ok(message)
469 }
470
471 pub fn open_with_replay_guard(
472 &self,
473 recipient_document: &Document,
474 recipient_key: &EncryptionKey,
475 sender_document: &Document,
476 replay_guard: &mut ReplayGuard,
477 ) -> Result<Message> {
478 self.verify()?;
479
480 if recipient_document.id == sender_document.id {
481 return Err(MaError::SameActor);
482 }
483
484 let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
485 let headers = self.decrypt_headers(&shared_secret)?;
486 replay_guard.check_and_insert(&headers)?;
487 let content = self.decrypt_content(&shared_secret)?;
488
489 let mut message = Message::from_headers(headers)?;
490 message.content = content;
491 message.verify_with_document(sender_document)?;
492 Ok(message)
493 }
494
495 fn decrypt_headers(&self, shared_secret: &[u8; 32]) -> Result<Headers> {
496 let decrypted = decrypt(
497 &self.encrypted_headers,
498 shared_secret,
499 constants::BLAKE3_HEADERS_LABEL,
500 )?;
501 ciborium::de::from_reader(decrypted.as_slice())
502 .map_err(|error| MaError::CborDecode(error.to_string()))
503 }
504
505 fn decrypt_content(&self, shared_secret: &[u8; 32]) -> Result<Vec<u8>> {
506 decrypt(
507 &self.encrypted_content,
508 shared_secret,
509 constants::blake3_content_label(),
510 )
511 }
512}
513
514fn validate_message_id(id: &str) -> Result<()> {
515 if id.is_empty() {
516 return Err(MaError::EmptyMessageId);
517 }
518
519 if !id
520 .chars()
521 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
522 {
523 return Err(MaError::InvalidMessageId);
524 }
525
526 Ok(())
527}
528
529fn validate_message_type(kind: &str) -> Result<()> {
530 if kind == message_type() {
531 return Ok(());
532 }
533
534 Err(MaError::InvalidMessageType)
535}
536
537fn now_unix_secs() -> Result<u64> {
538 #[cfg(target_arch = "wasm32")]
539 {
540 return Ok((js_sys::Date::now() / 1000.0) as u64);
542 }
543
544 #[cfg(not(target_arch = "wasm32"))]
545 SystemTime::now()
546 .duration_since(UNIX_EPOCH)
547 .map(|duration| duration.as_secs())
548 .map_err(|_| MaError::InvalidMessageTimestamp)
549}
550
551fn validate_message_freshness(created_at: u64, ttl: u64) -> Result<()> {
552 let now = now_unix_secs()?;
553
554 if created_at > now.saturating_add(DEFAULT_MAX_CLOCK_SKEW_SECS) {
555 return Err(MaError::MessageFromFuture);
556 }
557
558 if ttl == 0 {
559 return Ok(());
560 }
561
562 if now.saturating_sub(created_at) > ttl {
563 return Err(MaError::MessageTooOld);
564 }
565
566 Ok(())
567}
568
569fn compute_shared_secret(
570 ephemeral_key_bytes: &[u8],
571 recipient_key: &EncryptionKey,
572) -> Result<[u8; 32]> {
573 let ephemeral_public = X25519PublicKey::from(
574 <[u8; 32]>::try_from(ephemeral_key_bytes)
575 .map_err(|_| MaError::InvalidEphemeralKeyLength)?,
576 );
577 Ok(recipient_key.shared_secret(&ephemeral_public))
578}
579
580fn derive_symmetric_key(shared_secret: &[u8; 32], label: &str) -> Key {
581 let derived = blake3::derive_key(label, shared_secret);
582 *Key::from_slice(&derived)
583}
584
585fn encrypt(data: &[u8], key: Key) -> Result<Vec<u8>> {
586 let cipher = XChaCha20Poly1305::new(&key);
587 let nonce = XChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
588 let encrypted = cipher.encrypt(&nonce, data).map_err(|_| MaError::Crypto)?;
589
590 let mut out = nonce.to_vec();
591 out.extend_from_slice(&encrypted);
592 Ok(out)
593}
594
595fn decrypt(data: &[u8], shared_secret: &[u8; 32], label: &str) -> Result<Vec<u8>> {
596 if data.len() < 24 {
597 return Err(MaError::CiphertextTooShort);
598 }
599
600 let key = derive_symmetric_key(shared_secret, label);
601 let cipher = XChaCha20Poly1305::new(&key);
602 let nonce = XNonce::from_slice(&data[..24]);
603
604 cipher
605 .decrypt(nonce, &data[24..])
606 .map_err(|_| MaError::Crypto)
607}
608
609fn content_hash(content: &[u8]) -> [u8; 32] {
610 blake3::hash(content).into()
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616 use crate::{doc::VerificationMethod, key::EncryptionKey};
617
618 fn fixture_documents() -> (
619 SigningKey,
620 EncryptionKey,
621 Document,
622 SigningKey,
623 EncryptionKey,
624 Document,
625 ) {
626 let sender_did = Did::new_url("k51sender", None::<String>).expect("sender did");
627 let sender_sign_url = Did::new_url("k51sender", None::<String>).expect("sender sign did");
628 let sender_enc_url = Did::new_url("k51sender", None::<String>).expect("sender enc did");
629 let sender_signing = SigningKey::generate(sender_sign_url).expect("sender signing key");
630 let sender_encryption =
631 EncryptionKey::generate(sender_enc_url).expect("sender encryption key");
632
633 let recipient_did = Did::new_url("k51recipient", None::<String>).expect("recipient did");
634 let recipient_sign_url =
635 Did::new_url("k51recipient", None::<String>).expect("recipient sign did");
636 let recipient_enc_url =
637 Did::new_url("k51recipient", None::<String>).expect("recipient enc did");
638 let recipient_signing =
639 SigningKey::generate(recipient_sign_url).expect("recipient signing key");
640 let recipient_encryption =
641 EncryptionKey::generate(recipient_enc_url).expect("recipient encryption key");
642
643 let mut sender_document = Document::new(&sender_did, &sender_did);
644 let sender_assertion = VerificationMethod::new(
645 sender_did.base_id(),
646 sender_did.base_id(),
647 sender_signing.key_type.clone(),
648 sender_signing.did.fragment.as_deref().unwrap_or_default(),
649 sender_signing.public_key_multibase.clone(),
650 )
651 .expect("sender assertion vm");
652 let sender_key_agreement = VerificationMethod::new(
653 sender_did.base_id(),
654 sender_did.base_id(),
655 sender_encryption.key_type.clone(),
656 sender_encryption
657 .did
658 .fragment
659 .as_deref()
660 .unwrap_or_default(),
661 sender_encryption.public_key_multibase.clone(),
662 )
663 .expect("sender key agreement vm");
664 sender_document
665 .add_verification_method(sender_assertion.clone())
666 .expect("add sender assertion");
667 sender_document
668 .add_verification_method(sender_key_agreement.clone())
669 .expect("add sender key agreement");
670 sender_document.assertion_method = vec![sender_assertion.id.clone()];
671 sender_document.key_agreement = vec![sender_key_agreement.id.clone()];
672 sender_document
673 .sign(&sender_signing, &sender_assertion)
674 .expect("sign sender doc");
675
676 let mut recipient_document = Document::new(&recipient_did, &recipient_did);
677 let recipient_assertion = VerificationMethod::new(
678 recipient_did.base_id(),
679 recipient_did.base_id(),
680 recipient_signing.key_type.clone(),
681 recipient_signing
682 .did
683 .fragment
684 .as_deref()
685 .unwrap_or_default(),
686 recipient_signing.public_key_multibase.clone(),
687 )
688 .expect("recipient assertion vm");
689 let recipient_key_agreement = VerificationMethod::new(
690 recipient_did.base_id(),
691 recipient_did.base_id(),
692 recipient_encryption.key_type.clone(),
693 recipient_encryption
694 .did
695 .fragment
696 .as_deref()
697 .unwrap_or_default(),
698 recipient_encryption.public_key_multibase.clone(),
699 )
700 .expect("recipient key agreement vm");
701 recipient_document
702 .add_verification_method(recipient_assertion.clone())
703 .expect("add recipient assertion");
704 recipient_document
705 .add_verification_method(recipient_key_agreement.clone())
706 .expect("add recipient key agreement");
707 recipient_document.assertion_method = vec![recipient_assertion.id.clone()];
708 recipient_document.key_agreement = vec![recipient_key_agreement.id.clone()];
709 recipient_document
710 .sign(&recipient_signing, &recipient_assertion)
711 .expect("sign recipient doc");
712
713 (
714 sender_signing,
715 sender_encryption,
716 sender_document,
717 recipient_signing,
718 recipient_encryption,
719 recipient_document,
720 )
721 }
722
723 #[test]
724 fn did_round_trip() {
725 let did = Did::new_url(
726 "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
727 Some("bahner"),
728 )
729 .expect("did must build");
730 let parsed = Did::try_from(did.id().as_str()).expect("did must parse");
731 assert_eq!(did, parsed);
732 }
733
734 #[test]
735 fn subject_url_round_trip() {
736 let did = Did::new_url(
737 "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
738 None::<String>,
739 )
740 .expect("subject did must build");
741 let parsed = Did::try_from(did.id().as_str()).expect("subject did must parse");
742 assert_eq!(did, parsed);
743 }
744
745 #[test]
746 fn document_signs_and_verifies() {
747 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
748 sender_signing.validate().expect("signing key validates");
749 sender_document.validate().expect("document validates");
750 }
751
752 #[test]
753 fn envelope_round_trip() {
754 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
755 fixture_documents();
756 let message = Message::new(
757 sender_document.id.clone(),
758 recipient_document.id.clone(),
759 "application/x-ma",
760 b"look".to_vec(),
761 &sender_signing,
762 )
763 .expect("message creation");
764 message
765 .verify_with_document(&sender_document)
766 .expect("message signature verifies");
767
768 let envelope = message
769 .enclose_for(&recipient_document)
770 .expect("message encloses");
771 let opened = envelope
772 .open(&recipient_document, &recipient_encryption, &sender_document)
773 .expect("envelope opens");
774
775 assert_eq!(opened.content, b"look");
776 assert_eq!(opened.from, sender_document.id);
777 assert_eq!(opened.to, recipient_document.id);
778 }
779
780 #[test]
781 fn tampered_content_fails_signature_verification() {
782 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
783 let mut message = Message::new(
784 sender_document.id.clone(),
785 recipient_document.id.clone(),
786 "application/x-ma",
787 b"look".to_vec(),
788 &sender_signing,
789 )
790 .expect("message creation");
791
792 message.content = b"tampered".to_vec();
793 let result = message.verify_with_document(&sender_document);
794 assert!(matches!(result, Err(MaError::InvalidMessageSignature)));
795 }
796
797 #[test]
798 fn stale_message_is_rejected() {
799 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
800 let mut message = Message::new(
801 sender_document.id.clone(),
802 recipient_document.id.clone(),
803 "application/x-ma",
804 b"look".to_vec(),
805 &sender_signing,
806 )
807 .expect("message creation");
808
809 message.created_at = 0;
810 let result = message.verify_with_document(&sender_document);
811 assert!(matches!(result, Err(MaError::MessageTooOld)));
812 }
813
814 #[test]
815 fn future_message_is_rejected() {
816 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
817 let mut message = Message::new(
818 sender_document.id.clone(),
819 recipient_document.id.clone(),
820 "application/x-ma",
821 b"look".to_vec(),
822 &sender_signing,
823 )
824 .expect("message creation");
825
826 message.created_at =
827 now_unix_secs().expect("current timestamp") + DEFAULT_MAX_CLOCK_SKEW_SECS + 60;
828 message
829 .sign(&sender_signing)
830 .expect("re-sign with updated timestamp");
831
832 let result = message.verify_with_document(&sender_document);
833 assert!(matches!(result, Err(MaError::MessageFromFuture)));
834 }
835
836 #[test]
837 fn ttl_zero_disables_expiration() {
838 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
839 let mut message = Message::new(
840 sender_document.id.clone(),
841 recipient_document.id.clone(),
842 "application/x-ma",
843 b"look".to_vec(),
844 &sender_signing,
845 )
846 .expect("message creation");
847
848 message.created_at = 0;
849 message.ttl = 0;
850 message.sign(&sender_signing).expect("re-sign with ttl=0");
851
852 message
853 .verify_with_document(&sender_document)
854 .expect("ttl=0 should bypass max-age rejection");
855 }
856
857 #[test]
858 fn custom_ttl_rejects_expired_message() {
859 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
860 let mut message = Message::new_with_ttl(
861 sender_document.id.clone(),
862 recipient_document.id.clone(),
863 "application/x-ma",
864 b"look".to_vec(),
865 1,
866 &sender_signing,
867 )
868 .expect("message creation with ttl");
869
870 message.created_at = now_unix_secs()
871 .expect("current timestamp")
872 .saturating_sub(5);
873 message
874 .sign(&sender_signing)
875 .expect("re-sign with stale timestamp");
876
877 let result = message.verify_with_document(&sender_document);
878 assert!(matches!(result, Err(MaError::MessageTooOld)));
879 }
880
881 #[test]
882 fn replay_guard_rejects_duplicate_envelope() {
883 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
884 fixture_documents();
885 let message = Message::new(
886 sender_document.id.clone(),
887 recipient_document.id.clone(),
888 "application/x-ma",
889 b"look".to_vec(),
890 &sender_signing,
891 )
892 .expect("message creation");
893
894 let envelope = message
895 .enclose_for(&recipient_document)
896 .expect("message encloses");
897 let mut replay_guard = ReplayGuard::default();
898
899 envelope
900 .open_with_replay_guard(
901 &recipient_document,
902 &recipient_encryption,
903 &sender_document,
904 &mut replay_guard,
905 )
906 .expect("first delivery accepted");
907
908 let second = envelope.open_with_replay_guard(
909 &recipient_document,
910 &recipient_encryption,
911 &sender_document,
912 &mut replay_guard,
913 );
914 assert!(matches!(second, Err(MaError::ReplayDetected)));
915 }
916
917 #[test]
918 fn chat_allows_empty_recipient() {
919 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
920 let message = Message::new(
921 sender_document.id.clone(),
922 String::new(),
923 "application/x-ma-chat",
924 b"hello room".to_vec(),
925 &sender_signing,
926 )
927 .expect("chat message creation");
928
929 message
930 .verify_with_document(&sender_document)
931 .expect("chat with empty recipient verifies");
932 }
933
934 #[test]
935 fn non_chat_rejects_empty_recipient() {
936 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
937 let message = Message::new(
938 sender_document.id.clone(),
939 String::new(),
940 "application/x-ma-cmd",
941 b"look".to_vec(),
942 &sender_signing,
943 )
944 .expect("command message creation");
945
946 let result = message.verify_with_document(&sender_document);
947 assert!(matches!(result, Err(MaError::InvalidRecipient)));
948 }
949}