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