1use chacha20poly1305::{
2 aead::{Aead, AeadCore, KeyInit},
3 Key, XChaCha20Poly1305, XNonce,
4};
5use ed25519_dalek::{Signature, Verifier};
6use nanoid::nanoid;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use web_time::{SystemTime, UNIX_EPOCH};
10
11use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
12
13use crate::{
14 constants,
15 did::Did,
16 doc::Document,
17 error::{MaError, MaResult as 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 SystemTime::now()
559 .duration_since(UNIX_EPOCH)
560 .map(|duration| duration.as_nanos() as f64 / 1_000_000_000.0)
561 .map_err(|_| MaError::InvalidMessageTimestamp)
562}
563
564fn validate_message_freshness(created_at: f64, ttl: u64) -> Result<()> {
565 let now = now_unix_secs()?;
566
567 if created_at > now + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 {
568 return Err(MaError::MessageFromFuture);
569 }
570
571 if ttl == 0 {
572 return Ok(());
573 }
574
575 if now - created_at > ttl as f64 {
576 return Err(MaError::MessageTooOld);
577 }
578
579 Ok(())
580}
581
582fn compute_shared_secret(
583 ephemeral_key_bytes: &[u8],
584 recipient_key: &EncryptionKey,
585) -> Result<[u8; 32]> {
586 let ephemeral_public = X25519PublicKey::from(
587 <[u8; 32]>::try_from(ephemeral_key_bytes)
588 .map_err(|_| MaError::InvalidEphemeralKeyLength)?,
589 );
590 Ok(recipient_key.shared_secret(&ephemeral_public))
591}
592
593fn derive_symmetric_key(shared_secret: &[u8; 32], label: &str) -> Key {
594 let derived = blake3::derive_key(label, shared_secret);
595 *Key::from_slice(&derived)
596}
597
598fn encrypt(data: &[u8], key: Key) -> Result<Vec<u8>> {
599 let cipher = XChaCha20Poly1305::new(&key);
600 let nonce = XChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
601 let encrypted = cipher.encrypt(&nonce, data).map_err(|_| MaError::Crypto)?;
602
603 let mut out = nonce.to_vec();
604 out.extend_from_slice(&encrypted);
605 Ok(out)
606}
607
608fn decrypt(data: &[u8], shared_secret: &[u8; 32], label: &str) -> Result<Vec<u8>> {
609 if data.len() < 24 {
610 return Err(MaError::CiphertextTooShort);
611 }
612
613 let key = derive_symmetric_key(shared_secret, label);
614 let cipher = XChaCha20Poly1305::new(&key);
615 let nonce = XNonce::from_slice(&data[..24]);
616
617 cipher
618 .decrypt(nonce, &data[24..])
619 .map_err(|_| MaError::Crypto)
620}
621
622fn content_hash(content: &[u8]) -> [u8; 32] {
623 blake3::hash(content).into()
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use crate::{doc::VerificationMethod, key::EncryptionKey};
630
631 fn fixture_documents() -> (
632 SigningKey,
633 EncryptionKey,
634 Document,
635 SigningKey,
636 EncryptionKey,
637 Document,
638 ) {
639 let sender_did = Did::new_url("k51sender", None::<String>).expect("sender did");
640 let sender_sign_url = Did::new_url("k51sender", None::<String>).expect("sender sign did");
641 let sender_enc_url = Did::new_url("k51sender", None::<String>).expect("sender enc did");
642 let sender_signing = SigningKey::generate(sender_sign_url).expect("sender signing key");
643 let sender_encryption =
644 EncryptionKey::generate(sender_enc_url).expect("sender encryption key");
645
646 let recipient_did = Did::new_url("k51recipient", None::<String>).expect("recipient did");
647 let recipient_sign_url =
648 Did::new_url("k51recipient", None::<String>).expect("recipient sign did");
649 let recipient_enc_url =
650 Did::new_url("k51recipient", None::<String>).expect("recipient enc did");
651 let recipient_signing =
652 SigningKey::generate(recipient_sign_url).expect("recipient signing key");
653 let recipient_encryption =
654 EncryptionKey::generate(recipient_enc_url).expect("recipient encryption key");
655
656 let mut sender_document = Document::new(&sender_did, &sender_did);
657 let sender_assertion = VerificationMethod::new(
658 sender_did.base_id(),
659 sender_did.base_id(),
660 sender_signing.key_type.clone(),
661 sender_signing.did.fragment.as_deref().unwrap_or_default(),
662 sender_signing.public_key_multibase.clone(),
663 )
664 .expect("sender assertion vm");
665 let sender_key_agreement = VerificationMethod::new(
666 sender_did.base_id(),
667 sender_did.base_id(),
668 sender_encryption.key_type.clone(),
669 sender_encryption
670 .did
671 .fragment
672 .as_deref()
673 .unwrap_or_default(),
674 sender_encryption.public_key_multibase.clone(),
675 )
676 .expect("sender key agreement vm");
677 sender_document
678 .add_verification_method(sender_assertion.clone())
679 .expect("add sender assertion");
680 sender_document
681 .add_verification_method(sender_key_agreement.clone())
682 .expect("add sender key agreement");
683 sender_document.assertion_method = vec![sender_assertion.id.clone()];
684 sender_document.key_agreement = vec![sender_key_agreement.id.clone()];
685 sender_document
686 .sign(&sender_signing, &sender_assertion)
687 .expect("sign sender doc");
688
689 let mut recipient_document = Document::new(&recipient_did, &recipient_did);
690 let recipient_assertion = VerificationMethod::new(
691 recipient_did.base_id(),
692 recipient_did.base_id(),
693 recipient_signing.key_type.clone(),
694 recipient_signing
695 .did
696 .fragment
697 .as_deref()
698 .unwrap_or_default(),
699 recipient_signing.public_key_multibase.clone(),
700 )
701 .expect("recipient assertion vm");
702 let recipient_key_agreement = VerificationMethod::new(
703 recipient_did.base_id(),
704 recipient_did.base_id(),
705 recipient_encryption.key_type.clone(),
706 recipient_encryption
707 .did
708 .fragment
709 .as_deref()
710 .unwrap_or_default(),
711 recipient_encryption.public_key_multibase.clone(),
712 )
713 .expect("recipient key agreement vm");
714 recipient_document
715 .add_verification_method(recipient_assertion.clone())
716 .expect("add recipient assertion");
717 recipient_document
718 .add_verification_method(recipient_key_agreement.clone())
719 .expect("add recipient key agreement");
720 recipient_document.assertion_method = vec![recipient_assertion.id.clone()];
721 recipient_document.key_agreement = vec![recipient_key_agreement.id.clone()];
722 recipient_document
723 .sign(&recipient_signing, &recipient_assertion)
724 .expect("sign recipient doc");
725
726 (
727 sender_signing,
728 sender_encryption,
729 sender_document,
730 recipient_signing,
731 recipient_encryption,
732 recipient_document,
733 )
734 }
735
736 #[test]
737 fn did_round_trip() {
738 let did = Did::new_url(
739 "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
740 Some("bahner"),
741 )
742 .expect("did must build");
743 let parsed = Did::try_from(did.id().as_str()).expect("did must parse");
744 assert_eq!(did, parsed);
745 }
746
747 #[test]
748 fn subject_url_round_trip() {
749 let did = Did::new_url(
750 "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
751 None::<String>,
752 )
753 .expect("subject did must build");
754 let parsed = Did::try_from(did.id().as_str()).expect("subject did must parse");
755 assert_eq!(did, parsed);
756 }
757
758 #[test]
759 fn document_signs_and_verifies() {
760 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
761 sender_signing.validate().expect("signing key validates");
762 sender_document.validate().expect("document validates");
763 }
764
765 #[test]
766 fn envelope_round_trip() {
767 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
768 fixture_documents();
769 let message = Message::new(
770 sender_document.id.clone(),
771 recipient_document.id.clone(),
772 "application/x-ma",
773 b"look".to_vec(),
774 &sender_signing,
775 )
776 .expect("message creation");
777 message
778 .verify_with_document(&sender_document)
779 .expect("message signature verifies");
780
781 let envelope = message
782 .enclose_for(&recipient_document)
783 .expect("message encloses");
784 let opened = envelope
785 .open(&recipient_document, &recipient_encryption, &sender_document)
786 .expect("envelope opens");
787
788 assert_eq!(opened.content, b"look");
789 assert_eq!(opened.from, sender_document.id);
790 assert_eq!(opened.to, recipient_document.id);
791 }
792
793 #[test]
794 fn tampered_content_fails_signature_verification() {
795 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
796 let mut message = Message::new(
797 sender_document.id.clone(),
798 recipient_document.id.clone(),
799 "application/x-ma",
800 b"look".to_vec(),
801 &sender_signing,
802 )
803 .expect("message creation");
804
805 message.content = b"tampered".to_vec();
806 let result = message.verify_with_document(&sender_document);
807 assert!(matches!(result, Err(MaError::InvalidMessageSignature)));
808 }
809
810 #[test]
811 fn stale_message_is_rejected() {
812 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
813 let mut message = Message::new(
814 sender_document.id.clone(),
815 recipient_document.id.clone(),
816 "application/x-ma",
817 b"look".to_vec(),
818 &sender_signing,
819 )
820 .expect("message creation");
821
822 message.created_at = 0.0;
823 let result = message.verify_with_document(&sender_document);
824 assert!(matches!(result, Err(MaError::MessageTooOld)));
825 }
826
827 #[test]
828 fn future_message_is_rejected() {
829 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
830 let mut message = Message::new(
831 sender_document.id.clone(),
832 recipient_document.id.clone(),
833 "application/x-ma",
834 b"look".to_vec(),
835 &sender_signing,
836 )
837 .expect("message creation");
838
839 message.created_at =
840 now_unix_secs().expect("current timestamp") + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 + 60.0;
841 message
842 .sign(&sender_signing)
843 .expect("re-sign with updated timestamp");
844
845 let result = message.verify_with_document(&sender_document);
846 assert!(matches!(result, Err(MaError::MessageFromFuture)));
847 }
848
849 #[test]
850 fn ttl_zero_disables_expiration() {
851 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
852 let mut message = Message::new(
853 sender_document.id.clone(),
854 recipient_document.id.clone(),
855 "application/x-ma",
856 b"look".to_vec(),
857 &sender_signing,
858 )
859 .expect("message creation");
860
861 message.created_at = 0.0;
862 message.ttl = 0;
863 message.sign(&sender_signing).expect("re-sign with ttl=0");
864
865 message
866 .verify_with_document(&sender_document)
867 .expect("ttl=0 should bypass max-age rejection");
868 }
869
870 #[test]
871 fn custom_ttl_rejects_expired_message() {
872 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
873 let mut message = Message::new_with_ttl(
874 sender_document.id.clone(),
875 recipient_document.id.clone(),
876 "application/x-ma",
877 b"look".to_vec(),
878 1,
879 &sender_signing,
880 )
881 .expect("message creation with ttl");
882
883 message.created_at = now_unix_secs().expect("current timestamp") - 5.0;
884 message
885 .sign(&sender_signing)
886 .expect("re-sign with stale timestamp");
887
888 let result = message.verify_with_document(&sender_document);
889 assert!(matches!(result, Err(MaError::MessageTooOld)));
890 }
891
892 #[test]
893 fn replay_guard_rejects_duplicate_envelope() {
894 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
895 fixture_documents();
896 let message = Message::new(
897 sender_document.id.clone(),
898 recipient_document.id.clone(),
899 "application/x-ma",
900 b"look".to_vec(),
901 &sender_signing,
902 )
903 .expect("message creation");
904
905 let envelope = message
906 .enclose_for(&recipient_document)
907 .expect("message encloses");
908 let mut replay_guard = ReplayGuard::default();
909
910 envelope
911 .open_with_replay_guard(
912 &recipient_document,
913 &recipient_encryption,
914 &sender_document,
915 &mut replay_guard,
916 )
917 .expect("first delivery accepted");
918
919 let second = envelope.open_with_replay_guard(
920 &recipient_document,
921 &recipient_encryption,
922 &sender_document,
923 &mut replay_guard,
924 );
925 assert!(matches!(second, Err(MaError::ReplayDetected)));
926 }
927
928 #[test]
929 fn broadcast_allows_empty_recipient() {
930 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
931 let message = Message::new(
932 sender_document.id.clone(),
933 String::new(),
934 "application/x-ma-broadcast",
935 b"hello everyone".to_vec(),
936 &sender_signing,
937 )
938 .expect("broadcast message creation");
939
940 message
941 .verify_with_document(&sender_document)
942 .expect("broadcast with empty recipient verifies");
943 }
944
945 #[test]
946 fn broadcast_rejects_recipient() {
947 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
948 let result = Message::new(
949 sender_document.id.clone(),
950 recipient_document.id.clone(),
951 "application/x-ma-broadcast",
952 b"hello everyone".to_vec(),
953 &sender_signing,
954 );
955
956 assert!(matches!(
957 result,
958 Err(MaError::BroadcastMustNotHaveRecipient)
959 ));
960 }
961
962 #[test]
963 fn message_requires_recipient() {
964 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
965 let result = Message::new(
966 sender_document.id.clone(),
967 String::new(),
968 "application/x-ma-message",
969 b"secret".to_vec(),
970 &sender_signing,
971 );
972
973 assert!(matches!(result, Err(MaError::MessageRequiresRecipient)));
974 }
975
976 #[test]
977 fn unknown_content_type_allows_empty_recipient() {
978 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
979 let message = Message::new(
980 sender_document.id.clone(),
981 String::new(),
982 "application/x-ma-custom",
983 b"whatever".to_vec(),
984 &sender_signing,
985 )
986 .expect("custom content type message creation");
987
988 message
989 .verify_with_document(&sender_document)
990 .expect("custom type with empty recipient verifies");
991 }
992
993 #[test]
994 fn unknown_content_type_allows_recipient() {
995 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
996 let message = Message::new(
997 sender_document.id.clone(),
998 recipient_document.id.clone(),
999 "application/x-ma-custom",
1000 b"whatever".to_vec(),
1001 &sender_signing,
1002 )
1003 .expect("custom content type with recipient");
1004
1005 message
1006 .verify_with_document(&sender_document)
1007 .expect("custom type with recipient verifies");
1008 }
1009}