advanced
JWKS
JWT
RS256

JWKS-Schluesselverwaltung

Pro-Subaccount RS256-Schluesselpaare, kid-Rotation, Karenzfenster und wo private Schluessel gespeichert werden.

Levy Fleets TeamMay 18, 20269 min read

JWKS-Schluesselverwaltung

Jede MDS 2.0-Antwort traegt einen MDS-JWT-Header -- einen RS256-signierten JWT, der nachweist, dass der Response-Body-sha256 von Ihrem Subaccount stammt. Die oeffentliche Seite dieses Schluesselpaars wird unter .well-known/jwks.json veroeffentlicht. Dieser Artikel behandelt Speicherung, Rotation und Wiederherstellung dieser Schluessel.

Speicherung

Schluessel liegen in der mds_jwks_keys-Tabelle, eine oder mehrere Zeilen pro Subaccount:

SpalteInhalt
idUUID
subaccount_idFK zum Betreiber-Subaccount
kidIm JWT-Header und in der JWKS-Antwort sichtbare Key-ID
algorithmHeute immer RS256
public_key_pemPEM-kodierter SPKI-Public-Key
private_key_pemPEM-kodierter PKCS#8-Private-Key
is_activeGenau eine Zeile pro Subaccount hat is_active = true
created_atErstellungszeit
rotated_atWann der Schluessel aufgehoert hat aktiv zu sein (null, falls noch aktiv)
expires_atWann der Schluessel das JWKS-Karenzfenster verlaesst

Speicherung des privaten Schluessels

private_key_pem wird derzeit unverschluesselt in Supabase gespeichert. Eine Migration zu pgsodium / Supabase Vault steht vor dem Rollout an einen zahlenden Enterprise-Kunden auf der Roadmap. Behandeln Sie die Zeile als sensibel -- nicht in Exporte, nicht verschluesselte Backups oder geteilte Kopien an Staedte aufnehmen.

Lazy-Minting

Beim ersten Aufruf von /api/mds/{subaccountId}/.well-known/jwks.json oder eines /provider/v2/*-Endpunkts macht getOrCreateActiveJwksKey() Folgendes:

  1. Pruefen, ob in mds_jwks_keys eine Zeile mit is_active = true existiert.
  2. Falls nicht, frisches RS256-Schluesselpaar via Nodes crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }) generieren.
  3. PEM in JWK-Format konvertieren (n, e, kty, alg, kid, use).
  4. Zeile mit frisch generierter kid einfuegen (Format: mds-<YYYY-MM>-<short_hash>).
  5. Die aktive Zeile an den Aufrufer zurueckgeben.

Damit kann ein Betreiber komplett ohne manuelle Schluesseleinrichtung onboarden -- das erste Ping des Stadt-Validators loest die Erstellung aus.

Rotation

Jaehrliche Rotation ist die empfohlene Kadenz und entspricht der OMF-Vorgabe.

Zur Rotation:

1

Neuen Schluessel erzeugen

Neue Zeile mit is_active = true und frischer kid einfuegen. Das Schluesselpaar wird serverseitig generiert; Sie beruehren nie den privaten Schluessel.

2

Vorherigen Schluessel zurueckstufen

is_active = false und rotated_at = now() auf der vorherigen aktiven Zeile setzen. expires_at = now() + 7 Tage, damit Staedte ihren JWKS-Cache aktualisieren koennen.

3

Bestaetigen, dass JWKS beide veroeffentlicht

/.well-known/jwks.json aufrufen und bestaetigen, dass beide kids im keys-Array erscheinen. Vor der Rotation signierte JWTs verifizieren weiterhin; neue Antworten nutzen den neuen Schluessel.

4

Nach dem Karenzfenster

Die zurueckgestufte Zeile loeschen (oder expires_at = now() markieren). Sie faellt automatisch aus der JWKS.

JWKS-Antwortform

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "mds-2026-05-a7c2",
      "use": "sig",
      "alg": "RS256",
      "n": "0vx7agoebGcQ...",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "kid": "mds-2025-05-9f1d",
      "use": "sig",
      "alg": "RS256",
      "n": "1vyx8wagoeb2X...",
      "e": "AQAB"
    }
  ]
}

Staedte waehlen den richtigen Schluessel, indem sie die kid des JWT-Headers gegen die kids in der JWKS abgleichen. Wir veroeffentlichen aktive + Karenzfenster-Schluessel; rotierte und abgelaufene Schluessel werden nie veroeffentlicht.

Einen JWT lokal verifizieren

Der OMF-mds-provider-validator macht das automatisch, aber Sie koennen manuell verifizieren:

# Eine Antwort abrufen
curl -i -H "Authorization: Bearer <token>" \
  https://fleets.levyelectric.com/api/mds/<subaccountId>/provider/v2/vehicles/status > resp.txt

# Den JWT extrahieren
JWT=$(grep -i '^MDS-JWT:' resp.txt | awk '{print $2}' | tr -d '\r')

# JWKS holen
curl https://fleets.levyelectric.com/api/mds/<subaccountId>/.well-known/jwks.json > jwks.json

# Mit mkjwk oder jose CLI verifizieren
jose jws ver -i <(echo "$JWT") -k <(jq '.keys[0]' jwks.json)

Eine saubere Verifikation gibt payload verified plus die JWT-Payload (Issuer, Audience, Body-sha256, iat, exp) zurueck.

Wiederherstellung bei privatem Schluessel-Leak

Falls der private Schluessel exponiert wird (versehentlich geloggt, an eine Stadt geteilt usw.):

  1. Sofort ein neues Schluesselpaar erzeugen (Schritte oben).
  2. Die geleakte Zeile auf expires_at = now() setzen -- sie faellt sofort aus der JWKS, sodass keine laufenden Anfragen mehr damit verifizieren.
  3. Staedte benachrichtigen, die JWT-Verifikation nutzen (sie holen die JWKS still neu).
  4. mds_policy_audit und Sentry auf anomale Anfragen im Leak-Fenster pruefen.

Bearer-Token-Auth ist von JWKS-Leaks unberuehrt -- der JWT ist eine zusaetzliche Integritaetsschicht, nicht das Primaer-Credential. Staedte, die nur Bearer validieren, sehen keine Auswirkung.

Fehlerbehebung

  • MDS-JWT-Header fehlt in einer Antwort -- mds_jwks_keys hat keine Zeile mit is_active = true. /.well-known/jwks.json direkt aufrufen, um eine Lazy-Mint zu erzwingen.
  • JWKS gibt {"keys": []} zurueck -- wahrscheinlich dieselbe Ursache. Die Lazy-Mint sollte sie beim naechsten Lesevorgang fuellen.
  • JWT-Verifikation scheitert mit "kid mismatch" -- der JWKS-Cache der Stadt ist veraltet. Sie sollte neu holen und es erneut versuchen. Falls weiterhin fehlschlagend, bestaetigen, dass die kid der aktiven Zeile dem im Response-Header gefuehrten Wert entspricht.
  • JWT-Verifikation scheitert mit "signature invalid" -- der oeffentliche Schluessel in JWKS passt nicht zum privaten, der den JWT signiert hat. Wahrscheinlich zwei konkurrierende Schreibvorgaenge bei der Lazy-Mint; aeltere Zeile deaktivieren, um es zu klaeren.

Was kommt als naechstes?