0%

Hashes, MACs und Zufallswerte · Abschlussprojekt

Abschlussprojekt: Projekt: ein Signiermodul

Dieses Projekt stellt signing.py fertig und gibt ihm die Tests, die es für den Rest des Kurses bewachen. Kapitel 5 baut Tokens auf diesem auf, also muss es richtig sein, und es muss richtig bleiben, wenn es später jemand bearbeitet.

Die Anforderungen:

  • new_key(), sign() und verify() verhalten sich wie in Lektion 2.

  • verify() trifft seine Entscheidung über hmac.compare_digest, auf Bytes.

  • verify() gibt für ein falsches Tag aus beliebigem Text, den ein Header tragen kann, False zurück und löst nichts aus: für ein kurzes, ein leeres, eines mit Zeichen außerhalb von ASCII.

  • tests/test_attacks.py enthält vier neue Angriffstests mit festen Namen.

Vier Angriffe, vier Tests

Jeder Test spielt einen Angreifer und sichert zu, dass signing.verify(...) False zurückgibt, oder im letzten Fall, dass richtig verglichen wurde.

Der Fälscher hat keinen Schlüssel. Er ändert die Nachricht und braucht ein Tag dafür. Das Beste, was er berechnen kann, ist hashlib.sha256(message).hexdigest(), der einfache Digest seiner neuen Nachricht. test_attack_forged_tag_is_refused übergibt genau das an verify.

Der Kürzer hat ein richtiges Tag gesehen und sendet nur dessen erste Zeichen, in der Hoffnung, dass die Prüfung nicht weiter schaut. test_attack_truncated_tag_is_refused signiert eine Nachricht und kürzt das Tag mit einem , etwa tag[:8].

Der Außenstehende hat einen eigenen Schlüssel, auf dieselbe Weise erzeugt wie deiner, und signiert damit. test_attack_tag_from_another_key_is_refused erzeugt zwei Schlüssel, signiert mit dem einen und verifiziert mit dem anderen. Die Nachricht bleibt dieselbe, also ist der Schlüssel das Einzige, was sich unterscheidet.

Der vierte Angreifer ist der aus Lektion 3, der aus der Arbeitsweise des Vergleichs lernt. Dieser Angriff lässt sich hier nicht ausführen, weil auf dieser Website nichts gemessen wird. Also prüft der vierte Test das, was ihn aufhält. helpers.count_compare_digest() gibt ein zurück, dessen Eintrag "calls" ab da jeden Aufruf von hmac.compare_digest zählt, während die echte weiterhin vergleicht. Signiere zuerst eine Nachricht, starte dann den Zähler, ruf dann verify mit dem richtigen Tag auf und erwarte True und genau einen Aufruf. Starte den Zähler zuletzt, direkt vor dem Aufruf, den du zählen willst. In späteren Kapiteln führt die Vorbereitung eigene Vergleiche aus.

Dieser vierte Test ist von anderer Art als die anderen drei. Sie testen, was ein Außenstehender beobachten könnte. Dieser hier schaut hinein und prüft, welche Funktion verglichen hat. Er testet, wie der Code geschrieben ist, nicht, was er durchlässt, und er ist der einzige Weg, diese Eigenschaft ohne Uhr zu testen.

Meilenstein 1: die Tests schreiben

Schreib die vier Tests unter den Kommentar für diese Lektion in tests/test_attacks.py. Greif auf das Modul als signing.new_key(), signing.sign(...) und signing.verify(...) zu. Führ sie dann aus:

python -m pytest -q

Der Lauf endet mit 2 failed, 3 passed. Drei deiner neuen Tests laufen sofort gegen den Code aus Lektion 2 durch, der diese Fälschungen bereits ablehnt. Zwei Tests schlagen fehl: der aus Kapitel 1, den der Anfang der Datei als erwartet aufführt, und dein Vergleichstest mit assert 0 == 1. Null Aufrufe: Dein verify verwendet noch ==. Wenn dein verify bereits hmac.compare_digest aufruft, läuft auch dieser Test durch.

Um einen einzelnen Test auszuführen, wähle ihn über ein Wort aus seinem Namen aus. Die Option -k behält nur die Tests, deren Namen dieses Wort enthalten:

python -m pytest -q -k forged

Meilenstein 2: jeden Test einmal fehlschlagen lassen

Drei Tests sind beim ersten Ausführen durchgelaufen. Das ist angenehm, und es beweist wenig. Ein Test, der nie fehlgeschlagen ist, kann vielleicht gar nicht fehlschlagen.

