mirror of
https://github.com/nostr-protocol/nips.git
synced 2024-12-22 08:25:53 -05:00
228 lines
12 KiB
Markdown
228 lines
12 KiB
Markdown
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://<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.
|
|
|
|
### 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": <local_keypair_pubkey>,
|
|
"content": <nip04(<request>)>,
|
|
"tags": [["p", <remote-signer-pubkey>]],
|
|
}
|
|
```
|
|
|
|
The `content` field is a JSON-RPC-like message that is [NIP-04](04.md) encrypted and has the following structure:
|
|
|
|
```jsonc
|
|
{
|
|
"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` | `[<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>` |
|
|
|
|
### 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. 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": <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](04.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": 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
|
|
|
|
```jsonc
|
|
{
|
|
"kind": 24133,
|
|
"pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
|
|
"content": nip04({
|
|
"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.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 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>`.
|