0%

Signierte Tokens und geheime Schlüssel · Übung

Den Schlüssel laden oder den Start verweigern

Starte deinen Server so, wie es ein Betreiber tun würde, für eine Verbindung und mit einer Sekunde Geduld:

python reading_server.py --max-connections 1 --idle-timeout 1

Er gibt aus, dass er auf lauscht, an einem , den das System gewählt hat, wartet eine Sekunde auf einen Client, der nie kommt, und gibt served its connections and stopped aus. Niemand hat ihn nach einem Schlüssel gefragt. Ein Server, der Tokens signieren wird, darf so nicht starten.

Ein Schlüssel als Text

Eine Umgebung enthält Text, und ein Schlüssel besteht aus 32 zufälligen Bytes. Also reist der Schlüssel als , die Standardart mit ihrem Padding. Öffne README.md: Es zeigt den Testschlüssel für diesen Kurs und wie man den Server damit startet. Dieser Schlüssel ist absichtlich lesbar. Er dekodiert zu den Wörtern test-only key, never use it: 001, damit ihn niemand für einen echten halten kann.

Um den Text zurück in Bytes zu verwandeln, verwende base64.b64decode(text, validate=True), wie es basic_credentials bei einer Anmeldung tut. Das zweite zählt hier mehr als dort. Ohne es verwirft b64decode jedes Zeichen, das es nicht kennt, und dekodiert, was übrig bleibt:

import base64

text = "change this text to the real signing key before the server goes live"
print(len(base64.b64decode(text)))

Das gibt 42 aus. Eine Notiz, die jemand in einer Einstellungsdatei hinterlassen hat, ist zu 42 Bytes Schlüssel geworden, und jeder, der die Datei gesehen hat, kennt sie. Mit validate=True löst derselbe Aufruf binascii.Error aus, eine Art von , mit der Meldung Only base64 data is allowed.

Ein Schlüssel muss außerdem lang genug sein. Einer, der kürzer ist als die 32 Bytes eines -Digests, ist der schwache Teil des Tags, und deshalb hat Kapitel 2 Schlüssel 32 Bytes lang gemacht. signing.py hat diese Zahl als KEY_BYTES.

Fail closed

Wenn eine Sicherheitsanforderung nicht erfüllt ist, lehne ab. Diese Regel heißt fail closed (im Fehlerfall geschlossen), und darum ging es in der letzten Frage von Lektion 3. Jede andere Wahl lässt den Dienst gesund aussehen, obwohl er es nicht ist: ein eingebauter Schlüssel, den jeder kennt, ein zufälliger Schlüssel, der bei jedem Neustart jedes Token beendet, oder ein Server, der läuft und jeden ablehnt.

Ein Entwickler schreibt environ.get("READING_KEY", "dev-key"). Was kostet dieser Standardwert?

Schreib zuerst den Angriff

Der Angriff ist ein Server, der ohne Schlüssel in seiner Umgebung hochfährt. Als Test: signing.load_keys({}) muss ValueError auslösen.

Ein Ladeprogramm, das für alles ValueError auslöst, würde diesen Test bestehen und wäre nutzlos, also beginnt test_attack_missing_key_is_refused mit einer Vorbedingung: Der Testschlüssel lässt sich laden. steps.TEST_KEY enthält denselben Text wie README.md, und signing.load_keys({"READING_KEY": steps.TEST_KEY})["current"] muss "29c565a5" sein. Dann kommt die Ablehnung. Dieser Kurs verwendet pytest.raises nicht: Setz ein Flag auf False, ruf die in try auf, setz das Flag in except ValueError auf True und sichere das Flag zu.

Führ python -m pytest -q aus. Der neue Test schlägt in seiner ersten Zeile fehl, weil load_keys noch NotImplementedError auslöst:

1 failed, 13 passed

Die Zuordnung ist ein Argument

load_keys(environ) liest nur das , das es bekommt. Es importiert os nicht und sieht sich nie die echte Umgebung an. main übergibt os.environ, ein Test übergibt {}, und beide führen denselben Code aus.

handle() sieht sich die Umgebung überhaupt nie an. main speichert, was load_keys zurückgegeben hat, in state["keys"], und ab Lektion 6 findet der Anfragecode seine Schlüssel dort.

Sag, warum, auf dem richtigen Stream, ohne das Geheimnis

Wenn load_keys ValueError auslöst, fängt main es ab, gibt eine Zeile auf der Standardfehlerausgabe aus und gibt 1 zurück. Fang es als except ValueError as error ab und gib mit file=sys.stderr aus. main hat dafür bereits einen environ; wenn er None ist, verwende os.environ.

Die Standardfehlerausgabe und ein Exit-Code ungleich null sind der Weg, auf dem das, was den Server gestartet hat, erfährt, dass er nicht gestartet ist. Die Zeile nennt die und wiederholt nie ihren . Eine Fehlermeldung ist irgendwo eine Log-Zeile, und Lektion 3 hat ein Log zu den Stellen gezählt, an denen ein Schlüssel nicht sein darf. Das gilt auch für einen falschen Wert: Ein Schlüssel, dem ein Zeichen fehlt, ist immer noch fast ein ganzer Schlüssel.

Warum gibt main die Ablehnung auf der Standardfehlerausgabe aus und gibt 1 zurück, statt auf der Standardausgabe auszugeben und 0 zurückzugeben?

Was load_keys zurückgibt

