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