Skip to main content

ma_core/
msg.rs

1use chacha20poly1305::{
2    aead::{Aead, AeadCore, KeyInit},
3    Key, XChaCha20Poly1305, XNonce,
4};
5use ed25519_dalek::{Signature, Verifier};
6use nanoid::nanoid;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use web_time::{SystemTime, UNIX_EPOCH};
10
11use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
12
13use crate::{
14    constants,
15    did::Did,
16    doc::Document,
17    error::{MaError, MaResult as Result},
18    key::{EncryptionKey, SigningKey},
19};
20
21pub const MESSAGE_PREFIX: &str = "/ma/";
22
23pub const DEFAULT_REPLAY_WINDOW_SECS: u64 = 120;
24pub const DEFAULT_MAX_CLOCK_SKEW_SECS: u64 = 30;
25pub const DEFAULT_MESSAGE_TTL_SECS: u64 = 3600;
26
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 wire format
138/// let bytes = msg.encode().unwrap();
139/// let restored = Message::decode(&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 encode(&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 decode(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 encode(&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 decode(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    SystemTime::now()
559        .duration_since(UNIX_EPOCH)
560        .map(|duration| duration.as_nanos() as f64 / 1_000_000_000.0)
561        .map_err(|_| MaError::InvalidMessageTimestamp)
562}
563
564fn validate_message_freshness(created_at: f64, ttl: u64) -> Result<()> {
565    let now = now_unix_secs()?;
566
567    if created_at > now + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 {
568        return Err(MaError::MessageFromFuture);
569    }
570
571    if ttl == 0 {
572        return Ok(());
573    }
574
575    if now - created_at > ttl as f64 {
576        return Err(MaError::MessageTooOld);
577    }
578
579    Ok(())
580}
581
582fn compute_shared_secret(
583    ephemeral_key_bytes: &[u8],
584    recipient_key: &EncryptionKey,
585) -> Result<[u8; 32]> {
586    let ephemeral_public = X25519PublicKey::from(
587        <[u8; 32]>::try_from(ephemeral_key_bytes)
588            .map_err(|_| MaError::InvalidEphemeralKeyLength)?,
589    );
590    Ok(recipient_key.shared_secret(&ephemeral_public))
591}
592
593fn derive_symmetric_key(shared_secret: &[u8; 32], label: &str) -> Key {
594    let derived = blake3::derive_key(label, shared_secret);
595    *Key::from_slice(&derived)
596}
597
598fn encrypt(data: &[u8], key: Key) -> Result<Vec<u8>> {
599    let cipher = XChaCha20Poly1305::new(&key);
600    let nonce = XChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
601    let encrypted = cipher.encrypt(&nonce, data).map_err(|_| MaError::Crypto)?;
602
603    let mut out = nonce.to_vec();
604    out.extend_from_slice(&encrypted);
605    Ok(out)
606}
607
608fn decrypt(data: &[u8], shared_secret: &[u8; 32], label: &str) -> Result<Vec<u8>> {
609    if data.len() < 24 {
610        return Err(MaError::CiphertextTooShort);
611    }
612
613    let key = derive_symmetric_key(shared_secret, label);
614    let cipher = XChaCha20Poly1305::new(&key);
615    let nonce = XNonce::from_slice(&data[..24]);
616
617    cipher
618        .decrypt(nonce, &data[24..])
619        .map_err(|_| MaError::Crypto)
620}
621
622fn content_hash(content: &[u8]) -> [u8; 32] {
623    blake3::hash(content).into()
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629    use crate::{doc::VerificationMethod, key::EncryptionKey};
630
631    fn fixture_documents() -> (
632        SigningKey,
633        EncryptionKey,
634        Document,
635        SigningKey,
636        EncryptionKey,
637        Document,
638    ) {
639        let sender_did = Did::new_url("k51sender", None::<String>).expect("sender did");
640        let sender_sign_url = Did::new_url("k51sender", None::<String>).expect("sender sign did");
641        let sender_enc_url = Did::new_url("k51sender", None::<String>).expect("sender enc did");
642        let sender_signing = SigningKey::generate(sender_sign_url).expect("sender signing key");
643        let sender_encryption =
644            EncryptionKey::generate(sender_enc_url).expect("sender encryption key");
645
646        let recipient_did = Did::new_url("k51recipient", None::<String>).expect("recipient did");
647        let recipient_sign_url =
648            Did::new_url("k51recipient", None::<String>).expect("recipient sign did");
649        let recipient_enc_url =
650            Did::new_url("k51recipient", None::<String>).expect("recipient enc did");
651        let recipient_signing =
652            SigningKey::generate(recipient_sign_url).expect("recipient signing key");
653        let recipient_encryption =
654            EncryptionKey::generate(recipient_enc_url).expect("recipient encryption key");
655
656        let mut sender_document = Document::new(&sender_did, &sender_did);
657        let sender_assertion = VerificationMethod::new(
658            sender_did.base_id(),
659            sender_did.base_id(),
660            sender_signing.key_type.clone(),
661            sender_signing.did.fragment.as_deref().unwrap_or_default(),
662            sender_signing.public_key_multibase.clone(),
663        )
664        .expect("sender assertion vm");
665        let sender_key_agreement = VerificationMethod::new(
666            sender_did.base_id(),
667            sender_did.base_id(),
668            sender_encryption.key_type.clone(),
669            sender_encryption
670                .did
671                .fragment
672                .as_deref()
673                .unwrap_or_default(),
674            sender_encryption.public_key_multibase.clone(),
675        )
676        .expect("sender key agreement vm");
677        sender_document
678            .add_verification_method(sender_assertion.clone())
679            .expect("add sender assertion");
680        sender_document
681            .add_verification_method(sender_key_agreement.clone())
682            .expect("add sender key agreement");
683        sender_document.assertion_method = vec![sender_assertion.id.clone()];
684        sender_document.key_agreement = vec![sender_key_agreement.id.clone()];
685        sender_document
686            .sign(&sender_signing, &sender_assertion)
687            .expect("sign sender doc");
688
689        let mut recipient_document = Document::new(&recipient_did, &recipient_did);
690        let recipient_assertion = VerificationMethod::new(
691            recipient_did.base_id(),
692            recipient_did.base_id(),
693            recipient_signing.key_type.clone(),
694            recipient_signing
695                .did
696                .fragment
697                .as_deref()
698                .unwrap_or_default(),
699            recipient_signing.public_key_multibase.clone(),
700        )
701        .expect("recipient assertion vm");
702        let recipient_key_agreement = VerificationMethod::new(
703            recipient_did.base_id(),
704            recipient_did.base_id(),
705            recipient_encryption.key_type.clone(),
706            recipient_encryption
707                .did
708                .fragment
709                .as_deref()
710                .unwrap_or_default(),
711            recipient_encryption.public_key_multibase.clone(),
712        )
713        .expect("recipient key agreement vm");
714        recipient_document
715            .add_verification_method(recipient_assertion.clone())
716            .expect("add recipient assertion");
717        recipient_document
718            .add_verification_method(recipient_key_agreement.clone())
719            .expect("add recipient key agreement");
720        recipient_document.assertion_method = vec![recipient_assertion.id.clone()];
721        recipient_document.key_agreement = vec![recipient_key_agreement.id.clone()];
722        recipient_document
723            .sign(&recipient_signing, &recipient_assertion)
724            .expect("sign recipient doc");
725
726        (
727            sender_signing,
728            sender_encryption,
729            sender_document,
730            recipient_signing,
731            recipient_encryption,
732            recipient_document,
733        )
734    }
735
736    #[test]
737    fn did_round_trip() {
738        let did = Did::new_url(
739            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
740            Some("bahner"),
741        )
742        .expect("did must build");
743        let parsed = Did::try_from(did.id().as_str()).expect("did must parse");
744        assert_eq!(did, parsed);
745    }
746
747    #[test]
748    fn subject_url_round_trip() {
749        let did = Did::new_url(
750            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
751            None::<String>,
752        )
753        .expect("subject did must build");
754        let parsed = Did::try_from(did.id().as_str()).expect("subject did must parse");
755        assert_eq!(did, parsed);
756    }
757
758    #[test]
759    fn document_signs_and_verifies() {
760        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
761        sender_signing.validate().expect("signing key validates");
762        sender_document.validate().expect("document validates");
763    }
764
765    #[test]
766    fn envelope_round_trip() {
767        let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
768            fixture_documents();
769        let message = Message::new(
770            sender_document.id.clone(),
771            recipient_document.id.clone(),
772            "application/x-ma",
773            b"look".to_vec(),
774            &sender_signing,
775        )
776        .expect("message creation");
777        message
778            .verify_with_document(&sender_document)
779            .expect("message signature verifies");
780
781        let envelope = message
782            .enclose_for(&recipient_document)
783            .expect("message encloses");
784        let opened = envelope
785            .open(&recipient_document, &recipient_encryption, &sender_document)
786            .expect("envelope opens");
787
788        assert_eq!(opened.content, b"look");
789        assert_eq!(opened.from, sender_document.id);
790        assert_eq!(opened.to, recipient_document.id);
791    }
792
793    #[test]
794    fn tampered_content_fails_signature_verification() {
795        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
796        let mut message = Message::new(
797            sender_document.id.clone(),
798            recipient_document.id.clone(),
799            "application/x-ma",
800            b"look".to_vec(),
801            &sender_signing,
802        )
803        .expect("message creation");
804
805        message.content = b"tampered".to_vec();
806        let result = message.verify_with_document(&sender_document);
807        assert!(matches!(result, Err(MaError::InvalidMessageSignature)));
808    }
809
810    #[test]
811    fn stale_message_is_rejected() {
812        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
813        let mut message = Message::new(
814            sender_document.id.clone(),
815            recipient_document.id.clone(),
816            "application/x-ma",
817            b"look".to_vec(),
818            &sender_signing,
819        )
820        .expect("message creation");
821
822        message.created_at = 0.0;
823        let result = message.verify_with_document(&sender_document);
824        assert!(matches!(result, Err(MaError::MessageTooOld)));
825    }
826
827    #[test]
828    fn future_message_is_rejected() {
829        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
830        let mut message = Message::new(
831            sender_document.id.clone(),
832            recipient_document.id.clone(),
833            "application/x-ma",
834            b"look".to_vec(),
835            &sender_signing,
836        )
837        .expect("message creation");
838
839        message.created_at =
840            now_unix_secs().expect("current timestamp") + DEFAULT_MAX_CLOCK_SKEW_SECS as f64 + 60.0;
841        message
842            .sign(&sender_signing)
843            .expect("re-sign with updated timestamp");
844
845        let result = message.verify_with_document(&sender_document);
846        assert!(matches!(result, Err(MaError::MessageFromFuture)));
847    }
848
849    #[test]
850    fn ttl_zero_disables_expiration() {
851        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
852        let mut message = Message::new(
853            sender_document.id.clone(),
854            recipient_document.id.clone(),
855            "application/x-ma",
856            b"look".to_vec(),
857            &sender_signing,
858        )
859        .expect("message creation");
860
861        message.created_at = 0.0;
862        message.ttl = 0;
863        message.sign(&sender_signing).expect("re-sign with ttl=0");
864
865        message
866            .verify_with_document(&sender_document)
867            .expect("ttl=0 should bypass max-age rejection");
868    }
869
870    #[test]
871    fn custom_ttl_rejects_expired_message() {
872        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
873        let mut message = Message::new_with_ttl(
874            sender_document.id.clone(),
875            recipient_document.id.clone(),
876            "application/x-ma",
877            b"look".to_vec(),
878            1,
879            &sender_signing,
880        )
881        .expect("message creation with ttl");
882
883        message.created_at = now_unix_secs().expect("current timestamp") - 5.0;
884        message
885            .sign(&sender_signing)
886            .expect("re-sign with stale timestamp");
887
888        let result = message.verify_with_document(&sender_document);
889        assert!(matches!(result, Err(MaError::MessageTooOld)));
890    }
891
892    #[test]
893    fn replay_guard_rejects_duplicate_envelope() {
894        let (sender_signing, _, sender_document, _, recipient_encryption, recipient_document) =
895            fixture_documents();
896        let message = Message::new(
897            sender_document.id.clone(),
898            recipient_document.id.clone(),
899            "application/x-ma",
900            b"look".to_vec(),
901            &sender_signing,
902        )
903        .expect("message creation");
904
905        let envelope = message
906            .enclose_for(&recipient_document)
907            .expect("message encloses");
908        let mut replay_guard = ReplayGuard::default();
909
910        envelope
911            .open_with_replay_guard(
912                &recipient_document,
913                &recipient_encryption,
914                &sender_document,
915                &mut replay_guard,
916            )
917            .expect("first delivery accepted");
918
919        let second = envelope.open_with_replay_guard(
920            &recipient_document,
921            &recipient_encryption,
922            &sender_document,
923            &mut replay_guard,
924        );
925        assert!(matches!(second, Err(MaError::ReplayDetected)));
926    }
927
928    #[test]
929    fn broadcast_allows_empty_recipient() {
930        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
931        let message = Message::new(
932            sender_document.id.clone(),
933            String::new(),
934            "application/x-ma-broadcast",
935            b"hello everyone".to_vec(),
936            &sender_signing,
937        )
938        .expect("broadcast message creation");
939
940        message
941            .verify_with_document(&sender_document)
942            .expect("broadcast with empty recipient verifies");
943    }
944
945    #[test]
946    fn broadcast_rejects_recipient() {
947        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
948        let result = Message::new(
949            sender_document.id.clone(),
950            recipient_document.id.clone(),
951            "application/x-ma-broadcast",
952            b"hello everyone".to_vec(),
953            &sender_signing,
954        );
955
956        assert!(matches!(
957            result,
958            Err(MaError::BroadcastMustNotHaveRecipient)
959        ));
960    }
961
962    #[test]
963    fn message_requires_recipient() {
964        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
965        let result = Message::new(
966            sender_document.id.clone(),
967            String::new(),
968            "application/x-ma-message",
969            b"secret".to_vec(),
970            &sender_signing,
971        );
972
973        assert!(matches!(result, Err(MaError::MessageRequiresRecipient)));
974    }
975
976    #[test]
977    fn unknown_content_type_allows_empty_recipient() {
978        let (sender_signing, _, sender_document, _, _, _) = fixture_documents();
979        let message = Message::new(
980            sender_document.id.clone(),
981            String::new(),
982            "application/x-ma-custom",
983            b"whatever".to_vec(),
984            &sender_signing,
985        )
986        .expect("custom content type message creation");
987
988        message
989            .verify_with_document(&sender_document)
990            .expect("custom type with empty recipient verifies");
991    }
992
993    #[test]
994    fn unknown_content_type_allows_recipient() {
995        let (sender_signing, _, sender_document, _, _, recipient_document) = fixture_documents();
996        let message = Message::new(
997            sender_document.id.clone(),
998            recipient_document.id.clone(),
999            "application/x-ma-custom",
1000            b"whatever".to_vec(),
1001            &sender_signing,
1002        )
1003        .expect("custom content type with recipient");
1004
1005        message
1006            .verify_with_document(&sender_document)
1007            .expect("custom type with recipient verifies");
1008    }
1009}