Eine digitale Signatur beweist zwei Dinge auf einmal: dass eine Nachricht wirklich von Ihnen stammt und dass niemand sie unterwegs verändert hat. Genau das brauchen Sie für Webhooks, Software-Updates, API-Anfragen oder signierte Dokumente. In diesem Tutorial bauen Sie in 11 Schritten ein vollständiges Signatur-Werkzeug in Node.js, ganz ohne externe Bibliothek. Sie nutzen nur das eingebaute crypto-Modul und lernen Ed25519, ECDSA und RSA-PSS im praktischen Einsatz kennen. Rechnen Sie mit rund 40 Minuten bis zum lauffähigen CLI-Tool.
Das Ergebnis ist ein Kommandozeilen-Programm, das Dateien signiert, Signaturen prüft und Schlüsselpaare sicher verwaltet. Jeder Schritt enthält lauffähigen Code, eine erwartete Ausgabe und einen Hinweis, was schiefgehen kann. Am Ende finden Sie eine Vergleichstabelle der Algorithmen, acht Troubleshooting-Fälle und einen Abschnitt zur rechtlichen Einordnung nach eIDAS und ID Austria.
Was eine digitale Signatur technisch leistet
Eine digitale Signatur kombiniert zwei kryptografische Bausteine: eine Hashfunktion und ein asymmetrisches Schlüsselpaar. Zuerst berechnet die Hashfunktion (etwa SHA-256) einen festen Fingerabdruck der Nachricht. Danach verschlüsselt der private Schlüssel diesen Fingerabdruck. Das Ergebnis ist die Signatur. Wer Ihren öffentlichen Schlüssel besitzt, kann die Rechnung umkehren und prüfen, ob Fingerabdruck und Nachricht zusammenpassen.
Verwechseln Sie das nicht mit Verschlüsselung. Verschlüsselung schützt die Vertraulichkeit, also den Inhalt. Eine Signatur schützt Echtheit (Authentizität) und Unversehrtheit (Integrität), gibt den Inhalt aber im Klartext frei. Ein drittes Merkmal kommt dazu: die Nichtabstreitbarkeit. Weil nur Sie den privaten Schlüssel besitzen, können Sie eine gültige Signatur später nicht glaubhaft leugnen.
Node.js stellt dafür im crypto-Modul alles bereit. Die direkten Einzelaufrufe heißen crypto.sign() und crypto.verify(). Für große Datenmengen gibt es die stream-orientierten Objekte crypto.createSign() und crypto.createVerify(). Schlüsselpaare erzeugen Sie mit crypto.generateKeyPairSync(). Diese vier Funktionen tragen das gesamte Tutorial.
Der Ablauf einer Signaturprüfung in fünf Schritten
Damit Sie die spätere Code-Logik gedanklich einordnen können, hier der vollständige Ablauf. Auf der Senderseite passieren drei Dinge: Erstens berechnet die Hashfunktion einen Digest der Nachricht. Zweitens wandelt der private Schlüssel diesen Digest in eine Signatur um. Drittens verschickt der Sender Nachricht und Signatur gemeinsam, oft als getrennte Datei mit der Endung .sig oder als HTTP-Header.
Auf der Empfängerseite folgen zwei weitere Schritte: Viertens berechnet der Empfänger denselben Hash über die empfangene Nachricht. Fünftens prüft crypto.verify() mit dem öffentlichen Schlüssel, ob die mitgelieferte Signatur zu diesem Hash passt. Stimmt etwas nicht, weil ein Byte verändert wurde oder ein falscher Schlüssel im Spiel ist, liefert die Funktion false. Dieser Mechanismus ist die Grundlage jeder vertrauenswürdigen Software-Auslieferung, jedes signierten Tokens und jeder geprüften API-Anfrage.
Wichtig: Eine Signatur garantiert nicht, dass der öffentliche Schlüssel wirklich der erwarteten Person gehört. Dafür sorgt eine zusätzliche Vertrauensschicht, etwa ein Zertifikat einer Zertifizierungsstelle oder ein fest hinterlegter Schlüssel (Key Pinning). In diesem Tutorial tauschen Sie die Schlüssel direkt aus, was für interne Systeme völlig ausreicht.
Voraussetzungen und Versionen
Sie brauchen keine externen npm-Pakete. Das gesamte Projekt läuft auf dem Standard-Funktionsumfang von Node.js. Prüfen Sie vor dem Start diese Versionen:
| Komponente | Empfohlene Version | Prüfbefehl |
|---|---|---|
| Node.js | 24.x LTS (Codename “Krypton”) | node -v |
| npm | 11.x (mit Node 24 gebündelt) | npm -v |
| OpenSSL (in Node integriert) | 3.x | node -p "process.versions.openssl" |
| Editor | VS Code oder beliebig | – |
| Betriebssystem | Windows, macOS, Linux | – |
Node.js 24 ist im Juni 2026 die aktive LTS-Linie und damit die richtige Wahl für neue Projekte. Wer noch auf der älteren LTS-Linie 22 (“Jod”) sitzt, kann diesem Tutorial trotzdem folgen, denn die hier genutzten Signatur-APIs sind seit Jahren stabil. Vom aktuellen Release 26 (“Current”) raten wir in der Produktion ab, solange es nicht in LTS überführt wurde.
Grundkenntnisse in JavaScript und der Kommandozeile reichen aus. Wenn Sie das Konzept hinter Fingerabdrücken vertiefen möchten, lesen Sie vorab unsere Erklärung zu digitalen Signaturen und zu SHA-256.
Schritt 1: Projekt anlegen
Legen Sie einen Projektordner an und initialisieren Sie ihn. Wir setzen ES-Module ein, daher kommt "type": "module" in die package.json.
mkdir signatur-tool && cd signatur-tool
npm init -y
npm pkg set type="module"
mkdir keys docs
node -v
Erwartete Ausgabe der letzten Zeile:
v24.4.1
Der Ordner keys nimmt später die Schlüsseldateien auf, docs die zu signierenden Beispieldateien. Legen Sie sofort eine .gitignore an, damit private Schlüssel niemals im Repository landen:
# .gitignore
node_modules/
keys/*.pem
*.sig
.env
Schritt 2: Ed25519-Schlüsselpaar erzeugen
Wir beginnen mit Ed25519, dem modernen Standard für Signaturen. Erstellen Sie keygen.js:
// keygen.js
import { generateKeyPairSync } from 'node:crypto';
import { writeFileSync } from 'node:fs';
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
const pubPem = publicKey.export({ type: 'spki', format: 'pem' });
const privPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
writeFileSync('keys/ed25519_public.pem', pubPem);
writeFileSync('keys/ed25519_private.pem', privPem, { mode: 0o600 });
console.log('Schlüsselpaar erstellt:');
console.log(' Öffentlich:', 'keys/ed25519_public.pem');
console.log(' Privat: ', 'keys/ed25519_private.pem (Rechte 600)');
Starten Sie das Skript:
node keygen.js
Erwartete Ausgabe:
Schlüsselpaar erstellt:
Öffentlich: keys/ed25519_public.pem
Privat: keys/ed25519_private.pem (Rechte 600)
Der private Schlüssel beruht bei Ed25519 auf nur 32 Byte Schlüsselmaterial, das macht das Verfahren so leichtgewichtig. Die Dateirechte 0o600 bedeuten: nur der Eigentümer darf lesen und schreiben. Auf Windows greift diese Einschränkung nicht, dort schützen Sie den Ordner über die NTFS-Berechtigungen.
Schritt 3: Die erste digitale Signatur erstellen
Jetzt signieren wir eine Nachricht. Für Ed25519 gilt eine Besonderheit: Das Algorithmus-Argument bei crypto.sign() muss null sein, weil der Hash fest zum Verfahren gehört. Erstellen Sie sign-basic.js:
// sign-basic.js
import { sign, createPrivateKey } from 'node:crypto';
import { readFileSync } from 'node:fs';
const privateKey = createPrivateKey(readFileSync('keys/ed25519_private.pem'));
const message = Buffer.from('Überweisung 250 EUR an Konto AT61 1904 3002 3457 3201');
// Bei Ed25519 ist der erste Parameter null (Hash ist im Verfahren festgelegt)
const signature = sign(null, message, privateKey);
console.log('Nachricht :', message.toString());
console.log('Signatur :', signature.toString('base64'));
console.log('Länge :', signature.length, 'Byte');
Ausführen:
node sign-basic.js
Erwartete Ausgabe (die Signatur unterscheidet sich pro Schlüssel):
Nachricht : Überweisung 250 EUR an Konto AT61 1904 3002 3457 3201
Signatur : 9Qm2k0Vd...c7Lp4w==
Länge : 64 Byte
64 Byte ist die feste Signaturlänge bei Ed25519, unabhängig von der Größe der Nachricht. Weil Ed25519 deterministisch arbeitet, liefert dieselbe Nachricht mit demselben Schlüssel immer dieselbe Signatur. Das vereinfacht Tests und schließt eine ganze Klasse von Nonce-Fehlern aus, die ECDSA plagen.
Schritt 4: Eine Signatur verifizieren
Die Prüfung übernimmt crypto.verify(). Sie gibt einen booleschen Wert zurück. Erstellen Sie verify-basic.js:
// verify-basic.js
import { sign, verify, createPrivateKey, createPublicKey } from 'node:crypto';
import { readFileSync } from 'node:fs';
const privateKey = createPrivateKey(readFileSync('keys/ed25519_private.pem'));
const publicKey = createPublicKey(readFileSync('keys/ed25519_public.pem'));
const message = Buffer.from('Überweisung 250 EUR an Konto AT61 1904 3002 3457 3201');
const signature = sign(null, message, privateKey);
const echt = verify(null, message, publicKey, signature);
console.log('Originalnachricht gültig:', echt);
// Manipulierte Nachricht: ein Zeichen geändert
const gefaelscht = Buffer.from('Überweisung 950 EUR an Konto AT61 1904 3002 3457 3201');
const echt2 = verify(null, gefaelscht, publicKey, signature);
console.log('Manipulierte Nachricht gültig:', echt2);
Erwartete Ausgabe:
Originalnachricht gültig: true
Manipulierte Nachricht gültig: false
Genau das ist der Kern jeder digitale Signatur: Ändert ein Angreifer auch nur einen Betrag von 250 auf 950, fällt die Prüfung sofort auf false. Der öffentliche Schlüssel reicht für die Prüfung, der private bleibt geheim beim Unterzeichner.
Schritt 5: ECDSA mit der Kurve P-256 nutzen
Viele bestehende Systeme verlangen ECDSA. Node unterstützt die gängigen Kurven prime256v1 (auch P-256 genannt), secp384r1 (P-384) und secp256k1 (die Bitcoin-Kurve). Bei ECDSA müssen Sie den Hash explizit angeben und das Signaturformat festlegen. Erstellen Sie ecdsa.js:
// ecdsa.js
import { generateKeyPairSync, sign, verify } from 'node:crypto';
const { publicKey, privateKey } = generateKeyPairSync('ec', {
namedCurve: 'prime256v1', // P-256
});
const message = Buffer.from('Vertrag #2026-0815 freigegeben');
// dsaEncoding 'ieee-p1363' liefert feste Länge (64 Byte bei P-256)
const sigP1363 = sign('sha256', message, {
key: privateKey,
dsaEncoding: 'ieee-p1363',
});
const sigDer = sign('sha256', message, {
key: privateKey,
dsaEncoding: 'der',
});
console.log('P1363-Länge:', sigP1363.length, 'Byte (fest)');
console.log('DER-Länge :', sigDer.length, 'Byte (variabel)');
const ok = verify('sha256', message, {
key: publicKey,
dsaEncoding: 'ieee-p1363',
}, sigP1363);
console.log('ECDSA-Prüfung:', ok);
Erwartete Ausgabe:
P1363-Länge: 64 Byte (fest)
DER-Länge : 70 Byte (variabel)
ECDSA-Prüfung: true
Der Unterschied zwischen DER und IEEE-P1363 ist eine häufige Fehlerquelle. DER ist die ASN.1-codierte Form mit variabler Länge (meist 70 bis 72 Byte bei P-256). IEEE-P1363 ist die rohe Aneinanderreihung von r und s mit fester Länge (64 Byte). Webstandards wie JOSE und JWT erwarten P1363, viele X.509-Werkzeuge erwarten DER. Wer das Format verwechselt, bekommt eine gültige Signatur, die beim Gegenüber trotzdem als ungültig durchfällt.
Schritt 6: RSA-PSS-Signaturen erzeugen
RSA bleibt für die Interoperabilität mit älteren Systemen wichtig. Für neue Entwürfe ist RSA-PSS dem klassischen PKCS#1 v1.5 vorzuziehen, weil es probabilistisch arbeitet und besser gegen bestimmte Angriffe gewappnet ist. Erstellen Sie rsa-pss.js:
// rsa-pss.js
import { generateKeyPairSync, sign, verify, constants } from 'node:crypto';
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 3072, // 3072 Bit gilt 2026 als solides Minimum
});
const message = Buffer.from('Software-Update v2.4.0 freigegeben');
const options = {
key: privateKey,
padding: constants.RSA_PKCS1_PSS_PADDING,
saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
};
const signature = sign('sha256', message, options);
console.log('RSA-PSS-Signaturlänge:', signature.length, 'Byte');
const ok = verify('sha256', message, {
key: publicKey,
padding: constants.RSA_PKCS1_PSS_PADDING,
saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
}, signature);
console.log('RSA-PSS-Prüfung:', ok);
Erwartete Ausgabe:
RSA-PSS-Signaturlänge: 384 Byte
RSA-PSS-Prüfung: true
Die Signaturlänge entspricht der Modulusgröße: 3072 Bit ergeben 384 Byte. Das ist sechsmal so groß wie eine Ed25519-Signatur. Achten Sie darauf, dieselben Optionen (Padding und Salt-Länge) beim Signieren und Prüfen zu verwenden, sonst schlägt die Prüfung fehl.
Schritt 7: Große Dateien per Stream signieren
Eine ganze Datei in den Speicher zu laden ist bei mehreren Gigabyte keine gute Idee. Für ECDSA und RSA nutzen Sie crypto.createSign() und füttern den Stream häppchenweise. Erstellen Sie zuerst eine Beispieldatei und dann sign-file.js:
node -e "require('fs').writeFileSync('docs/vertrag.pdf','%PDF-1.7 Beispielinhalt fuer das Tutorial')"
// sign-file.js
import { createSign, createPrivateKey } from 'node:crypto';
import { createReadStream, readFileSync, writeFileSync } from 'node:fs';
const privateKey = createPrivateKey({
key: readFileSync('keys/rsa_private.pem'),
passphrase: process.env.KEY_PASSPHRASE,
});
const signer = createSign('sha256');
const stream = createReadStream('docs/vertrag.pdf');
stream.on('data', (chunk) => signer.update(chunk));
stream.on('end', () => {
signer.end();
const signature = signer.sign(privateKey);
writeFileSync('docs/vertrag.pdf.sig', signature);
console.log('Datei signiert ->', 'docs/vertrag.pdf.sig');
console.log('Signaturgröße :', signature.length, 'Byte');
});
Hinweis: Ed25519 lässt sich in Node nicht über createSign() streamen, weil das Verfahren die gesamte Nachricht auf einmal braucht. Für Ed25519-Dateien laden Sie den Inhalt entweder ganz oder bilden zuerst einen SHA-512-Hash und signieren diesen Digest. Für klassisches Streaming sind ECDSA und RSA die richtige Wahl.
Schritt 8: Private Schlüssel mit Passphrase schützen
Ein privater Schlüssel im Klartext auf der Festplatte ist ein Risiko. Verschlüsseln Sie ihn beim Export mit einer Passphrase. Das Beispiel zeigt es für ein RSA-Paar, das wir für Schritt 7 ohnehin brauchen. Erstellen Sie keygen-rsa.js:
// keygen-rsa.js
import { generateKeyPairSync } from 'node:crypto';
import { writeFileSync } from 'node:fs';
const passphrase = process.env.KEY_PASSPHRASE || 'aendere-mich-sofort';
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 3072,
});
writeFileSync('keys/rsa_public.pem',
publicKey.export({ type: 'spki', format: 'pem' }));
writeFileSync('keys/rsa_private.pem',
privateKey.export({
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase,
}), { mode: 0o600 });
console.log('RSA-Schlüssel erstellt, privater Teil mit AES-256 verschlüsselt.');
Setzen Sie die Passphrase über eine Umgebungsvariable, niemals im Code:
KEY_PASSPHRASE='Ein-langes-Geheimnis-2026!' node keygen-rsa.js
Beim Laden eines verschlüsselten Schlüssels übergeben Sie die Passphrase an createPrivateKey, wie schon im Skript aus Schritt 7 gezeigt. So bleibt der Schlüssel selbst dann nutzlos, wenn die PEM-Datei in falsche Hände gerät. In produktiven Systemen gehört das Geheimnis in einen Tresor wie HashiCorp Vault oder den Secrets-Manager Ihres Cloud-Anbieters, nicht in eine Datei neben den Code.
Schritt 9: Konstante Laufzeit mit timingSafeEqual
Wenn Sie Signaturen oder HMAC-Werte selbst vergleichen, etwa bei der Prüfung von Webhook-Signaturen, dürfen Sie niemals den Operator === verwenden. Ein naiver Vergleich bricht beim ersten unterschiedlichen Byte ab und verrät über die Laufzeit, wie viele Stellen korrekt waren. Angreifer nutzen das aus. Die Lösung heißt crypto.timingSafeEqual():
// webhook-verify.js
import { createHmac, timingSafeEqual } from 'node:crypto';
function pruefeWebhook(rohBody, signaturHeader, secret) {
const erwartet = createHmac('sha256', secret)
.update(rohBody)
.digest();
const empfangen = Buffer.from(signaturHeader, 'hex');
// Längen müssen vor timingSafeEqual gleich sein
if (erwartet.length !== empfangen.length) return false;
return timingSafeEqual(erwartet, empfangen);
}
const secret = 'webhook-geheimnis';
const body = '{"event":"zahlung","betrag":250}';
const gueltigeSig = createHmac('sha256', secret).update(body).digest('hex');
console.log('Gültig:', pruefeWebhook(body, gueltigeSig, secret));
console.log('Gefälscht:', pruefeWebhook(body, 'deadbeef'.repeat(8), secret));
Erwartete Ausgabe:
Gültig: true
Gefälscht: false
Prüfen Sie die Länge vor dem Aufruf, denn timingSafeEqual wirft eine Ausnahme, wenn die Puffer unterschiedlich lang sind. Bei den eingebauten Funktionen crypto.verify() und verifier.verify() übernimmt Node den sicheren Vergleich bereits intern.
Schritt 10: Das vollständige CLI-Werkzeug bauen
Jetzt fügen wir alles zu einem benutzbaren Kommandozeilen-Tool zusammen. Es unterstützt drei Befehle: keygen, sign und verify. Erstellen Sie cli.js:
// cli.js
import {
generateKeyPairSync, sign, verify,
createPrivateKey, createPublicKey,
} from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';
const [,, befehl, ...args] = process.argv;
function keygen() {
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
writeFileSync('keys/ed25519_public.pem',
publicKey.export({ type: 'spki', format: 'pem' }));
writeFileSync('keys/ed25519_private.pem',
privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });
console.log('OK: Ed25519-Schlüsselpaar in keys/ erstellt.');
}
function signieren(datei) {
const key = createPrivateKey(readFileSync('keys/ed25519_private.pem'));
const inhalt = readFileSync(datei);
const signatur = sign(null, inhalt, key);
writeFileSync(datei + '.sig', signatur.toString('base64'));
console.log(`OK: ${datei} signiert -> ${datei}.sig`);
}
function pruefen(datei) {
const key = createPublicKey(readFileSync('keys/ed25519_public.pem'));
const inhalt = readFileSync(datei);
const signatur = Buffer.from(readFileSync(datei + '.sig', 'utf8'), 'base64');
const ok = verify(null, inhalt, key, signatur);
console.log(ok ? 'GÜLTIG: Signatur korrekt.' : 'UNGÜLTIG: Signatur passt nicht.');
process.exit(ok ? 0 : 1);
}
switch (befehl) {
case 'keygen': keygen(); break;
case 'sign': signieren(args[0]); break;
case 'verify': pruefen(args[0]); break;
default:
console.log('Nutzung: node cli.js [keygen | sign <datei> | verify <datei>]');
}
Probieren Sie den kompletten Ablauf aus:
node cli.js keygen
echo "Rechnung 2026-0815: 1.480,00 EUR" > docs/rechnung.txt
node cli.js sign docs/rechnung.txt
node cli.js verify docs/rechnung.txt
Erwartete Ausgabe:
OK: Ed25519-Schlüsselpaar in keys/ erstellt.
OK: docs/rechnung.txt signiert -> docs/rechnung.txt.sig
GÜLTIG: Signatur korrekt.
Ändern Sie jetzt ein Zeichen in docs/rechnung.txt und prüfen Sie erneut. Das Tool meldet “UNGÜLTIG” und beendet sich mit Exit-Code 1, was sich in CI-Pipelines auswerten lässt.
Schritt 11: Automatisierte Tests schreiben
Node 24 bringt einen eingebauten Test-Runner mit, kein zusätzliches Paket nötig. Erstellen Sie test/signatur.test.js:
// test/signatur.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { generateKeyPairSync, sign, verify } from 'node:crypto';
test('gültige Signatur wird akzeptiert', () => {
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
const msg = Buffer.from('Testnachricht');
const sig = sign(null, msg, privateKey);
assert.equal(verify(null, msg, publicKey, sig), true);
});
test('manipulierte Nachricht wird abgelehnt', () => {
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
const sig = sign(null, Buffer.from('Original'), privateKey);
assert.equal(verify(null, Buffer.from('Gefälscht'), publicKey, sig), false);
});
test('fremder Schlüssel wird abgelehnt', () => {
const a = generateKeyPairSync('ed25519');
const b = generateKeyPairSync('ed25519');
const msg = Buffer.from('Nachricht');
const sig = sign(null, msg, a.privateKey);
assert.equal(verify(null, msg, b.publicKey, sig), false);
});
Starten Sie die Tests:
node --test
Erwartete Ausgabe (gekürzt):
# tests 3
# pass 3
# fail 0
Damit ist das Projekt komplett: Schlüsselverwaltung, Signieren, Prüfen und drei grüne Tests. Wer das Konzept hinter dem Vergleich vertiefen will, findet bei unseren Hashfunktionen die Grundlagen.
Praxis: Signaturprüfung in einer Express-API
Das CLI-Tool zeigt das Prinzip. In der Praxis prüfen Sie Signaturen meist serverseitig, etwa wenn ein Zahlungsdienstleister oder ein Partner-System per Webhook eine signierte Anfrage schickt. Hier sehen Sie, wie Sie eine eingehende Anfrage in Express absichern. Der entscheidende Punkt: Sie müssen den rohen Body lesen, bevor ein JSON-Parser ihn anfasst, sonst verändert sich die Byte-Folge und die digitale Signatur passt nicht mehr.
// server.js (npm install express)
import express from 'express';
import { verify, createPublicKey } from 'node:crypto';
import { readFileSync } from 'node:fs';
const app = express();
const publicKey = createPublicKey(readFileSync('keys/ed25519_public.pem'));
// Rohen Body als Buffer bewahren, NICHT vorher JSON-parsen
app.use(express.raw({ type: '*/*' }));
app.post('/webhook', (req, res) => {
const signatur = Buffer.from(req.header('X-Signature') || '', 'base64');
const gueltig = signatur.length === 64 &&
verify(null, req.body, publicKey, signatur);
if (!gueltig) {
return res.status(401).json({ fehler: 'Ungültige Signatur' });
}
const daten = JSON.parse(req.body.toString('utf8'));
console.log('Echte Anfrage verarbeitet:', daten.event);
res.json({ status: 'ok' });
});
app.listen(3000, () => console.log('Server lauscht auf Port 3000'));
Testen lässt sich der Endpunkt mit einer signierten Anfrage. Die Logik dahinter ist identisch mit Schritt 4: Erst prüfen Sie die Länge (64 Byte bei Ed25519), dann die kryptografische Gültigkeit. Eine ungültige Signatur beantwortet der Server mit HTTP 401, bevor irgendeine Geschäftslogik läuft. Genau diese Reihenfolge schützt vor manipulierten Anfragen, denn ein Angreifer kann zwar einen Body schicken, aber keine passende Signatur ohne Ihren privaten Schlüssel erzeugen.
In einer echten Integration ergänzen Sie zwei Dinge: einen Zeitstempel-Header gegen Replay-Angriffe und ein Rate-Limit gegen Brute-Force-Versuche. Wer tiefer in die serverseitige Absicherung einsteigen will, findet bei unserer Anleitung zu SSH-Keys verwandte Konzepte zur Schlüsselverwaltung.
Welcher Algorithmus für welchen Zweck?
Die Wahl des Verfahrens hängt von Kompatibilität, Geschwindigkeit und Signaturgröße ab. Diese Tabelle fasst die Eckdaten zusammen:
| Verfahren | Sicherheit | Schlüssel | Signaturgröße | Empfehlung |
|---|---|---|---|---|
| Ed25519 | 128 Bit | 32 Byte | 64 Byte | Standardwahl für Neues |
| ECDSA P-256 | 128 Bit | 32 Byte | 64 Byte (P1363) | für JWT, TLS, Kompatibilität |
| ECDSA P-384 | 192 Bit | 48 Byte | 96 Byte (P1363) | höhere Sicherheitsstufe |
| RSA-PSS 3072 | 128 Bit | 384 Byte | 384 Byte | Interoperabilität, Altsysteme |
| RSA-PKCS1 v1.5 | abhängig | variabel | = Modulus | nur für Legacy |
| secp256k1 | 128 Bit | 32 Byte | 64-72 Byte | Blockchain-Kontext |
Die kurze Faustregel: Für neue Anwendungen ohne Altlasten wählen Sie Ed25519. Müssen Sie mit dem Web-Ökosystem (JWT, TLS, WebAuthn) zusammenarbeiten, ist ECDSA P-256 die sichere Bank. RSA-PSS nehmen Sie nur, wenn ein Partner-System es zwingend verlangt. Eine vertiefte Einordnung zur Transportverschlüsselung liefert unser Beitrag zu HTTPS und TLS.
Performance: Wie schnell signiert Node.js?
Die Wahl des Algorithmus wirkt sich spürbar auf den Durchsatz aus. Ed25519 ist beim Signieren deutlich schneller als RSA-3072, weil die zugrunde liegenden Operationen auf kleineren Zahlen rechnen. RSA hingegen verifiziert vergleichsweise schnell, signiert aber langsam. Wenn Ihr Dienst pro Sekunde tausende Tokens ausstellt, ist dieser Unterschied entscheidend. Mit dem folgenden Mini-Benchmark messen Sie es selbst:
// bench.js
import { generateKeyPairSync, sign } from 'node:crypto';
import { performance } from 'node:perf_hooks';
const msg = Buffer.from('Benchmark-Nachricht');
const ed = generateKeyPairSync('ed25519');
const start = performance.now();
for (let i = 0; i < 10000; i++) {
sign(null, msg, ed.privateKey);
}
const dauer = performance.now() - start;
console.log(`10.000 Ed25519-Signaturen in ${dauer.toFixed(0)} ms`);
console.log(`Das sind rund ${Math.round(10000 / (dauer / 1000))} Signaturen/Sekunde`);
Auf moderner Hardware erzeugt Ed25519 problemlos zehntausende Signaturen pro Sekunde. Die exakten Zahlen hängen von CPU und Node-Version ab, daher führen Sie den Benchmark am besten auf Ihrer Zielumgebung aus. Als grobe Orientierung gilt: Ed25519 signiert schneller als ECDSA, und beide schlagen RSA-3072 beim Signieren um Größenordnungen. Für reine Prüflast (etwa ein API-Gateway, das viele Tokens validiert) ist der Abstand kleiner, weil RSA hier von seiner schnellen Verifikation profitiert.
Ein praktischer Tipp: Erzeugen Sie Schlüsselpaare nie im heißen Pfad einer Anfrage. generateKeyPairSync() ist teuer, besonders bei RSA. Laden Sie den Schlüssel einmal beim Start in den Speicher und verwenden Sie ihn danach wieder, genau wie im Express-Beispiel weiter oben.
Häufige Fehler und wie Sie sie vermeiden
Falsches Signaturformat (DER gegen P1363)
Der häufigste ECDSA-Fehler. Sie signieren mit dsaEncoding: 'der', das Gegenüber erwartet aber ieee-p1363. Die Signatur ist mathematisch korrekt, fällt aber durch. Legen Sie das Format auf beiden Seiten explizit fest und dokumentieren Sie es.
Signieren des geparsten JSON statt des Roh-Bodys
Bei Webhooks signieren Sie immer den exakten rohen Anfrage-Body als Byte-Folge, niemals ein wieder serialisiertes JSON-Objekt. Schon eine andere Reihenfolge der Schlüssel oder ein zusätzliches Leerzeichen verändert den Hash und macht die Prüfung ungültig.
Weitere typische Stolperfallen:
- Privaten Schlüssel ins Git-Repo committen. Setzen Sie die
.gitignoreaus Schritt 1, bevor Sie den ersten Commit machen. - MD5 oder SHA-1 als Hash wählen. Beide gelten als gebrochen. Nutzen Sie mindestens SHA-256.
- Vergleich mit
===. Nutzen SietimingSafeEqualfür eigene Vergleiche von MAC- oder Signaturwerten. - Unterschiedliche PSS-Optionen. Salt-Länge und Padding müssen beim Signieren und Prüfen identisch sein.
- RSA-Schlüssel unter 3072 Bit. 2048 Bit ist Auslaufmodell, 3072 Bit ist 2026 das Minimum für neue Schlüssel.
Troubleshooting: 8 typische Fehlermeldungen
| Symptom / Fehlermeldung | Ursache | Lösung |
|---|---|---|
error:1E08010C:DECODER routines::unsupported | PEM-Format passt nicht zum Schlüsseltyp | Export-Typ prüfen: spki für öffentlich, pkcs8 für privat |
verify() liefert false trotz korrektem Code | Falsches dsaEncoding oder PSS-Salt | Format auf beiden Seiten identisch setzen |
Input buffers must have the same byte length | Puffer für timingSafeEqual ungleich lang | Länge vorher prüfen und bei Ungleichheit false zurückgeben |
error:06800066:asn1 encoding routines | Verschlüsselter Schlüssel ohne Passphrase geladen | passphrase an createPrivateKey übergeben |
| Leere oder beschnittene Signatur | Datei mit falscher Kodierung gelesen | Base64 korrekt dekodieren, utf8 beim Lesen der .sig-Datei |
| Signatur ändert sich bei ECDSA jedes Mal | Normales Verhalten (zufälliges k) | Kein Fehler; nur Ed25519 ist deterministisch |
ERR_OSSL_EVP_UNSUPPORTED | Algorithmus von OpenSSL deaktiviert | Node aktualisieren, modernes Verfahren wählen |
| Prüfung scheitert nach Datenbankspeicherung | Signatur als String beschnitten oder umkodiert | Als Base64 oder Hex speichern, nicht als rohes Binär in Textspalte |
Wenn ein Fehler bleibt, isolieren Sie das Problem: Signieren und prüfen Sie dieselbe Nachricht im selben Skript (wie in Schritt 4). Funktioniert das, liegt der Fehler im Transport, in der Speicherung oder in der Kodierung zwischen den Systemen, nicht in der Kryptografie selbst.
Erweiterte Tipps für die Produktion
Schlüsselrotation und Versionierung
Geben Sie jedem Schlüssel eine ID (kid) und legen Sie diese neben die Signatur. So können Sie alte Signaturen weiter prüfen, während neue mit einem frischen Schlüssel entstehen. Rotieren Sie Signaturschlüssel mindestens jährlich und sofort bei Verdacht auf Kompromittierung.
Zeitstempel gegen Replay-Angriffe
Eine Signatur allein verhindert kein erneutes Einspielen einer abgefangenen Nachricht. Nehmen Sie einen Zeitstempel und eine Nonce in die signierten Daten auf und lehnen Sie alles ab, was älter als wenige Minuten ist. Stripe und GitHub machen das bei ihren Webhook-Signaturen genau so.
Weitere Praxistipps:
- Trennen Sie Signatur- und Verschlüsselungsschlüssel strikt. Ein Schlüssel, eine Aufgabe.
- Nutzen Sie für hochsensible Schlüssel ein Hardware-Sicherheitsmodul (HSM) oder einen Cloud-KMS, damit der private Schlüssel das Gerät nie verlässt.
- Loggen Sie jede Signaturprüfung mit Ergebnis und Schlüssel-ID für die Nachvollziehbarkeit.
- Planen Sie Post-Quanten-Verfahren ein. NIST hat mit ML-DSA (Dilithium) bereits einen Standard für quantensichere Signaturen veröffentlicht.
- Verwenden Sie für die Langzeitvalidierung einen qualifizierten Zeitstempeldienst, damit Signaturen auch nach Ablauf eines Zertifikats beweiskräftig bleiben.
Rechtlicher Rahmen: eIDAS und ID Austria
Eine technisch korrekte digitale Signatur ist nicht automatisch eine rechtsgültige elektronische Signatur. In der EU regelt das die eIDAS-Verordnung. Sie kennt drei Stufen: die einfache, die fortgeschrittene und die qualifizierte elektronische Signatur (QES). Nur die QES ist der handschriftlichen Unterschrift rechtlich gleichgestellt.
Für eine QES brauchen Sie ein qualifiziertes Zertifikat von einem Vertrauensdiensteanbieter und eine sichere Signaturerstellungseinheit. In Österreich ist ID Austria die zentrale Plattform für die elektronische Identität und ermöglicht qualifizierte Signaturen, etwa als Nachfolge der Handy-Signatur. Das in diesem Tutorial gebaute Tool erzeugt fortgeschrittene Signaturen im technischen Sinn, ersetzt aber keinen qualifizierten Vertrauensdiensteanbieter, wenn Sie Verträge rechtsverbindlich unterzeichnen wollen.
Für interne Zwecke wie Software-Updates, Webhook-Authentifizierung oder API-Signierung reicht die selbst erzeugte Signatur vollkommen aus. Sobald es um Rechtsgeschäfte mit Dritten geht, prüfen Sie den eIDAS-Status. Die offiziellen Grundlagen finden Sie in der eIDAS-Dokumentation der EU-Kommission und auf der Seite zu ID Austria.
Fazit: In 11 Schritten zum eigenen Signatur-Tool
Sie haben in diesem Tutorial ein vollständiges Signatur-Werkzeug gebaut, das Schlüssel erzeugt, Dateien signiert und Signaturen prüft, dazu eine Express-Integration für Webhooks und drei automatisierte Tests. Das Wichtigste in Kürze: Wählen Sie Ed25519 für neue Projekte, ECDSA P-256 für die Web-Kompatibilität und RSA-PSS nur für Altsysteme. Schützen Sie den privaten Schlüssel mit einer Passphrase und halten Sie ihn aus dem Repository heraus. Achten Sie bei ECDSA penibel auf das Signaturformat und vergleichen Sie eigene MAC-Werte ausschließlich mit timingSafeEqual.
Der nächste sinnvolle Schritt ist die Integration in Ihre bestehende Pipeline: Signieren Sie Ihre Release-Artefakte automatisch im CI-Lauf und prüfen Sie sie vor jedem Deployment. So wird die digitale Signatur vom Einzelbeispiel zum festen Bestandteil Ihrer Sicherheitskette. Wer rechtsverbindliche Unterschriften braucht, kombiniert das hier Gelernte mit einem qualifizierten Vertrauensdiensteanbieter über ID Austria.
Häufige Fragen zur digitalen Signatur in Node.js
Brauche ich für digitale Signaturen in Node.js eine externe Bibliothek?
Nein. Das eingebaute crypto-Modul deckt Ed25519, ECDSA, RSA-PSS und alle gängigen Hashfunktionen ab. Externe Pakete brauchen Sie erst für Spezialfälle wie das Signieren von PDF-Dokumenten nach PAdES oder für Post-Quanten-Verfahren.
Was ist der Unterschied zwischen Signatur und Verschlüsselung?
Verschlüsselung verbirgt den Inhalt, eine Signatur lässt ihn lesbar und beweist nur Echtheit und Unversehrtheit. Beim Signieren nutzen Sie den privaten Schlüssel zum Erstellen und den öffentlichen zum Prüfen. Bei der asymmetrischen Verschlüsselung ist es umgekehrt.
Warum gilt Ed25519 als beste Standardwahl?
Ed25519 ist schnell, hat winzige Schlüssel (32 Byte) und kurze Signaturen (64 Byte) und arbeitet deterministisch. Dadurch entfällt die riskante Nonce-Erzeugung, die ECDSA bei schlechter Zufallsquelle gefährlich macht. Für neue Projekte ohne Kompatibilitätszwang ist es die erste Wahl.
Ist eine selbst erstellte Signatur in Österreich rechtsgültig?
Technisch ja, rechtlich nur eingeschränkt. Eine handschriftliche Unterschrift ersetzt nur die qualifizierte elektronische Signatur (QES) nach eIDAS. Dafür brauchen Sie einen qualifizierten Vertrauensdiensteanbieter, in Österreich typischerweise über ID Austria. Für technische Zwecke wie Webhooks oder Updates ist die selbst erzeugte Signatur ausreichend.
Wie sichere ich den privaten Schlüssel richtig?
Verschlüsseln Sie ihn beim Export mit einer Passphrase (Schritt 8), setzen Sie restriktive Dateirechte und halten Sie ihn aus dem Git-Repository heraus. In der Produktion gehört er in einen Secrets-Manager oder ein HSM, nicht in eine Datei neben dem Code.
Bedroht Quantencomputing meine digitalen Signaturen?
Langfristig ja. Ein ausreichend großer Quantencomputer könnte RSA und ECDSA brechen. NIST hat dafür bereits quantensichere Standards wie ML-DSA (Dilithium) veröffentlicht. Für heutige Systeme ist Ed25519 oder ECDSA sicher, planen Sie aber die Migration ein. Mehr dazu in unserem Beitrag zu Hashing und Kryptographie.
Welche Hashfunktion soll ich für die Signatur verwenden?
Mindestens SHA-256. MD5 und SHA-1 sind gebrochen und dürfen nicht mehr eingesetzt werden. Bei Ed25519 ist die Hashfunktion (SHA-512) fest im Verfahren verankert, Sie müssen sie nicht angeben.
Related Coverage
Weiterführende Artikel
- Digitale Signaturen erklärt: So funktionieren sie
- SHA-256 erklärt: So funktioniert es
- Was ist eine Hashfunktion? So funktioniert Hashing
- HTTPS und TLS: Wie das Schloss im Browser Sie schützt
- Hashing und Kryptographie erklärt
- SSH-Key einrichten: Server härten in 10 Schritten
Externe Referenzen zum Vertiefen: die offizielle Node.js crypto-Dokumentation, der RFC 8032 zu EdDSA und Ed25519 sowie der NIST-Standard FIPS 186-5 für digitale Signaturen.




