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 sample_state(). Dieses Projekt verbindet beides und entfernt die Strings. Das muss der fertige Server tun:
| Frage | Antwort |
|---|---|
| Wer darf um ein Token bitten? | Eine angemeldete POST /tokens mit dem |
| 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 ist | 401 mit WWW-Authenticate: Bearer |
| Ein Token, das die Prüfung besteht und nicht schreiben darf | 403, wie in |
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
Schreib den Angriff.
test_attack_token_from_the_source_is_refusedbaut seinen Zustand mitsteps.keyed_state()auf, das istsample_state()mit dem Testschlüssel instate["keys"]. Ein Test führtmainnie aus, also würde sonst nichts Schlüssel dort hineinlegen. Die Vorbedingung ist eine Anmeldung, die funktioniert; der Angriff ist einPOST /booksmitBearer tok-ana-writes-7Qx2. Führpython -m pytest -qaus:1 failed, 14 passed, weil der String die Route immer noch öffnet.Tokens ausgeben. Schreib
token_response, fügehandleden Zweig fürtokenshinzu und nimmtokensin die Regel fürno-storeauf: Ein Token ist ein Berechtigungsnachweis, und kein Cache darf eins behalten.Tokens prüfen. Schreib
token_claimsund schreibwrite_errorso um, dass es allein anhand der Claims entscheidet. Dein Test läuft durch.Die Strings entfernen. Lösche den Eintrag
"tokens"aussample_state(). Nichts liest ihn mehr.Zwei weitere Angriffe. Beide beginnen mit
steps.bearer_for(state, user, password), das sich anmeldet,POST /tokensfragt und den ganzenfür Authorizationzurückgibt,Bearereingeschlossen. Sie werden unten beschrieben.Sieh zu. Führ noch einmal
python pl_sec.py forged-tokenaus.Schreib es auf. Fülle und schließe F6 in
SECURITY.mdund 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
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
Schreib
test_attack_token_from_the_source_is_refused(): Sichere mitstate = steps.keyed_state()zu, dasssteps.login_cookie(state, "ana", "correct horse")nichtNoneist, sende dannPOST /booksmit einem gültigen Buch und dem HeaderAuthorization: Bearer tok-ana-writes-7Qx2und sichere den Status 401 zu. Er muss zuerst fehlschlagen.Schreib
token_response(state, user):expiresistint(state["clock"]()) + TOKEN_LIFETIME; die Claims sind der Benutzer und ob der Benutzer instate["writers"]steht; erzeuge das Token mitsigning.issue_token(state["keys"], claims, expires=expires); gibjson_response(200, {"token": token, "expires": expires})zurück.Füge die Route hinzu:
POST /tokensantwortet mitunauthorized(REALM), wenncurrent_session(request, state)Noneist oder keinen Benutzer hat, und sonst mittoken_response(...); eine anderebekommt not_allowed("POST"). Nimmtokensin die Regel fürno-storeauf.Schreib
token_claims(request, state): das Bearer-Token, gelesen mitsigning.read_token(state["keys"], token, now=state["clock"]()), oderNone, wenn es kein Bearer-Token gibt.Schreib
write_errorso um, dass estoken_claimsverwendet: 401 mitunauthorized("Bearer"), wenn esNonezurückgibt, 403, wenncan_writefalsch ist.Entferne
"tokens"aussample_state()und nimm die zwei Tokens aus seinem Docstring.Schreib
test_attack_changed_token_is_refused()undtest_attack_expired_token_is_refused(), wie die Lektion sie beschreibt.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 POST /tokens ohne 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 SECURITY.md; die Formulierung deiner Befunde wird nicht bewertet. Nichts aus deinem Terminal wird bewertet.