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