0%

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 setzen und neu starten. Jedes Token, das vor dem Neustart ausgegeben wurde, trägt ein Tag vom alten Schlüssel, also wird jedes davon abgelehnt. Bei Tokens, die 15 Minuten leben, ist das ein kleiner Ausfall. Bei jedem signierten Wert, der länger lebt, trifft es alle auf einmal.

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 -Digests und verrät nichts über einen Schlüssel aus 32 zufälligen Bytes. Die zwei Testschlüssel in 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 älterer Schlüssel der Reihe nach aus. Die Gefahr liegt in Systemen, in denen die ID entscheidet, wie ein Schlüssel geholt wird oder welcher Algorithmus das Tag prüft; dort hat das Vertrauen in die ID zu echten Fälschungen geführt. Hier ist es ein Nachschlagen in einem , und die Regel sorgt dafür, dass es so bleibt.

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:

  1. Leg den alten Schlüssel in READING_KEY_PREVIOUS und einen neuen in READING_KEY und starte neu. Neue Tokens nennen den neuen Schlüssel. Alte Tokens nennen den alten Schlüssel, und beide IDs stehen in by_id.

  2. Warte länger als TOKEN_LIFETIME. Jedes Token, das der alte Schlüssel signiert hat, ist jetzt abgelaufen.

  3. Entferne READING_KEY_PREVIOUS und 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 über 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 das Dictionary 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 . Das Projekt bringt das in Ordnung.

Aufgabe

Gib jedem Token eine Schlüssel-ID und lass den Server einen vorherigen Schlüssel halten, in signing.py.

  1. In load_keys, nach dem aktuellen Schlüssel: Wenn environ.get("READING_KEY_PREVIOUS", "") nicht leer ist, hol diesen Schlüssel mit key_from und füge ihn unter seiner eigenen key_id zu by_id hinzu. current nennt weiterhin den Schlüssel aus READING_KEY.

  2. Ändere issue_token so, dass sein erster keys ist, das , das load_keys zurückgibt. Die Schlüssel-ID ist keys["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.

  3. Ändere read_token auf dieselbe Weise: drei Teile oder None; schlag den ersten Teil in keys["by_id"] nach und gib None zurü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üfe exp wie vorher.

Führ im die Zeile mit 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 und einem vorherigen Schlüssel, der zu kurz ist und auslösen muss. Es ruft 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.