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/vnd.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: u64,
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 if self.message_type == crate::service::MESSAGE_TYPE_BROADCAST {
96 if !recipient_is_empty {
97 return Err(MaError::BroadcastMustNotHaveRecipient);
98 }
99 } else {
100 if recipient_is_empty {
101 return Err(MaError::MessageRequiresRecipient);
102 }
103 Did::validate_url(&self.to).map_err(|_| MaError::InvalidRecipient)?;
104 }
105 validate_message_freshness(self.created_at, self.exp)?;
106
107 Ok(())
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub struct Message {
151 pub id: String,
152 #[serde(rename = "protocol")]
153 pub protocol: String,
154 #[serde(rename = "type")]
155 pub message_type: String,
156 pub from: String,
157 pub to: String,
158 #[serde(rename = "createdAt")]
159 pub created_at: u64,
160 #[serde(default)]
161 pub exp: u64,
162 #[serde(rename = "contentType")]
163 pub content_type: String,
164 #[serde(default, skip_serializing_if = "Option::is_none", rename = "replyTo")]
165 pub reply_to: Option<String>,
166 pub content: Vec<u8>,
167 pub signature: Vec<u8>,
168}
169
170struct MessageOptions {
171 exp: u64,
172 reply_to: Option<String>,
173}
174
175impl Message {
176 pub fn new(
177 from: impl Into<String>,
178 to: impl Into<String>,
179 message_type: impl Into<String>,
180 content_type: impl Into<String>,
181 content: &[u8],
182 signing_key: &SigningKey,
183 ) -> Result<Self> {
184 let exp = now_unix_secs()? + DEFAULT_MESSAGE_TTL_SECS;
185 Self::new_with_exp(
186 from,
187 to,
188 message_type,
189 content_type,
190 content,
191 exp,
192 signing_key,
193 )
194 }
195
196 pub fn new_reply(
198 from: impl Into<String>,
199 to: impl Into<String>,
200 message_type: impl Into<String>,
201 content_type: impl Into<String>,
202 content: &[u8],
203 reply_to: impl Into<String>,
204 signing_key: &SigningKey,
205 ) -> Result<Self> {
206 let exp = now_unix_secs()? + DEFAULT_MESSAGE_TTL_SECS;
207 Self::new_with_options(
208 from,
209 to,
210 message_type,
211 content_type,
212 content,
213 MessageOptions {
214 exp,
215 reply_to: Some(reply_to.into()),
216 },
217 signing_key,
218 )
219 }
220
221 pub fn new_with_exp(
222 from: impl Into<String>,
223 to: impl Into<String>,
224 message_type: impl Into<String>,
225 content_type: impl Into<String>,
226 content: &[u8],
227 exp: u64,
228 signing_key: &SigningKey,
229 ) -> Result<Self> {
230 Self::new_with_options(
231 from,
232 to,
233 message_type,
234 content_type,
235 content,
236 MessageOptions {
237 exp,
238 reply_to: None,
239 },
240 signing_key,
241 )
242 }
243
244 fn new_with_options(
245 from: impl Into<String>,
246 to: impl Into<String>,
247 message_type: impl Into<String>,
248 content_type: impl Into<String>,
249 content: &[u8],
250 options: MessageOptions,
251 signing_key: &SigningKey,
252 ) -> Result<Self> {
253 let content_type_str: String = content_type.into();
254 let encoded = encode_content(codec_for(&content_type_str), content);
255 let mut message = Self {
256 id: nanoid!(),
257 protocol: default_protocol(),
258 message_type: message_type.into(),
259 from: from.into(),
260 to: to.into(),
261 created_at: now_unix_secs()?,
262 exp: options.exp,
263 content_type: content_type_str,
264 reply_to: options.reply_to,
265 content: encoded,
266 signature: Vec::new(),
267 };
268
269 message.unsigned_headers().validate()?;
270 message.validate_content()?;
271 message.sign(signing_key)?;
272 Ok(message)
273 }
274
275 pub fn encode(&self) -> Result<Vec<u8>> {
276 let mut out = Vec::new();
277 ciborium::ser::into_writer(self, &mut out)
278 .map_err(|error| MaError::CborEncode(error.to_string()))?;
279 Ok(out)
280 }
281
282 pub fn decode(bytes: &[u8]) -> Result<Self> {
283 ciborium::de::from_reader(bytes).map_err(|error| MaError::CborDecode(error.to_string()))
284 }
285
286 #[must_use]
289 pub fn payload(&self) -> Vec<u8> {
290 decode_content(&self.content)
291 .map(|(_, p)| p)
292 .unwrap_or_else(|_| self.content.clone())
293 }
294
295 #[must_use]
296 pub fn unsigned_headers(&self) -> Headers {
297 Headers {
298 id: self.id.clone(),
299 protocol: self.protocol.clone(),
300 message_type: self.message_type.clone(),
301 from: self.from.clone(),
302 to: self.to.clone(),
303 created_at: self.created_at,
304 exp: self.exp,
305 content_type: self.content_type.clone(),
306 reply_to: self.reply_to.clone(),
307 content_hash: content_hash(&self.content),
308 signature: Vec::new(),
309 }
310 }
311
312 #[must_use]
313 pub fn headers(&self) -> Headers {
314 let mut headers = self.unsigned_headers();
315 headers.signature.clone_from(&self.signature);
316 headers
317 }
318
319 pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
320 let bytes = self.unsigned_headers_cbor()?;
321 self.signature = signing_key.sign(&bytes);
322 Ok(())
323 }
324
325 pub fn verify_with_document(&self, sender_document: &Document) -> Result<()> {
326 if self.from.is_empty() {
327 return Err(MaError::MissingSender);
328 }
329
330 if self.signature.is_empty() {
331 return Err(MaError::MissingSignature);
332 }
333
334 let sender_did = Did::try_from(self.from.as_str())?;
335 if sender_document.id != sender_did.base_id() {
336 return Err(MaError::InvalidRecipient);
337 }
338
339 self.headers().validate()?;
340 let bytes = self.unsigned_headers_cbor()?;
341 let signature =
342 Signature::from_slice(&self.signature).map_err(|_| MaError::InvalidMessageSignature)?;
343 sender_document
344 .assertion_method_public_key()?
345 .verify(&bytes, &signature)
346 .map_err(|_| MaError::InvalidMessageSignature)
347 }
348
349 pub fn enclose_for(&self, recipient_document: &Document) -> Result<Envelope> {
350 self.headers().validate()?;
351
352 let recipient_public_key =
353 X25519PublicKey::from(recipient_document.key_agreement_public_key_bytes()?);
354 let ephemeral_secret = StaticSecret::random_from_rng(rand_core::OsRng);
355 let ephemeral_public = X25519PublicKey::from(&ephemeral_secret);
356 let shared_secret = ephemeral_secret
357 .diffie_hellman(&recipient_public_key)
358 .to_bytes();
359
360 let encrypted_headers = encrypt(
361 &self.headers_cbor()?,
362 derive_symmetric_key(&shared_secret, constants::BLAKE3_HEADERS_LABEL),
363 )?;
364
365 let encrypted_content = encrypt(
366 &self.content,
367 derive_symmetric_key(&shared_secret, constants::blake3_content_label()),
368 )?;
369
370 Ok(Envelope {
371 ephemeral_key: ephemeral_public.as_bytes().to_vec(),
372 encrypted_content,
373 encrypted_headers,
374 })
375 }
376
377 fn headers_cbor(&self) -> Result<Vec<u8>> {
378 to_cbor(&self.headers())
379 }
380
381 fn unsigned_headers_cbor(&self) -> Result<Vec<u8>> {
382 to_cbor(&self.unsigned_headers())
383 }
384
385 fn validate_content(&self) -> Result<()> {
386 if self.content.is_empty() {
387 return Err(MaError::MissingContent);
388 }
389 Ok(())
390 }
391
392 fn from_headers(headers: Headers) -> Result<Self> {
393 headers.validate()?;
394 Ok(Self {
395 id: headers.id,
396 protocol: headers.protocol,
397 message_type: headers.message_type,
398 from: headers.from,
399 to: headers.to,
400 created_at: headers.created_at,
401 exp: headers.exp,
402 content_type: headers.content_type,
403 reply_to: headers.reply_to,
404 content: Vec::new(),
405 signature: headers.signature,
406 })
407 }
408}
409
410fn to_cbor<T: Serialize>(value: &T) -> Result<Vec<u8>> {
411 let mut out = Vec::new();
412 ciborium::ser::into_writer(value, &mut out)
413 .map_err(|error| MaError::CborEncode(error.to_string()))?;
414 Ok(out)
415}
416
417#[derive(Debug, Clone)]
433pub struct ReplayGuard {
434 seen: HashMap<String, u64>,
435 window_secs: u64,
436}
437
438impl Default for ReplayGuard {
439 fn default() -> Self {
440 Self::new(DEFAULT_REPLAY_WINDOW_SECS)
441 }
442}
443
444impl ReplayGuard {
445 #[must_use]
446 pub fn new(window_secs: u64) -> Self {
447 Self {
448 seen: HashMap::new(),
449 window_secs,
450 }
451 }
452
453 pub fn check_and_insert(&mut self, headers: &Headers) -> Result<()> {
454 headers.validate()?;
455 self.prune_old()?;
456 if self.seen.contains_key(&headers.id) {
457 return Err(MaError::ReplayDetected);
458 }
459 self.seen.insert(headers.id.clone(), now_unix_secs()?);
460 Ok(())
461 }
462
463 fn prune_old(&mut self) -> Result<()> {
464 let now = now_unix_secs()?;
465 self.seen
466 .retain(|_, seen_at| now.saturating_sub(*seen_at) <= self.window_secs);
467 Ok(())
468 }
469}
470
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
513pub struct Envelope {
514 #[serde(rename = "ephemeralKey")]
515 pub ephemeral_key: Vec<u8>,
516 #[serde(rename = "encryptedContent")]
517 pub encrypted_content: Vec<u8>,
518 #[serde(rename = "encryptedHeaders")]
519 pub encrypted_headers: Vec<u8>,
520}
521
522impl Envelope {
523 pub fn verify(&self) -> Result<()> {
524 if self.ephemeral_key.is_empty() {
525 return Err(MaError::MissingEnvelopeField("ephemeralKey"));
526 }
527 if self.ephemeral_key.len() != 32 {
528 return Err(MaError::InvalidEphemeralKeyLength);
529 }
530 if self.encrypted_content.is_empty() {
531 return Err(MaError::MissingEnvelopeField("encryptedContent"));
532 }
533 if self.encrypted_headers.is_empty() {
534 return Err(MaError::MissingEnvelopeField("encryptedHeaders"));
535 }
536 Ok(())
537 }
538
539 pub fn encode(&self) -> Result<Vec<u8>> {
540 let mut out = Vec::new();
541 ciborium::ser::into_writer(self, &mut out)
542 .map_err(|error| MaError::CborEncode(error.to_string()))?;
543 Ok(out)
544 }
545
546 pub fn decode(bytes: &[u8]) -> Result<Self> {
547 ciborium::de::from_reader(bytes).map_err(|error| MaError::CborDecode(error.to_string()))
548 }
549
550 pub fn open(
551 &self,
552 recipient_key: &EncryptionKey,
553 sender_document: &Document,
554 ) -> Result<Message> {
555 self.verify()?;
556
557 let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
558 let headers = self.decrypt_headers(&shared_secret)?;
559 headers.validate()?;
560 let content = self.decrypt_content(&shared_secret)?;
561
562 let mut message = Message::from_headers(headers)?;
563 message.content = content;
564 message.verify_with_document(sender_document)?;
565 Ok(message)
566 }
567
568 #[cfg(feature = "iroh")]
569 pub(crate) fn decrypt(&self, recipient_key: &EncryptionKey) -> Result<Message> {
570 self.verify()?;
571
572 let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
573 let headers = self.decrypt_headers(&shared_secret)?;
574 headers.validate()?;
575 let content = self.decrypt_content(&shared_secret)?;
576
577 let mut message = Message::from_headers(headers)?;
578 message.content = content;
579 Ok(message)
580 }
581
582 pub fn open_with_replay_guard(
583 &self,
584 recipient_key: &EncryptionKey,
585 sender_document: &Document,
586 replay_guard: &mut ReplayGuard,
587 ) -> Result<Message> {
588 self.verify()?;
589
590 let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
591 let headers = self.decrypt_headers(&shared_secret)?;
592 let content = self.decrypt_content(&shared_secret)?;
593
594 let mut message = Message::from_headers(headers)?;
595 message.content = content;
596 message.verify_with_document(sender_document)?;
597 replay_guard.check_and_insert(&message.headers())?;
598 Ok(message)
599 }
600
601 fn decrypt_headers(&self, shared_secret: &[u8; 32]) -> Result<Headers> {
602 let decrypted = decrypt(
603 &self.encrypted_headers,
604 shared_secret,
605 constants::BLAKE3_HEADERS_LABEL,
606 )?;
607 ciborium::de::from_reader(decrypted.as_slice())
608 .map_err(|error| MaError::CborDecode(error.to_string()))
609 }
610
611 fn decrypt_content(&self, shared_secret: &[u8; 32]) -> Result<Vec<u8>> {
612 decrypt(
613 &self.encrypted_content,
614 shared_secret,
615 constants::blake3_content_label(),
616 )
617 }
618}
619
620fn validate_message_id(id: &str) -> Result<()> {
621 if id.is_empty() {
622 return Err(MaError::EmptyMessageId);
623 }
624
625 if !id
626 .chars()
627 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
628 {
629 return Err(MaError::InvalidMessageId);
630 }
631
632 Ok(())
633}
634
635fn validate_protocol(kind: &str) -> Result<()> {
636 if kind == default_protocol() {
637 return Ok(());
638 }
639
640 Err(MaError::InvalidMessageType)
641}
642
643fn now_unix_secs() -> Result<u64> {
644 SystemTime::now()
645 .duration_since(UNIX_EPOCH)
646 .map(|duration| duration.as_secs())
647 .map_err(|_| MaError::InvalidMessageTimestamp)
648}
649
650fn validate_message_freshness(created_at: u64, exp: u64) -> Result<()> {
651 let now = now_unix_secs()?;
652
653 if created_at > now + DEFAULT_MAX_CLOCK_SKEW_SECS {
654 return Err(MaError::MessageFromFuture);
655 }
656
657 if exp == 0 {
658 return Ok(()); }
660
661 if now > exp + DEFAULT_MAX_CLOCK_SKEW_SECS {
662 return Err(MaError::MessageTooOld);
663 }
664
665 Ok(())
666}
667
668fn compute_shared_secret(
669 ephemeral_key_bytes: &[u8],
670 recipient_key: &EncryptionKey,
671) -> Result<[u8; 32]> {
672 let ephemeral_public = X25519PublicKey::from(
673 <[u8; 32]>::try_from(ephemeral_key_bytes)
674 .map_err(|_| MaError::InvalidEphemeralKeyLength)?,
675 );
676 Ok(recipient_key.shared_secret(&ephemeral_public))
677}
678
679fn derive_symmetric_key(shared_secret: &[u8; 32], label: &str) -> Key {
680 let derived = blake3::derive_key(label, shared_secret);
681 *Key::from_slice(&derived)
682}
683
684fn encrypt(data: &[u8], key: Key) -> Result<Vec<u8>> {
685 let cipher = XChaCha20Poly1305::new(&key);
686 let nonce = XChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
687 let encrypted = cipher.encrypt(&nonce, data).map_err(|_| MaError::Crypto)?;
688
689 let mut out = nonce.to_vec();
690 out.extend_from_slice(&encrypted);
691 Ok(out)
692}
693
694fn decrypt(data: &[u8], shared_secret: &[u8; 32], label: &str) -> Result<Vec<u8>> {
695 if data.len() < 24 {
696 return Err(MaError::CiphertextTooShort);
697 }
698
699 let key = derive_symmetric_key(shared_secret, label);
700 let cipher = XChaCha20Poly1305::new(&key);
701 let nonce = XNonce::from_slice(&data[..24]);
702
703 cipher
704 .decrypt(nonce, &data[24..])
705 .map_err(|_| MaError::Crypto)
706}
707
708fn content_hash(content: &[u8]) -> [u8; 32] {
709 blake3::hash(content).into()
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::{doc::VerificationMethod, key::EncryptionKey};
716
717 fn fixture_documents() -> (
718 SigningKey,
719 EncryptionKey,
720 Document,
721 SigningKey,
722 EncryptionKey,
723 Document,
724 ) {
725 let sender_ipns = crate::ipns_from_secret([1; 32]).expect("sender ipns");
726 let sender_did = Did::new_url(&sender_ipns, None::<String>).expect("sender did");
727 let sender_sign_url = Did::new_url(&sender_ipns, None::<String>).expect("sender sign did");
728 let sender_enc_url = Did::new_url(&sender_ipns, None::<String>).expect("sender enc did");
729 let sender_signing = SigningKey::generate(sender_sign_url).expect("sender signing key");
730 let sender_encryption =
731 EncryptionKey::generate(sender_enc_url).expect("sender encryption key");
732
733 let recipient_ipns = crate::ipns_from_secret([2; 32]).expect("recipient ipns");
734 let recipient_did = Did::new_url(&recipient_ipns, None::<String>).expect("recipient did");
735 let recipient_sign_url =
736 Did::new_url(&recipient_ipns, None::<String>).expect("recipient sign did");
737 let recipient_enc_url =
738 Did::new_url(&recipient_ipns, None::<String>).expect("recipient enc did");
739 let recipient_signing =
740 SigningKey::generate(recipient_sign_url).expect("recipient signing key");
741 let recipient_encryption =
742 EncryptionKey::generate(recipient_enc_url).expect("recipient encryption key");
743
744 let mut sender_document = Document::new(&sender_did, &sender_did);
745 let sender_assertion = VerificationMethod::new(
746 sender_did.base_id(),
747 sender_did.base_id(),
748 sender_signing.key_type.clone(),
749 sender_signing.did.fragment.as_deref().unwrap_or_default(),
750 sender_signing.public_key_multibase.clone(),
751 )
752 .expect("sender assertion vm");
753 let sender_key_agreement = VerificationMethod::new(
754 sender_did.base_id(),
755 sender_did.base_id(),
756 sender_encryption.key_type.clone(),
757 sender_encryption
758 .did
759 .fragment
760 .as_deref()
761 .unwrap_or_default(),
762 sender_encryption.public_key_multibase.clone(),
763 )
764 .expect("sender key agreement vm");
765 sender_document
766 .add_verification_method(sender_assertion.clone())
767 .expect("add sender assertion");
768 sender_document
769 .add_verification_method(sender_key_agreement.clone())
770 .expect("add sender key agreement");
771 sender_document.assertion_method = vec![sender_assertion.id.clone()];
772 sender_document.key_agreement = vec![sender_key_agreement.id.clone()];
773 sender_document
774 .sign(&sender_signing, &sender_assertion)
775 .expect("sign sender doc");
776
777 let mut recipient_document = Document::new(&recipient_did, &recipient_did);
778 let recipient_assertion = VerificationMethod::new(
779 recipient_did.base_id(),
780 recipient_did.base_id(),
781 recipient_signing.key_type.clone(),
782 recipient_signing
783 .did
784 .fragment
785 .as_deref()
786 .unwrap_or_default(),
787 recipient_signing.public_key_multibase.clone(),
788 )
789 .expect("recipient assertion vm");
790 let recipient_key_agreement = VerificationMethod::new(
791 recipient_did.base_id(),
792 recipient_did.base_id(),
793 recipient_encryption.key_type.clone(),
794 recipient_encryption
795 .did
796 .fragment
797 .as_deref()
798 .unwrap_or_default(),
799 recipient_encryption.public_key_multibase.clone(),
800 )
801 .expect("recipient key agreement vm");
802 recipient_document
803 .add_verification_method(recipient_assertion.clone())
804 .expect("add recipient assertion");
805 recipient_document
806 .add_verification_method(recipient_key_agreement.clone())
807 .expect("add recipient key agreement");
808 recipient_document.assertion_method = vec![recipient_assertion.id.clone()];
809 recipient_document.key_agreement = vec![recipient_key_agreement.id.clone()];
810 recipient_document
811 .sign(&recipient_signing, &recipient_assertion)
812 .expect("sign recipient doc");
813
814 (
815 sender_signing,
816 sender_encryption,
817 sender_document,
818 recipient_signing,
819 recipient_encryption,
820 recipient_document,
821 )
822 }
823
824 fn inbox_url(document: &Document) -> String {
825 format!("{}#inbox", document.id)
826 }
827
828 #[test]
829 fn did_round_trip() {
830 let did = Did::new_url(
831 "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
832 Some("bahner"),
833 )
834 .expect("did must build");
835 let parsed = Did::try_from(did.id().as_str()).expect("did must parse");
836 assert_eq!(did, parsed);
837 }
838
839 #[test]
840 fn subject_url_round_trip() {
841 let did = Did::new_url(
842 "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
843 None::<String>,
844 )
845 .expect("subject did must build");
846 let parsed = Did::try_from(did.id().as_str()).expect("subject did must parse");
847 assert_eq!(did, parsed);
848 }
849
850 #[test]
851 fn document_signs_and_verifies() {
852 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
853 sender_signing.validate().expect("signing key validates");
854 sender_document.validate().expect("document validates");
855 }
856
857 #[test]
858 fn envelope_round_trip() {
859 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
860 fixture_documents();
861 let message = Message::new(
862 sender_document.id.clone(),
863 inbox_url(&recipient_document),
864 "application/vnd.ma.message",
865 "text/plain",
866 b"look",
867 &sender_signing,
868 )
869 .expect("message creation");
870 message
871 .verify_with_document(&sender_document)
872 .expect("message signature verifies");
873
874 let envelope = message
875 .enclose_for(&recipient_document)
876 .expect("message encloses");
877 let opened = envelope
878 .open(&recipient_encryption, &sender_document)
879 .expect("envelope opens");
880
881 assert_eq!(opened.payload(), b"look");
882 assert_eq!(opened.from, sender_document.id);
883 assert_eq!(opened.to, inbox_url(&recipient_document));
884 }
885
886 #[test]
887 fn tampered_content_fails_signature_verification() {
888 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
889 let mut message = Message::new(
890 sender_document.id.clone(),
891 inbox_url(&recipient_document),
892 "application/vnd.ma.message",
893 "text/plain",
894 b"look",
895 &sender_signing,
896 )
897 .expect("message creation");
898
899 message.content = b"tampered".to_vec();
900 let result = message.verify_with_document(&sender_document);
901 assert!(matches!(result, Err(MaError::InvalidMessageSignature)));
902 }
903
904 #[test]
905 fn reply_constructor_signs_correlation_header() {
906 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
907 let mut reply = Message::new_reply(
908 sender_document.id.clone(),
909 inbox_url(&recipient_document),
910 "application/vnd.ma.rpc.reply",
911 "application/vnd.ma.term",
912 b":ok",
913 "request-id",
914 &sender_signing,
915 )
916 .expect("reply creation");
917
918 reply
919 .verify_with_document(&sender_document)
920 .expect("reply signature covers replyTo");
921 assert_eq!(reply.reply_to.as_deref(), Some("request-id"));
922
923 reply.reply_to = Some("different-request-id".to_string());
924 assert!(matches!(
925 reply.verify_with_document(&sender_document),
926 Err(MaError::InvalidMessageSignature)
927 ));
928 }
929
930 #[test]
931 fn stale_message_is_rejected() {
932 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
933 let mut message = Message::new(
934 sender_document.id.clone(),
935 inbox_url(&recipient_document),
936 "application/vnd.ma.message",
937 "text/plain",
938 b"look",
939 &sender_signing,
940 )
941 .expect("message creation");
942
943 message.created_at = 0;
944 message.exp = 1; message
946 .sign(&sender_signing)
947 .expect("re-sign with past timestamps");
948 let result = message.verify_with_document(&sender_document);
949 assert!(matches!(result, Err(MaError::MessageTooOld)));
950 }
951
952 #[test]
953 fn future_message_is_rejected() {
954 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
955 let mut message = Message::new(
956 sender_document.id.clone(),
957 inbox_url(&recipient_document),
958 "application/vnd.ma.message",
959 "text/plain",
960 b"look",
961 &sender_signing,
962 )
963 .expect("message creation");
964
965 message.created_at =
966 now_unix_secs().expect("current timestamp") + DEFAULT_MAX_CLOCK_SKEW_SECS + 60;
967 message
968 .sign(&sender_signing)
969 .expect("re-sign with updated timestamp");
970
971 let result = message.verify_with_document(&sender_document);
972 assert!(matches!(result, Err(MaError::MessageFromFuture)));
973 }
974
975 #[test]
976 fn exp_zero_disables_expiration() {
977 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
978 let mut message = Message::new(
979 sender_document.id.clone(),
980 inbox_url(&recipient_document),
981 "application/vnd.ma.message",
982 "text/plain",
983 b"look",
984 &sender_signing,
985 )
986 .expect("message creation");
987
988 message.created_at = 0;
989 message.exp = 0; message.sign(&sender_signing).expect("re-sign with exp=0");
991
992 message
993 .verify_with_document(&sender_document)
994 .expect("exp=0 should bypass expiration check");
995 }
996
997 #[test]
998 fn custom_ttl_rejects_expired_message() {
999 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
1000 let now_secs = now_unix_secs().expect("current timestamp");
1001 let mut message = Message::new_with_exp(
1003 sender_document.id.clone(),
1004 inbox_url(&recipient_document),
1005 "application/vnd.ma.message",
1006 "text/plain",
1007 b"look",
1008 now_secs + 60,
1009 &sender_signing,
1010 )
1011 .expect("message creation with custom exp");
1012
1013 message.exp = 1;
1015 message
1016 .sign(&sender_signing)
1017 .expect("re-sign with expired exp");
1018
1019 let result = message.verify_with_document(&sender_document);
1020 assert!(matches!(result, Err(MaError::MessageTooOld)));
1021 }
1022
1023 #[test]
1024 fn replay_guard_rejects_duplicate_envelope() {
1025 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
1026 fixture_documents();
1027 let message = Message::new(
1028 sender_document.id.clone(),
1029 inbox_url(&recipient_document),
1030 "application/vnd.ma.message",
1031 "text/plain",
1032 b"look",
1033 &sender_signing,
1034 )
1035 .expect("message creation");
1036
1037 let envelope = message
1038 .enclose_for(&recipient_document)
1039 .expect("message encloses");
1040 let mut replay_guard = ReplayGuard::default();
1041
1042 envelope
1043 .open_with_replay_guard(&recipient_encryption, &sender_document, &mut replay_guard)
1044 .expect("first delivery accepted");
1045
1046 let second = envelope.open_with_replay_guard(
1047 &recipient_encryption,
1048 &sender_document,
1049 &mut replay_guard,
1050 );
1051 assert!(matches!(second, Err(MaError::ReplayDetected)));
1052 }
1053
1054 #[test]
1055 fn rejected_envelope_does_not_consume_replay_id() {
1056 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
1057 fixture_documents();
1058 let message = Message::new(
1059 sender_document.id.clone(),
1060 inbox_url(&recipient_document),
1061 "application/vnd.ma.message",
1062 "text/plain",
1063 b"look",
1064 &sender_signing,
1065 )
1066 .expect("message creation");
1067 let envelope = message
1068 .enclose_for(&recipient_document)
1069 .expect("message encloses");
1070 let mut tampered = envelope.clone();
1071 tampered.encrypted_content[0] ^= 0xff;
1072 let mut guard = ReplayGuard::default();
1073
1074 assert!(tampered
1075 .open_with_replay_guard(&recipient_encryption, &sender_document, &mut guard,)
1076 .is_err());
1077
1078 envelope
1079 .open_with_replay_guard(&recipient_encryption, &sender_document, &mut guard)
1080 .expect("valid envelope remains acceptable");
1081 }
1082
1083 #[test]
1084 fn broadcast_allows_empty_recipient() {
1085 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
1086 let message = Message::new(
1087 sender_document.id.clone(),
1088 String::new(),
1089 "application/vnd.ma.broadcast",
1090 "text/plain",
1091 b"hello everyone",
1092 &sender_signing,
1093 )
1094 .expect("broadcast message creation");
1095
1096 message
1097 .verify_with_document(&sender_document)
1098 .expect("broadcast with empty recipient verifies");
1099 }
1100
1101 #[test]
1102 fn broadcast_rejects_recipient() {
1103 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
1104 let result = Message::new(
1105 sender_document.id.clone(),
1106 inbox_url(&recipient_document),
1107 "application/vnd.ma.broadcast",
1108 "text/plain",
1109 b"hello everyone",
1110 &sender_signing,
1111 );
1112
1113 assert!(matches!(
1114 result,
1115 Err(MaError::BroadcastMustNotHaveRecipient)
1116 ));
1117 }
1118
1119 #[test]
1120 fn message_requires_recipient() {
1121 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
1122 let result = Message::new(
1123 sender_document.id.clone(),
1124 String::new(),
1125 "application/vnd.ma.message",
1126 "text/plain",
1127 b"secret",
1128 &sender_signing,
1129 );
1130
1131 assert!(matches!(result, Err(MaError::MessageRequiresRecipient)));
1132 }
1133
1134 #[test]
1135 fn custom_message_requires_recipient() {
1136 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
1137 let result = Message::new(
1138 sender_document.id.clone(),
1139 String::new(),
1140 "application/x-ma-custom",
1141 "text/plain",
1142 b"whatever",
1143 &sender_signing,
1144 );
1145
1146 assert!(matches!(result, Err(MaError::MessageRequiresRecipient)));
1147 }
1148
1149 #[test]
1150 fn unknown_content_type_allows_recipient() {
1151 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
1152 let message = Message::new(
1153 sender_document.id.clone(),
1154 inbox_url(&recipient_document),
1155 "application/x-ma-custom",
1156 "text/plain",
1157 b"whatever",
1158 &sender_signing,
1159 )
1160 .expect("custom content type with recipient");
1161
1162 message
1163 .verify_with_document(&sender_document)
1164 .expect("custom type with recipient verifies");
1165 }
1166}