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            | Action::BondSlashed
334    )
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::message::{Action, MessageKind, Payload};
341    use uuid::uuid;
342
343    fn sample_order_message(request_id: Option<u64>) -> Message {
344        let peer = crate::message::Peer::new(
345            "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
346            None,
347        );
348        Message::Order(MessageKind::new(
349            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
350            request_id,
351            Some(1),
352            Action::FiatSentOk,
353            Some(Payload::Peer(peer)),
354        ))
355    }
356
357    #[tokio::test]
358    async fn wrap_then_unwrap_roundtrip() {
359        let identity_keys = Keys::generate();
360        let trade_keys = Keys::generate();
361        let receiver_keys = Keys::generate();
362
363        let message = sample_order_message(Some(42));
364
365        let wrapped = wrap_message(
366            &message,
367            &identity_keys,
368            &trade_keys,
369            receiver_keys.public_key(),
370            WrapOptions::default(),
371        )
372        .await
373        .expect("wrap");
374
375        assert_eq!(wrapped.kind, Kind::GiftWrap);
376        assert!(wrapped
377            .tags
378            .iter()
379            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("p")));
380
381        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
382            .await
383            .expect("unwrap result")
384            .expect("unwrap some");
385
386        assert_eq!(unwrapped.sender, trade_keys.public_key());
387        assert_eq!(unwrapped.identity, identity_keys.public_key());
388        assert_eq!(
389            unwrapped.message.as_json().unwrap(),
390            message.as_json().unwrap()
391        );
392        assert!(unwrapped.signature.is_some());
393    }
394
395    #[tokio::test]
396    async fn full_privacy_mode_identity_equals_sender() {
397        // Caller that opts out of reputation passes trade_keys as identity.
398        let trade_keys = Keys::generate();
399        let receiver_keys = Keys::generate();
400
401        let wrapped = wrap_message(
402            &sample_order_message(Some(1)),
403            &trade_keys,
404            &trade_keys,
405            receiver_keys.public_key(),
406            WrapOptions::default(),
407        )
408        .await
409        .expect("wrap");
410
411        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
412            .await
413            .expect("unwrap")
414            .expect("some");
415
416        assert_eq!(unwrapped.sender, trade_keys.public_key());
417        assert_eq!(unwrapped.identity, trade_keys.public_key());
418    }
419
420    #[tokio::test]
421    async fn unwrap_with_corrupted_seal_returns_err() {
422        let receiver_keys = Keys::generate();
423        let ephemeral = Keys::generate();
424
425        // GiftWrap whose outer ciphertext decrypts successfully but yields
426        // a string that is not a valid seal Event JSON. Must surface as
427        // Err, not be silently absorbed as Ok(None).
428        let encrypted = nip44::encrypt(
429            ephemeral.secret_key(),
430            &receiver_keys.public_key(),
431            "not a seal",
432            nip44::Version::default(),
433        )
434        .expect("encrypt");
435
436        let corrupted = EventBuilder::new(Kind::GiftWrap, encrypted)
437            .tags([Tag::public_key(receiver_keys.public_key())])
438            .sign_with_keys(&ephemeral)
439            .expect("sign");
440
441        let result = unwrap_message(&corrupted, &receiver_keys).await;
442        assert!(
443            matches!(result, Err(MostroError::MostroInternalErr(_))),
444            "expected Err for corrupted gift wrap, got {result:?}",
445        );
446    }
447
448    // Build a GiftWrap by hand with a custom inner rumor tuple so tests
449    // can inject a malformed or wrong-signature payload that `wrap_message`
450    // would never emit.
451    async fn wrap_with_raw_inner(
452        identity_keys: &Keys,
453        trade_keys: &Keys,
454        receiver: PublicKey,
455        inner: (&Message, Option<String>),
456    ) -> Event {
457        let content = serde_json::to_string(&inner).unwrap();
458        let rumor = EventBuilder::text_note(content).build(trade_keys.public_key());
459        let seal = EventBuilder::seal(identity_keys, &receiver, rumor)
460            .await
461            .unwrap()
462            .sign(identity_keys)
463            .await
464            .unwrap();
465        gift_wrap_from_seal_with_pow(&seal, receiver, 0, None).unwrap()
466    }
467
468    #[tokio::test]
469    async fn unwrap_with_malformed_signature_errors() {
470        let identity_keys = Keys::generate();
471        let trade_keys = Keys::generate();
472        let receiver_keys = Keys::generate();
473        let msg = sample_order_message(Some(1));
474
475        let wrapped = wrap_with_raw_inner(
476            &identity_keys,
477            &trade_keys,
478            receiver_keys.public_key(),
479            (&msg, Some("not-a-hex-signature".to_string())),
480        )
481        .await;
482
483        let result = unwrap_message(&wrapped, &receiver_keys).await;
484        assert!(
485            matches!(result, Err(MostroError::MostroInternalErr(_))),
486            "malformed signature must surface as Err, got {result:?}",
487        );
488    }
489
490    #[tokio::test]
491    async fn unwrap_with_signature_for_other_content_errors() {
492        let identity_keys = Keys::generate();
493        let trade_keys = Keys::generate();
494        let receiver_keys = Keys::generate();
495        let msg = sample_order_message(Some(1));
496        // Well-formed signature, but over a completely different payload.
497        let bogus = Message::sign("not the real message".to_string(), &trade_keys);
498
499        let wrapped = wrap_with_raw_inner(
500            &identity_keys,
501            &trade_keys,
502            receiver_keys.public_key(),
503            (&msg, Some(bogus.to_string())),
504        )
505        .await;
506
507        let result = unwrap_message(&wrapped, &receiver_keys).await;
508        assert!(
509            matches!(result, Err(MostroError::MostroInternalErr(_))),
510            "non-verifying signature must surface as Err, got {result:?}",
511        );
512    }
513
514    #[tokio::test]
515    async fn unwrap_with_wrong_receiver_keys_returns_none() {
516        let identity_keys = Keys::generate();
517        let trade_keys = Keys::generate();
518        let receiver_keys = Keys::generate();
519        let stranger_keys = Keys::generate();
520
521        let wrapped = wrap_message(
522            &sample_order_message(Some(1)),
523            &identity_keys,
524            &trade_keys,
525            receiver_keys.public_key(),
526            WrapOptions::default(),
527        )
528        .await
529        .expect("wrap");
530
531        let result = unwrap_message(&wrapped, &stranger_keys)
532            .await
533            .expect("call should not error");
534        assert!(result.is_none());
535    }
536
537    #[tokio::test]
538    async fn signature_is_verifiable_with_trade_pubkey() {
539        let identity_keys = Keys::generate();
540        let trade_keys = Keys::generate();
541        let receiver_keys = Keys::generate();
542        let message = sample_order_message(Some(7));
543
544        let wrapped = wrap_message(
545            &message,
546            &identity_keys,
547            &trade_keys,
548            receiver_keys.public_key(),
549            WrapOptions::default(),
550        )
551        .await
552        .unwrap();
553
554        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
555            .await
556            .unwrap()
557            .unwrap();
558
559        let sig = unwrapped.signature.expect("signed");
560        let json = unwrapped.message.as_json().unwrap();
561        assert!(Message::verify_signature(
562            json,
563            trade_keys.public_key(),
564            sig
565        ));
566    }
567
568    #[tokio::test]
569    async fn unsigned_wrap_has_no_signature() {
570        let identity_keys = Keys::generate();
571        let trade_keys = Keys::generate();
572        let receiver_keys = Keys::generate();
573
574        let wrapped = wrap_message(
575            &sample_order_message(Some(3)),
576            &identity_keys,
577            &trade_keys,
578            receiver_keys.public_key(),
579            WrapOptions {
580                signed: false,
581                ..WrapOptions::default()
582            },
583        )
584        .await
585        .expect("wrap");
586
587        let unwrapped = unwrap_message(&wrapped, &receiver_keys)
588            .await
589            .unwrap()
590            .unwrap();
591        assert!(unwrapped.signature.is_none());
592    }
593
594    #[tokio::test]
595    async fn expiration_tag_is_set_when_provided() {
596        let identity_keys = Keys::generate();
597        let trade_keys = Keys::generate();
598        let receiver_keys = Keys::generate();
599        let exp = Timestamp::from_secs(Timestamp::now().as_secs() + 3600);
600
601        let wrapped = wrap_message(
602            &sample_order_message(Some(1)),
603            &identity_keys,
604            &trade_keys,
605            receiver_keys.public_key(),
606            WrapOptions {
607                expiration: Some(exp),
608                ..WrapOptions::default()
609            },
610        )
611        .await
612        .expect("wrap");
613
614        let has_expiration = wrapped
615            .tags
616            .iter()
617            .any(|t| t.as_slice().first().map(|s| s.as_str()) == Some("expiration"));
618        assert!(has_expiration);
619    }
620
621    #[test]
622    fn validate_response_cant_do_short_circuits() {
623        let msg = Message::cant_do(
624            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
625            Some(5),
626            Some(Payload::CantDo(Some(CantDoReason::NotAuthorized))),
627        );
628        let err = validate_response(&msg, Some(5)).unwrap_err();
629        match err {
630            MostroError::MostroCantDo(CantDoReason::NotAuthorized) => {}
631            _ => panic!("expected CantDo(NotAuthorized)"),
632        }
633    }
634
635    #[test]
636    fn validate_response_request_id_match() {
637        let msg = sample_order_message(Some(9));
638        validate_response(&msg, Some(9)).unwrap();
639    }
640
641    #[test]
642    fn validate_response_request_id_mismatch_errors() {
643        let msg = sample_order_message(Some(9));
644        let err = validate_response(&msg, Some(10)).unwrap_err();
645        assert!(matches!(err, MostroError::MostroInternalErr(_)));
646    }
647
648    #[test]
649    fn validate_response_allows_unsolicited_actions_without_request_id() {
650        let msg = Message::Order(MessageKind::new(
651            Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
652            None,
653            None,
654            Action::BuyerTookOrder,
655            None,
656        ));
657        validate_response(&msg, Some(1)).unwrap();
658    }
659
660    #[test]
661    fn validate_response_with_no_expected_id_is_ok() {
662        let msg = sample_order_message(None);
663        validate_response(&msg, None).unwrap();
664    }
665}