0%

Ondertekende tokens en geheime sleutels · eindproject

Eindproject: Project: tokens die de server kan vertrouwen

signing.py kan een token uitgeven, er een lezen, en een sleutel vervangen. reading_server.py gebruikt daar nog niets van: de schrijfroutes accepteren nog steeds twee uit sample_state(). Dit project verbindt de twee en verwijdert de strings. Dit moet de afgemaakte server doen:

VraagAntwoord
Wie mag om een token vragen?Een ingelogde : POST /tokens met de . Iedereen anders krijgt 401 met de Basic-challenge van de loginroute
Wat bevat het token?user, can_write en exp
Hoe lang leeft het?TOKEN_LIFETIME, 900 seconden, geteld vanaf state["clock"]()
Wat controleert een schrijfroute?De tag onder state["keys"], daarna exp tegen state["clock"]()
Een token dat niet geverifieerd wordt, of verlopen is401 met WWW-Authenticate: Bearer
Een token dat geverifieerd wordt en niet mag schrijven403, zoals in with Python

Kijk waar je staat:

python pl_sec.py forged-token

Het logt in als ben, vraagt om een token, en probeert daarna tokens die niet mogen werken. Vandaag vertellen twee regels het verhaal:

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

Waar een token nu vandaan komt

Een lezer logt in, wat hem de sessie uit hoofdstuk 4 geeft, en stuurt daarna POST /tokens met de sessiecookie. De sessie bewijst wie hij is. Het token is wat een programma daarna stuurt, zonder cookie en zonder wachtwoord.

current_session(request, state) vindt de sessie. Een bezoekerssessie heeft geen gebruiker, dus die krijgt dezelfde 401 als helemaal geen sessie. Die 401 draagt de Basic-challenge van /login, omdat inloggen de manier is waarop een client de sessie krijgt die hij mist.

can_write stond vroeger naast elk vast token. Nu komt het uit state["writers"], een set van namen die sample_state() al heeft: het token zegt user in state["writers"]. Een token draagt dat antwoord zoals het was toen het token gemaakt werd. Een gebruiker die uit de set verwijderd wordt, blijft schrijven tot het token verloopt.

Mijlpalen

  1. Schrijf de aanval. test_attack_token_from_the_source_is_refused bouwt zijn state met steps.keyed_state(), dat is sample_state() met de testsleutel in state["keys"]. Een test draait main nooit, dus niets anders zou daar sleutels zetten. De vooraf is een login die werkt; de aanval is een POST /books met Bearer tok-ana-writes-7Qx2. Voer python -m pytest -q uit: 1 failed, 14 passed, omdat de string de route nog opent.

  2. Geef tokens uit. Schrijf token_response, voeg de tak tokens toe aan handle, en voeg tokens toe aan de regel voor no-store: een token is een inloggegeven, en geen cache mag er een bewaren.

  3. Controleer tokens. Schrijf token_claims, en herschrijf write_error zo dat hij alleen op grond van de claims beslist. Je test slaagt.

  4. Verwijder de strings. Verwijder de vermelding "tokens" uit sample_state(). Niets leest hem nog.

  5. Nog twee aanvallen. Beide beginnen met steps.bearer_for(state, user, password), dat inlogt, POST /tokens vraagt, en de hele van Authorization teruggeeft, inclusief Bearer. Ze worden hieronder beschreven.

  6. Bekijk het. Voer python pl_sec.py forged-token opnieuw uit.

  7. Schrijf het op. Vul en sluit F6 in SECURITY.md, en voeg F14 toe.

test_attack_changed_token_is_refused gebruikt steps.keyed_state({"now": 1000000}), zodat de klok stilstaat op een moment waarop een exp van 2000000 nog in de toekomst ligt. Dan kan alleen de tag het vervalste token weigeren. Haal de waarde van ben op met bearer_for en splits die bij de punten. Bouw de payload {"user": "ana", "can_write": True, "exp": 2000000}, maak er tekst van met signing.b64u_encode(json.dumps(payload).encode("utf-8")), en verbind het eerste deel, die tekst en het oude derde deel met punten. Controleer eerst dat het eerlijke token van ben als het zijne wordt teruggelezen: signing.read_token(state["keys"], bearer.split(" ")[1], now=1000000)["user"] is "ben". Stuur daarna POST /books met de vervalste waarde en controleer 401.

test_attack_expired_token_is_refused houdt zijn klok bij: clock = {"now": 1000000}, daarna steps.keyed_state(clock). Haal de waarde van ana op, stuur POST /books, en controleer 201. Tel 16 * 60 op bij clock["now"], stuur hetzelfde verzoek opnieuw, en controleer 401.

