Zum Inhalt springen
ecdsa.com
WebCrypto · Node.js

WebCrypto (crypto.subtle)

Die Web Cryptography API, eingebaut in jeden Browser sowie in Node.js, Deno und Bun: ECDSA auf den drei NIST-Kurven mit rohen r‖s-Signaturen, Ed25519 überall und Post-Quanten-Algorithmen bisher nur in einigen Engines.
Lizenz
W3C Software and Document License (specification); each engine ships under its own license
Geprüfte Version
Node.js 24.22.0, Chromium 156.0.8078.4, Firefox 157.0, WebKit 27.2 ·
Quellcode
github.com/w3c/webcrypto
Dokumentation
w3c.github.io/webcrypto/

Kompatibilität auf einen Blick

ECDSAEdDSA

ECDSA-Kurven
P-256P-384P-521
EdDSA
Ed25519
Formate öffentlicher Schlüssel
spki-derjwksec1-uncompressed

crypto.subtle ist die Standardschnittstelle der Webplattform für Kryptografie. Dieselben Aufrufe – generateKey, importKey, sign, verify, exportKey – funktionieren in Chromium, Firefox und WebKit und in Server-Laufzeitumgebungen wie Node.js; damit ist die API die naheliegende Wahl für Code, der auf beiden Seiten laufen muss. Die API gibt es nur in sicheren Kontexten: Auf einer Seite über einfaches http ist crypto.subtle undefined.

Die Spezifikation legt das Format der ECDSA-Signatur auf rohes r‖s fest, auf P-256 also 64 Byte, und der Test fand genau das in allen vier Engines. Das passt zu JWS und JWT, ES256-Tokens brauchen also keine Konvertierung, aber nicht zu OpenSSL, SHA256withECDSA von Java, SignASN1 von Go oder node:crypto, die alle ASN.1-DER verwenden. Weil eine an verify() übergebene DER-Signatur schlicht false liefert statt eines Fehlers, sieht der Formatunterschied wie eine Fälschung aus; konvertiere mit dem Konverter DER ⇄ Raw oder prüfe beide Formen im Signatur-Verifizierer.

Die zweite häufige Hürde sind Schlüssel. importKey("spki") nimmt DER-Bytes entgegen, nie PEM-Text; "raw" nimmt den SEC1-Punkt, und die komprimierte Form akzeptieren Node.js, Chromium und Firefox, WebKit lehnt sie dagegen ab – sende für portablen Code also den unkomprimierten Punkt. Private Schlüssel lassen sich nur als PKCS#8-DER oder JWK importieren – die SEC1-Dateien EC PRIVATE KEY, die OpenSSL standardmäßig schreibt, musst du zuerst nach PKCS#8 konvertieren, etwa mit dem JWK-Konverter.

Die Liste der Kurven endet bei P-256, P-384 und P-521: secp256k1 und Brainpool lösen überall NotSupportedError aus, Bitcoin- und Ethereum-Schlüssel brauchen also eine Bibliothek wie @noble/curves. Ed25519 funktioniert in allen vier Engines, Ed448 nur in Node.js. Die Post-Quanten-Unterstützung ist uneinheitlich: Node.js 24.22 implementiert ML-DSA und ML-KEM mit einer Warnung, dass sie experimentell sind, Chromium 156 akzeptiert ML-DSA und ML-KEM-768/1024, aber nicht ML-KEM-512, und Firefox 157 und WebKit 27.2 bieten keines von beiden – prüfe die Verfügbarkeit per Feature-Detection, bevor du dich darauf verlässt, und probiere die Algorithmen im Playground für Post-Quanten-Signaturen aus.

Das Beispiel zeigt das portable Muster: Nenne den Hash im Algorithmusobjekt, importiere fremde PEM-Schlüssel, indem du sie zuerst zu DER dekodierst, und denk daran, dass die 64 Byte lange Ausgabe in DER verpackt werden muss, bevor ein Verifizierer im Stil von OpenSSL sie liest.

Fakten und die Prüfungen dahinter

