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