somethinglikegames.de

Mein Blog über Spieleentwicklung und das Drumherum


Kategorien


Tags

Der Quellcode zu den Artikeln ist in diesem GitHub Repository zu finden. Die folgende Liste zeigt alle bislang erschienenen Artikel dieser Serie:

  1. Netzwerktutorial 1: Allgemeines und Übersicht
  2. Netzwerktutorial 2: Das "gehende Skelett"
  3. Netzwerktutorial 3: Login 1 - Der Game-Client
  4. Netzwerktutorial 4: Login 2 - Gateway- und Authentication-Server
  5. Netzwerktutorial 5: Login 3 - World-Server
  6. Netzwerktutorial 6: Verschlüsselte Verbindungen mit Hilfe von DTLS

Im letzten Artikel haben wir den Loginvorgang fertiggestellt. Vor dessen Anwendung in der Praxis sollten wir aber zuerst unsere Netzwerkverbindungen absichern. Das Mindeste, was man tun sollte und, wenn ich die DSGVO richtig verstehe, auch tun muss, ist, Benutzerpasswörter nur mit Hilfe von verschlüsselten Kanälen im Internet zu übertragen. Im Idealfall weitet man die verschlüsselte Kommunikation auf die gesamte Infrastruktur aus. Darum soll es also in diesem Artikel gehen: Wir verschlüsseln die Netzwerkverbindungen zwischen unseren Servern und Clients.

Um zu beweisen, dass aktuell die Passwörter wirklich als Klartext übertragen werden, habe ich im nachfolgenden Bild mit Hilfe von Wireshark ein Login-Paket zwischen Client und Gateway-Server abgefangen. Wie man im rechten Teil des Ausschnitts erkennen kann, wurde der Login mit dem Benutzernamen “my_username” und dem Passwort “my_secret_password” versucht.

Unverschlüsselter Login

Netzwerkdump mit unverschlüsselter Passwortübertragung

Aber selbst wenn der Schutz der eigenen Benutzer nicht Grund genug wäre, gibt es ebenfalls einen weiteren guten Grund: Die Absicherung gegen Proxy-Angriffe. Damit sind alle Angriffe bzw. Betrugsversuche gemeint, die mit Hilfe von Netzwerkproxies umgesetzt werden können. Dabei analysiert der Netzwerkproxy die Netzwerkpakete zwischen Spielclient und -server. Sollte der Proxy etwas erkannt haben, bspw. einen Gegner, könnte er die Netzwerkpakete des Spielclients so verändern, dass der Spielserver glaubt, der Nutzer des Spielclients würde den Gegner angreifen. Es handelt sich dabei um eine Art Machine-in-the-middle Attacke, die vom Nutzer des Spielclients selbst initiiert und gewünscht wird, um sich bspw. übermenschliche Reflexe zu ergaunern. Diese Art von Angriff funktioniert aber nur, wenn die Netzwerkverbindungen unverschlüsselt sind.

DTLS

DTLS steht für Datagram Transport Layer Security und ist vereinfacht gesagt TLS für UDP. TLS wiederum sollte mittlerweile jeder von seinem Browser kennen, denn wenn die Adresszeile mit https beginnt bzw. dort ein Schloss angezeigt wird, ist die Verbindung mit Hilfe von TLS verschlüsselt. Man kann sich TLS so vorstellen, als ob zwischen Client und Server ein Tunnel etabliert wird, alle (Klartext-)Daten fließen durch den Tunnel und Außenstehende sehen beim Blick zum Tunnel nur Kauderwelsch, da die Tunnelwände alle Daten verschlüsseln. Das heißt, sobald dieser Tunnel aufgebaut ist, kommunizieren Client und Server für sich weiterhin unverschlüsselt, aber alle Mithörenden bekommen nur die verschlüsselte Kommunikation mit.

Generell werden neue Versionen von DTLS immer einige Jahre nach der jeweiligen TLS-Version spezifiziert. Welche Version konkret von Godot eingesetzt wird, konnte ich zwar noch nicht herausfinden, aber sobald ich es weiß, werde ich es mitteilen. Generell benutzt Godot 4.0.2 für (D)TLS intern die Bibliothek mbedtls 2.18.2, welche DTLS 1.0 und 1.2 unterstützt.

Die Version 1.1 von DTLS wurde nie veröffentlicht, um sich ab Version 1.2 dem Versionierungsschema von TLS anzugleichen. DTLS 1.0 basiert auf TLS 1.1 und DTLS 1.2 auf TLS 1.2.

