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