Mach den Code also absichtlich kaputt und sieh zu. Ändere sign in signing.py kurz so, dass es hashlib.sha256(message).hexdigest() zurückgibt und den Schlüssel weglässt. Führ python -m pytest -q -k forged aus. Der Test für gefälschte Tags muss jetzt fehlschlagen, weil der einfache Digest eines Fälschers die Prüfung besteht. Stell die Zeile wieder her und sieh, wie der Test wieder durchläuft. Mach dasselbe für die anderen beiden: Ein verify, das nur expected[:len(tag)] mit dem Tag vergleicht, muss den Test für gekürzte Tags fehlschlagen lassen, und ein sign, das immer einen festen Schlüssel verwendet, etwa b"k" * 32, muss den Test mit dem anderen Schlüssel fehlschlagen lassen.

Check my work tut dasselbe mit vier kaputten Versionen des Moduls, einer für jeden Test, und erwartet, dass jeder Test gegen seine eigene fehlschlägt.

Dein Test für gekürzte Tags läuft durch. Dann änderst du verify so, dass es jedes Tag akzeptiert, das ein Anfang des richtigen ist, und der Test läuft immer noch durch. Was hast du gelernt?

Meilenstein 3: der Vergleich

Bring jetzt den letzten Test zum Durchlaufen. Behalte in verify das neu berechnete Tag in einer , kodiere beide Tags mit .encode("utf-8") und gib zurück, was hmac.compare_digest(...) für die beiden Byte- zurückgibt.

Führ mit python -m pytest -q noch einmal alle Tests aus. Der Lauf endet mit 1 failed, 4 passed, und der eine Fehlschlag ist der Test aus Kapitel 1. Führ dann das Hilfsprogramm aus:

python pl_sec.py forged-message

Seine letzten Zeilen lauten jetzt:

--- how verify() compared ---
hmac.compare_digest was called 5 times in 5 checks
--- result ---
5 of 5 forgeries refused

Warum verwendet der Fälscher im Test für gefälschte Tags hashlib.sha256(message).hexdigest() als Tag?

Was sich noch nicht ändert

SECURITY.md bleibt, wie es ist. F2 betrifft die Anmeldung, und die Anmeldung in reading_server.py vergleicht ein Passwort immer noch mit !=. Dieses Kapitel hat das Werkzeug gebaut. Kapitel 3 benutzt es, um die Zeile zu schließen.

Klicke auf Check my work, wenn alle vier Tests durchlaufen. Du hast jetzt zwei Dinge, auf die sich jedes spätere Kapitel verlässt: ein Tag, das nur der Besitzer des Schlüssels erzeugen kann, und einen Vergleich, der nicht zeigt, wo sich zwei unterscheiden. Kapitel 3 wendet sich dem ältesten Geheimnis des Dienstes zu, dem Passwort.

Aufgabe

Stell signing.py fertig und schreib seine vier Angriffstests.

Schreib in tests/test_attacks.py unter dem Kommentar für diese Lektion:

  • test_attack_forged_tag_is_refused: Erzeuge mit signing.new_key() einen Schlüssel. Der Fälscher ändert die Nachricht und verwendet hashlib.sha256(message).hexdigest() als Tag. Sichere zu, dass signing.verify(...) False zurückgibt.

  • test_attack_truncated_tag_is_refused: Signiere eine Nachricht, kürze das Tag mit einem und sichere False zu.

  • test_attack_tag_from_another_key_is_refused: Erzeuge zwei Schlüssel, signiere eine Nachricht mit dem einen, verifiziere dieselbe Nachricht mit dem anderen und sichere False zu.

  • test_attack_tag_is_compared_with_compare_digest: Signiere eine Nachricht, starte dann counter = helpers.count_compare_digest(), ruf signing.verify(...) mit dem richtigen Tag auf und sichere zu, dass es True zurückgegeben hat und dass counter["calls"] 1 ist.

Ändere dann verify in signing.py so, dass es beide Tags mit .encode("utf-8") kodiert und zurückgibt, was hmac.compare_digest(...) zurückgibt.

Führ im python -m pytest -q aus: Es muss mit 1 failed, 4 passed enden, wobei der eine Fehlschlag der Test aus Kapitel 1 ist.

Klicke dann auf Check my work. Es prüft new_key(), sign() und verify() wie in Lektion 2, mit anderen . Es ruft verify() mit einem Ersatz für hmac.compare_digest auf, der immer dieselbe Antwort gibt, um zu sehen, dass deine zurückgibt, was der Vergleich zurückgegeben hat, und es sendet ein Tag aus 64 Nicht-ASCII-Buchstaben, das False ergeben muss. Dann führt es deine vier Tests über ihre Namen aus, wobei signing.new_key, signing.sign und signing.verify durch andere Versionen ersetzt werden: Gegen ein korrektes müssen alle vier durchlaufen, und gegen vier kaputte Module (ein sign ohne den Schlüssel, ein verify, das einen Anfang akzeptiert, ein sign, das seinen Schlüssel ignoriert, ein verify, das == verwendet) muss der jeweils passende Test fehlschlagen. Der Test aus Kapitel 1 wird nicht ausgeführt, und nichts aus deinem Terminal wird bewertet.