0%

Signierte Tokens und geheime Schlüssel · Abschlussprojekt

Abschlussprojekt: Projekt: Tokens, denen der Server vertrauen kann

signing.py kann ein Token ausgeben, eins lesen und einen Schlüssel ersetzen. reading_server.py benutzt davon noch nichts: Die Schreibrouten akzeptieren immer noch zwei aus sample_state(). Dieses Projekt verbindet beides und entfernt die Strings. Das muss der fertige Server tun:

FrageAntwort
Wer darf um ein Token bitten?Eine angemeldete : POST /tokens mit dem . Jeder andere bekommt 401 mit der Basic-Aufforderung der Anmelderoute
Was enthält das Token?user, can_write und exp
Wie lange lebt es?TOKEN_LIFETIME, 900 Sekunden, gezählt ab state["clock"]()
Was prüft eine Schreibroute?Das Tag unter state["keys"], dann exp gegen state["clock"]()
Ein Token, das die Prüfung nicht besteht oder abgelaufen ist401 mit WWW-Authenticate: Bearer
Ein Token, das die Prüfung besteht und nicht schreiben darf403, wie in with Python

Sieh nach, wo du stehst:

python pl_sec.py forged-token

Es meldet sich als ben an, bittet um ein Token und probiert dann Tokens aus, die nicht funktionieren dürfen. Heute erzählen zwei Zeilen die Geschichte:

ben asks for a token, with the cookie  POST /tokens  -> 404
a token that was once in the source code  POST /books -> 201  accepted

Woher ein Token jetzt kommt

Ein Leser meldet sich an, was ihm die Session aus Kapitel 4 gibt, und sendet dann POST /tokens mit dem Session-Cookie. Die Session beweist, wer er ist. Das Token ist das, was ein Programm danach sendet, ohne Cookie und ohne Passwort.

current_session(request, state) findet die Session. Eine Besucher-Session hat keinen Benutzer, also bekommt sie dieselbe 401 wie gar keine Session. Diese 401 trägt die Basic-Aufforderung von /login, weil ein Client durch die Anmeldung die Session bekommt, die ihm fehlt.

can_write stand früher neben jedem festen Token. Jetzt kommt es aus state["writers"], einem Set von Namen, das sample_state() schon hat: Das Token sagt user in state["writers"]. Ein Token trägt diese Antwort so, wie sie war, als das Token erzeugt wurde. Ein Benutzer, der aus dem Set entfernt wird, schreibt weiter, bis das Token abläuft.

Meilensteine

  1. Schreib den Angriff. test_attack_token_from_the_source_is_refused baut seinen Zustand mit steps.keyed_state() auf, das ist sample_state() mit dem Testschlüssel in state["keys"]. Ein Test führt main nie aus, also würde sonst nichts Schlüssel dort hineinlegen. Die Vorbedingung ist eine Anmeldung, die funktioniert; der Angriff ist ein POST /books mit Bearer tok-ana-writes-7Qx2. Führ python -m pytest -q aus: 1 failed, 14 passed, weil der String die Route immer noch öffnet.

  2. Tokens ausgeben. Schreib token_response, füge handle den Zweig für tokens hinzu und nimm tokens in die Regel für no-store auf: Ein Token ist ein Berechtigungsnachweis, und kein Cache darf eins behalten.

  3. Tokens prüfen. Schreib token_claims und schreib write_error so um, dass es allein anhand der Claims entscheidet. Dein Test läuft durch.

  4. Die Strings entfernen. Lösche den Eintrag "tokens" aus sample_state(). Nichts liest ihn mehr.

  5. Zwei weitere Angriffe. Beide beginnen mit steps.bearer_for(state, user, password), das sich anmeldet, POST /tokens fragt und den ganzen für Authorization zurückgibt, Bearer eingeschlossen. Sie werden unten beschrieben.

  6. Sieh zu. Führ noch einmal python pl_sec.py forged-token aus.

  7. Schreib es auf. Fülle und schließe F6 in SECURITY.md und füge F14 hinzu.

test_attack_changed_token_is_refused verwendet steps.keyed_state({"now": 1000000}), sodass die Uhr in einem Moment stillsteht, in dem ein exp von 2000000 noch in der Zukunft liegt. Dann kann nur das Tag das gefälschte Token ablehnen. Hol bens Wert mit bearer_for und teile ihn an den Punkten. Bau die Payload {"user": "ana", "can_write": True, "exp": 2000000}, mach daraus mit signing.b64u_encode(json.dumps(payload).encode("utf-8")) Text und verbinde den ersten Teil, diesen Text und den alten dritten Teil mit Punkten. Sichere zuerst zu, dass bens ehrliches Token als seins zurückgelesen wird: signing.read_token(state["keys"], bearer.split(" ")[1], now=1000000)["user"] ist "ben". Sende dann POST /books mit dem gefälschten Wert und sichere 401 zu.

test_attack_expired_token_is_refused behält seine Uhr: clock = {"now": 1000000}, dann steps.keyed_state(clock). Hol anas Wert, sende POST /books und sichere 201 zu. Addiere 16 * 60 zu clock["now"], sende dieselbe Anfrage noch einmal und sichere 401 zu.

Nach Meilenstein 5 sagt python -m pytest -q 17 passed. Das Szenario aus Meilenstein 6 zeigt dann, was in bens Token steckt, und drei Ablehnungen. Unter seinen Zeilen:

