Skip to main content

ma_core/
msg.rs

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
27/// Prefix `payload` with a multicodec varint so the codec is self-describing.
28pub fn encode_content(codec: u64, payload: &[u8]) -> Vec<u8> {
29    crate::multiformat::multicodec_encode(codec, payload)
30}
31
32/// Peel the multicodec varint prefix from `content` bytes.
33/// Returns `(codec, payload)`.
34pub fn decode_content(content: &[u8]) -> crate::error::MaResult<(u64, Vec<u8>)> {
35    crate::multiformat::multicodec_decode(content)
36}
37
38/// Map a `content_type` string to the multicodec codec used to prefix the payload.
39/// `Message::new` applies this automatically — callers never handle raw prefixes.
40fn codec_for(content_type: &str) -> u64 {
41    match content_type {
42        "application/vnd.ipld.dag-cbor" => crate::multiformat::CODEC_DAG_CBOR,
43        // application/vnd.ma.term: CBOR term — bare atom (:ok, :pong) or tuple ([:verb, ...]).
44        "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/// Signed message headers (without content body).
55///
56/// Headers include a BLAKE3 hash of the content for integrity verification.
57/// Extracted from a [`Message`] via [`Message::headers`] or [`Message::unsigned_headers`].
58#[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/// A signed actor-to-actor message.
112///
113/// Messages are signed on creation using the sender's [`SigningKey`].
114/// The signature covers the CBOR-serialized headers (including a BLAKE3
115/// hash of the content), ensuring both integrity and authenticity.
116///
117/// # Examples
118///
119/// ```
120/// use ma_core::{generate_identity_from_secret, Message, SigningKey, Did};
121///
122/// let sender = generate_identity_from_secret([1u8; 32]).unwrap();
123/// let recipient = generate_identity_from_secret([2u8; 32]).unwrap();
124///
125/// let sign_url = Did::new_url(&sender.subject_url.ipns, None::<String>).unwrap();
126/// let signing_key = SigningKey::from_private_key_bytes(
127///     sign_url,
128///     hex::decode(&sender.signing_private_key_hex).unwrap().try_into().unwrap(),
129/// ).unwrap();
130///
131/// // Create a signed message
132/// let msg = Message::new(
133///     sender.document.id.clone(),
134///     format!("{}#inbox", recipient.document.id),
135///     "application/vnd.ma.message",
136///     "text/plain",
137///     b"hello",
138///     &signing_key,
139/// ).unwrap();
140///
141/// // Verify against sender's document
142/// msg.verify_with_document(&sender.document).unwrap();
143///
144/// // Serialize to wire format
145/// let bytes = msg.encode().unwrap();
146/// let restored = Message::decode(&bytes).unwrap();
147/// assert_eq!(msg.id, restored.id);
148/// ```
149#[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    /// Create and sign a reply whose correlation header is covered by the signature.
197    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    /// Return the decoded content payload, stripping the multicodec varint prefix
287    /// applied by [`Message::new`].
288    #[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/// Sliding-window replay guard for message deduplication.
417///
418/// Tracks seen message IDs within a configurable time window and rejects
419/// duplicates. Use with [`Envelope::open_with_replay_guard`] for
420/// transport-level replay protection.
421///
422/// # Examples
423///
424/// ```
425/// use ma_core::ReplayGuard;
426///
427/// let mut guard = ReplayGuard::new(120); // 2-minute window
428/// // or use the default (120 seconds):
429/// let mut guard = ReplayGuard::default();
430/// ```
431#[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/// An encrypted message envelope for transport.
471///
472/// Contains an ephemeral X25519 public key and XChaCha20-Poly1305 encrypted
473/// headers and content. Created by [`Message::enclose_for`] and opened by
474/// [`Envelope::open`] or [`Envelope::open_with_replay_guard`].
475///
476/// # Examples
477///
478/// ```
479/// use ma_core::{generate_identity_from_secret, Message, Envelope, EncryptionKey, SigningKey, Did};
480///
481/// let alice = generate_identity_from_secret([1u8; 32]).unwrap();
482/// let bob = generate_identity_from_secret([2u8; 32]).unwrap();
483///
484/// let alice_sign_url = Did::new_url(&alice.subject_url.ipns, None::<String>).unwrap();
485/// let alice_key = SigningKey::from_private_key_bytes(
486///     alice_sign_url,
487///     hex::decode(&alice.signing_private_key_hex).unwrap().try_into().unwrap(),
488/// ).unwrap();
489///
490/// let msg = Message::new(
491///     alice.document.id.clone(),
492///     format!("{}#inbox", bob.document.id),
493///     "application/vnd.ma.message",
494///     "text/plain",
495///     b"secret",
496///     &alice_key,
497/// ).unwrap();
498///
499/// // Encrypt for Bob
500/// let envelope = msg.enclose_for(&bob.document).unwrap();
501///
502/// // Bob decrypts
503/// let bob_enc_url = Did::new_url(&bob.subject_url.ipns, None::<String>).unwrap();
504/// let bob_enc_key = EncryptionKey::from_private_key_bytes(
505///     bob_enc_url,
506///     hex::decode(&bob.encryption_private_key_hex).unwrap().try_into().unwrap(),
507/// ).unwrap();
508/// let decrypted = envelope.open(&bob_enc_key, &alice.document).unwrap();
509/// assert_eq!(decrypted.payload(), b"secret");
510/// ```
511#[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(()); // 0 = never expires
658    }
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; // 1 s epoch — well in the past
944        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; // 0 = never expires
989        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        // Create with a valid 60-second window.
1001        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        // Rewind exp to 1 ns (well in the past) and re-sign.
1013        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}