Zum Inhalt springen
ecdsa.com

JWS vs. JWE: signierte oder verschlüsselte JOSE-Tokens

Ein JWS beweist, wer eine Payload erstellt hat, aber jeder kann sie lesen. Ein JWE verbirgt die Payload vor allen außer ihrem Empfänger, sagt aber mit Public-Key-Algorithmen nichts darüber, wer sie erstellt hat.

Bei JWS vs. JWE geht es darum, was ein JOSE-Token schützt. Eine JSON Web Signature (RFC 7515) versieht eine Payload, die lesbar bleibt, mit Integrität und Authentizität; eine JSON Web Encryption (RFC 7516) sorgt für Vertraulichkeit. Fast jedes JWT, das dir begegnet, ist ein JWS – das überrascht viele, die erwarten, dass die Claims geheim sind.

Auf einen Blick

JWSJWE
SpezifikationRFC 7515RFC 7516
SchütztIntegrität und HerkunftVertraulichkeit (und Integrität über das AEAD)
Kompakte Formheader.payload.signature (3 Teile)header.encrypted_key.iv.ciphertext.tag (5 Teile)
Algorithmen im Headeralg (zum Beispiel ES256, RS256, HS256, Ed25519)alg für das Schlüsselmanagement (ECDH-ES, ECDH-ES+A256KW, RSA-OAEP-256, A256KW, dir) plus enc für den Inhalt (A256GCM, A128CBC-HS256)
Payload sichtbarJa, nur base64urlNein
Absender authentifiziertJa (Signatur oder MAC)Nur mit symmetrischer oder authentifizierter Schlüsseleinigung; nicht mit ECDH-ES oder RSA-OAEP allein
Größe bei 70 Byte Claims (unten gemessen)186 Zeichen (ES256)405 Zeichen (ECDH-ES+A256KW mit A256GCM)
Typische EinsätzeAccess Tokens und ID-Tokens als JWT, DPoP, signierte Anfragen, SD-JWTVerschlüsselte ID-Tokens, clientseitig gespeicherte Tokens, einige Antworten in FAPI und EUDI

Was jedes Format schützt

Ein JWS signiert base64url(header) + "." + base64url(payload). Wer das Token hat, kann die Claims dekodieren und lesen; der JWT-Debugger macht genau das. Das Signieren beweist, dass das Token vom Inhaber des Schlüssels stammt und nicht verändert wurde – genau das braucht eine API, um Claims zu vertrauen.

Ein JWE erzeugt einen zufälligen Schlüssel für die Inhaltsverschlüsselung (Content Encryption Key, CEK), verschlüsselt die Payload mit dem in enc genannten AEAD und übermittelt den CEK dem Empfänger mit dem in alg genannten Algorithmus für das Schlüsselmanagement: mit RSA-OAEP verpackt, nach einer Schlüsseleinigung per ECDH-ES mit AES Key Wrap verpackt, direkt aus ECDH-ES abgeleitet oder vorab geteilt (dir). Der geschützte Header wird als zugehörige Daten (Associated Data) authentifiziert. Nur der Empfänger kann die Payload lesen.

Die entscheidende Asymmetrie: Beim Schlüsselmanagement mit Public-Key-Verfahren kann jeder, der den öffentlichen Schlüssel des Empfängers hat, ein gültiges JWE erzeugen. Eine erfolgreiche Entschlüsselung beweist nichts über den Absender. Deshalb werden sensible Tokens, denen der Empfänger auch vertrauen muss, verschachtelt: Ein signiertes JWT (JWS) steckt in einem JWE mit cty: JWT und wird nach der Entschlüsselung geprüft.

Größen in der Praxis

Das folgende Skript, ausgeführt mit jose 6.2.13 unter Node.js 26.10, schützt dieselben Claims auf beide Arten.

jose: dieselben Claims als JWS und als JWE
import { CompactEncrypt, CompactSign, compactDecrypt, compactVerify, generateKeyPair } from "jose";

const claims = new TextEncoder().encode(JSON.stringify({ sub: "user-42", email: "alice@example.com", plan: "pro" }));

// JWS: integrity and origin; the payload is only base64url-encoded.
const signer = await generateKeyPair("ES256");
const jws = await new CompactSign(claims).setProtectedHeader({ alg: "ES256" }).sign(signer.privateKey);
await compactVerify(jws, signer.publicKey);

// JWE: confidentiality for the holder of the recipient's private key.
const recipient = await generateKeyPair("ECDH-ES+A256KW");
const jwe = await new CompactEncrypt(claims).setProtectedHeader({ alg: "ECDH-ES+A256KW", enc: "A256GCM" }).encrypt(recipient.publicKey);
const { plaintext } = await compactDecrypt(jwe, recipient.privateKey);