Nichts im Folgenden ist aus einer Dokumentation abgeschrieben. Das Prüfskript content/libraries/verify/webcrypto/run.sh hat WebCrypto (crypto.subtle) Node.js 24.22.0, Chromium 156.0.8078.4, Firefox 157.0, WebKit 27.2 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.

  • Kodierung von ECDSA-Signaturen

    raw

    sign() gab auf P-256 in Node.js, Chromium, Firefox und WebKit genau 64 Byte rohes r‖s zurück; die Spezifikation sieht keine DER-Option vor.

    Von der Prüfung aufgezeichnet
    fact ecdsa-signature-encoding = raw
  • Low-S beim Signieren

    either

    Signaturen werden nicht normalisiert: In jeder Engine traten unter 64 Signaturen High-S-Werte auf.

    Von der Prüfung aufgezeichnet
    fact ecdsa-low-s-sign = either
  • High-S bei der Prüfung

    accepts-high-s

    Jede Engine akzeptierte eine Signatur, in der s durch n − s ersetzt war.

    Von der Prüfung aufgezeichnet
    fact ecdsa-low-s-verify = accepts-high-s
  • ECDSA-Nonce

    random

    Zufällige Nonce in jeder Engine – zweimaliges Signieren derselben Daten ergibt unterschiedliche Bytes, und eine deterministische Option gibt es nicht.

    Von der Prüfung aufgezeichnet
    fact ecdsa-nonce = random
  • Was der Signaturaufruf hasht

    hashes-message

    sign() hasht die Daten mit dem Hash, der im Algorithmusobjekt genannt ist; die Signatur jeder Engine bestand in einer unabhängigen Implementierung die Prüfung gegen den SHA-256-Hash der Nachricht. Einen vorab berechneten Digest zu signieren ist nicht möglich.

    Von der Prüfung aufgezeichnet
    fact ecdsa-prehash = hashes-message
  • Fehlerhafte ECDSA-Signatur

    returns-false

    Eine DER-Signatur, eine 63-Byte-Signatur und ein 3-Byte-Blob ließen verify() in allen vier Engines false zurückgeben; nichts löst eine Ausnahme aus.

    Von der Prüfung aufgezeichnet
    fact ecdsa-malformed-signature = returns-false
    evidence ecdsa-malformed-signature: Node.js: DER false, 63 bytes false, 3 bytes false
    evidence ecdsa-malformed-signature: Chromium: DER false, 63 bytes false, 3 bytes false
    evidence ecdsa-malformed-signature: Firefox: DER false, 63 bytes false, 3 bytes false
    evidence ecdsa-malformed-signature: WebKit: DER false, 63 bytes false, 3 bytes false
  • ECDSA-Kurven

    P-256P-384P-521

    Nur P-256, P-384 und P-521; secp256k1, K-256 und brainpoolP256r1 lösten in jeder Engine NotSupportedError aus.

    Von der Prüfung aufgezeichnet
    fact ecdsa-curves = P-256, P-384, P-521
    evidence ecdsa-curves: Node.js: P-256 true; P-384 true; P-521 true; secp256k1 NotSupportedError; K-256 NotSupportedError; brainpoolP256r1 NotSupportedError
    evidence ecdsa-curves: Chromium: P-256 true; P-384 true; P-521 true; secp256k1 NotSupportedError; K-256 NotSupportedError; brainpoolP256r1 NotSupportedError
    evidence ecdsa-curves: Firefox: P-256 true; P-384 true; P-521 true; secp256k1 NotSupportedError; K-256 NotSupportedError; brainpoolP256r1 NotSupportedError
    evidence ecdsa-curves: WebKit: P-256 true; P-384 true; P-521 true; secp256k1 NotSupportedError; K-256 NotSupportedError; brainpoolP256r1 NotSupportedError
  • EdDSA

    Ed25519

    Signieren und Prüfen mit Ed25519 funktionieren in allen vier Engines.

    Von der Prüfung aufgezeichnet
    fact eddsa = Ed25519
  • Ed448-Unterstützung nach Engine

    Node.js

    Ed448 funktionierte nur in Node.js, das es als experimentell kennzeichnet; Chromium, Firefox und WebKit lehnten es ab.

    Von der Prüfung aufgezeichnet
    fact ed448-engines = Node.js
  • Post-Quanten-Algorithmen

    none

    Noch funktioniert kein Post-Quanten-Algorithmus in allen vier Engines, portabler Code kann sich also auf keinen verlassen.

    Von der Prüfung aufgezeichnet
    fact pqc = none
    evidence pqc: Node.js: ML-DSA-44 true; ML-DSA-65 true; ML-DSA-87 true; ML-KEM-512 true; ML-KEM-768 true; ML-KEM-1024 true
    evidence pqc: Chromium: ML-DSA-44 true; ML-DSA-65 true; ML-DSA-87 true; ML-KEM-512 NotSupportedError; ML-KEM-768 true; ML-KEM-1024 true
    evidence pqc: Firefox: ML-DSA-44 NotSupportedError; ML-DSA-65 NotSupportedError; ML-DSA-87 NotSupportedError; ML-KEM-512 NotSupportedError; ML-KEM-768 NotSupportedError; ML-KEM-1024 NotSupportedError
    evidence pqc: WebKit: ML-DSA-44 NotSupportedError; ML-DSA-65 NotSupportedError; ML-DSA-87 NotSupportedError; ML-KEM-512 TypeError; ML-KEM-768 TypeError; ML-KEM-1024 TypeError
  • ML-DSA-Unterstützung nach Engine

    Node.jsChromium

    Mit ML-DSA-44, ML-DSA-65 und ML-DSA-87 funktionierten Signieren und Prüfen in Node.js (experimentell) und Chromium 156; Firefox und WebKit lösten NotSupportedError aus.

    Von der Prüfung aufgezeichnet
    fact ml-dsa-engines = Node.js, Chromium
  • ML-KEM-Unterstützung nach Engine

    Node.jsChromium

    encapsulateBits und decapsulateBits mit ML-KEM-768 funktionierten in Node.js und Chromium 156; Chromium lehnte ML-KEM-512 ab, und Firefox und WebKit unterstützten überhaupt kein ML-KEM.

    Von der Prüfung aufgezeichnet
    fact ml-kem-engines = Node.js, Chromium
  • Formate öffentlicher Schlüssel

    spki-derjwksec1-uncompressed

    Jede Engine importierte SPKI als DER-Bytes, einen JWK und den unkomprimierten Punkt im SEC1-Format ("raw"); PEM-Text und ein 64 Byte langes x‖y ohne das Präfix 0x04 lösten DataError aus.

    Von der Prüfung aufgezeichnet
    fact public-key-formats = spki-der, jwk, sec1-uncompressed
    evidence public-key-formats: Node.js: spki-pem DataError; spki-der true; jwk true; sec1-uncompressed true; sec1-compressed true; raw-xy DataError
    evidence public-key-formats: Chromium: spki-pem DataError; spki-der true; jwk true; sec1-uncompressed true; sec1-compressed true; raw-xy DataError
    evidence public-key-formats: Firefox: spki-pem DataError; spki-der true; jwk true; sec1-uncompressed true; sec1-compressed true; raw-xy DataError
    evidence public-key-formats: WebKit: spki-pem DataError; spki-der true; jwk true; sec1-uncompressed true; sec1-compressed DataError; raw-xy DataError
  • Komprimierte Punkte nach Engine

    Node.jsChromiumFirefox

    Ein komprimierter 33-Byte-Punkt ließ sich mit "raw" in Node.js, Chromium und Firefox importieren, WebKit dagegen löste DataError aus.

    Von der Prüfung aufgezeichnet
    fact compressed-point-engines = Node.js, Chromium, Firefox
  • Formate privater Schlüssel

    pkcs8-derjwk

    Private Schlüssel lassen sich nur als PKCS#8-DER oder JWK importieren; einen bloßen 32-Byte-Skalar lehnte jede Engine ab.

    Von der Prüfung aufgezeichnet
    fact private-key-formats = pkcs8-der, jwk
    evidence private-key-formats: Node.js: pkcs8-der true; jwk true; raw-scalar SyntaxError
    evidence private-key-formats: Chromium: pkcs8-der true; jwk true; raw-scalar SyntaxError
    evidence private-key-formats: Firefox: pkcs8-der true; jwk true; raw-scalar DataError
    evidence private-key-formats: WebKit: pkcs8-der true; jwk true; raw-scalar SyntaxError

