12 KiB
NIP-46
Nostr Remote Signing
Changes
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.
Rationale
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.
Terminology
- 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.
Initiating a connection
There are two ways to initiate a connection:
Direct connection initiated by remote-signer
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 pastes this token on client, which then uses the details to connect 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 optional secret.
Direct connection initiated by the client
In this case, basically the opposite direction of the first case, client provides a connection token (or encodes the token in a QR code) and remote-signer initiates a connection via the specified relays.
nostrconnect://<client-pubkey>?relay=<wss://relay-to-connect-on>&metadata=<json metadata in the form: {"name":"...", "url": "...", "description": "..."}>
The flow
- 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; - client gets
remote-signer-pubkey
(either via abunker://
connection string or a NIP-05 login-flow; shown below); - client use
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
.
Example flow for signing an event
remote-signer-pubkey
isfa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52
user-pubkey
is alsofa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52
client-pubkey
iseff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86
Signature request
{
"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
}
Response event
{
"kind": 24133,
"pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
"content": nip04({
"id": <random_string>,
"result": json_stringified(<signed-event>)
}),
"tags": [["p", "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86"]], // p-tags the client-pubkey
}
Diagram
Request Events kind: 24133
{
"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.
Methods/Commands
Each of the following are methods that the client sends to the remote signer.
Command | Params | Result |
---|---|---|
connect |
[<user_pubkey>, <optional_secret>, <optional_requested_permissions>] |
"ack" |
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> |
create_account |
[<username>, <domain>, <optional_email>, <optional_requested_permissions>] |
<newly_created_user_pubkey> |
Requested permissions
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.
Response Events kind:24133
{
"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.
Auth Challenges
An Auth Challenge is a response that a remote signer can send back when it needs the user to authenticate via other means. This is currently used in the OAuth-like flow enabled by signers like Nsecbunker. The response content
object will take the following form:
{
"id": <request_id>,
"result": "auth_url",
"error": <URL_to_display_to_end_user>
}
Clients 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). It's also possible to add a redirect_uri
url parameter to the auth_url, which is helpful in situations when a client cannot open a new window or tab to display the auth challenge.
Example event signing request with auth challenge
Appendix
NIP-05 Login Flow
Clients might choose to present a more familiar login flow, so users can type a NIP-05 address instead of a bunker://
string.
When the user types a NIP-05 the client:
- Queries the
/.well-known/nostr.json
file from the domain for the NIP-05 address provided to get the user's pubkey (this is theuser-pubkey
) - In the same
/.well-known/nostr.json
file, queries for thenip46
key to get the relays that the remote signer will be listening on. - Now the client has enough information to send commands to the remote signer on behalf of the user.
OAuth-like Flow
Remote signer discovery via NIP-89
In this last case, most often used to facilitate an OAuth-like signin flow, the client first looks for remote signers that have announced themselves via NIP-89 application handler events.
First the client will query for kind: 31990
events that have a k
tag of 24133
.
These are generally shown to a user, and once the user selects which remote signer to use and provides the user-pubkey
they want to use (via npub, pubkey, or nip-05 value), the client can initiate a connection. Note that it's on the user to select the remote-signer that is actually managing the user-keypair
that they would like to use in this case. If the user-pubkey
is managed on another remote-signer the connection will fail.
In addition, it's important that clients validate that the pubkey of the announced remote-signer matches the pubkey of the _
entry in the /.well-known/nostr.json
file of the remote signer's announced domain.
Clients that allow users to create new accounts should also consider validating the availability of a given username in the namespace of remote signer's domain by checking the /.well-known/nostr.json
file for existing usernames. Clients can then show users feedback in the UI before sending a create_account
event to the remote signer and receiving an error in return. Ideally, remote signers would also respond with understandable error messages if a client tries to create an account with an existing username.
Example Oauth-like flow to create a new user account with Nsecbunker
Coming soon...