diff options
Diffstat (limited to 'docs/rendezvous.md')
| -rw-r--r-- | docs/rendezvous.md | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/docs/rendezvous.md b/docs/rendezvous.md new file mode 100644 index 0000000..68b5df3 --- /dev/null +++ b/docs/rendezvous.md | |||
| @@ -0,0 +1,336 @@ | |||
| 1 | # Bitflash `.btf` Rendezvous Protocol | ||
| 2 | |||
| 3 | This is the specification of how Bitflash nodes reach each other, and how miners | ||
| 4 | reach pools, without revealing an IP address and without any central directory. | ||
| 5 | It documents the protocol as implemented in `src/btfaddr.cpp`, `src/btfchan.cpp`, | ||
| 6 | `src/btfrv.cpp`, `src/btftunnel.cpp` and `src/nostr.cpp`. | ||
| 7 | |||
| 8 | The design is ported from the Itzal hidden service design. A `.btf` address names | ||
| 9 | a public key, a node proves ownership of that key by signing a service | ||
| 10 | descriptor, other nodes find the descriptor over Nostr, and the two sides then | ||
| 11 | meet through an untrusted relay over an end to end encrypted tunnel. The relay | ||
| 12 | carries ciphertext only and never learns the plaintext. | ||
| 13 | |||
| 14 | ## 1. Overview | ||
| 15 | |||
| 16 | A connection to a `.btf` service has three layers: | ||
| 17 | |||
| 18 | 1. **Discovery (Nostr).** The service publishes a signed descriptor as a Nostr | ||
| 19 | event. A caller fetches it by the service public key and verifies the | ||
| 20 | signature. The descriptor says which rendezvous relay the service is waiting | ||
| 21 | at, which encryption key to use, and optionally which Tor hidden service can | ||
| 22 | be dialed directly. | ||
| 23 | 2. **Rendezvous (TCP).** Caller and service both connect out to the same relay | ||
| 24 | and are paired by public key. Neither needs an inbound port or a public IP. | ||
| 25 | After pairing the relay is a blind byte pipe. | ||
| 26 | 3. **Direct onion fallback bypass.** If the descriptor carries a valid signed | ||
| 27 | `.onion:port` endpoint and the caller is running with Tor mode, the caller | ||
| 28 | tries that endpoint before using rendezvous. This removes the rendezvous | ||
| 29 | relay from the data path for peers that choose to run hidden services. | ||
| 30 | 4. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the | ||
| 31 | caller and service run an ephemeral to static handshake and then exchange | ||
| 32 | authenticated frames. The relay sees only ciphertext. | ||
| 33 | |||
| 34 | The caller ends up holding an ordinary local socket that behaves like a direct | ||
| 35 | TCP connection to the peer. The node treats it exactly like any other peer | ||
| 36 | socket. | ||
| 37 | |||
| 38 | ## 2. Identities and keys | ||
| 39 | |||
| 40 | A node holds two independent keypairs. | ||
| 41 | |||
| 42 | - **Signing / identity key**: a secp256k1 x-only key (BIP340 Schnorr). This is | ||
| 43 | the `.btf` identity and also the node's Nostr identity. Persisted in | ||
| 44 | `nostr.key`. | ||
| 45 | - **Encryption key**: an X25519 (Curve25519) keypair. Persisted in | ||
| 46 | `btf_enc.key`. Its public half is published in the descriptor as `enc` and is | ||
| 47 | used for the tunnel key agreement. | ||
| 48 | |||
| 49 | The identity key names the service and authenticates the descriptor. The | ||
| 50 | encryption key secures the tunnel. They are separate so the long term identity | ||
| 51 | never performs Diffie-Hellman and the encryption key can be rotated in a new | ||
| 52 | signed descriptor. | ||
| 53 | |||
| 54 | ## 3. The `.btf` address | ||
| 55 | |||
| 56 | An address is the identity public key plus a short checksum, Base32 encoded, with | ||
| 57 | the suffix `.btf`. | ||
| 58 | |||
| 59 | ``` | ||
| 60 | label = Base32( pubkey[32] || checksum[2] ) | ||
| 61 | address = label + ".btf" | ||
| 62 | ``` | ||
| 63 | |||
| 64 | - `pubkey` is the 32 byte secp256k1 x-only public key. | ||
| 65 | - `checksum` is the first two bytes of `SHA256(".btf-checksum-v1" || pubkey)`. | ||
| 66 | - Base32 uses the lowercase alphabet `abcdefghijklmnopqrstuvwxyz234567`, no | ||
| 67 | padding. The 34 byte payload encodes to 55 characters. | ||
| 68 | - Parsing is case insensitive, trims surrounding whitespace, and strips a | ||
| 69 | trailing dot. A wrong checksum is rejected. | ||
| 70 | |||
| 71 | The address is self certifying: it *is* the public key, so no registry has to be | ||
| 72 | trusted to map a name to a key. | ||
| 73 | |||
| 74 | ## 4. Discovery over Nostr | ||
| 75 | |||
| 76 | Nodes publish and read three kinds of parameterized replaceable Nostr events. | ||
| 77 | Parameterized replaceable means the newest event for a given author and `d` tag | ||
| 78 | supersedes the older one, so a descriptor is always the latest state. | ||
| 79 | |||
| 80 | | Purpose | kind | `d` tag | | ||
| 81 | |---|---|---| | ||
| 82 | | Service descriptor | 38501 | `btf-descriptor-2` | | ||
| 83 | | Rendezvous relay announcement | 38502 | `btf-relay-2` | | ||
| 84 | | Pool announcement | 38503 | `btf-pool-2` | | ||
| 85 | |||
| 86 | The Nostr event is signed with the node's identity key, so the event author | ||
| 87 | equals the `.btf` public key. That binds the descriptor to the address at the | ||
| 88 | Nostr layer, and the descriptor is *also* signed inside its own content (see | ||
| 89 | below), so a relay operator cannot forge or swap a descriptor for an address it | ||
| 90 | does not control. | ||
| 91 | |||
| 92 | **Resolving an address.** To reach `X.btf`, a caller parses the address to the 32 | ||
| 93 | byte key and sends a Nostr `REQ` filtered by `authors = [X]`, `kinds = [38501]`, | ||
| 94 | `#d = ["btf-descriptor-2"]`, `limit = 1`. It verifies the returned descriptor and | ||
| 95 | uses its `meeting_node` and `enc`. | ||
| 96 | |||
| 97 | **Relay auto discovery.** A relay operator runs a rendezvous relay and publishes a | ||
| 98 | kind 38502 event carrying `host:port`. Every node that reads it adds the relay to | ||
| 99 | the set it can register at and dial through. No seed list edit or maintainer | ||
| 100 | approval is needed. A small curated seed list is still shipped for bootstrap: | ||
| 101 | |||
| 102 | ``` | ||
| 103 | 92.246.128.180:8434 | ||
| 104 | 31.44.4.249:8434 | ||
| 105 | 90.156.222.107:8434 | ||
| 106 | ``` | ||
| 107 | |||
| 108 | The default rendezvous port is `8434`. | ||
| 109 | |||
| 110 | ## 5. The service descriptor | ||
| 111 | |||
| 112 | The descriptor is a JSON object, signed with the identity key over a domain | ||
| 113 | separated digest of its own fields. | ||
| 114 | |||
| 115 | Legacy descriptor: | ||
| 116 | |||
| 117 | ```json | ||
| 118 | { | ||
| 119 | "v": 2, | ||
| 120 | "pubkey": "<hex 32-byte x-only identity key>", | ||
| 121 | "enc": "<hex 32-byte X25519 public key>", | ||
| 122 | "meeting_node": "host:port", | ||
| 123 | "created": <unix seconds>, | ||
| 124 | "sig": "<hex 64-byte BIP340 Schnorr signature>" | ||
| 125 | } | ||
| 126 | ``` | ||
| 127 | |||
| 128 | Onion-capable descriptor: | ||
| 129 | |||
| 130 | ```json | ||
| 131 | { | ||
| 132 | "v": 3, | ||
| 133 | "pubkey": "<hex 32-byte x-only identity key>", | ||
| 134 | "enc": "<hex 32-byte X25519 public key>", | ||
| 135 | "meeting_node": "host:port", | ||
| 136 | "onion": "<56-char-v3-name>.onion:8433", | ||
| 137 | "created": <unix seconds>, | ||
| 138 | "sig": "<legacy v2 signature>", | ||
| 139 | "sig3": "<signature over the onion-capable digest>" | ||
| 140 | } | ||
| 141 | ``` | ||
| 142 | |||
| 143 | The legacy signed digest is: | ||
| 144 | |||
| 145 | ``` | ||
| 146 | digest = SHA256( ".btf-descriptor-v2" | ||
| 147 | || pubkey[32] | ||
| 148 | || enc (raw bytes of the string) | ||
| 149 | || meeting_node (raw bytes of the string) | ||
| 150 | || created (8 bytes, big endian) ) | ||
| 151 | ``` | ||
| 152 | |||
| 153 | `sig` is kept on v3 descriptors so old nodes can still resolve and dial through | ||
| 154 | the rendezvous relay. The onion-capable digest is: | ||
| 155 | |||
| 156 | ``` | ||
| 157 | digest3 = SHA256( ".btf-descriptor-v3-onion" | ||
| 158 | || pubkey[32] | ||
| 159 | || enc (raw bytes of the string) | ||
| 160 | || meeting_node (raw bytes of the string) | ||
| 161 | || onion (raw bytes of the normalized string) | ||
| 162 | || created (8 bytes, big endian) ) | ||
| 163 | ``` | ||
| 164 | |||
| 165 | `sig` and `sig3` are deterministic BIP340 Schnorr signatures (no auxiliary | ||
| 166 | randomness) over their respective digests by the identity key. | ||
| 167 | |||
| 168 | **Verification** (untrusted input, must never throw): parse the JSON, require all | ||
| 169 | fields with the right types, confirm `pubkey` equals the address being resolved, | ||
| 170 | recompute the digest, and verify the Schnorr signature. Any failure rejects the | ||
| 171 | descriptor. | ||
| 172 | |||
| 173 | If `onion` is present, readers accept it only when it is a normalized v3 onion | ||
| 174 | endpoint and `sig3` verifies. A bad or tampered onion field is ignored; the v2 | ||
| 175 | descriptor can still be used for rendezvous fallback if `sig` verifies. | ||
| 176 | |||
| 177 | `created` lets a reader prefer the freshest descriptor when more than one is | ||
| 178 | seen. A node without a direct onion endpoint only publishes a descriptor once it | ||
| 179 | has actually registered at a `meeting_node`, so it never advertises a relay it | ||
| 180 | cannot be reached at. A node with a direct onion endpoint may publish before | ||
| 181 | rendezvous registration using `meeting_node = "rendezvous-pending"`; Tor peers | ||
| 182 | can still dial the signed onion endpoint, and rendezvous becomes fallback once | ||
| 183 | registration succeeds. | ||
| 184 | |||
| 185 | ## 6. Rendezvous transport | ||
| 186 | |||
| 187 | The relay is a small TCP server. Every connection opens with a fixed 33 byte | ||
| 188 | header: | ||
| 189 | |||
| 190 | ``` | ||
| 191 | byte 0 role: 'S' (service) or 'C' (client) | ||
| 192 | bytes 1..32 target public key (the .btf identity, 32 bytes) | ||
| 193 | ``` | ||
| 194 | |||
| 195 | **Service registration (`S`).** The service connects out to the relay and sends | ||
| 196 | `'S' || own_pubkey`. The relay stores the connection in a table keyed by the | ||
| 197 | public key and leaves it open, waiting. There is one slot per key: a new | ||
| 198 | registration for the same key replaces and closes the old one. The service does | ||
| 199 | not block waiting to be paired at registration time; it is considered registered | ||
| 200 | the moment the relay reads the header, which is what lets it publish a descriptor | ||
| 201 | pointing at this relay. | ||
| 202 | |||
| 203 | **Client dial (`C`).** The caller connects out to the same relay and sends | ||
| 204 | `'C' || target_pubkey`. The relay looks up the waiting service: | ||
| 205 | |||
| 206 | - If present, it removes it from the table, writes a single `0x01` byte to both | ||
| 207 | sockets, and then forwards raw bytes in both directions until either side | ||
| 208 | closes. From this point the relay is a blind pipe. | ||
| 209 | - If absent, it writes `0x00` to the caller and closes. | ||
| 210 | |||
| 211 | Both sides connect *outbound* to the relay, so neither needs a public IP or an | ||
| 212 | open inbound port. The relay never sees the identity of the caller (the caller | ||
| 213 | sends no key of its own, only the target it wants). | ||
| 214 | |||
| 215 | **Liveness.** A registration socket is idle by design and can outlive the network | ||
| 216 | path that created it (a NAT drops the idle mapping, the relay restarts, a route | ||
| 217 | changes) with no FIN and nothing readable. TCP keepalive was measured and does | ||
| 218 | not reliably detect this. The service therefore imposes its own timeout on the | ||
| 219 | wait for the pairing byte and re-registers on expiry. Expiring is not a failure; | ||
| 220 | it costs one reconnect per interval and returns the loop to a known state. | ||
| 221 | |||
| 222 | ## 7. The encrypted channel | ||
| 223 | |||
| 224 | Once paired through the relay, the caller and service run an end to end handshake | ||
| 225 | and then exchange authenticated frames. The primitive is libsodium | ||
| 226 | `crypto_box_curve25519xchacha20poly1305` (X25519 key agreement, XChaCha20-Poly1305 | ||
| 227 | AEAD), the same construction as Rust's `crypto_box` ChaChaBox. | ||
| 228 | |||
| 229 | **Handshake (ephemeral to static).** | ||
| 230 | |||
| 231 | 1. The caller generates a fresh ephemeral X25519 keypair, computes | ||
| 232 | `shared = X25519(eph_sk, service_enc_pub)` reduced to a box key | ||
| 233 | (`crypto_box_beforenm`), and sends the 32 byte ephemeral public key over the | ||
| 234 | paired pipe. | ||
| 235 | 2. The service reads the ephemeral public key and computes the same | ||
| 236 | `shared = X25519(enc_sk, eph_pub)`. | ||
| 237 | |||
| 238 | Because the caller uses a new ephemeral key per connection, each session has an | ||
| 239 | independent key and forward secrecy against later compromise of the service | ||
| 240 | encryption key. | ||
| 241 | |||
| 242 | **Framing.** Every message is length prefixed: | ||
| 243 | |||
| 244 | ``` | ||
| 245 | frame = len[4] (big endian, length of the body that follows) | ||
| 246 | body = nonce[24] || ciphertext || mac[16] | ||
| 247 | ``` | ||
| 248 | |||
| 249 | The nonce is 24 random bytes per frame (XChaCha20), the MAC is 16 bytes | ||
| 250 | (Poly1305). A body shorter than `nonce + mac` or larger than 1 MiB is rejected. | ||
| 251 | Decryption that fails authentication drops the frame and tears the tunnel down. | ||
| 252 | |||
| 253 | The tunnel is transparent: the application is handed a local socket pair end and | ||
| 254 | reads and writes plaintext. Two pump threads move data between that socket and | ||
| 255 | the relay socket, encrypting outbound and decrypting inbound. | ||
| 256 | |||
| 257 | ## 8. End to end connection flow | ||
| 258 | |||
| 259 | To dial `X.btf`: | ||
| 260 | |||
| 261 | 1. Parse `X.btf` to the 32 byte identity key. | ||
| 262 | 2. Fetch and verify the descriptor over Nostr (kind 38501). Obtain | ||
| 263 | `meeting_node` and `enc`. | ||
| 264 | 3. Connect to `meeting_node`, send `'C' || X`, and wait for `0x01`. | ||
| 265 | 4. Generate an ephemeral X25519 key, derive the shared key against `enc`, send | ||
| 266 | the ephemeral public key. | ||
| 267 | 5. Exchange XChaCha20-Poly1305 frames. Hand the application a local socket that | ||
| 268 | speaks the ordinary peer protocol. | ||
| 269 | |||
| 270 | A service, symmetrically: registers at a relay (`'S' || own_pubkey`), publishes a | ||
| 271 | descriptor pointing at that relay, waits for `0x01`, reads the caller ephemeral | ||
| 272 | public key, derives the shared key with its encryption secret, and serves the | ||
| 273 | tunnel. | ||
| 274 | |||
| 275 | ## 9. Security properties | ||
| 276 | |||
| 277 | - **Address unforgeability.** The address is the identity key and the descriptor | ||
| 278 | is Schnorr signed by it, and the Nostr event is signed by it. A relay or Nostr | ||
| 279 | operator cannot publish a working descriptor for an address it does not own. | ||
| 280 | - **Relay confidentiality.** The relay forwards ciphertext and never holds the | ||
| 281 | channel key. It cannot read or modify traffic undetected; a tampered frame | ||
| 282 | fails the Poly1305 tag and the tunnel closes. | ||
| 283 | - **Caller anonymity to the service and relay.** The caller presents no identity | ||
| 284 | key, only the target it wants to reach. It connects outbound, so its address is | ||
| 285 | not published anywhere. | ||
| 286 | - **Forward secrecy.** The caller's per connection ephemeral key means a later | ||
| 287 | compromise of the service encryption key does not decrypt past sessions. | ||
| 288 | - **IP privacy.** Neither side needs a public IP or an inbound port for | ||
| 289 | rendezvous. With a signed `.onion` endpoint, the reachable side exposes only a | ||
| 290 | Tor hidden service and the relay is not in the peer connection path. | ||
| 291 | |||
| 292 | ## 10. Threat model and limitations | ||
| 293 | |||
| 294 | - **The relay is trusted for availability, not for confidentiality.** It can | ||
| 295 | refuse to pair, drop a connection, or go offline. This is why relays are | ||
| 296 | plural, auto discovered, and re-registration is periodic. It cannot read | ||
| 297 | traffic. | ||
| 298 | - **Traffic analysis is not addressed.** A network observer, or the relay, can | ||
| 299 | see connection timing and byte volumes. There is no padding or mixing. | ||
| 300 | - **Single slot registration causes churn.** One waiting slot per key per relay | ||
| 301 | means a race or a stale registration can briefly make a service unreachable at | ||
| 302 | that relay. Multiple relays and the wait timeout bound the damage, but this is | ||
| 303 | a known source of connection churn. | ||
| 304 | - **Descriptor freshness is best effort.** `created` is a self reported | ||
| 305 | timestamp; readers prefer the newest descriptor they have seen but there is no | ||
| 306 | global clock. | ||
| 307 | - **Discovery depends on public Nostr relays.** If the relays a node uses are all | ||
| 308 | unreachable, discovery stalls until one is reachable again. Relay TLS is | ||
| 309 | verified, which closes a man in the middle path on discovery, but availability | ||
| 310 | still rests on the relay set. | ||
| 311 | - **Direct onion needs Tor.** With `-managedtor`, Bitflash starts a Tor process, | ||
| 312 | writes a local hidden-service config, and advertises the generated onion | ||
| 313 | endpoint. With `-onionservice`, an operator can still run Tor manually and | ||
| 314 | provide the endpoint. In both cases Tor owns the hidden-service private key and | ||
| 315 | uptime. | ||
| 316 | |||
| 317 | ## 11. Wire reference | ||
| 318 | |||
| 319 | | Item | Value | | ||
| 320 | |---|---| | ||
| 321 | | `.btf` Base32 alphabet | `abcdefghijklmnopqrstuvwxyz234567`, no padding | | ||
| 322 | | Address payload | `pubkey[32] || checksum[2]`, 55 Base32 chars | | ||
| 323 | | Address checksum | `SHA256(".btf-checksum-v1" || pubkey)[0..2]` | | ||
| 324 | | Descriptor domain sep | `.btf-descriptor-v2`; optional onion sep `.btf-descriptor-v3-onion` | | ||
| 325 | | Descriptor version | `2`; `3` when a signed onion endpoint is present | | ||
| 326 | | Descriptor signature | BIP340 Schnorr, deterministic (no aux rand), v3 keeps `sig` plus `sig3` | | ||
| 327 | | Nostr descriptor event | kind `38501`, `d = btf-descriptor-2` | | ||
| 328 | | Nostr relay event | kind `38502`, `d = btf-relay-2` | | ||
| 329 | | Nostr pool event | kind `38503`, `d = btf-pool-2` | | ||
| 330 | | Rendezvous port (default) | `8434` | | ||
| 331 | | Rendezvous header | `role[1] ('S'/'C') || pubkey[32]`, 33 bytes | | ||
| 332 | | Rendezvous pairing byte | `0x01` paired, `0x00` no service | | ||
| 333 | | Channel primitive | `crypto_box_curve25519xchacha20poly1305` | | ||
| 334 | | Nonce / MAC / key sizes | 24 / 16 / 32 bytes | | ||
| 335 | | Frame | `len[4 big endian] || nonce[24] || ct || mac[16]` | | ||
| 336 | | Max frame body | 1 MiB | | ||