Digitale Signaturen halten das halbe Internet zusammen. Jedes TLS-Zertifikat, jedes signierte JSON Web Token, jede Bitcoin-Transaktion und jeder SSH-Login stützt sich auf dieselbe Idee: Ein privater Schlüssel erzeugt eine Signatur, die jeder mit dem passenden öffentlichen Schlüssel prüfen kann, ohne den geheimen Teil je zu sehen. Der mit Abstand am häufigsten eingesetzte Algorithmus dafür heißt ECDSA (Elliptic Curve Digital Signature Algorithm).
In diesem Tutorial bauen Sie ECDSA von Grund auf in Node.js, ausschließlich mit dem eingebauten crypto-Modul, ohne externe Bibliotheken. Sie erzeugen Schlüsselpaare, signieren und verifizieren Nachrichten, exportieren Schlüssel als PEM, arbeiten mit der Bitcoin-Kurve secp256k1, vermeiden die berüchtigte Nonce-Falle und bauen am Ende eine signierte REST-API mit ES256-Tokens. Geplante Zeit: rund 45 Minuten. Stand: 12. Juni 2026.
Was ist ECDSA und warum es 2026 noch zählt
ECDSA ist die Variante des klassischen Digital Signature Algorithm, die auf elliptischen Kurven rechnet statt auf großen Primzahl-Modulen wie RSA. Der Vorteil ist drastisch kleinere Schlüssel bei gleicher Sicherheit. Ein P-256-Schlüssel liefert etwa 128 Bit Sicherheit und entspricht damit grob einem 3072-Bit-RSA-Schlüssel. Ein P-384-Schlüssel liegt ungefähr auf dem Niveau von 7680-Bit-RSA. Kleinere Schlüssel bedeuten schnellere Handshakes, kleinere Zertifikate und weniger Bandbreite, weshalb moderne TLS-Verbindungen ECDSA-Zertifikaten den Vorzug geben.
Eine digitale Signatur garantiert drei Dinge gleichzeitig: Authentizität (die Nachricht stammt vom Inhaber des privaten Schlüssels), Integrität (sie wurde nicht verändert) und Nichtabstreitbarkeit (der Unterzeichner kann die Signatur nicht glaubhaft leugnen). ECDSA erreicht das, indem es zuerst einen Hash der Nachricht bildet, meist mit SHA-256, und dann diesen Hash mit dem privaten Schlüssel und einer Zufallszahl signiert. Ohne den Hash würde der Algorithmus nicht funktionieren, weshalb ein solides Verständnis von SHA-256 die Basis für jede ECDSA-Implementierung bildet.
Wo begegnet Ihnen ECDSA konkret? In TLS-Zertifikatsketten und im Handshake. In JWT mit dem Algorithmus ES256. In SSH als Host- und Nutzerschlüssel neben Ed25519 und RSA. Und in jeder einzelnen Bitcoin- und Ethereum-Transaktion, dort über die spezielle Kurve secp256k1. Wer Backend-Software baut, kommt an ECDSA praktisch nicht vorbei. Genau deshalb lohnt es sich, den Algorithmus einmal mit eigenen Händen in Node.js umzusetzen statt ihn als Blackbox zu behandeln.
Voraussetzungen: Versionen und Werkzeuge
Dieses Tutorial setzt eine moderne Node.js-Umgebung voraus. Alle Code-Beispiele nutzen ausschließlich Bordmittel, sodass Sie keine npm-Pakete installieren müssen (bis auf das optionale JWT-Beispiel am Ende). Prüfen Sie zuerst Ihre Versionen.
| Komponente | Empfohlene Version (2026) | Mindestversion | Prüfbefehl |
|---|---|---|---|
| Node.js | 24.x LTS (Krypton) | 20.x | node -v |
| OpenSSL (in Node gebündelt) | 3.5 | 3.0 | node -p "process.versions.openssl" |
| npm | 11.x | 10.x | npm -v |
| Editor | VS Code / beliebig | – | – |
| Terminal | bash / zsh / PowerShell | – | – |
Node.js 24 ist 2026 die aktuelle LTS-Linie für neue Projekte und wird bis April 2028 unterstützt. Sein crypto-Modul basiert auf OpenSSL 3.5 und läuft mit dem Standard-Sicherheitslevel 2. Das ist wichtig: Auf Sicherheitslevel 2 verbietet OpenSSL elliptische Kurven mit weniger als 224 Bit sowie RSA-Schlüssel unter 2048 Bit. Alle Kurven in diesem Tutorial (P-256, P-384, secp256k1) liegen klar über dieser Grenze, sodass es keine Probleme gibt. Node.js 22 funktioniert ebenfalls, befindet sich aber im Wartungsmodus.
Falls node -v eine Version unter 20 zeigt, aktualisieren Sie zuerst. Auf Linux und macOS ist nvm der bequemste Weg, unter Windows der offizielle Installer von nodejs.org. Legen Sie danach einen Arbeitsordner an und stellen Sie sicher, dass Sie Schreibrechte besitzen. Den vollständigen Funktionsumfang des Moduls finden Sie in der offiziellen Node.js crypto-Dokumentation, die wir in diesem Tutorial mehrfach als Referenz heranziehen.
ECDSA vs. RSA vs. Ed25519: der direkte Vergleich
Bevor Sie Code schreiben, sollten Sie wissen, wann ECDSA die richtige Wahl ist und wann nicht. Drei Signaturverfahren dominieren 2026 die Praxis. Die folgende Tabelle stellt sie gegenüber.
| Eigenschaft | ECDSA (P-256) | RSA-3072 | Ed25519 (EdDSA) |
|---|---|---|---|
| Sicherheitsniveau | ~128 Bit | ~128 Bit | ~128 Bit |
| Öffentlicher Schlüssel | ~65 Byte | ~398 Byte | 32 Byte |
| Signaturgröße | ~64 bis 72 Byte | ~384 Byte | 64 Byte |
| Signieren (relativ) | schnell | langsam | sehr schnell |
| Verifizieren (relativ) | mittel | sehr schnell | schnell |
| Nonce-empfindlich | ja (kritisch) | nein | nein (deterministisch) |
| JWT-Kennung | ES256 / ES384 | RS256 / PS256 | EdDSA |
| Bitcoin / Ethereum | ja (secp256k1) | nein | nein |
Die wichtigste Erkenntnis: Ed25519 gehört nicht zu ECDSA, sondern zur Familie EdDSA. Es nutzt einen deterministischen Ansatz und umgeht damit die gefährlichste Schwäche von ECDSA, die Zufallszahl pro Signatur. Wenn Sie heute ein neues System ohne Altlasten bauen und Ihre Gegenstellen Ed25519 unterstützen, ist es oft die robustere Wahl. ECDSA gewinnt dort, wo Sie Kompatibilität brauchen: TLS-Zertifikate von öffentlichen CAs, ES256-Tokens in OAuth-Ökosystemen, SSH gegen ältere Server und vor allem Blockchain. Ein SSH-Schlüssel auf Basis von Ed25519 lässt sich übrigens in wenigen Minuten erzeugen, wie unser Leitfaden zum SSH-Key mit Ed25519 zeigt.
RSA bleibt relevant, wenn Verifikation extrem häufig und Signierung selten passiert, etwa bei Software-Signaturen, die Millionen Clients prüfen. Für die meisten Web-Backends ist ECDSA mit P-256 jedoch der pragmatische Mittelweg aus Geschwindigkeit, Kompatibilität und kleinen Schlüsseln.
Schritt 1: Das Projekt einrichten
Erstellen Sie einen Projektordner und initialisieren Sie ihn. Wir aktivieren ES-Module, damit wir modernes import-Syntax nutzen können.
mkdir ecdsa-tutorial && cd ecdsa-tutorial
npm init -y
npm pkg set type=module
node -v # erwartet v24.x oder mindestens v20.x
node -p "process.versions.openssl" # erwartet 3.5 oder höher
Der Befehl npm pkg set type=module trägt "type": "module" in die package.json ein. Damit behandelt Node jede .js-Datei als ES-Modul. Legen Sie nun eine erste Datei an und prüfen Sie, dass das crypto-Modul verfügbar ist.
// check.js
import { getCurves } from 'node:crypto';
console.log('Verfügbare Kurven (Auszug):');
console.log(getCurves().filter(c => ['prime256v1', 'secp384r1', 'secp256k1'].includes(c)));
Führen Sie die Datei mit node check.js aus. Die erwartete Ausgabe bestätigt, dass die drei Kurven, die wir nutzen werden, vorhanden sind:
Verfügbare Kurven (Auszug):
[ 'prime256v1', 'secp384r1', 'secp256k1' ]
Wichtig zur Namensgebung: In OpenSSL und damit in Node heißt die NIST-Kurve P-256 intern prime256v1. P-384 heißt secp384r1. Diese internen Namen verwenden Sie überall dort, wo Node eine Kurve als Zeichenkette erwartet. Merken Sie sich die Zuordnung, sie ist eine häufige Stolperfalle.
Schritt 2: Ein ECDSA-Schlüsselpaar erzeugen
Jetzt erzeugen Sie das Herzstück: ein Schlüsselpaar auf der Kurve P-256. Node bietet dafür generateKeyPairSync, die synchrone Variante, ideal zum Lernen. In Produktion bevorzugen Sie die asynchrone Variante, dazu später mehr.
// keys.js
import { generateKeyPairSync } from 'node:crypto';
const { publicKey, privateKey } = generateKeyPairSync('ec', {
namedCurve: 'prime256v1', // entspricht NIST P-256
});
console.log('Privater Schlüssel-Typ:', privateKey.asymmetricKeyType);
console.log('Kurve:', privateKey.asymmetricKeyDetails.namedCurve);
export { publicKey, privateKey };
Die Ausgabe sieht so aus:
Privater Schlüssel-Typ: ec
Kurve: prime256v1
Beachten Sie, dass publicKey und privateKey hier KeyObject-Instanzen sind, keine reinen Zeichenketten. Ein KeyObject ist Nodes interne, sichere Repräsentation eines Schlüssels. Es verhindert versehentliches Loggen des geheimen Materials und lässt sich direkt an Signierfunktionen übergeben. Erst wenn Sie Schlüssel speichern oder über das Netz schicken, wandeln Sie sie in PEM- oder DER-Format um, das zeigt Schritt 5.
Wollen Sie stattdessen die stärkere Kurve P-384, ändern Sie lediglich namedCurve: 'secp384r1'. Für Bitcoin-kompatible Schlüssel verwenden Sie 'secp256k1'. Der restliche Code bleibt identisch, das ist die Eleganz der KeyObject-Abstraktion.
Schritt 3: Eine Nachricht signieren
Mit dem Schlüsselpaar in der Hand signieren Sie nun Daten. Node stellt die sign-Funktion bereit, die den Hash intern berechnet. Sie geben nur den Hash-Algorithmus an, hier SHA-256, was zusammen mit P-256 dem JWT-Standard ES256 entspricht.
// sign.js
import { sign } from 'node:crypto';
import { privateKey } from './keys.js';
const nachricht = Buffer.from('Überweisung: 250 EUR an Konto DE89...');
// SHA-256 wird intern gebildet, dann mit dem privaten Schlüssel signiert
const signatur = sign('sha256', nachricht, {
key: privateKey,
dsaEncoding: 'ieee-p1363', // festes 64-Byte-Format, ideal für JWT/Web
});
console.log('Signaturlänge:', signatur.length, 'Byte');
console.log('Signatur (base64url):', signatur.toString('base64url'));
export { nachricht, signatur };
Eine typische Ausgabe:
Signaturlänge: 64 Byte
Signatur (base64url): k3Jx9... (gekürzt)
Der Parameter dsaEncoding ist entscheidend und wird oft übersehen. ECDSA-Signaturen bestehen aus zwei Zahlen, r und s. Es gibt zwei Wege, sie zu kodieren. Der Standard 'der' verpackt sie in eine ASN.1-Struktur variabler Länge (etwa 70 bis 72 Byte). Das Format 'ieee-p1363' hängt r und s einfach aneinander und liefert konstant 64 Byte bei P-256. Web-Standards wie JWT (ES256) und WebCrypto verlangen P1363. Wenn Sie mit OpenSSL-Kommandozeile oder X.509-Zertifikaten arbeiten, brauchen Sie DER. Verwechseln Sie die beiden, schlägt jede Verifikation fehl, obwohl Schlüssel und Daten korrekt sind.
Ein zweiter wichtiger Punkt: Jeder Aufruf von sign erzeugt eine andere Signatur für dieselbe Nachricht. Das ist kein Fehler, sondern liegt an der internen Zufallszahl k. Solange diese Zufallszahl echt zufällig ist, ist das Verhalten korrekt und sicher.
Schritt 4: Die Signatur verifizieren
Die Verifikation nutzt den öffentlichen Schlüssel. Sie liefert einen schlichten Booleschen Wert: true bei gültiger Signatur, false sonst. Wichtig ist, dass Sie exakt dieselbe dsaEncoding wie beim Signieren angeben.
// verify.js
import { verify } from 'node:crypto';
import { publicKey } from './keys.js';
import { nachricht, signatur } from './sign.js';
const gueltig = verify('sha256', nachricht, {
key: publicKey,
dsaEncoding: 'ieee-p1363',
}, signatur);
console.log('Signatur gültig?', gueltig); // true
// Manipulationstest: ein Byte ändern
const gefaelscht = Buffer.from('Überweisung: 999 EUR an Konto DE89...');
const gueltig2 = verify('sha256', gefaelscht, {
key: publicKey,
dsaEncoding: 'ieee-p1363',
}, signatur);
console.log('Manipulierte Nachricht gültig?', gueltig2); // false
Erwartete Ausgabe:
Signatur gültig? true
Manipulierte Nachricht gültig? false
Dieser Manipulationstest demonstriert die Kernaussage jeder Signatur: Schon die Änderung eines einzigen Zeichens (250 zu 999) führt zu einem völlig anderen SHA-256-Hash, und die alte Signatur passt nicht mehr. Genau dieses Prinzip schützt TLS-Verbindungen, Software-Updates und Blockchain-Transaktionen. Wer tiefer verstehen will, warum schon kleinste Änderungen den Hash komplett umwerfen, findet die Erklärung in unserem Beitrag über digitale Signaturen.
Ein praktischer Sicherheitshinweis: Behandeln Sie eine false-Rückgabe immer als Ablehnung der gesamten Anfrage. Loggen Sie den Vorfall, aber verraten Sie dem Aufrufer nie, warum genau die Prüfung scheiterte. Detaillierte Fehlermeldungen helfen Angreifern beim systematischen Ausprobieren.
Schritt 5: Schlüssel als PEM exportieren und laden
Bisher lebten die Schlüssel nur im Speicher. Für echte Anwendungen müssen Sie sie persistieren, etwa in einer Datei oder einem Secret-Manager. Das Standardformat dafür ist PEM, eine base64-kodierte Textdarstellung mit Kopf- und Fußzeile.
// export.js
import { generateKeyPairSync, createPrivateKey, createPublicKey } from 'node:crypto';
import { writeFileSync, readFileSync } from 'node:fs';
const { publicKey, privateKey } = generateKeyPairSync('ec', {
namedCurve: 'prime256v1',
});
// Export als PEM-Text
const privPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
const pubPem = publicKey.export({ type: 'spki', format: 'pem' });
writeFileSync('private.pem', privPem);
writeFileSync('public.pem', pubPem);
console.log('Schlüssel geschrieben.');
// Wieder einlesen
const privGeladen = createPrivateKey(readFileSync('private.pem'));
const pubGeladen = createPublicKey(readFileSync('public.pem'));
console.log('Geladene Kurve:', privGeladen.asymmetricKeyDetails.namedCurve);
Die Ausgabe bestätigt das Schreiben und Laden:
Schlüssel geschrieben.
Geladene Kurve: prime256v1
Zur Terminologie: pkcs8 ist das moderne Containerformat für private Schlüssel, spki das für öffentliche. Diese Kombination ist 2026 die empfohlene Wahl und mit nahezu jeder anderen Krypto-Bibliothek kompatibel. Die erzeugte private.pem beginnt mit -----BEGIN PRIVATE KEY-----, die public.pem mit -----BEGIN PUBLIC KEY-----.
Private Schlüssel niemals im Klartext ablegen
Eine unverschlüsselte private.pem auf der Festplatte ist ein Risiko. In Produktion verschlüsseln Sie den privaten Schlüssel mit einer Passphrase oder lagern ihn in einem Secret-Manager wie HashiCorp Vault, AWS KMS oder einem Hardware-Sicherheitsmodul aus. Für die passphrasen-geschützte Variante ergänzen Sie beim Export einfach eine cipher– und passphrase-Option. Wer einen verschlüsselten Datenträger als zusätzliche Schutzschicht möchte, findet praktische Schritte in unserem Tutorial zu VeraCrypt.
Schritt 6: Mit secp256k1 arbeiten, der Bitcoin-Kurve
Bitcoin und Ethereum nutzen nicht P-256, sondern die Kurve secp256k1. Sie bietet ebenfalls rund 128 Bit Sicherheit, wurde aber mit besonders nachvollziehbaren Parametern entworfen, was Misstrauen gegenüber möglichen Hintertüren reduziert. In Node ist sie ein einzeiliger Tausch.
// bitcoin-curve.js
import { generateKeyPairSync, sign, verify } from 'node:crypto';
const { publicKey, privateKey } = generateKeyPairSync('ec', {
namedCurve: 'secp256k1', // die Bitcoin- und Ethereum-Kurve
});
const tx = Buffer.from('send 0.05 BTC to bc1q...');
const sig = sign('sha256', tx, { key: privateKey, dsaEncoding: 'ieee-p1363' });
const ok = verify('sha256', tx, { key: publicKey, dsaEncoding: 'ieee-p1363' }, sig);
console.log('secp256k1-Signatur gültig?', ok); // true
console.log('Signaturlänge:', sig.length, 'Byte'); // 64
Die Mechanik ist identisch zu P-256, nur die Kurve wechselt. In echten Wallets kommen jedoch zusätzliche Konventionen hinzu: Bitcoin verlangt sogenannte Low-S-Signaturen (BIP 62), um Transaktions-Malleability zu verhindern, und nutzt double-SHA256 statt einfachem SHA-256. Das eingebaute crypto-Modul deckt die Kryptografie ab, die Blockchain-spezifischen Regeln implementieren spezialisierte Bibliotheken. Eine technische Referenz zur Kurve selbst bietet das Bitcoin-Wiki zu secp256k1.
Wer den privaten Schlüssel einer Kryptowährung verwaltet, sollte ihn niemals auf einem Server im Klartext halten. Hardware-Wallets isolieren das Schlüsselmaterial physisch. Der Unterschied zwischen den gängigen Geräten ist Thema unseres Vergleichs Ledger vs. Trezor.
Schritt 7: Das Nonce-Problem und deterministische Signaturen
Jetzt zur gefährlichsten Eigenschaft von ECDSA. Bei jeder Signatur wählt der Algorithmus eine geheime Zufallszahl k, die Nonce. Diese Zahl darf niemals zweimal mit demselben privaten Schlüssel verwendet werden, und sie darf nicht vorhersagbar sein. Verstoßen Sie dagegen, lässt sich der private Schlüssel aus zwei Signaturen mathematisch zurückrechnen. Das ist kein theoretisches Risiko.
Das bekannteste Beispiel ist die Sony PlayStation 3 aus dem Jahr 2010. Sony verwendete eine feste, konstante Nonce für alle ECDSA-Signaturen der Firmware. Angreifer extrahierten daraus den privaten Signierschlüssel und konnten beliebigen Code als offiziell signiert ausgeben. Der gesamte Schutzmechanismus der Konsole brach an einem einzigen Implementierungsfehler zusammen.
Die gute Nachricht: Node.js und OpenSSL erzeugen die Nonce mit einem kryptografisch sicheren Zufallsgenerator. Solange Sie das eingebaute sign nutzen und nicht selbst an der Nonce schrauben, sind Sie geschützt. Sie können die Robustheit zusätzlich erhöhen, indem Sie auf deterministische ECDSA-Signaturen nach RFC 6979 setzen, die k aus dem Schlüssel und der Nachricht ableiten statt aus einem Zufallsgenerator.
// Hinweis: Das eingebaute crypto-Modul nutzt zufällige Nonces.
// Für deterministische Signaturen nach RFC 6979 nutzen Sie
// eine spezialisierte Bibliothek oder wechseln zu Ed25519,
// das von Haus aus deterministisch arbeitet.
import { generateKeyPairSync, sign } from 'node:crypto';
const { privateKey } = generateKeyPairSync('ed25519'); // EdDSA, deterministisch
const msg = Buffer.from('keine Nonce-Sorgen');
const sig1 = sign(null, msg, privateKey); // Hash-Algorithmus = null bei Ed25519
const sig2 = sign(null, msg, privateKey);
console.log('Ed25519 deterministisch?', sig1.equals(sig2)); // true
Beachten Sie zwei Details im Ed25519-Beispiel: Der Hash-Parameter ist null, weil Ed25519 das Hashing selbst übernimmt, und zwei Signaturen derselben Nachricht sind identisch. Diese Determiniertheit ist genau der Grund, warum viele neue Protokolle Ed25519 bevorzugen. Die formale Grundlage für deterministisches ECDSA beschreibt RFC 6979.
Schritt 8: ECDSA in JSON Web Tokens (ES256)
Die häufigste Begegnung mit ECDSA im Web ist der Algorithmus ES256: ECDSA mit P-256 und SHA-256, der genau das P1363-Format aus Schritt 3 nutzt. Wir bauen ein minimales, signiertes Token von Hand, um zu zeigen, dass kein Zauber dahintersteckt.
// es256.js
import { generateKeyPairSync, sign, verify } from 'node:crypto';
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
const header = b64({ alg: 'ES256', typ: 'JWT' });
const payload = b64({ sub: 'user-42', role: 'admin', exp: 1781000000 });
const signingInput = `${header}.${payload}`;
const signature = sign('sha256', Buffer.from(signingInput), {
key: privateKey, dsaEncoding: 'ieee-p1363',
}).toString('base64url');
const jwt = `${signingInput}.${signature}`;
console.log('JWT:', jwt);
// Verifikation
const [h, p, s] = jwt.split('.');
const ok = verify('sha256', Buffer.from(`${h}.${p}`), {
key: publicKey, dsaEncoding: 'ieee-p1363',
}, Buffer.from(s, 'base64url'));
console.log('Token gültig?', ok); // true
Dieses Beispiel zeigt die Anatomie eines JWT: Header, Payload und Signatur, jeweils base64url-kodiert und mit Punkten getrennt. Der entscheidende Punkt ist dsaEncoding: 'ieee-p1363'. Mit dem Standard-DER-Format würde jede konforme JWT-Bibliothek das Token ablehnen, weil der ES256-Standard das feste 64-Byte-Format vorschreibt. Die formale Definition steht in RFC 7518 (JSON Web Algorithms).
In der Praxis schreiben Sie JWTs natürlich nicht von Hand, sondern nutzen eine geprüfte Bibliothek, die auch Ablauf, Audience und weitere Claims validiert. Das Handbeispiel dient dem Verständnis. So sehen Sie, dass ein ES256-Token nichts weiter ist als eine ECDSA-Signatur über zwei base64url-Strings.
Schritt 9: Komplettprojekt, eine signierte REST-API
Jetzt fügen wir alles zu einem lauffähigen Mini-Server zusammen. Er stellt zwei Endpunkte bereit: /sign erzeugt für eine Nachricht eine Signatur, /verify prüft sie. Wir nutzen nur den eingebauten HTTP-Server, also weiterhin keine Abhängigkeiten.
// server.js
import { createServer } from 'node:http';
import { generateKeyPairSync, sign, verify } from 'node:crypto';
// Schlüsselpaar einmalig beim Start (in Produktion aus Secret-Manager laden)
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const ENC = 'ieee-p1363';
function readBody(req) {
return new Promise((resolve) => {
let data = '';
req.on('data', (c) => (data += c));
req.on('end', () => resolve(data));
});
}
const server = createServer(async (req, res) => {
res.setHeader('Content-Type', 'application/json');
try {
if (req.method === 'POST' && req.url === '/sign') {
const { message } = JSON.parse(await readBody(req));
if (typeof message !== 'string') throw new Error('message fehlt');
const sig = sign('sha256', Buffer.from(message), { key: privateKey, dsaEncoding: ENC });
res.end(JSON.stringify({ message, signature: sig.toString('base64url') }));
return;
}
if (req.method === 'POST' && req.url === '/verify') {
const { message, signature } = JSON.parse(await readBody(req));
const ok = verify('sha256', Buffer.from(message), { key: publicKey, dsaEncoding: ENC },
Buffer.from(signature, 'base64url'));
res.statusCode = ok ? 200 : 401;
res.end(JSON.stringify({ valid: ok }));
return;
}
res.statusCode = 404;
res.end(JSON.stringify({ error: 'not found' }));
} catch (err) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'bad request' }));
}
});
server.listen(3000, () => console.log('ECDSA-API läuft auf http://localhost:3000'));
Starten Sie den Server mit node server.js und testen Sie ihn in einem zweiten Terminal mit curl:
# Signieren
curl -s -X POST http://localhost:3000/sign \
-H 'Content-Type: application/json' \
-d '{"message":"Hallo ECDSA"}'
# Antwort: {"message":"Hallo ECDSA","signature":"q8Fa...gekuerzt..."}
# Verifizieren (Signatur aus der Antwort oben einsetzen)
curl -s -X POST http://localhost:3000/verify \
-H 'Content-Type: application/json' \
-d '{"message":"Hallo ECDSA","signature":"q8Fa...gekuerzt..."}'
# Antwort: {"valid":true}
Damit haben Sie eine vollständige, lauffähige ECDSA-Anwendung. Der Server hält den privaten Schlüssel, Clients erhalten nur Signaturen und prüfen Nachrichten. Beachten Sie die Fehlerbehandlung: Jeder Ausnahmefall endet in einem generischen 400 oder einem 401 ohne Detailinformation. Diese Zurückhaltung ist Absicht und gehört zu sicherer API-Gestaltung.
Den öffentlichen Schlüssel verteilen
In einer echten Architektur würden Clients den öffentlichen Schlüssel kennen, um Signaturen selbst zu prüfen, ohne dem Server zu vertrauen. Ergänzen Sie dafür einen GET /pubkey-Endpunkt, der publicKey.export({ type: 'spki', format: 'pem' }) ausliefert. So verifiziert jeder Client lokal, was die Skalierbarkeit erhöht und die Vertrauensbasis verkleinert.
Schritt 10: Automatisierte Tests schreiben
Krypto-Code ohne Tests ist riskant, weil Fehler oft still bleiben: Eine falsche Kodierung führt nicht zum Absturz, sondern liefert einfach immer false. Node 24 bringt einen eingebauten Test-Runner mit, sodass Sie keine Test-Bibliothek installieren müssen.
// test/ecdsa.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { generateKeyPairSync, sign, verify } from 'node:crypto';
const ENC = 'ieee-p1363';
const keys = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
test('gültige Signatur verifiziert als true', () => {
const msg = Buffer.from('test');
const sig = sign('sha256', msg, { key: keys.privateKey, dsaEncoding: ENC });
assert.equal(verify('sha256', msg, { key: keys.publicKey, dsaEncoding: ENC }, sig), true);
});
test('manipulierte Nachricht verifiziert als false', () => {
const sig = sign('sha256', Buffer.from('test'), { key: keys.privateKey, dsaEncoding: ENC });
assert.equal(verify('sha256', Buffer.from('test!'), { key: keys.publicKey, dsaEncoding: ENC }, sig), false);
});
test('falsches Encoding schlägt fehl', () => {
const sig = sign('sha256', Buffer.from('test'), { key: keys.privateKey, dsaEncoding: 'der' });
assert.equal(verify('sha256', Buffer.from('test'), { key: keys.publicKey, dsaEncoding: ENC }, sig), false);
});
Führen Sie die Tests mit node --test aus. Die erwartete Ausgabe:
✔ gültige Signatur verifiziert als true
✔ manipulierte Nachricht verifiziert als false
✔ falsches Encoding schlägt fehl
ℹ tests 3
ℹ pass 3
ℹ fail 0
Der dritte Test ist besonders lehrreich: Er beweist, dass eine DER-Signatur gegen einen P1363-Verifizierer scheitert, obwohl Schlüssel und Nachricht stimmen. Genau dieser Fall verursacht in der Praxis die meisten ratlosen Debug-Stunden.
Fünf häufige Fehler und wie Sie sie vermeiden
Diese Stolperfallen tauchen immer wieder auf, wenn Entwickler ECDSA das erste Mal einsetzen. Wer sie kennt, spart sich Stunden.
- Encoding verwechseln. DER beim Signieren, P1363 beim Verifizieren, oder umgekehrt. Das Ergebnis ist immer
falseohne Fehlermeldung. Legen Sie die Kodierung als Konstante fest und nutzen Sie sie an beiden Stellen. - Kurvennamen falsch schreiben. P-256 heißt in Node
prime256v1, nichtp-256odersecp256r1. Ein falscher Name wirft sofort einen Fehler, was immerhin schnell auffällt. - Den privaten Schlüssel im Klartext committen. Eine
private.pemim Git-Repository ist ein klassischer Vorfall. Tragen Sie sie in.gitignoreein und nutzen Sie Secret-Manager. - Signatur und Nachricht entkoppeln. Wer eine Signatur prüft, aber die zugehörige Nachricht aus einer anderen Quelle nimmt, öffnet die Tür für Verwechslungsangriffe. Halten Sie beide untrennbar zusammen.
- Selbst an der Nonce schrauben. Jeder Versuch, k manuell zu setzen oder einen schwachen Zufallsgenerator zu verwenden, kann den privaten Schlüssel preisgeben. Überlassen Sie die Nonce immer dem crypto-Modul.
Troubleshooting: acht typische Probleme
Wenn etwas nicht funktioniert, hilft diese Übersicht der häufigsten Fehlermeldungen und ihrer Ursachen.
| Symptom | Wahrscheinliche Ursache | Lösung |
|---|---|---|
verify liefert immer false | Unterschiedliche dsaEncoding bei Sign und Verify | Beide auf denselben Wert setzen (meist ieee-p1363) |
Invalid EC curve name | Falscher Kurvenname | prime256v1, secp384r1 oder secp256k1 verwenden |
EXPECTING: PRIVATE KEY | PEM-Datei beschädigt oder falscher Typ | Datei auf vollständige BEGIN/END-Zeilen prüfen |
| JWT wird von Bibliothek abgelehnt | DER- statt P1363-Format | dsaEncoding: 'ieee-p1363' setzen |
ERR_OSSL_UNSUPPORTED | Kurve unter Sicherheitslevel 2 verboten | Kurve mit mindestens 224 Bit wählen |
Cannot use import statement | type: module fehlt | npm pkg set type=module ausführen |
| Signatur mal 64, mal 72 Byte | Gemischte Encodings im Code | Eine einheitliche Konstante nutzen |
| Ed25519-Signatur scheitert | Hash-Algorithmus angegeben statt null | Bei Ed25519 immer sign(null, ...) |
Bei hartnäckigen Problemen prüfen Sie zuerst die OpenSSL-Version mit node -p "process.versions.openssl". Verhalten und Fehlermeldungen unterscheiden sich teils zwischen OpenSSL 1.1 (alte Node-Versionen) und 3.x. Die offizielle OpenSSL-Seite dokumentiert die Unterschiede der Sicherheitslevel im Detail.
Fortgeschrittene Tipps für den Produktivbetrieb
Sobald Ihr ECDSA-Code die Lernphase verlässt, kommen zusätzliche Anforderungen ins Spiel. Diese Praktiken trennen ein Tutorial-Beispiel von produktionsreifer Software.
- Asynchron signieren. Unter Last blockiert
generateKeyPairSyncdie Event-Loop. Nutzen SiegenerateKeyPair(callback oder Promise), damit der Server während der Schlüsselerzeugung weiter Anfragen bedient. - Schlüsselrotation einplanen. Versehen Sie jeden Schlüssel mit einer Kennung (kid) und halten Sie alte öffentliche Schlüssel für eine Übergangszeit gültig, damit bereits ausgestellte Signaturen prüfbar bleiben.
- Konstante-Zeit-Vergleiche. Wenn Sie selbst Bytes vergleichen, etwa Token-Bestandteile, nutzen Sie
crypto.timingSafeEqualstatt===, um Timing-Angriffe zu verhindern. - Hardware-Schutz. Für hochwertige Schlüssel lagern Sie das private Material in ein HSM oder einen Cloud-KMS aus. Der Server signiert dann über eine API, sieht den geheimen Schlüssel aber nie.
- Eingaben begrenzen. Setzen Sie eine maximale Nachrichtengröße, bevor Sie signieren oder hashen, um Speicher-Erschöpfung durch riesige Anfragen zu vermeiden.
Ergänzend lohnt ein Blick auf die formalen Standards. Die aktuelle US-Norm für digitale Signaturen ist FIPS 186-5, die ECDSA und EdDSA gemeinsam spezifiziert. Sie ist die maßgebliche Referenz, wenn Sie Compliance-Anforderungen erfüllen müssen.
ECDSA und die Post-Quanten-Frage
Eine ehrliche Einordnung für 2026 gehört dazu: ECDSA ist nicht quantensicher. Ein ausreichend großer Quantencomputer könnte mit dem Shor-Algorithmus den privaten Schlüssel aus dem öffentlichen berechnen und damit jede ECDSA-Signatur fälschen. Solche Maschinen existieren heute nicht in der nötigen Größe, doch die Migration läuft bereits an.
Die NIST hat 2024 die ersten Post-Quanten-Standards finalisiert, darunter ML-DSA (basierend auf dem Verfahren Dilithium) als quantenresistenten Signaturalgorithmus. Der realistische Pfad für die nächsten Jahre sind hybride Signaturen, die ECDSA und einen Post-Quanten-Algorithmus kombinieren. So bleibt die Verbindung sicher, selbst wenn eines der beiden Verfahren später fällt. Einen breiteren Einstieg in das Thema bietet unsere Übersicht zu Hashing und Kryptographie.
Für die Praxis 2026 heißt das: ECDSA ist für neue Projekte weiterhin sicher und Standard, aber bauen Sie Ihre Software so, dass sich der Signaturalgorithmus austauschen lässt. Eine klar gekapselte Sign- und Verify-Schicht, wie das Komplettprojekt in Schritt 9 sie andeutet, macht den späteren Wechsel zu hybriden oder rein quantensicheren Verfahren zur überschaubaren Aufgabe statt zum Großprojekt.
Schritt 11: Durchsatz und Performance messen
Bevor Sie ECDSA in einen Hochlast-Dienst einbauen, sollten Sie wissen, wie viele Operationen Ihre Hardware schafft. Ein einfacher Mikro-Benchmark mit der eingebauten performance-API liefert belastbare Zahlen für Ihre konkrete Maschine, statt sich auf fremde Messwerte zu verlassen.
// bench.js
import { generateKeyPairSync, sign, verify } from 'node:crypto';
import { performance } from 'node:perf_hooks';
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const ENC = 'ieee-p1363';
const msg = Buffer.from('benchmark-nachricht');
const N = 10000;
// Signieren messen
let sig;
let t0 = performance.now();
for (let i = 0; i < N; i++) {
sig = sign('sha256', msg, { key: privateKey, dsaEncoding: ENC });
}
let signMs = performance.now() - t0;
// Verifizieren messen
t0 = performance.now();
for (let i = 0; i < N; i++) {
verify('sha256', msg, { key: publicKey, dsaEncoding: ENC }, sig);
}
let verifyMs = performance.now() - t0;
console.log(`Signieren: ${Math.round(N / (signMs / 1000))} Ops/s`);
console.log(`Verifizieren: ${Math.round(N / (verifyMs / 1000))} Ops/s`);
Die absoluten Zahlen hängen stark von CPU, Node-Version und OpenSSL-Build ab, weshalb Sie den Test auf Ihrer Zielhardware ausführen sollten. Als grobe Orientierung gilt: Auf einer modernen Server-CPU liegen ECDSA-Signaturen mit P-256 typischerweise im Bereich von mehreren zehntausend Operationen pro Sekunde pro Kern. Das Verifizieren ist bei ECDSA etwas langsamer als das Signieren, anders als bei RSA, wo die Verifikation deutlich schneller läuft als die Signatur. Wenn Ihr Lastprofil also von extrem häufiger Verifikation dominiert wird, kann RSA trotz größerer Schlüssel im Vorteil sein. Für ausgewogene Sign- und Verify-Last bleibt ECDSA die effizientere Wahl.
Ein praktischer Tipp für die Messung: Lassen Sie den Benchmark mehrere Sekunden laufen und verwerfen Sie die erste Runde. Die JIT-Optimierung von V8 braucht ein paar Iterationen, bis sie den heißen Pfad kompiliert hat. Messen Sie zu früh, erhalten Sie zu pessimistische Werte. Vergleichen Sie außerdem P-256 gegen secp256k1 und Ed25519, indem Sie nur die Schlüsselerzeugung tauschen. In den meisten Setups signiert Ed25519 spürbar schneller, was es für Systeme mit sehr hohem Signaturdurchsatz attraktiv macht.
Wie ECDSA den TLS-Handshake beschleunigt
Der häufigste produktive Einsatz von ECDSA spielt sich unsichtbar im TLS-Handshake ab, jedes Mal, wenn ein Browser eine HTTPS-Verbindung aufbaut. Der Server präsentiert sein Zertifikat und beweist mit einer ECDSA-Signatur, dass er den zugehörigen privaten Schlüssel besitzt. Weil ein ECDSA-Zertifikat mit P-256 nur einen Bruchteil der Größe eines gleichwertigen RSA-3072-Zertifikats hat, sinkt die übertragene Datenmenge im Handshake, und der rechenintensive Signaturschritt läuft schneller. Auf mobilen Verbindungen mit hoher Latenz macht das einen messbaren Unterschied bei der Zeit bis zum ersten sichtbaren Inhalt.
Moderne TLS-1.3-Verbindungen kombinieren ECDSA zur Authentifizierung mit ECDH zum Schlüsselaustausch. Beide rechnen auf elliptischen Kurven, nutzen aber unterschiedliche Schlüssel und Zwecke, was den in der FAQ erwähnten Grundsatz der Zwecktrennung unterstreicht. Wer genauer verstehen will, was beim Verbindungsaufbau zwischen Browser und Server passiert und wie das Schloss-Symbol zustande kommt, findet die Details in unserem Beitrag zu HTTPS und TLS.
Für Betreiber bedeutet das eine konkrete Empfehlung: Stellen Sie nach Möglichkeit ECDSA-Zertifikate aus, idealerweise mit P-256, und halten Sie ein RSA-Zertifikat nur für sehr alte Clients als Fallback bereit. Viele öffentliche Zertifizierungsstellen unterstützen das parallele Ausstellen beider Typen. Der Server wählt dann je nach Fähigkeiten des Clients automatisch das passende Zertifikat, sodass Sie die Geschwindigkeitsvorteile von ECDSA nutzen, ohne Kompatibilität zu opfern.
Häufig gestellte Fragen zu ECDSA in Node.js
Brauche ich für ECDSA eine externe Bibliothek?
Nein. Das eingebaute node:crypto-Modul deckt Schlüsselerzeugung, Signieren und Verifizieren für alle gängigen Kurven vollständig ab. Externe Pakete brauchen Sie erst für spezialisierte Aufgaben wie deterministische RFC-6979-Signaturen, Bitcoin-spezifische Konventionen oder komfortable JWT-Verwaltung.
Welche Kurve soll ich wählen, P-256 oder secp256k1?
Für Web, TLS, JWT und allgemeine Backends nehmen Sie P-256 (prime256v1), den De-facto-Standard. secp256k1 brauchen Sie nur, wenn Sie mit Bitcoin, Ethereum oder anderen Blockchains interagieren, die genau diese Kurve verlangen. Beide bieten rund 128 Bit Sicherheit.
Warum sind zwei Signaturen derselben Nachricht unterschiedlich?
ECDSA verwendet pro Signatur eine geheime Zufallszahl (Nonce). Dadurch unterscheiden sich die Signaturen, obwohl beide gültig sind. Das ist korrektes Verhalten. Wollen Sie identische Signaturen für identische Eingaben, nutzen Sie deterministisches ECDSA nach RFC 6979 oder wechseln zu Ed25519.
Ist ECDSA sicherer als RSA?
Bei gleichem Sicherheitsniveau bietet ECDSA deutlich kleinere Schlüssel und schnellere Signaturen. Ein P-256-Schlüssel entspricht grob RSA-3072. RSA ist nicht unsicherer, nur ineffizienter bei vergleichbarer Stärke. Beide sind anfällig für Quantencomputer und werden langfristig durch Post-Quanten-Verfahren ergänzt.
Was bedeutet ES256 in einem JWT?
ES256 bezeichnet ECDSA mit der Kurve P-256 und dem Hash SHA-256 im festen 64-Byte-Format (IEEE P1363). Es ist die ECDSA-Variante für JSON Web Tokens. In Node setzen Sie sie um, indem Sie sign('sha256', ..., { dsaEncoding: 'ieee-p1363' }) mit einem prime256v1-Schlüssel verwenden.
Kann ich denselben Schlüssel zum Verschlüsseln und Signieren nutzen?
Nein, und Sie sollten es auch nicht. ECDSA ist ein reines Signaturverfahren und kann nicht verschlüsseln. Für Verschlüsselung auf elliptischen Kurven nutzen Sie ECDH zum Schlüsselaustausch und anschließend ein symmetrisches Verfahren. Trennen Sie Schlüssel grundsätzlich nach Zweck.
Wie schütze ich den privaten Schlüssel auf dem Server?
Speichern Sie ihn nie im Klartext neben dem Code. Nutzen Sie einen passphrasen-verschlüsselten PEM, besser noch einen Secret-Manager (Vault, AWS KMS) oder ein Hardware-Sicherheitsmodul, das nur Signaturen über eine API ausgibt und das Schlüsselmaterial nie herausrückt.
Related Coverage
- Digitale Signaturen erklärt: So funktionieren sie
- SSH-Key erstellen: Ed25519 in 8 Schritten
- SHA-256 erklärt: So funktioniert es
- HTTPS und TLS: Wie das Schloss im Browser Sie schützt
- GPG-Verschlüsselung mit GnuPG in 12 Schritten
- Ledger vs. Trezor: Hardware-Wallets im Vergleich
- Hashing und Kryptographie verständlich erklärt
Letzte Aktualisierung: 12. Juni 2026. Alle Code-Beispiele wurden mit Node.js 24 LTS und OpenSSL 3.5 konzipiert. Die genannten Algorithmen und Standards entsprechen dem Stand von 2026.




