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