Zwei Server müssen einen gemeinsamen Verschlüsselungsschlüssel aushandeln, ohne diesen jemals über das Netzwerk zu senden. Genau das leistet ECDH (Elliptic Curve Diffie-Hellman) in Millisekunden. Das Protokoll bildet die Grundlage von TLS 1.3, dem Signal-Protokoll, WireGuard und nahezu jeder modernen sicheren Verbindung. Mit dem Node.js-Modul node:crypto sind alle nötigen Werkzeuge bereits eingebaut. Dieses Tutorial zeigt die vollständige Implementierung in 12 Schritten: von der mathematischen Grundlage bis zur praxistauglichen Ende-zu-Ende-Verschlüsselung mit Perfect Forward Secrecy. Eine externe Bibliothek wird nicht benötigt.
Was ist ECDH? Elliptic Curve Cryptography kurz erklärt
ECDH (Elliptic Curve Diffie-Hellman) ist ein Schlüsselaustauschprotokoll auf Basis der Elliptic Curve Cryptography (ECC). Es ermöglicht zwei Parteien, über einen unsicheren Kanal einen gemeinsamen geheimen Wert (den “Shared Secret”) zu berechnen, ohne diesen jemals zu übertragen. Angreifer, die den Kanal belauschen, sehen nur öffentliche Werte und können daraus den geheimen Schlüssel nicht ableiten.
Der entscheidende Vorteil gegenüber klassischen Verfahren liegt in der Schlüsselgröße. Eine 256-Bit-ECC-Schlüssel bietet laut NIST SP 800-57 die gleiche Sicherheit wie ein 3.072-Bit-RSA-Schlüssel. Bei 128-Bit-Sicherheitsniveau benötigt ECC nur 256 Bit, während RSA 3.072 Bit braucht. Das BSI schreibt in seiner Technischen Richtlinie TR-02102 (Version 2026-01) für RSA eine Mindestlänge von 3.000 Bit vor, während P-256 die gleiche Sicherheit mit einem zwölffach kleineren Schlüssel erreicht.
Aus einer 2024 vergleichenden Studie der St. Mary’s University Texas geht hervor: Bei 256-Bit-ECC gegenüber 3.072-Bit-RSA ist die Elliptic-Curve-Exponentiation 20 bis 60 Mal schneller als eine RSA-Private-Key-Operation, je nach Optimierung. Die Schlüsselgenerierung ist so effizient, dass man in der Zeit eines RSA-Schlüssels etwa 8.000 ECC-Schlüsselpaare erzeugen kann (Verhältnis 1:8.400).
ECDH verwendet das diskrete Logarithmusproblem auf elliptischen Kurven (ECDLP) als kryptographische Grundlage. Das Problem ist mit klassischen Computern nicht effizient lösbar. Mit einem hinreichend großen Quantencomputer wäre es durch Shors Algorithmus lösbar, weshalb parallel zur ECDH-Nutzung bereits die Migration zu post-quantensicheren Algorithmen wie ML-KEM (FIPS 203, veröffentlicht August 2024) eingeleitet wird.
In der Praxis findet ECDH Einsatz in TLS 1.3 (RFC 8446) als primäres Key-Exchange-Verfahren, im Signal-Protokoll für Ende-zu-Ende-Verschlüsselung, in WireGuard VPN (X25519), in SSH für ephemere Sitzungsschlüssel sowie in der Blockchain-Technologie (Bitcoin und Ethereum verwenden die eng verwandte Kurve secp256k1).
Wie ECDH mathematisch funktioniert
Eine elliptische Kurve ist definiert durch die Weierstraß-Gleichung:
y² = x³ + ax + b (mod p)
Dabei sind a und b Kurvenparameter und p eine große Primzahl. Alle Berechnungen finden im endlichen Körper GF(p) statt. Die Kurve P-256 (auch prime256v1 oder secp256r1) verwendet ein 256-Bit-Primfeld und ist seit Jahren der Standard für Produktivsysteme.
Auf dieser Kurve ist eine Gruppenoperation definiert: die Punktaddition. Zwei Punkte auf der Kurve ergeben durch diese Operation einen dritten Punkt. Durch wiederholte Addition eines Basispunkts G (dem Generator) mit sich selbst entsteht die skalare Multiplikation:
k · G = G + G + G + ... + G (k-mal addiert)
Das ECDLP besagt: Gegeben k·G und G, ist es praktisch unmöglich, k zu berechnen. Dieser Einwegcharakter ist die kryptographische Grundlage.
ECDH funktioniert in sechs Schritten:
- Alice wählt einen zufälligen privaten Schlüssel
aund berechnet ihren öffentlichen SchlüsselA = a·G - Bob wählt einen zufälligen privaten Schlüssel
bund berechnet seinen öffentlichen SchlüsselB = b·G - Alice und Bob tauschen ihre öffentlichen Schlüssel aus (A und B können abgehört werden, ohne die Sicherheit zu gefährden)
- Alice berechnet den Shared Secret:
S = a·B = a·(b·G) = ab·G - Bob berechnet:
S = b·A = b·(a·G) = ab·G - Beide erhalten denselben Kurvenpunkt
S. Die x-Koordinate dieses Punktes dient als Shared Secret.
Ein Angreifer sieht nur A, B und G. Um a oder b aus diesen Werten zu berechnen, müsste er das ECDLP lösen, was bei P-256 etwa 2¹²⁸ Operationen erfordern würde. Das entspricht mehr Berechnungsschritten als Atome im sichtbaren Universum vorhanden sind.
Voraussetzungen für das ECDH-Tutorial
Für dieses Tutorial werden folgende Versionen benötigt:
| Komponente | Mindestversion | Empfohlene Version | Hinweis |
|---|---|---|---|
| Node.js | 18.x LTS | 22.x LTS | Integriertes node:crypto-Modul erforderlich |
| npm | 9.x | 10.x | Für Paketverwaltung im Projekt |
| OpenSSL | 1.1.1 | 3.x | Von Node.js mitgeliefert, keine separate Installation |
| Betriebssystem | Linux/macOS/Windows | Ubuntu 22.04+ | Alle Beispiele plattformunabhängig |
Vorausgesetztes Wissen:
- Grundlagen von Node.js und JavaScript (ES6+)
- Grundverständnis symmetrischer und asymmetrischer Verschlüsselung
- Umgang mit Buffern und binären Daten in Node.js
- Grundkenntnisse der Kommandozeile
Keine externen npm-Pakete notwendig. Alle Funktionen in diesem Tutorial nutzen das eingebaute node:crypto-Modul. Für Produktionsumgebungen empfiehlt sich der node:-Präfix, der explizit auf Node-Builtins verweist und mögliche Konflikte mit gleichnamigen npm-Paketen verhindert.
Node.js-Version und verfügbare ECC-Kurven prüfen:
node --version
# Erwartete Ausgabe: v22.x.x oder v20.x.x
node -e "const c=require('node:crypto'); console.log(c.getCurves().filter(k=>['prime256v1','secp384r1','x25519','x448'].includes(k)).join(', '))"
# Erwartete Ausgabe: prime256v1, secp384r1, x25519, x448
Schritt 1 und 2: Projekt einrichten und ECDH-Objekt erstellen
Im ersten Schritt wird die Projektstruktur erstellt. Da keine externen Abhängigkeiten benötigt werden, reicht eine einfache JavaScript-Datei.
Schritt 1: Projektordner anlegen
mkdir ecdh-tutorial && cd ecdh-tutorial
touch ecdh-demo.js
# Optional: package.json für Node.js-Modul-Syntax
node -e "require('fs').writeFileSync('package.json', JSON.stringify({type:'module'},null,2))"
Schritt 2: Verfügbare ECC-Kurven abfragen
Bevor ein ECDH-Objekt erstellt wird, lohnt sich ein Blick auf die unterstützten Kurven. Node.js erbt alle ECC-Kurven von der installierten OpenSSL-Version.
'use strict';
const crypto = require('node:crypto');
// Alle verfügbaren Kurven anzeigen
const allCurves = crypto.getCurves();
console.log('Gesamtanzahl verfügbarer Kurven:', allCurves.length);
// Für ECDH relevante Kurven prüfen
const ecdhCurves = ['prime256v1', 'secp384r1', 'secp521r1', 'x25519', 'x448'];
ecdhCurves.forEach(curve => {
const supported = allCurves.includes(curve);
console.log(`${curve}: ${supported ? '✓ verfügbar' : '✗ nicht verfügbar'}`);
});
Erwartete Ausgabe:
Gesamtanzahl verfügbarer Kurven: 81
prime256v1: ✓ verfügbar
secp384r1: ✓ verfügbar
secp521r1: ✓ verfügbar
x25519: ✓ verfügbar
x448: ✓ verfügbar
Die Kurve prime256v1 (auch bekannt als P-256 oder secp256r1) ist für die meisten Anwendungsfälle die optimale Wahl: Sie bietet 128-Bit-Sicherheit, ist von NIST und BSI empfohlen, in TLS 1.3 obligatorisch und auf allen Plattformen mit Hardwarebeschleunigung verfügbar. Die Kurve x25519 (Curve25519 in Montgomery-Form) wird für neue Protokolle empfohlen, wenn die Gegenseite X25519 unterstützt.
Schritt 3 und 4: Schlüsselpaare generieren
Der Kern von ECDH ist die Erzeugung von Schlüsselpaaren. Jede Partei generiert lokal einen privaten und einen öffentlichen Schlüssel. Die Methode generateKeys() kombiniert beide Schritte in einem Aufruf.
Schritt 3: Schlüsselpaar für Alice erzeugen
'use strict';
const crypto = require('node:crypto');
// ECDH-Objekt für Alice mit P-256 erstellen
const alice = crypto.createECDH('prime256v1');
// Schlüsselpaar generieren (privat + öffentlich)
alice.generateKeys();
// Öffentlichen Schlüssel in verschiedenen Formaten abrufen
const alicePublicKeyBuffer = alice.getPublicKey(); // 65 Bytes (unkomprimiert)
const alicePublicKeyHex = alice.getPublicKey('hex'); // 130 Hex-Zeichen
const alicePublicKeyComp = alice.getPublicKey('hex', 'compressed'); // 66 Hex-Zeichen (33 Bytes)
console.log('Alice - Öffentlicher Schlüssel (hex, unkomprimiert):');
console.log(alicePublicKeyHex);
console.log('\nAlice - Komprimierter öffentlicher Schlüssel (hex):');
console.log(alicePublicKeyComp);
console.log('\nGröße unkomprimiert:', alicePublicKeyBuffer.length, 'Bytes');
console.log('Größe komprimiert :', alicePublicKeyComp.length / 2, 'Bytes');
Erwartete Ausgabe:
Alice - Öffentlicher Schlüssel (hex, unkomprimiert):
04a3f2d1... (130 Hex-Zeichen = 65 Bytes)
Alice - Komprimierter öffentlicher Schlüssel (hex):
03a3f2d1... (66 Hex-Zeichen = 33 Bytes)
Größe unkomprimiert: 65 Bytes
Größe komprimiert : 33 Bytes
Der unkomprimierte öffentliche Schlüssel beginnt immer mit dem Präfix 04 und enthält sowohl die x- als auch die y-Koordinate des Kurvenpunkts (je 32 Bytes = 256 Bit). Der komprimierte Schlüssel enthält nur die x-Koordinate plus ein Paritätsbit (02 für gerades y, 03 für ungerades y), da die y-Koordinate bei bekanntem x eindeutig rekonstruiert werden kann.
Schritt 4: Schlüsselpaar für Bob erzeugen
// ECDH-Objekt für Bob, zwingend dieselbe Kurve
const bob = crypto.createECDH('prime256v1');
bob.generateKeys();
const bobPublicKey = bob.getPublicKey(); // Buffer für computeSecret()
const bobPublicKeyHex = bob.getPublicKey('hex'); // Zum Versenden/Speichern
console.log('Bob - Öffentlicher Schlüssel (hex):');
console.log(bobPublicKeyHex);
// WICHTIG: Den privaten Schlüssel niemals protokollieren!
// Nur zu Demonstrationszwecken hier:
console.log('\nBob - Privater Schlüssel (32 Bytes):', bob.getPrivateKey().length, 'Bytes');
Schritt 5 und 6: Öffentliche Schlüssel austauschen und Shared Secret berechnen
Der kritischste Schritt ist die Berechnung des Shared Secrets. Die öffentlichen Schlüssel werden ausgetauscht (können unverschlüsselt übertragen werden), und beide Seiten berechnen unabhängig voneinander denselben Wert.
Schritt 5: Empfangenen öffentlichen Schlüssel validieren
Bevor ein empfangener öffentlicher Schlüssel für computeSecret() verwendet wird, muss er validiert werden. Ein ungültiger Schlüssel (nicht auf der Kurve) kann zu sogenannten Invalid Curve Attacks führen, bei denen ein Angreifer den privaten Schlüssel schrittweise extrahiert. Node.js führt bei computeSecret() eine Basisprüfung durch, eine explizite Validierung ist aber sicherer:
'use strict';
const crypto = require('node:crypto');
/**
* Validiert einen empfangenen ECDH-Public-Key auf der angegebenen Kurve.
* Schützt vor Invalid-Curve-Angriffen und Punkt-am-Unendlich-Angriffen.
*/
function validateECDHPublicKey(curve, publicKeyBuffer) {
if (!Buffer.isBuffer(publicKeyBuffer)) {
throw new TypeError('publicKeyBuffer muss ein Buffer sein');
}
// Mindestgröße prüfen: P-256 unkomprimiert = 65 Bytes, komprimiert = 33 Bytes
if (publicKeyBuffer.length < 33) {
throw new RangeError('Öffentlicher Schlüssel zu kurz (min. 33 Bytes für P-256)');
}
// Punkt auf der Kurve validieren durch temporäres ECDH-Objekt
try {
const temp = crypto.createECDH(curve);
temp.setPublicKey(publicKeyBuffer);
return true; // Kein Fehler = Punkt liegt auf der Kurve
} catch (error) {
throw new Error(`Ungültiger öffentlicher Schlüssel: ${error.message}`);
}
}
Schritt 6: Shared Secret berechnen
// Öffentliche Schlüssel austauschen und validieren
const alicePublicKey = alice.getPublicKey();
const bobPublicKey = bob.getPublicKey();
validateECDHPublicKey('prime256v1', alicePublicKey);
validateECDHPublicKey('prime256v1', bobPublicKey);
// Shared Secret berechnen (von beiden Seiten unabhängig)
const aliceSharedSecret = alice.computeSecret(bobPublicKey);
const bobSharedSecret = bob.computeSecret(alicePublicKey);
// Prüfen: Beide Raw-Secrets müssen identisch sein
console.log('Shared Secrets identisch:', aliceSharedSecret.equals(bobSharedSecret));
// Ausgabe: Shared Secrets identisch: true
console.log('Shared Secret (hex):', aliceSharedSecret.toString('hex'));
console.log('Shared Secret Länge:', aliceSharedSecret.length, 'Bytes');
// Bei P-256: genau 32 Bytes (x-Koordinate des gemeinsamen Kurvenpunkts)
Das Ergebnis von computeSecret() ist die x-Koordinate des gemeinsamen Kurvenpunkts als Buffer. Bei P-256 sind das genau 32 Bytes (256 Bit). Dieser Raw-Secret darf jedoch nicht direkt als Verschlüsselungsschlüssel verwendet werden, weil er keine gleichmäßige Zufallsverteilung hat. Der nächste Schritt behebt das.
Schritt 7 und 8: Schlüsselableitung mit HKDF
Der ECDH Shared Secret muss durch eine Key Derivation Function (KDF) verarbeitet werden, bevor er als Schlüssel eingesetzt wird. Der Standard ist HKDF (HMAC-based Key Derivation Function), definiert in RFC 5869. Node.js bietet seit Version 15 die eingebaute Funktion crypto.hkdfSync(), die ohne externe Pakete auskommt.
HKDF arbeitet in zwei Phasen:
- Extract: Aus dem Shared Secret und einem zufälligen Salt wird ein Pseudorandom Key (PRK) abgeleitet
- Expand: Aus dem PRK werden beliebig viele Schlüsselbytes für verschiedene Zwecke erzeugt
Schritt 7: HKDF zur Schlüsselableitung einsetzen
'use strict';
const crypto = require('node:crypto');
/**
* Leitet einen kryptographisch sicheren Schlüssel aus dem ECDH Shared Secret ab.
*
* @param {Buffer} sharedSecret - Raw-Output von computeSecret()
* @param {Buffer} salt - Zufälliger Salt (32 Bytes empfohlen, muss übertragen werden)
* @param {string} info - Kontext-String zur Schlüsseltrennung
* @param {number} keyLength - Gewünschte Schlüssellänge in Bytes (32 für AES-256)
* @returns {Buffer} - Abgeleiteter Schlüssel
*/
function deriveKey(sharedSecret, salt, info, keyLength = 32) {
// ACHTUNG: hkdfSync gibt ein ArrayBuffer zurück, nicht einen Buffer!
const arrayBuffer = crypto.hkdfSync(
'sha256', // Hash-Algorithmus
sharedSecret, // Input Key Material (IKM)
salt, // Salt (nicht geheim, aber zufällig und einmalig)
Buffer.from(info), // Info/Kontext (bindet Schlüssel an bestimmten Zweck)
keyLength // Output-Länge in Bytes
);
return Buffer.from(arrayBuffer); // In Buffer umwandeln!
}
// Salt muss für jeden ECDH-Austausch neu generiert und mit dem Public Key übertragen werden
const salt = crypto.randomBytes(32);
const info = 'ecdh-tutorial-v1-aes-256-gcm'; // Kontext für diesen Schlüsselzweck
// 32-Byte-Schlüssel für AES-256-GCM ableiten
const encryptionKey = deriveKey(aliceSharedSecret, salt, info, 32);
console.log('Abgeleiteter AES-Schlüssel (hex):', encryptionKey.toString('hex'));
console.log('Schlüssellänge:', encryptionKey.length, 'Bytes'); // 32
Schritt 8: Mehrere Schlüssel aus einem Shared Secret ableiten
In der Praxis werden oft mehrere Schlüssel benötigt: ein Verschlüsselungsschlüssel, ein Authentifizierungsschlüssel. Mit HKDF können beliebig viele separate Schlüssel aus einem einzigen Shared Secret abgeleitet werden, ohne dass sie sich gegenseitig kompromittieren.
// 64 Bytes ableiten und in zwei 32-Byte-Schlüssel aufteilen
const keyMaterial = Buffer.from(
crypto.hkdfSync('sha256', aliceSharedSecret, salt, Buffer.from('ecdh-keys-v1'), 64)
);
const encKey = keyMaterial.subarray(0, 32); // Schlüssel für AES-256-GCM
const macKey = keyMaterial.subarray(32, 64); // Schlüssel für HMAC-SHA256
console.log('Verschlüsselungsschlüssel:', encKey.toString('hex'));
console.log('Authentifizierungsschlüssel:', macKey.toString('hex'));
// Beide Schlüssel sind kryptographisch unabhängig voneinander
Der Salt muss für jeden ECDH-Austausch frisch generiert und sicher an die Gegenseite übertragen werden. Da er nicht geheim ist, kann er zusammen mit dem öffentlichen Schlüssel im Klartext übertragen werden. Der info-Parameter bindet den abgeleiteten Schlüssel an einen spezifischen Kontext und verhindert die versehentliche Wiederverwendung desselben Schlüsselmaterials für verschiedene Zwecke.
Schritt 9 und 10: AES-256-GCM Verschlüsselung mit abgeleitetem Schlüssel
Mit dem abgeleiteten Schlüssel lassen sich Nachrichten symmetrisch verschlüsseln. AES-256-GCM ist der empfohlene Modus: Er bietet Vertraulichkeit und Authentizität (Integritätsprüfung) in einem einzigen Verfahren und ist der Standard in TLS 1.3.
Schritt 9: Verschlüsselungs- und Entschlüsselungsfunktionen implementieren
'use strict';
const crypto = require('node:crypto');
/**
* Verschlüsselt Daten mit AES-256-GCM.
* Gibt IV, Ciphertext und Authentication Tag zurück.
*/
function encrypt(key, plaintext) {
if (key.length !== 32) {
throw new RangeError('Schlüssel muss genau 32 Bytes lang sein (AES-256)');
}
const iv = crypto.randomBytes(12); // 96-Bit IV ist der Standard für GCM
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const plaintextBuf = Buffer.isBuffer(plaintext)
? plaintext
: Buffer.from(plaintext, 'utf8');
const encrypted = Buffer.concat([
cipher.update(plaintextBuf),
cipher.final()
]);
const tag = cipher.getAuthTag(); // 16 Bytes GCM-Authentifizierungstoken
return {
iv: iv.toString('base64'),
ciphertext: encrypted.toString('base64'),
tag: tag.toString('base64')
};
}
/**
* Entschlüsselt AES-256-GCM-Daten.
* Wirft Fehler, wenn Ciphertext manipuliert wurde (Tag-Verifizierung schlägt fehl).
*/
function decrypt(key, { iv, ciphertext, tag }) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
const decrypted = Buffer.concat([
decipher.update(Buffer.from(ciphertext, 'base64')),
decipher.final() // Fehler hier = Tag ungültig = Daten manipuliert
]);
return decrypted.toString('utf8');
}
Schritt 10: Vollständige ECDH-Verschlüsselung testen
// Vollständiger Demo-Ablauf: ECDH + HKDF + AES-256-GCM
// 1. Schlüsselpaare generieren
const alice = crypto.createECDH('prime256v1');
const bob = crypto.createECDH('prime256v1');
alice.generateKeys();
bob.generateKeys();
// 2. Öffentliche Schlüssel austauschen und validieren
const alicePubKey = alice.getPublicKey();
const bobPubKey = bob.getPublicKey();
// 3. Shared Secrets berechnen (identisch auf beiden Seiten)
const aliceSecret = alice.computeSecret(bobPubKey);
const bobSecret = bob.computeSecret(alicePubKey);
console.assert(aliceSecret.equals(bobSecret), 'Secrets müssen identisch sein');
// 4. Schlüssel ableiten (Salt wird zusammen mit Public Key übertragen)
const salt = crypto.randomBytes(32);
const aliceEncKey = deriveKey(aliceSecret, salt, 'enc-v1', 32);
const bobEncKey = deriveKey(bobSecret, salt, 'enc-v1', 32);
// 5. Alice verschlüsselt eine Nachricht für Bob
const nachricht = 'Geheime Nachricht: ECDH in Node.js funktioniert!';
const verschluesselt = encrypt(aliceEncKey, nachricht);
console.log('Ciphertext:', verschluesselt.ciphertext);
console.log('IV:', verschluesselt.iv);
console.log('Tag:', verschluesselt.tag);
// 6. Bob entschlüsselt die Nachricht
const entschluesselt = decrypt(bobEncKey, verschluesselt);
console.log('Entschlüsselt:', entschluesselt);
// Ausgabe: Entschlüsselt: Geheime Nachricht: ECDH in Node.js funktioniert!
Schritt 11: X25519 als moderne Alternative zu P-256
X25519 (Curve25519 in Montgomery-Form) ist die modernere Alternative zu P-256 für den Schlüsselaustausch. Im Gegensatz zu den NIST-Kurven (P-256, P-384) wurde Curve25519 von Daniel J. Bernstein und Tanja Lange mit besonderem Fokus auf Implementierungssicherheit und Widerstandsfähigkeit gegen Seitenkanalangriffe entworfen.
Vorteile von X25519 gegenüber P-256:
- Resistent gegen Timing-Angriffe durch konstante Ausführungszeit (const-time Montgomery-Ladder)
- Keine Notwendigkeit zur Punkt-Validierung (alle 32-Byte-Eingaben sind gültige öffentliche Schlüssel)
- Etwas schneller als P-256 auf den meisten Prozessoren
- Keine Zweifel an der Kurvenparametrierung (Curve25519 wurde transparent ohne behördliche Beteiligung gewählt)
In Node.js erfolgt X25519-ECDH über generateKeyPairSync('x25519') und diffieHellman(), nicht über createECDH():
'use strict';
const crypto = require('node:crypto');
// X25519 Schlüsselpaare erzeugen
const aliceKeyPair = crypto.generateKeyPairSync('x25519');
const bobKeyPair = crypto.generateKeyPairSync('x25519');
// Öffentliche Schlüssel für Übertragung exportieren (DER-Format = binär)
const alicePubDER = aliceKeyPair.publicKey.export({ type: 'spki', format: 'der' });
const bobPubDER = bobKeyPair.publicKey.export({ type: 'spki', format: 'der' });
console.log('Alice X25519 Public Key (base64):');
console.log(alicePubDER.toString('base64'));
console.log('Größe:', alicePubDER.length, 'Bytes'); // 44 Bytes mit SPKI-Header
// Öffentlichen Schlüssel der Gegenseite importieren und in KeyObject umwandeln
const bobPubImported = crypto.createPublicKey({ key: bobPubDER, format: 'der', type: 'spki' });
const alicePubImported = crypto.createPublicKey({ key: alicePubDER, format: 'der', type: 'spki' });
// Shared Secret mit diffieHellman() berechnen (nicht computeSecret!)
const aliceShared = crypto.diffieHellman({
privateKey: aliceKeyPair.privateKey,
publicKey: bobPubImported
});
const bobShared = crypto.diffieHellman({
privateKey: bobKeyPair.privateKey,
publicKey: alicePubImported
});
console.log('X25519 Secrets identisch:', aliceShared.equals(bobShared));
// Ausgabe: X25519 Secrets identisch: true
console.log('X25519 Shared Secret (32 Bytes):', aliceShared.toString('hex'));
X25519-Schlüssel haben eine feste Größe von 32 Bytes für den privaten und den öffentlichen Schlüssel (ohne Header). Der Shared Secret ist ebenfalls immer 32 Bytes. Im SPKI-Format mit Algorithmus-Header sind öffentliche X25519-Schlüssel 44 Bytes groß. Im Vergleich: P-256 unkomprimiert ist 65 Bytes groß, also fast 50% größer.
Schritt 12: Perfect Forward Secrecy implementieren
Perfect Forward Secrecy (PFS) ist eine Sicherheitseigenschaft, die garantiert, dass die Kompromittierung eines langfristigen privaten Schlüssels keine Entschlüsselung vergangener Sitzungen ermöglicht. PFS wird durch ephemere Schlüsselpaare erreicht: Für jede Sitzung werden neue Schlüsselpaare generiert und nach der Sitzung sofort vernichtet.
TLS 1.3 erzwingt PFS für alle Verbindungen, indem es ausschließlich ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) zulässt. Statische RSA-Schlüsselübertragung, die in TLS 1.2 noch möglich war, ist in TLS 1.3 vollständig entfernt.
'use strict';
const crypto = require('node:crypto');
/**
* Sitzungsmanager mit Perfect Forward Secrecy.
* Erstellt für jede Verbindung ein neues ephemeres Schlüsselpaar.
*/
class SecureSession {
#curve;
#ephemeralKey = null;
#sessionKey = null;
#salt = null;
constructor(curve = 'prime256v1') {
this.#curve = curve;
}
// Phase 1: Ephemeres Schlüsselpaar für diese Sitzung erzeugen
initiate() {
this.#ephemeralKey = crypto.createECDH(this.#curve);
this.#ephemeralKey.generateKeys();
this.#salt = crypto.randomBytes(32);
return {
publicKey: this.#ephemeralKey.getPublicKey('base64'),
salt: this.#salt.toString('base64')
};
}
// Phase 2: Sitzungsschlüssel aus dem Public Key der Gegenseite ableiten
complete(peerPayload) {
if (!this.#ephemeralKey) {
throw new Error('initiate() muss vor complete() aufgerufen werden');
}
const peerPubKey = Buffer.from(peerPayload.publicKey, 'base64');
// Public Key der Gegenseite validieren
try {
const temp = crypto.createECDH(this.#curve);
temp.setPublicKey(peerPubKey);
} catch {
throw new Error('Ungültiger öffentlicher Schlüssel der Gegenseite');
}
const rawSecret = this.#ephemeralKey.computeSecret(peerPubKey);
const salt = Buffer.from(peerPayload.salt, 'base64');
this.#sessionKey = Buffer.from(
crypto.hkdfSync('sha256', rawSecret, salt, Buffer.from('pfs-session-v1'), 32)
);
// Privaten Schlüssel sofort vernichten (das ist PFS!)
this.#ephemeralKey = null;
return this.#sessionKey;
}
getSessionKey() {
if (!this.#sessionKey) throw new Error('Sitzung nicht abgeschlossen');
return this.#sessionKey;
}
// Sitzung sicher beenden: Schlüsselmaterial überschreiben
destroy() {
if (this.#sessionKey) this.#sessionKey.fill(0);
this.#sessionKey = this.#ephemeralKey = this.#salt = null;
}
}
// Demo: Sitzung aufbauen und Schlüssel verwenden
const sessionA = new SecureSession();
const sessionB = new SecureSession();
const payloadA = sessionA.initiate();
const payloadB = sessionB.initiate();
const keyA = sessionA.complete(payloadB);
const keyB = sessionB.complete(payloadA);
console.log('PFS-Sitzungsschlüssel identisch:', keyA.equals(keyB));
// Ausgabe: PFS-Sitzungsschlüssel identisch: true
// Neue Sitzung hat komplett anderen Schlüssel (das ist PFS)
const session2A = new SecureSession();
const session2B = new SecureSession();
const pay2A = session2A.initiate();
const pay2B = session2B.initiate();
const key2A = session2A.complete(pay2B);
console.log('Schlüssel verschiedener Sitzungen unterschiedlich:', !keyA.equals(key2A));
// Ausgabe: Schlüssel verschiedener Sitzungen unterschiedlich: true
// Sitzungen sauber beenden
sessionA.destroy();
sessionB.destroy();
ECC-Kurven im Vergleich: P-256, P-384, P-521, X25519, X448
Die Kurvenauswahl beeinflusst Sicherheitsniveau, Kompatibilität und Leistung erheblich. Die folgende Tabelle fasst die wichtigsten Parameter der in Node.js verfügbaren ECDH-Kurven zusammen:
| Kurve | Andere Namen | Sicherheitsniveau | Schlüsselgröße | Shared Secret | Geschwindigkeit | Typischer Einsatz |
|---|---|---|---|---|---|---|
| prime256v1 | P-256, secp256r1 | 128 Bit | 256 Bit | 32 Bytes | Sehr schnell | TLS 1.3, HTTPS, ECDSA |
| secp384r1 | P-384 | 192 Bit | 384 Bit | 48 Bytes | Schnell | NSA Suite B, Behörden |
| secp521r1 | P-521 | 256 Bit | 521 Bit | 66 Bytes | Mittel | Klassifizierte Anwendungen |
| x25519 | Curve25519 | 128 Bit | 255 Bit eff. | 32 Bytes | Sehr schnell | Signal, WireGuard, SSH |
| x448 | Curve448, Goldilocks | 224 Bit | 448 Bit | 56 Bytes | Schnell | Langfristige Sicherheit |
Für neue Protokolle in 2026 empfiehlt sich X25519 für den Schlüsselaustausch und Ed25519 für digitale Signaturen, sofern die Gegenseite beides unterstützt. Für Systeme mit FIPS-140-3-Anforderungen oder BSI-Zulassung sind ausschließlich NIST-Kurven (P-256, P-384, P-521) zulässig.
P-384 und P-521 bieten höhere Sicherheitsmargen auf Kosten der Leistung. Bei P-521 ist der Rechenaufwand erheblich höher als bei P-256. Ein Benchmark zeigt typische Werte pro ECDH-Operation:
- prime256v1: 0,05 bis 0,2 ms pro Schlüsselaustausch
- secp384r1: 0,1 bis 0,4 ms pro Schlüsselaustausch
- secp521r1: 0,3 bis 1,0 ms pro Schlüsselaustausch
- x25519: 0,04 bis 0,15 ms pro Schlüsselaustausch
Für die meisten Web-Anwendungen ist P-256 die optimale Wahl: weit verbreitet, hardwarebeschleunigt und in TLS 1.3 nativ unterstützt. X25519 ist bei vergleichbarer Sicherheit schneller und einfacher sicher zu implementieren.
ECDH vs. klassisches Diffie-Hellman: Leistung und Sicherheit im Vergleich
Klassisches Diffie-Hellman (DH) basiert auf dem diskreten Logarithmusproblem in einer multiplikativen Gruppe (Zp*). ECDH überträgt dieses Konzept auf elliptische Kurven und erzielt mit kleineren Schlüsseln dieselbe Sicherheit. TLS 1.3 hat klassisches DH aus der Spezifikation entfernt.
| Eigenschaft | ECDH (P-256) | Klass. DH (2048 Bit) | Klass. DH (3072 Bit) |
|---|---|---|---|
| Sicherheitsniveau | 128 Bit | 112 Bit | 128 Bit |
| Schlüsselgröße | 256 Bit | 2.048 Bit | 3.072 Bit |
| Shared Secret | 32 Bytes | 256 Bytes | 384 Bytes |
| Schlüsselgenerierung | Mikrosekunden | Millisekunden | Sekunden |
| Relative Geschwindigkeit | 20–60x schneller | 1x (Referenz) | 0,1x |
| BSI-Empfehlung 2026 | Ja | Nein (< 3000 Bit) | Bedingt |
| TLS 1.3 Support | Ja (Standard) | Nein | Nein |
| Quantum-Sicher | Nein | Nein | Nein |
Die 2024 vergleichende Studie der St. Mary's University Texas quantifiziert den Unterschied präzise: Bei einem Sicherheitsniveau von 128 Bit ist ECC 20 bis 60 Mal schneller als RSA für eine private Schlüsseloperation. Bei 256-Bit-Sicherheit, also ECC-521 gegenüber 15.360-Bit-RSA, steigt der Faktor auf durchschnittlich 400. Die Schlüsselgenerierung ist noch drastischer: In der Zeit für einen RSA-Schlüssel können ungefähr 8.400 ECC-Schlüsselpaare erzeugt werden.
6 häufige Fehler bei ECDH in Node.js und wie man sie vermeidet
Diese sechs Fehler treten in der Praxis am häufigsten auf und führen von stillen Protokollfehlern bis hin zu kritischen Sicherheitslücken.
Fehler 1: Raw Shared Secret direkt als AES-Schlüssel verwenden
Der Raw-Output von computeSecret() ist die x-Koordinate eines Kurvenpunkts, kein gleichmäßig verteilter Zufallswert. Ohne KDF ist der Schlüssel vorhersagbarer als er sein sollte.
// FALSCH: Raw Secret direkt als Schlüssel
const badKey = alice.computeSecret(bobPublicKey);
const cipher = crypto.createCipheriv('aes-256-gcm', badKey, iv); // Unsicher!
// RICHTIG: Immer KDF zwischenschalten
const rawSecret = alice.computeSecret(bobPublicKey);
const goodKey = Buffer.from(
crypto.hkdfSync('sha256', rawSecret, salt, Buffer.from('context'), 32)
);
Fehler 2: Empfangenen Public Key nicht validieren (Invalid Curve Attack)
Ein Angreifer kann einen Punkt senden, der nicht auf der Kurve liegt. Bei Verwendung solcher Punkte kann der private Schlüssel schrittweise extrahiert werden.
// FALSCH: Direkt verwenden ohne Prüfung
const secret = alice.computeSecret(untrustedKey); // Gefährlich!
// RICHTIG: Validierung vor computeSecret()
try {
const temp = crypto.createECDH('prime256v1');
temp.setPublicKey(untrustedKey); // Fehler wenn ungültig
const secret = alice.computeSecret(untrustedKey); // Sicher
} catch (err) {
throw new Error('Ungültiger Public Key: ' + err.message);
}
Fehler 3: Statische Schlüsselpaare ohne PFS verwenden
// FALSCH: Ein globales Schlüsselpaar für alle Sitzungen
const globalKey = crypto.createECDH('prime256v1');
globalKey.generateKeys(); // Einmal generiert, nie erneuert
// RICHTIG: Ephemere Schlüsselpaare pro Sitzung
function createSession() {
const ephemeral = crypto.createECDH('prime256v1');
ephemeral.generateKeys();
return ephemeral; // Nach Sitzung verworfen
}
Fehler 4: Kurven-Mismatch zwischen den Parteien
// FALSCH: Alice und Bob verwenden verschiedene Kurven
const alice = crypto.createECDH('prime256v1');
const bob = crypto.createECDH('secp384r1'); // Andere Kurve!
// alice.computeSecret(bob.getPublicKey()) wirft einen Fehler
// RICHTIG: Kurve im Protokoll als Konstante definieren
const CURVE = 'prime256v1'; // An einer Stelle definiert, überall verwendet
const alice = crypto.createECDH(CURVE);
const bob = crypto.createECDH(CURVE);
Fehler 5: X25519 mit createECDH() versuchen
// FALSCH: X25519 ist keine Standard-OpenSSL-ECC-Kurve für createECDH
const ecdh = crypto.createECDH('x25519'); // TypeError!
// RICHTIG: X25519 mit generateKeyPairSync und diffieHellman()
const keyPair = crypto.generateKeyPairSync('x25519');
const shared = crypto.diffieHellman({
privateKey: keyPair.privateKey,
publicKey: peerPublicKey
});
Fehler 6: hkdfSync-Rückgabe nicht in Buffer umwandeln
// FALSCH: hkdfSync gibt ArrayBuffer zurück, nicht Buffer
const rawResult = crypto.hkdfSync('sha256', secret, salt, info, 32);
const cipher = crypto.createCipheriv('aes-256-gcm', rawResult, iv);
// TypeError: Invalid key length (ArrayBuffer ist kein Buffer!)
// RICHTIG: Explizit in Buffer umwandeln
const key = Buffer.from(crypto.hkdfSync('sha256', secret, salt, info, 32));
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); // Kein Fehler
Fehlerbehebung: 8 typische ECDH-Probleme in Node.js
Die folgenden Probleme treten häufig bei der ECDH-Implementierung auf. Jede Lösung wurde gegen Node.js 20.x und 22.x geprüft.
Problem 1: "Error: Invalid EC key"
Ursache: Der übergebene öffentliche Schlüssel hat falsches Format oder liegt nicht auf der Kurve.
// Diagnose: Schlüsselformat prüfen
const key = Buffer.from(receivedHex, 'hex');
console.log('Länge:', key.length); // P-256 unkomprimiert: 65 Bytes
console.log('Präfix:', key[0].toString(16)); // 04 = unkomprimiert, 02/03 = komprimiert
// Lösung: Korrekte Konvertierung aus verschiedenen Eingabeformaten
const keyFromBase64 = Buffer.from(base64String, 'base64');
const keyFromArray = Buffer.from(arrayLike);
Problem 2: "TypeError: curve.toLowerCase is not a function"
Ursache: Der Kurvenname ist kein String (z. B. undefined aus fehlerhafter Konfiguration).
// Diagnose
console.log(typeof config.curve); // Sollte 'string' sein
// Lösung: Eingabe validieren
const ALLOWED = ['prime256v1', 'secp384r1', 'x25519'];
const curve = String(config.curve || 'prime256v1');
if (!ALLOWED.includes(curve)) throw new Error(`Ungültige Kurve: ${curve}`);
const ecdh = crypto.createECDH(curve);
Problem 3: Beide Secrets berechnet, aber sie stimmen nicht überein
Ursache: Schlüssel wurden vertauscht, verschiedene Kurven genutzt, oder generateKeys() wurde nach getPublicKey() erneut aufgerufen.
// Häufige Ursache: generateKeys() nach getPublicKey() erneut aufgerufen
const alice = crypto.createECDH('prime256v1');
alice.generateKeys();
const alicePub = alice.getPublicKey(); // Schlüssel gespeichert
alice.generateKeys(); // NEUES Schlüsselpaar! alicePub ist jetzt ungültig!
const secret = bob.computeSecret(alicePub); // Falsches Ergebnis!
// Lösung: generateKeys() nur einmal pro Sitzung aufrufen
Problem 4: "Error: Invalid key length" bei AES-Cipher
Ursache: hkdfSync() gibt ArrayBuffer zurück, aber createCipheriv() erwartet einen Buffer.
// Immer Buffer.from() um den hkdfSync-Return-Wert wrappen
const key = Buffer.from(crypto.hkdfSync('sha256', secret, salt, info, 32));
console.log(Buffer.isBuffer(key)); // true
console.log(key.length); // 32
Problem 5: Performance-Einbruch mit P-521 unter Last
Ursache: P-521 ist signifikant langsamer als P-256. Bei 1.000 parallelen Verbindungen kann das zum Engpass werden.
// P-521 für allgemeine Web-Anwendungen vermeiden
// P-256 bietet 128-Bit-Sicherheit bei 5-10x höherer Leistung
// X25519 ist noch schneller und ebenfalls 128-Bit sicher
// Wenn P-521 nötig: Worker Threads für rechenintensive Operationen nutzen
const { Worker } = require('node:worker_threads');
Problem 6: HKDF erzeugt verschiedene Schlüssel auf beiden Seiten
Ursache: Salt oder Info-Parameter haben auf beiden Seiten unterschiedliche Werte, oft durch verschiedene String-Encodings.
// Häufige Ursache: Salt als Hex-String statt als Buffer
const saltHex = 'a1b2c3d4...';
// FALSCH: Salt ist die UTF-8-Kodierung des Hex-Strings
crypto.hkdfSync('sha256', secret, saltHex, info, 32);
// RICHTIG: Salt ist der binäre Inhalt
crypto.hkdfSync('sha256', secret, Buffer.from(saltHex, 'hex'), info, 32);
Problem 7: "Unsupported state or unable to authenticate data" beim Entschlüsseln
Ursache: Der GCM Authentication Tag stimmt nicht überein. Das kann auf manipulierte Daten oder einen falschen Schlüssel hinweisen.
// NIEMALS: Fehler abfangen und trotzdem Plaintext verwenden
try {
const plaintext = Buffer.concat([dec.update(ct), dec.final()]);
// Wenn final() keinen Fehler wirft, ist die Integrität bestätigt
} catch (err) {
// Tag-Fehler = mögliche Manipulation = Verbindung abbrechen
console.error('Integritätsprüfung fehlgeschlagen:', err.message);
connection.destroy(); // Verbindung sofort beenden
}
Problem 8: "TypeError: publicKey.export is not a function" bei X25519
Ursache: Verwechslung von createECDH()-Objekten (geben Buffer zurück) mit KeyObject-Instanzen aus generateKeyPairSync().
// createECDH gibt KEIN KeyObject zurück
const ecdh = crypto.createECDH('prime256v1');
ecdh.generateKeys();
ecdh.publicKey; // undefined - gibt es nicht!
ecdh.getPublicKey(); // Buffer - das ist die richtige Methode
// generateKeyPairSync gibt KeyObject zurück
const { publicKey } = crypto.generateKeyPairSync('x25519');
publicKey.export({ type: 'spki', format: 'der' }); // Korrekt für KeyObject
Praxiseinsatz: Wo ECDH in der realen Welt verwendet wird
ECDH bildet das Rückgrat moderner sicherer Kommunikation. Die folgenden Systeme und Protokolle basieren direkt auf den in diesem Tutorial gezeigten Mechanismen.
TLS 1.3 (RFC 8446): Jede HTTPS-Verbindung mit TLS 1.3 führt einen ECDHE-Schlüsselaustausch durch. Der Standard schreibt vor, dass mindestens die Kurvengruppen P-256 (secp256r1) und X25519 unterstützt werden müssen. Browser und Server verhandeln die Kurve im ClientHello-Handshake. Der gesamte Vorgang dauert weniger als eine Millisekunde.
Signal-Protokoll (Signal, WhatsApp): Das Signal-Protokoll verwendet X25519 für den ECDH-Schlüsselaustausch im Extended Triple Diffie-Hellman (X3DH) Algorithmus. X3DH kombiniert mehrere ECDH-Austausche zur gleichzeitigen Authentifizierung und Schlüsselableitung. Darauf aufbauend schafft das Double-Ratchet-Protokoll Perfect Forward Secrecy für jede einzelne Nachricht.
WireGuard VPN: WireGuard verwendet ausschließlich X25519 für den Schlüsselaustausch und Curve25519 in der Twisted-Edwards-Form (Ed25519) für die Authentifizierung. Die Protokoll-Einfachheit trägt zur kleinen Codebasis bei (unter 4.000 Zeilen), was die Sicherheitsanalyse erheblich vereinfacht im Vergleich zu OpenVPN.
Bitcoin und Ethereum (secp256k1): Beide Blockchains verwenden die Kurve secp256k1 für ECDSA-Signaturen bei Transaktionen. secp256k1 ist eng mit P-256 verwandt, aber mit unterschiedlichen Kurvenparametern. Der private Schlüssel einer Bitcoin-Wallet ist ein 256-Bit-Zufallswert; der öffentliche Schlüssel ist der entsprechende Kurvenpunkt, aus dem die Wallet-Adresse abgeleitet wird.
Post-Quantum-Transition: Da Quantencomputer ECDH über Shors Algorithmus brechen könnten, empfiehlt NIST seit August 2024 den Einsatz von ML-KEM (FIPS 203) als Ersatz für ECDH in Key-Exchange-Protokollen. TLS 1.3 unterstützt bereits hybride Verfahren, die ECDH und ML-KEM kombinieren (Hybrid Key Exchange). Chrome, Firefox und Cloudflare aktivieren diese Hybridmethode standardmäßig, um "Harvest Now, Decrypt Later"-Angriffe zu verhindern.
Verwandte Artikel
- ECDSA in Node.js: Digitale Signaturen in 11 Schritten [2026] - ECDSA-Signaturen auf Basis derselben ECC-Kurven
- SSH-Key erstellen: Ed25519 in 8 Schritten [2026] - Ed25519 (Curve25519) für SSH-Authentifizierung
- ML-KEM (Kyber) in Node.js: 12 Schritte [2026] - Post-Quantum-Schlüsselaustausch als ECDH-Nachfolger
- OpenSSL-Tutorial: Schlüssel und Zertifikate in 12 Schritten [2026] - ECC-Schlüsselpaare mit OpenSSL erstellen
- GPG-Verschlüsselung mit Gpg4win: 12 Schritte [2026] - Asymmetrische Verschlüsselung mit OpenPGP
- Digitale Signaturen erklärt: ECDSA und Hash-Funktionen - Mathematische Grundlagen digitaler Signaturen
- HTTPS und TLS erklärt: Wie das Schloss im Browser Sie schützt - ECDH im TLS-Handshake verständlich erklärt
FAQ: Häufige Fragen zu ECDH in Node.js
Kann ich ECDH ohne externe npm-Pakete implementieren?
Ja. Das eingebaute Modul node:crypto enthält alle benötigten Funktionen: createECDH(), generateKeyPairSync('x25519'), diffieHellman(), hkdfSync(), createCipheriv() und createDecipheriv(). Externe Pakete wie noble-curves können zusätzliche Kurven bieten, sind aber für Standard-ECDH mit P-256 oder X25519 nicht notwendig.
Was ist der Unterschied zwischen ECDH und ECDHE?
ECDHE (Ephemeral) ist ECDH mit einmaligen Schlüsselpaaren pro Sitzung. Bei statischem ECDH werden langfristige Schlüsselpaare genutzt, was Perfect Forward Secrecy ausschließt: Ein kompromittierter langfristiger Private Key würde alle vergangenen und zukünftigen Sitzungen enthüllen. TLS 1.3 verbietet statische Schlüssel vollständig und erzwingt ausschließlich ECDHE.
Warum brauche ich HKDF, wenn computeSecret() bereits 32 Bytes ausgibt?
Der Raw-Output von computeSecret() ist die x-Koordinate eines Kurvenpunkts. Diese hat keine gleichmäßige Verteilung über alle möglichen 32-Byte-Werte. HKDF erzeugt aus diesem Wert einen kryptographisch sicheren Schlüssel mit gleichmäßiger Verteilung. Außerdem ermöglicht HKDF, mehrere unabhängige Schlüssel aus einem Shared Secret abzuleiten (Verschlüsselung, Authentifizierung, ...) ohne dass sie sich gegenseitig beeinflussen.
Ist ECDH sicher gegen Quantencomputer?
Nein. Ein hinreichend großer Quantencomputer kann das ECDLP über Shors Algorithmus lösen. Für P-256 würden laut Forschung etwa 2.330 logische Qubits und 126 Milliarden Toffoli-Gates benötigt, was weit über den Fähigkeiten heutiger Systeme liegt. NIST empfiehlt die Migration zu ML-KEM (FIPS 203) für neuen Code und hybride Verfahren (ECDH und ML-KEM parallel) für Übergangslösungen.
Welche Kurve soll ich für eine neue Anwendung in 2026 wählen?
Schlüsselaustausch: X25519 wenn die Gegenseite es unterstützt, sonst P-256 (prime256v1). Digitale Signaturen: Ed25519, sonst P-256 mit ECDSA. P-384 für behördliche Anforderungen (BSI, NSA Suite B). FIPS-140-3-Compliance: Ausschließlich NIST-Kurven. Blockchain-Kompatibilität: secp256k1.
Kann ECDH für Datenverschlüsselung genutzt werden?
ECDH verschlüsselt selbst keine Daten. Es liefert einen gemeinsamen Schlüssel, aus dem via HKDF ein Verschlüsselungsschlüssel abgeleitet wird. Die eigentliche Datenverschlüsselung erfolgt mit AES-256-GCM. Dieses Muster nennt sich ECIES (Elliptic Curve Integrated Encryption Scheme) und ist der Standard für hybride ECC-Verschlüsselung.
Was ist der Unterschied zwischen secp256k1 und prime256v1 (P-256)?
Beide bieten 128-Bit-Sicherheit mit 256-Bit-Schlüsseln, haben aber unterschiedliche Kurvenparameter. prime256v1 (P-256, secp256r1) ist die NIST-Standardkurve für TLS und allgemeine Kryptographie. secp256k1 ist eine Koblitz-Kurve aus dem Bitcoin-Standard mit speziellen algebraischen Eigenschaften, die bestimmte Implementierungsoptimierungen ermöglichen. Für TLS und Node.js-Anwendungen ist prime256v1 die richtige Wahl.
Wie überträgt man den öffentlichen Schlüssel sicher?
Öffentliche Schlüssel sind per Definition nicht geheim und können im Klartext übertragen werden. Sie müssen jedoch authentifiziert werden, um Man-in-the-Middle-Angriffe zu verhindern. Gängige Ansätze: Signatur des Public Keys mit einem langfristigen Identitätsschlüssel (wie in SSH oder Signal), Einbettung in ein TLS-Zertifikat (signiert von einer CA), oder Out-of-Band-Verifizierung (z. B. QR-Code). Ohne Authentifizierung schützt ECDH nur gegen passive Lauschangriffe, nicht gegen aktive Man-in-the-Middle-Angriffe.
Weiterführende offizielle Ressourcen:
- Node.js Crypto-Dokumentation: createECDH()
- BSI TR-02102-1: Kryptographische Empfehlungen 2026
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
- RFC 7748: Elliptic Curves for Diffie-Hellman Key Agreement (X25519, X448)
- NIST SP 800-56A Rev. 3: Recommendation for Pair-Wise Key Establishment Schemes
Vollständiges Beispielprojekt: Sicheres Nachrichtensystem mit ECDH
Das folgende vollständige Beispiel kombiniert alle Schritte zu einem funktionsfähigen sicheren Nachrichtensystem. Es implementiert ECDH-Schlüsselaustausch, HKDF-Schlüsselableitung, AES-256-GCM-Verschlüsselung und Perfect Forward Secrecy in einer klaren, produktionstauglichen Klasse.
'use strict';
const crypto = require('node:crypto');
// ==============================
// Hilfsfunktionen
// ==============================
function deriveKey(sharedSecret, salt, info, length = 32) {
return Buffer.from(
crypto.hkdfSync('sha256', sharedSecret, salt, Buffer.from(info), length)
);
}
function encrypt(key, plaintext) {
const iv = crypto.randomBytes(12);
const c = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([c.update(Buffer.from(plaintext, 'utf8')), c.final()]);
return {
iv: iv.toString('base64'),
ct: ct.toString('base64'),
tag: c.getAuthTag().toString('base64')
};
}
function decrypt(key, { iv, ct, tag }) {
const d = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64'));
d.setAuthTag(Buffer.from(tag, 'base64'));
return Buffer.concat([d.update(Buffer.from(ct, 'base64')), d.final()]).toString('utf8');
}
// ==============================
// SecureMessenger: ECDH + HKDF + AES-256-GCM + PFS
// ==============================
class SecureMessenger {
#curve;
#ecdh;
#sessionKey;
#name;
#messageCounter;
constructor(name, curve = 'prime256v1') {
this.#name = name;
this.#curve = curve;
this.#ecdh = null;
this.#sessionKey = null;
this.#messageCounter = 0;
}
// Handshake-Payload erzeugen (Public Key + Salt für den Empfänger)
getHandshakePayload() {
this.#ecdh = crypto.createECDH(this.#curve);
this.#ecdh.generateKeys();
return {
from: this.#name,
curve: this.#curve,
publicKey: this.#ecdh.getPublicKey('base64'),
salt: crypto.randomBytes(32).toString('base64')
};
}
// Handshake abschließen und Sitzungsschlüssel ableiten
completeHandshake(peerPayload) {
if (peerPayload.curve !== this.#curve) {
throw new Error(`Kurven-Mismatch: erwartet ${this.#curve}, erhalten ${peerPayload.curve}`);
}
const peerPub = Buffer.from(peerPayload.publicKey, 'base64');
// Public Key der Gegenseite validieren
const tempCheck = crypto.createECDH(this.#curve);
try { tempCheck.setPublicKey(peerPub); }
catch { throw new Error('Ungültiger Public Key der Gegenseite'); }
const rawSecret = this.#ecdh.computeSecret(peerPub);
const salt = Buffer.from(peerPayload.salt, 'base64');
this.#sessionKey = deriveKey(rawSecret, salt, `session-${this.#curve}-v1`, 32);
this.#ecdh = null; // Privaten Schlüssel vernichten (PFS)
this.#messageCounter = 0;
console.log(`[${this.#name}] Sitzungsschlüssel erfolgreich abgeleitet`);
}
// Nachricht verschlüsseln und mit Sequenznummer versehen
send(message) {
if (!this.#sessionKey) throw new Error('Kein Sitzungsschlüssel. completeHandshake() zuerst aufrufen.');
this.#messageCounter++;
const envelope = { seq: this.#messageCounter, msg: message, ts: Date.now() };
const payload = encrypt(this.#sessionKey, JSON.stringify(envelope));
console.log(`[${this.#name}] Nachricht #${this.#messageCounter} verschlüsselt`);
return payload;
}
// Nachricht empfangen und entschlüsseln
receive(encryptedPayload) {
if (!this.#sessionKey) throw new Error('Kein Sitzungsschlüssel.');
const json = decrypt(this.#sessionKey, encryptedPayload);
const envelope = JSON.parse(json);
console.log(`[${this.#name}] Nachricht #${envelope.seq} empfangen: "${envelope.msg}"`);
return envelope;
}
// Sitzung sicher beenden
destroy() {
if (this.#sessionKey) this.#sessionKey.fill(0);
this.#sessionKey = this.#ecdh = null;
this.#messageCounter = 0;
console.log(`[${this.#name}] Sitzung beendet und Schlüssel bereinigt`);
}
}
// ==============================
// Demo
// ==============================
async function main() {
const alice = new SecureMessenger('Alice');
const bob = new SecureMessenger('Bob');
// Schritt 1: Handshake
const alicePayload = alice.getHandshakePayload();
const bobPayload = bob.getHandshakePayload();
alice.completeHandshake(bobPayload);
bob.completeHandshake(alicePayload);
// Schritt 2: Nachrichtenaustausch
const msg1 = alice.send('Hallo Bob! ECDH-Handshake abgeschlossen.');
bob.receive(msg1);
const msg2 = bob.send('Hallo Alice! Ja, die Verschlüsselung funktioniert.');
alice.receive(msg2);
const msg3 = alice.send('Perfekt. Alle Nachrichten sind AES-256-GCM verschlüsselt.');
bob.receive(msg3);
// Schritt 3: Sitzung sauber beenden
alice.destroy();
bob.destroy();
}
main();
Erwartete Ausgabe:
[Alice] Sitzungsschlüssel erfolgreich abgeleitet
[Bob] Sitzungsschlüssel erfolgreich abgeleitet
[Alice] Nachricht #1 verschlüsselt
[Bob] Nachricht #1 empfangen: "Hallo Bob! ECDH-Handshake abgeschlossen."
[Bob] Nachricht #2 verschlüsselt
[Alice] Nachricht #2 empfangen: "Hallo Alice! Ja, die Verschlüsselung funktioniert."
[Alice] Nachricht #3 verschlüsselt
[Bob] Nachricht #3 empfangen: "Perfekt. Alle Nachrichten sind AES-256-GCM verschlüsselt."
[Alice] Sitzung beendet und Schlüssel bereinigt
[Bob] Sitzung beendet und Schlüssel bereinigt
Dieses Beispiel implementiert alle wesentlichen Sicherheitseigenschaften: zufällige ephemere Schlüsselpaare (PFS), Schlüsselableitung via HKDF, authentifizierte Verschlüsselung mit AES-256-GCM, Kurvenmismatch-Schutz, Public-Key-Validierung, Sequenznummern zur Replay-Erkennung und sichere Schlüsselbereinigung am Ende der Sitzung.
ECDH in der Produktionsumgebung: Best Practices für Node.js-Anwendungen
Eine funktionierende ECDH-Implementierung ist der erste Schritt. Für Produktionsumgebungen sind weitere Maßnahmen notwendig, um den Schlüsselaustausch gegen reale Angriffe zu härten.
Schlüsselmaterial sicher im Speicher behandeln
Node.js verwendet einen Garbage Collector, der Objekte im Heap verschieben kann. Das erschwert die sichere Löschung von Schlüsselmaterial. Best-Effort-Maßnahmen umfassen das Überschreiben von Buffern mit buffer.fill(0) nach der Verwendung und das sofortige Null-Setzen von Referenzen. Für Hochsicherheitsanwendungen bietet das Paket sodium-native Nativen-Speicher-Allokation mit garantierter Speicherlöschung.
Schlüsselaustausch gegen Man-in-the-Middle absichern
ECDH allein schützt nur gegen passive Lauschangriffe. Ein aktiver Man-in-the-Middle-Angreifer kann die öffentlichen Schlüssel austauschen und damit die Kommunikation belauschen, ohne dass Alice oder Bob es merken. Gegenmaßnahmen:
- TLS für den Transportkanal: ECDH-Handshake über eine TLS-gesicherte Verbindung führen
- Signatur des Public Keys: Public Key mit einem langfristigen ECDSA- oder Ed25519-Schlüssel signieren
- Pinning: Erwarteten öffentlichen Schlüssel vorab festlegen (z. B. in einem sicheren Kanal)
- Zertifikate: Public Keys in X.509-Zertifikaten einbetten, die von einer CA signiert wurden
Replay-Schutz implementieren
Ohne Replay-Schutz kann ein Angreifer aufgezeichnete verschlüsselte Nachrichten erneut abspielen. Das vollständige Beispiel oben verwendet eine Sequenznummer (seq), die der Empfänger prüfen kann. Für verteilte Systeme eignen sich Nonces (einmalige Zufallswerte) oder Zeitstempel mit Gültigkeitsfenstern.
Schlüsselrotation und Sitzungslimits
Auch mit Perfect Forward Secrecy sollten Sitzungsschlüssel rotiert werden. Empfehlungen:
- Sitzungsschlüssel nach einer bestimmten Anzahl von Nachrichten (z. B. 1.000) erneuern
- Sitzungsschlüssel nach einer Zeitdauer (z. B. 24 Stunden) erneuern
- Nach jedem erkannten Fehler (fehlgeschlagene Tag-Verifikation) sofort neue Sitzung aufbauen
- Private Schlüssel nach Sitzungsende überschreiben (bereits im Beispiel gezeigt)
Logging und Monitoring
Schlüsselmaterial darf niemals in Logs erscheinen. Das umfasst nicht nur den privaten Schlüssel und den Shared Secret, sondern auch den abgeleiteten Schlüssel und den IV. Sichere Logging-Praxis:
// FALSCH: Schlüsselmaterial in Logs
console.log('Shared secret:', rawSecret.toString('hex')); // Kritischer Fehler!
console.log('Session key:', sessionKey.toString('hex')); // Niemals!
// RICHTIG: Nur Metadaten protokollieren
console.log('ECDH-Handshake abgeschlossen, Kurve:', curve, 'Dauer:', duration + 'ms');
console.log('Sitzung ID:', sessionId, 'Schlüssellänge:', sessionKey.length, 'Bytes');
Fehlerbehandlung in Produktionscode
Kryptographische Fehler dürfen keine internen Details preisgeben. Ein Angreifer kann aus Fehlermeldungen Rückschlüsse auf das System ziehen. Allgemeine Regel: Spezifische Fehlerdetails nur in Logs (intern), generische Meldungen an den Client.
// Produktions-Fehlerbehandlung für ECDH
async function handleECDHRequest(peerPublicKey, salt) {
try {
const rawSecret = ecdh.computeSecret(peerPublicKey);
const key = deriveKey(rawSecret, salt, 'context', 32);
return { success: true, key };
} catch (err) {
// Intern: vollständiger Fehler
logger.error({ err, event: 'ecdh_failed' });
// Extern: nur generische Meldung
throw new Error('Schlüsselaustausch fehlgeschlagen');
}
}
Rate Limiting für den Handshake-Endpunkt
ECDH-Handshakes sind rechenintensiv. Ein Angreifer könnte durch viele schnelle Handshake-Anfragen eine DoS-Attacke auf die CPU ausführen. Schutzmaßnahmen: Rate Limiting pro IP-Adresse, Connection Throttling und CPU-Monitoring mit automatischen Alerts. Das Node.js-Paket express-rate-limit oder eine vorgelagerte nginx-Konfiguration eignen sich für die Implementierung.
ECDH und TLS 1.3: Wie Node.js HTTPS intern ECC verwendet
Wenn eine Node.js-Anwendung HTTPS über das tls- oder https-Modul verwendet, läuft ECDH unsichtbar im Hintergrund. Ein Verständnis dieses Ablaufs hilft, TLS-Konfigurationen zu optimieren und Sicherheitsprobleme zu diagnostizieren.
Der TLS 1.3 Handshake im Detail
TLS 1.3 hat den Handshake gegenüber TLS 1.2 auf zwei Schritte reduziert und erzwingt ECDHE:
- ClientHello: Client sendet unterstützte Kurvengruppen (Key Share Extension) inklusive eines X25519- oder P-256-Public-Keys
- ServerHello: Server antwortet mit eigenem Public Key für die ausgehandelte Kurve und berechnet sofort den Shared Secret
- Finished: Beide Seiten leiten den Master Secret via HKDF aus dem Shared Secret ab und verschlüsseln alle weiteren Daten
TLS 1.3 unterstützt folgende ECDH-Gruppen (named_groups): x25519, secp256r1 (P-256), secp384r1 (P-384), secp521r1 (P-521) und x448. Node.js wählt standardmäßig X25519 und P-256 bevorzugt.
TLS-Kurven in Node.js konfigurieren
'use strict';
const https = require('node:https');
const tls = require('node:tls');
const fs = require('node:fs');
// HTTPS-Server mit expliziter Kurvenauswahl
const server = https.createServer({
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
// Kurvenreihenfolge für ECDHE (höhere Priorität zuerst)
ecdhCurve: 'X25519:P-256:P-384',
// Nur TLS 1.3 zulassen
minVersion: 'TLSv1.3',
// Cipher Suites (TLS 1.3 wählt automatisch)
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256'
}, (req, res) => {
res.writeHead(200);
res.end('Secure connection via ECDHE\n');
});
server.listen(443, () => {
console.log('HTTPS-Server läuft auf Port 443');
});
// Verwendete Kurve bei neuer Verbindung anzeigen
server.on('secureConnection', (tlsSocket) => {
console.log('Verbundene Kurve:', tlsSocket.getEphemeralKeyInfo());
// Ausgabe: { type: 'ECDH', name: 'X25519', size: 253 }
});
Mit tlsSocket.getEphemeralKeyInfo() kann die tatsächlich für die Verbindung verwendete Kurve abgerufen werden. Das ist nützlich für Debugging und Sicherheitsmonitoring. In Produktionssystemen sollte diese Information in Metriken fließen, um zu verfolgen, ob Client und Server die erwarteten Kurven aushandeln.
Node.js verwendet OpenSSL intern für alle TLS-Operationen. Die Performance von TLS-Handshakes hängt direkt von der ECDH-Implementierung in OpenSSL ab. Neuere OpenSSL-Versionen (3.x) nutzen hardwarebeschleunigte Montgomery-Multiplikation für X25519 auf x86-64-Prozessoren, was die Handshake-Zeit auf modernen Servern unter 0,1 ms senkt.
Sicherheitsaudit einer TLS-Konfiguration
Die Sicherheit der TLS-Konfiguration kann mit openssl s_client oder dem Online-Tool SSL Labs überprüft werden:
# TLS-Verbindung testen und verwendete Kurve anzeigen
openssl s_client -connect example.com:443 -tls1_3 -curves x25519 2>/dev/null | grep -E "Protocol|Curve"
# Erwartete Ausgabe:
# Protocol : TLSv1.3
# Server Temp Key: X25519, 253 bits
# Alle unterstützten Kurven eines Servers ermitteln
openssl s_client -connect example.com:443 -curves "secp256r1" 2>&1 | grep "Server Temp Key"
openssl s_client -connect example.com:443 -curves "secp384r1" 2>&1 | grep "Server Temp Key"
Advanced Tips: ECDH in verteilten Systemen und Microservices
Für komplexere Architekturen ergeben sich spezifische Anforderungen an ECDH-Implementierungen.
Schlüsselaustausch zwischen Microservices
In Microservice-Architekturen kommunizieren viele Services miteinander. Für jeden Service-to-Service-Kanal gelten dieselben ECDH-Prinzipien wie für Client-Server-Kommunikation. Praktische Empfehlungen:
- mTLS (mutual TLS) für Service-to-Service-Kommunikation: Beide Seiten authentifizieren sich mit Zertifikaten, ECDHE übernimmt den Schlüsselaustausch automatisch
- Service Mesh (Istio, Linkerd): Fügt mTLS transparenz hinzu, ohne dass jeder Service es selbst implementieren muss
- Vault (HashiCorp) für Zertifikatsmanagement: Kurzlebige Zertifikate (TTL wenige Stunden) für Services
ECDH in WebSockets
WebSocket-Verbindungen werden üblicherweise über TLS gesichert (WSS), das ECDHE automatisch enthält. Für eine zusätzliche Ende-zu-Ende-Verschlüsselung auf Anwendungsebene (z. B. wenn der Server nicht vollständig vertrauenswürdig ist) kann ECDH explizit implementiert werden:
'use strict';
const crypto = require('node:crypto');
// Client-seitiger ECDH-Handshake über WebSocket
class E2EWebSocketClient {
constructor(ws) {
this.ws = ws;
this.ecdh = crypto.createECDH('prime256v1');
this.ecdh.generateKeys();
this.key = null;
}
initiate() {
// Öffentlichen Schlüssel und Salt an Server senden
const salt = crypto.randomBytes(32);
this._salt = salt;
this.ws.send(JSON.stringify({
type: 'ecdh-init',
publicKey: this.ecdh.getPublicKey('base64'),
salt: salt.toString('base64')
}));
}
handleServerKey(serverPublicKeyBase64) {
const serverPub = Buffer.from(serverPublicKeyBase64, 'base64');
const rawSecret = this.ecdh.computeSecret(serverPub);
this.key = Buffer.from(
crypto.hkdfSync('sha256', rawSecret, this._salt, Buffer.from('ws-e2e-v1'), 32)
);
this.ecdh = null; // PFS
console.log('E2E-Schlüssel abgeleitet');
}
sendEncrypted(message) {
if (!this.key) throw new Error('Kein Schlüssel');
const iv = crypto.randomBytes(12);
const c = crypto.createCipheriv('aes-256-gcm', this.key, iv);
const ct = Buffer.concat([c.update(Buffer.from(message)), c.final()]);
this.ws.send(JSON.stringify({
type: 'encrypted',
iv: iv.toString('base64'),
ct: ct.toString('base64'),
tag: c.getAuthTag().toString('base64')
}));
}
}
ECDH mit Node.js Worker Threads für hohen Durchsatz
ECDH-Schlüsselgenerierung und Shared-Secret-Berechnung sind CPU-gebundene Operationen. In Node.js blockieren sie den Event Loop kurz. Bei sehr hohem Durchsatz (tausende Handshakes pro Sekunde) empfiehlt sich die Auslagerung in Worker Threads:
// worker.js
const { parentPort } = require('node:worker_threads');
const crypto = require('node:crypto');
parentPort.on('message', ({ peerPublicKey, curve }) => {
const ecdh = crypto.createECDH(curve);
ecdh.generateKeys();
const rawSecret = ecdh.computeSecret(Buffer.from(peerPublicKey, 'base64'));
const salt = crypto.randomBytes(32);
const key = Buffer.from(
crypto.hkdfSync('sha256', rawSecret, salt, Buffer.from('worker-key-v1'), 32)
);
parentPort.postMessage({
publicKey: ecdh.getPublicKey('base64'),
salt: salt.toString('base64'),
// key wird NUR intern verwendet, niemals übertragen!
});
});
Ein Worker-Thread-Pool mit 4-8 Workern kann auf einem modernen Server typisch 10.000 bis 50.000 ECDH-Handshakes pro Sekunde mit P-256 durchführen. Mit X25519 steigt dieser Wert nochmals, da die Montgomery-Ladder-Implementierung hardware-optimierter ist.
ECDH und Schlüsseldiversifikation
In Protokollen wie Signal oder Noise Protocol Framework wird ECDH mehrfach kombiniert, um sowohl Authentifizierung als auch Vertraulichkeit zu erreichen. Das X3DH-Protokoll (Extended Triple Diffie-Hellman) verwendet vier ECDH-Operationen:
- DH1: Sender-Identity-Key + Empfänger-Signed-Prekey
- DH2: Sender-Ephemeral-Key + Empfänger-Identity-Key
- DH3: Sender-Ephemeral-Key + Empfänger-Signed-Prekey
- DH4: Sender-Ephemeral-Key + Empfänger-One-Time-Prekey (optional)
Die vier ECDH-Outputs werden via HKDF kombiniert, was sowohl Perfect Forward Secrecy als auch wechselseitige Authentifizierung ohne zentralisierte PKI ermöglicht. Das ist das Modell hinter WhatsApp, Signal und Facebook Messenger End-to-End-Verschlüsselung.
ECDH in der Post-Quantum-Ära: Hybride Verfahren und Migration
ECDH bleibt bis auf Weiteres der Industriestandard für den Schlüsselaustausch, aber die Vorbereitung auf Quantencomputer sollte bereits jetzt beginnen. NIST veröffentlichte im August 2024 die finalen Post-Quantum-Standards FIPS 203 (ML-KEM), FIPS 204 (ML-DSA) und FIPS 205 (SLH-DSA). Im März 2025 wählte NIST zusätzlich HQC als Backup für ML-KEM aus, mit einem Entwurfsstandard für 2026 und Finalisierung 2027.
Die empfohlene Migrationsstrategie ist ein hybrides Verfahren: ECDH und ML-KEM parallel ausführen, beide Shared Secrets kombinieren, und das kombinierte Material via HKDF zu einem einzigen Schlüssel ableiten. So ist die Sicherheit gewährleistet, solange mindestens eines der beiden Verfahren nicht gebrochen ist:
'use strict';
const crypto = require('node:crypto');
/**
* Konzeptuelle Darstellung eines hybriden Key-Exchange (ECDH + PQ).
* In der Praxis würde ML-KEM über eine Bibliothek wie @noble/post-quantum eingebunden.
* Hier mit einem Platzhalter für den ML-KEM-Output demonstriert.
*/
function hybridKeyExchange(ecdhSecret, mlkemSecret, salt) {
// Beide Secrets kombinieren: Konkatenation, dann HKDF
const combined = Buffer.concat([ecdhSecret, mlkemSecret]);
return Buffer.from(
crypto.hkdfSync('sha256', combined, salt, Buffer.from('hybrid-ecdh-mlkem-v1'), 32)
);
}
// ECDH-Teil (klassisch)
const alice = crypto.createECDH('prime256v1');
const bob = crypto.createECDH('prime256v1');
alice.generateKeys();
bob.generateKeys();
const ecdhSecret = alice.computeSecret(bob.getPublicKey());
// ML-KEM-Teil (in diesem Beispiel simuliert durch einen Zufallswert)
// In der Produktion: const { mlkemSharedSecret } = await mlkemDecapsulate(mlkemCiphertext, mlkemPrivKey);
const mlkemSecret = crypto.randomBytes(32); // Platzhalter
const salt = crypto.randomBytes(32);
const hybridKey = hybridKeyExchange(ecdhSecret, mlkemSecret, salt);
console.log('Hybrider Schlüssel (ECDH + ML-KEM):', hybridKey.toString('hex'));
console.log('Sicher gegen klassische UND Quantenangriffe (bei korrekter ML-KEM-Integration)');
Chrome, Firefox und Cloudflare aktivieren hybrides ECDH plus ML-KEM-768 bereits standardmäßig in TLS 1.3 Verbindungen (als Named Group X25519MLKEM768). Das zeigt, dass die Migration nicht abrupt erfolgen muss, sondern inkrementell: bestehende ECDH-Implementierungen können um einen ML-KEM-Layer ergänzt werden, ohne das gesamte Protokoll zu ersetzen.
Für Node.js-Anwendungen, die heute entwickelt werden, empfiehlt sich folgende Roadmap: Implementierung mit ECDH (X25519 oder P-256) als primäres Verfahren, Protokoll-Architektur so gestalten, dass der Key-Exchange-Mechanismus austauschbar ist (abstrakte Interface), und sobald stabile ML-KEM-Bibliotheken für Node.js verfügbar sind (erwartet 2025-2026), hybrides Verfahren aktivieren. Die Schlüsselgrößen von ML-KEM-768 (öffentlicher Schlüssel: 1.184 Bytes, Ciphertext: 1.088 Bytes) sind zwar größer als ECC, aber durch Hardwareoptimierungen und moderne Prozessoren in der Leistung vergleichbar mit ECDH.
Die Kombination von solidem ECDH-Wissen und der Bereitschaft zur Post-Quantum-Migration ist 2026 der Standard für sicherheitsbewusste Node.js-Entwickler im DACH-Raum. Die in diesem Tutorial gezeigten Muster (ephemere Schlüssel, HKDF, AES-256-GCM, Public-Key-Validierung) bleiben auch im hybriden Zeitalter gültig, da die Struktur des Schlüsselaustauschs dieselbe bleibt, nur der Algorithmus für einen der ECDH-Partner durch ML-KEM ersetzt wird.




