Zum Hauptinhalt springen

Handshake- und Rekeying-Protokoll

Status: Lebendes Dokument Zuletzt aktualisiert: 2026-02-17

Überblick

TunGo verwendet einen Noise IK-Handshake für gegenseitige Authentifizierung und Schlüsselvereinbarung, gefolgt von periodischem X25519 + HKDF-SHA256-Rekeying. Die Transportverschlüsselung nutzt ChaCha20-Poly1305 AEAD mit epochenbasierter Nonce-Verwaltung.

Cipher-Suite: X25519 / ChaChaPoly / SHA-256 Protokoll-ID: "TunGo", Version 0x01


1. Handshake (Noise IK)

Noise IK setzt voraus, dass der Initiator (Client) den statischen öffentlichen Schlüssel des Responders (Server) bereits kennt.

1.1 Nachrichtenfluss

Client                                          Server
│ │
│─── MSG1: (e, es, s, ss) + MAC1 + MAC2 ───────>│
│ │
│<── COOKIE REPLY (optional, under load) ───────│
│ │
│─── MSG1 (retry with cookie) ─────────────────>│
│ │
│<── MSG2: (e, ee, se) ─────────────────────────│
│ │
├═══ Transport keys established ════════════════╡

1.2 MSG1 (Client -> Server)

Drahtformat:

[1B version] [>=80B noise_payload] [16B MAC1] [16B MAC2]
  • version: 0x01
  • noise_payload: Noise IK erste Nachricht — öffentlicher ephemerer Client-Schlüssel (32B, Klartext) + verschlüsselter statischer Client-Schlüssel (48B)
  • MAC1: Zustandslose Authentifizierung (wird immer verifiziert)
  • MAC2: Cookie-basierte Authentifizierung (wird nur unter Last verifiziert)

Minimalgröße: 113 Bytes.

1.3 MSG2 (Server -> Client)

Noise IK zweite Nachricht. Keine MACs — bidirektionale Authentifizierung ist nach Abschluss von Noise implizit.

Nach MSG2 leiten beide Seiten ab:

  • c2sKey (32 Bytes) — Client-zu-Server-Transportschlüssel
  • s2cKey (32 Bytes) — Server-zu-Client-Transportschlüssel
  • sessionId (32 Bytes) — aus der Noise-Kanalbindung

1.4 Server-Verifizierungsreihenfolge

1. CheckVersion()         — reject unknown protocol versions
2. VerifyMAC1() — stateless, before any DH or allocation
3. VerifyMAC2() — only under load (LoadMonitor)
4. Noise handshake — DH computations, peer lookup
5. Peer ACL check — AllowedPeers / PeerDisabled

Alle Fehler liefern ein einheitliches ErrHandshakeFailed zurück, um Informationslecks zu verhindern.


2. DoS-Schutz

2.1 MAC1 (Zustandslos, immer erforderlich)

key  = BLAKE2s-256("mac1" || "TunGo" || 0x01 || server_pubkey)
MAC1 = BLAKE2s-128(key, noise_msg1)

Wird vor jeder Zustandsallokation oder DH-Berechnung verifiziert.

2.2 MAC2 (Zustandsbehaftet, unter Last)

key  = BLAKE2s-256("mac2" || "TunGo" || 0x01 || cookie_value)
MAC2 = BLAKE2s-128(key, noise_msg1 || MAC1)

Wird nur geprüft, wenn LoadMonitor Lastdruck erkennt.

Cookie-Wert (IP-gebunden, zeitbucketiert):

bucket = unix_seconds / 120
cookie = BLAKE2s-128(server_secret[32], client_ip[16] || bucket[2])

Gültig für den aktuellen und vorherigen Bucket (behandelt Übergänge).

Cookie-Antwort (verschlüsselt, 56 Bytes):

[24B nonce] [16B encrypted_cookie] [16B poly1305_tag]

Verschlüsselung:

key = BLAKE2s-256("cookie" || "TunGo" || 0x01 || server_pubkey || client_ephemeral)
ciphertext = XChaCha20-Poly1305.Seal(key, nonce, cookie, aad=client_ephemeral)

3. Transportverschlüsselung

3.1 AEAD

ChaCha20-Poly1305 mit 60-Byte AAD:

AAD [60 bytes]:
[ 0..31] sessionId (32 bytes)
[32..47] direction (16 bytes: "client-to-server" or "server-to-client")
[48..59] nonce (12 bytes)

SessionId und Richtung werden bei der Sitzungserstellung vorbelegt. Nur die Nonce wird pro Paket aktualisiert.

3.2 Nonce-Struktur (12 Bytes)

[0..7]   counterLow   (uint64, big-endian)
[8..9] counterHigh (uint16, big-endian)
[10..11] epoch (uint16, big-endian)
  • Counter: 80-Bit monoton (2^80 Nachrichten pro Epoche). Überlauf gibt einen Fehler zurück.
  • Epoch: Unveränderlich pro Sitzung, identifiziert die Rekeying-Generation.

3.3 TCP-Transport

Wire frame: [2B epoch] [ciphertext + 16B tag]
  • Duale Epoche: aktuelle + vorherige Sitzung koexistieren während des Rekeying.
  • Automatische Bereinigung: vorherige Sitzung wird bei der ersten Entschlüsselung der aktuellen Epoche genullt (TCP-Reihenfolgegarantie).
  • Kein Replay-Schutz (TCP stellt die Reihenfolge sicher).

3.4 UDP-Transport

