Zum Inhalt springen
ecdsa.com
← Alle Artikel

ES256 vs. RS256 für JWTs: richtig wählen, ohne dir die Finger zu verbrennen

Zwei Buchstaben im Header eines JWT entscheiden über die Größe deiner Tokens, über dein Schlüsselmanagement und – wenn du die Prüfung nachlässig konfigurierst – darüber, ob sich deine Tokens überhaupt fälschen lassen. Hier erfährst du, was ES256 und RS256 tatsächlich bedeuten und wie du sie sicher einsetzt.

Was die alg-Namen tatsächlich bedeuten

Ein JWT ist ein JWS (JSON Web Signature) mit einer JSON-Payload: header.payload.signature, jeder Teil base64url-kodiert. Der Header alg benennt das Signaturverfahren, und die zugehörige Registry ist in RFC 7518 (JSON Web Algorithms) definiert. Die vier Werte, denen du in der Praxis begegnest:

algVerfahrenSchlüsseltypSignaturgröße
HS256HMAC mit SHA-256Ein gemeinsames Geheimnis (symmetrisch)32 Byte
RS256RSASSA-PKCS1-v1_5 mit SHA-256RSA-Schlüsselpaar256 Byte (RSA-2048)
PS256RSASSA-PSS mit SHA-256RSA-Schlüsselpaar256 Byte (RSA-2048)
ES256ECDSA auf P-256 mit SHA-256EC-Schlüsselpaar64 Byte (roh, r‖s)

In dieser Tabelle stecken zwei strukturelle Punkte. Erstens ist HS256 grundlegend anders: Es ist symmetrisch, also kann jede Partei, die ein Token prüfen kann, auch selbst eines ausstellen. Es passt nur, wenn Aussteller und Verifizierer derselbe Dienst sind. RS256, PS256 und ES256 sind asymmetrisch: Verifizierer besitzen nur den öffentlichen Schlüssel, und den können sie bedenkenlos erhalten, etwa über einen JWKS-Endpunkt.

Zweitens legen die ES-Namen die Kurve fest: ES256 bedeutet P-256 mit SHA-256, ES384 bedeutet P-384 mit SHA-384, und ES512 bedeutet – Achtung bei den Zahlen – die 521-Bit-Kurve P-521 mit SHA-512. PS256 nutzt dasselbe RSA-Schlüsselmaterial wie RS256, aber mit dem neueren PSS-Padding; wenn dein Ökosystem es unterstützt, ist PSS der bessere RSA-Modus. (Den tiefergehenden Vergleich der Algorithmen über JWTs hinaus findest du unter ECDSA vs. RSA.)

Tokengröße: 64 Byte vs. 256 Byte Signatur

Eine ES256-Signatur besteht aus 64 Byte im Rohformat – etwa 86 Zeichen in base64url. Eine RS256-Signatur mit dem üblichen 2048-Bit-Schlüssel ist 256 Byte groß – etwa 342 Zeichen –, mit RSA-3072 sind es 384 Byte. Da Header und Payload in beiden Fällen gleich sind, spart der Wechsel RS256 → ES256 typischerweise ~250 Byte pro Token. Das zählt, wenn Tokens in Cookies (Budget: 4 KB), im Header Authorization jeder Anfrage, in QR-Codes oder im Datenverkehr von Mobil- und IoT-Geräten unterwegs sind. Auch der öffentliche ES256-Schlüssel ist deutlich kleiner, was JWKS-Dokumente kompakt hält.

Bei der Geschwindigkeit gilt das Muster aus dem Leitfaden ECDSA vs. RSA: ES256 signiert schneller, RS256 prüft schneller. Für einen Token-Aussteller, der in großer Menge signiert, ist ES256 die günstigere Seite dieser Abwägung.

Die Regel, die die meisten JWT-Desaster verhindert: den Algorithmus fest vorgeben

Der klassische Konfigurationsfehler bei JWTs besteht darin, das Token entscheiden zu lassen, wie es geprüft wird. Der Header alg ist eine Eingabe, die der Angreifer in der Hand hat – sie kommt im selben Token an, das du noch gar nicht geprüft hast. Ein Verifizierer, der alg aus dem Header liest und danach verzweigt, während ein einziges Schlüsselobjekt mehrere Algorithmusfamilien bedient, hebelt seine eigenen Garantien aus. Der bekannte Fall der Verwechslung: Ein Dienst prüft RS256-Tokens mit einem öffentlichen Schlüssel, aber seine Bibliothek akzeptiert mit demselben Schlüsselobjekt auch HS256 – und behandelt die Bytes des öffentlichen RSA-Schlüssels als HMAC-Secret. Da der öffentliche Schlüssel konstruktionsbedingt öffentlich ist, kann jeder ein passendes HS256-Tag berechnen und Tokens ausstellen, die der Dienst akzeptiert.

Die Lösung ist mechanisch und in RFC 8725 (JWT Best Current Practices) festgeschrieben:

  • Prüfe mit einer expliziten Allowlist für Algorithmen – idealerweise mit einem einzigen Wert –, die serverseitig konfiguriert und nie aus dem Token abgeleitet wird.
  • Binde Schlüssel an Algorithmen. Gib in einem JWKS jedem Schlüssel seine Werte für alg und kid; ein für ES256 veröffentlichter Schlüssel darf nie für etwas anderes verwendbar sein. Auch Prüfungen des Schlüsseltyps helfen: Ein EC-Schlüssel für P-256 kann RS256 prinzipbedingt nicht prüfen – ein Argument mehr für ES256 in gemischten Umgebungen.
  • Prüfe die Claims explizit – Aussteller, Audience, Ablauf – im selben Aufruf.
