0%

Ondertekende tokens en geheime sleutels · oefening

Tokens die verlopen

Het token van les 1 zegt voor wie het is en bewijst dat de server het schreef. Het werkt ook voor altijd, net als de vaste die het moet vervangen. Een verlooptijd lost dat op, op één : de houder mag hem niet kunnen bewerken.

In deze les schrijf je de twee waarop de rest van het hoofdstuk staat, in signing.py: issue_token maakt een token, en read_token beslist of het er een gelooft.

Tekst die in een header past

Een token reist in een header Authorization: Bearer, en die mag = alleen helemaal aan het eind bevatten. vult zijn uitvoer op met =, en in payload.tag zou de opvulling in het midden belanden. Daarom heeft signing.py nu twee kleine hulpfuncties, die je kunt lezen en nooit hoeft te veranderen:

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 neemt bytes en geeft tekst terug zonder de opvulling. b64u_decode zet de opvulling terug en geeft de bytes terug. Gebruik deze twee overal waar een payload tekst wordt of tekst een payload wordt.

Zet het verloop erin

De payload krijgt een derde veld:

payload = {"user": claims["user"], "can_write": claims["can_write"], "exp": expires}

exp is een tijd in seconden, hetzelfde soort getal dat state["clock"]() teruggeeft. Bouw de payload veld voor veld, zoals hier te zien is. claims is een die een of andere aanroeper heeft gevuld, en een payload die hem helemaal kopieerde, zou ondertekenen wat die aanroeper er toevallig in had gezet.

Wat het meest telt, is waar exp staat: in de payload, dus in de tekst die de tag dekt. Het plaatje uit les 1 klopt nog steeds, met één veld meer in de bovenaan. Het token bestaat nog steeds uit twee delen. issue_token maakt van de payload tekst met b64u_encode(json.dumps(payload).encode("utf-8")), ondertekent die tekst met de sign uit hoofdstuk 2, en geeft de tekst, een punt en de tag terug.

Een token is payload.1731240900.tag, en de tag dekt alleen de payload. Wat kan de houder doen?

Controleer de tag, decodeer daarna

read_token ontvangt tekst van een vreemde. Tot de tag gecontroleerd is, is dat alles wat het is: de bytes van een vreemde. Ze decoderen en parsen laat je code draaien op invoer waar niemand voor instond, en een veld eruit gebruiken is nog erger.

De volgorde ligt dus vast. Splits het token bij de punten. Controleer de tag over het eerste deel met verify(key, ...), de functie uit hoofdstuk 2 die met hmac.compare_digest vergelijkt. Pas als de tag klopt, decodeer je het eerste deel met b64u_decode en parse je het met json.loads. Kijk daarna naar exp.

verify neemt het bericht als bytes en de tag als tekst, dus het eerste deel gaat erin als parts[0].encode("utf-8"). kan elke tekst coderen die een header kan afleveren. Een strengere codering zou een fout geven bij een vreemd teken, en read_token mag voor een token nooit een fout geven, wat het ook bevat.

read_token decodeert de payload om exp te lezen en controleert de tag daarna. Er komt een token met een verkeerde tag binnen. Wat is er mis met die volgorde?

Elk verkeerd token is None

Een token kan op verschillende manieren verkeerd zijn: het heeft niet precies twee delen, de tag is verkeerd, de tag is met een andere sleutel gemaakt, of de exp is verstreken. read_token geeft voor al die gevallen één antwoord, None, zoals hoofdstuk 3 één antwoord gaf voor elke verkeerde login. De aanroeper leert dat het token niet goed is, en niets over waarom. Een token dat elke controle doorstaat, geeft zijn payload terug als dict, zodat de aanroeper user en can_write eruit kan lezen.

Een token verloopt als now exp heeft bereikt: op die seconde, en op elke seconde daarna. now is een van read_token, dus niets in signing.py leest een klok. De server geeft state["clock"]() mee, en een test kan elk getal meegeven dat hij wil.

Vijftien minuten

Les 1 noemde de prijs van een token dat de server niet opslaat: niemand kan het terugnemen. Verloop is de enige manier waarop dit token eindigt, dus zijn leven moet kort zijn. In het project van dit hoofdstuk zet de server exp hoogstens 900 seconden vooruit, dat is 15 minuten. Een token dat lekt, is dan 15 minuten een probleem, niet zo lang als de server draait.

Je opdracht

Schrijf in signing.py issue_token(key, claims, *, expires) en read_token(key, token, *, now). De oefening noemt de stappen. Probeer het paar daarna met de hand:

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))"

De eerste regel is het token. De payloadtekst is eyJ1c2VyIjogImFuYSIsICJjYW5fd3JpdGUiOiB0cnVlLCAiZXhwIjogMjAwMH0, zonder = aan het eind, en na de punt komen 64 hextekens die bij elke run verschillen, omdat new_key() elke keer een nieuwe sleutel maakt. De andere drie regels zijn bij elke run gelijk:

{'user': 'ana', 'can_write': True, 'exp': 2000}
None
None

Eén seconde vóór exp geeft het token zijn payload terug. Eén seconde erna is het None. Gelezen met een andere sleutel is het ook None.

Let op waar de sleutel vandaan kwam: signing.new_key(), ter plekke gemaakt en weg als het commando eindigt. Zo kan een server niet werken. Waar zijn sleutel woont, is het onderwerp van de volgende les.

Klik daarna op Check my work.

Opdracht

Schrijf de twee tokenfuncties in signing.py.

  1. Schrijf issue_token(key, claims, *, expires). Bouw de payload {"user": claims["user"], "can_write": claims["can_write"], "exp": expires}. Maak er tekst van met b64u_encode(json.dumps(payload).encode("utf-8")). Geef die tekst terug, een punt, en sign(key, ...) van de tekst gecodeerd als .

  2. Schrijf read_token(key, token, *, now). Splits het token bij punten; alles behalve twee delen is None. Controleer de tag met verify(key, ...) over het eerste deel gecodeerd als UTF-8; een verkeerde tag is None. Decodeer pas daarna het eerste deel met b64u_decode en parse het met json.loads. Als now exp heeft bereikt, geef None terug. Geef anders de payload terug.

Voer de regel python -c uit de les uit in de om een token te maken en het op drie manieren terug te lezen.

Klik daarna op Check my work. Die roept issue_token aan met een sleutel, gebruikers en verlooptijden die je niet hebt gezien, en met claims die een extra veld bevatten, dat de payload niet mag bereiken; hij decodeert de payload zelf en berekent de tag zelf. Hij roept read_token aan met een geldig token één seconde vóór en één seconde na zijn exp, en met tokens die None moeten geven en nooit een fout: een veranderde payload met de oude tag, een token van een andere sleutel, een ingekorte tag, een lege tag, en teksten die helemaal geen token zijn. Hij telt ook aanroepen: een geldig token lezen moet de tag vergelijken via verify of hmac.compare_digest, en een token met een verkeerde tag lezen mag b64u_decode of json.loads niet aanroepen. Niets uit je terminal wordt beoordeeld.