Zum Inhalt springen
ecdsa.com
Node.js · WebCrypto

jose (panva)

jose von Filip Skokan: eine JOSE-Implementierung ohne Abhängigkeiten (JWS, JWE, JWK, JWKS, JWT) auf Basis von WebCrypto, die unverändert in Node.js, Browsern, Deno, Bun und Edge-Laufzeitumgebungen läuft – mit EdDSA und ML-DSA, aber ohne secp256k1.
Lizenz
MIT
Geprüfte Version
6.2.13 ·
Quellcode
github.com/panva/jose
Dokumentation
github.com/panva/jose/tree/main/docs

Kompatibilität auf einen Blick

JOSE und JWT

JWS-Algorithmen
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSAEd25519ML-DSA-44ML-DSA-65ML-DSA-87

jose deckt die gesamte JOSE-Familie ab – signierte und verschlüsselte Tokens, Import und Export von JWKs, lokale und entfernte JWK-Sets – und bietet dafür eine einzige API in jeder JavaScript-Laufzeitumgebung. Seit Version 6 läuft die gesamte Kryptografie über WebCrypto. Schlüssel sind deshalb Objekte vom Typ CryptoKey (Instanzen von KeyObject aus Node.js werden weiterhin akzeptiert), und die Bibliothek erbt die Kurvenliste von WebCrypto: ES256, ES384 und ES512, EdDSA und Ed25519 sowie in 6.2.13 die ML-DSA-Algorithmen, sofern die Laufzeitumgebung sie bereitstellt. ES256K und Ed448 werden mit ERR_JOSE_NOT_SUPPORTED abgelehnt.

Wo es darauf ankommt, sind die Standardeinstellungen streng. jwtVerify() akzeptiert alg: none nie – unsignierte Tokens lassen sich nur über die separate Klasse UnsecuredJWT lesen –, und ein Schlüssel ist an eine Algorithmusfamilie gebunden: Ein HS256-Token, geprüft mit einem öffentlichen Schlüssel vom Typ EC, oder ein RS256-Token mit einem EC-Schlüssel scheitert mit einem TypeError, statt die Prüfung zu bestehen. ES256 lehnt außerdem einen P-384-Schlüssel ab. Übergib algorithms trotzdem: Bei einem Schlüsselsatz mit mehreren Schlüsseltypen verhindert erst diese Option, dass ein Aussteller stillschweigend den Algorithmus wechselt.

Zwei Standardeinstellungen fallen gegensätzlich aus. clockTolerance ist null und damit streng, exp dagegen ist optional und damit großzügig: Ein Token ohne Ablaufzeit besteht die Prüfung, sofern du den Claim nicht in requiredClaims aufführst, wie es das Beispiel tut. Ein JWS, dessen ECDSA-Signatur als DER ankommt – typisch, wenn ein Token von Hand mit einem Cloud-KMS oder openssl dgst zusammengesetzt wird –, scheitert mit der allgemeinen Meldung signature verification failed, und nichts deutet auf die Kodierung hin; der JWT-Debugger zeigt die Länge der Signatur auf einen Blick, und der Konverter DER ⇄ Raw behebt das Problem.

Der Schlüsselimport ist explizit: importSPKI und importPKCS8 nehmen PEM-Strings entgegen – DER-Bytes werden mit "spki" must be SPKI formatted string abgelehnt –, importX509 nimmt ein PEM-Zertifikat und importJWK einen JWK. Die SEC1-Dateien EC PRIVATE KEY, die OpenSSL standardmäßig schreibt, lehnt importPKCS8 ab; konvertiere sie zuerst in PKCS#8, zum Beispiel mit dem JWK-Konverter.

Um Tokens eines Identity Providers zu prüfen, nutze createRemoteJWKSet mit dem jwks_uri des Anbieters: Die Funktion speichert Schlüssel zwischen, lädt sie bei einem unbekannten kid neu und drosselt diese erneuten Abrufe per Rate Limit. Prüfe mit dem JWKS-Checker, was der Anbieter tatsächlich veröffentlicht.

Fakten und die Prüfungen dahinter

