Ordentliche Sicherheit für Anmeldungscodes
Anmeldungscodes werden ab dem nächsten Release (4.0.0, t. b. a.) vernünftig abgesichert sein. Ja, kein Mensch will einen Anmeldungscode fälschen. Nein, das ist auch noch nie vorgekommen. Aber es geht hier ja nicht um „für ein Hobbyprojekt reicht’s schon“ oder „funktioniert doch“ – es geht darum, vernünftige Code-Qualität und eine vernünftige, professionelle Implementierung vorzulegen. Das gebietet die Programmierer-Ehre. Soll ja keiner sagen können, ich würde schlechten Code schreiben :-P
Was wir hatten, und warum das schlecht war
Ich dachte mir damals folgendes: Es braucht, zusätzlich zur eigentlichen Datenübertragung, einen Sicherheitsmechanismus. Weil sonst ist ein Muckturnier ausgebucht, und einer kommt auf die Idee, sich die Spezifikation anzuschauen (die ist einfach), und sich seinen eigenen Anmeldungscode zu bauen (auch das ist einfach). Und dann steht jemand da, und behauptet, er wäre ja vorangemeldet gewesen – obwohl das nicht stimmt. Oder – und sowas würde ich persönlich machen: Bauen wir einfach mal so einen Code, nur, um den Entwickler zu ärgen, bzw. ihm vor Augen zu führen, dass sein Kram nichts taugt.
Also: Wie machen wir das, einfach, und mit wenig Zeichen (damit die QR-Codes kompakt bleiben)? Augenscheinlich braucht es einen kryptographischen Hash, eine Prüfsumme der Nachricht. Für die Netzwerkfunktionalität nutze ich intern MD5. Da geht es zwar nicht um Sicherheit, sondern einfach nur darum, dass alle Clients auf demselben Stand sind – aber dann nehmen wir halt das. MD5 ist schnell und leicht zu berechnen, und der Hash ist vor allem kurz. Wichtig, weil die QR-Codes sollen ja kompakt bleiben. Also nehmen wir einfach einen geheimen Schlüssel und berechnen den MD5-Hash aus dem Schlüssel und dem Nachrichtentext, und hängen den Hash an die Nachricht an.
Ja, ich wusste auch zu dem Zeitpunkt bereits, das MD5 in einem kryptographischen Kontext schon lang gebrochen wurde, aber für so kurze Nachrichten und für den Zweck wird’s schon reichen. Oder?!
Warum war das ein Trugschluss bzw. eine schlechte Idee?
- MD5 gilt seit vielen Jahren als nicht kollisionssicher. Angreifer können gezielt zwei verschiedene Nachrichten mit demselben Hash erzeugen. Das wusste ich wie gesagt schon, aber ich dachte, für so kurze Nachrichten wie ein Anmeldungs-Datagram wäre das irrelevant.
-
MD5 ist anfällig für sog. Length-Extension-Angriffe. Wenn ein Angreifer
MD5(Schlüssel + Nachricht)kennt, kann er, ohne den Schlüssel zu kennen, einen gültigen Hash für(Schlüssel + Nachricht + zusätzliche Daten)berechnen.
Da der bisherige MD5-Hash nicht aus dem gesamten Datagram berechnet wurde, sondern nur aus Schlüssel, Turniername und Paar/Spielername (vor der Serialisierung, ohne den Header mit den Längenangaben), hätte man tatsächlich ohne großen Aufwand aus einem generierten Anmeldungscode einen anderen als gültig validierten berechen können, ohne den geheimen Schlüssel zu kennen. Das ist katastrophal! - In der Sicherheitsinformatik gilt das eherne Gesetz Never roll your own crypto – aber genau das habe ich mit meiner unreflektierten, selbergestrickten Lösung gemacht (Asche auf mein Haupt!).
Wie man es richtig macht
Wenig überraschend: Für exakt diesen Einsatzzweck haben schlaue Leute ein Verfahren definiert, das sicher ist und funktioniert: Hash-based Message Authentication Codes (HMAC), standardisiert in RFC 2104.
HMAC basieren ebenfalls auf einer Hash-Funktion, das Verfahren behebt aber das Problem der potenziellen Anfälligkeit für einen Length-Extension-Angriff. Der geheime Schlüssel wird bei dem Verfahren zweistufig mit dem Nachrichtentext verwoben: Zunächst wird der Schlüssel mit einem „inneren Pad“ vermischt, dann wird das Ergebnis zusammen mit der Nachricht gehasht. Dann wird der Schlüssel mit einem „äußeren Pad“ vermischt, und das Ergbnis wird dann mit dem Ergbnis des ersten Hash-Vorgangs erneut gehasht. Das mutet erwas umständlich an, sorgt aber dafür, dass mit diesem Verfahren selbst eine an sich unsichere Hash-Funktion wie MD5 auch Stand jetzt noch sicher angewendet werden könnte.
Aber wenn wir es schon anders machen, dann machen wir es gleich „richtig“ richtig. Die jetzt benutzte HMAC-Hash-Funktion ist SHA-256. Der resultierende Hash von SHA-256 ist allerdings erheblich länger, als der von MD5. Beispiel: Die hexadezimale Repräsentation des MD5-Hashs von „Muckturnier.org“ ist 91765900c6885d64ef95987f05123140. Berechnet man den Hash mit SHA-256, ist das Resultat b4d08106b5b663dc4b4d18275fe8fcdb26e46abd3ab0ca27621ddcc7f9875579. Jetzt sollen ja aber die QR-Codes kompakt bleiben.
Netterweise kann man den Hash einfach abschneiden, indem man nur die ersten paar Bytes benutzt („Truncation“) – und das Ergebnis ist, vorausgesetzt, man benutzt genügend Bytes, immer noch kryptographisch sicher. Zum Einsatz kommen die ersten 16 Bytes – das ist dieselbe Länge, die der MD5-Hash vorher auch hatte. Das sind 128 Bits eines SHA-256-Hashs – und damit sprechen wir hier nicht von einem Kompromiss, sondern von Enterprise-Level Security.
Weiterhin wird per Standard jetzt kein zufälliger Sicherheitsschlüssel aus einer Untergruppe von ASCII-Zeichen mehr generiert, sondern 32 kryptographisch sicher zufällig generierte Bytes (irgendwann erkennt man mal den Unterschied zwischen Strings und Byte Arrays, was das eine und was das andere ist, und wann man was davon benutzt ;-) Das führt dazu, dass der Sicherheitsschlüssel (der jetzt auch nur noch auf explizite Anfrage nach einer Warnung geändert werden kann) 256 Bits an kryptograpsch sicherer Entropie darstellt – und das ist ein astronomisch großer Wert, der noch nicht einmal rein theoretisch gebrochen werden kann. Auch nicht mit Quantencomputern.
Wie Anmeldungscodes jetzt funktionieren
Die Spezifikation des Protokolls für Anmeldungscodes war von vornherein gut designt. Die Struktur ist exakt gleich geblieben. Nur kommen jetzt statt eines „nackten“ MD5-Hashs der Eingabedaten zur Absicherung die ersten 16 Bytes eines HMAC-SHA-256-Hashs des gesamten kodierten Datagrams zum Einsatz, unter Verwendung eines extrem sicheren Schlüssels. Besagter Schlüssel wird nach wie vor für jedes Turnier neu generiert und kommt nur ein Mal zum Einsatz.
Damit ist das Sicherheitskonzept der Anmeldungscodes jetzt keine undurchdachte „Wird schon passen dafür“-Lösung, sondern – auf absehbare Zeit – wirklich kryptographisch sicher. Das würde in dieser Form jetzt auch einem kryptographischen Audit standhalten. Und das, obwohl die Codes von der Struktur her genauso aussehen, wie bisher.
Das neue Protokoll ist nicht abwärtskompatibel
Es gibt zwei substanzielle Änderungen:
- Die Hash-Funktion zum Generieren der Prüfsummen wurde geändert
- Der Sicherheitsschlüssel wird jetzt nicht mehr direkt als Zeichenkette (String) gespeichert, sondern als Base64-Repräsentation von Binärdaten (Byte Array). Das ist deswegen notwendig, weil er ja nicht mehr aus druckbaren Zeichen besteht, sondern aus vollkommen zufälligen Bytes.
Bedingt dadurch ist das neue Anmeldungs-Code-Protokoll nicht abwärtskompatibel. Selbst, wenn man einen bereits gespeicherten Sicherheitsschlüssel in seine Base64-Darstellung konvertieren würde, könnten vorher erstellte Anmeldungscodes nicht verarbeitet werden, da sich ja die Hash-Funktion geändert hat. Das Einführen einer Kompatibilitätsschicht (also das weitere Unterstützen von Anmeldungscodes, die mit Protokollversion 2 erzeugt wurden) erscheint mir nicht sinnvoll, da die Codes ja nur für dieses eine Turnier genutzt werden. Später tauchen ja keine alten Codes mehr auf.
Also bitte beachten: Wenn bereits Anmeldungscodes generiert wurden, dann das Turnier auch mit der bisherigen Version des Programms auswerten, und erst hinterher updaten. Darauf wird aber im Release Announcement nochmal hingewiesen.
Da haben wir ja gerade nochmal die Kurve gekriegt ;-)