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        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/// Sliding-window replay guard for message deduplication.
418///
419/// Tracks seen message IDs within a configurable time window and rejects
420/// duplicates. Use with [`Envelope::open_with_replay_guard`] for
421/// transport-level replay protection.
422///
423/// # Examples
424///
425/// ```
426/// use ma_core::ReplayGuard;
427///
428/// let mut guard = ReplayGuard::new(120); // 2-minute window
429/// // or use the default (120 seconds):
430/// let mut guard = ReplayGuard::default();
431/// ```
432#[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/// An encrypted message envelope for transport.
472///
473/// Contains an ephemeral X25519 public key and XChaCha20-Poly1305 encrypted
474/// headers and content. Created by [`Message::enclose_for`] and opened by
475/// [`Envelope::open`] or [`Envelope::open_with_replay_guard`].
476///
477/// # Examples
478///
479/// ```
480/// use ma_core::{generate_identity_from_secret, Message, Envelope, EncryptionKey, SigningKey, Did};
481///
482/// let alice = generate_identity_from_secret([1u8; 32]).unwrap();
483/// let bob = generate_identity_from_secret([2u8; 32]).unwrap();
484///
485/// let alice_sign_url = Did::new_url(&alice.subject_url.ipns, None::<String>).unwrap();
486/// let alice_key = SigningKey::from_private_key_bytes(
487///     alice_sign_url,
488///     hex::decode(&alice.signing_private_key_hex).unwrap().try_into().unwrap(),
489/// ).unwrap();
490///
491/// let msg = Message::new(
492///     alice.document.id.clone(),
493///     format!("{}#inbox", bob.document.id),
494///     "application/vnd.ma.message",
495///     "text/plain",
496///     b"secret",
497///     &alice_key,
498/// ).unwrap();
499///
500/// // Encrypt for Bob
501/// let envelope = msg.enclose_for(&bob.document).unwrap();
502///
503/// // Bob decrypts
504/// let bob_enc_url = Did::new_url(&bob.subject_url.ipns, None::<String>).unwrap();
505/// let bob_enc_key = EncryptionKey::from_private_key_bytes(
506///     bob_enc_url,
507///     hex::decode(&bob.encryption_private_key_hex).unwrap().try_into().unwrap(),
508/// ).unwrap();
509/// let decrypted = envelope.open(&bob_enc_key, &alice.document).unwrap();
510/// assert_eq!(decrypted.payload(), b"secret");
511/// ```
512#[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(()); // 0 = never expires
659    }
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; // 1 s epoch — well in the past
945        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; // 0 = never expires
990        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        // Create with a valid 60-second window.
1002        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        // Rewind exp to 1 ns (well in the past) and re-sign.
1014        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}