Files
lambda-prize/solutions/LP-0009.md

6.0 KiB

Solution: LP-0009 — Keycard NIP-46 Nostr Signer Proxy

Submitted by: mmlado

Summary

A NIP-46 remote signer daemon written in Nim that bridges Nostr signing requests to a Status Keycard hardware wallet over USB. The daemon handles connect, get_public_key, and sign_event, manages WebSocket relay connections with automatic reconnect, and applies a configurable event approval policy. Tested end-to-end against noStrudel.

Repository

Approach

The daemon runs as a CLI process. On startup it:

  1. Loads or generates a signer keypair (used for NIP-46 relay communication).
  2. Attempts to connect to a Keycard via PC/SC. If found, it pairs (once), opens an encrypted secure channel, and verifies the PIN. The Keycard's key becomes the user identity.
  3. Subscribes to configured Nostr relays, listening for kind:24133 events addressed to the signer pubkey.

For each incoming request:

  • Content is decrypted with NIP-44 v2.
  • The JSON-RPC method is dispatched to the appropriate handler.
  • For sign_event, the approval policy is checked first. If approved, signing proceeds via Keycard.
  • The response is NIP-44 encrypted and published back as a kind:24133 event.

Keycard signing strategy:

The Keycard applet (v3.x) ignores the P2 algorithm byte on the SIGN command and always executes ALG_ECDSA_SHA_256 — BIP-340 Schnorr is not implemented in firmware (confirmed by inspecting KeycardApplet.java; the P2=0x03 byte is silently discarded and ECDSA is used regardless, returning SW_SUCCESS).

The daemon exports the private key over the encrypted secure channel (the EIP-1581 subtree — path m/43'/60'/1581'/0'/0 — is the only subtree the Keycard applet permits private key export for), performs BIP-340 Schnorr in software, and immediately wipes the key from memory.

This means the private key is only accessible when the physical card is present and the PIN is verified. When the applet gains native Schnorr support, switching to on-card signing requires changing one call in keycard.nim.

NIP-44 v2 is implemented from scratch in Nim and tested against the official spec vectors (conversation key derivation, message key derivation, padding, encrypt/decrypt round-trips, and all invalid cases).

Relay management uses exponential backoff reconnect (3 s → 30 s cap) and re-subscribes automatically after each reconnect.

Approval policy supports two modes: always (approve everything) and policy (auto-approve configured event kinds).

Success Criteria Checklist

  • connect, get_public_key, sign_event handled — all three methods are implemented and tested end-to-end with noStrudel. sign_event goes through the Keycard with the fallback described above.
  • WebSocket relay connection management — connects to multiple relays, reconnects with exponential backoff, re-subscribes after reconnect.
  • Basic event approval policy — configurable always / policy modes; policy mode auto-approves by event kind before invoking the Keycard.
  • Tested end-to-end against a NIP-46-compatible client — tested against noStrudel: connect, get_public_key, and sign_event (publishing a kind:1 note) all work.
  • Documentation and clean public repository under Apache-2.0 — README covers installation, Keycard setup, relay configuration, client integration, and known limitations.

FURPS Self-Assessment

Functionality

Supported NIP-46 methods: connect, get_public_key, sign_event, ping. The daemon correctly routes all three required methods and handles the NIP-44 v2 encryption layer. Keycard integration provides hardware-backed key protection: the private key is only accessible when the physical card is present and the PIN is verified. Graceful software fallback when no card is detected.

Not supported (out of scope per prize spec): nip04_encrypt/decrypt, nip44_encrypt/decrypt, NFC transport.

Usability

Single binary, single TOML config file. On first run the signer keypair is generated automatically. Pairing with the Keycard happens once and is stored alongside the key file. The bunker:// URL is printed to stdout on startup — copy and paste into any NIP-46 client.

Reliability

  • Relay reconnect with exponential backoff (3 s → 30 s). Re-subscribes automatically after each reconnect.
  • Decrypt and JSON parse errors are caught and logged; the daemon continues running.
  • Graceful fallback to software signing if the Keycard is not present.
  • NIP-44 MAC verification is constant-time.

Performance

Signing latency is dominated by the PC/SC round-trip to the card (typically < 200 ms). NIP-44 encrypt/decrypt and SHA-256 event ID computation are negligible. The daemon is single-threaded and async; it handles one relay at a time but can connect to multiple relays concurrently.

Supportability

  • 30 unit tests across three suites: NIP-44 v2 (spec vectors), NIP-01 (event ID and signing), NIP-46 (dispatch and approval policy). Run with nimble test.
  • Structured logging at DEBUG/INFO/WARN levels throughout.
  • Code is split into focused modules: nip44, nip01, nip46, keycard, relay, config, keypair.
  • Keycard integration point is clearly marked — swapping in native Schnorr requires changing one call in keycard.nim.

Supporting Materials

  • End-to-end demo: noStrudel connecting via bunker://, posting a kind:1 note — connect, get_public_key, and sign_event all handled successfully.
  • NIP-44 test vectors: official vectors from paulmillr/nip44.
  • Keycard applet source (KeycardApplet.java) inspected to confirm Schnorr is not implemented — P2 algorithm byte is ignored, ECDSA is always used.

Terms & Conditions

By submitting this solution, I confirm that I have read and agree to the Terms & Conditions.