|
| 1 | +# Handshake and Rekeying Protocol |
| 2 | + |
| 3 | +**Status:** Living document |
| 4 | +**Last updated:** 2026-02-17 |
| 5 | + |
| 6 | +## Overview |
| 7 | + |
| 8 | +TunGo uses a **Noise IK** handshake for mutual authentication and key agreement, followed by periodic **X25519 + HKDF-SHA256** rekeying. Transport encryption uses **ChaCha20-Poly1305** AEAD with epoch-based nonce management. |
| 9 | + |
| 10 | +**Cipher suite:** X25519 / ChaChaPoly / SHA-256 |
| 11 | +**Protocol ID:** `"TunGo"`, version `0x01` |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## 1. Handshake (Noise IK) |
| 16 | + |
| 17 | +Noise IK assumes the initiator (client) already knows the responder's (server) static public key. |
| 18 | + |
| 19 | +### 1.1 Message Flow |
| 20 | + |
| 21 | +``` |
| 22 | +Client Server |
| 23 | + │ │ |
| 24 | + │─── MSG1: (e, es, s, ss) + MAC1 + MAC2 ───────>│ |
| 25 | + │ │ |
| 26 | + │<── COOKIE REPLY (optional, under load) ───────│ |
| 27 | + │ │ |
| 28 | + │─── MSG1 (retry with cookie) ─────────────────>│ |
| 29 | + │ │ |
| 30 | + │<── MSG2: (e, ee, se) ─────────────────────────│ |
| 31 | + │ │ |
| 32 | + ├═══ Transport keys established ════════════════╡ |
| 33 | +``` |
| 34 | + |
| 35 | +### 1.2 MSG1 (Client -> Server) |
| 36 | + |
| 37 | +Wire format: |
| 38 | + |
| 39 | +``` |
| 40 | +[1B version] [>=80B noise_payload] [16B MAC1] [16B MAC2] |
| 41 | +``` |
| 42 | + |
| 43 | +- **version:** `0x01` |
| 44 | +- **noise_payload:** Noise IK first message — client ephemeral public (32B, plaintext) + encrypted client static (48B) |
| 45 | +- **MAC1:** Stateless authentication (always verified) |
| 46 | +- **MAC2:** Cookie-based authentication (verified only under load) |
| 47 | + |
| 48 | +Minimum size: 113 bytes. |
| 49 | + |
| 50 | +### 1.3 MSG2 (Server -> Client) |
| 51 | + |
| 52 | +Noise IK second message. No MACs — bidirectional authentication is implicit after Noise completes. |
| 53 | + |
| 54 | +After MSG2, both sides derive: |
| 55 | +- `c2sKey` (32 bytes) — client-to-server transport key |
| 56 | +- `s2cKey` (32 bytes) — server-to-client transport key |
| 57 | +- `sessionId` (32 bytes) — from Noise channel binding |
| 58 | + |
| 59 | +### 1.4 Server Verification Order |
| 60 | + |
| 61 | +``` |
| 62 | +1. CheckVersion() — reject unknown protocol versions |
| 63 | +2. VerifyMAC1() — stateless, before any DH or allocation |
| 64 | +3. VerifyMAC2() — only under load (LoadMonitor) |
| 65 | +4. Noise handshake — DH computations, peer lookup |
| 66 | +5. Peer ACL check — AllowedPeers / PeerDisabled |
| 67 | +``` |
| 68 | + |
| 69 | +All failures return a uniform `ErrHandshakeFailed` to prevent information leakage. |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +## 2. DoS Protection |
| 74 | + |
| 75 | +### 2.1 MAC1 (Stateless, Always Required) |
| 76 | + |
| 77 | +``` |
| 78 | +key = BLAKE2s-256("mac1" || "TunGo" || 0x01 || server_pubkey) |
| 79 | +MAC1 = BLAKE2s-128(key, noise_msg1) |
| 80 | +``` |
| 81 | + |
| 82 | +Verified before any state allocation or DH computation. |
| 83 | + |
| 84 | +### 2.2 MAC2 (Stateful, Under Load) |
| 85 | + |
| 86 | +``` |
| 87 | +key = BLAKE2s-256("mac2" || "TunGo" || 0x01 || cookie_value) |
| 88 | +MAC2 = BLAKE2s-128(key, noise_msg1 || MAC1) |
| 89 | +``` |
| 90 | + |
| 91 | +Checked only when `LoadMonitor` detects pressure. |
| 92 | + |
| 93 | +### 2.3 Cookie Mechanism |
| 94 | + |
| 95 | +**Cookie value** (IP-bound, time-bucketed): |
| 96 | +``` |
| 97 | +bucket = unix_seconds / 120 |
| 98 | +cookie = BLAKE2s-128(server_secret[32], client_ip[16] || bucket[2]) |
| 99 | +``` |
| 100 | + |
| 101 | +Valid for current and previous bucket (handles transitions). |
| 102 | + |
| 103 | +**Cookie reply** (encrypted, 56 bytes): |
| 104 | +``` |
| 105 | +[24B nonce] [16B encrypted_cookie] [16B poly1305_tag] |
| 106 | +``` |
| 107 | + |
| 108 | +Encryption: |
| 109 | +``` |
| 110 | +key = BLAKE2s-256("cookie" || "TunGo" || 0x01 || server_pubkey || client_ephemeral) |
| 111 | +ciphertext = XChaCha20-Poly1305.Seal(key, nonce, cookie, aad=client_ephemeral) |
| 112 | +``` |
| 113 | + |
| 114 | +--- |
| 115 | + |
| 116 | +## 3. Transport Encryption |
| 117 | + |
| 118 | +### 3.1 AEAD |
| 119 | + |
| 120 | +ChaCha20-Poly1305 with 60-byte AAD: |
| 121 | + |
| 122 | +``` |
| 123 | +AAD [60 bytes]: |
| 124 | + [ 0..31] sessionId (32 bytes) |
| 125 | + [32..47] direction (16 bytes: "client-to-server" or "server-to-client") |
| 126 | + [48..59] nonce (12 bytes) |
| 127 | +``` |
| 128 | + |
| 129 | +SessionId and direction are pre-filled at session creation. Only the nonce is updated per packet. |
| 130 | + |
| 131 | +### 3.2 Nonce Structure (12 bytes) |
| 132 | + |
| 133 | +``` |
| 134 | +[0..7] counterLow (uint64, big-endian) |
| 135 | +[8..9] counterHigh (uint16, big-endian) |
| 136 | +[10..11] epoch (uint16, big-endian) |
| 137 | +``` |
| 138 | + |
| 139 | +- **Counter:** 80-bit monotonic (2^80 messages per epoch). Overflow returns error. |
| 140 | +- **Epoch:** Immutable per session, identifies rekeying generation. |
| 141 | + |
| 142 | +### 3.3 TCP Transport |
| 143 | + |
| 144 | +``` |
| 145 | +Wire frame: [2B epoch] [ciphertext + 16B tag] |
| 146 | +``` |
| 147 | + |
| 148 | +- Dual-epoch: current + previous session coexist during rekey. |
| 149 | +- Auto-cleanup: previous session zeroed on first current-epoch decryption (TCP ordering guarantee). |
| 150 | +- No replay protection (TCP provides ordering). |
| 151 | + |
| 152 | +### 3.4 UDP Transport |
| 153 | + |
| 154 | +``` |
| 155 | +Wire frame: [8B route-id] [12B nonce] [ciphertext + 16B tag] |
| 156 | +``` |
| 157 | + |
| 158 | +- Route-id is derived from `sessionId` (first 8 bytes, big-endian) and enables O(1) session lookup. |
| 159 | +- Epoch embedded in nonce bytes 10..11. |
| 160 | +- **Replay protection:** 1024-bit sliding window bitmap per epoch. |
| 161 | + - Tentative check before decryption (Check). |
| 162 | + - Committed only after AEAD authentication succeeds (Accept). |
| 163 | + - Prevents window poisoning by invalid packets. |
| 164 | +- **Epoch ring:** Fixed-capacity FIFO of sessions. Evicted sessions are zeroed. |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +## 4. Rekeying |
| 169 | + |
| 170 | +### 4.1 Key Derivation |
| 171 | + |
| 172 | +Both sides perform X25519 ECDH, then derive new transport keys via HKDF-SHA256: |
| 173 | + |
| 174 | +``` |
| 175 | +shared = X25519(local_private, remote_public) |
| 176 | +newC2S = HKDF-SHA256(ikm=shared, salt=currentC2S, info="tungo-rekey-c2s") |
| 177 | +newS2C = HKDF-SHA256(ikm=shared, salt=currentS2C, info="tungo-rekey-s2c") |
| 178 | +``` |
| 179 | + |
| 180 | +Current keys serve as HKDF salt, providing forward secrecy chaining. |
| 181 | + |
| 182 | +### 4.2 Control Plane Packets |
| 183 | + |
| 184 | +``` |
| 185 | +RekeyInit: [0xFF] [0x01] [0x02] [32B X25519 public key] (35 bytes) |
| 186 | +RekeyAck: [0xFF] [0x01] [0x03] [32B X25519 public key] (35 bytes) |
| 187 | +``` |
| 188 | + |
| 189 | +### 4.3 Rekey FSM |
| 190 | + |
| 191 | +``` |
| 192 | + StartRekey installPending |
| 193 | +Stable ──────────> Rekeying ──────────────> Pending |
| 194 | + ^ │ |
| 195 | + │ ActivateSendEpoch │ |
| 196 | + └───────────────────────────────────────────┘ |
| 197 | + ^ │ |
| 198 | + │ AbortPendingIfExpired (5s) │ |
| 199 | + └───────────────────────────────────────────┘ |
| 200 | +``` |
| 201 | + |
| 202 | +| State | Description | |
| 203 | +|-------|-------------| |
| 204 | +| **Stable** | Normal operation. One active send epoch. | |
| 205 | +| **Rekeying** | StartRekey called, new keys computed, new epoch installed for receive. | |
| 206 | +| **Pending** | Awaiting peer confirmation (first successful decryption with new epoch). | |
| 207 | + |
| 208 | +### 4.4 Rekey Flow |
| 209 | + |
| 210 | +``` |
| 211 | +Client Server |
| 212 | + │ │ |
| 213 | + │── RekeyInit (client X25519 pub) ────────>│ |
| 214 | + │ │ derive newC2S, newS2C |
| 215 | + │ │ install new epoch (recv) |
| 216 | + │<── RekeyAck (server X25519 pub) ─────────│ |
| 217 | + │ │ |
| 218 | + │ derive newC2S, newS2C │ |
| 219 | + │ install new epoch (recv + send) │ |
| 220 | + │ │ |
| 221 | + │── first packet with new epoch ──────────>│ |
| 222 | + │ │ peer confirmed → activate send |
| 223 | + │<── first packet with new epoch ──────────│ |
| 224 | + │ │ |
| 225 | + ├═══ Both sides on new epoch ══════════════╡ |
| 226 | +``` |
| 227 | + |
| 228 | +### 4.5 Safety Invariants |
| 229 | + |
| 230 | +- Only one in-flight rekey at a time. |
| 231 | +- Epochs monotonically increase. Max safe epoch: 65000 (of 65535). Beyond this, `ErrEpochExhausted` forces re-handshake. |
| 232 | +- Send epoch never decreases. |
| 233 | +- Pending keys never overwrite active keys until peer proves possession (via successful decryption). |
| 234 | +- Pending rekey auto-aborts after 5 seconds if no peer confirmation. |
| 235 | +- Default rekey interval: 120 seconds. |
| 236 | + |
| 237 | +--- |
| 238 | + |
| 239 | +## 5. Key Zeroization |
| 240 | + |
| 241 | +| Material | When Zeroed | |
| 242 | +|----------|-------------| |
| 243 | +| Ephemeral DH private keys | Immediately after DH computation (`defer mem.ZeroBytes`) | |
| 244 | +| Shared secrets (rekey) | Immediately after key derivation (`defer mem.ZeroBytes`) | |
| 245 | +| Pending rekey keys (FSM) | On abort or promotion to active | |
| 246 | +| Previous session keys | On first current-epoch decryption (TCP) or epoch eviction (UDP) | |
| 247 | +| Nonce replay window | On session teardown (`SlidingWindow.Zeroize`) | |
| 248 | +| AAD buffers | On session teardown (`DefaultUdpSession.Zeroize`) | |
| 249 | + |
| 250 | +**Limitation:** Go GC may copy heap objects before zeroing. `mem.ZeroBytes` is best-effort defense against memory forensics, verified by compiler output analysis to not be optimized away (Go 1.26.x, all target platforms). |
| 251 | + |
| 252 | +--- |
| 253 | + |
| 254 | +## 6. Constants |
| 255 | + |
| 256 | +| Constant | Value | Purpose | |
| 257 | +|----------|-------|---------| |
| 258 | +| Protocol version | `0x01` | Wire format versioning | |
| 259 | +| MAC1 / MAC2 size | 16 bytes | BLAKE2s-128 output | |
| 260 | +| Cookie bucket | 120 seconds | IP-bound cookie validity window | |
| 261 | +| Cookie reply size | 56 bytes | nonce (24) + encrypted cookie (16) + tag (16) | |
| 262 | +| AAD length | 60 bytes | sessionId (32) + direction (16) + nonce (12) | |
| 263 | +| UDP route-id | 8 bytes | session identifier prefix for O(1) peer lookup | |
| 264 | +| Nonce counter | 80 bits | Messages per epoch before overflow | |
| 265 | +| Replay window | 1024 bits | UDP out-of-order tolerance | |
| 266 | +| Epoch capacity | uint16 | 65535 values, safe threshold 65000 | |
| 267 | +| Rekey interval | 120 seconds | Default periodic rekey trigger | |
| 268 | +| Pending timeout | 5 seconds | Auto-abort unconfirmed rekey | |
0 commit comments