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