Wire frame: [8B route-id] [12B nonce] [ciphertext + 16B tag]
  • Route-id is derived from sessionId (first 8 bytes, big-endian) and enables O(1) session lookup.

  • Epoche eingebettet in Nonce-Bytes 10..11.

  • Replay-Schutz: 1024-Bit-Schiebefenster-Bitmap pro Epoche.

    • Vorläufige Prüfung vor der Entschlüsselung (Check).
    • Bestätigung erst nach erfolgreicher AEAD-Authentifizierung (Accept).
    • Verhindert Fenstervergiftung durch ungültige Pakete.
  • Epoch-Ring: FIFO mit fester Kapazität für Sitzungen. Entfernte Sitzungen werden genullt.


4. Rekeying

4.1 Schlüsselableitung

Beide Seiten führen X25519 ECDH durch und leiten dann neue Transportschlüssel über HKDF-SHA256 ab:

shared  = X25519(local_private, remote_public)
newC2S = HKDF-SHA256(ikm=shared, salt=currentC2S, info="tungo-rekey-c2s")
newS2C = HKDF-SHA256(ikm=shared, salt=currentS2C, info="tungo-rekey-s2c")

Aktuelle Schlüssel dienen als HKDF-Salt und ermöglichen verkettete Forward Secrecy.

4.2 Steuerungsebene-Pakete

RekeyInit:  [0xFF] [0x01] [0x02] [32B X25519 public key]   (35 bytes)
RekeyAck: [0xFF] [0x01] [0x03] [32B X25519 public key] (35 bytes)

4.3 Rekey-FSM

         StartRekey            installPending
Stable ──────────> Rekeying ──────────────> Pending
^ │
│ ActivateSendEpoch │
└───────────────────────────────────────────┘
^ │
│ AbortPendingIfExpired (5s) │
└───────────────────────────────────────────┘
ZustandBeschreibung
StableNormalbetrieb. Eine aktive Sende-Epoche.
RekeyingStartRekey aufgerufen, neue Schlüssel berechnet, neue Epoche für den Empfang installiert.
PendingWartet auf Bestätigung des Peers (erste erfolgreiche Entschlüsselung mit neuer Epoche).

4.4 Rekeying-Ablauf

Client                                     Server
│ │
│── RekeyInit (client X25519 pub) ────────>│
│ │ derive newC2S, newS2C
│ │ install new epoch (recv)
│<── RekeyAck (server X25519 pub) ─────────│
│ │
│ derive newC2S, newS2C │
│ install new epoch (recv + send) │
│ │
│── first packet with new epoch ──────────>│
│ │ peer confirmed → activate send
│<── first packet with new epoch ──────────│
│ │
├═══ Both sides on new epoch ══════════════╡

4.5 Sicherheitsinvarianten

  • Nur ein laufendes Rekeying gleichzeitig.
  • Epochen steigen monoton. Maximale sichere Epoche: 65000 (von 65535). Darüber hinaus erzwingt ErrEpochExhausted einen erneuten Handshake.
  • Die Sende-Epoche nimmt nie ab.
  • Ausstehende Schlüssel überschreiben niemals aktive Schlüssel, bis der Peer den Besitz nachweist (durch erfolgreiche Entschlüsselung).
  • Ausstehendes Rekeying wird nach 5 Sekunden automatisch abgebrochen, wenn keine Bestätigung vom Peer erfolgt.
  • Standard-Rekeying-Intervall: 120 Sekunden.

5. Schlüssellöschung

MaterialWann gelöscht
Ephemere DH-PrivatschlüsselSofort nach der DH-Berechnung (defer mem.ZeroBytes)
Gemeinsame Geheimnisse (Rekeying)Sofort nach der Schlüsselableitung (defer mem.ZeroBytes)
Ausstehende Rekeying-Schlüssel (FSM)Bei Abbruch oder Beförderung zum aktiven Schlüssel
Vorherige SitzungsschlüsselBei der ersten Entschlüsselung der aktuellen Epoche (TCP) oder Epochen-Verdrängung (UDP)
Nonce-Replay-FensterBei Sitzungsabbau (SlidingWindow.Zeroize)
AAD-PufferBei Sitzungsabbau (DefaultUdpSession.Zeroize)

Einschränkung: Der Go-GC kann Heap-Objekte vor dem Nullen kopieren. mem.ZeroBytes ist eine Best-Effort-Verteidigung gegen Speicherforensik, durch Compiler-Ausgabeanalyse verifiziert, dass sie nicht wegoptimiert wird (Go 1.26.x, alle Zielplattformen).


6. Konstanten

KonstanteWertZweck
Protokollversion0x01Drahtformat-Versionierung
MAC1- / MAC2-Größe16 BytesBLAKE2s-128-Ausgabe
Cookie-Bucket120 SekundenIP-gebundenes Cookie-Gültigkeitsfenster
Cookie-Antwortgröße56 BytesNonce (24) + verschlüsseltes Cookie (16) + Tag (16)
AAD-Länge60 BytessessionId (32) + direction (16) + nonce (12)
UDP route-id8 bytessession identifier prefix for O(1) peer lookup
Nonce-Counter80 BitsNachrichten pro Epoche vor Überlauf
Replay-Fenster1024 BitsUDP-Toleranz für ungeordnete Pakete
Epochen-Kapazitätuint1665535 Werte, sicherer Schwellenwert 65000
Rekeying-Intervall120 SekundenStandard-Auslöser für periodisches Rekeying
Ausstehend-Timeout5 SekundenAutomatischer Abbruch eines unbestätigten Rekeying