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 let mut out = Vec::new();
379 ciborium::ser::into_writer(&self.headers(), &mut out)
380 .map_err(|error| MaError::CborEncode(error.to_string()))?;
381 Ok(out)
382 }
383
384 fn unsigned_headers_cbor(&self) -> Result<Vec<u8>> {
385 let mut out = Vec::new();
386 ciborium::ser::into_writer(&self.unsigned_headers(), &mut out)
387 .map_err(|error| MaError::CborEncode(error.to_string()))?;
388 Ok(out)
389 }
390
391 fn validate_content(&self) -> Result<()> {
392 if self.content.is_empty() {
393 return Err(MaError::MissingContent);
394 }
395 Ok(())
396 }
397
398 fn from_headers(headers: Headers) -> Result<Self> {
399 headers.validate()?;
400 Ok(Self {
401 id: headers.id,
402 protocol: headers.protocol,
403 message_type: headers.message_type,
404 from: headers.from,
405 to: headers.to,
406 created_at: headers.created_at,
407 exp: headers.exp,
408 content_type: headers.content_type,
409 reply_to: headers.reply_to,
410 content: Vec::new(),
411 signature: headers.signature,
412 })
413 }
414}
415
416#[derive(Debug, Clone)]
432pub struct ReplayGuard {
433 seen: HashMap<String, u64>,
434 window_secs: u64,
435}
436
437impl Default for ReplayGuard {
438 fn default() -> Self {
439 Self::new(DEFAULT_REPLAY_WINDOW_SECS)
440 }
441}
442
443impl ReplayGuard {
444 #[must_use]
445 pub fn new(window_secs: u64) -> Self {
446 Self {
447 seen: HashMap::new(),
448 window_secs,
449 }
450 }
451
452 pub fn check_and_insert(&mut self, headers: &Headers) -> Result<()> {
453 headers.validate()?;
454 self.prune_old()?;
455 if self.seen.contains_key(&headers.id) {
456 return Err(MaError::ReplayDetected);
457 }
458 self.seen.insert(headers.id.clone(), now_unix_secs()?);
459 Ok(())
460 }
461
462 fn prune_old(&mut self) -> Result<()> {
463 let now = now_unix_secs()?;
464 self.seen
465 .retain(|_, seen_at| now.saturating_sub(*seen_at) <= self.window_secs);
466 Ok(())
467 }
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
512pub struct Envelope {
513 #[serde(rename = "ephemeralKey")]
514 pub ephemeral_key: Vec<u8>,
515 #[serde(rename = "encryptedContent")]
516 pub encrypted_content: Vec<u8>,
517 #[serde(rename = "encryptedHeaders")]
518 pub encrypted_headers: Vec<u8>,
519}
520
521impl Envelope {
522 pub fn verify(&self) -> Result<()> {
523 if self.ephemeral_key.is_empty() {
524 return Err(MaError::MissingEnvelopeField("ephemeralKey"));
525 }
526 if self.ephemeral_key.len() != 32 {
527 return Err(MaError::InvalidEphemeralKeyLength);
528 }
529 if self.encrypted_content.is_empty() {
530 return Err(MaError::MissingEnvelopeField("encryptedContent"));
531 }
532 if self.encrypted_headers.is_empty() {
533 return Err(MaError::MissingEnvelopeField("encryptedHeaders"));
534 }
535 Ok(())
536 }
537
538 pub fn encode(&self) -> Result<Vec<u8>> {
539 let mut out = Vec::new();
540 ciborium::ser::into_writer(self, &mut out)
541 .map_err(|error| MaError::CborEncode(error.to_string()))?;
542 Ok(out)
543 }
544
545 pub fn decode(bytes: &[u8]) -> Result<Self> {
546 ciborium::de::from_reader(bytes).map_err(|error| MaError::CborDecode(error.to_string()))
547 }
548
549 pub fn open(
550 &self,
551 recipient_key: &EncryptionKey,
552 sender_document: &Document,
553 ) -> Result<Message> {
554 self.verify()?;
555
556 let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
557 let headers = self.decrypt_headers(&shared_secret)?;
558 headers.validate()?;
559 let content = self.decrypt_content(&shared_secret)?;
560
561 let mut message = Message::from_headers(headers)?;
562 message.content = content;
563 message.verify_with_document(sender_document)?;
564 Ok(message)
565 }
566
567 #[cfg(feature = "iroh")]
568 pub(crate) fn decrypt(&self, recipient_key: &EncryptionKey) -> Result<Message> {
569 self.verify()?;
570
571 let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
572 let headers = self.decrypt_headers(&shared_secret)?;
573 headers.validate()?;
574 let content = self.decrypt_content(&shared_secret)?;
575
576 let mut message = Message::from_headers(headers)?;
577 message.content = content;
578 Ok(message)
579 }
580
581 pub fn open_with_replay_guard(
582 &self,
583 recipient_key: &EncryptionKey,
584 sender_document: &Document,
585 replay_guard: &mut ReplayGuard,
586 ) -> Result<Message> {
587 self.verify()?;
588
589 let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
590 let headers = self.decrypt_headers(&shared_secret)?;
591 let content = self.decrypt_content(&shared_secret)?;
592
593 let mut message = Message::from_headers(headers)?;
594 message.content = content;
595 message.verify_with_document(sender_document)?;
596 replay_guard.check_and_insert(&message.headers())?;
597 Ok(message)
598 }
599
600 fn decrypt_headers(&self, shared_secret: &[u8; 32]) -> Result<Headers> {
601 let decrypted = decrypt(
602 &self.encrypted_headers,
603 shared_secret,
604 constants::BLAKE3_HEADERS_LABEL,
605 )?;
606 ciborium::de::from_reader(decrypted.as_slice())
607 .map_err(|error| MaError::CborDecode(error.to_string()))
608 }
609
610 fn decrypt_content(&self, shared_secret: &[u8; 32]) -> Result<Vec<u8>> {
611 decrypt(
612 &self.encrypted_content,
613 shared_secret,
614 constants::blake3_content_label(),
615 )
616 }
617}
618
619fn validate_message_id(id: &str) -> Result<()> {
620 if id.is_empty() {
621 return Err(MaError::EmptyMessageId);
622 }
623
624 if !id
625 .chars()
626 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
627 {
628 return Err(MaError::InvalidMessageId);
629 }
630
631 Ok(())
632}
633
634fn validate_protocol(kind: &str) -> Result<()> {
635 if kind == default_protocol() {
636 return Ok(());
637 }
638
639 Err(MaError::InvalidMessageType)
640}
641
642fn now_unix_secs() -> Result<u64> {
643 SystemTime::now()
644 .duration_since(UNIX_EPOCH)
645 .map(|duration| duration.as_secs())
646 .map_err(|_| MaError::InvalidMessageTimestamp)
647}
648
649fn validate_message_freshness(created_at: u64, exp: u64) -> Result<()> {
650 let now = now_unix_secs()?;
651
652 if created_at > now + DEFAULT_MAX_CLOCK_SKEW_SECS {
653 return Err(MaError::MessageFromFuture);
654 }
655
656 if exp == 0 {
657 return Ok(()); }
659
660 if now > exp + DEFAULT_MAX_CLOCK_SKEW_SECS {
661 return Err(MaError::MessageTooOld);
662 }
663
664 Ok(())
665}
666
667fn compute_shared_secret(
668 ephemeral_key_bytes: &[u8],
669 recipient_key: &EncryptionKey,
670) -> Result<[u8; 32]> {
671 let ephemeral_public = X25519PublicKey::from(
672 <[u8; 32]>::try_from(ephemeral_key_bytes)
673 .map_err(|_| MaError::InvalidEphemeralKeyLength)?,
674 );
675 Ok(recipient_key.shared_secret(&ephemeral_public))
676}
677
678fn derive_symmetric_key(shared_secret: &[u8; 32], label: &str) -> Key {
679 let derived = blake3::derive_key(label, shared_secret);
680 *Key::from_slice(&derived)
681}
682
683fn encrypt(data: &[u8], key: Key) -> Result<Vec<u8>> {
684 let cipher = XChaCha20Poly1305::new(&key);
685 let nonce = XChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
686 let encrypted = cipher.encrypt(&nonce, data).map_err(|_| MaError::Crypto)?;
687
688 let mut out = nonce.to_vec();
689 out.extend_from_slice(&encrypted);
690 Ok(out)
691}
692
693fn decrypt(data: &[u8], shared_secret: &[u8; 32], label: &str) -> Result<Vec<u8>> {
694 if data.len() < 24 {
695 return Err(MaError::CiphertextTooShort);
696 }
697
698 let key = derive_symmetric_key(shared_secret, label);
699 let cipher = XChaCha20Poly1305::new(&key);
700 let nonce = XNonce::from_slice(&data[..24]);
701
702 cipher
703 .decrypt(nonce, &data[24..])
704 .map_err(|_| MaError::Crypto)
705}
706
707fn content_hash(content: &[u8]) -> [u8; 32] {
708 blake3::hash(content).into()
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714 use crate::{doc::VerificationMethod, key::EncryptionKey};
715
716 fn fixture_documents() -> (
717 SigningKey,
718 EncryptionKey,
719 Document,
720 SigningKey,
721 EncryptionKey,
722 Document,
723 ) {
724 let sender_ipns = crate::ipns_from_secret([1; 32]).expect("sender ipns");
725 let sender_did = Did::new_url(&sender_ipns, None::<String>).expect("sender did");
726 let sender_sign_url = Did::new_url(&sender_ipns, None::<String>).expect("sender sign did");
727 let sender_enc_url = Did::new_url(&sender_ipns, None::<String>).expect("sender enc did");
728 let sender_signing = SigningKey::generate(sender_sign_url).expect("sender signing key");
729 let sender_encryption =
730 EncryptionKey::generate(sender_enc_url).expect("sender encryption key");
731
732 let recipient_ipns = crate::ipns_from_secret([2; 32]).expect("recipient ipns");
733 let recipient_did = Did::new_url(&recipient_ipns, None::<String>).expect("recipient did");
734 let recipient_sign_url =
735 Did::new_url(&recipient_ipns, None::<String>).expect("recipient sign did");
736 let recipient_enc_url =
737 Did::new_url(&recipient_ipns, None::<String>).expect("recipient enc did");
738 let recipient_signing =
739 SigningKey::generate(recipient_sign_url).expect("recipient signing key");
740 let recipient_encryption =
741 EncryptionKey::generate(recipient_enc_url).expect("recipient encryption key");
742
743 let mut sender_document = Document::new(&sender_did, &sender_did);
744 let sender_assertion = VerificationMethod::new(
745 sender_did.base_id(),
746 sender_did.base_id(),
747 sender_signing.key_type.clone(),
748 sender_signing.did.fragment.as_deref().unwrap_or_default(),
749 sender_signing.public_key_multibase.clone(),
750 )
751 .expect("sender assertion vm");
752 let sender_key_agreement = VerificationMethod::new(
753 sender_did.base_id(),
754 sender_did.base_id(),
755 sender_encryption.key_type.clone(),
756 sender_encryption
757 .did
758 .fragment
759 .as_deref()
760 .unwrap_or_default(),
761 sender_encryption.public_key_multibase.clone(),
762 )
763 .expect("sender key agreement vm");
764 sender_document
765 .add_verification_method(sender_assertion.clone())
766 .expect("add sender assertion");
767 sender_document
768 .add_verification_method(sender_key_agreement.clone())
769 .expect("add sender key agreement");
770 sender_document.assertion_method = vec![sender_assertion.id.clone()];
771 sender_document.key_agreement = vec![sender_key_agreement.id.clone()];
772 sender_document
773 .sign(&sender_signing, &sender_assertion)
774 .expect("sign sender doc");
775
776 let mut recipient_document = Document::new(&recipient_did, &recipient_did);
777 let recipient_assertion = VerificationMethod::new(
778 recipient_did.base_id(),
779 recipient_did.base_id(),
780 recipient_signing.key_type.clone(),
781 recipient_signing
782 .did
783 .fragment
784 .as_deref()
785 .unwrap_or_default(),
786 recipient_signing.public_key_multibase.clone(),
787 )
788 .expect("recipient assertion vm");
789 let recipient_key_agreement = VerificationMethod::new(
790 recipient_did.base_id(),
791 recipient_did.base_id(),
792 recipient_encryption.key_type.clone(),
793 recipient_encryption
794 .did
795 .fragment
796 .as_deref()
797 .unwrap_or_default(),
798 recipient_encryption.public_key_multibase.clone(),
799 )
800 .expect("recipient key agreement vm");
801 recipient_document
802 .add_verification_method(recipient_assertion.clone())
803 .expect("add recipient assertion");
804 recipient_document
805 .add_verification_method(recipient_key_agreement.clone())
806 .expect("add recipient key agreement");
807 recipient_document.assertion_method = vec![recipient_assertion.id.clone()];
808 recipient_document.key_agreement = vec![recipient_key_agreement.id.clone()];
809 recipient_document
810 .sign(&recipient_signing, &recipient_assertion)
811 .expect("sign recipient doc");
812
813 (
814 sender_signing,
815 sender_encryption,
816 sender_document,
817 recipient_signing,
818 recipient_encryption,
819 recipient_document,
820 )
821 }
822
823 fn inbox_url(document: &Document) -> String {
824 format!("{}#inbox", document.id)
825 }
826
827 #[test]
828 fn did_round_trip() {
829 let did = Did::new_url(
830 "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
831 Some("bahner"),
832 )
833 .expect("did must build");
834 let parsed = Did::try_from(did.id().as_str()).expect("did must parse");
835 assert_eq!(did, parsed);
836 }
837
838 #[test]
839 fn subject_url_round_trip() {
840 let did = Did::new_url(
841 "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
842 None::<String>,
843 )
844 .expect("subject did must build");
845 let parsed = Did::try_from(did.id().as_str()).expect("subject did must parse");
846 assert_eq!(did, parsed);
847 }
848
849 #[test]
850 fn document_signs_and_verifies() {
851 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
852 sender_signing.validate().expect("signing key validates");
853 sender_document.validate().expect("document validates");
854 }
855
856 #[test]
857 fn envelope_round_trip() {
858 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
859 fixture_documents();
860 let message = Message::new(
861 sender_document.id.clone(),
862 inbox_url(&recipient_document),
863 "application/vnd.ma.message",
864 "text/plain",
865 b"look",
866 &sender_signing,
867 )
868 .expect("message creation");
869 message
870 .verify_with_document(&sender_document)
871 .expect("message signature verifies");
872
873 let envelope = message
874 .enclose_for(&recipient_document)
875 .expect("message encloses");
876 let opened = envelope
877 .open(&recipient_encryption, &sender_document)
878 .expect("envelope opens");
879
880 assert_eq!(opened.payload(), b"look");
881 assert_eq!(opened.from, sender_document.id);
882 assert_eq!(opened.to, inbox_url(&recipient_document));
883 }
884
885 #[test]
886 fn tampered_content_fails_signature_verification() {
887 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
888 let mut message = Message::new(
889 sender_document.id.clone(),
890 inbox_url(&recipient_document),
891 "application/vnd.ma.message",
892 "text/plain",
893 b"look",
894 &sender_signing,
895 )
896 .expect("message creation");
897
898 message.content = b"tampered".to_vec();
899 let result = message.verify_with_document(&sender_document);
900 assert!(matches!(result, Err(MaError::InvalidMessageSignature)));
901 }
902
903 #[test]
904 fn reply_constructor_signs_correlation_header() {
905 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
906 let mut reply = Message::new_reply(
907 sender_document.id.clone(),
908 inbox_url(&recipient_document),
909 "application/vnd.ma.rpc.reply",
910 "application/vnd.ma.term",
911 b":ok",
912 "request-id",
913 &sender_signing,
914 )
915 .expect("reply creation");
916
917 reply
918 .verify_with_document(&sender_document)
919 .expect("reply signature covers replyTo");
920 assert_eq!(reply.reply_to.as_deref(), Some("request-id"));
921
922 reply.reply_to = Some("different-request-id".to_string());
923 assert!(matches!(
924 reply.verify_with_document(&sender_document),
925 Err(MaError::InvalidMessageSignature)
926 ));
927 }
928
929 #[test]
930 fn stale_message_is_rejected() {
931 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
932 let mut message = Message::new(
933 sender_document.id.clone(),
934 inbox_url(&recipient_document),
935 "application/vnd.ma.message",
936 "text/plain",
937 b"look",
938 &sender_signing,
939 )
940 .expect("message creation");
941
942 message.created_at = 0;
943 message.exp = 1; message
945 .sign(&sender_signing)
946 .expect("re-sign with past timestamps");
947 let result = message.verify_with_document(&sender_document);
948 assert!(matches!(result, Err(MaError::MessageTooOld)));
949 }
950
951 #[test]
952 fn future_message_is_rejected() {
953 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
954 let mut message = Message::new(
955 sender_document.id.clone(),
956 inbox_url(&recipient_document),
957 "application/vnd.ma.message",
958 "text/plain",
959 b"look",
960 &sender_signing,
961 )
962 .expect("message creation");
963
964 message.created_at =
965 now_unix_secs().expect("current timestamp") + DEFAULT_MAX_CLOCK_SKEW_SECS + 60;
966 message
967 .sign(&sender_signing)
968 .expect("re-sign with updated timestamp");
969
970 let result = message.verify_with_document(&sender_document);
971 assert!(matches!(result, Err(MaError::MessageFromFuture)));
972 }
973
974 #[test]
975 fn exp_zero_disables_expiration() {
976 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
977 let mut message = Message::new(
978 sender_document.id.clone(),
979 inbox_url(&recipient_document),
980 "application/vnd.ma.message",
981 "text/plain",
982 b"look",
983 &sender_signing,
984 )
985 .expect("message creation");
986
987 message.created_at = 0;
988 message.exp = 0; message.sign(&sender_signing).expect("re-sign with exp=0");
990
991 message
992 .verify_with_document(&sender_document)
993 .expect("exp=0 should bypass expiration check");
994 }
995
996 #[test]
997 fn custom_ttl_rejects_expired_message() {
998 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
999 let now_secs = now_unix_secs().expect("current timestamp");
1000 let mut message = Message::new_with_exp(
1002 sender_document.id.clone(),
1003 inbox_url(&recipient_document),
1004 "application/vnd.ma.message",
1005 "text/plain",
1006 b"look",
1007 now_secs + 60,
1008 &sender_signing,
1009 )
1010 .expect("message creation with custom exp");
1011
1012 message.exp = 1;
1014 message
1015 .sign(&sender_signing)
1016 .expect("re-sign with expired exp");
1017
1018 let result = message.verify_with_document(&sender_document);
1019 assert!(matches!(result, Err(MaError::MessageTooOld)));
1020 }
1021
1022 #[test]
1023 fn replay_guard_rejects_duplicate_envelope() {
1024 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
1025 fixture_documents();
1026 let message = Message::new(
1027 sender_document.id.clone(),
1028 inbox_url(&recipient_document),
1029 "application/vnd.ma.message",
1030 "text/plain",
1031 b"look",
1032 &sender_signing,
1033 )
1034 .expect("message creation");
1035
1036 let envelope = message
1037 .enclose_for(&recipient_document)
1038 .expect("message encloses");
1039 let mut replay_guard = ReplayGuard::default();
1040
1041 envelope
1042 .open_with_replay_guard(&recipient_encryption, &sender_document, &mut replay_guard)
1043 .expect("first delivery accepted");
1044
1045 let second = envelope.open_with_replay_guard(
1046 &recipient_encryption,
1047 &sender_document,
1048 &mut replay_guard,
1049 );
1050 assert!(matches!(second, Err(MaError::ReplayDetected)));
1051 }
1052
1053 #[test]
1054 fn rejected_envelope_does_not_consume_replay_id() {
1055 let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
1056 fixture_documents();
1057 let message = Message::new(
1058 sender_document.id.clone(),
1059 inbox_url(&recipient_document),
1060 "application/vnd.ma.message",
1061 "text/plain",
1062 b"look",
1063 &sender_signing,
1064 )
1065 .expect("message creation");
1066 let envelope = message
1067 .enclose_for(&recipient_document)
1068 .expect("message encloses");
1069 let mut tampered = envelope.clone();
1070 tampered.encrypted_content[0] ^= 0xff;
1071 let mut guard = ReplayGuard::default();
1072
1073 assert!(tampered
1074 .open_with_replay_guard(&recipient_encryption, &sender_document, &mut guard,)
1075 .is_err());
1076
1077 envelope
1078 .open_with_replay_guard(&recipient_encryption, &sender_document, &mut guard)
1079 .expect("valid envelope remains acceptable");
1080 }
1081
1082 #[test]
1083 fn broadcast_allows_empty_recipient() {
1084 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
1085 let message = Message::new(
1086 sender_document.id.clone(),
1087 String::new(),
1088 "application/vnd.ma.broadcast",
1089 "text/plain",
1090 b"hello everyone",
1091 &sender_signing,
1092 )
1093 .expect("broadcast message creation");
1094
1095 message
1096 .verify_with_document(&sender_document)
1097 .expect("broadcast with empty recipient verifies");
1098 }
1099
1100 #[test]
1101 fn broadcast_rejects_recipient() {
1102 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
1103 let result = Message::new(
1104 sender_document.id.clone(),
1105 inbox_url(&recipient_document),
1106 "application/vnd.ma.broadcast",
1107 "text/plain",
1108 b"hello everyone",
1109 &sender_signing,
1110 );
1111
1112 assert!(matches!(
1113 result,
1114 Err(MaError::BroadcastMustNotHaveRecipient)
1115 ));
1116 }
1117
1118 #[test]
1119 fn message_requires_recipient() {
1120 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
1121 let result = Message::new(
1122 sender_document.id.clone(),
1123 String::new(),
1124 "application/vnd.ma.message",
1125 "text/plain",
1126 b"secret",
1127 &sender_signing,
1128 );
1129
1130 assert!(matches!(result, Err(MaError::MessageRequiresRecipient)));
1131 }
1132
1133 #[test]
1134 fn custom_message_requires_recipient() {
1135 let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
1136 let result = Message::new(
1137 sender_document.id.clone(),
1138 String::new(),
1139 "application/x-ma-custom",
1140 "text/plain",
1141 b"whatever",
1142 &sender_signing,
1143 );
1144
1145 assert!(matches!(result, Err(MaError::MessageRequiresRecipient)));
1146 }
1147
1148 #[test]
1149 fn unknown_content_type_allows_recipient() {
1150 let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
1151 let message = Message::new(
1152 sender_document.id.clone(),
1153 inbox_url(&recipient_document),
1154 "application/x-ma-custom",
1155 "text/plain",
1156 b"whatever",
1157 &sender_signing,
1158 )
1159 .expect("custom content type with recipient");
1160
1161 message
1162 .verify_with_document(&sender_document)
1163 .expect("custom type with recipient verifies");
1164 }
1165}