Hashes, MACs und Zufallswerte · Übung
Eine Nachricht mit einem Schlüssel signieren
Angenommen, ein Server will einem Client eine kurze Nachricht geben, etwa „Buch 7 gehört ana“, und sie wiedererkennen, wenn sie zurückkommt. Die einfachste Idee ist, einen
import hashlib
message = b"book 7 belongs to ana"
tag = hashlib.sha256(message).hexdigest()
forged = b"book 7 belongs to ben"
forged_tag = hashlib.sha256(forged).hexdigest()
# The server's check: digest the message that arrived,
# and compare it with the tag that arrived.
print(hashlib.sha256(forged).hexdigest() == forged_tag)
Es gibt True aus. Der Fälscher hat die Nachricht geändert, den Digest der neuen berechnet, und die Prüfung des Servers geht durch. Nichts im Tag hing von etwas ab, das dem Fälscher fehlt. Das ist der schwache Entwurf, und diese Lektion ersetzt ihn.
Ein Schlüssel
Die fehlende Zutat ist ein Geheimnis, das der Ersteller des Tags und sein Prüfer beide haben und sonst niemand: ein Schlüssel (key).
Ein solches Geheimnis hast du schon einmal erzeugt. In secrets.token_urlsafe(16): 128 zufällige Bits vom random vorhersagbar ist. Ein Schlüssel ist dieselbe Idee mit zwei Unterschieden. Er besteht aus Bytes, nicht aus Text, und dieser Kurs verwendet 32 davon:
key = secrets.token_bytes(32)
Ein Schlüssel ist nie ein Text, den ein Mensch gewählt hat. Ein Wort oder ein Satz hat viel weniger Zufall, als seine Länge vermuten lässt, und ein Angreifer kann wahrscheinliche Formulierungen ausprobieren. Zweiunddreißig zufällige Bytes sind 256 zufällige Bits, so viele, wie SHA-256 ausgibt, und es gibt keine Abkürzung, um sie zu erraten. signing.py hält die Zahl 32 in einer Konstanten, KEY_BYTES.
Ein Schlüssel ist außerdem das Einzige in diesem Modul, das beim Server bleiben muss. Die Nachricht ist nicht geheim, und das Tag, das der Server mit ihr mitschickt, auch nicht. In diesem Kapitel lebt ein Schlüssel nur so lange wie ein Lauf eines Programms. Kapitel 5 behandelt, wo ein echter Dienst einen aufbewahrt.
hmac
Das Modul hmac der Standardbibliothek berechnet ein Tag mit Schlüssel.
tag = hmac.new(key, message, hashlib.sha256).hexdigest()
Drei Dinge solltest du über diesen Aufruf wissen:
Schlüssel und Nachricht sind beide
bytes. Einstrlöstaus. Das dritte
nennt die Hashfunktion, und es ist Pflicht. hexdigest()gibt das Tag als Text zurück: 64 hexadezimale Zeichen, unabhängig von der Länge der Nachricht. SHA-256 liefert 32 Bytes, und Hex schreibt jedes Byte als zwei Zeichen.
Mit demselben Schlüssel: Wie lang sind die zwei Tags von hmac.new(key, b"", hashlib.sha256).hexdigest() und hmac.new(key, b"a" * 300, hashlib.sha256).hexdigest()?
Durch Neuberechnen verifizieren
Ein Tag wird nicht dekodiert. Es steckt nichts darin, das man lesen könnte. Der Prüfer nimmt die Nachricht, die er bekommen hat, und seine eigene Kopie des Schlüssels, berechnet das Tag noch einmal und vergleicht das Ergebnis mit dem Tag, das er bekommen hat. Wenn sich die Nachricht geändert hat, das Tag sich geändert hat oder der Schlüssel ein anderer war, passen die beiden nicht zusammen.
verify(key, message, tag) ist also kurz: die Nachricht noch einmal signieren und vergleichen. Es gibt True oder False zurück und sonst nichts. Ein Tag, das zu kurz ist, leer ist oder mit einem anderen Schlüssel erzeugt wurde, ist kein Fehler. Es ist ein falsches Tag, und die Antwort ist False.
Vergleiche in dieser Lektion mit ==. Die nächste Lektion sieht sich genau diese eine Zeile an.
Bau keine eigene Konstruktion
hashlib.sha256(key + message) sieht nach derselben Idee aus: den Schlüssel voranstellen und hashen. Es ist nicht dasselbe. Diese Konstruktion hat mit SHA-256 eine bekannte Schwäche: Wer ein gültiges Tag sieht, kann ohne den Schlüssel ein gültiges Tag für eine längere Nachricht berechnen, die mit der ursprünglichen beginnt. HMAC wurde so entworfen, dass das nicht funktioniert. Verwende hmac.
Deine Aufgabe
Öffne signing.py. Seine drei main() ist bereit, sie zu benutzen. Schreib:
new_key(), dassecrets.token_bytes(KEY_BYTES)zurückgibt.sign(key, message), das das HMAC-SHA-256-Tag als 64 Hex-Zeichen zurückgibt.verify(key, message, tag), dasTruezurückgibt, wenntagdas Tag vonmessageunterkeyist, und sonstFalse.
Greif über die Module auf die Standardbibliothek zu, wie es die Importe oben in der Datei tun: secrets.token_bytes(...), hmac.new(...).
Führ dann die Datei aus:
python signing.py
Sie gibt key: 32 bytes aus, ein Tag, das bei jedem Lauf anders ist, weil der Schlüssel neu ist, unchanged message: True und changed message: False.
Lass jetzt das Kurs-Hilfsprogramm den Fälscher spielen:
python pl_sec.py forged-message
Der Lauf verwendet keinen Server und keinen verify():
--- five forgeries, each handed to your verify() ---
the message changed, the old tag kept -> refused
the message changed, the tag is SHA-256 of the new message -> refused
the right tag cut to its first 8 characters -> refused
an empty tag -> refused
a tag made with another key -> refused
--- how verify() compared ---
hmac.compare_digest was not called: verify() compares some other way
--- result ---
5 of 5 forgeries refused
Die zweite Fälschung ist die, die den Digest-Entwurf oben in dieser Lektion getäuscht hat. Die Zeile unter how verify() compared ist heute so zu erwarten. Sie betrifft das == in deinem verify, und sie ändert sich in Lektion 4.
Ein Server signiert Nachrichten mit einem Schlüssel und prüft sie mit verify. Wer kann ein Tag erzeugen, das die Prüfung besteht?
Klicke dann auf Check my work. Dein Modul lehnt jetzt jede Fälschung ab, die das Hilfsprogramm versucht hat. Die nächste Lektion fragt, was ein Angreifer aus der Art lernen könnte, wie verify Nein sagt.
Aufgabe
Schreib in signing.py drei
new_key()gibtsecrets.token_bytes(KEY_BYTES)zurück.sign(key, message)gibthmac.new(key, message, hashlib.sha256).hexdigest()zurück.keyundmessagesindbytes, und das Ergebnis ist Text aus 64 Hex-Zeichen.verify(key, message, tag)berechnet das Tag vonmessageunterkeynoch einmal und gibtTruezurück, wenn es gleichtagist, und sonstFalse. Vergleiche vorerst mit==.
Greif über die
Führ im python signing.py und python pl_sec.py forged-message aus.
Klicke dann auf Check my work. Es ruft new_key() mit einem Ersatz für secrets.token_bytes auf, der die verlangte Größe festhält, damit es erkennen kann, dass der Schlüssel das Ergebnis eines Aufrufs von secrets.token_bytes(32) ist, und es prüft, dass sich zwei echte Schlüssel unterscheiden. Es vergleicht sign() mit verify() mit dem richtigen Schlüssel, der richtigen Nachricht und dem richtigen Tag auf und mit einer geänderten Nachricht, einem geänderten Tag, einem anderen Schlüssel, einem gekürzten Tag, einem leeren Tag, einem verlängerten Tag und dem einfachen True ergeben und die anderen genau False. Wie verify() vergleicht, wird in dieser Lektion nicht geprüft. Nichts aus deinem Terminal wird bewertet.