0%

Hashes, MACs und Zufallswerte

Warum == der falsche Vergleich ist

Dein verify antwortet True oder False, und ein Angreifer, der ein falsches Tag sendet, erfährt nur, dass es falsch war. Angenommen, er erführe noch eine Sache mehr: wie viel Arbeit der Vergleich leisten musste, bevor er False gesagt hat. Diese Lektion zeigt, was dieses zusätzliche Stück ist. Es ist der Grund hinter Befund F2, den du in Kapitel 1 ohne Begründung aufgeschrieben hast.

Ein Zählmodell

Hier ist ein von Hand ausgeschriebener Vergleich. Er geht zwei Zeichen für Zeichen durch, hält beim ersten Unterschied an und meldet, wie viele Zeichen er angesehen hat:

Try it

Jeder Versuch außer dem letzten ist falsch, und die Antwort ist jedes Mal False. Die Zählung ist nicht jedes Mal gleich. 00000000 ist schon beim ersten Zeichen falsch, also wurde ein Zeichen angesehen. 7f3a0000 beginnt mit vier richtigen Zeichen, also ist die Zählung 5. Je besser der Versuch, desto höher die Zählung.

Das ist ein Modell. Pythons eigenes == auf zwei Strings prüft zuerst, ob sie gleich lang sind, und vergleicht dann ihren Speicher. Es führt keine Zählung, und auf dieser Website wird nichts gemessen. Was das Modell mit dem Original gemeinsam hat, ist die Form: Die Arbeit kann davon abhängen, wo der erste Unterschied liegt.

Was gibt examined("7f300000", secret) mit dem Geheimnis 7f3a91c2 zurück?

Was die Zählung wert ist

Ein Angreifer, der die Zählung sehen kann, muss nicht das ganze Geheimnis auf einmal raten. Er kann es Zeichen für Zeichen finden: jede der 16 Hex-Ziffern an der ersten Stelle ausprobieren und die behalten, bei der die Zählung steigt. Dann dasselbe für die zweite Stelle. recover tut das und benutzt nichts als das, was check meldet:

Try it

Es gibt ('7f3a91c2', 67) aus: das ganze Geheimnis nach 67 Versuchen. Ein falscher Buchstabe an Stelle p hält die Zählung bei p + 1 an, also bedeutet eine höhere Zählung einen richtigen Buchstaben. Ohne die Zählung muss ein Angreifer ganze Werte ausprobieren, und davon gibt es 16 ** 8, also 4.294.967.296. Die Zählung hat ein schweres Problem in acht leichte verwandelt. Für ein echtes Tag mit 64 Zeichen braucht dieselbe höchstens 16 Versuche pro Stelle, insgesamt 1.024.

Welches Geheimnis ist in verify gefährdet? Lektion 2 hat gesagt, dass ein Tag nicht geheim ist. Für eine Nachricht, die der Server signiert hat, stimmt das: Der Client hat beides. Das richtige Tag für eine Nachricht, die der Angreifer geschrieben hat, ist etwas anderes. Nur wer den Schlüssel hat, kann es berechnen, und verify berechnet es bei jedem Aufruf. Wenn der Vergleich zeigt, wie viel von einem Versuch richtig ist, kann der Angreifer dieses Tag Zeichen für Zeichen herausfinden und hält dann eine gefälschte Nachricht in der Hand, die die Prüfung besteht, ohne den Schlüssel.

Von einer Zählung zu einer Uhr

Ein echter Angreifer hat keine Zählung. Was er haben kann, ist die Zeit, die eine Antwort braucht, denn mehr Arbeit dauert länger. Dieses Signal ist winzig und im Rauschen vergraben, also braucht es sehr viele Anfragen und sorgfältige Statistik, um es zu lesen.

Du musst nicht beurteilen, wie praktikabel der Angriff gegen deinen Dienst ist, denn die Abwehr kostet eine Zeile. Dieses Muster lohnt sich zu behalten: Wenn das Schließen einer Schwachstelle billig ist, schließ sie, statt zu argumentieren, dass niemand sie ausnutzen würde.

hmac.compare_digest

Die Standardbibliothek hat einen Vergleich, der für Geheimnisse gemacht ist:

Try it

Es gibt True, False, False aus, wie es == täte. Der Unterschied liegt darin, wie es arbeitet: hmac.compare_digest sieht sich jedes Zeichen an, egal wie der Versuch aussieht, also verrät seine Arbeit nichts darüber, wo sich zwei Werte unterscheiden. Einen Vergleich mit dieser Eigenschaft nennt man einen Vergleich in konstanter Zeit (constant-time comparison).

Zwei Einzelheiten zählen, wenn du ihn verwendest:

  • Er kann trotzdem verraten, dass die beiden Werte verschieden lang sind, und seine Dokumentation sagt das. Für ein Tag ist das harmlos, denn jeder weiß, dass ein Tag 64 Zeichen hat.

  • Er akzeptiert zwei str-Werte nur, wenn beide reines ASCII sind, und löst sonst aus. Ein Tag kommt von einem Client, und ein Client kann alles senden. Vergleiche also Bytes: Ruf zuerst auf beiden Seiten .encode("utf-8") auf.

Was kann hmac.compare_digest trotzdem verraten?

Wo die Regel gilt

Die Regel lautet: Wenn ein Geheimnis mit einem erwarteten Wert verglichen wird und die andere Seite den Versuch wählt, verwende hmac.compare_digest. In diesem Kurs ist das ein Tag, in Kapitel 3 ein Passwort-Hash und in Kapitel 8 noch ein Token.

Es ist nicht jedes Nachschlagen. state["sessions"].get(session_id) findet eine über ihre ID, und das braucht diese Behandlung nicht. Ein -Zugriff arbeitet mit dem ganzen Schlüssel auf einmal, ein Beinahetreffer findet nichts, worauf ein Angreifer aufbauen kann, und die ID hat 128 zufällige Bits. Deshalb betrifft F2 das != der Anmeldung bei einem Passwort und nicht den Session-Speicher.

Welche Zeile braucht hmac.compare_digest?

Warum Schlüssel aus secrets kommen

Noch ein offenes Ende aus Lektion 2: Warum secrets.token_bytes und nicht das random?

Try it

Die zwei Zeilen sind dieselben 32 Zeichen, und die letzte Zeile gibt True aus. random ist dafür gebaut, wiederholbar zu sein: Vom selben Ausgangspunkt, dem Seed, erzeugt es immer dieselben Werte. Für eine Simulation oder ein Spiel ist das eine . Für einen Schlüssel ist es fatal, denn jeder, der den Seed erfährt oder genug von der Ausgabe sieht, kann berechnen, was als Nächstes kommt. secrets fragt das nach Bytes, die niemand vorhersagen kann.

Ein Programm ruft random.seed(2026) und dann random.randbytes(16).hex() auf. Es wird morgen noch einmal ausgeführt. Was gibt der zweite Lauf aus?

Du hast jetzt den Grund für F2 und das Werkzeug, das ihn behebt. Das Projekt in der nächsten Lektion setzt hmac.compare_digest in verify ein und beweist es mit Tests.