upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/docs/rendezvous.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/rendezvous.md')
-rw-r--r--docs/rendezvous.md336
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
3This is the specification of how Bitflash nodes reach each other, and how miners
4reach pools, without revealing an IP address and without any central directory.
5It documents the protocol as implemented in `src/btfaddr.cpp`, `src/btfchan.cpp`,
6`src/btfrv.cpp`, `src/btftunnel.cpp` and `src/nostr.cpp`.
7
8The design is ported from the Itzal hidden service design. A `.btf` address names
9a public key, a node proves ownership of that key by signing a service
10descriptor, other nodes find the descriptor over Nostr, and the two sides then
11meet through an untrusted relay over an end to end encrypted tunnel. The relay
12carries ciphertext only and never learns the plaintext.
13
14## 1. Overview
15
16A connection to a `.btf` service has three layers:
17
181. **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.
232. **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.
263. **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.
304. **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
34The caller ends up holding an ordinary local socket that behaves like a direct
35TCP connection to the peer. The node treats it exactly like any other peer
36socket.
37
38## 2. Identities and keys
39
40A 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
49The identity key names the service and authenticates the descriptor. The
50encryption key secures the tunnel. They are separate so the long term identity
51never performs Diffie-Hellman and the encryption key can be rotated in a new
52signed descriptor.
53
54## 3. The `.btf` address
55
56An address is the identity public key plus a short checksum, Base32 encoded, with
57the suffix `.btf`.
58
59```
60label = Base32( pubkey[32] || checksum[2] )
61address = 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
71The address is self certifying: it *is* the public key, so no registry has to be
72trusted to map a name to a key.
73
74## 4. Discovery over Nostr
75
76Nodes publish and read three kinds of parameterized replaceable Nostr events.
77Parameterized replaceable means the newest event for a given author and `d` tag
78supersedes 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
86The Nostr event is signed with the node's identity key, so the event author
87equals the `.btf` public key. That binds the descriptor to the address at the
88Nostr layer, and the descriptor is *also* signed inside its own content (see
89below), so a relay operator cannot forge or swap a descriptor for an address it
90does not control.
91
92**Resolving an address.** To reach `X.btf`, a caller parses the address to the 32
93byte 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
95uses its `meeting_node` and `enc`.
96
97**Relay auto discovery.** A relay operator runs a rendezvous relay and publishes a
98kind 38502 event carrying `host:port`. Every node that reads it adds the relay to
99the set it can register at and dial through. No seed list edit or maintainer
100approval is needed. A small curated seed list is still shipped for bootstrap:
101
102```
10392.246.128.180:8434
10431.44.4.249:8434
10590.156.222.107:8434
106```
107
108The default rendezvous port is `8434`.
109
110## 5. The service descriptor
111
112The descriptor is a JSON object, signed with the identity key over a domain
113separated digest of its own fields.
114
115Legacy 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
128Onion-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
143The legacy signed digest is:
144
145```
146digest = 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
154the rendezvous relay. The onion-capable digest is:
155
156```
157digest3 = 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
166randomness) over their respective digests by the identity key.
167
168**Verification** (untrusted input, must never throw): parse the JSON, require all
169fields with the right types, confirm `pubkey` equals the address being resolved,
170recompute the digest, and verify the Schnorr signature. Any failure rejects the
171descriptor.
172
173If `onion` is present, readers accept it only when it is a normalized v3 onion
174endpoint and `sig3` verifies. A bad or tampered onion field is ignored; the v2
175descriptor 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
178seen. A node without a direct onion endpoint only publishes a descriptor once it
179has actually registered at a `meeting_node`, so it never advertises a relay it
180cannot be reached at. A node with a direct onion endpoint may publish before
181rendezvous registration using `meeting_node = "rendezvous-pending"`; Tor peers
182can still dial the signed onion endpoint, and rendezvous becomes fallback once
183registration succeeds.
184
185## 6. Rendezvous transport
186
187The relay is a small TCP server. Every connection opens with a fixed 33 byte
188header:
189
190```
191byte 0 role: 'S' (service) or 'C' (client)
192bytes 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
197public key and leaves it open, waiting. There is one slot per key: a new
198registration for the same key replaces and closes the old one. The service does
199not block waiting to be paired at registration time; it is considered registered
200the moment the relay reads the header, which is what lets it publish a descriptor
201pointing 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
211Both sides connect *outbound* to the relay, so neither needs a public IP or an
212open inbound port. The relay never sees the identity of the caller (the caller
213sends 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
216path that created it (a NAT drops the idle mapping, the relay restarts, a route
217changes) with no FIN and nothing readable. TCP keepalive was measured and does
218not reliably detect this. The service therefore imposes its own timeout on the
219wait for the pairing byte and re-registers on expiry. Expiring is not a failure;
220it costs one reconnect per interval and returns the loop to a known state.
221
222## 7. The encrypted channel
223
224Once paired through the relay, the caller and service run an end to end handshake
225and then exchange authenticated frames. The primitive is libsodium
226`crypto_box_curve25519xchacha20poly1305` (X25519 key agreement, XChaCha20-Poly1305
227AEAD), the same construction as Rust's `crypto_box` ChaChaBox.
228
229**Handshake (ephemeral to static).**
230
2311. 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.
2352. The service reads the ephemeral public key and computes the same
236 `shared = X25519(enc_sk, eph_pub)`.
237
238Because the caller uses a new ephemeral key per connection, each session has an
239independent key and forward secrecy against later compromise of the service
240encryption key.
241
242**Framing.** Every message is length prefixed:
243
244```
245frame = len[4] (big endian, length of the body that follows)
246 body = nonce[24] || ciphertext || mac[16]
247```
248
249The 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.
251Decryption that fails authentication drops the frame and tears the tunnel down.
252
253The tunnel is transparent: the application is handed a local socket pair end and
254reads and writes plaintext. Two pump threads move data between that socket and
255the relay socket, encrypting outbound and decrypting inbound.
256
257## 8. End to end connection flow
258
259To dial `X.btf`:
260
2611. Parse `X.btf` to the 32 byte identity key.
2622. Fetch and verify the descriptor over Nostr (kind 38501). Obtain
263 `meeting_node` and `enc`.
2643. Connect to `meeting_node`, send `'C' || X`, and wait for `0x01`.
2654. Generate an ephemeral X25519 key, derive the shared key against `enc`, send
266 the ephemeral public key.
2675. Exchange XChaCha20-Poly1305 frames. Hand the application a local socket that
268 speaks the ordinary peer protocol.
269
270A service, symmetrically: registers at a relay (`'S' || own_pubkey`), publishes a
271descriptor pointing at that relay, waits for `0x01`, reads the caller ephemeral
272public key, derives the shared key with its encryption secret, and serves the
273tunnel.
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 |