Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions rust/src/api/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,11 @@ pub(crate) async fn subscribe_gift_wraps(trade_pubkey: nostr_sdk::PublicKey, tra

/// Dispatch a Mostro `Message` recovered from a gift-wrap.
///
/// Authenticates the sender against the active Mostro pubkey, runs the
/// The caller recovers the `UnwrappedMessage` via
/// `crate::nostr::gift_wrap::unwrap_mostro_message`, which verifies the seal
/// signature so the `identity` field is cryptographically attributable.
/// This function authenticates that seal signer's `identity` against the
/// active Mostro pubkey (never the self-asserted rumor `sender`), runs the
/// centralized `validate_response` check (catches `CantDo` responses and
/// malformed `request_id` fields), then routes by action.
async fn dispatch_mostro_message(
Expand All @@ -1058,29 +1062,36 @@ async fn dispatch_mostro_message(
) {
use mostro_core::message::Action;

// mostro-core 0.10 adds `identity` (seal signer, long-lived) alongside
// `sender` (rumor author, per-trade). A Mostro node that opts out of the
// identity/trade split — the current daemon — reuses the same key for
// both, so `identity == sender`. We keep the existing sender-based
// authentication check below; the extra `identity` field is deliberately
// ignored here.
// mostro-core 0.10 splits the wrap into two pubkeys:
//
// * `identity` — seal signer, proven by `seal.verify_signature()` in
// `unwrap_message`. This is the only field a forger cannot spoof.
// * `sender` — rumor author. Self-asserted unless an inner
// `signature` is present and verifies against the rumor pubkey.
//
// In Mostro's reputation-mode key split `sender` (per-trade key) and
// `identity` (long-lived seal key) are expected to differ, and protocol
// responses commonly omit the inner signature, so we cannot use a
// sender/identity equality check to gate dispatch. Authenticate against
// `identity` only — a forger who seals with their own key cannot make
// it match the configured Mostro pubkey.
let mostro_core::nip59::UnwrappedMessage {
message: msg,
sender,
identity: _,
sender: _,
identity,
signature: _,
created_at: _,
} = unwrapped;

// Daemon authentication: the active Mostro pubkey is the only legitimate
// sender for protocol responses. Reject anything else loudly — previously
// we trusted whatever decrypted under our trade key.
// Daemon authentication: the seal signer must be the active Mostro
// pubkey. The seal signature is verified inside `unwrap_message`, so
// `identity` is the cryptographically authoritative origin.
match nostr_sdk::PublicKey::from_hex(&crate::config::active_mostro_pubkey()) {
Ok(expected) if expected == sender => {}
Ok(expected) if expected == identity => {}
Ok(expected) => {
crate::api::logging::blog_warn("gift-wrap", format!(
"rejecting gift-wrap: sender={} != active mostro={} (trade={})",
&sender.to_hex()[..8],
"rejecting gift-wrap: identity={} != active mostro={} (trade={})",
&identity.to_hex()[..8],
&expected.to_hex()[..8],
&trade_pubkey_hex[..8],
));
Expand Down
12 changes: 7 additions & 5 deletions rust/src/nostr/gift_wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ pub async fn wrap_mostro_message(
/// differ. The returned `UnwrappedMessage` exposes both (`identity` vs.
/// `sender`) so callers can route and authorize accordingly.
///
/// Daemon authentication — verifying that `UnwrappedMessage.sender` equals
/// the active Mostro pubkey — is NOT performed here. The seal author is
/// proven via the inner signature but could in principle be any key; the
/// upstream dispatcher in `api/orders.rs` enforces the Mostro-pubkey check
/// before routing the message.
/// Daemon authentication is NOT performed here. The seal signature is
/// verified (so `identity` is cryptographically attributable) but the seal
/// signer could in principle be any key, and the rumor pubkey (`sender`)
/// is only meaningful when an inner signature accompanies it. The upstream
/// dispatcher in `api/orders.rs` enforces the Mostro-pubkey check against
/// `identity` — never against the unauthenticated `sender` — before
/// routing the message.
pub async fn unwrap_mostro_message(
trade_keys: &Keys,
event: &Event,
Expand Down