console.log("JWS parts:", jws.split(".").length, "length:", jws.length, "payload readable:", Buffer.from(jws.split(".")[1], "base64url").toString().includes("alice"));
console.log("JWE parts:", jwe.split(".").length, "length:", jwe.length, "decrypts:", new TextDecoder().decode(plaintext).includes("alice"));
Ausgabe
JWS parts: 3 length: 186 payload readable: true
JWE parts: 5 length: 405 decrypts: true

Der Großteil des Overheads beim JWE entfällt auf den ephemeren öffentlichen Schlüssel (epk) im Header, den verpackten Schlüssel, den 12-Byte-IV und das 16-Byte-Tag. Ein verschachteltes, erst signiertes und dann verschlüsseltes Token wird noch größer, weil das ganze JWS zum Klartext des JWE wird.

Fallstricke

  • JWS-Claims für geheim halten. Schreibe keine personenbezogenen Daten, keine internen Kennungen, die du nicht veröffentlichen würdest, und keine Secrets in ein nur signiertes Token; es landet in Logs, im Browser-Speicher und in Support-Tickets.
  • Einem unsignierten JWE vertrauen. Die Entschlüsselung mit ECDH-ES oder RSA-OAEP beweist nur, dass das Token für dich verschlüsselt wurde.
  • Schwache oder veraltete JWE-Algorithmen. RSA1_5 ist anfällig für Padding-Oracle-Angriffe, PBES2 mit einer riesigen Iterationszahl in p2c lässt sich für Denial-of-Service-Angriffe missbrauchen, und Implementierungen von ECDH-ES, die Punkte außerhalb der Kurve akzeptierten, gaben 2017 private Schlüssel preis (RFC 8725 §2.5). Nimm bevorzugt ECDH-ES+A256KW oder RSA-OAEP-256 mit A256GCM, und begrenze die PBES2-Iterationen, wenn du PBES2 unterstützen musst.
  • Algorithmuswahl durch das Token. Wie bei JWS sollte der Empfänger für jeden Schlüssel nur die Werte von alg und enc akzeptieren, die er erwartet.
  • Kompression. zip: DEF in JWE komprimiert vor dem Verschlüsseln, wodurch über die Länge Informationen über den Klartext durchsickern können (RFC 8725 §3.6); verzichte darauf.

Wann welches Format passt

  • Access Tokens, die deine APIs prüfen: JWS; halte die Claims minimal, oder nutze opake Tokens mit Introspection, wenn sie nicht lesbar sein dürfen.
  • ID-Tokens mit personenbezogenen Daten, die ein Browser oder eine mobile App speichert: JWS ist üblich; ein verschachteltes JWE, wenn dein Bedrohungsmodell die Client-Umgebung einschließt.
  • Tokens, die über nicht vertrauenswürdige Zwischenstationen laufen (Weiterleitungen über Dritte, in URLs oder QR-Codes eingebettete Tokens): ein verschachteltes JWS in einem JWE.
  • Machine-to-Machine-Nachrichten über gegenseitig authentifiziertes TLS: oft keines von beiden; TLS sorgt auf dem Transportweg bereits für Vertraulichkeit und Integrität, und ein JWS bringt nur dann etwas, wenn die Nachricht prüfbar bleiben muss, nachdem sie gespeichert oder weitergeleitet wurde.
  • Ende-zu-Ende-verschlüsselte Payloads an einen bekannten Empfänger: JWE mit ECDH-ES+A256KW und A256GCM oder HPKE-basierte Formate, sobald sie für JOSE standardisiert sind.

Hinweise zur Migration

  1. Ordne die Claims in bestehenden JWS-Tokens ein: Alles, was für den Inhaber nicht sichtbar sein darf, verlässt entweder das Token oder wandert in ein JWE.
  2. Führe verschachtelte Tokens schrittweise ein: Stelle JWEs mit cty: JWT für Clients aus, die sie entschlüsseln können, und lass das innere JWS unverändert, damit die bestehende Logik zur Prüfung weiter genutzt wird.
  3. Verteile Schlüssel für die Verschlüsselung getrennt von Signaturschlüsseln: JWKS-Einträge sollten use: enc oder use: sig und unterschiedliche Werte für kid haben.
  4. Lege die Algorithmen auf beiden Seiten fest und lehne RSA1_5, PBES2 ohne Obergrenze und Kompression ab.
  5. Prüfe die Protokollierung erneut: Sobald Tokens verschlüsselt sind, stelle sicher, dass entschlüsselte Claims nicht dort protokolliert werden, wo das Token selbst nie auftauchte. Der JWE-Inspektor zeigt Header und Struktur eines JWE, ohne es zu entschlüsseln.

Quellen

Alle Vergleiche