0%

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 , die es ersetzen soll. Ein Ablaufzeitpunkt behebt das, unter einer : Der Besitzer darf ihn nicht bearbeiten können.

In dieser Lektion schreibst du in signing.py die zwei , auf denen der Rest des Kapitels steht: 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 darf = nur ganz am Ende enthalten. füllt seine Ausgabe mit = 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 , das irgendein Aufrufer gefüllt hat, und eine Payload, die alles davon kopiert, würde signieren, was auch immer dieser Aufrufer zufällig hineingelegt hat.

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 oben. Das Token besteht immer noch aus zwei Teilen. 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. kann jeden Text kodieren, den ein Header liefern kann. Eine strengere Kodierung würde bei einem ungewöhnlichen Zeichen einen Fehler auslösen, und 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 von 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- in signing.py.

  1. Schreib issue_token(key, claims, *, expires). Bau die Payload {"user": claims["user"], "can_write": claims["can_write"], "exp": expires}. Mach daraus mit b64u_encode(json.dumps(payload).encode("utf-8")) Text. Gib diesen Text, einen Punkt und sign(key, ...) des als kodierten Texts zurück.

  2. Schreib read_token(key, token, *, now). Teile das Token an Punkten; alles außer zwei Teilen ist None. Prüfe das Tag mit verify(key, ...) über den als UTF-8 kodierten ersten Teil; ein falsches Tag ist None. Erst dann dekodiere den ersten Teil mit b64u_decode und parse ihn mit json.loads. Wenn now exp erreicht hat, gib None zurück. Andernfalls gib die Payload zurück.

Führ im die Zeile mit 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.