13 KiB
NIP-46 Remote Signing — Implementation Plan
Overview
NIP-46 defines a protocol for Nostr Remote Signing, enabling 2-way communication between a client application and a remote signer (bunker) over Nostr relays. The remote signer holds the user's private keys and performs cryptographic operations on behalf of the client, reducing the attack surface by keeping keys off the client device.
This plan covers implementing both sides of the protocol:
- Client-side: sends requests to a remote signer and processes responses
- Remote-signer-side: receives requests and produces responses (for building bunker applications)
Architecture
Protocol Flow
sequenceDiagram
participant C as Client
participant R as Relay
participant S as Remote Signer
Note over C: Generate client-keypair
C->>R: Subscribe to kind:24133 p-tagged to client-pubkey
C->>R: Publish connect request, kind:24133, p-tag remote-signer-pubkey
R->>S: Deliver connect request
S->>R: Publish connect response, kind:24133, p-tag client-pubkey
R->>C: Deliver connect response
Note over C: Connection established
C->>R: Publish get_public_key request
R->>S: Deliver request
S->>R: Publish response with user-pubkey
R->>C: Deliver response
Note over C: Now knows user-pubkey
C->>R: Publish sign_event request
R->>S: Deliver request
S->>R: Publish signed event response
R->>C: Deliver signed event
Connection Initiation
flowchart TD
A[Connection Initiation] --> B{Who initiates?}
B -->|Remote Signer| C[bunker:// URL]
B -->|Client| D[nostrconnect:// URL]
C --> E[Client parses bunker URL]
E --> F[Extract remote-signer-pubkey + relays + secret]
F --> G[Client sends connect request to remote-signer]
D --> H[Remote signer parses nostrconnect URL]
H --> I[Extract client-pubkey + relays + secret + perms]
I --> J[Remote signer sends connect response to client]
G --> K[Connection Established]
J --> K
Component Dependency Map
graph TD
NIP46[nip046.c/h] --> NIP44[nip044 - NIP-44 Encryption]
NIP46 --> NIP04[nip004 - NIP-04 Encryption]
NIP46 --> NIP01[nip001 - Event Creation/Signing]
NIP46 --> POOL[core_relay_pool - Relay Communication]
NIP46 --> UTILS[utils - Hex/Bytes/Random]
NIP46 --> CJSON[cJSON - JSON Handling]
NIP46 --> COMMON[nostr_common - Error Codes/Constants]
Data Structures
Core Types
// NIP-46 event kind
#define NOSTR_NIP46_REQUEST_KIND 24133
// Connection types
typedef enum {
NOSTR_NIP46_CONN_BUNKER, // bunker:// initiated by remote-signer
NOSTR_NIP46_CONN_NOSTRCONNECT // nostrconnect:// initiated by client
} nostr_nip46_connection_type_t;
// RPC Method types
typedef enum {
NOSTR_NIP46_METHOD_CONNECT,
NOSTR_NIP46_METHOD_SIGN_EVENT,
NOSTR_NIP46_METHOD_PING,
NOSTR_NIP46_METHOD_GET_PUBLIC_KEY,
NOSTR_NIP46_METHOD_NIP04_ENCRYPT,
NOSTR_NIP46_METHOD_NIP04_DECRYPT,
NOSTR_NIP46_METHOD_NIP44_ENCRYPT,
NOSTR_NIP46_METHOD_NIP44_DECRYPT
} nostr_nip46_method_t;
// Parsed bunker:// URL
typedef struct {
char remote_signer_pubkey[65]; // hex pubkey
char relays[8][256]; // up to 8 relay URLs
int relay_count;
char secret[128]; // optional secret
} nostr_nip46_bunker_url_t;
// Parsed nostrconnect:// URL
typedef struct {
char client_pubkey[65]; // hex pubkey
char relays[8][256]; // relay URLs
int relay_count;
char secret[128]; // required secret
char perms[512]; // optional permissions
char name[128]; // optional app name
char url[256]; // optional app URL
char image[256]; // optional app image
} nostr_nip46_nostrconnect_url_t;
// JSON-RPC Request
typedef struct {
char id[65]; // random request ID
nostr_nip46_method_t method;
char method_str[32]; // string form of method
char** params; // array of string params
int param_count;
} nostr_nip46_request_t;
// JSON-RPC Response
typedef struct {
char id[65]; // matching request ID
char* result; // result string
char* error; // error string, NULL if success
} nostr_nip46_response_t;
// Client session state
typedef struct {
unsigned char client_private_key[32];
char client_pubkey_hex[65];
char remote_signer_pubkey_hex[65];
unsigned char remote_signer_pubkey[32];
char user_pubkey_hex[65]; // learned via get_public_key
char relays[8][256];
int relay_count;
int connected;
} nostr_nip46_client_session_t;
// Signer session state
typedef struct {
unsigned char signer_private_key[32];
char signer_pubkey_hex[65];
unsigned char user_private_key[32];
char user_pubkey_hex[65];
char client_pubkey_hex[65];
unsigned char client_pubkey[32];
char relays[8][256];
int relay_count;
int connected;
} nostr_nip46_signer_session_t;
Public API Functions
URL Parsing
| Function | Description |
|---|---|
nostr_nip46_parse_bunker_url() |
Parse bunker://<pubkey>?relay=...&secret=... |
nostr_nip46_parse_nostrconnect_url() |
Parse nostrconnect://<pubkey>?relay=...&secret=...&perms=... |
nostr_nip46_create_bunker_url() |
Generate a bunker:// connection token |
nostr_nip46_create_nostrconnect_url() |
Generate a nostrconnect:// connection token |
JSON-RPC Message Construction
| Function | Description |
|---|---|
nostr_nip46_create_request() |
Build a JSON-RPC request object |
nostr_nip46_create_response() |
Build a JSON-RPC response object |
nostr_nip46_parse_request() |
Parse decrypted JSON into request struct |
nostr_nip46_parse_response() |
Parse decrypted JSON into response struct |
nostr_nip46_free_request() |
Free request struct memory |
nostr_nip46_free_response() |
Free response struct memory |
Event Construction (kind: 24133)
| Function | Description |
|---|---|
nostr_nip46_create_request_event() |
Create NIP-44 encrypted kind:24133 request event |
nostr_nip46_create_response_event() |
Create NIP-44 encrypted kind:24133 response event |
nostr_nip46_decrypt_event() |
Decrypt a kind:24133 event content |
Client-Side Operations
| Function | Description |
|---|---|
nostr_nip46_client_session_init() |
Initialize client session from bunker URL |
nostr_nip46_client_session_destroy() |
Clean up client session |
nostr_nip46_client_connect() |
Send connect request to remote signer |
nostr_nip46_client_get_public_key() |
Request user pubkey from remote signer |
nostr_nip46_client_sign_event() |
Request event signing from remote signer |
nostr_nip46_client_ping() |
Send ping to remote signer |
nostr_nip46_client_nip04_encrypt() |
Request NIP-04 encryption |
nostr_nip46_client_nip04_decrypt() |
Request NIP-04 decryption |
nostr_nip46_client_nip44_encrypt() |
Request NIP-44 encryption |
nostr_nip46_client_nip44_decrypt() |
Request NIP-44 decryption |
Remote-Signer-Side Operations
| Function | Description |
|---|---|
nostr_nip46_signer_session_init() |
Initialize signer session |
nostr_nip46_signer_session_destroy() |
Clean up signer session |
nostr_nip46_signer_handle_request() |
Process an incoming request and produce a response |
nostr_nip46_signer_create_bunker_url() |
Generate bunker URL for distribution |
Utility
| Function | Description |
|---|---|
nostr_nip46_generate_request_id() |
Generate random request ID string |
nostr_nip46_method_to_string() |
Convert method enum to string |
nostr_nip46_string_to_method() |
Convert string to method enum |
Error Codes
New error codes to add to nostr_common.h:
// NIP-46 Remote Signing error codes
#define NOSTR_ERROR_NIP46_INVALID_BUNKER_URL -300
#define NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT -301
#define NOSTR_ERROR_NIP46_INVALID_REQUEST -302
#define NOSTR_ERROR_NIP46_INVALID_RESPONSE -303
#define NOSTR_ERROR_NIP46_ENCRYPTION_FAILED -304
#define NOSTR_ERROR_NIP46_DECRYPTION_FAILED -305
#define NOSTR_ERROR_NIP46_CONNECTION_FAILED -306
#define NOSTR_ERROR_NIP46_TIMEOUT -307
#define NOSTR_ERROR_NIP46_SECRET_MISMATCH -308
#define NOSTR_ERROR_NIP46_UNKNOWN_METHOD -309
#define NOSTR_ERROR_NIP46_AUTH_CHALLENGE -310
#define NOSTR_ERROR_NIP46_NOT_CONNECTED -311
Files to Create/Modify
New Files
| File | Purpose |
|---|---|
nostr_core/nip046.h |
Header with all NIP-46 types, constants, and function declarations |
nostr_core/nip046.c |
Implementation of all NIP-46 functions |
tests/nip46_test.c |
Comprehensive test suite |
examples/nip46_remote_signer.c |
Example showing both client and signer usage |
Modified Files
| File | Change |
|---|---|
nostr_core/nostr_core.h |
Add #include "nip046.h" and NIP-46 API docs in header comment |
nostr_core/nostr_common.h |
Add NIP-46 error code defines |
build.sh |
Add NIP-046 to auto-detection, --nips=all list, and description mapping |
Implementation Details
URL Parsing Strategy
The bunker:// and nostrconnect:// URLs use standard URI format with query parameters. Implementation will:
- Validate the scheme prefix
- Extract the pubkey from the authority section
- Parse query parameters using simple string splitting — no external URL parsing library needed
- URL-decode relay values since they contain
://characters
NIP-44 Encryption Integration
All kind:24133 event content is NIP-44 encrypted. The implementation will use the existing nostr_nip44_encrypt() and nostr_nip44_decrypt() functions from nip044.h. The flow:
- Build JSON-RPC payload as a string
- Encrypt with
nostr_nip44_encrypt(client_privkey, remote_signer_pubkey, payload, ...) - Set as event content
- On receive:
nostr_nip44_decrypt(my_privkey, sender_pubkey, content, ...) - Parse decrypted JSON-RPC payload
Relay Communication
For the initial implementation, the NIP-46 module will provide event construction and parsing only — it will not manage relay connections directly. Users will use the existing relay pool API to:
- Subscribe to kind:24133 events p-tagged to their pubkey
- Publish kind:24133 request/response events
This keeps the module focused and composable, matching the pattern used by NIP-59 and NIP-17.
A higher-level convenience layer could be added later that wraps the relay pool for a fully managed NIP-46 session.
Auth Challenge Handling
When a response has result: "auth_url", the error field contains a URL. The client-side API will return a specific error code NOSTR_ERROR_NIP46_AUTH_CHALLENGE and provide the URL in the response struct's error field, allowing the application to handle it appropriately.
Test Plan
The test suite will cover:
-
URL Parsing Tests
- Parse valid bunker:// URLs with single and multiple relays
- Parse valid nostrconnect:// URLs with all optional fields
- Reject malformed URLs
- Handle URL-encoded relay values
-
URL Generation Tests
- Generate bunker:// URLs and verify round-trip parsing
- Generate nostrconnect:// URLs and verify round-trip parsing
-
JSON-RPC Message Tests
- Create and parse each method type request
- Create and parse success responses
- Create and parse error responses
- Verify request ID matching
-
Event Construction Tests
- Create kind:24133 request events with proper encryption
- Create kind:24133 response events with proper encryption
- Decrypt events and verify content matches
-
Method Enum Conversion Tests
- Round-trip all method enum values through string conversion
-
Signer Request Handling Tests
- Handle connect request and produce ack response
- Handle sign_event request and produce signed event response
- Handle ping request and produce pong response
- Handle get_public_key and return correct pubkey
- Handle NIP-04/NIP-44 encrypt/decrypt requests
-
End-to-End Flow Tests
- Client creates connect request → Signer handles → Client processes response
- Client creates sign_event request → Signer signs → Client gets signed event
- Full bunker:// connection flow
- Secret validation on connect
Implementation Order
- Error codes in
nostr_common.h - Header file
nip046.hwith all declarations - URL parsing functions
- JSON-RPC request/response construction and parsing
- Kind:24133 event creation and decryption
- Client session management
- Signer session and request handling
- Utility functions
- Build system updates
- Test suite
- Example program
- Documentation updates in
nostr_core.h