Die Arbeit ist auf zwei Funktionen verteilt. key_from(environ, name) liest eine Variable und gibt die Schlüssel-Bytes zurück oder löst ValueError aus. load_keys(environ) ruft es für READING_KEY auf und gibt ein Dictionary zurück:

{"current": "29c565a5", "by_id": {"29c565a5": b"test-only key, never use it: 001"}}

Mit einem Schlüssel ist das mehr Struktur, als du brauchst. Die nächste Lektion fügt einen zweiten Schlüssel hinzu, und sonst muss sich nichts ändern. Der kurze Text ist eine Schlüssel-ID, aus dem mitgelieferten key_id(key): die ersten 8 Hex-Ziffern des SHA-256-Digests des Schlüssels. Er ist ein öffentlicher Name für eine geheime Sache. Ein Digest lässt sich nicht in einen Schlüssel aus 32 zufälligen Bytes zurückverwandeln.

Deine Aufgabe

Schreib den Test und sieh zu, wie er fehlschlägt. Schreib dann key_from und load_keys in signing.py und lade die Schlüssel in main in reading_server.py, an dem Kommentar, der die Stelle markiert. Die Übung führt die Schritte auf. Führ dann beide Befehle aus:

python reading_server.py; echo "exit code: $?"
READING_KEY=dGVzdC1vbmx5IGtleSwgbmV2ZXIgdXNlIGl0OiAwMDE= python reading_server.py --max-connections 1 --idle-timeout 1; echo "exit code: $?"

Der erste gibt eine Zeile und den Exit-Code aus. Wenn dein key_from READING_KEY is not set sagt, lautet sie:

reading_server: not starting: READING_KEY is not set
exit code: 1

Der zweite lauscht, wartet eine Sekunde auf einen Client, der nie kommt, hält an und gibt exit code: 0 aus. Führ python -m pytest -q noch einmal aus: 14 passed.

Fülle zuletzt die Zeile F7 von SECURITY.md aus. Was offenliegt, ist die Macht, ein Token für jeden Benutzer zu signieren. Nenne deinen Test in der Zelle Test und setz den Status auf closed in 5.4.

Klicke dann auf Check my work. Dein Server hat jetzt einen Schlüssel mit einem Zuhause. Er hat allerdings genau einen und keine Möglichkeit, ihn zu ersetzen, ohne jedes Token auf einmal zu beenden. Das ist die nächste Lektion.

Aufgabe

Lass den Server seinen Schlüssel aus der Umgebung laden und ohne einen den Start verweigern.

  1. Schreib in tests/test_attacks.py test_attack_missing_key_is_refused(): Sichere zuerst zu, dass signing.load_keys({"READING_KEY": steps.TEST_KEY})["current"] "29c565a5" ist, damit der Test ein Ladeprogramm betrifft, das einen guten Schlüssel akzeptiert; setz dann refused = False, ruf signing.load_keys({}) in try auf, setz refused = True in except ValueError und sichere refused zu. Er muss zuerst fehlschlagen.

  2. Schreib in signing.py key_from(environ, name). Lies environ.get(name, ""). Leerer Text löst mit einer Meldung aus, die die nennt. Dekodiere mit base64.b64decode(text, validate=True); Text, der kein ist, oder ein Schlüssel, der kürzer als KEY_BYTES ist, löst ebenfalls ValueError aus. Setz nie den der Variablen in eine Meldung. Gib die Schlüssel-Bytes zurück.

  3. Schreib load_keys(environ): Hol den Schlüssel mit key_from(environ, "READING_KEY") und gib {"current": key_id(key), "by_id": {key_id(key): key}} zurück.

  4. In main in reading_server.py: Wenn environ None ist, verwende os.environ. Ruf signing.load_keys(environ) auf. Bei ValueError (except ValueError as error) gib eine Zeile, die mit reading_server: not starting: beginnt, auf der Standardfehlerausgabe aus (print(..., file=sys.stderr)) und gib 1 zurück. Andernfalls speichere das Ergebnis in state["keys"], bevor der Server erzeugt wird.

  5. Fülle die Zeile F7 von SECURITY.md aus, nenne deinen Test und setz den Status auf closed in 5.4.

Führ im python -m pytest -q aus und die zwei Befehle aus der Lektion, um zu sehen, wie der Server ablehnt und dann startet.

Klicke dann auf Check my work. Es ruft load_keys mit eigenen auf: ohne Schlüssel, mit einem leeren, mit zu kurzen Schlüsseln, mit Text, der kein Base64 ist, und mit guten Schlüsseln, die du noch nicht gesehen hast. Eine Ablehnung muss ein ValueError sein, dessen Meldung den Wert nicht wiederholt. Es ruft key_from mit einem anderen Variablennamen auf und main über seinen environ, ohne dass etwas lauscht: Ohne Schlüssel darf main keinen Server bauen, muss einen Code ungleich null zurückgeben und genau eine Zeile auf die Standardfehlerausgabe schreiben. Es führt deinen Test dreimal aus, jedes Mal mit anderen Schlüsselfunktionen anstelle derer in signing.py: einem korrekten Ladeprogramm, wo dein Test durchlaufen muss; einem Ladeprogramm mit einem eingebauten Schlüssel, wo er fehlschlagen muss; und einem Ladeprogramm, das jeden Schlüssel ablehnt, wo er ebenfalls fehlschlagen muss. Außerdem liest es die Zeile F7 von SECURITY.md; die Formulierung deines Befunds wird nicht bewertet. Deine echte Umgebung wird nie gelesen. Nichts aus deinem Terminal wird bewertet.