Signierte Tokens und geheime Schlüssel · Übung
Tokens, die ablaufen
Das Token aus Lektion 1 sagt, für wen es ist, und beweist, dass der Server es geschrieben hat. Es funktioniert auch für immer, wie die festen
In dieser Lektion schreibst du in signing.py die zwei issue_token erzeugt ein Token, und read_token entscheidet, ob man einem glaubt.
Text, der in einen Header passt
Ein Token reist in einem Header Authorization: Bearer, und dieser = nur ganz am Ende enthalten. = auf, und in payload.tag würde das Padding in der Mitte landen. Deshalb hat signing.py jetzt zwei kleine Hilfsfunktionen, die du lesen kannst und nie ändern musst:
def b64u_encode(data):
"""Return bytes as URL-safe base64 text without the = padding."""
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
def b64u_decode(text):
"""Return the bytes that b64u_encode turned into text."""
return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
b64u_encode nimmt Bytes und gibt Text ohne Padding zurück. b64u_decode fügt das Padding wieder an und gibt die Bytes zurück. Verwende diese beiden überall, wo aus einer Payload Text wird oder aus Text eine Payload.
Den Ablauf hineinlegen
Die Payload bekommt ein drittes Feld:
payload = {"user": claims["user"], "can_write": claims["can_write"], "exp": expires}
exp ist ein Zeitpunkt in Sekunden, dieselbe Art von Zahl, die state["clock"]() zurückgibt. Bau die Payload Feld für Feld auf, wie gezeigt. claims ist ein
Am wichtigsten ist, wo exp steht: in der Payload, also in dem Text, den das Tag abdeckt. Das Bild aus Lektion 1 gilt weiterhin, mit einem Feld mehr im issue_token macht aus der Payload mit b64u_encode(json.dumps(payload).encode("utf-8")) Text, signiert diesen Text mit sign aus Kapitel 2 und gibt den Text, einen Punkt und das Tag zurück.
Ein Token ist payload.1731240900.tag, und das Tag deckt nur die Payload ab. Was kann sein Besitzer tun?
Erst das Tag prüfen, dann dekodieren
read_token bekommt Text von einem Fremden. Bis das Tag geprüft ist, ist er genau das: die Bytes eines Fremden. Sie zu dekodieren und zu parsen lässt deinen Code auf einer Eingabe laufen, für die niemand gebürgt hat, und ein Feld daraus zu benutzen ist noch schlimmer.
Die Reihenfolge steht also fest. Teile das Token an den Punkten. Prüfe das Tag über den ersten Teil mit verify(key, ...), der Funktion aus Kapitel 2, die mit hmac.compare_digest vergleicht. Erst wenn das Tag stimmt, dekodiere den ersten Teil mit b64u_decode und parse ihn mit json.loads. Sieh dir dann exp an.
verify nimmt die Nachricht als Bytes und das Tag als Text, also geht der erste Teil als parts[0].encode("utf-8") hinein. read_token darf bei einem Token nie einen Fehler auslösen, egal was es enthält.
read_token dekodiert die Payload, um exp zu lesen, und prüft das Tag danach. Ein Token mit einem falschen Tag kommt an. Was ist an dieser Reihenfolge falsch?
Jedes falsche Token ist None
Ein Token kann auf mehrere Arten falsch sein: Es hat nicht genau zwei Teile, sein Tag ist falsch, sein Tag wurde mit einem anderen Schlüssel erzeugt, oder sein exp ist vorbei. read_token gibt für alle eine Antwort, None, so wie Kapitel 3 eine Antwort für jede falsche Anmeldung gegeben hat. Der Aufrufer erfährt, dass das Token nicht gut ist, und nichts darüber, warum. Ein Token, das jede Prüfung besteht, gibt seine Payload als Dictionary zurück, sodass der Aufrufer user und can_write daraus lesen kann.
Ein Token läuft ab, wenn now exp erreicht hat: in dieser Sekunde und in jeder Sekunde danach. now ist ein read_token, also liest nichts in signing.py eine Uhr. Der Server wird state["clock"]() übergeben, und ein Test kann jede beliebige Zahl übergeben.
Fünfzehn Minuten
Lektion 1 hat den Preis eines Tokens genannt, das der Server nicht speichert: Niemand kann es zurücknehmen. Der Ablauf ist der einzige Weg, wie dieses Token endet, also muss sein Leben kurz sein. Im Projekt dieses Kapitels setzt der Server exp höchstens 900 Sekunden in die Zukunft, also 15 Minuten. Ein Token, das geleakt wird, ist dann 15 Minuten lang ein Problem, nicht so lange, wie der Server läuft.
Deine Aufgabe
Schreib in signing.py issue_token(key, claims, *, expires) und read_token(key, token, *, now). Die Übung führt die Schritte auf. Probier das Paar dann von Hand aus:
python -c "import signing; k = signing.new_key(); t = signing.issue_token(k, {'user': 'ana', 'can_write': True}, expires=2000); print(t); print(signing.read_token(k, t, now=1999)); print(signing.read_token(k, t, now=2001)); print(signing.read_token(signing.new_key(), t, now=1999))"
Die erste Zeile ist das Token. Sein Payload-Text ist eyJ1c2VyIjogImFuYSIsICJjYW5fd3JpdGUiOiB0cnVlLCAiZXhwIjogMjAwMH0, ohne = am Ende, und nach dem Punkt kommen 64 Hex-Zeichen, die bei jedem Lauf anders sind, weil new_key() jedes Mal einen neuen Schlüssel erzeugt. Die anderen drei Zeilen sind bei jedem Lauf gleich:
{'user': 'ana', 'can_write': True, 'exp': 2000}
None
None
Eine Sekunde vor exp gibt das Token seine Payload zurück. Eine Sekunde danach ist es None. Mit einem anderen Schlüssel gelesen, ist es ebenfalls None.
Beachte, woher der Schlüssel kam: aus signing.new_key(), auf der Stelle erzeugt und weg, wenn der Befehl endet. So kann ein Server nicht arbeiten. Wo sein Schlüssel lebt, ist das Thema der nächsten Lektion.
Klicke dann auf Check my work.
Aufgabe
Schreib die zwei Token-signing.py.
Schreib
issue_token(key, claims, *, expires). Bau die Payload{"user": claims["user"], "can_write": claims["can_write"], "exp": expires}. Mach daraus mitb64u_encode(json.dumps(payload).encode("utf-8"))Text. Gib diesen Text, einen Punkt undsign(key, ...)des alskodierten Texts zurück. Schreib
read_token(key, token, *, now). Teile das Token an Punkten; alles außer zwei Teilen istNone. Prüfe das Tag mitverify(key, ...)über den als UTF-8 kodierten ersten Teil; ein falsches Tag istNone. Erst dann dekodiere den ersten Teil mitb64u_decodeund parse ihn mitjson.loads. Wennnowexperreicht hat, gibNonezurück. Andernfalls gib die Payload zurück.
Führ im python -c aus der Lektion aus, um ein Token zu erzeugen und es auf drei Arten zurückzulesen.
Klicke dann auf Check my work. Es ruft issue_token mit einem Schlüssel, Benutzern und Ablaufzeitpunkten auf, die du noch nicht gesehen hast, und mit Claims, die ein zusätzliches Feld enthalten, das nicht in die Payload gelangen darf; es dekodiert die Payload selbst und berechnet das Tag selbst. Es ruft read_token mit einem gültigen Token eine Sekunde vor und eine Sekunde nach seinem exp auf und mit Tokens, die None ergeben müssen und nie einen Fehler: einer geänderten Payload mit dem alten Tag, einem Token von einem anderen Schlüssel, einem gekürzten Tag, einem leeren Tag und Texten, die gar kein Token sind. Außerdem zählt es Aufrufe: Beim Lesen eines gültigen Tokens muss das Tag über verify oder hmac.compare_digest verglichen werden, und beim Lesen eines Tokens mit falschem Tag dürfen b64u_decode und json.loads nicht aufgerufen werden. Nichts aus deinem Terminal wird bewertet.