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