mirror of
https://github.com/logos-co/lambda-prize.git
synced 2026-08-31 03:21:07 +00:00
88 lines
6.0 KiB
Markdown
88 lines
6.0 KiB
Markdown
# 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
|
|
|
|
- **Repo:** https://github.com/mmlado/nip46-keycard
|
|
|
|
## 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`](https://github.com/status-im/status-keycard/blob/ff4bf8ac2706c34c9256a637ebc24ab0fbe4e070/src/main/java/im/status/keycard/KeycardApplet.java#L1130-L1194); 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
|
|
|
|
- [x] **`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.
|
|
- [x] **WebSocket relay connection management** — connects to multiple relays, reconnects with exponential backoff, re-subscribes after reconnect.
|
|
- [x] **Basic event approval policy** — configurable `always` / `policy` modes; `policy` mode auto-approves by event kind before invoking the Keycard.
|
|
- [x] **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.
|
|
- [x] **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](https://github.com/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](../TERMS.md).
|