Signierte Tokens und geheime Schlüssel · Übung
Einen Schlüssel ersetzen, ohne alle abzumelden
Dein Server hat einen Schlüssel, und er kommt aus READING_KEY. Stell dir jetzt vor, ihn zu ändern: einen neuen
Ein Schlüssel, der sich nicht in Ruhe ersetzen lässt, ist ein Schlüssel, der zu lange behalten wird. Diese Lektion macht das Ersetzen zu einem gewöhnlichen Schritt, genannt Schlüsselrotation (key rotation): Eine Zeit lang kennt der Server zwei Schlüssel, und jedes Token sagt, welcher es signiert hat.
Den Schlüssel benennen
Ein Token nennt seinen Schlüssel mit key_id(key), der kurzen ID, die du in Lektion 4 kennengelernt hast. Die ID ist öffentlich. Sie besteht aus 8 Hex-Ziffern eines README.md haben die IDs 29c565a5 und a49130cb.
kid.payload.tag
Das Token bekommt einen dritten Teil, vorne. Man nennt ihn kid, für key id:
29c565a5 . eyJ1c2VyIjogImFuYSIsIC...MH0 . de374237...2eab4cf5
[ kid ] [---- the payload text ---] [----- the tag -----]
[----- the tag covers all of this ----]
Das ist der Anfang und das Ende eines echten Tokens, signiert mit dem ersten Testschlüssel, mit Leerzeichen zum besseren Lesen.
Die ID ist ein eigener Teil, damit der Server sie lesen kann, ohne irgendetwas zu dekodieren. Das Tag wird über die kid, den Punkt und den Payload-Text zusammen berechnet. Alles, was der Server in einem Token liest, ist dann entweder vom Tag abgedeckt oder ist das Tag.
Warum ist die Schlüssel-ID ein eigener Teil, vorne, und kein Feld in der Payload?
Nachschlagen, dann verifizieren
read_token beginnt jetzt mit einem Nachschlagen: keys["by_id"].get(kid). Wenn es keine solche ID gibt, gibt es sofort None zurück, vor jedem Vergleich. Es greift nicht auf den aktuellen Schlüssel zurück, und es probiert nicht der Reihe nach jeden Schlüssel aus.
Die Gründe sind schlicht. Die ID nennt genau einen Schlüssel, also wurde ein Token, das eine ID nennt, die dieser Server nicht hat, nicht von ihm ausgegeben, und es wird abgelehnt, bevor Arbeit darauf verwendet wird. Jeden Schlüssel auszuprobieren würde die ID sinnlos machen, für jedes schlechte Token einen Vergleich pro geladenem Schlüssel kosten und verbergen, welchen Schlüssel ein Token wirklich verwendet hat. Das musst du an dem Tag wissen, an dem du einen Schlüssel entfernst.
Sei dir klar darüber, was diese Regel nicht ist. Keiner der beiden Fehler würde hier jemanden ein Token fälschen lassen: Das Tag muss immer noch unter einem Schlüssel stimmen, den der Server hat. Manche echten Systeme probieren absichtlich eine kurze
Zwei Variablen, drei Schritte
READING_KEY ist der Schlüssel, der signiert. READING_KEY_PREVIOUS ist, wenn gesetzt, ein Schlüssel, der noch verifiziert. Einen Schlüssel zu ersetzen braucht drei Schritte:
Leg den alten Schlüssel in
READING_KEY_PREVIOUSund einen neuen inREADING_KEYund starte neu. Neue Tokens nennen den neuen Schlüssel. Alte Tokens nennen den alten Schlüssel, und beide IDs stehen inby_id.Warte länger als
TOKEN_LIFETIME. Jedes Token, das der alte Schlüssel signiert hat, ist jetzt abgelaufen.Entferne
READING_KEY_PREVIOUSund starte neu. Der alte Schlüssel ist weg, und niemand hat es bemerkt.
Hier dasselbe als Tabelle, für ein Token, das vor Schritt 1 erzeugt wurde:
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 liest die zweite key_from, also ist ein vorheriger Schlüssel, der gesetzt und kaputt ist, ein Grund, den Start zu verweigern, wie ein kaputter aktueller. Ein leeres READING_KEY_PREVIOUS bedeutet, dass es keinen vorherigen Schlüssel gibt.
Schritt 1 ist erledigt: Der neue Schlüssel signiert, und der alte steht in READING_KEY_PREVIOUS. Welche Tokens bestehen die Prüfung?
Ein geleakter Schlüssel ist ein anderer Fall
Die Überlappung ist für eine geplante Änderung. Ein Schlüssel, der geleakt ist, bekommt keine Überlappung: Solange er geladen ist, in einer der beiden Variablen, kann jeder, der ihn hat, Tokens signieren, die der Server akzeptiert. Entferne ihn sofort und lass die Tokens, die er signiert hat, mit ihm enden. Ihre Besitzer müssen neue Tokens anfordern, und das ist der richtige Preis.
Der alte Schlüssel ist heute Morgen geleakt. Darf er die üblichen 15 Minuten in READING_KEY_PREVIOUS bleiben?
Deine Aufgabe
Erweitere in signing.py load_keys und ändere issue_token und read_token so, dass ihr erster keys ist, das load_keys zurückgibt. Die Übung führt die Schritte auf. Ersetze dann von Hand einen Schlüssel, mit den zwei Testschlüsseln aus 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))"
Es erzeugt ein Token mit dem ersten Schlüssel und liest es dann mit beiden geladenen Schlüsseln und mit dem zweiten Schlüssel allein:
29c565a5
{'user': 'ana', 'can_write': True, 'exp': 2000}
None
Das Token nennt den Schlüssel 29c565a5. Solange dieser Schlüssel noch als vorheriger geladen ist, besteht das Token die Prüfung. Sobald er entfernt ist, ist das Token None.
Führ auch python -m pytest -q aus. Es sagt immer noch 14 passed, weil noch keiner deiner Tests issue_token oder read_token aufruft. Die Prüfung von Lektion 2 hat das zweiteilige Token bewertet, also besteht sie mit dieser Datei nicht mehr; jede Prüfung bewertet den Schritt, den ihre eigene Lektion verlangt.
Klicke dann auf Check my work. Die Token- und Schlüsselfunktionen sind fertig. Noch keine Route im Server gibt ein solches Token aus, und die Schreibrouten akzeptieren immer noch die zwei festen
Aufgabe
Gib jedem Token eine Schlüssel-ID und lass den Server einen vorherigen Schlüssel halten, in signing.py.
In
load_keys, nach dem aktuellen Schlüssel: Wennenviron.get("READING_KEY_PREVIOUS", "")nicht leer ist, hol diesen Schlüssel mitkey_fromund füge ihn unter seiner eigenenkey_idzuby_idhinzu.currentnennt weiterhin den Schlüssel ausREADING_KEY.Ändere
issue_tokenso, dass sein ersterkeysist, das, das load_keyszurückgibt. Die Schlüssel-ID istkeys["current"]. Der signierte Text ist die Schlüssel-ID, ein Punkt und der Payload-Text; das Token ist das, ein Punkt und das Tag dieses Texts unter dem aktuellen Schlüssel.Ändere
read_tokenauf dieselbe Weise: drei Teile oderNone; schlag den ersten Teil inkeys["by_id"]nach und gibNonezurück, wenn er nicht da ist; verifiziere das Tag über die ersten zwei Teile, mit einem Punkt verbunden; dekodiere dann den zweiten Teil und prüfeexpwie vorher.
Führ im python -c aus der Lektion aus, um von Hand einen Schlüssel zu ersetzen, und python -m pytest -q, um zu sehen, dass deine Tests weiterhin durchlaufen.
Klicke dann auf Check my work. Es ruft load_keys mit eigenen Dictionaries auf: einem aktuellen Schlüssel allein, einem aktuellen und einem vorherigen Schlüssel, einem leeren vorherigen issue_token und read_token mit Schlüsseln auf, die du noch nicht gesehen hast: Ein neues Token muss drei Teile haben, den aktuellen Schlüssel nennen und ein Tag über die ersten zwei Teile und den Punkt dazwischen tragen; ein Token jedes geladenen Schlüssels muss akzeptiert werden; und ein Token eines entfernten Schlüssels, ein abgelaufenes Token, ein Token, das eine unbekannte Schlüssel-ID nennt, ein Token, dessen Schlüssel-ID ausgetauscht wurde, und Texte, die kein Token sind, müssen None ergeben und nie einen Fehler. Außerdem zählt es Aufrufe: Ein Token mit einer unbekannten Schlüssel-ID muss vor jedem Vergleich abgelehnt werden, und ein Token mit einem falschen Tag, bevor b64u_decode oder json.loads aufgerufen wird. Nichts aus deinem Terminal wird bewertet.