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, nip05 login is removed, create_account moved to another NIP. ## 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. ## Overview 1. _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; 2. A connection is established (see below), _remote-signer_ learns `client-pubkey`, _client_ learns `remote-signer-pubkey`. 3. _client_ uses `client-keypair` to send requests to _remote-signer_ by `p`-tagging and encrypting to `remote-signer-pubkey`; 4. _remote-signer_ responds to _client_ by `p`-tagging and encrypting to the `client-pubkey`. 5. _client_ requests `get_public_key` to learn `user-pubkey`. ## 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://?relay=&relay=&secret= ``` _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. ### Direct connection initiated by the _client_ _client_ provides a connection token using `nostrconnect://` as the protocol, and `client-pubkey` as the origin. Additional information should be passed as query parameters: - `relay` (required) - one or more relay urls on which the _client_ is listening for responses from the _remote-signer_. - `secret` (required) - a short random string that the _remote-signer_ should return as the `result` field of its response. - `perms` (optional) - a comma-separated list of permissions the _client_ is requesting be approved by the _remote-signer_ - `name` (optional) - the name of the _client_ application - `url` (optional) - the canonical url of the _client_ application - `image` (optional) - a small image representing the _client_ application Here's an example: ``` nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay1.example.com&perms=nip44_encrypt%2Cnip44_decrypt%2Csign_event%3A13%2Csign_event%3A14%2Csign_event%3A1059&name=My+Client&secret=0s8j2djs&relay=wss%3A%2F%2Frelay2.example2.com ``` _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. ## Request Events `kind: 24133` ```jsonc { "kind": 24133, "pubkey": , "content": )>, "tags": [["p", ]], } ``` The `content` field is a JSON-RPC-like message that is [NIP-44](44.md) encrypted and has the following structure: ```jsonc { "id": , "method": , "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` | `[, , ]` | "ack" OR `` | | `sign_event` | `[<{kind, content, tags, created_at}>]` | `json_stringified()` | | `ping` | `[]` | "pong" | | `get_relays` | `[]` | `json_stringified({: {read: , write: }})` | | `get_public_key` | `[]` | `` | | `nip04_encrypt` | `[, ]` | `` | | `nip04_decrypt` | `[, ]` | `` | | `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. `nip44_encrypt,sign_event:4` meaning permissions to call `nip44_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. ## Response Events `kind:24133` ```json { "id": <id>, "kind": 24133, "pubkey": <remote-signer-pubkey>, "content": <nip44(<response>)>, "tags": [["p", <client-pubkey>]], "created_at": <unix timestamp in seconds> } ``` The `content` field is a JSON-RPC-like message that is [NIP-44](44.md) encrypted and has the following structure: ```json { "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. ## Example flow for signing an event - `remote-signer-pubkey` is `fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52` - `user-pubkey` is also `fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52` - `client-pubkey` is `eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86` ### Signature request ```jsonc { "kind": 24133, "pubkey": "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86", "content": nip44({ "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 ```jsonc { "kind": 24133, "pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "content": nip44({ "id": <random_string>, "result": json_stringified(<signed-event>) }), "tags": [["p", "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86"]], // p-tags the client-pubkey } ``` ### Diagram ![signing-example](https://i.nostr.build/P3gW.png) ## 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. The response `content` object will take the following form: ```json { "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). ### Example event signing request with auth challenge ![signing-example-with-auth-challenge](https://i.nostr.build/W3aj.png) ## Appendix ### Announcing _remote-signer_ metadata _remote-signer_ MAY publish it's metadata by using [NIP-05](05.md) and [NIP-89](89.md). With NIP-05, a request to `<remote-signer>/.well-known/nostr.json?name=_` MAY return this: ```jsonc { "names":{ "_": <remote-signer-app-pubkey>, }, "nip46": { "relays": ["wss://relay1","wss://relay2"...], "nostrconnect_url": "https://remote-signer-domain.example/<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 discovery via NIP-89 _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>`.