{"id":102,"date":"2026-06-15T16:21:41","date_gmt":"2026-06-15T16:21:41","guid":{"rendered":"https:\/\/shattered.io\/at\/2026\/06\/15\/digitale-signatur-nodejs\/"},"modified":"2026-06-15T16:23:10","modified_gmt":"2026-06-15T16:23:10","slug":"digitale-signatur-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/at\/digitale-signatur-nodejs\/","title":{"rendered":"Digitale Signatur in Node.js: 11 Schritte, 40 Min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Eine <strong>digitale Signatur<\/strong> beweist zwei Dinge auf einmal: dass eine Nachricht wirklich von Ihnen stammt und dass niemand sie unterwegs ver\u00e4ndert hat. Genau das brauchen Sie f\u00fcr Webhooks, Software-Updates, API-Anfragen oder signierte Dokumente. In diesem Tutorial bauen Sie in 11 Schritten ein vollst\u00e4ndiges Signatur-Werkzeug in Node.js, ganz ohne externe Bibliothek. Sie nutzen nur das eingebaute <code>crypto<\/code>-Modul und lernen Ed25519, ECDSA und RSA-PSS im praktischen Einsatz kennen. Rechnen Sie mit rund 40 Minuten bis zum lauff\u00e4higen CLI-Tool.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das Ergebnis ist ein Kommandozeilen-Programm, das Dateien signiert, Signaturen pr\u00fcft und Schl\u00fcsselpaare sicher verwaltet. Jeder Schritt enth\u00e4lt lauff\u00e4higen Code, eine erwartete Ausgabe und einen Hinweis, was schiefgehen kann. Am Ende finden Sie eine Vergleichstabelle der Algorithmen, acht Troubleshooting-F\u00e4lle und einen Abschnitt zur rechtlichen Einordnung nach eIDAS und ID Austria.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"was-ist-eine-digitale-signatur\">Was eine digitale Signatur technisch leistet<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Eine <strong>digitale Signatur<\/strong> kombiniert zwei kryptografische Bausteine: eine Hashfunktion und ein asymmetrisches Schl\u00fcsselpaar. Zuerst berechnet die Hashfunktion (etwa SHA-256) einen festen Fingerabdruck der Nachricht. Danach verschl\u00fcsselt der private Schl\u00fcssel diesen Fingerabdruck. Das Ergebnis ist die Signatur. Wer Ihren \u00f6ffentlichen Schl\u00fcssel besitzt, kann die Rechnung umkehren und pr\u00fcfen, ob Fingerabdruck und Nachricht zusammenpassen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Verwechseln Sie das nicht mit Verschl\u00fcsselung. Verschl\u00fcsselung sch\u00fctzt die Vertraulichkeit, also den Inhalt. Eine Signatur sch\u00fctzt Echtheit (Authentizit\u00e4t) und Unversehrtheit (Integrit\u00e4t), gibt den Inhalt aber im Klartext frei. Ein drittes Merkmal kommt dazu: die Nichtabstreitbarkeit. Weil nur Sie den privaten Schl\u00fcssel besitzen, k\u00f6nnen Sie eine g\u00fcltige Signatur sp\u00e4ter nicht glaubhaft leugnen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Node.js stellt daf\u00fcr im <code>crypto<\/code>-Modul alles bereit. Die direkten Einzelaufrufe hei\u00dfen <code>crypto.sign()<\/code> und <code>crypto.verify()<\/code>. F\u00fcr gro\u00dfe Datenmengen gibt es die stream-orientierten Objekte <code>crypto.createSign()<\/code> und <code>crypto.createVerify()<\/code>. Schl\u00fcsselpaare erzeugen Sie mit <code>crypto.generateKeyPairSync()<\/code>. Diese vier Funktionen tragen das gesamte Tutorial.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ablauf-signaturpruefung\">Der Ablauf einer Signaturpr\u00fcfung in f\u00fcnf Schritten<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Damit Sie die sp\u00e4tere Code-Logik gedanklich einordnen k\u00f6nnen, hier der vollst\u00e4ndige Ablauf. Auf der Senderseite passieren drei Dinge: Erstens berechnet die Hashfunktion einen Digest der Nachricht. Zweitens wandelt der private Schl\u00fcssel diesen Digest in eine Signatur um. Drittens verschickt der Sender Nachricht und Signatur gemeinsam, oft als getrennte Datei mit der Endung <code>.sig<\/code> oder als HTTP-Header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Auf der Empf\u00e4ngerseite folgen zwei weitere Schritte: Viertens berechnet der Empf\u00e4nger denselben Hash \u00fcber die empfangene Nachricht. F\u00fcnftens pr\u00fcft <code>crypto.verify()<\/code> mit dem \u00f6ffentlichen Schl\u00fcssel, ob die mitgelieferte Signatur zu diesem Hash passt. Stimmt etwas nicht, weil ein Byte ver\u00e4ndert wurde oder ein falscher Schl\u00fcssel im Spiel ist, liefert die Funktion <code>false<\/code>. Dieser Mechanismus ist die Grundlage jeder vertrauensw\u00fcrdigen Software-Auslieferung, jedes signierten Tokens und jeder gepr\u00fcften API-Anfrage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wichtig: Eine Signatur garantiert nicht, dass der \u00f6ffentliche Schl\u00fcssel wirklich der erwarteten Person geh\u00f6rt. Daf\u00fcr sorgt eine zus\u00e4tzliche Vertrauensschicht, etwa ein Zertifikat einer Zertifizierungsstelle oder ein fest hinterlegter Schl\u00fcssel (Key Pinning). In diesem Tutorial tauschen Sie die Schl\u00fcssel direkt aus, was f\u00fcr interne Systeme v\u00f6llig ausreicht.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"voraussetzungen\">Voraussetzungen und Versionen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sie brauchen keine externen npm-Pakete. Das gesamte Projekt l\u00e4uft auf dem Standard-Funktionsumfang von Node.js. Pr\u00fcfen Sie vor dem Start diese Versionen:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Komponente<\/th><th>Empfohlene Version<\/th><th>Pr\u00fcfbefehl<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>24.x LTS (Codename &#8220;Krypton&#8221;)<\/td><td><code>node -v<\/code><\/td><\/tr><tr><td>npm<\/td><td>11.x (mit Node 24 geb\u00fcndelt)<\/td><td><code>npm -v<\/code><\/td><\/tr><tr><td>OpenSSL (in Node integriert)<\/td><td>3.x<\/td><td><code>node -p \"process.versions.openssl\"<\/code><\/td><\/tr><tr><td>Editor<\/td><td>VS Code oder beliebig<\/td><td>&#8211;<\/td><\/tr><tr><td>Betriebssystem<\/td><td>Windows, macOS, Linux<\/td><td>&#8211;<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Node.js 24 ist im Juni 2026 die aktive LTS-Linie und damit die richtige Wahl f\u00fcr neue Projekte. Wer noch auf der \u00e4lteren LTS-Linie 22 (&#8220;Jod&#8221;) sitzt, kann diesem Tutorial trotzdem folgen, denn die hier genutzten Signatur-APIs sind seit Jahren stabil. Vom aktuellen Release 26 (&#8220;Current&#8221;) raten wir in der Produktion ab, solange es nicht in LTS \u00fcberf\u00fchrt wurde.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Grundkenntnisse in JavaScript und der Kommandozeile reichen aus. Wenn Sie das Konzept hinter Fingerabdr\u00fccken vertiefen m\u00f6chten, lesen Sie vorab unsere Erkl\u00e4rung zu <a href=\"\/at\/digitale-signaturen\/\">digitalen Signaturen<\/a> und zu <a href=\"\/at\/sha-256\/\">SHA-256<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-1-projekt\">Schritt 1: Projekt anlegen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Legen Sie einen Projektordner an und initialisieren Sie ihn. Wir setzen ES-Module ein, daher kommt <code>\"type\": \"module\"<\/code> in die <code>package.json<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir signatur-tool && cd signatur-tool\nnpm init -y\nnpm pkg set type=\"module\"\nmkdir keys docs\nnode -v<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erwartete Ausgabe der letzten Zeile:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>v24.4.1<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Ordner <code>keys<\/code> nimmt sp\u00e4ter die Schl\u00fcsseldateien auf, <code>docs<\/code> die zu signierenden Beispieldateien. Legen Sie sofort eine <code>.gitignore<\/code> an, damit private Schl\u00fcssel niemals im Repository landen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .gitignore\nnode_modules\/\nkeys\/*.pem\n*.sig\n.env<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-2-schluesselpaar\">Schritt 2: Ed25519-Schl\u00fcsselpaar erzeugen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wir beginnen mit Ed25519, dem modernen Standard f\u00fcr Signaturen. Erstellen Sie <code>keygen.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ keygen.js\nimport { generateKeyPairSync } from 'node:crypto';\nimport { writeFileSync } from 'node:fs';\n\nconst { publicKey, privateKey } = generateKeyPairSync('ed25519');\n\nconst pubPem = publicKey.export({ type: 'spki', format: 'pem' });\nconst privPem = privateKey.export({ type: 'pkcs8', format: 'pem' });\n\nwriteFileSync('keys\/ed25519_public.pem', pubPem);\nwriteFileSync('keys\/ed25519_private.pem', privPem, { mode: 0o600 });\n\nconsole.log('Schl\u00fcsselpaar erstellt:');\nconsole.log('  \u00d6ffentlich:', 'keys\/ed25519_public.pem');\nconsole.log('  Privat:    ', 'keys\/ed25519_private.pem (Rechte 600)');<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Starten Sie das Skript:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node keygen.js<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erwartete Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Schl\u00fcsselpaar erstellt:\n  \u00d6ffentlich: keys\/ed25519_public.pem\n  Privat:     keys\/ed25519_private.pem (Rechte 600)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der private Schl\u00fcssel beruht bei Ed25519 auf nur 32 Byte Schl\u00fcsselmaterial, das macht das Verfahren so leichtgewichtig. Die Dateirechte <code>0o600<\/code> bedeuten: nur der Eigent\u00fcmer darf lesen und schreiben. Auf Windows greift diese Einschr\u00e4nkung nicht, dort sch\u00fctzen Sie den Ordner \u00fcber die NTFS-Berechtigungen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-3-erste-signatur\">Schritt 3: Die erste digitale Signatur erstellen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Jetzt signieren wir eine Nachricht. F\u00fcr Ed25519 gilt eine Besonderheit: Das Algorithmus-Argument bei <code>crypto.sign()<\/code> muss <code>null<\/code> sein, weil der Hash fest zum Verfahren geh\u00f6rt. Erstellen Sie <code>sign-basic.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ sign-basic.js\nimport { sign, createPrivateKey } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\n\nconst privateKey = createPrivateKey(readFileSync('keys\/ed25519_private.pem'));\nconst message = Buffer.from('\u00dcberweisung 250 EUR an Konto AT61 1904 3002 3457 3201');\n\n\/\/ Bei Ed25519 ist der erste Parameter null (Hash ist im Verfahren festgelegt)\nconst signature = sign(null, message, privateKey);\n\nconsole.log('Nachricht :', message.toString());\nconsole.log('Signatur  :', signature.toString('base64'));\nconsole.log('L\u00e4nge     :', signature.length, 'Byte');<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ausf\u00fchren:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node sign-basic.js<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erwartete Ausgabe (die Signatur unterscheidet sich pro Schl\u00fcssel):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Nachricht : \u00dcberweisung 250 EUR an Konto AT61 1904 3002 3457 3201\nSignatur  : 9Qm2k0Vd...c7Lp4w==\nL\u00e4nge     : 64 Byte<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">64 Byte ist die feste Signaturl\u00e4nge bei Ed25519, unabh\u00e4ngig von der Gr\u00f6\u00dfe der Nachricht. Weil Ed25519 deterministisch arbeitet, liefert dieselbe Nachricht mit demselben Schl\u00fcssel immer dieselbe Signatur. Das vereinfacht Tests und schlie\u00dft eine ganze Klasse von Nonce-Fehlern aus, die ECDSA plagen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-4-verifizieren\">Schritt 4: Eine Signatur verifizieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die Pr\u00fcfung \u00fcbernimmt <code>crypto.verify()<\/code>. Sie gibt einen booleschen Wert zur\u00fcck. Erstellen Sie <code>verify-basic.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ verify-basic.js\nimport { sign, verify, createPrivateKey, createPublicKey } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\n\nconst privateKey = createPrivateKey(readFileSync('keys\/ed25519_private.pem'));\nconst publicKey = createPublicKey(readFileSync('keys\/ed25519_public.pem'));\n\nconst message = Buffer.from('\u00dcberweisung 250 EUR an Konto AT61 1904 3002 3457 3201');\nconst signature = sign(null, message, privateKey);\n\nconst echt = verify(null, message, publicKey, signature);\nconsole.log('Originalnachricht g\u00fcltig:', echt);\n\n\/\/ Manipulierte Nachricht: ein Zeichen ge\u00e4ndert\nconst gefaelscht = Buffer.from('\u00dcberweisung 950 EUR an Konto AT61 1904 3002 3457 3201');\nconst echt2 = verify(null, gefaelscht, publicKey, signature);\nconsole.log('Manipulierte Nachricht g\u00fcltig:', echt2);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erwartete Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Originalnachricht g\u00fcltig: true\nManipulierte Nachricht g\u00fcltig: false<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Genau das ist der Kern jeder <strong>digitale Signatur<\/strong>: \u00c4ndert ein Angreifer auch nur einen Betrag von 250 auf 950, f\u00e4llt die Pr\u00fcfung sofort auf <code>false<\/code>. Der \u00f6ffentliche Schl\u00fcssel reicht f\u00fcr die Pr\u00fcfung, der private bleibt geheim beim Unterzeichner.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-5-ecdsa\">Schritt 5: ECDSA mit der Kurve P-256 nutzen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Viele bestehende Systeme verlangen ECDSA. Node unterst\u00fctzt die g\u00e4ngigen Kurven <code>prime256v1<\/code> (auch P-256 genannt), <code>secp384r1<\/code> (P-384) und <code>secp256k1<\/code> (die Bitcoin-Kurve). Bei ECDSA m\u00fcssen Sie den Hash explizit angeben und das Signaturformat festlegen. Erstellen Sie <code>ecdsa.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ ecdsa.js\nimport { generateKeyPairSync, sign, verify } from 'node:crypto';\n\nconst { publicKey, privateKey } = generateKeyPairSync('ec', {\n  namedCurve: 'prime256v1', \/\/ P-256\n});\n\nconst message = Buffer.from('Vertrag #2026-0815 freigegeben');\n\n\/\/ dsaEncoding 'ieee-p1363' liefert feste L\u00e4nge (64 Byte bei P-256)\nconst sigP1363 = sign('sha256', message, {\n  key: privateKey,\n  dsaEncoding: 'ieee-p1363',\n});\n\nconst sigDer = sign('sha256', message, {\n  key: privateKey,\n  dsaEncoding: 'der',\n});\n\nconsole.log('P1363-L\u00e4nge:', sigP1363.length, 'Byte (fest)');\nconsole.log('DER-L\u00e4nge  :', sigDer.length, 'Byte (variabel)');\n\nconst ok = verify('sha256', message, {\n  key: publicKey,\n  dsaEncoding: 'ieee-p1363',\n}, sigP1363);\n\nconsole.log('ECDSA-Pr\u00fcfung:', ok);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erwartete Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>P1363-L\u00e4nge: 64 Byte (fest)\nDER-L\u00e4nge  : 70 Byte (variabel)\nECDSA-Pr\u00fcfung: true<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Unterschied zwischen DER und IEEE-P1363 ist eine h\u00e4ufige Fehlerquelle. DER ist die ASN.1-codierte Form mit variabler L\u00e4nge (meist 70 bis 72 Byte bei P-256). IEEE-P1363 ist die rohe Aneinanderreihung von <code>r<\/code> und <code>s<\/code> mit fester L\u00e4nge (64 Byte). Webstandards wie JOSE und JWT erwarten P1363, viele X.509-Werkzeuge erwarten DER. Wer das Format verwechselt, bekommt eine g\u00fcltige Signatur, die beim Gegen\u00fcber trotzdem als ung\u00fcltig durchf\u00e4llt.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-6-rsa-pss\">Schritt 6: RSA-PSS-Signaturen erzeugen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">RSA bleibt f\u00fcr die Interoperabilit\u00e4t mit \u00e4lteren Systemen wichtig. F\u00fcr neue Entw\u00fcrfe ist RSA-PSS dem klassischen PKCS#1 v1.5 vorzuziehen, weil es probabilistisch arbeitet und besser gegen bestimmte Angriffe gewappnet ist. Erstellen Sie <code>rsa-pss.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ rsa-pss.js\nimport { generateKeyPairSync, sign, verify, constants } from 'node:crypto';\n\nconst { publicKey, privateKey } = generateKeyPairSync('rsa', {\n  modulusLength: 3072, \/\/ 3072 Bit gilt 2026 als solides Minimum\n});\n\nconst message = Buffer.from('Software-Update v2.4.0 freigegeben');\n\nconst options = {\n  key: privateKey,\n  padding: constants.RSA_PKCS1_PSS_PADDING,\n  saltLength: constants.RSA_PSS_SALTLEN_DIGEST,\n};\n\nconst signature = sign('sha256', message, options);\nconsole.log('RSA-PSS-Signaturl\u00e4nge:', signature.length, 'Byte');\n\nconst ok = verify('sha256', message, {\n  key: publicKey,\n  padding: constants.RSA_PKCS1_PSS_PADDING,\n  saltLength: constants.RSA_PSS_SALTLEN_DIGEST,\n}, signature);\n\nconsole.log('RSA-PSS-Pr\u00fcfung:', ok);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erwartete Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>RSA-PSS-Signaturl\u00e4nge: 384 Byte\nRSA-PSS-Pr\u00fcfung: true<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Signaturl\u00e4nge entspricht der Modulusgr\u00f6\u00dfe: 3072 Bit ergeben 384 Byte. Das ist sechsmal so gro\u00df wie eine Ed25519-Signatur. Achten Sie darauf, dieselben Optionen (Padding und Salt-L\u00e4nge) beim Signieren und Pr\u00fcfen zu verwenden, sonst schl\u00e4gt die Pr\u00fcfung fehl.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-7-dateien-signieren\">Schritt 7: Gro\u00dfe Dateien per Stream signieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Eine ganze Datei in den Speicher zu laden ist bei mehreren Gigabyte keine gute Idee. F\u00fcr ECDSA und RSA nutzen Sie <code>crypto.createSign()<\/code> und f\u00fcttern den Stream h\u00e4ppchenweise. Erstellen Sie zuerst eine Beispieldatei und dann <code>sign-file.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node -e \"require('fs').writeFileSync('docs\/vertrag.pdf','%PDF-1.7 Beispielinhalt fuer das Tutorial')\"<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ sign-file.js\nimport { createSign, createPrivateKey } from 'node:crypto';\nimport { createReadStream, readFileSync, writeFileSync } from 'node:fs';\n\nconst privateKey = createPrivateKey({\n  key: readFileSync('keys\/rsa_private.pem'),\n  passphrase: process.env.KEY_PASSPHRASE,\n});\n\nconst signer = createSign('sha256');\nconst stream = createReadStream('docs\/vertrag.pdf');\n\nstream.on('data', (chunk) => signer.update(chunk));\nstream.on('end', () => {\n  signer.end();\n  const signature = signer.sign(privateKey);\n  writeFileSync('docs\/vertrag.pdf.sig', signature);\n  console.log('Datei signiert ->', 'docs\/vertrag.pdf.sig');\n  console.log('Signaturgr\u00f6\u00dfe :', signature.length, 'Byte');\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hinweis: Ed25519 l\u00e4sst sich in Node nicht \u00fcber <code>createSign()<\/code> streamen, weil das Verfahren die gesamte Nachricht auf einmal braucht. F\u00fcr Ed25519-Dateien laden Sie den Inhalt entweder ganz oder bilden zuerst einen SHA-512-Hash und signieren diesen Digest. F\u00fcr klassisches Streaming sind ECDSA und RSA die richtige Wahl.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-8-schluessel-schuetzen\">Schritt 8: Private Schl\u00fcssel mit Passphrase sch\u00fctzen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ein privater Schl\u00fcssel im Klartext auf der Festplatte ist ein Risiko. Verschl\u00fcsseln Sie ihn beim Export mit einer Passphrase. Das Beispiel zeigt es f\u00fcr ein RSA-Paar, das wir f\u00fcr Schritt 7 ohnehin brauchen. Erstellen Sie <code>keygen-rsa.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ keygen-rsa.js\nimport { generateKeyPairSync } from 'node:crypto';\nimport { writeFileSync } from 'node:fs';\n\nconst passphrase = process.env.KEY_PASSPHRASE || 'aendere-mich-sofort';\n\nconst { publicKey, privateKey } = generateKeyPairSync('rsa', {\n  modulusLength: 3072,\n});\n\nwriteFileSync('keys\/rsa_public.pem',\n  publicKey.export({ type: 'spki', format: 'pem' }));\n\nwriteFileSync('keys\/rsa_private.pem',\n  privateKey.export({\n    type: 'pkcs8',\n    format: 'pem',\n    cipher: 'aes-256-cbc',\n    passphrase,\n  }), { mode: 0o600 });\n\nconsole.log('RSA-Schl\u00fcssel erstellt, privater Teil mit AES-256 verschl\u00fcsselt.');<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Setzen Sie die Passphrase \u00fcber eine Umgebungsvariable, niemals im Code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>KEY_PASSPHRASE='Ein-langes-Geheimnis-2026!' node keygen-rsa.js<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Beim Laden eines verschl\u00fcsselten Schl\u00fcssels \u00fcbergeben Sie die Passphrase an <code>createPrivateKey<\/code>, wie schon im Skript aus Schritt 7 gezeigt. So bleibt der Schl\u00fcssel selbst dann nutzlos, wenn die PEM-Datei in falsche H\u00e4nde ger\u00e4t. In produktiven Systemen geh\u00f6rt das Geheimnis in einen Tresor wie HashiCorp Vault oder den Secrets-Manager Ihres Cloud-Anbieters, nicht in eine Datei neben den Code.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-9-timing-safe\">Schritt 9: Konstante Laufzeit mit timingSafeEqual<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn Sie Signaturen oder HMAC-Werte selbst vergleichen, etwa bei der Pr\u00fcfung von Webhook-Signaturen, d\u00fcrfen Sie niemals den Operator <code>===<\/code> verwenden. Ein naiver Vergleich bricht beim ersten unterschiedlichen Byte ab und verr\u00e4t \u00fcber die Laufzeit, wie viele Stellen korrekt waren. Angreifer nutzen das aus. Die L\u00f6sung hei\u00dft <code>crypto.timingSafeEqual()<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ webhook-verify.js\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\nfunction pruefeWebhook(rohBody, signaturHeader, secret) {\n  const erwartet = createHmac('sha256', secret)\n    .update(rohBody)\n    .digest();\n\n  const empfangen = Buffer.from(signaturHeader, 'hex');\n\n  \/\/ L\u00e4ngen m\u00fcssen vor timingSafeEqual gleich sein\n  if (erwartet.length !== empfangen.length) return false;\n\n  return timingSafeEqual(erwartet, empfangen);\n}\n\nconst secret = 'webhook-geheimnis';\nconst body = '{\"event\":\"zahlung\",\"betrag\":250}';\nconst gueltigeSig = createHmac('sha256', secret).update(body).digest('hex');\n\nconsole.log('G\u00fcltig:', pruefeWebhook(body, gueltigeSig, secret));\nconsole.log('Gef\u00e4lscht:', pruefeWebhook(body, 'deadbeef'.repeat(8), secret));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erwartete Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>G\u00fcltig: true\nGef\u00e4lscht: false<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pr\u00fcfen Sie die L\u00e4nge vor dem Aufruf, denn <code>timingSafeEqual<\/code> wirft eine Ausnahme, wenn die Puffer unterschiedlich lang sind. Bei den eingebauten Funktionen <code>crypto.verify()<\/code> und <code>verifier.verify()<\/code> \u00fcbernimmt Node den sicheren Vergleich bereits intern.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-10-cli-tool\">Schritt 10: Das vollst\u00e4ndige CLI-Werkzeug bauen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Jetzt f\u00fcgen wir alles zu einem benutzbaren Kommandozeilen-Tool zusammen. Es unterst\u00fctzt drei Befehle: <code>keygen<\/code>, <code>sign<\/code> und <code>verify<\/code>. Erstellen Sie <code>cli.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ cli.js\nimport {\n  generateKeyPairSync, sign, verify,\n  createPrivateKey, createPublicKey,\n} from 'node:crypto';\nimport { readFileSync, writeFileSync } from 'node:fs';\n\nconst [,, befehl, ...args] = process.argv;\n\nfunction keygen() {\n  const { publicKey, privateKey } = generateKeyPairSync('ed25519');\n  writeFileSync('keys\/ed25519_public.pem',\n    publicKey.export({ type: 'spki', format: 'pem' }));\n  writeFileSync('keys\/ed25519_private.pem',\n    privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });\n  console.log('OK: Ed25519-Schl\u00fcsselpaar in keys\/ erstellt.');\n}\n\nfunction signieren(datei) {\n  const key = createPrivateKey(readFileSync('keys\/ed25519_private.pem'));\n  const inhalt = readFileSync(datei);\n  const signatur = sign(null, inhalt, key);\n  writeFileSync(datei + '.sig', signatur.toString('base64'));\n  console.log(`OK: ${datei} signiert -> ${datei}.sig`);\n}\n\nfunction pruefen(datei) {\n  const key = createPublicKey(readFileSync('keys\/ed25519_public.pem'));\n  const inhalt = readFileSync(datei);\n  const signatur = Buffer.from(readFileSync(datei + '.sig', 'utf8'), 'base64');\n  const ok = verify(null, inhalt, key, signatur);\n  console.log(ok ? 'G\u00dcLTIG: Signatur korrekt.' : 'UNG\u00dcLTIG: Signatur passt nicht.');\n  process.exit(ok ? 0 : 1);\n}\n\nswitch (befehl) {\n  case 'keygen': keygen(); break;\n  case 'sign':   signieren(args[0]); break;\n  case 'verify': pruefen(args[0]); break;\n  default:\n    console.log('Nutzung: node cli.js [keygen | sign &lt;datei&gt; | verify &lt;datei&gt;]');\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Probieren Sie den kompletten Ablauf aus:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node cli.js keygen\necho \"Rechnung 2026-0815: 1.480,00 EUR\" &gt; docs\/rechnung.txt\nnode cli.js sign docs\/rechnung.txt\nnode cli.js verify docs\/rechnung.txt<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erwartete Ausgabe:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>OK: Ed25519-Schl\u00fcsselpaar in keys\/ erstellt.\nOK: docs\/rechnung.txt signiert -> docs\/rechnung.txt.sig\nG\u00dcLTIG: Signatur korrekt.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c4ndern Sie jetzt ein Zeichen in <code>docs\/rechnung.txt<\/code> und pr\u00fcfen Sie erneut. Das Tool meldet &#8220;UNG\u00dcLTIG&#8221; und beendet sich mit Exit-Code 1, was sich in CI-Pipelines auswerten l\u00e4sst.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-11-tests\">Schritt 11: Automatisierte Tests schreiben<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Node 24 bringt einen eingebauten Test-Runner mit, kein zus\u00e4tzliches Paket n\u00f6tig. Erstellen Sie <code>test\/signatur.test.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ test\/signatur.test.js\nimport { test } from 'node:test';\nimport assert from 'node:assert\/strict';\nimport { generateKeyPairSync, sign, verify } from 'node:crypto';\n\ntest('g\u00fcltige Signatur wird akzeptiert', () => {\n  const { publicKey, privateKey } = generateKeyPairSync('ed25519');\n  const msg = Buffer.from('Testnachricht');\n  const sig = sign(null, msg, privateKey);\n  assert.equal(verify(null, msg, publicKey, sig), true);\n});\n\ntest('manipulierte Nachricht wird abgelehnt', () => {\n  const { publicKey, privateKey } = generateKeyPairSync('ed25519');\n  const sig = sign(null, Buffer.from('Original'), privateKey);\n  assert.equal(verify(null, Buffer.from('Gef\u00e4lscht'), publicKey, sig), false);\n});\n\ntest('fremder Schl\u00fcssel wird abgelehnt', () => {\n  const a = generateKeyPairSync('ed25519');\n  const b = generateKeyPairSync('ed25519');\n  const msg = Buffer.from('Nachricht');\n  const sig = sign(null, msg, a.privateKey);\n  assert.equal(verify(null, msg, b.publicKey, sig), false);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Starten Sie die Tests:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node --test<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erwartete Ausgabe (gek\u00fcrzt):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># tests 3\n# pass 3\n# fail 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Damit ist das Projekt komplett: Schl\u00fcsselverwaltung, Signieren, Pr\u00fcfen und drei gr\u00fcne Tests. Wer das Konzept hinter dem Vergleich vertiefen will, findet bei unseren <a href=\"\/at\/hashfunktion\/\">Hashfunktionen<\/a> die Grundlagen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"praxis-express-api\">Praxis: Signaturpr\u00fcfung in einer Express-API<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Das CLI-Tool zeigt das Prinzip. In der Praxis pr\u00fcfen 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\u00fcssen den rohen Body lesen, bevor ein JSON-Parser ihn anfasst, sonst ver\u00e4ndert sich die Byte-Folge und die <strong>digitale Signatur<\/strong> passt nicht mehr.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js  (npm install express)\nimport express from 'express';\nimport { verify, createPublicKey } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\n\nconst app = express();\nconst publicKey = createPublicKey(readFileSync('keys\/ed25519_public.pem'));\n\n\/\/ Rohen Body als Buffer bewahren, NICHT vorher JSON-parsen\napp.use(express.raw({ type: '*\/*' }));\n\napp.post('\/webhook', (req, res) => {\n  const signatur = Buffer.from(req.header('X-Signature') || '', 'base64');\n  const gueltig = signatur.length === 64 &&\n    verify(null, req.body, publicKey, signatur);\n\n  if (!gueltig) {\n    return res.status(401).json({ fehler: 'Ung\u00fcltige Signatur' });\n  }\n\n  const daten = JSON.parse(req.body.toString('utf8'));\n  console.log('Echte Anfrage verarbeitet:', daten.event);\n  res.json({ status: 'ok' });\n});\n\napp.listen(3000, () => console.log('Server lauscht auf Port 3000'));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Testen l\u00e4sst sich der Endpunkt mit einer signierten Anfrage. Die Logik dahinter ist identisch mit Schritt 4: Erst pr\u00fcfen Sie die L\u00e4nge (64 Byte bei Ed25519), dann die kryptografische G\u00fcltigkeit. Eine ung\u00fcltige Signatur beantwortet der Server mit HTTP 401, bevor irgendeine Gesch\u00e4ftslogik l\u00e4uft. Genau diese Reihenfolge sch\u00fctzt vor manipulierten Anfragen, denn ein Angreifer kann zwar einen Body schicken, aber keine passende Signatur ohne Ihren privaten Schl\u00fcssel erzeugen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In einer echten Integration erg\u00e4nzen 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 <a href=\"\/at\/ssh-key-einrichten\/\">SSH-Keys<\/a> verwandte Konzepte zur Schl\u00fcsselverwaltung.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"algorithmus-vergleich\">Welcher Algorithmus f\u00fcr welchen Zweck?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die Wahl des Verfahrens h\u00e4ngt von Kompatibilit\u00e4t, Geschwindigkeit und Signaturgr\u00f6\u00dfe ab. Diese Tabelle fasst die Eckdaten zusammen:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Verfahren<\/th><th>Sicherheit<\/th><th>Schl\u00fcssel<\/th><th>Signaturgr\u00f6\u00dfe<\/th><th>Empfehlung<\/th><\/tr><\/thead><tbody><tr><td>Ed25519<\/td><td>128 Bit<\/td><td>32 Byte<\/td><td>64 Byte<\/td><td>Standardwahl f\u00fcr Neues<\/td><\/tr><tr><td>ECDSA P-256<\/td><td>128 Bit<\/td><td>32 Byte<\/td><td>64 Byte (P1363)<\/td><td>f\u00fcr JWT, TLS, Kompatibilit\u00e4t<\/td><\/tr><tr><td>ECDSA P-384<\/td><td>192 Bit<\/td><td>48 Byte<\/td><td>96 Byte (P1363)<\/td><td>h\u00f6here Sicherheitsstufe<\/td><\/tr><tr><td>RSA-PSS 3072<\/td><td>128 Bit<\/td><td>384 Byte<\/td><td>384 Byte<\/td><td>Interoperabilit\u00e4t, Altsysteme<\/td><\/tr><tr><td>RSA-PKCS1 v1.5<\/td><td>abh\u00e4ngig<\/td><td>variabel<\/td><td>= Modulus<\/td><td>nur f\u00fcr Legacy<\/td><\/tr><tr><td>secp256k1<\/td><td>128 Bit<\/td><td>32 Byte<\/td><td>64-72 Byte<\/td><td>Blockchain-Kontext<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Die kurze Faustregel: F\u00fcr neue Anwendungen ohne Altlasten w\u00e4hlen Sie Ed25519. M\u00fcssen Sie mit dem Web-\u00d6kosystem (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\u00fcsselung liefert unser Beitrag zu <a href=\"\/at\/https-und-tls\/\">HTTPS und TLS<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"performance\">Performance: Wie schnell signiert Node.js?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die Wahl des Algorithmus wirkt sich sp\u00fcrbar 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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ bench.js\nimport { generateKeyPairSync, sign } from 'node:crypto';\nimport { performance } from 'node:perf_hooks';\n\nconst msg = Buffer.from('Benchmark-Nachricht');\nconst ed = generateKeyPairSync('ed25519');\n\nconst start = performance.now();\nfor (let i = 0; i &lt; 10000; i++) {\n  sign(null, msg, ed.privateKey);\n}\nconst dauer = performance.now() - start;\nconsole.log(`10.000 Ed25519-Signaturen in ${dauer.toFixed(0)} ms`);\nconsole.log(`Das sind rund ${Math.round(10000 \/ (dauer \/ 1000))} Signaturen\/Sekunde`);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Auf moderner Hardware erzeugt Ed25519 problemlos zehntausende Signaturen pro Sekunde. Die exakten Zahlen h\u00e4ngen von CPU und Node-Version ab, daher f\u00fchren 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\u00f6\u00dfenordnungen. F\u00fcr reine Pr\u00fcflast (etwa ein API-Gateway, das viele Tokens validiert) ist der Abstand kleiner, weil RSA hier von seiner schnellen Verifikation profitiert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ein praktischer Tipp: Erzeugen Sie Schl\u00fcsselpaare nie im hei\u00dfen Pfad einer Anfrage. <code>generateKeyPairSync()<\/code> ist teuer, besonders bei RSA. Laden Sie den Schl\u00fcssel einmal beim Start in den Speicher und verwenden Sie ihn danach wieder, genau wie im Express-Beispiel weiter oben.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"haeufige-fehler\">H\u00e4ufige Fehler und wie Sie sie vermeiden<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fehler-formate\">Falsches Signaturformat (DER gegen P1363)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Der h\u00e4ufigste ECDSA-Fehler. Sie signieren mit <code>dsaEncoding: 'der'<\/code>, das Gegen\u00fcber erwartet aber <code>ieee-p1363<\/code>. Die Signatur ist mathematisch korrekt, f\u00e4llt aber durch. Legen Sie das Format auf beiden Seiten explizit fest und dokumentieren Sie es.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fehler-hash\">Signieren des geparsten JSON statt des Roh-Bodys<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00fcssel oder ein zus\u00e4tzliches Leerzeichen ver\u00e4ndert den Hash und macht die Pr\u00fcfung ung\u00fcltig.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Weitere typische Stolperfallen:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Privaten Schl\u00fcssel ins Git-Repo committen.<\/strong> Setzen Sie die <code>.gitignore<\/code> aus Schritt 1, bevor Sie den ersten Commit machen.<\/li><li><strong>MD5 oder SHA-1 als Hash w\u00e4hlen.<\/strong> Beide gelten als gebrochen. Nutzen Sie mindestens SHA-256.<\/li><li><strong>Vergleich mit <code>===<\/code>.<\/strong> Nutzen Sie <code>timingSafeEqual<\/code> f\u00fcr eigene Vergleiche von MAC- oder Signaturwerten.<\/li><li><strong>Unterschiedliche PSS-Optionen.<\/strong> Salt-L\u00e4nge und Padding m\u00fcssen beim Signieren und Pr\u00fcfen identisch sein.<\/li><li><strong>RSA-Schl\u00fcssel unter 3072 Bit.<\/strong> 2048 Bit ist Auslaufmodell, 3072 Bit ist 2026 das Minimum f\u00fcr neue Schl\u00fcssel.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"troubleshooting\">Troubleshooting: 8 typische Fehlermeldungen<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Symptom \/ Fehlermeldung<\/th><th>Ursache<\/th><th>L\u00f6sung<\/th><\/tr><\/thead><tbody><tr><td><code>error:1E08010C:DECODER routines::unsupported<\/code><\/td><td>PEM-Format passt nicht zum Schl\u00fcsseltyp<\/td><td>Export-Typ pr\u00fcfen: <code>spki<\/code> f\u00fcr \u00f6ffentlich, <code>pkcs8<\/code> f\u00fcr privat<\/td><\/tr><tr><td><code>verify()<\/code> liefert <code>false<\/code> trotz korrektem Code<\/td><td>Falsches <code>dsaEncoding<\/code> oder PSS-Salt<\/td><td>Format auf beiden Seiten identisch setzen<\/td><\/tr><tr><td><code>Input buffers must have the same byte length<\/code><\/td><td>Puffer f\u00fcr <code>timingSafeEqual<\/code> ungleich lang<\/td><td>L\u00e4nge vorher pr\u00fcfen und bei Ungleichheit <code>false<\/code> zur\u00fcckgeben<\/td><\/tr><tr><td><code>error:06800066:asn1 encoding routines<\/code><\/td><td>Verschl\u00fcsselter Schl\u00fcssel ohne Passphrase geladen<\/td><td><code>passphrase<\/code> an <code>createPrivateKey<\/code> \u00fcbergeben<\/td><\/tr><tr><td>Leere oder beschnittene Signatur<\/td><td>Datei mit falscher Kodierung gelesen<\/td><td>Base64 korrekt dekodieren, <code>utf8<\/code> beim Lesen der .sig-Datei<\/td><\/tr><tr><td>Signatur \u00e4ndert sich bei ECDSA jedes Mal<\/td><td>Normales Verhalten (zuf\u00e4lliges k)<\/td><td>Kein Fehler; nur Ed25519 ist deterministisch<\/td><\/tr><tr><td><code>ERR_OSSL_EVP_UNSUPPORTED<\/code><\/td><td>Algorithmus von OpenSSL deaktiviert<\/td><td>Node aktualisieren, modernes Verfahren w\u00e4hlen<\/td><\/tr><tr><td>Pr\u00fcfung scheitert nach Datenbankspeicherung<\/td><td>Signatur als String beschnitten oder umkodiert<\/td><td>Als Base64 oder Hex speichern, nicht als rohes Bin\u00e4r in Textspalte<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn ein Fehler bleibt, isolieren Sie das Problem: Signieren und pr\u00fcfen 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"erweiterte-tipps\">Erweiterte Tipps f\u00fcr die Produktion<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"schluesselrotation\">Schl\u00fcsselrotation und Versionierung<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Geben Sie jedem Schl\u00fcssel eine ID (<code>kid<\/code>) und legen Sie diese neben die Signatur. So k\u00f6nnen Sie alte Signaturen weiter pr\u00fcfen, w\u00e4hrend neue mit einem frischen Schl\u00fcssel entstehen. Rotieren Sie Signaturschl\u00fcssel mindestens j\u00e4hrlich und sofort bei Verdacht auf Kompromittierung.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"zeitstempel\">Zeitstempel gegen Replay-Angriffe<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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 \u00e4lter als wenige Minuten ist. Stripe und GitHub machen das bei ihren Webhook-Signaturen genau so.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Weitere Praxistipps:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Trennen Sie Signatur- und Verschl\u00fcsselungsschl\u00fcssel strikt. Ein Schl\u00fcssel, eine Aufgabe.<\/li><li>Nutzen Sie f\u00fcr hochsensible Schl\u00fcssel ein Hardware-Sicherheitsmodul (HSM) oder einen Cloud-KMS, damit der private Schl\u00fcssel das Ger\u00e4t nie verl\u00e4sst.<\/li><li>Loggen Sie jede Signaturpr\u00fcfung mit Ergebnis und Schl\u00fcssel-ID f\u00fcr die Nachvollziehbarkeit.<\/li><li>Planen Sie Post-Quanten-Verfahren ein. NIST hat mit ML-DSA (Dilithium) bereits einen Standard f\u00fcr quantensichere Signaturen ver\u00f6ffentlicht.<\/li><li>Verwenden Sie f\u00fcr die Langzeitvalidierung einen qualifizierten Zeitstempeldienst, damit Signaturen auch nach Ablauf eines Zertifikats beweiskr\u00e4ftig bleiben.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"eidas-id-austria\">Rechtlicher Rahmen: eIDAS und ID Austria<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Eine technisch korrekte <strong>digitale Signatur<\/strong> ist nicht automatisch eine rechtsg\u00fcltige 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr eine QES brauchen Sie ein qualifiziertes Zertifikat von einem Vertrauensdiensteanbieter und eine sichere Signaturerstellungseinheit. In \u00d6sterreich ist ID Austria die zentrale Plattform f\u00fcr die elektronische Identit\u00e4t und erm\u00f6glicht 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\u00e4ge rechtsverbindlich unterzeichnen wollen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr interne Zwecke wie Software-Updates, Webhook-Authentifizierung oder API-Signierung reicht die selbst erzeugte Signatur vollkommen aus. Sobald es um Rechtsgesch\u00e4fte mit Dritten geht, pr\u00fcfen Sie den eIDAS-Status. Die offiziellen Grundlagen finden Sie in der <a href=\"https:\/\/digital-strategy.ec.europa.eu\/en\/policies\/eidas-regulation\" rel=\"noopener\" target=\"_blank\">eIDAS-Dokumentation der EU-Kommission<\/a> und auf der Seite zu <a href=\"https:\/\/www.oesterreich.gv.at\/id-austria.html\" rel=\"noopener\" target=\"_blank\">ID Austria<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fazit\">Fazit: In 11 Schritten zum eigenen Signatur-Tool<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sie haben in diesem Tutorial ein vollst\u00e4ndiges Signatur-Werkzeug gebaut, das Schl\u00fcssel erzeugt, Dateien signiert und Signaturen pr\u00fcft, dazu eine Express-Integration f\u00fcr Webhooks und drei automatisierte Tests. Das Wichtigste in K\u00fcrze: W\u00e4hlen Sie Ed25519 f\u00fcr neue Projekte, ECDSA P-256 f\u00fcr die Web-Kompatibilit\u00e4t und RSA-PSS nur f\u00fcr Altsysteme. Sch\u00fctzen Sie den privaten Schl\u00fcssel 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\u00dflich mit <code>timingSafeEqual<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der n\u00e4chste sinnvolle Schritt ist die Integration in Ihre bestehende Pipeline: Signieren Sie Ihre Release-Artefakte automatisch im CI-Lauf und pr\u00fcfen Sie sie vor jedem Deployment. So wird die <strong>digitale Signatur<\/strong> vom Einzelbeispiel zum festen Bestandteil Ihrer Sicherheitskette. Wer rechtsverbindliche Unterschriften braucht, kombiniert das hier Gelernte mit einem qualifizierten Vertrauensdiensteanbieter \u00fcber ID Austria.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq\">H\u00e4ufige Fragen zur digitalen Signatur in Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"brauche-ich-fuer-digitale-signaturen-in-node-js-eine-externe-bibliothek\">Brauche ich f\u00fcr digitale Signaturen in Node.js eine externe Bibliothek?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nein. Das eingebaute <code>crypto<\/code>-Modul deckt Ed25519, ECDSA, RSA-PSS und alle g\u00e4ngigen Hashfunktionen ab. Externe Pakete brauchen Sie erst f\u00fcr Spezialf\u00e4lle wie das Signieren von PDF-Dokumenten nach PAdES oder f\u00fcr Post-Quanten-Verfahren.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"was-ist-der-unterschied-zwischen-signatur-und-verschluesselung\">Was ist der Unterschied zwischen Signatur und Verschl\u00fcsselung?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Verschl\u00fcsselung verbirgt den Inhalt, eine Signatur l\u00e4sst ihn lesbar und beweist nur Echtheit und Unversehrtheit. Beim Signieren nutzen Sie den privaten Schl\u00fcssel zum Erstellen und den \u00f6ffentlichen zum Pr\u00fcfen. Bei der asymmetrischen Verschl\u00fcsselung ist es umgekehrt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"warum-gilt-ed25519-als-beste-standardwahl\">Warum gilt Ed25519 als beste Standardwahl?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ed25519 ist schnell, hat winzige Schl\u00fcssel (32 Byte) und kurze Signaturen (64 Byte) und arbeitet deterministisch. Dadurch entf\u00e4llt die riskante Nonce-Erzeugung, die ECDSA bei schlechter Zufallsquelle gef\u00e4hrlich macht. F\u00fcr neue Projekte ohne Kompatibilit\u00e4tszwang ist es die erste Wahl.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ist-eine-selbst-erstellte-signatur-in-oesterreich-rechtsgueltig\">Ist eine selbst erstellte Signatur in \u00d6sterreich rechtsg\u00fcltig?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Technisch ja, rechtlich nur eingeschr\u00e4nkt. Eine handschriftliche Unterschrift ersetzt nur die qualifizierte elektronische Signatur (QES) nach eIDAS. Daf\u00fcr brauchen Sie einen qualifizierten Vertrauensdiensteanbieter, in \u00d6sterreich typischerweise \u00fcber ID Austria. F\u00fcr technische Zwecke wie Webhooks oder Updates ist die selbst erzeugte Signatur ausreichend.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"wie-sichere-ich-den-privaten-schluessel-richtig\">Wie sichere ich den privaten Schl\u00fcssel richtig?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Verschl\u00fcsseln 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\u00f6rt er in einen Secrets-Manager oder ein HSM, nicht in eine Datei neben dem Code.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"bedroht-quantencomputing-meine-digitalen-signaturen\">Bedroht Quantencomputing meine digitalen Signaturen?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Langfristig ja. Ein ausreichend gro\u00dfer Quantencomputer k\u00f6nnte RSA und ECDSA brechen. NIST hat daf\u00fcr bereits quantensichere Standards wie ML-DSA (Dilithium) ver\u00f6ffentlicht. F\u00fcr heutige Systeme ist Ed25519 oder ECDSA sicher, planen Sie aber die Migration ein. Mehr dazu in unserem Beitrag zu <a href=\"\/at\/cryptography-hub\/\">Hashing und Kryptographie<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"welche-hashfunktion-soll-ich-fuer-die-signatur-verwenden\">Welche Hashfunktion soll ich f\u00fcr die Signatur verwenden?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Mindestens SHA-256. MD5 und SHA-1 sind gebrochen und d\u00fcrfen nicht mehr eingesetzt werden. Bei Ed25519 ist die Hashfunktion (SHA-512) fest im Verfahren verankert, Sie m\u00fcssen sie nicht angeben.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"related-coverage\">Related Coverage<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"weiterfuehrende-artikel\">Weiterf\u00fchrende Artikel<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/at\/digitale-signaturen\/\">Digitale Signaturen erkl\u00e4rt: So funktionieren sie<\/a><\/li><li><a href=\"\/at\/sha-256\/\">SHA-256 erkl\u00e4rt: So funktioniert es<\/a><\/li><li><a href=\"\/at\/hashfunktion\/\">Was ist eine Hashfunktion? So funktioniert Hashing<\/a><\/li><li><a href=\"\/at\/https-und-tls\/\">HTTPS und TLS: Wie das Schloss im Browser Sie sch\u00fctzt<\/a><\/li><li><a href=\"\/at\/cryptography-hub\/\">Hashing und Kryptographie erkl\u00e4rt<\/a><\/li><li><a href=\"\/at\/ssh-key-einrichten\/\">SSH-Key einrichten: Server h\u00e4rten in 10 Schritten<\/a><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Externe Referenzen zum Vertiefen: die <a href=\"https:\/\/nodejs.org\/api\/crypto.html\" rel=\"noopener\" target=\"_blank\">offizielle Node.js crypto-Dokumentation<\/a>, der <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc8032\" rel=\"noopener\" target=\"_blank\">RFC 8032 zu EdDSA und Ed25519<\/a> sowie der <a href=\"https:\/\/csrc.nist.gov\/pubs\/fips\/186-5\/final\" rel=\"noopener\" target=\"_blank\">NIST-Standard FIPS 186-5 f\u00fcr digitale Signaturen<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Eine digitale Signatur beweist zwei Dinge auf einmal: dass eine Nachricht wirklich von Ihnen stammt und dass niemand sie unterwegs ver\u00e4ndert hat. Genau das brauchen Sie f\u00fcr Webhooks, Software-Updates, API-Anfragen\u2026<\/p>\n","protected":false},"author":4,"featured_media":103,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-102","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cryptography"],"_links":{"self":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts\/102","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/comments?post=102"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts\/102\/revisions"}],"predecessor-version":[{"id":104,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts\/102\/revisions\/104"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/media\/103"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/media?parent=102"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/categories?post=102"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/tags?post=102"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}