Nichts im Folgenden ist aus einer Dokumentation abgeschrieben. Das Prüfskript content/libraries/verify/jose/run.sh hat jose (panva) 6.2.13 unter Node.js 24.22.0 am 20. Oktober 2026 im Container-Image node:24.22.0-bookworm-slim ausgeführt; die Seite muss mit seiner Ausgabe übereinstimmen, und jeder Fakt führt die zugehörigen Zeilen auf.

  • JWS-Algorithmen

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSAEd25519ML-DSA-44ML-DSA-65ML-DSA-87

    In der Prüfung funktionierten Signieren und Prüfen mit HS, RS, PS und ES bei 256/384/512 Bit, mit EdDSA und Ed25519 sowie mit ML-DSA-44/65/87 (unter Node.js 24.22); ES256K und Ed448 lösten ERR_JOSE_NOT_SUPPORTED aus.

    Von der Prüfung aufgezeichnet
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA, Ed25519, ML-DSA-44, ML-DSA-65, ML-DSA-87
    evidence jws-algorithms: ES256K: ERR_JOSE_NOT_SUPPORTED: Invalid or unsupported "alg" (Algorithm) value
    evidence jws-algorithms: Ed448: ERR_JOSE_NOT_SUPPORTED: Invalid or unsupported "alg" (Algorithm) value
  • Kodierung von ES256-Signaturen

    raw

    ES256-Signaturen in kompakten Tokens bestehen aus 64 Byte rohem r‖s – der Form, die RFC 7518 verlangt.

    Von der Prüfung aufgezeichnet
    fact jws-ecdsa-encoding = raw
  • DER-Signatur in einer JWS

    rejected

    Ein Token, dessen ES256-Signatur in DER umkodiert wurde, scheitert mit ERR_JWS_SIGNATURE_VERIFICATION_FAILED – demselben Fehler wie bei einer gefälschten Signatur.

    Von der Prüfung aufgezeichnet
    fact jws-der-signature = rejected
    evidence jws-der-signature: ERR_JWS_SIGNATURE_VERIFICATION_FAILED: signature verification failed
  • Allowlist der Algorithmen

    from-key

    Ohne algorithms ist jeder Algorithmus zulässig, den der Schlüssel ausführen kann; ein RS256-Token scheiterte an einem EC-Schlüssel mit CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5.

    Von der Prüfung aufgezeichnet
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: RS256 token against an EC key: TypeError: CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5
  • Unsignierte Tokens (alg none)

    rejected

    jwtVerify() lehnt alg: none selbst dann ab, wenn none in algorithms steht; ungesicherte Tokens lassen sich nur mit UnsecuredJWT.decode() dekodieren.

    Von der Prüfung aufgezeichnet
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: jwtVerify: ERR_JOSE_NOT_SUPPORTED: alg none is not supported either by JOSE or your javascript runtime
    evidence jwt-alg-none: jwtVerify with algorithms ["none"]: ERR_JOSE_NOT_SUPPORTED: alg none is not supported either by JOSE or your javascript runtime
    evidence jwt-alg-none: UnsecuredJWT.decode: returned sub fact-check
  • Öffentlicher Schlüssel als HMAC-Secret

    rejected

    Ein HS256-Token, dessen HMAC-Schlüssel der PEM-Text des öffentlichen Schlüssels ist, scheitert an diesem öffentlichen Schlüssel mit CryptoKey instances must be of type "secret" for the HS256 algorithm – mit und ohne Algorithmusliste.

    Von der Prüfung aufgezeichnet
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: no list: TypeError: CryptoKey instances must be of type "secret" for the HS256 algorithm
    evidence jwt-hmac-public-key: algorithms ["HS256"]: TypeError: CryptoKey instances must be of type "secret" for the HS256 algorithm
  • Abgleich der Kurve mit alg

    enforced

    Das Signieren mit ES256 und einem P-384-Schlüssel scheitert mit its algorithm.namedCurve must be P-256.

    Von der Prüfung aufgezeichnet
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: TypeError: CryptoKey does not support this operation, its algorithm.namedCurve must be P-256
  • Standardtoleranz für Uhrabweichungen

    0s

    Standardmäßig keine Toleranz (Leeway): Ein Token, das drei Sekunden zuvor abgelaufen war, wurde abgelehnt; setze clockTolerance, um Uhrenabweichungen zuzulassen.

    Von der Prüfung aufgezeichnet
    fact jwt-clock-skew = 0s
  • Ablauf-Claim erforderlich

    no

    Ein Token ohne exp bestand die Prüfung; ergänze requiredClaims: ["exp"], um den Claim zur Pflicht zu machen.

    Von der Prüfung aufgezeichnet
    fact jwt-exp-required = no
  • Formate öffentlicher Schlüssel

    spki-pemx509jwk

    importSPKI (PEM-String), importX509 (PEM-Zertifikat) und importJWK funktionierten; an importSPKI übergebene DER-Bytes wurden abgelehnt. Instanzen von KeyObject aus Node.js wurden ebenfalls direkt akzeptiert.

    Von der Prüfung aufgezeichnet
    fact public-key-formats = spki-pem, x509, jwk
    evidence public-key-formats: spki-der: TypeError: "spki" must be SPKI formatted string
    evidence public-key-formats: KeyObject from node:crypto accepted as a key: true
  • Formate privater Schlüssel

    pkcs8-pemjwk

    importPKCS8 nimmt einen PKCS#8-PEM-String entgegen und importJWK einen JWK mit d; ein SEC1-PEM EC PRIVATE KEY wurde mit "pkcs8" must be PKCS#8 formatted string abgelehnt.

    Von der Prüfung aufgezeichnet
    fact private-key-formats = pkcs8-pem, jwk
    evidence private-key-formats: sec1-pem: TypeError: "pkcs8" must be PKCS#8 formatted string

Verwendung

Stelle ein ES256-Token mit Schlüssel-ID aus und prüfe es dann über ein JWK-Set, wobei Algorithmus, Aussteller, Audience und Ablauf allesamt verlangt werden.
import * as jose from "jose";

const { privateKey, publicKey } = await jose.generateKeyPair("ES256");
const jwk = { ...(await jose.exportJWK(publicKey)), kid: "2026-10", alg: "ES256" };

const token = await new jose.SignJWT({ sub: "user-42" })
  .setProtectedHeader({ alg: "ES256", kid: jwk.kid })
  .setIssuer("https://auth.example.com")
  .setAudience("api")
  .setExpirationTime("10m")
  .sign(privateKey);

// The key set picks the key by kid; createRemoteJWKSet(new URL(...)) does the same over HTTPS.
const keys = jose.createLocalJWKSet({ keys: [jwk] });
const { payload } = await jose.jwtVerify(token, keys, {
  algorithms: ["ES256"],
  issuer: "https://auth.example.com",
  audience: "api",
  requiredClaims: ["exp"],
});

console.log("sub:", payload.sub);

Die Snippet-Testumgebung führt jedes Beispiel unverändert im Container-Image node:24.22.0-bookworm-slim aus, und der Lauf schlägt fehl, wenn die Ausgabe nicht übereinstimmt.

Einrichtung
npm init -y >/dev/null && npm install --no-audit --no-fund jose@6.2.13
Befehl
node usage.mjs
Ausgabe muss passen (regulärer Ausdruck)
sub: user-42

Bibliotheken