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    pub fn encode(&self) -> Result<Vec<u8>> {
222        let mut out = Vec::new();
223        ciborium::ser::into_writer(self, &mut out)
224            .map_err(|error| MaError::CborEncode(error.to_string()))?;
225        Ok(out)
226    }
227
228    pub fn decode(bytes: &[u8]) -> Result<Self> {
229        ciborium::de::from_reader(bytes).map_err(|error| MaError::CborDecode(error.to_string()))
230    }
231
232    #[must_use]
233    pub fn unsigned_headers(&self) -> Headers {
234        Headers {
235            id: self.id.clone(),
236            protocol: self.protocol.clone(),
237            message_type: self.message_type.clone(),
238            from: self.from.clone(),
239            to: self.to.clone(),
240            created_at: self.created_at,
241            exp: self.exp,
242            content_type: self.content_type.clone(),
243            reply_to: self.reply_to.clone(),
244            content_hash: content_hash(&self.content),
245            signature: Vec::new(),
246        }
247    }
248
249    #[must_use]
250    pub fn headers(&self) -> Headers {
251        let mut headers = self.unsigned_headers();
252        headers.signature.clone_from(&self.signature);
253        headers
254    }
255
256    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
257        let bytes = self.unsigned_headers_cbor()?;
258        self.signature = signing_key.sign(&bytes);
259        Ok(())
260    }
261
262    pub fn verify_with_document(&self, sender_document: &Document) -> Result<()> {
263        if self.from.is_empty() {
264            return Err(MaError::MissingSender);
265        }
266
267        if self.signature.is_empty() {
268            return Err(MaError::MissingSignature);
269        }
270
271        let sender_did = Did::try_from(self.from.as_str())?;
272        if sender_document.id != sender_did.base_id() {
273            return Err(MaError::InvalidRecipient);
274        }
275
276        self.headers().validate()?;
277        let bytes = self.unsigned_headers_cbor()?;
278        let signature =
279            Signature::from_slice(&self.signature).map_err(|_| MaError::InvalidMessageSignature)?;
280        sender_document
281            .assertion_method_public_key()?
282            .verify(&bytes, &signature)
283            .map_err(|_| MaError::InvalidMessageSignature)
284    }
285
286    pub fn enclose_for(&self, recipient_document: &Document) -> Result<Envelope> {
287        self.headers().validate()?;
288
289        let recipient_public_key =
290            X25519PublicKey::from(recipient_document.key_agreement_public_key_bytes()?);
291        let ephemeral_secret = StaticSecret::random_from_rng(rand_core::OsRng);
292        let ephemeral_public = X25519PublicKey::from(&ephemeral_secret);
293        let shared_secret = ephemeral_secret
294            .diffie_hellman(&recipient_public_key)
295            .to_bytes();
296
297        let encrypted_headers = encrypt(
298            &self.headers_cbor()?,
299            derive_symmetric_key(&shared_secret, constants::BLAKE3_HEADERS_LABEL),
300        )?;
301
302        let encrypted_content = encrypt(
303            &self.content,
304            derive_symmetric_key(&shared_secret, constants::blake3_content_label()),
305        )?;
306
307        Ok(Envelope {
308            ephemeral_key: ephemeral_public.as_bytes().to_vec(),
309            encrypted_content,
310            encrypted_headers,
311        })
312    }
313
314    fn headers_cbor(&self) -> Result<Vec<u8>> {
315        let mut out = Vec::new();
316        ciborium::ser::into_writer(&self.headers(), &mut out)
317            .map_err(|error| MaError::CborEncode(error.to_string()))?;
318        Ok(out)
319    }
320
321    fn unsigned_headers_cbor(&self) -> Result<Vec<u8>> {
322        let mut out = Vec::new();
323        ciborium::ser::into_writer(&self.unsigned_headers(), &mut out)
324            .map_err(|error| MaError::CborEncode(error.to_string()))?;
325        Ok(out)
326    }
327
328    fn validate_content(&self) -> Result<()> {
329        if self.content.is_empty() {
330            return Err(MaError::MissingContent);
331        }
332        Ok(())
333    }
334
335    fn from_headers(headers: Headers) -> Result<Self> {
336        headers.validate()?;
337        Ok(Self {
338            id: headers.id,
339            protocol: headers.protocol,
340            message_type: headers.message_type,
341            from: headers.from,
342            to: headers.to,
343            created_at: headers.created_at,
344            exp: headers.exp,
345            content_type: headers.content_type,
346            reply_to: headers.reply_to,
347            content: Vec::new(),
348            signature: headers.signature,
349        })
350    }
351}
352
353/// Sliding-window replay guard for message deduplication.
354///
355/// Tracks seen message IDs within a configurable time window and rejects
356/// duplicates. Use with [`Envelope::open_with_replay_guard`] for
357/// transport-level replay protection.
358///
359/// # Examples
360///
361/// ```
362/// use ma_core::ReplayGuard;
363///
364/// let mut guard = ReplayGuard::new(120); // 2-minute window
365/// // or use the default (120 seconds):
366/// let mut guard = ReplayGuard::default();
367/// ```
368#[derive(Debug, Clone)]
369pub struct ReplayGuard {
370    seen: HashMap<String, f64>,
371    window_secs: u64,
372}
373
374impl Default for ReplayGuard {
375    fn default() -> Self {
376        Self::new(DEFAULT_REPLAY_WINDOW_SECS)
377    }
378}
379
380impl ReplayGuard {
381    #[must_use]
382    pub fn new(window_secs: u64) -> Self {
383        Self {
384            seen: HashMap::new(),
385            window_secs,
386        }
387    }
388
389    pub fn check_and_insert(&mut self, headers: &Headers) -> Result<()> {
390        headers.validate()?;
391        self.prune_old()?;
392        if self.seen.contains_key(&headers.id) {
393            return Err(MaError::ReplayDetected);
394        }
395        self.seen.insert(headers.id.clone(), now_unix_secs()?);
396        Ok(())
397    }
398
399    fn prune_old(&mut self) -> Result<()> {
400        let now = now_unix_secs()?;
401        self.seen
402            .retain(|_, seen_at| now - *seen_at <= self.window_secs as f64);
403        Ok(())
404    }
405}
406
407/// An encrypted message envelope for transport.
408///
409/// Contains an ephemeral X25519 public key and XChaCha20-Poly1305 encrypted
410/// headers and content. Created by [`Message::enclose_for`] and opened by
411/// [`Envelope::open`] or [`Envelope::open_with_replay_guard`].
412///
413/// # Examples
414///
415/// ```
416/// use ma_core::{generate_identity_from_secret, Message, Envelope, EncryptionKey, SigningKey, Did};
417///
418/// let alice = generate_identity_from_secret([1u8; 32]).unwrap();
419/// let bob = generate_identity_from_secret([2u8; 32]).unwrap();
420///
421/// let alice_sign_url = Did::new_url(&alice.subject_url.ipns, None::<String>).unwrap();
422/// let alice_key = SigningKey::from_private_key_bytes(
423///     alice_sign_url,
424///     hex::decode(&alice.signing_private_key_hex).unwrap().try_into().unwrap(),
425/// ).unwrap();
426///
427/// let msg = Message::new(
428///     alice.document.id.clone(),
429///     bob.document.id.clone(),
430///     "application/x-ma-message",
431///     "text/plain",
432///     b"secret".to_vec(),
433///     &alice_key,
434/// ).unwrap();
435///
436/// // Encrypt for Bob
437/// let envelope = msg.enclose_for(&bob.document).unwrap();
438///
439/// // Bob decrypts
440/// let bob_enc_url = Did::new_url(&bob.subject_url.ipns, None::<String>).unwrap();
441/// let bob_enc_key = EncryptionKey::from_private_key_bytes(
442///     bob_enc_url,
443///     hex::decode(&bob.encryption_private_key_hex).unwrap().try_into().unwrap(),
444/// ).unwrap();
445/// let decrypted = envelope.open(&bob_enc_key, &alice.document).unwrap();
446/// assert_eq!(decrypted.content, b"secret");
447/// ```
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449pub struct Envelope {
450    #[serde(rename = "ephemeralKey")]
451    pub ephemeral_key: Vec<u8>,
452    #[serde(rename = "encryptedContent")]
453    pub encrypted_content: Vec<u8>,
454    #[serde(rename = "encryptedHeaders")]
455    pub encrypted_headers: Vec<u8>,
456}
457
458impl Envelope {
459    pub fn verify(&self) -> Result<()> {
460        if self.ephemeral_key.is_empty() {
461            return Err(MaError::MissingEnvelopeField("ephemeralKey"));
462        }
463        if self.ephemeral_key.len() != 32 {
464            return Err(MaError::InvalidEphemeralKeyLength);
465        }
466        if self.encrypted_content.is_empty() {
467            return Err(MaError::MissingEnvelopeField("encryptedContent"));
468        }
469        if self.encrypted_headers.is_empty() {
470            return Err(MaError::MissingEnvelopeField("encryptedHeaders"));
471        }
472        Ok(())
473    }
474
475    pub fn encode(&self) -> Result<Vec<u8>> {
476        let mut out = Vec::new();
477        ciborium::ser::into_writer(self, &mut out)
478            .map_err(|error| MaError::CborEncode(error.to_string()))?;
479        Ok(out)
480    }
481
482    pub fn decode(bytes: &[u8]) -> Result<Self> {
483        ciborium::de::from_reader(bytes).map_err(|error| MaError::CborDecode(error.to_string()))
484    }
485
486    pub fn open(
487        &self,
488        recipient_key: &EncryptionKey,
489        sender_document: &Document,
490    ) -> Result<Message> {
491        self.verify()?;
492
493        let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
494        let headers = self.decrypt_headers(&shared_secret)?;
495        headers.validate()?;
496        let content = self.decrypt_content(&shared_secret)?;
497
498        let mut message = Message::from_headers(headers)?;
499        message.content = content;
500        message.verify_with_document(sender_document)?;
501        Ok(message)
502    }
503
504    pub fn open_with_replay_guard(
505        &self,
506        recipient_key: &EncryptionKey,
507        sender_document: &Document,
508        replay_guard: &mut ReplayGuard,
509    ) -> Result<Message> {
510        self.verify()?;
511
512        let shared_secret = compute_shared_secret(&self.ephemeral_key, recipient_key)?;
513        let headers = self.decrypt_headers(&shared_secret)?;
514        replay_guard.check_and_insert(&headers)?;
515        let content = self.decrypt_content(&shared_secret)?;
516
517        let mut message = Message::from_headers(headers)?;
518        message.content = content;
519        message.verify_with_document(sender_document)?;
520        Ok(message)
521    }
522
523    fn decrypt_headers(&self, shared_secret: &[u8; 32]) -> Result<Headers> {
524        let decrypted = decrypt(
525            &self.encrypted_headers,
526            shared_secret,
527            constants::BLAKE3_HEADERS_LABEL,
528        )?;
529        ciborium::de::from_reader(decrypted.as_slice())
530            .map_err(|error| MaError::CborDecode(error.to_string()))
531    }
532
533    fn decrypt_content(&self, shared_secret: &[u8; 32]) -> Result<Vec<u8>> {
534        decrypt(
535            &self.encrypted_content,
536            shared_secret,
537            constants::blake3_content_label(),
538        )
539    }
540}
541
542fn validate_message_id(id: &str) -> Result<()> {
543    if id.is_empty() {
544        return Err(MaError::EmptyMessageId);
545    }
546
547    if !id
548        .chars()
549        .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
550    {
551        return Err(MaError::InvalidMessageId);
552    }
553
554    Ok(())
555}
556
557fn validate_protocol(kind: &str) -> Result<()> {
558    if kind == default_protocol() {
559        return Ok(());
560    }
561
562    Err(MaError::InvalidMessageType)
563}
564
565fn now_unix_secs() -> Result<f64> {
566    SystemTime::now()
567        .duration_since(UNIX_EPOCH)
568        .map(|duration| duration.as_nanos() as f64 / 1_000_000_000.0)
569        .map_err(|_| MaError::InvalidMessageTimestamp)
570}
571
572fn now_unix_nanos() -> Result<u64> {
573    SystemTime::now()
574        .duration_since(UNIX_EPOCH)
575        .map(|d| d.as_nanos() as u64)
576        .map_err(|_| MaError::InvalidMessageTimestamp)
577}
578
579fn validate_message_freshness(created_at: f64, exp: u64) -> Result<()> {
580    let now = now_unix_secs()?;
581
582    if created_at > now + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 {
583        return Err(MaError::MessageFromFuture);
584    }
585
586    if exp == 0 {
587        return Ok(()); // 0 = never expires
588    }
589
590    let exp_secs = exp as f64 / 1_000_000_000.0;
591    if now > exp_secs + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 {
592        return Err(MaError::MessageTooOld);
593    }
594
595    Ok(())
596}
597
598fn compute_shared_secret(
599    ephemeral_key_bytes: &[u8],
600    recipient_key: &EncryptionKey,
601) -> Result<[u8; 32]> {
602    let ephemeral_public = X25519PublicKey::from(
603        <[u8; 32]>::try_from(ephemeral_key_bytes)
604            .map_err(|_| MaError::InvalidEphemeralKeyLength)?,
605    );
606    Ok(recipient_key.shared_secret(&ephemeral_public))
607}
608
609fn derive_symmetric_key(shared_secret: &[u8; 32], label: &str) -> Key {
610    let derived = blake3::derive_key(label, shared_secret);
611    *Key::from_slice(&derived)
612}
613
614fn encrypt(data: &[u8], key: Key) -> Result<Vec<u8>> {
615    let cipher = XChaCha20Poly1305::new(&key);
616    let nonce = XChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
617    let encrypted = cipher.encrypt(&nonce, data).map_err(|_| MaError::Crypto)?;
618
619    let mut out = nonce.to_vec();
620    out.extend_from_slice(&encrypted);
621    Ok(out)
622}
623
624fn decrypt(data: &[u8], shared_secret: &[u8; 32], label: &str) -> Result<Vec<u8>> {
625    if data.len() < 24 {
626        return Err(MaError::CiphertextTooShort);
627    }
628
629    let key = derive_symmetric_key(shared_secret, label);
630    let cipher = XChaCha20Poly1305::new(&key);
631    let nonce = XNonce::from_slice(&data[..24]);
632
633    cipher
634        .decrypt(nonce, &data[24..])
635        .map_err(|_| MaError::Crypto)
636}
637
638fn content_hash(content: &[u8]) -> [u8; 32] {
639    blake3::hash(content).into()
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645    use crate::{doc::VerificationMethod, key::EncryptionKey};
646
647    fn fixture_documents() -> (
648        SigningKey,
649        EncryptionKey,
650        Document,
651        SigningKey,
652        EncryptionKey,
653        Document,
654    ) {
655        let sender_did = Did::new_url("k51sender", None::<String>).expect("sender did");
656        let sender_sign_url = Did::new_url("k51sender", None::<String>).expect("sender sign did");
657        let sender_enc_url = Did::new_url("k51sender", None::<String>).expect("sender enc did");
658        let sender_signing = SigningKey::generate(sender_sign_url).expect("sender signing key");
659        let sender_encryption =
660            EncryptionKey::generate(sender_enc_url).expect("sender encryption key");
661
662        let recipient_did = Did::new_url("k51recipient", None::<String>).expect("recipient did");
663        let recipient_sign_url =
664            Did::new_url("k51recipient", None::<String>).expect("recipient sign did");
665        let recipient_enc_url =
666            Did::new_url("k51recipient", None::<String>).expect("recipient enc did");
667        let recipient_signing =
668            SigningKey::generate(recipient_sign_url).expect("recipient signing key");
669        let recipient_encryption =
670            EncryptionKey::generate(recipient_enc_url).expect("recipient encryption key");
671
672        let mut sender_document = Document::new(&sender_did, &sender_did);
673        let sender_assertion = VerificationMethod::new(
674            sender_did.base_id(),
675            sender_did.base_id(),
676            sender_signing.key_type.clone(),
677            sender_signing.did.fragment.as_deref().unwrap_or_default(),
678            sender_signing.public_key_multibase.clone(),
679        )
680        .expect("sender assertion vm");
681        let sender_key_agreement = VerificationMethod::new(
682            sender_did.base_id(),
683            sender_did.base_id(),
684            sender_encryption.key_type.clone(),
685            sender_encryption
686                .did
687                .fragment
688                .as_deref()
689                .unwrap_or_default(),
690            sender_encryption.public_key_multibase.clone(),
691        )
692        .expect("sender key agreement vm");
693        sender_document
694            .add_verification_method(sender_assertion.clone())
695            .expect("add sender assertion");
696        sender_document
697            .add_verification_method(sender_key_agreement.clone())
698            .expect("add sender key agreement");
699        sender_document.assertion_method = vec![sender_assertion.id.clone()];
700        sender_document.key_agreement = vec![sender_key_agreement.id.clone()];
701        sender_document
702            .sign(&sender_signing, &sender_assertion)
703            .expect("sign sender doc");
704
705        let mut recipient_document = Document::new(&recipient_did, &recipient_did);
706        let recipient_assertion = VerificationMethod::new(
707            recipient_did.base_id(),
708            recipient_did.base_id(),
709            recipient_signing.key_type.clone(),
710            recipient_signing
711                .did
712                .fragment
713                .as_deref()
714                .unwrap_or_default(),
715            recipient_signing.public_key_multibase.clone(),
716        )
717        .expect("recipient assertion vm");
718        let recipient_key_agreement = VerificationMethod::new(
719            recipient_did.base_id(),
720            recipient_did.base_id(),
721            recipient_encryption.key_type.clone(),
722            recipient_encryption
723                .did
724                .fragment
725                .as_deref()
726                .unwrap_or_default(),
727            recipient_encryption.public_key_multibase.clone(),
728        )
729        .expect("recipient key agreement vm");
730        recipient_document
731            .add_verification_method(recipient_assertion.clone())
732            .expect("add recipient assertion");
733        recipient_document
734            .add_verification_method(recipient_key_agreement.clone())
735            .expect("add recipient key agreement");
736        recipient_document.assertion_method = vec![recipient_assertion.id.clone()];
737        recipient_document.key_agreement = vec![recipient_key_agreement.id.clone()];
738        recipient_document
739            .sign(&recipient_signing, &recipient_assertion)
740            .expect("sign recipient doc");
741
742        (
743            sender_signing,
744            sender_encryption,
745            sender_document,
746            recipient_signing,
747            recipient_encryption,
748            recipient_document,
749        )
750    }
751
752    #[test]
753    fn did_round_trip() {
754        let did = Did::new_url(
755            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
756            Some("bahner"),
757        )
758        .expect("did must build");
759        let parsed = Did::try_from(did.id().as_str()).expect("did must parse");
760        assert_eq!(did, parsed);
761    }
762
763    #[test]
764    fn subject_url_round_trip() {
765        let did = Did::new_url(
766            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
767            None::<String>,
768        )
769        .expect("subject did must build");
770        let parsed = Did::try_from(did.id().as_str()).expect("subject did must parse");
771        assert_eq!(did, parsed);
772    }
773
774    #[test]
775    fn document_signs_and_verifies() {
776        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
777        sender_signing.validate().expect("signing key validates");
778        sender_document.validate().expect("document validates");
779    }
780
781    #[test]
782    fn envelope_round_trip() {
783        let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
784            fixture_documents();
785        let message = Message::new(
786            sender_document.id.clone(),
787            recipient_document.id.clone(),
788            "application/x-ma-message",
789            "text/plain",
790            b"look".to_vec(),
791            &sender_signing,
792        )
793        .expect("message creation");
794        message
795            .verify_with_document(&sender_document)
796            .expect("message signature verifies");
797
798        let envelope = message
799            .enclose_for(&recipient_document)
800            .expect("message encloses");
801        let opened = envelope
802            .open(&recipient_encryption, &sender_document)
803            .expect("envelope opens");
804
805        assert_eq!(opened.content, b"look");
806        assert_eq!(opened.from, sender_document.id);
807        assert_eq!(opened.to, recipient_document.id);
808    }
809
810    #[test]
811    fn tampered_content_fails_signature_verification() {
812        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
813        let mut 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
823        message.content = b"tampered".to_vec();
824        let result = message.verify_with_document(&sender_document);
825        assert!(matches!(result, Err(MaError::InvalidMessageSignature)));
826    }
827
828    #[test]
829    fn stale_message_is_rejected() {
830        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
831        let mut message = Message::new(
832            sender_document.id.clone(),
833            recipient_document.id.clone(),
834            "application/x-ma-message",
835            "text/plain",
836            b"look".to_vec(),
837            &sender_signing,
838        )
839        .expect("message creation");
840
841        message.created_at = 0.0;
842        message.exp = 1; // 1 ns epoch — well in the past
843        message
844            .sign(&sender_signing)
845            .expect("re-sign with past timestamps");
846        let result = message.verify_with_document(&sender_document);
847        assert!(matches!(result, Err(MaError::MessageTooOld)));
848    }
849
850    #[test]
851    fn future_message_is_rejected() {
852        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
853        let mut message = Message::new(
854            sender_document.id.clone(),
855            recipient_document.id.clone(),
856            "application/x-ma-message",
857            "text/plain",
858            b"look".to_vec(),
859            &sender_signing,
860        )
861        .expect("message creation");
862
863        message.created_at =
864            now_unix_secs().expect("current timestamp") + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 + 60.0;
865        message
866            .sign(&sender_signing)
867            .expect("re-sign with updated timestamp");
868
869        let result = message.verify_with_document(&sender_document);
870        assert!(matches!(result, Err(MaError::MessageFromFuture)));
871    }
872
873    #[test]
874    fn exp_zero_disables_expiration() {
875        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
876        let mut message = Message::new(
877            sender_document.id.clone(),
878            recipient_document.id.clone(),
879            "application/x-ma-message",
880            "text/plain",
881            b"look".to_vec(),
882            &sender_signing,
883        )
884        .expect("message creation");
885
886        message.created_at = 0.0;
887        message.exp = 0; // 0 = never expires
888        message.sign(&sender_signing).expect("re-sign with exp=0");
889
890        message
891            .verify_with_document(&sender_document)
892            .expect("exp=0 should bypass expiration check");
893    }
894
895    #[test]
896    fn custom_ttl_rejects_expired_message() {
897        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
898        let now_nanos = now_unix_nanos().expect("current timestamp");
899        // Create with a valid 60-second window.
900        let mut message = Message::new_with_exp(
901            sender_document.id.clone(),
902            recipient_document.id.clone(),
903            "application/x-ma-message",
904            "text/plain",
905            b"look".to_vec(),
906            now_nanos + 60_000_000_000,
907            &sender_signing,
908        )
909        .expect("message creation with custom exp");
910
911        // Rewind exp to 1 ns (well in the past) and re-sign.
912        message.exp = 1;
913        message
914            .sign(&sender_signing)
915            .expect("re-sign with expired exp");
916
917        let result = message.verify_with_document(&sender_document);
918        assert!(matches!(result, Err(MaError::MessageTooOld)));
919    }
920
921    #[test]
922    fn replay_guard_rejects_duplicate_envelope() {
923        let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
924            fixture_documents();
925        let message = Message::new(
926            sender_document.id.clone(),
927            recipient_document.id.clone(),
928            "application/x-ma-message",
929            "text/plain",
930            b"look".to_vec(),
931            &sender_signing,
932        )
933        .expect("message creation");
934
935        let envelope = message
936            .enclose_for(&recipient_document)
937            .expect("message encloses");
938        let mut replay_guard = ReplayGuard::default();
939
940        envelope
941            .open_with_replay_guard(&recipient_encryption, &sender_document, &mut replay_guard)
942            .expect("first delivery accepted");
943
944        let second = envelope.open_with_replay_guard(
945            &recipient_encryption,
946            &sender_document,
947            &mut replay_guard,
948        );
949        assert!(matches!(second, Err(MaError::ReplayDetected)));
950    }
951
952    #[test]
953    fn broadcast_allows_empty_recipient() {
954        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
955        let message = Message::new(
956            sender_document.id.clone(),
957            String::new(),
958            "application/x-ma-broadcast",
959            "text/plain",
960            b"hello everyone".to_vec(),
961            &sender_signing,
962        )
963        .expect("broadcast message creation");
964
965        message
966            .verify_with_document(&sender_document)
967            .expect("broadcast with empty recipient verifies");
968    }
969
970    #[test]
971    fn broadcast_rejects_recipient() {
972        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
973        let result = Message::new(
974            sender_document.id.clone(),
975            recipient_document.id.clone(),
976            "application/x-ma-broadcast",
977            "text/plain",
978            b"hello everyone".to_vec(),
979            &sender_signing,
980        );
981
982        assert!(matches!(
983            result,
984            Err(MaError::BroadcastMustNotHaveRecipient)
985        ));
986    }
987
988    #[test]
989    fn message_requires_recipient() {
990        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
991        let result = Message::new(
992            sender_document.id.clone(),
993            String::new(),
994            "application/x-ma-message",
995            "text/plain",
996            b"secret".to_vec(),
997            &sender_signing,
998        );
999
1000        assert!(matches!(result, Err(MaError::MessageRequiresRecipient)));
1001    }
1002
1003    #[test]
1004    fn unknown_content_type_allows_empty_recipient() {
1005        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
1006        let message = Message::new(
1007            sender_document.id.clone(),
1008            String::new(),
1009            "application/x-ma-custom",
1010            "text/plain",
1011            b"whatever".to_vec(),
1012            &sender_signing,
1013        )
1014        .expect("custom content type message creation");
1015
1016        message
1017            .verify_with_document(&sender_document)
1018            .expect("custom type with empty recipient verifies");
1019    }
1020
1021    #[test]
1022    fn unknown_content_type_allows_recipient() {
1023        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
1024        let message = Message::new(
1025            sender_document.id.clone(),
1026            recipient_document.id.clone(),
1027            "application/x-ma-custom",
1028            "text/plain",
1029            b"whatever".to_vec(),
1030            &sender_signing,
1031        )
1032        .expect("custom content type with recipient");
1033
1034        message
1035            .verify_with_document(&sender_document)
1036            .expect("custom type with recipient verifies");
1037    }
1038}