Elliptic Curve Cryptography (ECC) liefert bei gleichem Sicherheitsniveau deutlich kleinere Schlüssel als RSA: Ein 256-Bit-ECC-Schlüssel entspricht einem 3072-Bit-RSA-Schlüssel. Node.js ab Version 18 unterstützt ECC vollständig über das native crypto-Modul, ohne externe Abhängigkeiten. Dieses Tutorial zeigt Schritt für Schritt, wie du ECDH-Schlüsselaustausch, ECDSA-Signaturen und Ed25519 in 12 Schritten implementierst, mit ausführbarem Code für jede Technik.
Was ist Elliptic Curve Cryptography?
ECC basiert auf der algebraischen Struktur elliptischer Kurven über endlichen Körpern. Die Sicherheit beruht auf dem Elliptic Curve Discrete Logarithm Problem (ECDLP): Gegeben zwei Punkte P und Q auf einer Kurve mit Q = k·P, ist k praktisch nicht berechenbar, solange k groß genug ist. Klassische Computer benötigen für P-256 nach aktuellem Stand mehr Energie als im Sonnensystem vorhanden ist, um k zu ermitteln.
In der Praxis setzt die Industrie auf drei ECC-Anwendungsfälle:
- ECDH (Elliptic Curve Diffie-Hellman): Schlüsselaustausch, um einen gemeinsamen Geheimschlüssel über einen unsicheren Kanal zu etablieren, ohne ihn jemals direkt zu übertragen.
- ECDSA (Elliptic Curve Digital Signature Algorithm): Digitale Signaturen zum Nachweis der Authentizität und Integrität von Daten.
- Ed25519 / EdDSA: Schnellere und sicherere Signaturvariante auf Basis der Edwards-Kurve, bevorzugt für SSH, TLS 1.3 und moderne Protokolle.
Das NIST empfiehlt in FIPS 186-5 (2023) ausdrücklich P-256 und P-384 für neue Systeme. TLS 1.3 nutzt standardmäßig ECDHE mit P-256 oder X25519 für den Schlüsselaustausch. Die Node.js-Implementierung ruft dabei intern OpenSSL auf, das seit Version 3.0 alle relevanten Kurven unterstützt und nach FIPS 140-3 validiert ist.
Voraussetzungen
Stelle vor dem Start sicher, dass alle Werkzeuge in den richtigen Versionen vorhanden sind. ECC über das native crypto-Modul erfordert mindestens Node.js 18, da generateKeyPairSync mit EC-Parametern und die vollständige hkdfSync-API ab dieser Version stabil sind.
| Werkzeug | Mindestversion | Empfohlene Version | Zweck |
|---|---|---|---|
| Node.js | 18.0.0 | 22.x LTS | Laufzeit mit nativem ECC-Support |
| npm | 9.0.0 | 10.x | Paketverwaltung |
| OpenSSL | 3.0 | 3.3 | Hintergrund-Bibliothek (Teil von Node.js) |
| TypeScript (optional) | 5.0 | 5.4 | Typsicherheit |
| OS | Ubuntu 22.04 / macOS 12 / Windows 10 | Ubuntu 24.04 | Alle Betriebssysteme unterstützt |
Installierte Version prüfen und ECC-Unterstützung bestätigen:
node --version # Ausgabe: v22.x.x
npm --version # Ausgabe: 10.x.x
# ECC-Kurvenunterstützung prüfen
node -e "const c = require('crypto'); console.log('P-256:', c.getCurves().includes('prime256v1')); console.log('P-384:', c.getCurves().includes('secp384r1')); console.log('Ed25519:', c.generateKeyPairSync('ed25519') && true)"
# Erwartete Ausgabe:
# P-256: true
# P-384: true
# Ed25519: true
Keine externen Pakete notwendig. Alle Codebeispiele in diesem Tutorial laufen ausschließlich mit dem Node.js-Standard-Modul crypto und dem eingebauten fs-Modul.
Schritt 1: Projektstruktur anlegen
Erstelle ein sauberes Projektverzeichnis mit klarer Trennung zwischen Schlüsseln, Signaturen und Anwendungscode. Diese Struktur verhindert, dass private Schlüssel versehentlich in das Repository gelangen. Der erste Schritt ist eine verbindliche .gitignore-Datei, die alle Schlüsseldateien ausschließt.
mkdir ecc-nodejs-tutorial && cd ecc-nodejs-tutorial
npm init -y
# Verzeichnisstruktur
mkdir -p keys signatures messages
# .gitignore für Schlüssel ZUERST anlegen
cat << 'EOF' > .gitignore
keys/
*.pem
*.der
*.p8
*.p12
*.pfx
node_modules/
.env
EOF
echo "Projektstruktur bereit"
ls -la
Erstelle das Hauptmodul mit gemeinsamen Hilfsfunktionen:
// ecc.js - Basismodul für alle ECC-Operationen
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const KEYS_DIR = path.join(__dirname, 'keys');
const SIGNATURES_DIR = path.join(__dirname, 'signatures');
// Verzeichnisse erstellen falls nicht vorhanden
[KEYS_DIR, SIGNATURES_DIR].forEach(dir => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
});
// Erlaubte Kurven (Whitelist)
const ALLOWED_CURVES = new Set(['prime256v1', 'secp384r1', 'secp521r1']);
function validateCurve(namedCurve) {
if (!ALLOWED_CURVES.has(namedCurve)) {
throw new Error(`Kurve nicht erlaubt: ${namedCurve}. Verwende: ${[...ALLOWED_CURVES].join(', ')}`);
}
}
module.exports = { crypto, fs, path, KEYS_DIR, SIGNATURES_DIR, validateCurve };
Schritt 2: EC-Schlüsselpaar generieren
Node.js unterstützt alle wichtigen NIST-Kurven sowie die sichere Edwards-Kurve für Ed25519. Die Auswahl der Kurve bestimmt Sicherheitsniveau und Performance. Für den Produktionseinsatz empfiehlt das BSI österreichischen Behörden P-256 oder P-384 als bevorzugte asymmetrische Verfahren.
Unterstützte Kurven im Vergleich
| Kurvenname | Alias | Sicherheit | Schlüssellänge | Typischer Einsatz |
|---|---|---|---|---|
| prime256v1 | P-256, secp256r1 | 128 Bit | 256 Bit | TLS, JWT ES256, FIDO2/WebAuthn |
| secp384r1 | P-384 | 192 Bit | 384 Bit | NSA Suite B, Behörden |
| secp521r1 | P-521 | 256 Bit | 521 Bit | Höchste Sicherheitsanforderungen |
| secp256k1 | K-256 | 128 Bit | 256 Bit | Bitcoin, Ethereum |
| ed25519 | Edwards-25519 | 128 Bit | 256 Bit | SSH, GPG, Signal-Protokoll |
| ed448 | Edwards-448 | 224 Bit | 448 Bit | Langzeitsicherheit, GPG |
Die Funktion zur Schlüsselgenerierung legt Schlüsselpaare direkt als PEM-Dateien ab und setzt die korrekten Dateirechte:
// generate-keys.js
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
/**
* Generiert ein EC-Schlüsselpaar und speichert es als PEM-Dateien.
* @param {string} namedCurve - z.B. 'prime256v1', 'secp384r1', 'secp521r1'
* @param {string} label - Dateinamenpräfix
* @returns {{ privateKey: string, publicKey: string }}
*/
function generateECKeyPair(namedCurve = 'prime256v1', label = 'ec') {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
const privPath = path.join('keys', `${label}-private.pem`);
const pubPath = path.join('keys', `${label}-public.pem`);
// Privater Schlüssel: nur für Owner lesbar (Unix 600)
fs.writeFileSync(privPath, privateKey, { mode: 0o600 });
fs.writeFileSync(pubPath, publicKey);
const privKeyObj = crypto.createPrivateKey(privateKey);
const curve = privKeyObj.asymmetricKeyDetails?.namedCurve;
console.log(`[OK] Schlüsselpaar (${curve}) generiert:`);
console.log(` Privat: ${privPath}`);
console.log(` Öffentl: ${pubPath}`);
return { privateKey, publicKey };
}
// Drei Kurventypen demonstrieren
generateECKeyPair('prime256v1', 'alice-p256');
generateECKeyPair('secp384r1', 'alice-p384');
generateECKeyPair('prime256v1', 'bob-p256');
/* Erwartete Ausgabe:
[OK] Schlüsselpaar (prime256v1) generiert:
Privat: keys/alice-p256-private.pem
Öffentl: keys/alice-p256-public.pem
[OK] Schlüsselpaar (secp384r1) generiert:
Privat: keys/alice-p384-private.pem
Öffentl: keys/alice-p384-public.pem
[OK] Schlüsselpaar (prime256v1) generiert:
Privat: keys/bob-p256-private.pem
Öffentl: keys/bob-p256-public.pem
*/
Der Dateirechte-Modus 0o600 stellt auf Unix-Systemen sicher, dass der private Schlüssel nur vom erzeugenden Prozess gelesen werden kann. Unter Windows fehlt diese Kontrolle, weshalb dort DPAPI-Schutz oder ein Hardware-Sicherheitsmodul (HSM) empfohlen wird.
Schritt 3: Schlüssel in PEM und DER exportieren
PEM ist Base64-kodiertes DER mit ASCII-Kopfzeilen, geeignet für Textdateien und Konfigurationen. DER ist das rohe Binärformat, effizienter für Datenbankenspeicherung und eingebettete Systeme. Ein P-256-Schlüssel belegt im DER-Format 138 Bytes, ein äquivalenter RSA-3072-Schlüssel über 1.700 Bytes. Node.js unterstützt beide Formate nativ und ermöglicht die Konvertierung ohne externe Tools.
// key-export.js
'use strict';
const crypto = require('crypto');
const fs = require('fs');
function demonstrateKeyFormats(namedCurve = 'prime256v1') {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// PEM speichern
fs.writeFileSync('keys/key.pem', privateKey, { mode: 0o600 });
// DER exportieren
const privKeyObj = crypto.createPrivateKey(privateKey);
const derPriv = privKeyObj.export({ type: 'pkcs8', format: 'der' });
fs.writeFileSync('keys/key.der', derPriv);
// Größenvergleich
console.log(`Kurve: ${namedCurve}`);
console.log(`DER-Größe: ${derPriv.length} Bytes`);
console.log(`PEM-Größe: ${Buffer.byteLength(privateKey, 'utf8')} Bytes`);
// Schlüsseldetails
const details = privKeyObj.asymmetricKeyDetails;
console.log(`Kurvenname: ${details.namedCurve}`);
// PEM-Inhalt (öffentlicher Schlüssel)
console.log('\n--- Öffentlicher Schlüssel (PEM) ---');
console.log(publicKey);
return { privateKey, publicKey, derPriv };
}
demonstrateKeyFormats('prime256v1');
/* Erwartete Ausgabe:
Kurve: prime256v1
DER-Größe: 138 Bytes
PEM-Größe: 227 Bytes
Kurvenname: prime256v1
--- Öffentlicher Schlüssel (PEM) ---
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
*/
Schritt 4: Schlüssel aus Datei laden und validieren
Beim Laden von Schlüsseln aus dem Dateisystem ist robuste Fehlerbehandlung entscheidend. Fehlende oder korrumpierte Schlüssel dürfen die Anwendung nicht mit einer unbehandelten Exception beenden. Der geladene Schlüssel muss auf Typ und Kurve geprüft werden, bevor er in kryptografischen Operationen eingesetzt wird.
// key-loader.js
'use strict';
const crypto = require('crypto');
const fs = require('fs');
function loadPrivateKey(pemPath, allowedCurves = ['prime256v1', 'secp384r1', 'secp521r1']) {
if (!fs.existsSync(pemPath)) {
throw new Error(`Privater Schlüssel nicht gefunden: ${pemPath}`);
}
const pem = fs.readFileSync(pemPath, 'utf8');
const keyObj = crypto.createPrivateKey(pem);
if (keyObj.asymmetricKeyType !== 'ec') {
throw new Error(`Erwartet EC-Schlüssel, erhalten: ${keyObj.asymmetricKeyType}`);
}
const curve = keyObj.asymmetricKeyDetails?.namedCurve;
if (!allowedCurves.includes(curve)) {
throw new Error(`Kurve nicht erlaubt: ${curve}`);
}
console.log(`[OK] Privater Schlüssel geladen: ${curve}`);
return keyObj;
}
function loadPublicKey(pemPath) {
if (!fs.existsSync(pemPath)) {
throw new Error(`Öffentlicher Schlüssel nicht gefunden: ${pemPath}`);
}
const pem = fs.readFileSync(pemPath, 'utf8');
const keyObj = crypto.createPublicKey(pem);
console.log(`[OK] Öffentlicher Schlüssel: ${keyObj.asymmetricKeyType}, ${keyObj.asymmetricKeyDetails?.namedCurve}`);
return keyObj;
}
// Verwendung mit Fehlerbehandlung
try {
const priv = loadPrivateKey('keys/alice-p256-private.pem');
const pub = loadPublicKey('keys/alice-p256-public.pem');
console.log('Schlüssel bereit für kryptografische Operationen.');
} catch (err) {
console.error(`[FEHLER] ${err.message}`);
process.exit(1);
}
/* Erwartete Ausgabe:
[OK] Privater Schlüssel geladen: prime256v1
[OK] Öffentlicher Schlüssel: ec, prime256v1
Schlüssel bereit für kryptografische Operationen.
*/
Schritt 5: ECDH-Schlüsselaustausch implementieren
ECDH ermöglicht es zwei Parteien, einen gemeinsamen Geheimschlüssel zu berechnen, ohne ihn jemals über das Netzwerk zu senden. Jede Seite generiert ein Schlüsselpaar, tauscht den öffentlichen Schlüssel aus, und berechnet lokal dasselbe Shared Secret. Wenn für jede Sitzung neue Schlüsselpaare generiert werden (ephemere Schlüssel), entsteht Forward Secrecy: Vergangene Sitzungen bleiben sicher, selbst wenn ein Langzeitschlüssel kompromittiert wird.
Der Ablauf in vier Schritten:
- Alice generiert Schlüsselpaar (privA, pubA), Bob generiert (privB, pubB).
- Alice sendet pubA an Bob, Bob sendet pubB an Alice.
- Alice berechnet sharedSecret = ECDH(privA, pubB).
- Bob berechnet sharedSecret = ECDH(privB, pubA). Beide erhalten denselben Wert.
// ecdh-exchange.js
'use strict';
const crypto = require('crypto');
function demonstrateECDH(namedCurve = 'prime256v1') {
console.log(`\n=== ECDH Schlüsselaustausch (${namedCurve}) ===`);
// Schritt 1: Beide Parteien generieren ephemere Schlüsselpaare
const { privateKey: alicePriv, publicKey: alicePub } =
crypto.generateKeyPairSync('ec', { namedCurve });
const { privateKey: bobPriv, publicKey: bobPub } =
crypto.generateKeyPairSync('ec', { namedCurve });
// Schritt 2: Shared Secret berechnen
// Alice nutzt ihren privaten + Bobs öffentlichen Schlüssel
const aliceSecret = crypto.diffieHellman({
privateKey: alicePriv,
publicKey: bobPub
});
// Bob nutzt seinen privaten + Alices öffentlichen Schlüssel
const bobSecret = crypto.diffieHellman({
privateKey: bobPriv,
publicKey: alicePub
});
// Schritt 3: Beide erhalten dasselbe Ergebnis
const match = aliceSecret.equals(bobSecret);
console.log(`Secret (Alice): ${aliceSecret.toString('hex').substring(0, 32)}...`);
console.log(`Secret (Bob): ${bobSecret.toString('hex').substring(0, 32)}...`);
console.log(`Identisch: ${match}`);
console.log(`Länge: ${aliceSecret.length} Bytes`);
if (!match) throw new Error('ECDH-Fehler: Secrets stimmen nicht überein!');
return aliceSecret;
}
demonstrateECDH('prime256v1'); // 32 Bytes Secret
demonstrateECDH('secp384r1'); // 48 Bytes Secret
demonstrateECDH('secp521r1'); // 66 Bytes Secret
/* Erwartete Ausgabe:
=== ECDH Schlüsselaustausch (prime256v1) ===
Secret (Alice): 8f3a2b1c9d7e4f6a0b3c5d2e1f8a9b0c...
Secret (Bob): 8f3a2b1c9d7e4f6a0b3c5d2e1f8a9b0c...
Identisch: true
Länge: 32 Bytes
...
*/
Das Raw-ECDH-Secret darf niemals direkt als Verschlüsselungsschlüssel verwendet werden. Es hat keine gleichmäßige Verteilung und muss zuerst durch eine Key Derivation Function (KDF) verarbeitet werden.
Schritt 6: AES-Sitzungsschlüssel aus ECDH ableiten
HKDF (HMAC-based Key Derivation Function, RFC 5869) ist die korrekte Methode, um aus dem ECDH-Secret einen oder mehrere Anwendungsschlüssel abzuleiten. Node.js 15+ implementiert HKDF nativ via crypto.hkdfSync(). Der Salt-Wert (zufällig pro Sitzung) und der Info-Parameter (kontextgebunden) binden den abgeleiteten Schlüssel an eine spezifische Sitzung, sodass zwei Sitzungen mit demselben ECDH-Schlüsselpaar verschiedene AES-Schlüssel erhalten.
// key-derivation.js
'use strict';
const crypto = require('crypto');
/**
* Leitet aus einem ECDH-Secret einen AES-256-GCM-Schlüssel ab.
* @param {Buffer} sharedSecret - Raw ECDH-Ausgabe
* @param {string} context - Anwendungskontext (z.B. Sitzungs-ID)
* @returns {{ aesKey: Buffer, iv: Buffer, salt: Buffer }}
*/
function deriveSessionKey(sharedSecret, context = 'ecc-tutorial-v1') {
// Salt: zufällig pro Sitzung, muss dem Empfänger mitgeteilt werden
const salt = crypto.randomBytes(32);
// HKDF: Extract (aus Secret + Salt) + Expand (mit Info-Parameter)
const aesKeyRaw = crypto.hkdfSync(
'sha256', // Hash-Algorithmus
sharedSecret, // Input Key Material
salt, // Salt (zufällig)
Buffer.from(context, 'utf8'), // Info: bindet Schlüssel an Kontext
32 // 32 Bytes = AES-256
);
const aesKey = Buffer.from(aesKeyRaw);
const iv = crypto.randomBytes(12); // 96 Bit für AES-GCM
console.log(`AES-Schlüssel: ${aesKey.toString('hex').substring(0, 16)}...`);
console.log(`IV: ${iv.toString('hex')}`);
return { aesKey, iv, salt };
}
// Demonstration: ECDH + HKDF + AES-GCM
const { privateKey: alicePriv, publicKey: alicePub } =
crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const { privateKey: bobPriv, publicKey: bobPub } =
crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const sharedSecret = crypto.diffieHellman({ privateKey: alicePriv, publicKey: bobPub });
const { aesKey, iv } = deriveSessionKey(sharedSecret, 'chat-session-2026');
// Nachricht verschlüsseln
const plaintext = 'Vertrauliche Nachricht von Alice an Bob';
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
console.log(`Verschlüsselt: ${encrypted.toString('hex')}`);
console.log(`Auth Tag: ${authTag.toString('hex')}`);
// Entschlüsseln (Bob-Seite)
const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
console.log(`Entschlüsselt: ${decrypted.toString('utf8')}`);
/* Erwartete Ausgabe:
AES-Schlüssel: 3f8c2a1b9d7e4f6a...
IV: a1b2c3d4e5f67890a1b2c3d4
Verschlüsselt: 8f3a2b1c9d7e4f6a...
Auth Tag: 2b4f8c1a3d7e9f0c2b4f8c1a3d7e9f0c
Entschlüsselt: Vertrauliche Nachricht von Alice an Bob
*/
Schritt 7: Daten mit ECDSA signieren
ECDSA erzeugt eine digitale Signatur, die beweist, dass ein Datensatz vom Inhaber des privaten Schlüssels erstellt wurde. Die Signatur besteht aus zwei Werten (r, s), die Node.js standardmäßig als DER-Struktur kodiert. Das IEEE P1363-Format (feste Länge) wird in JWT-Implementierungen verwendet und ist über den dsaEncoding-Parameter wählbar.
// ecdsa-sign.js
'use strict';
const crypto = require('crypto');
const fs = require('fs');
function signDocument(document, privateKeyPem) {
const sign = crypto.createSign('sha256'); // P-256 + SHA-256 = ES256
sign.update(document);
sign.end();
// DER-Format (Standard, kompatibel mit OpenSSL)
const signature = sign.sign({ key: privateKeyPem, dsaEncoding: 'der' });
console.log(`Dokument: "${document.substring(0, 50)}"`);
console.log(`Signatur: ${signature.toString('hex').substring(0, 32)}...`);
console.log(`Länge: ${signature.length} Bytes`);
return signature;
}
// Schlüsselpaar
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'prime256v1',
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// Dokument signieren (immer den zu verifizierenden Inhalt signieren, nicht einen Hash davon)
const document = JSON.stringify({
action: 'Überweisung',
betrag: '1.500,00 EUR',
von: '[email protected]',
an: '[email protected]',
timestamp: '2026-06-20T10:00:00Z'
});
const signature = signDocument(document, privateKey);
fs.writeFileSync('signatures/dokument.sig', signature);
fs.writeFileSync('keys/signierer-public.pem', publicKey);
fs.writeFileSync('messages/dokument.json', document);
console.log('\nSignatur, Schlüssel und Dokument gespeichert.');
/* Erwartete Ausgabe:
Dokument: "{"action":"Überweisung","betrag":"1.500,00 EUR","vo"
Signatur: 3045022100f3a2b1c9d7e4f6a0b3c5d2e1f8...
Länge: 71 Bytes
Signatur, Schlüssel und Dokument gespeichert.
*/
Schritt 8: ECDSA-Signatur verifizieren
Die Verifikation einer ECDSA-Signatur beweist, dass die Daten weder verändert wurden noch von einer anderen Partei als dem Inhaber des privaten Schlüssels stammen. Nur der öffentliche Schlüssel ist für die Verifikation notwendig. Wichtig: Eine fehlgeschlagene Verifikation darf keine detaillierten Fehlermeldungen nach außen geben, da diese Informationen für Timing-Angriffe nutzbar sind.
// ecdsa-verify.js
'use strict';
const crypto = require('crypto');
const fs = require('fs');
function verifySignature(data, signature, publicKeyPem) {
const verify = crypto.createVerify('sha256');
verify.update(data);
verify.end();
try {
return verify.verify({ key: publicKeyPem, dsaEncoding: 'der' }, signature);
} catch {
// Keinen internen Fehler nach außen geben
return false;
}
}
// Daten, Signatur und Schlüssel laden
const publicKey = fs.readFileSync('keys/signierer-public.pem', 'utf8');
const signature = fs.readFileSync('signatures/dokument.sig');
const document = fs.readFileSync('messages/dokument.json', 'utf8');
// Test 1: Originaldokument verifizieren
const originalValid = verifySignature(document, signature, publicKey);
console.log(`Original gültig: ${originalValid}`); // true
// Test 2: Manipuliertes Dokument
const manipulated = JSON.stringify({
action: 'Überweisung',
betrag: '99.999,00 EUR', // Betrag manipuliert!
von: '[email protected]',
an: '[email protected]',
timestamp: '2026-06-20T10:00:00Z'
});
const manipulatedValid = verifySignature(manipulated, signature, publicKey);
console.log(`Manipuliert gültig: ${manipulatedValid}`); // false
// Test 3: Falsche Signatur
const wrongSig = crypto.randomBytes(72);
const wrongValid = verifySignature(document, wrongSig, publicKey);
console.log(`Falsche Sig gültig: ${wrongValid}`); // false
/* Erwartete Ausgabe:
Original gültig: true
Manipuliert gültig: false
Falsche Sig gültig: false
*/
Schritt 9: Ed25519 für schnelle, deterministische Signaturen
Ed25519 ist eine Implementierung des Edwards-Curve Digital Signature Algorithm (EdDSA) auf der Curve25519, entworfen von Daniel J. Bernstein. Sie gilt als eine der sichersten und schnellsten Signaturverfahren: Signaturen sind genau 64 Bytes kompakt, deterministisch (kein Zufallsgenerator für die Signaturerzeugung nötig), resistent gegen Timing-Angriffe und laut RFC 8032 für neue Protokolle bevorzugt. OpenSSH nutzt Ed25519 seit Version 6.5, TLS 1.3 unterstützt es via ed25519 Signature Scheme.
// ed25519-demo.js
'use strict';
const crypto = require('crypto');
function ed25519Demo() {
console.log('=== Ed25519 Signaturen ===\n');
// Schlüsselpaar (kein namedCurve-Parameter, Ed25519 ist kein NIST-Kurventyp)
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
console.log('Öffentlicher Schlüssel:');
console.log(publicKey);
// Nachricht signieren (null = kein separater Hash, Ed25519 hasht intern mit SHA-512)
const message = Buffer.from('Authentifizierungstoken: 2026-06-20T10:00:00Z');
const signature = crypto.sign(null, message, privateKey);
console.log(`Signatur (Base64): ${signature.toString('base64')}`);
console.log(`Länge: ${signature.length} Bytes`); // immer 64 Bytes
// Verifikation
const valid = crypto.verify(null, message, publicKey, signature);
console.log(`Gültig: ${valid}`);
// Performance: 1000 Signaturen
const count = 1000;
const msg = Buffer.from('Performance-Test');
const t0 = process.hrtime.bigint();
for (let i = 0; i < count; i++) crypto.sign(null, msg, privateKey);
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
console.log(`\n${count} Signaturen in ${ms.toFixed(1)}ms (${(ms/count).toFixed(3)}ms/Op)`);
return { privateKey, publicKey, signature };
}
ed25519Demo();
/* Erwartete Ausgabe:
=== Ed25519 Signaturen ===
Öffentlicher Schlüssel:
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...
-----END PUBLIC KEY-----
Signatur (Base64): 8f3a2b1c9d7e...
Länge: 64 Bytes
Gültig: true
1000 Signaturen in 45.2ms (0.045ms/Op)
*/
ECDSA vs. Ed25519: Entscheidungsmatrix
| Kriterium | ECDSA (P-256) | Ed25519 |
|---|---|---|
| Signaturlänge | 70-72 Bytes (DER, variabel) | 64 Bytes (fest) |
| Deterministisch | Nein (benötigt CSPRNG) | Ja (RFC 8032) |
| Timing-Resistenz | Implementierungsabhängig | By Design |
| Sign-Performance | ~0.08ms (P-256, i7-12700) | ~0.045ms |
| Verify-Performance | ~0.15ms | ~0.10ms |
| JWT-Algorithmus | ES256 (RFC 7518) | EdDSA (RFC 8037) |
| FIPS 140-2/3 | Ja | Ab FIPS 140-3 |
| Typischer Einsatz | TLS-Zertifikate, ECDSA-JWT | SSH, GPG, Signal-Protokoll |
Schritt 10: Komplettes Sicherheitsprotokoll aufbauen
Dieser Schritt kombiniert alle vorherigen Techniken zu einem vollständigen Protokoll für verschlüsselte, authentifizierte Nachrichten. Das Muster folgt dem Prinzip der signierten Verschlüsselung: Die Signatur wird über den Ciphertext berechnet (nicht über den Klartext), und die Signatur wird vor der Entschlüsselung geprüft. Dieses Prinzip heißt Encrypt-then-Sign und schützt vor Oracle-Angriffen.
// secure-channel.js
'use strict';
const crypto = require('crypto');
class SecureChannel {
constructor(namedCurve = 'prime256v1') {
this.namedCurve = namedCurve;
// Langzeit-Identitätsschlüssel
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve });
this.identityPriv = privateKey;
this.identityPub = publicKey;
}
getPublicKeyPem() {
return this.identityPub.export({ type: 'spki', format: 'pem' });
}
/**
* Nachricht verschlüsseln (ECDH + HKDF + AES-GCM) und signieren (ECDSA).
*/
encryptAndSign(plaintext, recipientPublicKey) {
// Ephemerer Schlüssel für Forward Secrecy
const { privateKey: ephPriv, publicKey: ephPub } =
crypto.generateKeyPairSync('ec', { namedCurve: this.namedCurve });
// Shared Secret mit Empfänger
const sharedSecret = crypto.diffieHellman({
privateKey: ephPriv,
publicKey: recipientPublicKey
});
// Sitzungsschlüssel ableiten
const salt = crypto.randomBytes(32);
const aesKey = Buffer.from(
crypto.hkdfSync('sha256', sharedSecret, salt, Buffer.from('secure-channel-v1'), 32)
);
const iv = crypto.randomBytes(12);
// Verschlüsseln
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
// Signatur über Ciphertext (Encrypt-then-Sign)
const toSign = Buffer.concat([encrypted, authTag, iv]);
const sign = crypto.createSign('sha256');
sign.update(toSign);
const signature = sign.sign(this.identityPriv);
return {
version: '1.0',
ephemeralPub: ephPub.export({ type: 'spki', format: 'pem' }),
salt: salt.toString('base64'),
iv: iv.toString('base64'),
ciphertext: encrypted.toString('base64'),
authTag: authTag.toString('base64'),
senderPub: this.getPublicKeyPem(),
signature: signature.toString('base64')
};
}
/**
* Paket empfangen: Signatur prüfen, dann entschlüsseln.
*/
decryptAndVerify(packet) {
const { ephemeralPub, salt, iv, ciphertext, authTag, senderPub, signature } = packet;
// SCHRITT 1: Signatur prüfen (vor der Entschlüsselung!)
const encBuf = Buffer.from(ciphertext, 'base64');
const ivBuf = Buffer.from(iv, 'base64');
const tagBuf = Buffer.from(authTag, 'base64');
const toVerify = Buffer.concat([encBuf, tagBuf, ivBuf]);
const verify = crypto.createVerify('sha256');
verify.update(toVerify);
const sigValid = verify.verify(senderPub, Buffer.from(signature, 'base64'));
if (!sigValid) {
throw new Error('Signatur ungültig: Nachricht manipuliert oder falscher Absender!');
}
// SCHRITT 2: ECDH mit ephemerem Schlüssel
const ephKey = crypto.createPublicKey(ephemeralPub);
const secret = crypto.diffieHellman({ privateKey: this.identityPriv, publicKey: ephKey });
const saltBuf = Buffer.from(salt, 'base64');
const aesKey = Buffer.from(
crypto.hkdfSync('sha256', secret, saltBuf, Buffer.from('secure-channel-v1'), 32)
);
// SCHRITT 3: Entschlüsseln
const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, ivBuf);
decipher.setAuthTag(tagBuf);
return Buffer.concat([decipher.update(encBuf), decipher.final()]).toString('utf8');
}
}
// Demonstration
const alice = new SecureChannel('prime256v1');
const bob = new SecureChannel('prime256v1');
const message = 'Vertraulich: Projekt X startet am 1. Juli 2026. Bitte bestätigen.';
const packet = alice.encryptAndSign(message, bob.identityPub);
const decrypted = bob.decryptAndVerify(packet);
console.log('Entschlüsselt:', decrypted);
console.log('Korrekt:', message === decrypted);
/* Erwartete Ausgabe:
Entschlüsselt: Vertraulich: Projekt X startet am 1. Juli 2026. Bitte bestätigen.
Korrekt: true
*/
Schritt 11: X25519 als Alternative zu ECDH
X25519 ist eine spezifische ECDH-Variante auf der Montgomery-Kurve Curve25519. Im Gegensatz zu ECDH auf NIST-Kurven ist X25519 by Design resistent gegen bestimmte Timing-Angriffe und Implementierungsfehler. TLS 1.3 bevorzugt X25519 vor P-256-ECDH und nutzt es als ersten angebotenen Schlüsselaustausch-Algorithmus. In Node.js ist X25519 als eigener Schlüsseltyp x25519 verfügbar.
// x25519-demo.js
'use strict';
const crypto = require('crypto');
function x25519KeyExchange() {
console.log('=== X25519 Schlüsselaustausch ===\n');
// X25519-Schlüsselpaare (kein namedCurve-Parameter)
const { privateKey: alicePriv, publicKey: alicePub } =
crypto.generateKeyPairSync('x25519');
const { privateKey: bobPriv, publicKey: bobPub } =
crypto.generateKeyPairSync('x25519');
// Öffentliche Schlüssel in PEM ausgeben
const alicePubPem = alicePub.export({ type: 'spki', format: 'pem' });
console.log(`Alice öffentlicher Schlüssel:\n${alicePubPem}`);
// Shared Secret berechnen (identisch für beide Parteien)
const aliceSecret = crypto.diffieHellman({ privateKey: alicePriv, publicKey: bobPub });
const bobSecret = crypto.diffieHellman({ privateKey: bobPriv, publicKey: alicePub });
console.log(`Secret übereinstimmend: ${aliceSecret.equals(bobSecret)}`);
console.log(`Secret-Länge: ${aliceSecret.length} Bytes`);
// Vergleich mit ECDH P-256
const { privateKey: p256Priv } = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const { publicKey: p256Pub } = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const count = 5000;
let t0 = process.hrtime.bigint();
for (let i = 0; i < count; i++) {
const { privateKey: a, publicKey: b } = crypto.generateKeyPairSync('x25519');
const { publicKey: c } = crypto.generateKeyPairSync('x25519');
crypto.diffieHellman({ privateKey: a, publicKey: c });
}
const x25519ms = Number(process.hrtime.bigint() - t0) / 1e6;
t0 = process.hrtime.bigint();
for (let i = 0; i < count; i++) {
const { privateKey: a } = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const { publicKey: b } = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
crypto.diffieHellman({ privateKey: a, publicKey: b });
}
const p256ms = Number(process.hrtime.bigint() - t0) / 1e6;
console.log(`\nX25519 (${count} Ops): ${(x25519ms/count).toFixed(3)}ms/Op`);
console.log(`P-256 (${count} Ops): ${(p256ms/count).toFixed(3)}ms/Op`);
}
x25519KeyExchange();
/* Erwartete Ausgabe:
=== X25519 Schlüsselaustausch ===
Alice öffentlicher Schlüssel:
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VuAyEA...
-----END PUBLIC KEY-----
Secret übereinstimmend: true
Secret-Länge: 32 Bytes
X25519 (5000 Ops): 0.038ms/Op
P-256 (5000 Ops): 0.052ms/Op
*/
Schritt 12: Produktionshärtung und Best Practices
Der Übergang von einer funktionierenden ECC-Implementierung zu einer produktionssicheren erfordert mehrere zusätzliche Maßnahmen. Die häufigsten Sicherheitslücken entstehen nicht durch mathematische Schwächen der Kurven, sondern durch falsche Implementierung: wiederverwendete Nonces, unsichere Schlüsselspeicherung und fehlende Signaturverifikation vor der Entschlüsselung.
// production-hardening.js
'use strict';
const crypto = require('crypto');
const fs = require('fs');
// 1. Schlüsselpaar mit Passphrase-Schutz (für Langzeitschlüssel auf Disk)
function generateProtectedKeyPair(namedCurve, passphrase) {
return crypto.generateKeyPairSync('ec', {
namedCurve,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase // AES-256-CBC-Schutz des privaten Schlüssels
}
});
}
// 2. Privaten Schlüssel aus Umgebungsvariable laden (12-Factor-App)
function loadPrivateKeyFromEnv(envVarName, passphrase = null) {
const pem = process.env[envVarName];
if (!pem) throw new Error(`Umgebungsvariable ${envVarName} nicht gesetzt`);
const options = passphrase ? { key: pem, passphrase } : pem;
return crypto.createPrivateKey(options);
}
// 3. Zeitkonstanter Vergleich für Schlüssel-IDs und Tokens
function safeEquals(a, b) {
const bufA = Buffer.from(String(a));
const bufB = Buffer.from(String(b));
if (bufA.length !== bufB.length) {
// Dummy-Vergleich verhindert Längen-Leakage via Timing
crypto.timingSafeEqual(bufA, bufA);
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
}
// 4. Schlüsselrotation: neues Paar generieren, altes deaktivieren
function rotateKeyPair(namedCurve, label, passphrase) {
const oldPath = `keys/${label}-private.pem`;
if (fs.existsSync(oldPath)) {
fs.renameSync(oldPath, `keys/${label}-private.pem.retired`);
console.log(`Alten Schlüssel archiviert: ${label}-private.pem.retired`);
}
const { privateKey, publicKey } = generateProtectedKeyPair(namedCurve, passphrase);
fs.writeFileSync(`keys/${label}-private.pem`, privateKey, { mode: 0o600 });
fs.writeFileSync(`keys/${label}-public.pem`, publicKey);
console.log(`Neues Schlüsselpaar (${namedCurve}) für "${label}" bereit.`);
return { privateKey, publicKey };
}
// Demonstration
const passphrase = process.env.KEY_PASSPHRASE || 'SicheresTestPasswort2026!';
const { privateKey, publicKey } = generateProtectedKeyPair('prime256v1', passphrase);
console.log('Geschützter privater Schlüssel (Auszug):');
console.log(privateKey.substring(0, 80));
// Laden und Entschlüsseln
const loaded = crypto.createPrivateKey({ key: privateKey, passphrase });
console.log(`\nEntschlüsselt: ${loaded.asymmetricKeyType} / ${loaded.asymmetricKeyDetails?.namedCurve}`);
/* Erwartete Ausgabe:
Geschützter privater Schlüssel (Auszug):
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAgBA...
Entschlüsselt: ec / prime256v1
*/
Häufige Fehler und Lösungen
ECC-Implementierungen scheitern in der Praxis selten an der Mathematik, sondern an handwerklichen Fehlern. Die folgenden sechs Fehler treten am häufigsten auf.
Fehler 1: Raw ECDH-Secret direkt als Schlüssel verwenden
Problem: Das von crypto.diffieHellman() zurückgegebene Buffer hat keine gleichmäßige Verteilung. Es eignet sich nicht direkt als AES-Schlüssel und kann Abhängigkeiten zwischen verschiedenen Sitzungen erzeugen.
// FALSCH
const sharedSecret = crypto.diffieHellman({ privateKey: alicePriv, publicKey: bobPub });
const wrongKey = sharedSecret.slice(0, 32); // Niemals!
// RICHTIG: HKDF anwenden
const correctKey = Buffer.from(
crypto.hkdfSync('sha256', sharedSecret, crypto.randomBytes(32), Buffer.from('app-v1'), 32)
);
Fehler 2: Kurven mischen
Problem: ECDH funktioniert nur, wenn beide Parteien dieselbe Kurve verwenden. P-256 und P-384 sind inkompatibel. Node.js gibt den Fehler error:0A00018E:SSL routines::point is not on curve zurück.
// Absicherung: Kurven vor ECDH prüfen
function safeECDH(privKey, pubKey) {
const privCurve = privKey.asymmetricKeyDetails?.namedCurve;
const pubCurve = pubKey.asymmetricKeyDetails?.namedCurve;
if (privCurve !== pubCurve) {
throw new Error(`Kurvenmismatch: privat=${privCurve}, öffentlich=${pubCurve}`);
}
return crypto.diffieHellman({ privateKey: privKey, publicKey: pubKey });
}
Fehler 3: ECDSA ohne passenden Hash
Problem: Für P-256 ist SHA-256 der korrekte Hash, für P-384 SHA-384 und für P-521 SHA-512. Ein schwächerer Hash reduziert die Gesamtsicherheit. crypto.sign(null, data, ecKey) wirft einen Fehler: Ed25519 erwartet null, ECDSA erwartet einen Hash-Algorithmus.
// Korrekte Hash-Kurven-Paarungen
const CURVE_HASH_MAP = {
'prime256v1': 'sha256', // ES256
'secp384r1': 'sha384', // ES384
'secp521r1': 'sha512' // ES512
};
function signECDSA(data, privateKey) {
const curve = privateKey.asymmetricKeyDetails?.namedCurve;
const hashAlgo = CURVE_HASH_MAP[curve];
if (!hashAlgo) throw new Error(`Keine Hash-Zuordnung für Kurve: ${curve}`);
const sign = crypto.createSign(hashAlgo);
sign.update(data);
return sign.sign(privateKey);
}
Fehler 4: Signaturen nach der Entschlüsselung prüfen
Problem: Wenn die Signatur erst nach der Entschlüsselung geprüft wird, kann ein Angreifer manipulierte Ciphertexte einschleusen und so Seitenkanal-Informationen aus dem Entschlüsselungsvorgang extrahieren, selbst bei AES-GCM.
// FALSCH: entschlüsseln, dann prüfen
const decrypted = decrypt(ciphertext); // ermöglicht Timing-Angriff
if (!verify(decrypted, signature)) throw new Error('...');
// RICHTIG: prüfen, dann entschlüsseln
if (!verify(ciphertext, signature, senderPub)) throw new Error('Signatur ungültig');
const decrypted = decrypt(ciphertext); // sicher
Fehler 5: Private Schlüssel in Versionskontrolle
Problem: Laut einer GitHub-Studie wurden 2024 über 300.000 private Schlüssel in öffentlichen Repositories gefunden. Ein in Git eingecheckter Schlüssel ist dauerhaft exponiert, auch nach dem Löschen.
# Git-Hook zur Verhinderung: .git/hooks/pre-commit (chmod +x setzen)
#!/bin/sh
if git diff --cached --name-only | grep -qE '\.(pem|key|p12|pfx|der)$'; then
echo "ABBRUCH: Schlüsseldatei in Staging gefunden!"
echo "Nutze: git rm --cached "
exit 1
fi
Fehler 6: Fehlerdetails an den Client senden
Problem: Detaillierte Krypto-Fehlermeldungen (Bibliotheksname, Reason-Code) helfen Angreifern bei der Analyse der Implementierung. Der Fehler sollte intern geloggt und nach außen generisch beantwortet werden.
// FALSCH: Interner Fehler nach außen
app.post('/verify', (req, res) => {
try { /* ... */ }
catch (err) {
res.status(400).json({ error: err.message }); // Gibt OpenSSL-Details preis!
}
});
// RICHTIG: Generische Antwort, internes Logging
app.post('/verify', (req, res) => {
try { /* ... */ }
catch (err) {
logger.error({ code: err.code, library: err.library }, 'Krypto-Fehler');
res.status(400).json({ error: 'Verifikation fehlgeschlagen' });
}
});
Fehlerbehebung: 8 häufige Fehlermeldungen
| Fehlermeldung | Ursache | Lösung |
|---|---|---|
ERR_CRYPTO_UNKNOWN_CIPHER | Algorithmusname falsch | crypto.getCiphers() zur Überprüfung nutzen |
error:0A00018E: point is not on curve | Kurven-Mismatch bei ECDH | Kurven beider Parteien vor ECDH validieren |
ERR_OSSL_PEM_NO_START_LINE | PEM-Datei korrumpiert oder Binary als Text gelesen | Datei explizit als UTF-8 laden |
digest too big for key size | Falscher Hash für Kurve (z.B. SHA-512 mit P-256) | P-256=SHA-256, P-384=SHA-384, P-521=SHA-512 |
Unsupported key type: OKP | Ed25519 in Node.js unter Version 15 | Node.js 18 LTS oder neuer verwenden |
ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE | Öffentlichen Schlüssel für sign() verwendet | Privaten Schlüssel für sign(), öffentlichen für verify() |
Decryption failed (AES-GCM) | AuthTag falsch, Daten manipuliert oder IV wiederverwendet | AuthTag korrekt übertragen, IV immer zufällig |
ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY | Empfangener Public Key nicht auf der Kurve | Schlüssel vor ECDH mit createPublicKey() validieren |
// Strukturiertes Fehler-Logging für Krypto-Operationen
function handleCryptoError(err, context) {
// Intern: vollständige Details loggen
console.error({
context,
message: err.message,
code: err.code,
library: err.library || null,
reason: err.reason || null
});
// Extern: generische Fehlermeldung
return new Error('Kryptografische Operation fehlgeschlagen');
}
try {
// Ungültige Operation auslösen
crypto.createPublicKey('kein-gültiges-PEM');
} catch (err) {
const safeErr = handleCryptoError(err, 'Schlüssel laden');
console.log('Antwort an Client:', safeErr.message);
}
/* Erwartete Ausgabe:
{ context: 'Schlüssel laden', message: 'error:09091064:...', code: undefined, ... }
Antwort an Client: Kryptografische Operation fehlgeschlagen
*/
ECC vs. RSA: Direktvergleich
Der entscheidende Vorteil von ECC liegt im Verhältnis von Schlüsselgröße zu Sicherheitsniveau. Die folgende Tabelle zeigt äquivalente Sicherheitsstufen nach NIST SP 800-57:
| Sicherheit (Bit) | RSA-Schlüssel | ECC-Schlüssel | Größenvorteil | TLS-Byte-Einsparung |
|---|---|---|---|---|
| 80 Bit (veraltet) | 1024 Bit | 160 Bit | 6,4x kleiner | (unsicher) |
| 112 Bit | 2048 Bit | 224 Bit | 9,1x kleiner | ~180 Bytes |
| 128 Bit (Standard) | 3072 Bit | 256 Bit | 12x kleiner | ~250 Bytes |
| 192 Bit | 7680 Bit | 384 Bit | 20x kleiner | ~700 Bytes |
| 256 Bit | 15360 Bit | 521 Bit | 29x kleiner | ~1.500 Bytes |
Laut RFC 6090 (ECC Fundamentals) ist ein 256-Bit-ECC-Schlüssel bis mindestens 2040 als ausreichend sicher eingestuft. Für neue Systeme empfiehlt das BSI (Stand 2025) P-256 oder P-384 als bevorzugte asymmetrische Verfahren, alternativ X25519 für den Schlüsselaustausch. Die OpenSSL-Dokumentation listet alle verfügbaren Kurven, die Node.js über das crypto-Modul zugänglich macht.
Kurvensicherheit nach SafeCurves-Kriterien
Das Projekt SafeCurves von Daniel Bernstein und Tanja Lange bewertet Kurven nach 11 Sicherheitskriterien. NIST-Kurven erfüllen nicht alle Kriterien, sind aber FIPS-zugelassen und in der Praxis ausreichend sicher, wenn korrekt implementiert.
| Kurve | SafeCurves-Score | FIPS-zugelassen | Patent-frei | Empfehlung |
|---|---|---|---|---|
| Curve25519 / X25519 | 11/11 | Ab FIPS 186-5 (2023) | Ja | Beste Wahl für Schlüsselaustausch |
| Ed25519 | 11/11 | Ab FIPS 186-5 | Ja | Beste Wahl für Signaturen |
| P-256 (prime256v1) | 7/11 | Ja (FIPS 186-5) | Ja | TLS, FIDO2, JWT, FIPS-Pflicht |
| P-384 | 7/11 | Ja | Ja | NSA Suite B, österr./dt. Behörden |
| P-521 | 7/11 | Ja | Ja | Höchste Sicherheitsanforderungen |
| secp256k1 | 9/11 | Nein | Ja | Nur für Blockchain-Anwendungen |
Weiterführende Artikel
Elliptic Curve Cryptography ist ein Baustein in einem größeren Sicherheitssystem. Diese Artikel vertiefen die verwandten Themen:
- Diffie-Hellman Key Exchange in Node.js: 12 Schritte, 45 Min [2026] - Das klassische DH-Protokoll als Grundlage zum Vergleich mit ECDH
- Digitale Signatur in Node.js: 11 Schritte, 40 Min [2026] - RSA-Signaturen im Vergleich zu ECDSA und Ed25519
- BLAKE3 Hashing in Node.js: 10 Schritte, 20 Min [2026] - Schnellere Hash-Alternativen zu SHA-256 für ECDSA-Anwendungen
- SHA-256 vs SHA-3: 3,7x Geschwindigkeit, ein Sieger [2026] - Welcher Hash-Algorithmus passt zu welcher ECC-Kurve
- WebAuthn in Node.js: Passwortlos in 12 Schritten [2026] - FIDO2/WebAuthn nutzt intern P-256 und Ed25519 für Attestierung
- HTTPS und TLS: Wie das Schloss im Browser Sie schützt - TLS 1.3 verwendet ECDHE und X25519 für Forward Secrecy
- Kryptographie - Alle Kryptographie-Tutorials im Überblick
Häufig gestellte Fragen
Welche ECC-Kurve soll ich für neue Node.js-Projekte wählen?
Für die meisten Anwendungen ist prime256v1 (P-256) die richtige Wahl: FIPS-zugelassen, von allen Bibliotheken unterstützt, 128 Bit sicher bis 2040 und im TLS-Handshake optimal. Für reine Signaturanwendungen ohne FIPS-Pflicht ist Ed25519 die bessere Option, da schneller, deterministisch und durch SafeCurves 11/11 bewertet. Für Behördenanwendungen in Österreich und Deutschland prüfe P-384 oder Brainpool P-256 (BSI-Empfehlung).
Ist ECC quantenresistent?
Nein. Shors Algorithmus auf einem Quantencomputer mit ausreichend fehlerkorrigierten Qubits würde ECDLP genauso brechen wie RSA. NIST hat 2024 mit FIPS 203 (ML-KEM/Kyber), FIPS 204 (ML-DSA/Dilithium) und FIPS 205 die ersten Post-Quantum-Standards veröffentlicht. Für Langzeitsicherheit über 2035 hinaus empfiehlt sich eine hybride Strategie: ECC kombiniert mit ML-KEM für den Schlüsselaustausch.
Kann ich denselben ECC-Schlüssel für ECDH und ECDSA verwenden?
Technisch möglich, sicherheitstechnisch nicht empfohlen. ECDH-Schlüssel für den Schlüsselaustausch sollten von ECDSA-Schlüsseln für Signaturen getrennt sein. Die gemeinsame Nutzung desselben Schlüsselmaterials in verschiedenen kryptografischen Operationen kann Querabhängigkeiten erzeugen, die Angreifern helfen, Informationen über den privaten Schlüssel zu sammeln.
Warum gibt ECDH bei jedem Aufruf ein anderes Shared Secret zurück?
Das passiert, wenn ephemere Schlüssel (neue Schlüssel pro Sitzung) verwendet werden, wie es für Forward Secrecy notwendig ist. Bei statischen Schlüsselpaaren ist das Secret reproduzierbar. In modernen Protokollen wie TLS 1.3 und Signal werden absichtlich neue Schlüsselpaare pro Sitzung generiert, damit vergangene Sitzungen selbst bei einem kompromittierten Langzeitschlüssel nicht entschlüsselt werden können.
Wie unterscheidet sich X25519 von ECDH?
X25519 ist eine spezifische ECDH-Implementierung auf der Montgomery-Kurve Curve25519. In Node.js als x25519-Schlüsseltyp verfügbar (crypto.generateKeyPairSync('x25519')). Im Gegensatz zu ECDH auf NIST-Kurven ist X25519 by Design resistent gegen Timing-Angriffe und Implementierungsfehler. TLS 1.3 bevorzugt X25519 vor P-256-ECDH: Es bietet dieselbe Sicherheit bei messbar niedrigerer Latenz.
Wie speichere ich ECC-Schlüssel sicher in einer Datenbank?
Öffentliche Schlüssel können unverschlüsselt als PEM-String gespeichert werden. Private Schlüssel müssen vor der Speicherung mit einem Hauptschlüssel (Master Key) verschlüsselt werden, der außerhalb der Datenbank liegt. Empfohlene Lösung: AWS KMS, Azure Key Vault, HashiCorp Vault oder ein HSM als Key Encryption Key. Als Minimalansatz ohne HSM: Privaten Schlüssel mit AES-256-GCM verschlüsseln (crypto.generateKeyPairSync mit cipher: 'aes-256-cbc'), den KEK aus einer Umgebungsvariable laden, nur das verschlüsselte Blob in der Datenbank speichern.
Welcher Hash-Algorithmus gehört zu welcher ECC-Kurve?
Die Kombination ist durch Standards vorgegeben: P-256 mit SHA-256 ergibt ES256 (JWT), P-384 mit SHA-384 ergibt ES384, P-521 mit SHA-512 ergibt ES512. Schwächere Hashes als die Kurve (z.B. SHA-256 mit P-384) reduzieren die Gesamtsicherheit auf das Niveau des Hashes. Ed25519 und Ed448 definieren ihren Hash intern (SHA-512 bzw. SHAKE256), weshalb der Hash-Parameter in Node.js auf null gesetzt wird.
Kann ich ECC mit der WebCrypto API (subtle) in Node.js verwenden?
Ja. Node.js 18+ stellt die WebCrypto API als globales Objekt (globalThis.crypto.subtle) bereit, browserkompatibel. Damit lässt sich Code zwischen Server und Browser teilen. Die Syntax unterscheidet sich vom crypto-Modul: await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey']). Die subtle-API ist vollständig asynchron und damit besser für hochvolumige Serveranwendungen geeignet als synchrone Operationen über generateKeyPairSync.