Na mijlpaal 5 zegt python -m pytest -q 17 passed. Het scenario van mijlpaal 6 laat daarna zien wat er in het token van ben zit, en drie weigeringen. Tussen de regels:

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

Eén bevinding blijft open

F6 sluiten opent een kleiner gat, en een eerlijke zegt dat. Voeg rij F14 toe onder F7:

  • Exposed: wat een token ook toestaat, tot 15 minuten lang.

  • Attack: een token dat lekt, werkt tot het verloopt. Uitloggen beëindigt het niet, en de server houdt geen lijst bij om het van door te strepen.

  • Fix: een kort leven, en dat is gedaan, en in een echte dienst een opgeslagen lijst van ingetrokken tokens, en die is niet gebouwd.

  • Test: none yet. Status: open.

Ana krijgt een token, en vijf minuten later logt ze uit. Het token wordt opnieuw gestuurd. Wat gebeurt er?

De tag van een token klopt, en zijn exp is een minuut geleden verstreken. Welk antwoord?

Klik daarna op Check my work. Die controleert de routes met eigen gebruikers, sleutels en een eigen klok, roept main aan zoals de controle van les 4 deed, en voert je vier tests van dit hoofdstuk uit tegen eigen servers, waar elke test moet falen op de fout waar hij over gaat.

De vaste strings zijn weg, en een token bewijst nu wie er vraagt. Het zegt nog niet welke boeken die persoon mag aanraken: het token van elke schrijver opent nog steeds elk boek. Dat is hoofdstuk 6.

Opdracht

Geef ondertekende tokens uit aan ingelogde , accepteer niets anders op de schrijfroutes, en leg vast wat open blijft.

  1. Schrijf test_attack_token_from_the_source_is_refused(): controleer met state = steps.keyed_state() dat steps.login_cookie(state, "ana", "correct horse") niet None is, stuur daarna POST /books met een geldig boek en de header Authorization: Bearer tok-ana-writes-7Qx2, en controleer status 401. Hij moet eerst falen.

  2. Schrijf token_response(state, user): expires is int(state["clock"]()) + TOKEN_LIFETIME; de claims zijn de gebruiker en of de gebruiker in state["writers"] staat; maak het token met signing.issue_token(state["keys"], claims, expires=expires); geef json_response(200, {"token": token, "expires": expires}) terug.

  3. Voeg de route toe: POST /tokens antwoordt unauthorized(REALM) als current_session(request, state) None is of geen gebruiker heeft, en anders token_response(...); een andere krijgt not_allowed("POST"). Voeg tokens toe aan de regel voor no-store.

  4. Schrijf token_claims(request, state): het bearer-token gelezen met signing.read_token(state["keys"], token, now=state["clock"]()), of None als er geen bearer-token is.

  5. Herschrijf write_error zo dat hij token_claims gebruikt: 401 met unauthorized("Bearer") als die None teruggeeft, 403 als can_write onwaar is.

  6. Verwijder "tokens" uit sample_state(), en haal de twee tokens uit de docstring ervan.

  7. Schrijf test_attack_changed_token_is_refused() en test_attack_expired_token_is_refused() zoals de les beschrijft.

  8. In SECURITY.md: vul en sluit F6, en voeg rij F14 toe als open.

Voer python -m pytest -q uit in de om je tests te draaien, en python pl_sec.py forged-token om drie slechte tokens geweigerd te zien worden.

Klik daarna op Check my work. Met eigen gebruikers, schrijvers, boeken, twee sleutels en een vaste klok stuurt die een vaste uit de oude , vraagt hij POST /tokens zonder , met een bezoekerssessie en met ingelogde sessies, en leest hij elk token dat hij krijgt met eigen code: de gebruiker, can_write, en een leven van hoogstens 900 seconden. Hij schrijft met het token van een schrijver, het token van een lezer, een veranderd token, een token van 901 seconden oud, en een token van een sleutel die je server niet heeft. Hij roept main aan zoals in les 4, en stuurt één POST /tokens als bytes door de handlerklasse van je server, zonder een te openen. Hij voert je vier tests van dit hoofdstuk uit tegen eigen servers: alle moeten slagen tegen een correcte, elk moet falen tegen de fout waar hij over gaat, en de drie tokentests moeten falen tegen een server die alles weigert. Hij leest de rijen F6, F7 en F14 van SECURITY.md; de formulering van je bevindingen wordt niet beoordeeld. Niets uit je terminal wordt beoordeeld.