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