security: verify Amber signatures + pin MCP server versions

Closes two items from the platform/completeness audit appendices.

Amber signature verification (audit "open verification item", LOW):
reconstructSignedEvent() previously trusted whatever `sig` the external
Amber signer app returned — it recomputed the event id but never
verified the Schnorr signature. A compromised or buggy signer could
therefore return a forged/garbage signature that the app would treat as
valid and publish. Now verifyEvent() is run on the reconstructed event
before it is returned; failures return null, which every caller already
handles as an "incomplete signature" error. This single choke point
covers all Amber paths: the native AmberIntent signer, AmberCallbackPage,
and AmberCallbackHandler.

F9 (LOW): pin MCP server versions in .mcp.json. Both js-dev-mcp and
nostrbook/mcp were fetched at @latest, silently pulling the newest
release into the agent tool environment every session. Pinned to the
current exact versions (0.6.3 / 0.5.3) so upgrades are deliberate.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-24 19:20:47 +07:00
parent 358708ee27
commit 8eae0a69e4
2 changed files with 20 additions and 4 deletions

View file

@ -3,12 +3,12 @@
"js-dev": { "js-dev": {
"type": "stdio", "type": "stdio",
"command": "npx", "command": "npx",
"args": ["-y", "@soapbox.pub/js-dev-mcp@latest"] "args": ["-y", "@soapbox.pub/js-dev-mcp@0.6.3"]
}, },
"nostr": { "nostr": {
"type": "stdio", "type": "stdio",
"command": "npx", "command": "npx",
"args": ["-y", "@nostrbook/mcp@latest"] "args": ["-y", "@nostrbook/mcp@0.5.3"]
} }
} }
} }

View file

@ -1,5 +1,5 @@
import type { NostrEvent, NostrSigner } from '@nostrify/nostrify'; import type { NostrEvent, NostrSigner } from '@nostrify/nostrify';
import { getEventHash } from 'nostr-tools'; import { getEventHash, verifyEvent } from 'nostr-tools';
/** /**
* Pending Amber operation stored in multiple places for PWA resilience. * Pending Amber operation stored in multiple places for PWA resilience.
@ -257,7 +257,23 @@ export function reconstructSignedEvent(
const id = const id =
parsed?.id && parsed.id.length === 64 ? parsed.id : getEventHash(base); parsed?.id && parsed.id.length === 64 ? parsed.id : getEventHash(base);
return { ...base, id, sig }; const signed: NostrEvent = { ...base, id, sig };
// DEFENSE-IN-DEPTH: the external signer (Amber) is a separate app and is not
// implicitly trusted. Before handing this event back to the app to be
// published or trusted, prove it is genuinely signed by `eventPubkey`.
// `verifyEvent` re-checks both the id (hash of the serialized event) and the
// Schnorr signature, so a compromised or buggy signer that returns a forged
// or mismatched signature cannot produce a publishable event — we return null
// (all callers treat null as "incomplete signature" and surface a friendly
// "approve the request in Amber, then try again" error).
try {
if (!verifyEvent(signed)) return null;
} catch {
return null;
}
return signed;
} }
/** /**