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
Die Anforderungen:
new_key(),sign()undverify()verhalten sich wie in Lektion 2.verify()trifft seine Entscheidung überhmac.compare_digest, auf Bytes.verify()gibt für ein falsches Tag aus beliebigem Text, den ein Header tragen kann,Falsezurück und löst nichts aus: für ein kurzes, ein leeres, eines mit Zeichen außerhalb von ASCII.tests/test_attacks.pyenthä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 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 "calls" ab da jeden Aufruf von hmac.compare_digest zählt, während die echte 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 .encode("utf-8") und gib zurück, was hmac.compare_digest(...) für die beiden Byte-
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
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 mitsigning.new_key()einen Schlüssel. Der Fälscher ändert die Nachricht und verwendethashlib.sha256(message).hexdigest()als Tag. Sichere zu, dasssigning.verify(...)Falsezurückgibt.test_attack_truncated_tag_is_refused: Signiere eine Nachricht, kürze das Tag mit einemund sichere Falsezu.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 sichereFalsezu.test_attack_tag_is_compared_with_compare_digest: Signiere eine Nachricht, starte danncounter = helpers.count_compare_digest(), rufsigning.verify(...)mit dem richtigen Tag auf und sichere zu, dass esTruezurückgegeben hat und dasscounter["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 verify() mit einem Ersatz für hmac.compare_digest auf, der immer dieselbe Antwort gibt, um zu sehen, dass deine 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 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.