Nostr NIPS 46

 

Page content

NIP-46 - Nostr Remote Signing

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

  • Local keypair: A local public and private key-pair used to encrypt content and communicate with the remote signer. Usually created by the client application.
  • Remote user pubkey: The public key that the user wants to sign as. The remote signer has control of the private key that matches this public key.
  • Remote signer pubkey: This is the public key of the remote signer itself. This is needed in both create_account command because you don’t yet have a remote user pubkey.

All pubkeys specified in this NIP are in hex format.

Initiating a connection

To initiate a connection between a client and a remote signer there are a few different options.

Direct connection initiated by remote signer

This is most common in a situation where you have your own nsecbunker or other type of remote signer and want to connect through a client that supports remote signing.

The remote signer would provide a connection token in the form:

bunker://<remote-pubkey>?relay=<wss://relay-to-connect-on>&relay=<wss://another-relay-to-connect-on>&secret=<optional-secret-value>

This token is pasted into the client by the user and the client then uses the details to connect to the remote signer via the specified relay(s).

Direct connection initiated by the client

In this case, basically the opposite direction of the first case, the client provides a connection token (or encodes the token in a QR code) and the signer initiates a connection to the client via the specified relay(s).

nostrconnect://<local-keypair-pubkey>?relay=<wss://relay-to-connect-on>&metadata=<json metadata in the form: {"name":"...", "url": "...", "description": "..."}>

The flow

  1. Client creates a local keypair. This keypair doesn’t need to be communicated to the user since it’s largely disposable (i.e. the user doesn’t need to see this pubkey). Clients might choose to store it locally and they should delete it when the user logs out.
  2. Client gets the remote user pubkey (either via a bunker:// connection string or a NIP-05 login-flow; shown below)
  3. Clients use the local keypair to send requests to the remote signer by p-tagging and encrypting to the remote user pubkey.
  4. The remote signer responds to the client by p-tagging and encrypting to the local keypair pubkey.

Example flow for signing an event

  • Remote user pubkey (e.g. signing as) fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52
  • Local pubkey is eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86
Signature request
{
    "kind": 24133,
    "pubkey": "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86",
    "content": nip04({
        "id": <random_string>,
        "method": "sign_event",
        "params": [json_stringified(<{
            content: "Hello, I'm signing remotely",
            pubkey: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
            // ...the rest of the event data
        }>)]
    }),
    "tags": [["p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"]], // p-tags the remote user pubkey
}
Response event
{
    "kind": 24133,
    "pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
    "content": nip04({
        "id": <random_string>,
        "result": json_stringified(<signed-event>)
    }),
    "tags": [["p", "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86"]], // p-tags the local keypair pubkey
}
Diagram

signing-example

Request Events kind: 24133

{
    "id": <id>,
    "kind": 24133,
    "pubkey": <local_keypair_pubkey>,
    "content": <nip04(<request>)>,
    "tags": [["p", <remote_user_pubkey>]], // NB: in the `create_account` event, the remote signer pubkey should be `p` tagged.
    "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": <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.

CommandParamsResult
connect[<remote_user_pubkey>, <optional_secret>, <optional_requested_permissions>]“ack”
sign_event[<json_stringified_event_to_sign>]json_stringified(<signed_event>)
ping[]“pong”
get_relays[]json_stringified({<relay_url>: {read: <boolean>, write: <boolean>}})
get_public_key[]<hex-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>

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", <local_keypair_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": <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 is an error in string form.

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

signing-example-with-auth-challenge

Remote Signer Commands

Remote signers might support additional commands when communicating directly with it. These commands follow the same flow as noted above, the only difference is that when the client sends a request event, the p-tag is the pubkey of the remote signer itself and the content payload is encrypted to the same remote signer pubkey.

Methods/Commands

Each of the following are methods that the client sends to the remote signer.

CommandParamsResult
create_account[<username>, <domain>, <optional_email>, <optional_requested_permissions>]<newly_created_remote_user_pubkey>

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 the remote user pubkey)
  • In the same /.well-known/nostr.json file, queries for the nip46 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 fascilitate 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 remote 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 remote key that they would like to use in this case. If the remote 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…

References


Source: nostr-protocol/nips/46.md version: 4b79bc6 2024-03-22T08:01:37+01:00