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/x-ma-term: CBOR term — bare atom (:ok, :pong) or tuple ([:verb, ...]).
44        "application/cbor" | "application/x-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: f64,
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            "application/x-ma-broadcast" => {
97                if !recipient_is_empty {
98                    return Err(MaError::BroadcastMustNotHaveRecipient);
99                }
100            }
101            "application/x-ma-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/x-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: f64,
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_nanos()? + DEFAULT_MESSAGE_TTL_SECS * 1_000_000_000;
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, f64>,
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 - *seen_at <= self.window_secs as f64);
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/x-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<f64> {
584    SystemTime::now()
585        .duration_since(UNIX_EPOCH)
586        .map(|duration| duration.as_nanos() as f64 / 1_000_000_000.0)
587        .map_err(|_| MaError::InvalidMessageTimestamp)
588}
589
590fn now_unix_nanos() -> Result<u64> {
591    SystemTime::now()
592        .duration_since(UNIX_EPOCH)
593        .map(|d| d.as_nanos() as u64)
594        .map_err(|_| MaError::InvalidMessageTimestamp)
595}
596
597fn validate_message_freshness(created_at: f64, exp: u64) -> Result<()> {
598    let now = now_unix_secs()?;
599
600    if created_at > now + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 {
601        return Err(MaError::MessageFromFuture);
602    }
603
604    if exp == 0 {
605        return Ok(()); // 0 = never expires
606    }
607
608    let exp_secs = exp as f64 / 1_000_000_000.0;
609    if now > exp_secs + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 {
610        return Err(MaError::MessageTooOld);
611    }
612
613    Ok(())
614}
615
616fn compute_shared_secret(
617    ephemeral_key_bytes: &[u8],
618    recipient_key: &EncryptionKey,
619) -> Result<[u8; 32]> {
620    let ephemeral_public = X25519PublicKey::from(
621        <[u8; 32]>::try_from(ephemeral_key_bytes)
622            .map_err(|_| MaError::InvalidEphemeralKeyLength)?,
623    );
624    Ok(recipient_key.shared_secret(&ephemeral_public))
625}
626
627fn derive_symmetric_key(shared_secret: &[u8; 32], label: &str) -> Key {
628    let derived = blake3::derive_key(label, shared_secret);
629    *Key::from_slice(&derived)
630}
631
632fn encrypt(data: &[u8], key: Key) -> Result<Vec<u8>> {
633    let cipher = XChaCha20Poly1305::new(&key);
634    let nonce = XChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
635    let encrypted = cipher.encrypt(&nonce, data).map_err(|_| MaError::Crypto)?;
636
637    let mut out = nonce.to_vec();
638    out.extend_from_slice(&encrypted);
639    Ok(out)
640}
641
642fn decrypt(data: &[u8], shared_secret: &[u8; 32], label: &str) -> Result<Vec<u8>> {
643    if data.len() < 24 {
644        return Err(MaError::CiphertextTooShort);
645    }
646
647    let key = derive_symmetric_key(shared_secret, label);
648    let cipher = XChaCha20Poly1305::new(&key);
649    let nonce = XNonce::from_slice(&data[..24]);
650
651    cipher
652        .decrypt(nonce, &data[24..])
653        .map_err(|_| MaError::Crypto)
654}
655
656fn content_hash(content: &[u8]) -> [u8; 32] {
657    blake3::hash(content).into()
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use crate::{doc::VerificationMethod, key::EncryptionKey};
664
665    fn fixture_documents() -> (
666        SigningKey,
667        EncryptionKey,
668        Document,
669        SigningKey,
670        EncryptionKey,
671        Document,
672    ) {
673        let sender_did = Did::new_url("k51sender", None::<String>).expect("sender did");
674        let sender_sign_url = Did::new_url("k51sender", None::<String>).expect("sender sign did");
675        let sender_enc_url = Did::new_url("k51sender", None::<String>).expect("sender enc did");
676        let sender_signing = SigningKey::generate(sender_sign_url).expect("sender signing key");
677        let sender_encryption =
678            EncryptionKey::generate(sender_enc_url).expect("sender encryption key");
679
680        let recipient_did = Did::new_url("k51recipient", None::<String>).expect("recipient did");
681        let recipient_sign_url =
682            Did::new_url("k51recipient", None::<String>).expect("recipient sign did");
683        let recipient_enc_url =
684            Did::new_url("k51recipient", None::<String>).expect("recipient enc did");
685        let recipient_signing =
686            SigningKey::generate(recipient_sign_url).expect("recipient signing key");
687        let recipient_encryption =
688            EncryptionKey::generate(recipient_enc_url).expect("recipient encryption key");
689
690        let mut sender_document = Document::new(&sender_did, &sender_did);
691        let sender_assertion = VerificationMethod::new(
692            sender_did.base_id(),
693            sender_did.base_id(),
694            sender_signing.key_type.clone(),
695            sender_signing.did.fragment.as_deref().unwrap_or_default(),
696            sender_signing.public_key_multibase.clone(),
697        )
698        .expect("sender assertion vm");
699        let sender_key_agreement = VerificationMethod::new(
700            sender_did.base_id(),
701            sender_did.base_id(),
702            sender_encryption.key_type.clone(),
703            sender_encryption
704                .did
705                .fragment
706                .as_deref()
707                .unwrap_or_default(),
708            sender_encryption.public_key_multibase.clone(),
709        )
710        .expect("sender key agreement vm");
711        sender_document
712            .add_verification_method(sender_assertion.clone())
713            .expect("add sender assertion");
714        sender_document
715            .add_verification_method(sender_key_agreement.clone())
716            .expect("add sender key agreement");
717        sender_document.assertion_method = vec![sender_assertion.id.clone()];
718        sender_document.key_agreement = vec![sender_key_agreement.id.clone()];
719        sender_document
720            .sign(&sender_signing, &sender_assertion)
721            .expect("sign sender doc");
722
723        let mut recipient_document = Document::new(&recipient_did, &recipient_did);
724        let recipient_assertion = VerificationMethod::new(
725            recipient_did.base_id(),
726            recipient_did.base_id(),
727            recipient_signing.key_type.clone(),
728            recipient_signing
729                .did
730                .fragment
731                .as_deref()
732                .unwrap_or_default(),
733            recipient_signing.public_key_multibase.clone(),
734        )
735        .expect("recipient assertion vm");
736        let recipient_key_agreement = VerificationMethod::new(
737            recipient_did.base_id(),
738            recipient_did.base_id(),
739            recipient_encryption.key_type.clone(),
740            recipient_encryption
741                .did
742                .fragment
743                .as_deref()
744                .unwrap_or_default(),
745            recipient_encryption.public_key_multibase.clone(),
746        )
747        .expect("recipient key agreement vm");
748        recipient_document
749            .add_verification_method(recipient_assertion.clone())
750            .expect("add recipient assertion");
751        recipient_document
752            .add_verification_method(recipient_key_agreement.clone())
753            .expect("add recipient key agreement");
754        recipient_document.assertion_method = vec![recipient_assertion.id.clone()];
755        recipient_document.key_agreement = vec![recipient_key_agreement.id.clone()];
756        recipient_document
757            .sign(&recipient_signing, &recipient_assertion)
758            .expect("sign recipient doc");
759
760        (
761            sender_signing,
762            sender_encryption,
763            sender_document,
764            recipient_signing,
765            recipient_encryption,
766            recipient_document,
767        )
768    }
769
770    #[test]
771    fn did_round_trip() {
772        let did = Did::new_url(
773            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
774            Some("bahner"),
775        )
776        .expect("did must build");
777        let parsed = Did::try_from(did.id().as_str()).expect("did must parse");
778        assert_eq!(did, parsed);
779    }
780
781    #[test]
782    fn subject_url_round_trip() {
783        let did = Did::new_url(
784            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
785            None::<String>,
786        )
787        .expect("subject did must build");
788        let parsed = Did::try_from(did.id().as_str()).expect("subject did must parse");
789        assert_eq!(did, parsed);
790    }
791
792    #[test]
793    fn document_signs_and_verifies() {
794        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
795        sender_signing.validate().expect("signing key validates");
796        sender_document.validate().expect("document validates");
797    }
798
799    #[test]
800    fn envelope_round_trip() {
801        let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
802            fixture_documents();
803        let message = Message::new(
804            sender_document.id.clone(),
805            recipient_document.id.clone(),
806            "application/x-ma-message",
807            "text/plain",
808            b"look",
809            &sender_signing,
810        )
811        .expect("message creation");
812        message
813            .verify_with_document(&sender_document)
814            .expect("message signature verifies");
815
816        let envelope = message
817            .enclose_for(&recipient_document)
818            .expect("message encloses");
819        let opened = envelope
820            .open(&recipient_encryption, &sender_document)
821            .expect("envelope opens");
822
823        assert_eq!(opened.payload(), b"look");
824        assert_eq!(opened.from, sender_document.id);
825        assert_eq!(opened.to, recipient_document.id);
826    }
827
828    #[test]
829    fn tampered_content_fails_signature_verification() {
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",
837            &sender_signing,
838        )
839        .expect("message creation");
840
841        message.content = b"tampered".to_vec();
842        let result = message.verify_with_document(&sender_document);
843        assert!(matches!(result, Err(MaError::InvalidMessageSignature)));
844    }
845
846    #[test]
847    fn stale_message_is_rejected() {
848        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
849        let mut message = Message::new(
850            sender_document.id.clone(),
851            recipient_document.id.clone(),
852            "application/x-ma-message",
853            "text/plain",
854            b"look",
855            &sender_signing,
856        )
857        .expect("message creation");
858
859        message.created_at = 0.0;
860        message.exp = 1; // 1 ns epoch — well in the past
861        message
862            .sign(&sender_signing)
863            .expect("re-sign with past timestamps");
864        let result = message.verify_with_document(&sender_document);
865        assert!(matches!(result, Err(MaError::MessageTooOld)));
866    }
867
868    #[test]
869    fn future_message_is_rejected() {
870        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
871        let mut message = Message::new(
872            sender_document.id.clone(),
873            recipient_document.id.clone(),
874            "application/x-ma-message",
875            "text/plain",
876            b"look",
877            &sender_signing,
878        )
879        .expect("message creation");
880
881        message.created_at =
882            now_unix_secs().expect("current timestamp") + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 + 60.0;
883        message
884            .sign(&sender_signing)
885            .expect("re-sign with updated timestamp");
886
887        let result = message.verify_with_document(&sender_document);
888        assert!(matches!(result, Err(MaError::MessageFromFuture)));
889    }
890
891    #[test]
892    fn exp_zero_disables_expiration() {
893        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
894        let mut message = Message::new(
895            sender_document.id.clone(),
896            recipient_document.id.clone(),
897            "application/x-ma-message",
898            "text/plain",
899            b"look",
900            &sender_signing,
901        )
902        .expect("message creation");
903
904        message.created_at = 0.0;
905        message.exp = 0; // 0 = never expires
906        message.sign(&sender_signing).expect("re-sign with exp=0");
907
908        message
909            .verify_with_document(&sender_document)
910            .expect("exp=0 should bypass expiration check");
911    }
912
913    #[test]
914    fn custom_ttl_rejects_expired_message() {
915        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
916        let now_nanos = now_unix_nanos().expect("current timestamp");
917        // Create with a valid 60-second window.
918        let mut message = Message::new_with_exp(
919            sender_document.id.clone(),
920            recipient_document.id.clone(),
921            "application/x-ma-message",
922            "text/plain",
923            b"look",
924            now_nanos + 60_000_000_000,
925            &sender_signing,
926        )
927        .expect("message creation with custom exp");
928
929        // Rewind exp to 1 ns (well in the past) and re-sign.
930        message.exp = 1;
931        message
932            .sign(&sender_signing)
933            .expect("re-sign with expired exp");
934
935        let result = message.verify_with_document(&sender_document);
936        assert!(matches!(result, Err(MaError::MessageTooOld)));
937    }
938
939    #[test]
940    fn replay_guard_rejects_duplicate_envelope() {
941        let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
942            fixture_documents();
943        let message = Message::new(
944            sender_document.id.clone(),
945            recipient_document.id.clone(),
946            "application/x-ma-message",
947            "text/plain",
948            b"look",
949            &sender_signing,
950        )
951        .expect("message creation");
952
953        let envelope = message
954            .enclose_for(&recipient_document)
955            .expect("message encloses");
956        let mut replay_guard = ReplayGuard::default();
957
958        envelope
959            .open_with_replay_guard(&recipient_encryption, &sender_document, &mut replay_guard)
960            .expect("first delivery accepted");
961
962        let second = envelope.open_with_replay_guard(
963            &recipient_encryption,
964            &sender_document,
965            &mut replay_guard,
966        );
967        assert!(matches!(second, Err(MaError::ReplayDetected)));
968    }
969
970    #[test]
971    fn broadcast_allows_empty_recipient() {
972        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
973        let message = Message::new(
974            sender_document.id.clone(),
975            String::new(),
976            "application/x-ma-broadcast",
977            "text/plain",
978            b"hello everyone",
979            &sender_signing,
980        )
981        .expect("broadcast message creation");
982
983        message
984            .verify_with_document(&sender_document)
985            .expect("broadcast with empty recipient verifies");
986    }
987
988    #[test]
989    fn broadcast_rejects_recipient() {
990        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
991        let result = Message::new(
992            sender_document.id.clone(),
993            recipient_document.id.clone(),
994            "application/x-ma-broadcast",
995            "text/plain",
996            b"hello everyone",
997            &sender_signing,
998        );
999
1000        assert!(matches!(
1001            result,
1002            Err(MaError::BroadcastMustNotHaveRecipient)
1003        ));
1004    }
1005
1006    #[test]
1007    fn message_requires_recipient() {
1008        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
1009        let result = Message::new(
1010            sender_document.id.clone(),
1011            String::new(),
1012            "application/x-ma-message",
1013            "text/plain",
1014            b"secret",
1015            &sender_signing,
1016        );
1017
1018        assert!(matches!(result, Err(MaError::MessageRequiresRecipient)));
1019    }
1020
1021    #[test]
1022    fn unknown_content_type_allows_empty_recipient() {
1023        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
1024        let message = Message::new(
1025            sender_document.id.clone(),
1026            String::new(),
1027            "application/x-ma-custom",
1028            "text/plain",
1029            b"whatever",
1030            &sender_signing,
1031        )
1032        .expect("custom content type message creation");
1033
1034        message
1035            .verify_with_document(&sender_document)
1036            .expect("custom type with empty recipient verifies");
1037    }
1038
1039    #[test]
1040    fn unknown_content_type_allows_recipient() {
1041        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
1042        let message = Message::new(
1043            sender_document.id.clone(),
1044            recipient_document.id.clone(),
1045            "application/x-ma-custom",
1046            "text/plain",
1047            b"whatever",
1048            &sender_signing,
1049        )
1050        .expect("custom content type with recipient");
1051
1052        message
1053            .verify_with_document(&sender_document)
1054            .expect("custom type with recipient verifies");
1055    }
1056}