Verwendung

Signiere mit P-256, importiere einen öffentlichen Schlüssel im PEM-Format, indem du ihn zu DER dekodierst, und prüfe – die Signatur liegt in der rohen 64-Byte-Form vor.
// The same code runs in browsers, Deno, Bun and Node.js (globalThis.crypto).
const { subtle } = globalThis.crypto;
const curve = { name: "ECDSA", namedCurve: "P-256" };
const ecdsa = { name: "ECDSA", hash: "SHA-256" };

const { privateKey, publicKey } = await subtle.generateKey(curve, true, ["sign", "verify"]);
const data = new TextEncoder().encode("order #1042 approved");
const signature = new Uint8Array(await subtle.sign(ecdsa, privateKey, data));

// A PEM public key from another system: importKey("spki") wants the DER bytes,
// so strip the armour and base64-decode first. PEM text itself is a DataError.
const spki = new Uint8Array(await subtle.exportKey("spki", publicKey));
const pem = `-----BEGIN PUBLIC KEY-----\n${btoa(String.fromCharCode(...spki))}\n-----END PUBLIC KEY-----`;
const der = Uint8Array.from(atob(pem.replace(/-----[^-]+-----|\s/g, "")), (c) => c.charCodeAt(0));
const imported = await subtle.importKey("spki", der, curve, false, ["verify"]);

// Always 64 bytes on P-256: raw r||s, never DER. Convert before handing it to OpenSSL or Java.
console.log("signature bytes:", signature.length);
console.log("verified:", await subtle.verify(ecdsa, imported, signature, data));

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.

Befehl
node usage.mjs
Ausgabe muss passen (regulärer Ausdruck)
signature bytes: 64\nverified: true

Bibliotheken