Skip to content

Security: SIP-01/SIP-Booster-Relay

Security

docs/SECURITY.md

Security model

This relay is public Internet infrastructure. It assumes every client is hostile until proven otherwise.

Ingestion hardening

  1. Message size cap before JSON parsing (256 KB default; close 1009).
  2. Field shape checks before any cryptography: lowercase-hex id, pubkey, sig; integer kind (0–65535) and created_at; tag count; content length.
  3. Event id = sha256(serialization) verified, then Schnorr signature verified (@noble/curves, constant-time primitives).
  4. created_at future limit (default +900 s; NIP-01 SHOULD).
  5. SIP-01 schema validation for kind 39697 (spec §12.4): URL allowlist (http/https only — javascript:, data:, file: rejected), d↔u and x↔content hash consistency, hard length caps, extension tag shapes.
  6. Policy engine: pubkey/kind/tag allow+block lists, blocked content phrases, optional NIP-05 gate, optional anti-spam content hashing.
  7. Rate limits: per-connection token buckets — writes (10/min), reads (50/min), and a roomier kind-39697 indexer bucket (120/min, burst 240).
  8. Query cost controls: complexity scoring, COUNT precheck against tag queries (>10k estimated rows refused), global per-REQ event cap, chunked execution of huge filter arrays, search length/result caps, COUNT complexity guard, NEG item cap (NEG-ERR blocked: beyond).

Payment verification (pay-to-relay mode)

Upstream's payment endpoint marked any caller-supplied ?npub= as paid — anyone could self-grant access. This fork only records payment from a cryptographically valid kind 9735 zap receipt:

  • valid Schnorr signature;
  • p tag = the operator's pubkey (decoded from the configured npub);
  • P tag = the payer receiving access;
  • amount (msats) ≥ configured price and a bolt11 invoice present;
  • receipt ≤ 30 days old.

Trust model: a zap receipt is issued and signed by the operator's own LNURL server after invoice settlement, so trusting it reduces to trusting the operator's Lightning infrastructure — the correct trust root for a relay policy decision. A receipt can be replayed, but only to grant access to the same payer it already paid for. Invoice-level double-checks against the LNURL verify endpoint can be layered on later without changing the module interface (outbound calls are deliberately not made by default — see SSRF).

Outbound requests (SSRF posture)

The relay never fetches URLs from events or search queries. The only outbound fetches are:

  • https://<domain>/.well-known/nostr.json for the optional NIP-05 gate (off by default; domains come from publishing clients' kind-0 metadata — enable only with allow-listed domains if you turn this on), and
  • the favicon proxy to the configured relayInfo.icon URL (static config).

Server-side crawling is out of scope for the relay itself — crawlers (Crawlstr/Indexstr) fetch URLs and apply their own SSRF protections; the relay only stores their signed observations.

WebSocket abuse protection

  • Hibernating Durable Objects with idle eviction alarms.
  • Max 100 subscriptions per connection; max 20 filters per REQ; sub-id ≤ 64.
  • NEG sessions: in-memory, one per sub-id, 10-minute idle reclamation, replaced on repeated NEG-OPEN (per NIP-77).
  • DO broadcast mesh validates event ids against a dedupe map before fanout.

Data safety

  • All SQL is parameterized (bound values); LIKE patterns are escaped with ESCAPE '\'; tag names come from a whitelist for cache-column lookups.
  • Deletion requires ownership (e tag targets must share the deletion's pubkey; a tag targets must match kind+pubkey).
  • D1 sessions (withSession bookmarks) give read-after-write consistency without trusting stale replicas.

Secrets

  • Never in source: Cloudflare API tokens, account IDs, Nostr private keys, Lightning node keys. Use wrangler secret for runtime secrets.
  • The relay needs no private key at all — it serves and validates; it does not sign.

Hosted deploy service (/api/service/*)

Optional product surface. Isolation (default): the relay ships with DEPLOY_SERVICE_ENABLED = false — a public relay should never handle customer Cloudflare credentials. Run the service as its own Worker: wrangler deploy --config wrangler.service.toml (entry src/service-worker.ts, its own D1 SERVICE_DATABASE, no WebSocket/relay surface at all). The shared modules make the two deploys one codebase, two security domains.

  • Config validation (P0-2): a zero-address PRE wallet or an undecodable zap npub disables that payment method; if no method is usable, payment and deployment calls fail with 503 naming the problem. Never silently accepts money into a misconfiguration.

  • Bundle pinning (P0-3): customer deployments fetch the CI-built worker.js at an immutable commit (DEPLOY_BUNDLE_REF), never a moving branch. Set DEPLOY_BUNDLE_SHA256 to also enforce the bundle's hash before provisioning (recommended for production).

  • Dual-RPC PRE verification (P1): every Base RPC in BASE_RPC_URLS is queried and at least BASE_RPC_QUORUM must return byte-identical receipts (status + logs) before a PRE payment counts. A single lying RPC cannot fake a payment.

  • Deployment idempotency (P1): each paid deploy is a deploy_jobs row (provisioning → deployed/failed with the step log); failures refund the payment credit, and Cloudflare calls are idempotent (D1 create-or-find, worker PUT overwrite, subdomain enable).

  • Federation budgets (P1): NEG-OPEN has a per-IP rate (NEG_OPEN_PER_IP_PER_MIN) and a per-DO concurrent session cap (NEG_MAX_CONCURRENT_SESSIONS) on top of the global item/frame/session limits — negentropy is CPU + D1 work, not just bandwidth.

  • Customer Cloudflare tokens arrive over TLS at POST /api/service/deploy, are used in memory for provisioning calls against api.cloudflare.com, and are never logged, stored, cached, or returned. The UI tells customers exactly which scoped permissions to grant (Workers Scripts Edit + D1 Edit) and to delete the token afterwards. The endpoint is rate-limited per IP per day and gated behind a verified payment credit.

  • Admin auth is NIP-98: every admin call is a fresh kind 27235 signed event bound to the exact URL, method, and payload hash, with a 5-minute freshness window, owner-pubkey-gated. No sessions or cookies to steal.

  • Lightning payments are verified from kind 9735 zap receipts (same trust model as pay-to-relay) and are single-use per receipt id.

  • PRE payments are verified on-chain via the public Base RPC (https://mainnet.base.org): confirmed receipt + a Transfer log on the PRE contract (0x3816dd4b…de7a) to the service wallet with value ≥ price. Tx hashes are single-use. Trust note: the RPC response is trusted as Base consensus — an RPC lie could fake a payment; for mainnet-value use, pin a reputable RPC (or two and cross-check) via BASE_RPC_URL.

  • The service deploys a stock-config relay (current worker.js from the repository) into the customer's account. Customers can reconfigure after deploy with wrangler.

Reporting

Open a GitHub issue (or contact the operator address in the NIP-11 document). Please include reproduction details; do not include secrets.

There aren't any published security advisories