Erstellen der Zertifikate

(D)TLS setzt bei der Verschlüsselung digitale Public-Key-Zertifikate ein. Ohne jetzt weiter auf X.509 einzugehen, ist ein solches Public-Key-Zertifikat fast wie ein RSA-Schlüsselpaar, welches wir bereits bei der Tokenerzeugung eingesetzt haben, nur dass das Public-Key-Zertifikat neben dem Public-Key noch weitere Informationen enthält, wie bspw. Aussteller, Gültigkeitszeitraum und Angaben zum Inhaber.

Im WWW ist es üblich, TLS-Zertifikate zu nutzen, die von “anerkannten” Zertifizierungsstellen (CAs = Certificate Authorities) ausgestellt wurden. Dabei wird das eigentliche Zertifikat durch eine Zertifizierungsstelle signiert, um die Vertraulichkeit zu erhöhen. So ergeben sich in der Praxis dann teilweise Zertifikatsketten, weil das Zertifikat, mit dem die Zertifizierungsstelle signiert hat, wiederum von einer weiteren Zertifizierungsstelle signiert wurde. “Anerkannt” bedeutet, dass die Software, in der Praxis in der Regel der Browser, dieser CA bzw. der “übergeordneten” CA vertraut. Dafür nutzt Firefox bspw. eine eigene Datenbank, während sich die meisten anderen Browser auf die Datenbank des Betriebssystem verlassen. Dank Let’s Encrypt ist dies auch kostenlos und bequem möglich. Ein Beispiel einer solchen Zertifikatskette sieht man bspw. auf diesem SSL Report von Qualys SSL Labs, wo ein Server, der diese Website ausliefert, auf bekannte TLS Sicherheitslücken getestet wurde.

Solange wir die Dinge nur lokal zum Testen nutzen, können wir uns den Weg über Zertifizierungsstellen sparen und stattdessen “self-signed” Zertifikate nutzen, sie also selbst signieren. Es gibt viele Wege, um self-signed Zertifikate zu erzeugen. Unter Linux bietet sich bspw. openssl an:

openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \
			-keyout authentication_server.key \
			-out authentication_server.crt

Dadurch erzeugen wir ein X.509-Zertifikat mit einem 4096-bit RSA-Schlüssel und SHA256-Hashwerten, welches 365 Tage gültig ist und keine Passphrase für den Schlüssel nutzt (-nodes). Der private Schlüssel soll authentication_server.key heißen und das Zertifikat authentication_server.crt. Für die Erzeugung werden auch noch einige allgemeine Angaben abgefragt, die für reguläre Zertifikate notwendig sind. Da wir diese Zertifikate aktuell nur lokal zum Testen nutzen wollen, sind die Angaben vorerst irrelevant.

Alternativ kann man sich auch selbst eine kleine Godot-Anwendung erstellen, die Zertifikate betriebssystemunabhängig erzeugt. Ein rudimentäres Beispiel dazu findet ihr mit Godot 3.2.3 hier im Tutorial von Game Development Center aka Stefan.

Insgesamt benötigen wir Zertifikate für den Authentication-, Gateway- und World-Server.

Aktivieren von DTLS in Godot

Nachdem wir nun alle notwendigen Zertifikate haben, müssen wir DTLS “nur” noch in unseren Godot-Projekten aktivieren. Um diesen Artikel nicht unnötig in die Länge zu ziehen und euch dadurch zu langweilen, werde ich es beispielhaft für einen Server und einen Client zeigen. Als Server nehme ich den Authentication-Server und als Client fungiert passenderweise der Gateway-Server. Da wir ja bislang den Aufbau der Projekte möglichst identisch gestaltet haben, sollten sich die notwendigen Schritte problemlos auf die anderen Projekte übertragen lassen, und zur Not kann man auch im GitHub Repository nachschauen.

Aktivieren von DTLS auf dem “Server” (authentication_server/scenes/network/authentication_server.gd)

Ich habe sowohl das Zertifikat als auch den zugehörigen privaten Schlüssel in das crypto Verzeichnis abgelegt, wo sich im Fall des Authentication-Servers bereits das RSA-Schlüsselpaar für den Token befindet. Das Verzeichnis wurde auch bereits aus Sicherheitsgründen als Ausnahme in die .gitignore Datei eingefügt, da meiner Meinung nach grundsätzlich keine Zertifikate und Schlüssel in ein Versionsverwaltungssystem gehören.

