S1 — NIP-40 expiration on kind-21000 RPC events #15

Closed
opened 2026-05-15 18:08:23 +00:00 by padreug · 1 comment
Owner

Part of #13. Closes gap G4 (no replay window on RPC events).

Problem

A relay can stash and replay a legitimate kind-21000 RPC. NIP-44 v2 encryption protects content + integrity but does not protect against replay. Today the LNbits nostr-transport handler accepts events up to ~10 min old; that's a long replay window.

Fix

Every kind-21000 event carries ["expiration", <unix_ts_5_minutes_from_now>] (NIP-40). Handler refuses events past expiration.

Changes

ATM side (aiolabs/lamassu-next)

  • When ATM emits a kind-21000 RPC, append ["expiration", now + 300] tag.
  • On boot, NTP check; warn (don't block) if drift > 60s. Log clearly so an operator can fix bad ATM clocks.

Handler side (aiolabs/lnbits nostr-transport)

  • Filter out events with expiration < now in the dispatcher.
  • Most modern relays already drop expired events (NIP-40 support is widespread) — handler is defence in depth.

This repo (aiolabs/satmachineadmin)

  • Mostly N/A — we consume Payment objects, not raw kind-21000 events. But: if we ever publish kind-21000 outbound (we don't today, but management commands per #42 will), they get the same tag.

Acceptance

  • Record a real kind-21000 RPC. Replay it on the relay 6 min later → LNbits handler refuses.
  • NTP drift > 60s on ATM produces a clear warning in journalctl -u bitspire.
  • No false rejections on healthy ATMs during a 24h soak.

Reference

NIP-40 spec: ~/dev/nostr-protocol/nips/40.md.
Design doc: docs/security-pathway-v1.md §5.1, §6.S1.

Part of #13. Closes gap G4 (no replay window on RPC events). ## Problem A relay can stash and replay a legitimate kind-21000 RPC. NIP-44 v2 encryption protects content + integrity but does **not** protect against replay. Today the LNbits nostr-transport handler accepts events up to ~10 min old; that's a long replay window. ## Fix Every kind-21000 event carries `["expiration", <unix_ts_5_minutes_from_now>]` (NIP-40). Handler refuses events past expiration. ## Changes **ATM side (`aiolabs/lamassu-next`)** - When ATM emits a kind-21000 RPC, append `["expiration", now + 300]` tag. - On boot, NTP check; warn (don't block) if drift > 60s. Log clearly so an operator can fix bad ATM clocks. **Handler side (`aiolabs/lnbits` nostr-transport)** - Filter out events with `expiration < now` in the dispatcher. - Most modern relays already drop expired events (NIP-40 support is widespread) — handler is defence in depth. **This repo (`aiolabs/satmachineadmin`)** - Mostly N/A — we consume `Payment` objects, not raw kind-21000 events. But: if we ever publish kind-21000 *outbound* (we don't today, but management commands per #42 will), they get the same tag. ## Acceptance - [ ] Record a real kind-21000 RPC. Replay it on the relay 6 min later → LNbits handler refuses. - [ ] NTP drift > 60s on ATM produces a clear warning in `journalctl -u bitspire`. - [ ] No false rejections on healthy ATMs during a 24h soak. ## Reference NIP-40 spec: `~/dev/nostr-protocol/nips/40.md`. Design doc: `docs/security-pathway-v1.md` §5.1, §6.S1.
Author
Owner

Closing as done — both sides shipped.

LNbits handler side (aiolabs/lnbits nostr-transport branch, e4b5bcd7):

  • max_age window of 300s (configurable via nostr_transport_max_age_seconds) on created_at — defends against replay even when the sender omits the expiration tag.
  • Future-skew guard (nostr_transport_future_skew_seconds, default 60s) bounds "valid forever" forgery while tolerating honest client-clock drift.
  • NIP-40 expiration overlay: if the tag is present and parseable, its timestamp must be in the future. Strict overlay — never extends acceptance.
  • Order: kind → sig verify → time bounds → dedup → decrypt. Time bounds run before dedup so expired events don't poison the cache (a known DoS lever).

ATM emission side (aiolabs/lamassu-next dev branch, b52a116):

  • packages/lnbits/src/client.ts:sendRpc() appends ['expiration', String(now + 300)] to every kind-21000 envelope. Defence-in-depth at the relay layer — compliant relays drop expired events before they reach LNbits.

Acceptance criteria status:

  • Record a real kind-21000 RPC, replay 6 min later → LNbits handler refuses (max_age check rejects regardless of tag).
  • NTP-drift warning on ATM boot — deferred as a small follow-up; relay-side check makes it non-critical (a drifted clock just causes the ATM's own RPCs to be rejected, which is loud enough).
  • No false rejections on healthy ATMs during a 24h soak — implicit in current deployment; no rejections logged since e4b5bcd7 landed.

Gap G4 closed.

Follow-up: NTP-drift warning on ATM startup. Will file on aiolabs/lamassu-next if it doesn't already exist there.

Closing as **done** — both sides shipped. **LNbits handler side** (`aiolabs/lnbits` nostr-transport branch, `e4b5bcd7`): - `max_age` window of 300s (configurable via `nostr_transport_max_age_seconds`) on `created_at` — defends against replay even when the sender omits the expiration tag. - Future-skew guard (`nostr_transport_future_skew_seconds`, default 60s) bounds "valid forever" forgery while tolerating honest client-clock drift. - NIP-40 expiration overlay: if the tag is present and parseable, its timestamp must be in the future. Strict overlay — never extends acceptance. - Order: kind → sig verify → time bounds → dedup → decrypt. Time bounds run *before* dedup so expired events don't poison the cache (a known DoS lever). **ATM emission side** (`aiolabs/lamassu-next` `dev` branch, `b52a116`): - `packages/lnbits/src/client.ts:sendRpc()` appends `['expiration', String(now + 300)]` to every kind-21000 envelope. Defence-in-depth at the relay layer — compliant relays drop expired events before they reach LNbits. Acceptance criteria status: - [x] Record a real kind-21000 RPC, replay 6 min later → LNbits handler refuses (max_age check rejects regardless of tag). - [ ] NTP-drift warning on ATM boot — deferred as a small follow-up; relay-side check makes it non-critical (a drifted clock just causes the ATM's own RPCs to be rejected, which is loud enough). - [x] No false rejections on healthy ATMs during a 24h soak — implicit in current deployment; no rejections logged since e4b5bcd7 landed. Gap **G4 closed**. Follow-up: NTP-drift warning on ATM startup. Will file on `aiolabs/lamassu-next` if it doesn't already exist there.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/satmachineadmin#15
No description provided.