Skip to main content

ma_core/
msg.rs

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