its payload, read without any key: {"user": "ben", "can_write": false, "exp": 1000900}
ben adds a book with his own token       POST /books -> 403  {"error": "this token may read the list but not change it"}
the changed payload with the old tag      POST /books -> 401  refused
a token that was once in the source code  POST /books -> 401  refused
ben's own token, 16 minutes old           POST /books -> 401  refused
3 of 3 bad tokens refused

Ein Befund bleibt offen

F6 zu schließen öffnet eine kleinere Lücke, und eine ehrliche sagt das. Füge unter F7 die Zeile F14 hinzu:

  • Exposed: was auch immer ein Token erlaubt, bis zu 15 Minuten lang.

  • Attack: Ein Token, das geleakt wird, funktioniert, bis es abläuft. Eine Abmeldung beendet es nicht, und der Server führt keine Liste, auf der er es streichen könnte.

  • Fix: ein kurzes Leben, was erledigt ist, und in einem echten Dienst eine gespeicherte Liste zurückgezogener Tokens, die nicht gebaut wird.

  • Test: none yet. Status: open.

Ana bekommt ein Token, und fünf Minuten später meldet sie sich ab. Das Token wird noch einmal gesendet. Was passiert?

Das Tag eines Tokens stimmt, und sein exp ist seit einer Minute vorbei. Welche Antwort?

Klicke dann auf Check my work. Es prüft die Routen mit eigenen Benutzern, Schlüsseln und einer eigenen Uhr, ruft main auf wie die Prüfung von Lektion 4 und führt deine vier Tests dieses Kapitels gegen eigene Server aus, wo jeder Test an dem Fehler scheitern muss, um den es ihm geht.

Die festen Strings sind weg, und ein Token beweist jetzt, wer fragt. Es sagt noch nicht, welche Bücher diese Person anfassen darf: Das Token jedes Schreibers öffnet immer noch jedes Buch. Das ist Kapitel 6.

Aufgabe

Gib angemeldeten signierte Tokens aus, akzeptiere auf den Schreibrouten nichts anderes und halte fest, was offen bleibt.

  1. Schreib test_attack_token_from_the_source_is_refused(): Sichere mit state = steps.keyed_state() zu, dass steps.login_cookie(state, "ana", "correct horse") nicht None ist, sende dann POST /books mit einem gültigen Buch und dem Header Authorization: Bearer tok-ana-writes-7Qx2 und sichere den Status 401 zu. Er muss zuerst fehlschlagen.

  2. Schreib token_response(state, user): expires ist int(state["clock"]()) + TOKEN_LIFETIME; die Claims sind der Benutzer und ob der Benutzer in state["writers"] steht; erzeuge das Token mit signing.issue_token(state["keys"], claims, expires=expires); gib json_response(200, {"token": token, "expires": expires}) zurück.

  3. Füge die Route hinzu: POST /tokens antwortet mit unauthorized(REALM), wenn current_session(request, state) None ist oder keinen Benutzer hat, und sonst mit token_response(...); eine andere bekommt not_allowed("POST"). Nimm tokens in die Regel für no-store auf.

  4. Schreib token_claims(request, state): das Bearer-Token, gelesen mit signing.read_token(state["keys"], token, now=state["clock"]()), oder None, wenn es kein Bearer-Token gibt.

  5. Schreib write_error so um, dass es token_claims verwendet: 401 mit unauthorized("Bearer"), wenn es None zurückgibt, 403, wenn can_write falsch ist.

  6. Entferne "tokens" aus sample_state() und nimm die zwei Tokens aus seinem Docstring.

  7. Schreib test_attack_changed_token_is_refused() und test_attack_expired_token_is_refused(), wie die Lektion sie beschreibt.

  8. In SECURITY.md: Fülle und schließe F6 und füge die Zeile F14 als offen hinzu.

Führ im python -m pytest -q aus, um deine Tests auszuführen, und python pl_sec.py forged-token, um zuzusehen, wie drei schlechte Tokens abgelehnt werden.

Klicke dann auf Check my work. Mit eigenen Benutzern, Schreibern, Büchern, zwei Schlüsseln und einer festen Uhr sendet es einen festen aus dem alten , fragt POST /tokens ohne , mit einer Besucher-Session und mit angemeldeten Sessions und liest jedes Token, das es bekommt, mit eigenem Code: den Benutzer, can_write und eine Lebensdauer von höchstens 900 Sekunden. Es schreibt mit dem Token eines Schreibers, dem Token eines Lesers, einem geänderten Token, einem Token, das 901 Sekunden alt ist, und einem Token von einem Schlüssel, den dein Server nicht hat. Es ruft main auf wie in Lektion 4 und schickt ein POST /tokens als Bytes durch die Handler-Klasse deines Servers, ohne einen zu öffnen. Es führt deine vier Tests dieses Kapitels gegen eigene Server aus: Gegen einen korrekten müssen alle durchlaufen, jeder muss gegen den Fehler fehlschlagen, um den es ihm geht, und die drei Token-Tests müssen gegen einen Server fehlschlagen, der alles ablehnt. Es liest die Zeilen F6, F7 und F14 von SECURITY.md; die Formulierung deiner Befunde wird nicht bewertet. Nichts aus deinem Terminal wird bewertet.