Ondertekende tokens en geheime sleutels · oefening
Een sleutel vervangen zonder iedereen uit te loggen
Je server heeft één sleutel, en die komt uit READING_KEY. Stel je nu voor dat je hem verandert: zet een nieuwe
Een sleutel die niet rustig vervangen kan worden, is een sleutel die te lang bewaard wordt. Deze les maakt het vervangen een gewone stap, sleutelrotatie (key rotation) genoemd: een tijdje kent de server twee sleutels, en elk token zegt welke het ondertekende.
Noem de sleutel
Een token noemt zijn sleutel met key_id(key), de korte id die je in les 4 tegenkwam. De id is openbaar. Het zijn 8 hexcijfers van een README.md hebben de id’s 29c565a5 en a49130cb.
kid.payload.tag
Het token krijgt een derde deel, vooraan. Men noemt het de kid, van key id:
29c565a5 . eyJ1c2VyIjogImFuYSIsIC...MH0 . de374237...2eab4cf5
[ kid ] [---- the payload text ---] [----- the tag -----]
[----- the tag covers all of this ----]
Dat is het begin en het eind van een echt token, ondertekend met de eerste testsleutel, met spaties toegevoegd om het te kunnen lezen.
De id is een eigen deel, zodat de server hem kan lezen zonder iets te decoderen. De tag wordt berekend over de kid, de punt en de payloadtekst samen. Alles wat de server in een token leest, wordt dan door de tag gedekt of is de tag.
Waarom is de sleutel-id een eigen deel, vooraan, en geen veld in de payload?
Opzoeken, dan verifiëren
read_token begint nu met een opzoeking: keys["by_id"].get(kid). Als die id niet bestaat, geeft hij meteen None terug, vóór enige vergelijking. Hij valt niet terug op de huidige sleutel, en hij probeert niet elke sleutel om de beurt.
De redenen zijn eenvoudig. De id noemt precies één sleutel, dus een token dat een id noemt die deze server niet heeft, is niet door hem uitgegeven, en het wordt geweigerd voordat er werk aan besteed wordt. Elke sleutel proberen zou de id zinloos maken, voor elk slecht token één vergelijking per geladen sleutel kosten, en verbergen welke sleutel een token echt gebruikte. Dat moet je weten op de dag dat je een sleutel verwijdert.
Wees duidelijk over wat deze regel niet is. Geen van beide fouten zou hier iemand een token laten vervalsen: de tag moet nog steeds kloppen onder een sleutel die de server heeft. Sommige echte systemen proberen met opzet een korte
Twee variabelen, drie stappen
READING_KEY is de sleutel die ondertekent. READING_KEY_PREVIOUS is, als hij gezet is, een sleutel die nog verifieert. Een sleutel vervangen kost drie stappen:
Zet de oude sleutel in
READING_KEY_PREVIOUSen een nieuwe inREADING_KEY, en herstart. Nieuwe tokens noemen de nieuwe sleutel. Oude tokens noemen de oude sleutel, en beide id’s staan inby_id.Wacht langer dan
TOKEN_LIFETIME. Elk token dat de oude sleutel ondertekende, is nu verlopen.Verwijder
READING_KEY_PREVIOUSen herstart. De oude sleutel is weg, en niemand heeft het gemerkt.
Hier is hetzelfde als tabel, voor een token dat vóór stap 1 gemaakt is:
READING_KEY keys that
signs with verify the old token
before key 001 001 verifies
after step 1 key 002 002 and 001 verifies: it names 001
after step 2 key 002 002 and 001 has expired
after step 3 key 002 002 names a key that is gone
load_keys leest de tweede key_from, dus een vorige sleutel die gezet en kapot is, is een weigering om te starten, net als een kapotte huidige. Een lege READING_KEY_PREVIOUS betekent dat er geen vorige sleutel is.
Stap 1 is gedaan: de nieuwe sleutel ondertekent, en de oude sleutel staat in READING_KEY_PREVIOUS. Welke tokens worden geverifieerd?
Een gelekte sleutel is een ander geval
De overlap is voor een geplande verandering. Een sleutel die gelekt is, krijgt geen overlap: zolang hij geladen is, in een van beide variabelen, kan wie hem heeft tokens ondertekenen die de server accepteert. Verwijder hem meteen, en laat de tokens die hij ondertekende met hem eindigen. Hun houders moeten om nieuwe tokens vragen, en dat is de juiste prijs.
De oude sleutel is vanochtend gelekt. Mag hij de gebruikelijke 15 minuten in READING_KEY_PREVIOUS blijven?
Je opdracht
Breid in signing.py load_keys uit, en verander issue_token en read_token zo dat hun eerste keys is die load_keys teruggeeft. De oefening noemt de stappen. Vervang daarna met de hand een sleutel, met de twee testsleutels uit README.md:
python -c "import signing; old = signing.load_keys({'READING_KEY': 'dGVzdC1vbmx5IGtleSwgbmV2ZXIgdXNlIGl0OiAwMDE='}); t = signing.issue_token(old, {'user': 'ana', 'can_write': True}, expires=2000); both = signing.load_keys({'READING_KEY': 'dGVzdC1vbmx5IGtleSwgbmV2ZXIgdXNlIGl0OiAwMDI=', 'READING_KEY_PREVIOUS': 'dGVzdC1vbmx5IGtleSwgbmV2ZXIgdXNlIGl0OiAwMDE='}); new = signing.load_keys({'READING_KEY': 'dGVzdC1vbmx5IGtleSwgbmV2ZXIgdXNlIGl0OiAwMDI='}); print(t.split('.')[0]); print(signing.read_token(both, t, now=1999)); print(signing.read_token(new, t, now=1999))"
Het maakt een token met de eerste sleutel, en leest het daarna met beide sleutels geladen en met alleen de tweede sleutel:
29c565a5
{'user': 'ana', 'can_write': True, 'exp': 2000}
None
Het token noemt sleutel 29c565a5. Zolang die sleutel nog als de vorige geladen is, wordt het token geverifieerd. Zodra hij verwijderd is, is het token None.
Voer ook python -m pytest -q uit. Het zegt nog steeds 14 passed, omdat geen van je tests issue_token of read_token al aanroept. De controle van les 2 beoordeelde het token met twee delen, dus die slaagt niet meer op dit bestand; elke controle beoordeelt de stap waar haar eigen les om vraagt.
Klik daarna op Check my work. De token- en sleutelfuncties zijn af. Geen enkele route in de server geeft al zo’n token uit, en de schrijfroutes accepteren nog steeds de twee vaste
Opdracht
Geef elk token een sleutel-id, en laat de server een vorige sleutel bewaren, in signing.py.
In
load_keys, na de huidige sleutel: alsenviron.get("READING_KEY_PREVIOUS", "")niet leeg is, haal die sleutel op metkey_fromen voeg hem toe aanby_idonder zijn eigenkey_id.currentnoemt nog steeds de sleutel uitREADING_KEY.Verander
issue_tokenzo dat de eerstekeysis, dedie load_keysteruggeeft. De sleutel-id iskeys["current"]. De ondertekende tekst is de sleutel-id, een punt, en de payloadtekst; het token is dat, een punt, en de tag van die tekst onder de huidige sleutel.Verander
read_tokenop dezelfde manier: drie delen ofNone; zoek het eerste deel op inkeys["by_id"]en geefNoneterug als het er niet in staat; verifieer de tag over de eerste twee delen verbonden door een punt; decodeer daarna het tweede deel en controleerexpzoals eerst.
Voer de regel python -c uit de les uit in de python -m pytest -q om te zien dat je tests nog slagen.
Klik daarna op Check my work. Die roept load_keys aan met eigen dicts: alleen een huidige sleutel, een huidige en een vorige sleutel, een lege vorige issue_token en read_token aan met sleutels die je niet hebt gezien: een nieuw token moet drie delen hebben, de huidige sleutel noemen, en een tag dragen over de eerste twee delen en de punt ertussen; een token van een van beide geladen sleutels moet geaccepteerd worden; en een token van een verwijderde sleutel, een verlopen token, een token dat een onbekende sleutel-id noemt, een token waarvan de sleutel-id verwisseld is, en teksten die geen token zijn, moeten None geven en nooit een fout. Hij telt ook aanroepen: een token met een onbekende sleutel-id moet vóór enige vergelijking geweigerd worden, en een token met een verkeerde tag voordat b64u_decode of json.loads wordt aangeroepen. Niets uit je terminal wordt beoordeeld.