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.

WerkzeugMindestversionEmpfohlene VersionZweck
Node.js18.0.022.x LTSLaufzeit mit nativem ECC-Support
npm9.0.010.xPaketverwaltung
OpenSSL3.03.3Hintergrund-Bibliothek (Teil von Node.js)
TypeScript (optional)5.05.4Typsicherheit
OSUbuntu 22.04 / macOS 12 / Windows 10Ubuntu 24.04Alle 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

KurvennameAliasSicherheitSchlüssellängeTypischer Einsatz
prime256v1P-256, secp256r1128 Bit256 BitTLS, JWT ES256, FIDO2/WebAuthn
secp384r1P-384192 Bit384 BitNSA Suite B, Behörden
secp521r1P-521256 Bit521 BitHöchste Sicherheitsanforderungen
secp256k1K-256128 Bit256 BitBitcoin, Ethereum
ed25519Edwards-25519128 Bit256 BitSSH, GPG, Signal-Protokoll
ed448Edwards-448224 Bit448 BitLangzeitsicherheit, 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:

  1. Alice generiert Schlüsselpaar (privA, pubA), Bob generiert (privB, pubB).
  2. Alice sendet pubA an Bob, Bob sendet pubB an Alice.
  3. Alice berechnet sharedSecret = ECDH(privA, pubB).
  4. 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

KriteriumECDSA (P-256)Ed25519
Signaturlänge70-72 Bytes (DER, variabel)64 Bytes (fest)
DeterministischNein (benötigt CSPRNG)Ja (RFC 8032)
Timing-ResistenzImplementierungsabhängigBy Design
Sign-Performance~0.08ms (P-256, i7-12700)~0.045ms
Verify-Performance~0.15ms~0.10ms
JWT-AlgorithmusES256 (RFC 7518)EdDSA (RFC 8037)
FIPS 140-2/3JaAb FIPS 140-3
Typischer EinsatzTLS-Zertifikate, ECDSA-JWTSSH, 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

FehlermeldungUrsacheLösung
ERR_CRYPTO_UNKNOWN_CIPHERAlgorithmusname falschcrypto.getCiphers() zur Überprüfung nutzen
error:0A00018E: point is not on curveKurven-Mismatch bei ECDHKurven beider Parteien vor ECDH validieren
ERR_OSSL_PEM_NO_START_LINEPEM-Datei korrumpiert oder Binary als Text gelesenDatei explizit als UTF-8 laden
digest too big for key sizeFalscher 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: OKPEd25519 in Node.js unter Version 15Node.js 18 LTS oder neuer verwenden
ERR_CRYPTO_INVALID_KEY_OBJECT_TYPEÖffentlichen Schlüssel für sign() verwendetPrivaten Schlüssel für sign(), öffentlichen für verify()
Decryption failed (AES-GCM)AuthTag falsch, Daten manipuliert oder IV wiederverwendetAuthTag korrekt übertragen, IV immer zufällig
ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEYEmpfangener Public Key nicht auf der KurveSchlü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üsselECC-SchlüsselGrößenvorteilTLS-Byte-Einsparung
80 Bit (veraltet)1024 Bit160 Bit6,4x kleiner(unsicher)
112 Bit2048 Bit224 Bit9,1x kleiner~180 Bytes
128 Bit (Standard)3072 Bit256 Bit12x kleiner~250 Bytes
192 Bit7680 Bit384 Bit20x kleiner~700 Bytes
256 Bit15360 Bit521 Bit29x 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.

KurveSafeCurves-ScoreFIPS-zugelassenPatent-freiEmpfehlung
Curve25519 / X2551911/11Ab FIPS 186-5 (2023)JaBeste Wahl für Schlüsselaustausch
Ed2551911/11Ab FIPS 186-5JaBeste Wahl für Signaturen
P-256 (prime256v1)7/11Ja (FIPS 186-5)JaTLS, FIDO2, JWT, FIPS-Pflicht
P-3847/11JaJaNSA Suite B, österr./dt. Behörden
P-5217/11JaJaHöchste Sicherheitsanforderungen
secp256k19/11NeinJaNur für Blockchain-Anwendungen

Weiterführende Artikel

Elliptic Curve Cryptography ist ein Baustein in einem größeren Sicherheitssystem. Diese Artikel vertiefen die verwandten Themen:

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.