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 sample_state(). Dit project verbindt de twee en verwijdert de strings. Dit moet de afgemaakte server doen:
| Vraag | Antwoord |
|---|---|
| Wie mag om een token vragen? | Een ingelogde POST /tokens met de |
| 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 is | 401 met WWW-Authenticate: Bearer |
| Een token dat geverifieerd wordt en niet mag schrijven | 403, zoals in |
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
Schrijf de aanval.
test_attack_token_from_the_source_is_refusedbouwt zijn state metsteps.keyed_state(), dat issample_state()met de testsleutel instate["keys"]. Een test draaitmainnooit, dus niets anders zou daar sleutels zetten. Devooraf is een login die werkt; de aanval is een POST /booksmetBearer tok-ana-writes-7Qx2. Voerpython -m pytest -quit:1 failed, 14 passed, omdat de string de route nog opent.Geef tokens uit. Schrijf
token_response, voeg de taktokenstoe aanhandle, en voegtokenstoe aan de regel voorno-store: een token is een inloggegeven, en geen cache mag er een bewaren.Controleer tokens. Schrijf
token_claims, en herschrijfwrite_errorzo dat hij alleen op grond van de claims beslist. Je test slaagt.Verwijder de strings. Verwijder de vermelding
"tokens"uitsample_state(). Niets leest hem nog.Nog twee aanvallen. Beide beginnen met
steps.bearer_for(state, user, password), dat inlogt,POST /tokensvraagt, en de helevan Authorizationteruggeeft, inclusiefBearer. Ze worden hieronder beschreven.Bekijk het. Voer
python pl_sec.py forged-tokenopnieuw uit.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
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
Schrijf
test_attack_token_from_the_source_is_refused(): controleer metstate = steps.keyed_state()datsteps.login_cookie(state, "ana", "correct horse")nietNoneis, stuur daarnaPOST /booksmet een geldig boek en de headerAuthorization: Bearer tok-ana-writes-7Qx2, en controleer status 401. Hij moet eerst falen.Schrijf
token_response(state, user):expiresisint(state["clock"]()) + TOKEN_LIFETIME; de claims zijn de gebruiker en of de gebruiker instate["writers"]staat; maak het token metsigning.issue_token(state["keys"], claims, expires=expires); geefjson_response(200, {"token": token, "expires": expires})terug.Voeg de route toe:
POST /tokensantwoordtunauthorized(REALM)alscurrent_session(request, state)Noneis of geen gebruiker heeft, en anderstoken_response(...); een anderekrijgt not_allowed("POST"). Voegtokenstoe aan de regel voorno-store.Schrijf
token_claims(request, state): het bearer-token gelezen metsigning.read_token(state["keys"], token, now=state["clock"]()), ofNoneals er geen bearer-token is.Herschrijf
write_errorzo dat hijtoken_claimsgebruikt: 401 metunauthorized("Bearer")als dieNoneteruggeeft, 403 alscan_writeonwaar is.Verwijder
"tokens"uitsample_state(), en haal de twee tokens uit de docstring ervan.Schrijf
test_attack_changed_token_is_refused()entest_attack_expired_token_is_refused()zoals de les beschrijft.In
SECURITY.md: vul en sluit F6, en voeg rij F14 toe als open.
Voer python -m pytest -q uit in de 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 POST /tokens zonder 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 SECURITY.md; de formulering van je bevindingen wordt niet beoordeeld. Niets uit je terminal wordt beoordeeld.