Skip to main content

ma_did/
msg.rs

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