clone

every nostr nip as a chapter of an illustrated, early-2000s printed game manual

main
nip.guide / site / nips / 46.html
46.html 36.9K
1<!DOCTYPE html>2<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width, initial-scale=1">6<title>NIP-46 - Nostr Remote Signing — Nostr Instruction Manual</title>7<meta name="description" content="NIP-46: Nostr remote signing. A client talks to a remote signer (bunker) over relays with encrypted kind 24133 events. Complete manual page.">8<link rel="preload" href="../assets/fonts/michroma-400-latin.woff2" as="font" type="font/woff2" crossorigin>9<link rel="preload" href="../assets/fonts/exo2-var-latin.woff2" as="font" type="font/woff2" crossorigin>10<link rel="preload" href="../assets/fonts/sairacondensed-600-latin.woff2" as="font" type="font/woff2" crossorigin>11<link rel="preload" href="../assets/fonts/sairacondensed-700-latin.woff2" as="font" type="font/woff2" crossorigin>12<link rel="preload" href="../assets/fonts/sairacondensed-500-latin.woff2" as="font" type="font/woff2" crossorigin>13<link rel="preload" href="../assets/fonts/sharetechmono-400-latin.woff2" as="font" type="font/woff2" crossorigin>14<link rel="preload" href="../assets/fonts/exo2-400i-latin.woff2" as="font" type="font/woff2" crossorigin>15<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">16<link rel="stylesheet" href="../assets/manual.css">17</head>18<body>1920<svg width="0" height="0" style="position:absolute">21 <defs>22 <marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">23 <path d="M1,1 L8,4.5 L1,8 Z" fill="#f26a00"/>24 </marker>25 <marker id="arrb" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">26 <path d="M1,1 L8,4.5 L1,8 Z" fill="#2e5f9e"/>27 </marker>28 </defs>29</svg>3031<div class="page">32 <span class="staple s1"></span><span class="staple s2"></span>3334 <header class="band">35 <a class="logo" href="../index.html">NOSTR<b>▸</b>PROTOCOL MANUAL</a>36 <span class="booklet-note">Chapter 46 - Key Security</span>37 </header>38 <nav class="tabs">39 <a href="../index.html">Contents</a>40 <a href="#overview" class="on">Overview</a>41 <a href="#spec">Spec</a>42 <a href="#examples">Examples</a>43 <a href="#history">History</a>44 </nav>4546 <div class="chapter">47 <div class="nipno" data-n="46">46</div>48 <div class="titles">49 <div class="kicker">Nostr Implementation Possibility - Hex № 46</div>50 <h1>Nostr Remote Signing</h1>51 <p class="sub">A client asks a remote signer — the &ldquo;bunker&rdquo; — to sign events over relays, so the user's private key never touches the client.</p>52 </div>53 </div>5455 <main class="spread">56 <div class="main">5758 <h2 class="bar" id="overview"><span class="num">1</span> Overview</h2>59 <p>A private key should touch as few systems as possible. Every app, operating system, and device that holds the key adds attack surface. This NIP moves the key out of the client. The key lives inside a <strong>remote signer</strong>. The remote signer can be a daemon, a server, or a dedicated hardware device.</p>60 <p>The client and the remote signer talk in two directions over normal Nostr relays. Each request and each response is a <strong>kind 24133</strong> event. The payload inside is encrypted with <a href="44.html">NIP-44</a>. The client asks: sign this event, encrypt this text, decrypt this text. The remote signer answers. The user's key stays in the bunker.</p>61 <p>The flow has five steps. First, the client generates a disposable <code>client-keypair</code>. Second, a connection is established; the remote signer learns the <code>client-pubkey</code> and the client learns the <code>remote-signer-pubkey</code>. Third, the client sends requests: it encrypts to the <code>remote-signer-pubkey</code> and adds a <code>p</code> tag with that pubkey. Fourth, the remote signer responds the same way toward the <code>client-pubkey</code>. Fifth, the client calls <code>get_public_key</code> to learn the <code>user-pubkey</code>.</p>6263 <div class="box hint">64 <span class="tag">Hint!</span>65 <p>Kind 24133 sits in the ephemeral range (20000–29999) from <a href="01.html">NIP-01</a>. Relays forward these events but do not store them. Both sides must be online, or use relays that briefly buffer.</p>66 </div>6768 <figure class="diagram">69 <div class="frame">70 <svg viewBox="0 0 560 190" role="img" aria-label="Client and remote signer exchange encrypted kind 24133 events through relays; the user key stays inside the signer">71 <rect class="d-actor" x="10" y="70" width="120" height="50" rx="8"/>72 <text class="d-actor-label" x="70" y="92" text-anchor="middle">Client</text>73 <text class="d-note" x="70" y="109" text-anchor="middle">client-keypair</text>74 <rect class="d-actor" x="225" y="70" width="110" height="50" rx="8"/>75 <text class="d-actor-label" x="280" y="99" text-anchor="middle">Relays</text>76 <rect class="d-actor" x="430" y="60" width="120" height="70" rx="8"/>77 <text class="d-actor-label" x="490" y="86" text-anchor="middle">Remote signer</text>78 <text class="d-note" x="490" y="103" text-anchor="middle">user-keypair</text>79 <text class="d-note" x="490" y="118" text-anchor="middle">(never leaves)</text>80 <path class="d-msg d-flow" d="M132,84 L222,84"/>81 <path class="d-msg d-flow" d="M337,84 L427,84"/>82 <path class="d-msg blue d-flow" d="M427,106 L337,106"/>83 <path class="d-msg blue d-flow" d="M222,106 L132,106"/>84 <text class="d-label" x="280" y="52" text-anchor="middle">kind 24133 - nip44(request)</text>85 <text class="d-label" x="280" y="150" text-anchor="middle">kind 24133 - nip44(response)</text>86 <circle r="4" fill="#f26a00">87 <animateMotion dur="2.6s" repeatCount="indefinite" path="M132,84 L427,84"/>88 </circle>89 <circle r="4" fill="#2e5f9e">90 <animateMotion dur="2.6s" begin="1.3s" repeatCount="indefinite" path="M427,106 L132,106"/>91 </circle>92 </svg>93 </div>94 <figcaption>Fig. 46-1 — <b>The bunker link.</b> Encrypted requests go out, encrypted responses come back. The user's key stays put.</figcaption>95 </figure>9697 <h2 class="bar" id="spec"><span class="num">2</span> Roles and Keys</h2>98 <p>This NIP uses three keypairs. Keep them apart. All pubkeys in this NIP are in hex format.</p>99 <div class="tscroll"><table class="ref">100 <caption>Terminology</caption>101 <tr><th>Term</th><th>Meaning</th></tr>102 <tr><td class="btn">user</td><td>A person that is trying to use Nostr.</td></tr>103 <tr><td class="btn">client</td><td>The user-facing application. It sends requests to the remote signer.</td></tr>104 <tr><td class="btn">remote-signer</td><td>A daemon or server that answers requests from the client. Also called the &ldquo;bunker&rdquo;.</td></tr>105 <tr><td class="btn">client-keypair</td><td>Keys the client generates. Used to encrypt content and talk to the remote signer.</td></tr>106 <tr><td class="btn">remote-signer-keypair</td><td>Keys the remote signer uses to encrypt content and talk to the client. May equal the user-keypair, but not necessarily.</td></tr>107 <tr><td class="btn">user-keypair</td><td>The keys that represent the user. They sign events for <code>sign_event</code> requests. The remote signer controls them.</td></tr>108 </table></div>109 <p>The <code>client-keypair</code> is largely disposable. The client does not need to show it to the user. The client may store it locally. The client must delete it on logout. Because the <code>remote-signer-pubkey</code> and the <code>user-pubkey</code> can differ, the client must not assume they are equal. The client must call <code>get_public_key</code> after <code>connect</code> to learn the <code>user-pubkey</code>.</p>110111 <h2 class="bar" id="connect"><span class="num">3</span> Initiating a Connection</h2>112 <p>There are two ways to connect. Either side can hand the other a token.</p>113114 <h3>Flow A — the remote signer starts: <code>bunker://</code></h3>115 <p>The remote signer provides a connection token in this form:</p>116 <div class="crt">117 <div class="titlebar">Connection token - signer side</div>118<pre>bunker://<span class="c">&lt;remote-signer-pubkey&gt;</span>?relay=<span class="c">&lt;wss://relay-to-connect-on&gt;</span>119 &amp;relay=<span class="c">&lt;wss://another-relay-to-connect-on&gt;</span>120 &amp;secret=<span class="c">&lt;optional-secret-value&gt;</span></pre>121 </div>122 <p>The user passes this token to the client. The client then sends a <code>connect</code> request to the remote signer on the listed relays. A <code>bunker://</code> token carries no information about the client. So in this flow the client should include client metadata in the <code>connect</code> request (see §4). The optional secret is valid for one successfully established connection only. The remote signer should ignore new connection attempts that reuse an old secret.</p>123124 <h3>Flow B — the client starts: <code>nostrconnect://</code></h3>125 <p>The client provides a token with <code>nostrconnect://</code> as the protocol and the <code>client-pubkey</code> as the origin. Query parameters carry the rest:</p>126 <div class="tscroll"><table class="ref">127 <caption>nostrconnect:// query parameters</caption>128 <tr><th>Parameter</th><th>Required</th><th>Meaning</th></tr>129 <tr><td class="btn">relay</td><td>required</td><td>One or more relay URLs on which the client listens for responses from the remote signer.</td></tr>130 <tr><td class="btn">secret</td><td>required</td><td>A short random string. The remote signer must return it as the <code>result</code> of its response.</td></tr>131 <tr><td class="btn">perms</td><td>optional</td><td>A comma-separated list of permissions the client asks the remote signer to approve.</td></tr>132 <tr><td class="btn">name</td><td>optional</td><td>The name of the client application.</td></tr>133 <tr><td class="btn">url</td><td>optional</td><td>The canonical URL of the client application.</td></tr>134 <tr><td class="btn">image</td><td>optional</td><td>A small image that represents the client application.</td></tr>135 </table></div>136 <div class="crt">137 <div class="titlebar">Connection token - client side (example)</div>138<pre>nostrconnect://<span class="c">83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5</span>139 ?relay=<span class="s">wss%3A%2F%2Frelay1.example.com</span>140 &amp;perms=<span class="s">nip44_encrypt%2Cnip44_decrypt%2Csign_event%3A13%2Csign_event%3A14%2Csign_event%3A1059</span>141 &amp;name=<span class="s">My+Client</span>142 &amp;secret=<span class="s">0s8j2djs</span>143 &amp;relay=<span class="s">wss%3A%2F%2Frelay2.example2.com</span></pre>144 </div>145 <p>The user passes this token to the remote signer. The remote signer then sends a <code>connect</code> <em>response</em> event to the <code>client-pubkey</code> on the listed relays. The client discovers the <code>remote-signer-pubkey</code> from the author of that response. The <code>secret</code> must be provided to avoid connection spoofing. The client must validate the <code>secret</code> that the <code>connect</code> response returns.</p>146147 <figure class="diagram">148 <div class="frame">149 <svg viewBox="0 0 560 300" role="img" aria-label="Sequence diagram of the client-initiated nostrconnect flow: token to signer, connect response with secret, get_public_key, user pubkey">150 <line class="d-wire" x1="110" y1="40" x2="110" y2="285" stroke-dasharray="3 4"/>151 <line class="d-wire" x1="450" y1="40" x2="450" y2="285" stroke-dasharray="3 4"/>152 <rect class="d-actor" x="55" y="8" width="110" height="30" rx="6"/>153 <text class="d-actor-label" x="110" y="28" text-anchor="middle">Client</text>154 <rect class="d-actor" x="395" y="8" width="110" height="30" rx="6"/>155 <text class="d-actor-label" x="450" y="28" text-anchor="middle">Remote signer</text>156 <path class="d-msg" d="M110,62 L445,62" stroke-dasharray="4 3"/>157 <text class="d-label" x="280" y="55" text-anchor="middle">nostrconnect://… token (user hands it over)</text>158 <path class="d-msg blue" d="M450,102 L115,102"/>159 <text class="d-label" x="280" y="95" text-anchor="middle">connect response: {"result":"0s8j2djs"}</text>160 <text class="d-note" x="280" y="122" text-anchor="middle">client checks secret, learns remote-signer-pubkey from author</text>161 <path class="d-msg" d="M110,152 L445,152"/>162 <text class="d-label" x="280" y="145" text-anchor="middle">{"method":"get_public_key","params":[]}</text>163 <path class="d-msg blue" d="M450,192 L115,192"/>164 <text class="d-label" x="280" y="185" text-anchor="middle">{"result":"&lt;user-pubkey&gt;"}</text>165 <path class="d-msg" d="M110,232 L445,232"/>166 <text class="d-label" x="280" y="225" text-anchor="middle">{"method":"switch_relays","params":[]}</text>167 <path class="d-msg blue" d="M450,268 L115,268"/>168 <text class="d-label" x="280" y="261" text-anchor="middle">{"result": relay list or null}</text>169 </svg>170 </div>171 <figcaption>Fig. 46-2 — <b>Client-initiated connection.</b> The signer answers the token with the secret. The client then asks for the user pubkey and the signer's preferred relays.</figcaption>172 </figure>173174 <h2 class="bar" id="requests"><span class="num">4</span> Request Events — <code>kind: 24133</code></h2>175 <p>A request is a normal <a href="01.html">NIP-01</a> event with kind 24133. Its author is the <code>client-pubkey</code>. It carries one <code>p</code> tag with the <code>remote-signer-pubkey</code>:</p>176 <div class="crt">177 <div class="titlebar">Wire format - request event</div>178<pre>{179 <span class="k">"kind"</span>: 24133,180 <span class="k">"pubkey"</span>: <span class="c">&lt;client-pubkey&gt;</span>,181 <span class="k">"content"</span>: <span class="c">&lt;nip44(&lt;request&gt;)&gt;</span>,182 <span class="k">"tags"</span>: [[<span class="s">"p"</span>, <span class="c">&lt;remote-signer-pubkey&gt;</span>]]183}</pre>184 </div>185 <p>The <code>content</code> field is a JSON-RPC-like message. It is encrypted with <a href="44.html">NIP-44</a>. Its plaintext structure is:</p>186 <div class="crt">187 <div class="titlebar">Request payload (before encryption)</div>188<pre>{189 <span class="k">"id"</span>: <span class="c">&lt;random_string&gt;</span>,190 <span class="k">"method"</span>: <span class="c">&lt;method_name&gt;</span>,191 <span class="k">"params"</span>: <span class="c">[array_of_strings]</span>192}</pre>193 </div>194 <ul>195 <li><code>id</code> is a random string that identifies the request. The response carries back the same ID.</li>196 <li><code>method</code> is the name of the method or command (see the table below).</li>197 <li><code>params</code> is a positional array of string parameters.</li>198 </ul>199200 <h3>Methods</h3>201 <p>The client can send these ten methods to the remote signer:</p>202 <div class="tscroll"><table class="ref">203 <caption>Request methods (client → remote signer)</caption>204 <tr><th>Command</th><th>Params</th><th>Result</th></tr>205 <tr><td class="btn">connect</td><td><code>[&lt;remote-signer-pubkey&gt;, &lt;optional_secret&gt;, &lt;optional_requested_perms&gt;, &lt;optional_client_metadata&gt;]</code></td><td><code>"ack"</code> or <code>&lt;required-secret-value&gt;</code></td></tr>206 <tr><td class="btn">sign_event</td><td><code>[&lt;{kind, content, tags, created_at}&gt;]</code></td><td><code>json_stringified(&lt;signed_event&gt;)</code></td></tr>207 <tr><td class="btn">ping</td><td><code>[]</code></td><td><code>"pong"</code></td></tr>208 <tr><td class="btn">get_public_key</td><td><code>[]</code></td><td><code>&lt;user-pubkey&gt;</code></td></tr>209 <tr><td class="btn">nip04_encrypt</td><td><code>[&lt;third_party_pubkey&gt;, &lt;plaintext_to_encrypt&gt;]</code></td><td><code>&lt;nip04_ciphertext&gt;</code> (<a href="04.html">NIP-04</a>)</td></tr>210 <tr><td class="btn">nip04_decrypt</td><td><code>[&lt;third_party_pubkey&gt;, &lt;nip04_ciphertext_to_decrypt&gt;]</code></td><td><code>&lt;plaintext&gt;</code></td></tr>211 <tr><td class="btn">nip44_encrypt</td><td><code>[&lt;third_party_pubkey&gt;, &lt;plaintext_to_encrypt&gt;]</code></td><td><code>&lt;nip44_ciphertext&gt;</code> (<a href="44.html">NIP-44</a>)</td></tr>212 <tr><td class="btn">nip44_decrypt</td><td><code>[&lt;third_party_pubkey&gt;, &lt;nip44_ciphertext_to_decrypt&gt;]</code></td><td><code>&lt;plaintext&gt;</code></td></tr>213 <tr><td class="btn">switch_relays</td><td><code>[]</code></td><td><code>["&lt;relay-url&gt;", "&lt;relay-url&gt;", ...]</code> or <code>null</code></td></tr>214 <tr><td class="btn">logout</td><td><code>[]</code></td><td><code>"ack"</code></td></tr>215 </table></div>216217 <h3>Requested permissions</h3>218 <p>The <code>connect</code> method may carry <code>optional_requested_perms</code> for user convenience. The permissions are a comma-separated list of <code>method[:params]</code>. Example: <code>nip44_encrypt,sign_event:4</code> asks for permission to call <code>nip44_encrypt</code> and to call <code>sign_event</code> with <code>kind:4</code>. For <code>sign_event</code> the optional parameter is the kind number. Parameters for the other methods are to be defined later. The <code>perms</code> parameter in a <code>nostrconnect://</code> string may use the same format.</p>219 <div class="box tech">220 <span class="tag">Tech Note</span>221 <p>The source text says the permission format &ldquo;may be used for the <code>perms</code> field of <code>metadata</code>&rdquo; in the <code>nostrconnect://</code> string. But <code>perms</code> is defined as a query parameter, and the metadata object lists only <code>name</code>, <code>url</code>, and <code>image</code>. The spec never resolves this wording. Treat <code>perms</code> as a query parameter.</p>222 </div>223224 <h3>Client metadata</h3>225 <p>The <code>connect</code> method may carry <code>optional_client_metadata</code>. This lets the remote signer identify and label the connection. It is a JSON-stringified object. Its fields mirror the <code>nostrconnect://</code> query parameters, and all are optional: <code>name</code>, <code>url</code>, and <code>image</code>. This helps most in the <code>bunker://</code> flow, where the remote signer has no other source for this information. To send metadata without requesting permissions, pass an empty string for <code>optional_requested_perms</code>. The metadata must then sit in the fourth position.</p>226 <div class="box warn">227 <span class="tag">Warning!</span>228 <p>Client metadata is client-supplied and unauthenticated. The remote signer should treat it as a display hint only. The remote signer must not use it for authorization decisions.</p>229 </div>230231 <h3>Switching relays</h3>232 <p>The remote signer stays in control of the relays used for the connection at all times. Its relay set can evolve as old relays shut down and new ones appear. In the client-initiated flow the client may even pick relays foreign to the signer's preferences. The signer must be able to switch those at once.</p>233 <p>So compliant clients should send a <code>switch_relays</code> request immediately after the connection is established — always, or at reasonable intervals. The remote signer replies with its updated relay list, or <code>null</code> when nothing changes. As soon as the client receives an updated list, it updates its local state and sends all further requests on the new relays. The remote signer is then free to disconnect from the previous relays.</p>234235 <h3>Ending a session</h3>236 <p>When the user logs out of the client, the client may send a <code>logout</code> request. This tells the remote signer that the session is no longer needed. On receipt, the remote signer should remove the session tied to the requesting <code>client-pubkey</code> and reject any further requests from it. A new <code>connect</code> is then required for a new session. The remote signer should reply with <code>"ack"</code> before it removes the session.</p>237 <p><code>logout</code> is a courtesy hint, not a security boundary. The remote signer must not rely on it being sent. The client must still delete its locally stored <code>client-keypair</code> on logout, whether or not the request was acknowledged.</p>238239 <h2 class="bar" id="responses"><span class="num">5</span> Response Events — <code>kind: 24133</code></h2>240 <p>A response is also a kind 24133 event. Its author is the <code>remote-signer-pubkey</code>. It carries one <code>p</code> tag with the <code>client-pubkey</code>:</p>241 <div class="crt">242 <div class="titlebar">Wire format - response event</div>243<pre>{244 <span class="k">"id"</span>: <span class="c">&lt;id&gt;</span>,245 <span class="k">"kind"</span>: 24133,246 <span class="k">"pubkey"</span>: <span class="c">&lt;remote-signer-pubkey&gt;</span>,247 <span class="k">"content"</span>: <span class="c">&lt;nip44(&lt;response&gt;)&gt;</span>,248 <span class="k">"tags"</span>: [[<span class="s">"p"</span>, <span class="c">&lt;client-pubkey&gt;</span>]],249 <span class="k">"created_at"</span>: <span class="c">&lt;unix timestamp in seconds&gt;</span>250}</pre>251 </div>252 <p>The <code>content</code> field is again a <a href="44.html">NIP-44</a> encrypted JSON-RPC-like message:</p>253 <div class="crt">254 <div class="titlebar">Response payload (before encryption)</div>255<pre>{256 <span class="k">"id"</span>: <span class="c">&lt;request_id&gt;</span>,257 <span class="k">"result"</span>: <span class="c">&lt;results_string&gt;</span>,258 <span class="k">"error"</span>: <span class="c">&lt;optional_error_string&gt;</span>259}</pre>260 </div>261 <ul>262 <li><code>id</code> is the ID of the request that this response answers.</li>263 <li><code>result</code> is a string with the result of the call. It can be a plain string or a JSON-stringified object.</li>264 <li><code>error</code> is optional. It is an error in string form. Its presence indicates an error with the request.</li>265 </ul>266 <p>The remote signer must reply with an error to any request whose method is unknown or unsupported. Silent timeouts are not allowed for such requests.</p>267 <div class="box tech">268 <span class="tag">Tech Note</span>269 <p>Two rough edges in the source. First, the field is named <code>result</code> in the JSON but <code>results</code> in the prose; the wire field is <code>result</code>. Second, <code>result</code> is defined as a string, yet <code>switch_relays</code> returns an array or <code>null</code>; the spec does not say whether that value is JSON-stringified. Implementations should accept both forms.</p>270 </div>271272 <h2 class="bar" id="auth"><span class="num">6</span> Auth Challenges</h2>273 <p>An auth challenge is a special response. The remote signer sends it when it needs the user to authenticate by other means. The response <code>content</code> object takes this form:</p>274 <div class="crt">275 <div class="titlebar">Response payload - auth challenge</div>276<pre>{277 <span class="k">"id"</span>: <span class="c">&lt;request_id&gt;</span>,278 <span class="k">"result"</span>: <span class="s">"auth_url"</span>,279 <span class="k">"error"</span>: <span class="c">&lt;URL_to_display_to_end_user&gt;</span>280}</pre>281 </div>282 <p>The client should display the URL from the <code>error</code> field in a popup or a new tab. Then the client subscribes and listens for another response from the remote signer with the same request ID. The remote signer sends that final response once the user authenticates in the other window. If the user never authenticates, the final response never arrives.</p>283 <div class="box tech">284 <span class="tag">Tech Note</span>285 <p>The auth challenge overloads the <code>error</code> field: it carries the auth URL, not an error. This contradicts §5, where the presence of <code>error</code> &ldquo;indicates an error with the request&rdquo;. Clients must check for <code>result == "auth_url"</code> before they treat <code>error</code> as a failure.</p>286 </div>287288 <figure class="diagram">289 <div class="frame">290 <svg viewBox="0 0 560 260" role="img" aria-label="Sequence diagram of an auth challenge: sign_event request, auth_url response, user authenticates in browser, final signed response with same id">291 <line class="d-wire" x1="110" y1="40" x2="110" y2="245" stroke-dasharray="3 4"/>292 <line class="d-wire" x1="450" y1="40" x2="450" y2="245" stroke-dasharray="3 4"/>293 <rect class="d-actor" x="55" y="8" width="110" height="30" rx="6"/>294 <text class="d-actor-label" x="110" y="28" text-anchor="middle">Client</text>295 <rect class="d-actor" x="395" y="8" width="110" height="30" rx="6"/>296 <text class="d-actor-label" x="450" y="28" text-anchor="middle">Remote signer</text>297 <path class="d-msg" d="M110,62 L445,62"/>298 <text class="d-label" x="280" y="55" text-anchor="middle">{"id":"a1","method":"sign_event",…}</text>299 <path class="d-msg blue" d="M450,98 L115,98"/>300 <text class="d-label" x="280" y="91" text-anchor="middle">{"id":"a1","result":"auth_url","error":"https://…"}</text>301 <text class="d-note" x="280" y="130" text-anchor="middle">client opens the URL - user authenticates in a new window</text>302 <text class="d-note" x="280" y="147" text-anchor="middle">client keeps listening on the same request ID</text>303 <path class="d-msg blue d-flow" d="M450,185 L115,185"/>304 <text class="d-label" x="280" y="178" text-anchor="middle">{"id":"a1","result": json_stringified(signed event)}</text>305 <text class="d-note" x="280" y="222" text-anchor="middle">If the user never authenticates, this message never arrives.</text>306 </svg>307 </div>308 <figcaption>Fig. 46-3 — <b>Auth challenge.</b> The signer parks the request, points the user at a URL, and answers later with the same ID.</figcaption>309 </figure>310311 <h2 class="bar" id="discovery"><span class="num">7</span> Announcing and Discovering Signers</h2>312 <p>A remote signer may publish its metadata with <a href="05.html">NIP-05</a> and <a href="89.html">NIP-89</a>. With NIP-05, a request to <code>&lt;remote-signer&gt;/.well-known/nostr.json?name=_</code> may return:</p>313 <div class="crt">314 <div class="titlebar">nostr.json - signer metadata</div>315<pre>{316 <span class="k">"names"</span>: {317 <span class="k">"_"</span>: <span class="c">&lt;remote-signer-app-pubkey&gt;</span>318 },319 <span class="k">"nip46"</span>: {320 <span class="k">"relays"</span>: [<span class="s">"wss://relay1"</span>, <span class="s">"wss://relay2"</span>, <span class="c">...</span>],321 <span class="k">"nostrconnect_url"</span>: <span class="s">"https://remote-signer-domain.example/&lt;nostrconnect&gt;"</span>322 }323}</pre>324 </div>325 <ul>326 <li>The <code>&lt;remote-signer-app-pubkey&gt;</code> may be used to verify the domain from the signer's NIP-89 event.</li>327 <li><code>relays</code> should be used to build a more precise <code>nostrconnect://</code> string for this signer.</li>328 <li>The <code>nostrconnect_url</code> template may redirect users into the signer's connection flow. Replace the <code>&lt;nostrconnect&gt;</code> placeholder with an actual <code>nostrconnect://</code> string.</li>329 </ul>330 <p>A remote signer may also publish a <a href="89.html">NIP-89</a> <code>kind: 31990</code> event with a <code>k</code> tag of <code>24133</code>. That event may include one or more <code>relay</code> tags and may include a <code>nostrconnect_url</code> tag. These tags carry the same meaning as the fields above.</p>331 <p>A client may improve UX by discovering remote signers through their <code>kind: 31990</code> events. The client may pre-generate <code>nostrconnect://</code> strings for them. In that case the client should verify that the <code>kind: 31990</code> event's author appears as the <code>&lt;remote-signer-app-pubkey&gt;</code> in the signer's <code>nostr.json?name=_</code> file.</p>332333 <h2 class="bar" id="examples"><span class="num">8</span> Worked Example</h2>334 <p>Three keys are in play. The <code>remote-signer-pubkey</code> is <code>fa984bd7dbb2…8018f52</code>. The <code>user-pubkey</code> is the same value in this example — the signer holds the user key directly. The <code>client-pubkey</code> is <code>eff37350d839…531d86</code>. The client asks the bunker to sign a kind-1 note.</p>335 <div class="crt">336 <div class="titlebar">Request event - sign_event</div>337<pre>{338 <span class="k">"kind"</span>: 24133,339 <span class="k">"pubkey"</span>: <span class="s">"eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86"</span>,340 <span class="k">"content"</span>: nip44({341 <span class="k">"id"</span>: <span class="c">&lt;random_string&gt;</span>,342 <span class="k">"method"</span>: <span class="s">"sign_event"</span>,343 <span class="k">"params"</span>: [json_stringified(<span class="c">&lt;{344 content: "Hello, I'm signing remotely",345 kind: 1,346 tags: [],347 created_at: 1714078911348 }&gt;</span>)]349 }),350 <span class="k">"tags"</span>: [[<span class="s">"p"</span>, <span class="s">"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"</span>]]351 <span class="c">// p-tags the remote-signer-pubkey</span>352}</pre>353 </div>354 <p>The remote signer fills in <code>pubkey</code>, <code>id</code>, and <code>sig</code> of the requested event, then answers:</p>355 <div class="crt">356 <div class="titlebar">Response event - signed result</div>357<pre>{358 <span class="k">"kind"</span>: 24133,359 <span class="k">"pubkey"</span>: <span class="s">"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"</span>,360 <span class="k">"content"</span>: nip44({361 <span class="k">"id"</span>: <span class="c">&lt;random_string&gt;</span>,362 <span class="k">"result"</span>: json_stringified(<span class="c">&lt;signed-event&gt;</span>)363 }),364 <span class="k">"tags"</span>: [[<span class="s">"p"</span>, <span class="s">"eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86"</span>]]365 <span class="c">// p-tags the client-pubkey</span>366}</pre>367 </div>368 <p>A full session over the bunker's relay looks like this. Orange lines are decrypted client requests. Blue lines are decrypted signer responses.</p>369 <div class="crt">370 <div class="titlebar">Session transcript - decrypted payloads on wss://relay1.example.com</div>371<pre><span class="c">-- user pasted bunker://fa984bd7…?relay=…&amp;secret=zx81 into the client --</span>372<span class="send">→ {"id":"r1","method":"connect","params":["fa984bd7…","zx81","",373 "{\"name\":\"My Client\",\"url\":\"https://myclient.example\"}"]}</span>374<span class="recv">← {"id":"r1","result":"ack"}</span>375<span class="send">→ {"id":"r2","method":"get_public_key","params":[]}</span>376<span class="recv">← {"id":"r2","result":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"}</span>377<span class="send">→ {"id":"r3","method":"switch_relays","params":[]}</span>378<span class="recv">← {"id":"r3","result":null}</span>379<span class="send">→ {"id":"r4","method":"sign_event","params":["{\"kind\":1,\"content\":\"Hello, I'm signing remotely\",…}"]}</span>380<span class="recv">← {"id":"r4","result":"{\"id\":\"…\",\"pubkey\":\"fa984bd7…\",\"sig\":\"…\",…}"}</span>381<span class="c">-- an unsupported method gets an error, never silence --</span>382<span class="send">→ {"id":"r5","method":"make_coffee","params":[]}</span>383<span class="recv">← {"id":"r5","result":"","error":"unsupported method"}</span>384<span class="c">-- user logs out --</span>385<span class="send">→ {"id":"r6","method":"logout","params":[]}</span>386<span class="recv">← {"id":"r6","result":"ack"}</span></pre>387 </div>388389 <h2 class="bar" id="history"><span class="num">9</span> Version History</h2>390 <p>NIP-46 began life as &ldquo;Nostr Connect&rdquo; and was rewritten twice. The current shape — bunker tokens, split signer and user keys, NIP-44 only — settled between 2024 and 2026. Notable changes, newest first:</p>391 <ul class="patchnotes">392 <li><span class="ver">2026</span><span class="when">2026-07</span>Required an error reply for unknown or unsupported methods; no more silent timeouts.</li>393 <li><span class="ver">2026</span><span class="when">2026-06</span>Added optional client metadata to <code>connect</code> and the <code>logout</code> method for ending sessions.</li>394 <li><span class="ver">2026</span><span class="when">2026-01</span>Added <code>switch_relays</code> so the signer controls the relay set.</li>395 <li><span class="ver">2025</span><span class="when">2025-02</span>Removed the old <code>get_relays</code> method; <code>relay</code> tags joined the <a href="89.html">NIP-89</a> discovery event.</li>396 <li><span class="ver">2024</span><span class="when">2024-12</span>Dropped <a href="04.html">NIP-04</a> for the transport; request and response payloads use <a href="44.html">NIP-44</a> only.</li>397 <li><span class="ver">2024</span><span class="when">2024-10</span>Introduced the <code>remote-signer-key</code> as distinct from the user key; clients must call <code>get_public_key</code> after <code>connect</code>. NIP-05 login was removed and <code>create_account</code> moved to another NIP.</li>398 <li><span class="ver">2024</span><span class="when">2024-02</span>Full rewrite (&ldquo;new version of NIP-46&rdquo;); <code>bunker://</code> tokens replaced the old <code>npub1…#?</code> notation.</li>399 <li class="minor"><span class="ver">2023</span><span class="when">2023-10</span>fiatjaf rewrote the flow around request and response events; NIP-44 calls were added soon after.</li>400 <li><span class="ver">Origin</span><span class="when">2023-02</span>Marco Argentieri proposed &ldquo;Nostr Connect&rdquo;: connect your Nostr app with remote signing devices.</li>401 </ul>402403 <div class="credits">404 <h3 class="cardhead">Original Proposal - Credits</h3>405 <table>406 <tr><td>Author</td><td>Marco Argentieri</td></tr>407 <tr><td>First commit</td><td>2023-02-20 — &ldquo;NIP-46: Nostr Connect 🔌 connect your Nostr app with remote signing devices&rdquo; (<a href="https://github.com/nostr-protocol/nips/pull/153">PR #153</a>)</td></tr>408 <tr><td>Status</td><td>active — widely implemented by signer apps and clients</td></tr>409 <tr><td>Source</td><td><a href="https://github.com/nostr-protocol/nips/blob/master/46.md">46.md on GitHub</a> - <a href="https://github.com/nostr-protocol/nips/commits/master/46.md">full change history</a></td></tr>410 </table>411 </div>412413 <h2 class="bar"><span class="num">10</span> See Also</h2>414 <ul>415 <li><a href="01.html">NIP-01</a> — the base event format, <code>p</code> tags, and the ephemeral kind range that 24133 lives in.</li>416 <li><a href="44.html">NIP-44</a> — the encryption that wraps every request and response payload.</li>417 <li><a href="04.html">NIP-04</a> — legacy encryption still exposed to apps via <code>nip04_encrypt</code> / <code>nip04_decrypt</code>.</li>418 <li><a href="05.html">NIP-05</a> — the <code>nostr.json</code> document a signer uses to announce its relays.</li>419 <li><a href="89.html">NIP-89</a> — <code>kind: 31990</code> handler events used for signer discovery.</li>420 <li><a href="07.html">NIP-07</a> — the browser-extension signer; the local cousin of this remote flow.</li>421 </ul>422 </div>423424 <aside>425 <div class="cartridge">426 <div class="inner">427 <h3 class="cardhead">Spec Card</h3>428 <div class="lights"><span class="led on"></span><span class="led on"></span><span class="led on"></span></div>429 <dl>430 <dt>Status</dt><dd><span class="chip final">active</span></dd>431 <dt>Defines</dt><dd>kind 24133 - bunker:// and nostrconnect:// tokens - 10 RPC methods - auth challenges</dd>432 <dt>Kind(s)</dt><dd>24133 (ephemeral) - uses 31990 for discovery</dd>433 <dt>Requires</dt><dd><a href="01.html">NIP-01</a> - <a href="44.html">NIP-44</a></dd>434 <dt>Transport</dt><dd>relays - encrypted JSON-RPC-like payloads</dd>435 <dt>Crypto</dt><dd>NIP-44 payload encryption - hex pubkeys</dd>436 <dt>First commit</dt><dd>2023-02-20</dd>437 <dt>Last updated</dt><dd>2026-07-15</dd>438 </dl>439 </div>440 </div>441 </aside>442 </main>443444 <nav class="navbtns">445 <a href="45.html">◀ NIP-45: Event Counts</a>446 <a href="47.html">NIP-47: Nostr Wallet Connect (NWC) ▶</a>447 </nav>448449 <footer class="pagefoot">450 <a href="../index.html">Nostr Protocol Manual</a>451 <span class="pageno">NIP 46 / F4</span>452 </footer>453</div>454455<script src="../assets/webmcp.js" defer></script>456</body>457</html>