0%

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 en herstart. Elk token dat vóór de herstart is uitgegeven, draagt een tag van de oude sleutel, dus elk ervan wordt geweigerd. Met tokens die 15 minuten leven, is dat een kleine storing. Met elke ondertekende waarde die langer leeft, is het iedereen tegelijk.

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 -digest, en hij verraadt niets over een sleutel die 32 willekeurige bytes is. De twee testsleutels in 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 oudere sleutels om de beurt. Het gevaar zit in systemen waar de id bepaalt hoe een sleutel wordt opgehaald of welk algoritme de tag controleert; daar heeft het vertrouwen van de id tot echte vervalsingen geleid. Hier is het een opzoeking in een , en de regel houdt dat zo.

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:

  1. Zet de oude sleutel in READING_KEY_PREVIOUS en een nieuwe in READING_KEY, en herstart. Nieuwe tokens noemen de nieuwe sleutel. Oude tokens noemen de oude sleutel, en beide id’s staan in by_id.

  2. Wacht langer dan TOKEN_LIFETIME. Elk token dat de oude sleutel ondertekende, is nu verlopen.

  3. Verwijder READING_KEY_PREVIOUS en 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 via 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 de dict 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 . Het project zet dat recht.

Opdracht

Geef elk token een sleutel-id, en laat de server een vorige sleutel bewaren, in signing.py.

  1. In load_keys, na de huidige sleutel: als environ.get("READING_KEY_PREVIOUS", "") niet leeg is, haal die sleutel op met key_from en voeg hem toe aan by_id onder zijn eigen key_id. current noemt nog steeds de sleutel uit READING_KEY.

  2. Verander issue_token zo dat de eerste keys is, de die load_keys teruggeeft. De sleutel-id is keys["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.

  3. Verander read_token op dezelfde manier: drie delen of None; zoek het eerste deel op in keys["by_id"] en geef None terug als het er niet in staat; verifieer de tag over de eerste twee delen verbonden door een punt; decodeer daarna het tweede deel en controleer exp zoals eerst.

Voer de regel python -c uit de les uit in de om met de hand een sleutel te vervangen, en 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 , en een vorige sleutel die te kort is, die een moet geven. Hij roept 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.