Skip to main content

mostro_core/
nip59.rs

1//! NIP-59 GiftWrap transport for Mostro messages.
2//!
3//! Every message exchanged with a Mostro node travels through the same
4//! pipeline:
5//!
6//! ```text
7//! Message -> JSON((Message, Option<Signature>)) -> Rumor -> Seal -> GiftWrap
8//! ```
9//!
10//! Mostro splits signing across two keys: a long-lived **identity key**
11//! signs the seal (and encrypts it to the receiver), while a per-trade
12//! **trade key** authors the rumor and produces the inner tuple signature.
13//! This deliberately breaks NIP-59's "rumor author == seal signer"
14//! convention that `nostr-sdk` 0.44 enforces via `SenderMismatch`, so the
15//! unwrap path does its own NIP-44 + signature verification instead of
16//! calling `nip59::extract_rumor`.
17//!
18//! The module centralizes wrap/unwrap so clients do not need to reimplement
19//! NIP-59 glue themselves. It does not manage relays, subscriptions,
20//! waiters or persistence — the returned `Event` is ready to publish, and
21//! the caller decides how to do so.
22
23use std::str::FromStr;
24
25use crate::message::{Action, Message, Payload};
26use crate::prelude::{CantDoReason, MostroError, ServiceError};
27use nostr_sdk::nips::{nip44, nip59};
28use nostr_sdk::prelude::*;
29
30/// Options controlling how a Mostro message is wrapped.
31#[derive(Debug, Clone)]
32pub struct WrapOptions {
33    /// NIP-13 proof-of-work difficulty applied to the outer GiftWrap event.
34    pub pow: u8,
35    /// Optional expiration tag for the outer GiftWrap event.
36    pub expiration: Option<Timestamp>,
37    /// When true the inner rumor content is `(Message, Some(Signature))`,
38    /// with the signature produced from the JSON of `Message` using
39    /// `trade_keys`. When false the content is `(Message, None)`. Traffic
40    /// to a Mostro node always uses `true`.
41    pub signed: bool,
42}
43
44impl Default for WrapOptions {
45    fn default() -> Self {
46        Self {
47            pow: 0,
48            expiration: None,
49            signed: true,
50        }
51    }
52}
53
54/// A Mostro message recovered from an incoming GiftWrap, plus metadata from
55/// the outer envelopes.
56#[derive(Debug, Clone)]
57pub struct UnwrappedMessage {
58    /// The logical Mostro message carried inside the rumor.
59    pub message: Message,
60    /// Signature of the JSON-serialized `Message`, produced with the sender's
61    /// trade keys. Present only when the sender set `signed = true`.
62    pub signature: Option<Signature>,
63    /// Rumor author — the sender's trade public key.
64    pub sender: PublicKey,
65    /// Seal signer — the sender's long-lived identity public key. In
66    /// full-privacy mode (where the client reuses its trade key as identity)
67    /// this equals `sender`.
68    pub identity: PublicKey,
69    /// Rumor `created_at` timestamp.
70    pub created_at: Timestamp,
71}
72
73/// Build a GiftWrap event (`kind: 1059`) ready to be published to a relay.
74///
75/// * `message` — the Mostro message to send.
76/// * `identity_keys` — long-lived identity keys. Sign the seal (kind 13)
77///   and encrypt it to `receiver` via NIP-44. Callers that want the
78///   "full privacy" mode (no stable identity, no reputation) should pass
79///   the same value as `trade_keys`.
80/// * `trade_keys` — per-trade keys. Author of the rumor (kind 1) and
81///   signer of the inner tuple signature when `opts.signed == true`.
82/// * `receiver` — the Mostro node public key.
83/// * `opts` — wrap options (PoW, expiration, signed).
84pub async fn wrap_message(
85    message: &Message,
86    identity_keys: &Keys,
87    trade_keys: &Keys,
88    receiver: PublicKey,
89    opts: WrapOptions,
90) -> Result<Event, MostroError> {
91    let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
92
93    let content = if opts.signed {
94        let sig = Message::sign(message_json, trade_keys);
95        serde_json::to_string(&(message, Some(sig.to_string())))
96            .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?
97    } else {
98        serde_json::to_string(&(message, Option::<String>::None))
99            .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?
100    };
101
102    // PoW only applies to the outer GiftWrap (per WrapOptions docs); the
103    // rumor is encrypted inside the seal and never published on its own,
104    // so mining its event id would burn CPU for nothing.
105    let rumor = EventBuilder::text_note(content).build(trade_keys.public_key());
106
107    // Seal is encrypted and signed with identity_keys so the receiver can
108    // decrypt it via (receiver_secret, seal.pubkey) — this keeps seal.pubkey
109    // consistent with the encryption key, while leaving rumor.pubkey free to
110    // carry the per-trade key (the mismatch standard NIP-59 rejects).
111    let seal: Event = EventBuilder::seal(identity_keys, &receiver, rumor)
112        .await
113        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?
114        .sign(identity_keys)
115        .await
116        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
117
118    gift_wrap_from_seal_with_pow(&seal, receiver, opts.pow, opts.expiration)
119}
120
121/// Wrap an already built Seal into a NIP-59 GiftWrap with optional PoW and
122/// expiration. The outer event is signed with a freshly generated ephemeral
123/// key and carries a mandatory `p` tag pointing at `receiver`.
124fn gift_wrap_from_seal_with_pow(
125    seal: &Event,
126    receiver: PublicKey,
127    pow: u8,
128    expiration: Option<Timestamp>,
129) -> Result<Event, MostroError> {
130    if seal.kind != Kind::Seal {
131        return Err(MostroError::MostroInternalErr(
132            ServiceError::UnexpectedError("expected Seal kind".to_string()),
133        ));
134    }
135
136    let ephemeral = Keys::generate();
137    let encrypted = nip44::encrypt(
138        ephemeral.secret_key(),
139        &receiver,
140        seal.as_json(),
141        nip44::Version::default(),
142    )
143    .map_err(|e| MostroError::MostroInternalErr(ServiceError::EncryptionError(e.to_string())))?;
144
145    let mut tags: Vec<Tag> = Vec::new();
146    if let Some(exp) = expiration {
147        tags.push(Tag::expiration(exp));
148    }
149    tags.push(Tag::public_key(receiver));
150
151    EventBuilder::new(Kind::GiftWrap, encrypted)
152        .tags(tags)
153        .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK))
154        .pow(pow)
155        .sign_with_keys(&ephemeral)
156        .map_err(|e| MostroError::MostroInternalErr(ServiceError::NostrError(e.to_string())))
157}
158
159/// Try to open an incoming GiftWrap with the given `receiver_keys`.
160///
161/// Returns `Ok(None)` only when the outer NIP-44 layer could not be
162/// decrypted with `receiver_keys` — the canonical "not addressed to me"
163/// signal, so callers can try multiple candidate keys without treating
164/// each miss as fatal. Every other failure (corrupted seal, malformed
165/// rumor JSON, invalid signatures, etc.) yields `Err` so callers can tell
166/// "not mine" apart from "broken".
167///
168/// Does **not** enforce `seal.pubkey == rumor.pubkey`: Mostro signs the
169/// seal with the identity key and authors the rumor with the per-trade
170/// key, so the two legitimately differ.
171pub async fn unwrap_message(
172    event: &Event,
173    receiver_keys: &Keys,
174) -> Result<Option<UnwrappedMessage>, MostroError> {
175    if event.kind != Kind::GiftWrap {
176        return Err(MostroError::MostroInternalErr(
177            ServiceError::UnexpectedError("event is not a GiftWrap".to_string()),
178        ));
179    }
180
181    // Decrypt outer GiftWrap using (receiver_secret, ephemeral_pub).
182    // Failure here is the "not addressed to me" signal.
183    let seal_json = match nip44::decrypt(receiver_keys.secret_key(), &event.pubkey, &event.content)
184    {
185        Ok(s) => s,
186        Err(_) => return Ok(None),
187    };
188
189    let seal: Event = Event::from_json(&seal_json).map_err(|e| {
190        MostroError::MostroInternalErr(ServiceError::NostrError(format!(
191            "malformed seal JSON: {e}"
192        )))
193    })?;
194
195    if seal.kind != Kind::Seal {
196        return Err(MostroError::MostroInternalErr(
197            ServiceError::UnexpectedError("inner event is not a Seal".to_string()),
198        ));
199    }
200
201    seal.verify_signature().then_some(()).ok_or_else(|| {
202        MostroError::MostroInternalErr(ServiceError::NostrError(
203            "invalid seal signature".to_string(),
204        ))
205    })?;
206
207    // Decrypt the seal content using (receiver_secret, seal.pubkey). In
208    // Mostro, seal.pubkey is the sender's identity key, which is also the
209    // key that performed the NIP-44 encryption in `wrap_message`.
210    let rumor_json = nip44::decrypt(receiver_keys.secret_key(), &seal.pubkey, &seal.content)
211        .map_err(|e| {
212            MostroError::MostroInternalErr(ServiceError::DecryptionError(e.to_string()))
213        })?;
214
215    let rumor: UnsignedEvent = UnsignedEvent::from_json(&rumor_json).map_err(|e| {
216        MostroError::MostroInternalErr(ServiceError::NostrError(format!(
217            "malformed rumor JSON: {e}"
218        )))
219    })?;
220
221    if rumor.kind != Kind::TextNote {
222        return Err(MostroError::MostroInternalErr(
223            ServiceError::UnexpectedError("rumor is not a TextNote".to_string()),
224        ));
225    }
226
227    let (message, sig_str): (Message, Option<String>) = serde_json::from_str(&rumor.content)
228        .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?;
229
230    let signature = match sig_str {
231        Some(s) => {
232            let sig = Signature::from_str(&s).map_err(|e| {
233                MostroError::MostroInternalErr(ServiceError::UnexpectedError(format!(
234                    "malformed rumor signature: {e}"
235                )))
236            })?;
237            let message_json = message.as_json().map_err(MostroError::MostroInternalErr)?;
238            if !Message::verify_signature(message_json, rumor.pubkey, sig) {
239                return Err(MostroError::MostroInternalErr(
240                    ServiceError::UnexpectedError(
241                        "rumor signature does not verify against sender".to_string(),
242                    ),
243                ));
244            }
245            Some(sig)
246        }
247        None => None,
248    };
249
250    Ok(Some(UnwrappedMessage {
251        message,
252        signature,
253        sender: rumor.pubkey,
254        identity: seal.pubkey,
255        created_at: rumor.created_at,
256    }))
257}
258
259/// Validate a response received from a Mostro node.
260///
261/// * Returns `Err(MostroCantDo(reason))` when the payload is `CantDo`.
262/// * Returns `Err(MostroInternalErr(...))` when `expected_request_id` is
263///   provided and the inner message carries a different id, or no id at all
264///   on an action that requires one.
265/// * Otherwise returns `Ok(())`.
266///
267/// The allow-list of actions that may arrive without a `request_id` (server
268/// push messages such as state transitions, DMs, payment failures, etc.) is
269/// intentionally kept on the caller side, because the exact set depends on
270/// the client flow; this function only enforces the universal rules.
271pub fn validate_response(
272    message: &Message,
273    expected_request_id: Option<u64>,
274) -> Result<(), MostroError> {
275    let inner = message.get_inner_message_kind();
276
277    if let Some(Payload::CantDo(reason)) = &inner.payload {
278        return Err(MostroError::MostroCantDo(
279            reason.clone().unwrap_or(CantDoReason::InvalidAction),
280        ));
281    }
282
283    if let Some(expected) = expected_request_id {
284        match inner.request_id {
285            Some(got) if got == expected => {}
286            Some(_) => {
287                return Err(MostroError::MostroInternalErr(
288                    ServiceError::UnexpectedError("mismatched request_id".to_string()),
289                ));
290            }
291            None => {
292                if !action_accepts_missing_request_id(&inner.action) {
293                    return Err(MostroError::MostroInternalErr(
294                        ServiceError::UnexpectedError(
295                            "missing request_id on a response that requires one".to_string(),
296                        ),
297                    ));
298                }
299            }
300        }
301    }
302
303    Ok(())
304}
305
306/// Actions that may legitimately arrive without a `request_id` even when the
307/// caller was waiting on one (unsolicited server-initiated events).
308fn action_accepts_missing_request_id(action: &Action) -> bool {
309    matches!(
310        action,
311        Action::BuyerTookOrder
312            | Action::HoldInvoicePaymentAccepted
313            | Action::HoldInvoicePaymentSettled
314            | Action::HoldInvoicePaymentCanceled
315            | Action::WaitingSellerToPay
316            | Action::WaitingBuyerInvoice
317            | Action::BuyerInvoiceAccepted
318            | Action::PurchaseCompleted
319            | Action::Released
320            | Action::FiatSentOk
321            | Action::Canceled
322            | Action::CooperativeCancelInitiatedByPeer
323            | Action::CooperativeCancelAccepted
324            | Action::DisputeInitiatedByPeer
325            | Action::AdminSettled
326            | Action::AdminCanceled
327            | Action::AdminTookDispute
328            | Action::PaymentFailed
329            | Action::InvoiceUpdated
330            | Action::Rate
331            | Action::RateReceived
332            | Action::SendDm
333    )
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::message::{Action, MessageKind, Payload};
340    use uuid::uuid;
341
342    fn sample_order_message(request_id: Option<u64>) -> Message {
343        let peer = crate::message::Peer::new(
344            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
345            None,
346        );
347        Message::Order(MessageKind::new(
348            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
349            request_id,
350            Some(1),
351            Action::FiatSentOk,
352            Some(Payload::Peer(peer)),
353        ))
354    }
355
356    #[tokio::test]
357    async fn wrap_then_unwrap_roundtrip() {
358        let identity_keys = Keys::generate();
359        let trade_keys = Keys::generate();
360        let receiver_keys = Keys::generate();
361
362        let message = sample_order_message(Some(42));
363
364        let wrapped = wrap_message(
365            &message,
366            &identity_keys,
367            &trade_keys,
368            receiver_keys.public_key(),
369            WrapOptions::default(),
370        )
371        .await
372        .expect("wrap");
373
374        assert_eq!(wrapped.kind, Kind::GiftWrap);
375        assert!(wrapped
376            .tags
377            .iter()
378            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("p")));
379
380        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
381            .await
382            .expect("unwrap result")
383            .expect("unwrap some");
384
385        assert_eq!(unwrapped.sender, trade_keys.public_key());
386        assert_eq!(unwrapped.identity, identity_keys.public_key());
387        assert_eq!(
388            unwrapped.message.as_json().unwrap(),
389            message.as_json().unwrap()
390        );
391        assert!(unwrapped.signature.is_some());
392    }
393
394    #[tokio::test]
395    async fn full_privacy_mode_identity_equals_sender() {
396        // Caller that opts out of reputation passes trade_keys as identity.
397        let trade_keys = Keys::generate();
398        let receiver_keys = Keys::generate();
399
400        let wrapped = wrap_message(
401            &sample_order_message(Some(1)),
402            &trade_keys,
403            &trade_keys,
404            receiver_keys.public_key(),
405            WrapOptions::default(),
406        )
407        .await
408        .expect("wrap");
409
410        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
411            .await
412            .expect("unwrap")
413            .expect("some");
414
415        assert_eq!(unwrapped.sender, trade_keys.public_key());
416        assert_eq!(unwrapped.identity, trade_keys.public_key());
417    }
418
419    #[tokio::test]
420    async fn unwrap_with_corrupted_seal_returns_err() {
421        let receiver_keys = Keys::generate();
422        let ephemeral = Keys::generate();
423
424        // GiftWrap whose outer ciphertext decrypts successfully but yields
425        // a string that is not a valid seal Event JSON. Must surface as
426        // Err, not be silently absorbed as Ok(None).
427        let encrypted = nip44::encrypt(
428            ephemeral.secret_key(),
429            &receiver_keys.public_key(),
430            "not a seal",
431            nip44::Version::default(),
432        )
433        .expect("encrypt");
434
435        let corrupted = EventBuilder::new(Kind::GiftWrap, encrypted)
436            .tags([Tag::public_key(receiver_keys.public_key())])
437            .sign_with_keys(&ephemeral)
438            .expect("sign");
439
440        let result = unwrap_message(&corrupted, &receiver_keys).await;
441        assert!(
442            matches!(result, Err(MostroError::MostroInternalErr(_))),
443            "expected Err for corrupted gift wrap, got {result:?}",
444        );
445    }
446
447    // Build a GiftWrap by hand with a custom inner rumor tuple so tests
448    // can inject a malformed or wrong-signature payload that `wrap_message`
449    // would never emit.
450    async fn wrap_with_raw_inner(
451        identity_keys: &Keys,
452        trade_keys: &Keys,
453        receiver: PublicKey,
454        inner: (&Message, Option<String>),
455    ) -> Event {
456        let content = serde_json::to_string(&inner).unwrap();
457        let rumor = EventBuilder::text_note(content).build(trade_keys.public_key());
458        let seal = EventBuilder::seal(identity_keys, &receiver, rumor)
459            .await
460            .unwrap()
461            .sign(identity_keys)
462            .await
463            .unwrap();
464        gift_wrap_from_seal_with_pow(&seal, receiver, 0, None).unwrap()
465    }
466
467    #[tokio::test]
468    async fn unwrap_with_malformed_signature_errors() {
469        let identity_keys = Keys::generate();
470        let trade_keys = Keys::generate();
471        let receiver_keys = Keys::generate();
472        let msg = sample_order_message(Some(1));
473
474        let wrapped = wrap_with_raw_inner(
475            &identity_keys,
476            &trade_keys,
477            receiver_keys.public_key(),
478            (&msg, Some("not-a-hex-signature".to_string())),
479        )
480        .await;
481
482        let result = unwrap_message(&wrapped, &receiver_keys).await;
483        assert!(
484            matches!(result, Err(MostroError::MostroInternalErr(_))),
485            "malformed signature must surface as Err, got {result:?}",
486        );
487    }
488
489    #[tokio::test]
490    async fn unwrap_with_signature_for_other_content_errors() {
491        let identity_keys = Keys::generate();
492        let trade_keys = Keys::generate();
493        let receiver_keys = Keys::generate();
494        let msg = sample_order_message(Some(1));
495        // Well-formed signature, but over a completely different payload.
496        let bogus = Message::sign("not the real message".to_string(), &trade_keys);
497
498        let wrapped = wrap_with_raw_inner(
499            &identity_keys,
500            &trade_keys,
501            receiver_keys.public_key(),
502            (&msg, Some(bogus.to_string())),
503        )
504        .await;
505
506        let result = unwrap_message(&wrapped, &receiver_keys).await;
507        assert!(
508            matches!(result, Err(MostroError::MostroInternalErr(_))),
509            "non-verifying signature must surface as Err, got {result:?}",
510        );
511    }
512
513    #[tokio::test]
514    async fn unwrap_with_wrong_receiver_keys_returns_none() {
515        let identity_keys = Keys::generate();
516        let trade_keys = Keys::generate();
517        let receiver_keys = Keys::generate();
518        let stranger_keys = Keys::generate();
519
520        let wrapped = wrap_message(
521            &sample_order_message(Some(1)),
522            &identity_keys,
523            &trade_keys,
524            receiver_keys.public_key(),
525            WrapOptions::default(),
526        )
527        .await
528        .expect("wrap");
529
530        let result = unwrap_message(&wrapped, &stranger_keys)
531            .await
532            .expect("call should not error");
533        assert!(result.is_none());
534    }
535
536    #[tokio::test]
537    async fn signature_is_verifiable_with_trade_pubkey() {
538        let identity_keys = Keys::generate();
539        let trade_keys = Keys::generate();
540        let receiver_keys = Keys::generate();
541        let message = sample_order_message(Some(7));
542
543        let wrapped = wrap_message(
544            &message,
545            &identity_keys,
546            &trade_keys,
547            receiver_keys.public_key(),
548            WrapOptions::default(),
549        )
550        .await
551        .unwrap();
552
553        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
554            .await
555            .unwrap()
556            .unwrap();
557
558        let sig = unwrapped.signature.expect("signed");
559        let json = unwrapped.message.as_json().unwrap();
560        assert!(Message::verify_signature(
561            json,
562            trade_keys.public_key(),
563            sig
564        ));
565    }
566
567    #[tokio::test]
568    async fn unsigned_wrap_has_no_signature() {
569        let identity_keys = Keys::generate();
570        let trade_keys = Keys::generate();
571        let receiver_keys = Keys::generate();
572
573        let wrapped = wrap_message(
574            &sample_order_message(Some(3)),
575            &identity_keys,
576            &trade_keys,
577            receiver_keys.public_key(),
578            WrapOptions {
579                signed: false,
580                ..WrapOptions::default()
581            },
582        )
583        .await
584        .expect("wrap");
585
586        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
587            .await
588            .unwrap()
589            .unwrap();
590        assert!(unwrapped.signature.is_none());
591    }
592
593    #[tokio::test]
594    async fn expiration_tag_is_set_when_provided() {
595        let identity_keys = Keys::generate();
596        let trade_keys = Keys::generate();
597        let receiver_keys = Keys::generate();
598        let exp = Timestamp::from_secs(Timestamp::now().as_secs() + 3600);
599
600        let wrapped = wrap_message(
601            &sample_order_message(Some(1)),
602            &identity_keys,
603            &trade_keys,
604            receiver_keys.public_key(),
605            WrapOptions {
606                expiration: Some(exp),
607                ..WrapOptions::default()
608            },
609        )
610        .await
611        .expect("wrap");
612
613        let has_expiration = wrapped
614            .tags
615            .iter()
616            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("expiration"));
617        assert!(has_expiration);
618    }
619
620    #[test]
621    fn validate_response_cant_do_short_circuits() {
622        let msg = Message::cant_do(
623            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
624            Some(5),
625            Some(Payload::CantDo(Some(CantDoReason::NotAuthorized))),
626        );
627        let err = validate_response(&msg, Some(5)).unwrap_err();
628        match err {
629            MostroError::MostroCantDo(CantDoReason::NotAuthorized) => {}
630            _ => panic!("expected CantDo(NotAuthorized)"),
631        }
632    }
633
634    #[test]
635    fn validate_response_request_id_match() {
636        let msg = sample_order_message(Some(9));
637        validate_response(&msg, Some(9)).unwrap();
638    }
639
640    #[test]
641    fn validate_response_request_id_mismatch_errors() {
642        let msg = sample_order_message(Some(9));
643        let err = validate_response(&msg, Some(10)).unwrap_err();
644        assert!(matches!(err, MostroError::MostroInternalErr(_)));
645    }
646
647    #[test]
648    fn validate_response_allows_unsolicited_actions_without_request_id() {
649        let msg = Message::Order(MessageKind::new(
650            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
651            None,
652            None,
653            Action::BuyerTookOrder,
654            None,
655        ));
656        validate_response(&msg, Some(1)).unwrap();
657    }
658
659    #[test]
660    fn validate_response_with_no_expected_id_is_ok() {
661        let msg = sample_order_message(None);
662        validate_response(&msg, None).unwrap();
663    }
664}