node – jose: Allowlist für Algorithmen, auf der prüfenden Seite fest vorgegeben
import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json"));

const { payload } = await jwtVerify(token, jwks, {
  algorithms: ["ES256"],          // fixed by the verifier, not by the token
  issuer: "https://issuer.example",
  audience: "api://orders",
});
python – PyJWT: dieselbe Regel
import jwt

payload = jwt.decode(
    token,
    public_key_pem,
    algorithms=["ES256"],   # required parameter in modern PyJWT — for good reason
    audience="api://orders",
    issuer="https://issuer.example",
)

Warum „none“ nie akzeptabel ist

RFC 7518 registriert auch "alg": "none" – das „ungesicherte JWS“ (Unsecured JWS), ein Token mit leerem Signaturteil. Es existiert für den seltenen Fall, dass eine äußere Schicht die Integrität garantiert. Ein Verifizierer, der none akzeptiert, akzeptiert unsignierte Tokens: Jeder kann eine beliebige Payload zusammenstellen und vorlegen. Frühe JWT-Bibliotheken (aus der Zeit um 2015) behandelten none bekanntlich als ganz normalen Algorithmus, wodurch unsignierte Tokens die Prüfung standardmäßig bestanden. Moderne Bibliotheken lehnen diesen Wert ab, sofern er nicht ausdrücklich aktiviert ist – und mit einer Allowlist von ["ES256"] kann er nie ausgewählt werden. Wenn du eine Codebasis auditierst, ist jeder Codepfad, der none oder eine leere Allowlist erreichen kann, ein Befund.

Die ES256-Falle: JWS-Signaturen sind Raw r‖s, nicht DER

Mathematisch ist eine ECDSA-Signatur ein Paar ganzer Zahlen (r, s), und für dieses Paar gibt es zwei verbreitete Byte-Kodierungen. RFC 7518 §3.4 ist eindeutig: In einem JWS besteht die ES256-Signatur aus r und s als zwei 32-Byte-Ganzzahlen im Big-Endian-Format, aneinandergehängt – genau 64 Byte, danach base64url-kodiert. Es handelt sich nicht um die SEQUENCE in ASN.1 DER, die OpenSSL, Signature in Java und crypto.sign() in Node standardmäßig erzeugen.

Das ist eine der häufigsten Ursachen für „invalid signature“ in selbst geschriebenem JWT-Code: Das Token trägt einen DER-Blob von ~70 Byte (oder der Verifizierer übergibt eine 64-Byte-Raw-Signatur an eine API, die DER erwartet), und die Prüfung schlägt fehl, obwohl Schlüssel, Nachricht und Algorithmus allesamt stimmen. Wenn du mit dem eingebauten Krypto-Modul von Node signierst, fordere direkt das JWS-Format an:

node – JWS-taugliche 64-Byte-ES256-Signatur erzeugen
import { createPrivateKey, sign } from "node:crypto";

const key = createPrivateKey(pem); // P-256 private key
const signingInput = Buffer.from(`${headerB64}.${payloadB64}`);

const signature = sign("sha256", signingInput, {
  key,
  dsaEncoding: "ieee-p1363",   // raw r||s — what JWS requires (default is DER)
});
console.log(signature.length); // 64

Begegnet dir in freier Wildbahn eine Signatur in der falschen Form? Der Konverter DER ⇄ Raw wandelt in beide Richtungen um und zeigt die Werte r und s; die Details auf Byte-Ebene samt der High-S-Feinheit behandelt ECDSA-Signaturformate: DER vs. Raw. Und um ein echtes Token zu prüfen – Header, Claims und eine ES256-Prüfung mit deinem öffentlichen Schlüssel –, füge es in den JWT-Debugger ein.

Wählen – und ohne Ausfälle migrieren

Die Migration eines bestehenden RS256-Deployments auf ES256 ist eine Schlüsselrotation, keine Umstellung über Nacht:

  • Veröffentliche den neuen EC-Schlüssel in deinem JWKS neben dem RSA-Schlüssel, jeweils mit korrekten Werten für kid und alg, und gib den Gegenstellen Zeit, ihre JWKS-Caches zu aktualisieren.
  • Stelle ab jetzt ES256-Tokens mit dem neuen kid aus. Verifizierer, die Schlüssel anhand von kid auswählen und pro Schlüssel einen Algorithmus erzwingen, akzeptieren weiterhin beide Token-Generationen, jede gegen genau einen Schlüssel.
  • Sobald jedes RS256-Token abgelaufen ist, entferne den RSA-Schlüssel aus dem JWKS und verkleinere die Allowlist des Verifizierers wieder auf ["ES256"].

Der eine Satz, den du dir merken solltest: Der Verifizierer wählt den Algorithmus, nicht das Token. Hast du das richtig umgesetzt, wird die Entscheidung zwischen ES256 und RS256 zu dem, was sie sein sollte – einer unspektakulären technischen Abwägung zwischen Bytes und Millisekunden.