Anschließend erweitern wir das Server-Skript, also in diesem beispielhaften Fall: authentication_server/scenes/network/authentication_server.gd. Zuerst laden wir unseren privaten Schlüssel und das Zertifikat:

7const dtls_key := preload("res://crypto/authentication_server.key")
8const dtls_cert := preload("res://crypto/authentication_server.crt")

Anschließend müssen wir unsere startup() Funktion noch minimal erweitern, um DTLS zu aktivieren (Zeile 31). Damit die einzelne notwendige Zeile nicht ganz ohne Kontext aufgeführt wird, habe ich auch noch ein paar Zeilen davor und dahinter mit hineinkopiert, auch wenn diese Zeilen aus früheren Artikeln stammen.

27func startup() -> void:
28	var network := ENetMultiplayerPeer.new()
29	var ret := network.create_server(network_port, max_clients)
30	if ret == OK:
31		network.host.dtls_server_setup(TLSOptions.server(dtls_key, dtls_cert))
32		multiplayer.server_relay = false
33		multiplayer.set_multiplayer_peer(network)
34		...

DTLS wird auf Ebene der ENetConnection aktiviert, welche wir über das host Attribut unseres ENetMultiplayerPeer erreichen. Als Parameter werden TLSOptions übergeben, und da diese Optionen nicht direkt instanziiert werden können, gibt es passende statische Factory-Methoden, die die korrekte Instanziierung für uns übernehmen. Da wir hier DTLS auf dem Server aktivieren wollen, wählen wir natürlich die statische Methode server().

Aktivieren von DTLS auf dem “Client” (gateway_server/scenes/network/authentication_server.gd)

Beim Gateway-Server als DTLS-Client des Authentication-Servers müssen wir ebenfalls unsere startup() Funktion erweitern, um DTLS zu aktivieren (Zeile 18). Auch hier habe ich ein paar Zeilen davor und dahinter hinzugefügt, damit der Kontext besser ersichtlich wird:

12func startup() -> void:
13	print("Connecting to %s:%d" % [authentication_server, authentication_server_port])
14	var ret = network.create_client(authentication_server, authentication_server_port)
15	var _hash := _crypto.hmac_digest(HashingContext.HASH_SHA256, shared_secret,
16			Time.get_date_string_from_system(true).to_utf8_buffer())
17	if ret == OK:
18		network.host.dtls_client_setup("localhost", TLSOptions.client_unsafe())
19		multiplayer.multiplayer_peer = network
20		...

Es gilt auch auf dem Client, dass DTLS auf Ebene der ENetConnection aktiviert wird, welche wir über das host Attribut unseres ENetMultiplayerPeer erreichen. Als erster Parameter von dtls_client_setup() wird der Hostname des Servers übergeben. Da ich in dieser Tutorialserie alles lokal auf einem einzelnen PC laufen lasse, ist mein Hostname “localhost”. Als zweiter Parameter wird ebenfalls TLSOptions übergeben, nur dass wir dieses Mal die statische Funktion client_unsafe() verwenden. Die Funktion client_unsafe() zur Initialisierung von TLSOptions sollte ausschließlich für Testzwecke verwendet werden, da hierdurch die Zertifikatsprüfung deaktiviert wird.

In einem produktiven Szenario würde man stattdessen die Funktion client() nutzen und entweder ein Zertifikat einer “anerkannten” Zertifizierungsstelle verwenden oder aber eine eigene CA erstellen, deren Zertifikatskette man bei der Funktion als Parameter übergeben kann. Hierbei ist auch noch wichtig zu erwähnen, dass man die Zertifikate rechtzeitig erneuern und ggf. auch Clients aktualisieren muss, falls sich etwas in der Zertifikatskette geändert hat.

Überprüfen der verschlüsselten Verbindung

Nachdem wir die Schritte für alle unsere Godot-Projekte ausgeführt haben, sollte die Kommunikation weiterhin wie gewohnt funktionieren, und man sollte eigentlich gar nicht merken, dass unsere Verbindungen nun verschlüsselt sind. Um aber zu prüfen, ob die Verbindung nun wirklich verschlüsselt ist, habe ich meinen anfänglichen Test wiederholt und die Kommunikation zwischen Game-Client und Gateway-Server mit Hilfe von Wireshark mitgeschnitten. Wie lauteten dieses Mal die Logindaten?

Verschlüsselter Login

Netzwerkdump mit verschlüsselter Passwortübertragung