remote-signer-key
is introduced, passed in bunker url, clients must differentiate between remote-signer-pubkey
and user-pubkey
, must call get_public_key
after connect, nip05 login is removed, create_account moved to another NIP.
Private keys should be exposed to as few systems - apps, operating systems, devices - as possible as each system adds to the attack surface.
This NIP describes a method for 2-way communication between a remote signer and a Nostr client. The remote signer could be, for example, a hardware device dedicated to signing Nostr events, while the client is a normal Nostr client.
- user: A person that is trying to use Nostr.
- client: A user-facing application that user is looking at and clicking buttons in. This application will send requests to remote-signer.
- remote-signer: A daemon or server running somewhere that will answer requests from client, also known as "bunker".
- client-keypair/pubkey: The keys generated by client. Used to encrypt content and communicate with remote-signer.
- remote-signer-keypair/pubkey: The keys used by remote-signer to encrypt content and communicate with client. This keypair MAY be same as user-keypair, but not necessarily.
- user-keypair/pubkey: The actual keys representing user (that will be used to sign events in response to
sign_event
requests, for example). The remote-signer generally has control over these keys.
All pubkeys specified in this NIP are in hex format.
- client generates
client-keypair
. This keypair doesn't need to be communicated to user since it's largely disposable. client might choose to store it locally and they should delete it on logout; - A connection is established (see below), remote-signer learns
client-pubkey
, client learnsremote-signer-pubkey
. - client uses
client-keypair
to send requests to remote-signer byp
-tagging and encrypting toremote-signer-pubkey
; - remote-signer responds to client by
p
-tagging and encrypting to theclient-pubkey
. - client requests
get_public_key
to learnuser-pubkey
.
There are two ways to initiate a connection:
remote-signer provides connection token in the form:
bunker://<remote-signer-pubkey>?relay=<wss://relay-to-connect-on>&relay=<wss://another-relay-to-connect-on>&secret=<optional-secret-value>
user passes this token to client, which then sends connect
request to remote-signer via the specified relays. Optional secret can be used for single successfully established connection only, remote-signer SHOULD ignore new attempts to establish connection with old secret.
client provides a connection token in the form:
nostrconnect://<client-pubkey>?relay=<wss://relay-to-connect-on>&metadata=<json metadata: {"name":"...", "url": "...", "description": "...", "perms": "..."}>&secret=<required-secret-value>
user passes this token to remote-signer, which then sends connect
response event to the client-pubkey
via the specified relays. Client discovers remote-signer-pubkey
from connect response author. secret
value MUST be provided to avoid connection spoofing, client MUST validate the secret
returned by connect
response.
{
"kind": 24133,
"pubkey": <local_keypair_pubkey>,
"content": <nip04(<request>)>,
"tags": [["p", <remote-signer-pubkey>]],
}
The content
field is a JSON-RPC-like message that is NIP-04 encrypted and has the following structure:
{
"id": <random_string>,
"method": <method_name>,
"params": [array_of_strings]
}
id
is a random string that is a request ID. This same ID will be sent back in the response payload.method
is the name of the method/command (detailed below).params
is a positional array of string parameters.
Each of the following are methods that the client sends to the remote-signer.
Command | Params | Result |
---|---|---|
connect |
[<remote-signer-pubkey>, <optional_secret>, <optional_requested_permissions>] |
"ack" OR <required-secret-value> |
sign_event |
[<{kind, content, tags, created_at}>] |
json_stringified(<signed_event>) |
ping |
[] |
"pong" |
get_relays |
[] |
json_stringified({<relay_url>: {read: <boolean>, write: <boolean>}}) |
get_public_key |
[] |
<user-pubkey> |
nip04_encrypt |
[<third_party_pubkey>, <plaintext_to_encrypt>] |
<nip04_ciphertext> |
nip04_decrypt |
[<third_party_pubkey>, <nip04_ciphertext_to_decrypt>] |
<plaintext> |
nip44_encrypt |
[<third_party_pubkey>, <plaintext_to_encrypt>] |
<nip44_ciphertext> |
nip44_decrypt |
[<third_party_pubkey>, <nip44_ciphertext_to_decrypt>] |
<plaintext> |
The connect
method may be provided with optional_requested_permissions
for user convenience. The permissions are a comma-separated list of method[:params]
, i.e. nip04_encrypt,sign_event:4
meaning permissions to call nip04_encrypt
and to call sign_event
with kind:4
. Optional parameter for sign_event
is the kind number, parameters for other methods are to be defined later. Same permission format may be used for perms
field of metadata
in nostrconnect://
string.
{
"id": <id>,
"kind": 24133,
"pubkey": <remote-signer-pubkey>,
"content": <nip04(<response>)>,
"tags": [["p", <client-pubkey>]],
"created_at": <unix timestamp in seconds>
}
The content
field is a JSON-RPC-like message that is NIP-04 encrypted and has the following structure:
{
"id": <request_id>,
"result": <results_string>,
"error": <optional_error_string>
}
id
is the request ID that this response is for.results
is a string of the result of the call (this can be either a string or a JSON stringified object)error
, optionally, it is an error in string form, if any. Its presence indicates an error with the request.
remote-signer-pubkey
isfa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52
user-pubkey
is alsofa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52
client-pubkey
iseff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86
{
"kind": 24133,
"pubkey": "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86",
"content": nip04({
"id": <random_string>,
"method": "sign_event",
"params": [json_stringified(<{
content: "Hello, I'm signing remotely",
kind: 1,
tags: [],
created_at: 1714078911
}>)]
}),
"tags": [["p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"]], // p-tags the remote-signer-pubkey
}
{
"kind": 24133,
"pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
"content": nip04({
"id": <random_string>,
"result": json_stringified(<signed-event>)
}),
"tags": [["p", "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86"]], // p-tags the client-pubkey
}
An Auth Challenge is a response that a remote-signer can send back when it needs the user to authenticate via other means. The response content
object will take the following form:
{
"id": <request_id>,
"result": "auth_url",
"error": <URL_to_display_to_end_user>
}
client should display (in a popup or new tab) the URL from the error
field and then subscribe/listen for another response from the remote-signer (reusing the same request ID). This event will be sent once the user authenticates in the other window (or will never arrive if the user doesn't authenticate).
remote-signer MAY publish it's metadata by using NIP-05 and NIP-89. With NIP-05, a request to <remote-signer>/.well-known/nostr.json?name=_
MAY return this:
{
"names":{
"_": <remote-signer-app-pubkey>,
},
"nip46": {
"relays": ["wss://relay1","wss://relay2"...],
"nostrconnect_url": "https://remote-signer-domain.com/<nostrconnect>"
}
}
The <remote-signer-app-pubkey>
MAY be used to verify the domain from remote-signer's NIP-89 event (see below). relays
SHOULD be used to construct a more precise nostrconnect://
string for the specific remote-signer
. nostrconnect_url
template MAY be used to redirect users to remote-signer's connection flow by replacing <nostrconnect>
placeholder with an actual nostrconnect://
string.
remote-signer MAY publish a NIP-89 kind: 31990
event with k
tag of 24133
, which MAY also include one or more relay
tags and MAY include nostrconnect_url
tag. The semantics of relay
and nostrconnect_url
tags are the same as in the section above.
client MAY improve UX by discovering remote-signers using their kind: 31990
events. client MAY then pre-generate nostrconnect://
strings for the remote-signers, and SHOULD in that case verify that kind: 31990
event's author is mentioned in signer's nostr.json?name=_
file as <remote-signer-app-pubkey>
.