{"id":67,"date":"2026-06-12T16:38:17","date_gmt":"2026-06-12T16:38:17","guid":{"rendered":"https:\/\/shattered.io\/dk\/2026\/06\/12\/ed25519-signaturer-nodejs\/"},"modified":"2026-06-12T16:40:06","modified_gmt":"2026-06-12T16:40:06","slug":"ed25519-signaturer-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/dk\/2026\/06\/12\/ed25519-signaturer-nodejs\/","title":{"rendered":"Ed25519 i Node.js: signaturer i 12 trin [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Digitale signaturer er rygraden i moderne tillid p\u00e5 nettet. De beviser, at en besked stammer fra en bestemt afsender, og at ingen har \u00e6ndret den undervejs. Blandt de mange signatur-algoritmer skiller <strong>Ed25519<\/strong> sig ud: n\u00f8glerne fylder 32 bytes, signaturen 64 bytes, og hele skemaet er bygget til at v\u00e6re sv\u00e6rt at bruge forkert. I denne tutorial bygger du et komplet, fungerende projekt i Node.js, der genererer n\u00f8gler, signerer data og verificerer signaturer, helt uden tredjepartsbiblioteker.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Vi bruger udelukkende det indbyggede <code>crypto<\/code>-modul, som f\u00f8lger med Node.js. Du skriver kode til n\u00f8glegenerering, PEM-lagring, signering, verifikation, et CLI-v\u00e6rkt\u00f8j og et lille API, der validerer signerede tokens. Undervejs gennemg\u00e5r vi de f\u00e6lder, danske udviklere oftest falder i, plus 8 konkrete fejlfindings-scenarier. Alt er testet mod Node.js 24 LTS og virker p\u00e5 Node.js 22 og nyere.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"hvad-er-ed25519-og-hvorfor-digitale-signaturer-i-node-js\">Hvad er Ed25519, og hvorfor digitale signaturer i Node.js?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ed25519 er en konkret instans af signatur-skemaet EdDSA (Edwards-curve Digital Signature Algorithm), defineret i <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc8032\" target=\"_blank\" rel=\"noopener\">RFC 8032<\/a>. Det bygger p\u00e5 den elliptiske kurve Curve25519, oprindeligt designet af kryptografen Daniel J. Bernstein. NIST standardiserede EdDSA i FIPS 186-5 i 2023, hvilket \u00e5bnede d\u00f8ren for brug i regulerede milj\u00f8er i USA og dermed indirekte ogs\u00e5 i mange europ\u00e6iske compliance-rammer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En digital signatur l\u00f8ser tre problemer p\u00e5 \u00e9n gang. Den giver <strong>autenticitet<\/strong> (beskeden kom fra indehaveren af den private n\u00f8gle), <strong>integritet<\/strong> (indholdet er u\u00e6ndret) og <strong>uafviselighed<\/strong> (afsenderen kan ikke senere n\u00e6gte at have signeret). Hvis du vil have det konceptuelle fundament p\u00e5 plads f\u00f8rst, har vi en grundig forklaring i artiklen <a href=\"https:\/\/shattered.io\/dk\/cryptography\/digitale-signaturer\/\">Digitale signaturer: hvordan hashing og n\u00f8gler skaber tillid<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Hvorfor Ed25519 frem for de \u00e6ldre alternativer? Tre grunde dominerer. For det f\u00f8rste er signaturerne <strong>deterministiske<\/strong>: samme n\u00f8gle plus samme besked giver altid samme signatur, s\u00e5 du undg\u00e5r hele klassen af fejl, der stammer fra d\u00e5rlig tilf\u00e6ldighed under signering. ECDSA blev berygtet, da Sony i 2010 genbrugte den samme tilf\u00e6ldige v\u00e6rdi og dermed l\u00e6kkede PlayStation 3-signaturn\u00f8glen. For det andet er n\u00f8gler og signaturer faste i st\u00f8rrelse og sm\u00e5. For det tredje k\u00f8rer verifikation hurtigt, hvilket betyder noget, n\u00e5r en server validerer tusindvis af tokens i sekundet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ed25519 leverer cirka 128 bit sikkerhed. Det er p\u00e5 niveau med RSA-3072 og AES-128, og det regnes som solidt mod alle kendte klassiske angreb. En vigtig advarsel: Ed25519 er <strong>ikke<\/strong> kvanteresistent. En tilstr\u00e6kkelig stor, fejltolerant kvantecomputer ville kunne bryde den med Shors algoritme. Til langtidsholdbar arkivsignering b\u00f8r du f\u00f8lge med i overgangen til post-kvante-signaturer som ML-DSA (Dilithium), men til langt de fleste produktionsbehov i 2026 er Ed25519 det rigtige valg.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ed25519-mod-ecdsa-og-rsa-tal-der-betyder-noget\">Ed25519 mod ECDSA og RSA: tal der betyder noget<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00f8r du skriver kode, hj\u00e6lper det at se de konkrete forskelle. Tabellen nedenfor sammenligner de tre mest udbredte signatur-familier p\u00e5 de parametre, der p\u00e5virker dit design: n\u00f8gle- og signaturst\u00f8rrelse, sikkerhedsniveau og praktisk hastighed. Tallene for n\u00f8gle- og signaturst\u00f8rrelser er faste egenskaber ved algoritmerne, ikke estimater.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Egenskab<\/th><th>Ed25519<\/th><th>ECDSA (P-256)<\/th><th>RSA-2048<\/th><th>RSA-4096<\/th><\/tr><\/thead><tbody><tr><td>Offentlig n\u00f8gle<\/td><td>32 bytes<\/td><td>~65 bytes<\/td><td>~270 bytes<\/td><td>~540 bytes<\/td><\/tr><tr><td>Privat n\u00f8gle (seed)<\/td><td>32 bytes<\/td><td>32 bytes<\/td><td>~1190 bytes<\/td><td>~2350 bytes<\/td><\/tr><tr><td>Signaturst\u00f8rrelse<\/td><td>64 bytes<\/td><td>~72 bytes<\/td><td>256 bytes<\/td><td>512 bytes<\/td><\/tr><tr><td>Sikkerhedsniveau<\/td><td>~128 bit<\/td><td>~128 bit<\/td><td>~112 bit<\/td><td>~140 bit<\/td><\/tr><tr><td>Deterministisk signatur<\/td><td>Ja (indbygget)<\/td><td>Nej (kr\u00e6ver RFC 6979)<\/td><td>Ja (for RSASSA)<\/td><td>Ja<\/td><\/tr><tr><td>Signeringshastighed<\/td><td>Meget hurtig<\/td><td>Hurtig<\/td><td>Langsom<\/td><td>Meget langsom<\/td><\/tr><tr><td>Verifikationshastighed<\/td><td>Hurtig<\/td><td>Hurtig<\/td><td>Meget hurtig<\/td><td>Hurtig<\/td><\/tr><tr><td>Kvanteresistent<\/td><td>Nej<\/td><td>Nej<\/td><td>Nej<\/td><td>Nej<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Det praktiske budskab: en Ed25519-signatur fylder 64 bytes mod 256 bytes for RSA-2048 og 512 bytes for RSA-4096. N\u00e5r du sender signaturer i HTTP-headers, QR-koder eller embeddede enheder, er den forskel afg\u00f8rende. RSA vinder kun \u00e9t sted, nemlig ren verifikationshastighed med lille offentlig eksponent, men det opvejes sj\u00e6ldent af de meget st\u00f8rre n\u00f8gler og signaturer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ed25519 er i dag udbredt i steder, du m\u00e5ske bruger dagligt. OpenSSH tilbyder <code>ssh-ed25519<\/code>-n\u00f8gler som standard-anbefaling, GitHub accepterer dem til Git-godkendelse, TLS 1.3 underst\u00f8tter dem til certifikater, Tor bruger dem til l\u00f8g-tjenester, krypteringsv\u00e6rkt\u00f8jet age bygger p\u00e5 X25519, og Signal-protokollen bruger en variant kaldet XEdDSA. Den brede ibrugtagning betyder modent \u00f8kosystem og god interoperabilitet p\u00e5 tv\u00e6rs af sprog.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"forudsaetninger-og-versioner\">Foruds\u00e6tninger og versioner<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Du skal bruge et lille, pr\u00e6cist s\u00e6t v\u00e6rkt\u00f8jer. Ed25519-underst\u00f8ttelsen i Node.js&#8217; <code>crypto<\/code>-modul har v\u00e6ret stabil l\u00e6nge, s\u00e5 kravene er beskedne. Kontroll\u00e9r din ops\u00e6tning mod denne liste, f\u00f8r du g\u00e5r videre.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Node.js 22 LTS eller nyere<\/strong>. Tutorialen er testet p\u00e5 Node.js 24 LTS, som er den anbefalede produktionslinje i 2026 (LTS frem til april 2028). Node.js 22 fungerer ogs\u00e5 fuldt ud.<\/li><li><strong>npm 10 eller nyere<\/strong>, der f\u00f8lger med Node.js-installationen.<\/li><li><strong>En terminal<\/strong> (Terminal p\u00e5 macOS\/Linux, PowerShell eller Windows Terminal p\u00e5 Windows).<\/li><li><strong>En teksteditor<\/strong> som VS Code.<\/li><li><strong>Grundl\u00e6ggende JavaScript<\/strong>: funktioner, <code>Buffer<\/code> og asynkron kode. Du beh\u00f8ver ingen kryptografi-baggrund.<\/li><li><strong>Ingen eksterne pakker<\/strong> til selve kryptografien. Vi tilf\u00f8jer kun Express i det sidste API-trin.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Bekr\u00e6ft din Node-version, s\u00e5 du ved, at API&#8217;erne i denne guide er til r\u00e5dighed:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ node --version\nv24.4.0\n\n$ npm --version\n10.9.2<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hvis <code>node --version<\/code> viser v20 eller \u00e6ldre, s\u00e5 opgrader. De \u00e6ldre linjer k\u00f8rer koden her, men de er enten uden for sikkerhedssupport eller p\u00e5 vej ud, og du vil have de nyeste rettelser i <code>crypto<\/code>-modulet. Tabellen viser kompatibilitet for de relevante API-kald.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Node.js-version<\/th><th>Status i 2026<\/th><th>Ed25519 n\u00f8glegen.<\/th><th>crypto.sign\/verify (null)<\/th><th>JWK-eksport<\/th><\/tr><\/thead><tbody><tr><td>Node 18<\/td><td>Udfaset<\/td><td>Ja<\/td><td>Ja<\/td><td>Ja<\/td><\/tr><tr><td>Node 20<\/td><td>Vedligehold<\/td><td>Ja<\/td><td>Ja<\/td><td>Ja<\/td><\/tr><tr><td>Node 22 LTS<\/td><td>Aktiv LTS<\/td><td>Ja<\/td><td>Ja<\/td><td>Ja<\/td><\/tr><tr><td>Node 24 LTS<\/td><td>Anbefalet LTS<\/td><td>Ja<\/td><td>Ja<\/td><td>Ja<\/td><\/tr><tr><td>Node 26 Current<\/td><td>Nyeste<\/td><td>Ja<\/td><td>Ja<\/td><td>Ja<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-1-opsaet-projektet\">Trin 1: Ops\u00e6t projektet<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start med en ren projektmappe og en minimal struktur. Vi adskiller n\u00f8gleh\u00e5ndtering, signering og verifikation i hver sin fil, s\u00e5 koden bliver let at genbruge i et CLI og et API senere.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ mkdir ed25519-signaturer && cd ed25519-signaturer\n$ npm init -y\n$ mkdir src keys\n$ touch src\/keys.js src\/sign.js src\/verify.js src\/token.js index.js<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c5bn <code>package.json<\/code>, og tilf\u00f8j et par scripts, s\u00e5 du kan k\u00f8re kommandoer uden at huske filstier. Vi bruger CommonJS gennem hele tutorialen, fordi det k\u00f8rer uden ekstra konfiguration p\u00e5 alle underst\u00f8ttede Node-versioner.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"name\": \"ed25519-signaturer\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Digitale signaturer med Ed25519 i Node.js\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"keygen\": \"node index.js keygen\",\n    \"sign\": \"node index.js sign\",\n    \"verify\": \"node index.js verify\",\n    \"serve\": \"node server.js\"\n  },\n  \"license\": \"MIT\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tilf\u00f8j med det samme en <code>.gitignore<\/code>, s\u00e5 du aldrig kommer til at committe private n\u00f8gler. Dette er ikke valgfrit. En l\u00e6kket privat n\u00f8gle i et Git-repo er en af de hyppigste \u00e5rsager til kompromitterede signaturn\u00f8gler.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .gitignore\nnode_modules\/\nkeys\/\n*.pem\n.env<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-2-generer-et-ed25519-noeglepar\">Trin 2: Gener\u00e9r et Ed25519-n\u00f8glepar<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00f8glegenerering er \u00e9n funktion. Node.js&#8217; <code>crypto.generateKeyPairSync('ed25519')<\/code> returnerer et privat og et offentligt <code>KeyObject<\/code>. Den synkrone variant er fin til et CLI; til en server, der genererer n\u00f8gler under belastning, ville du bruge den asynkrone <code>generateKeyPair<\/code>. Skriv f\u00f8lgende i <code>src\/keys.js<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/keys.js\nconst crypto = require('node:crypto');\nconst fs = require('node:fs');\nconst path = require('node:path');\n\nconst KEY_DIR = path.join(__dirname, '..', 'keys');\nconst PRIV_PATH = path.join(KEY_DIR, 'private.pem');\nconst PUB_PATH = path.join(KEY_DIR, 'public.pem');\n\nfunction generateKeyPair() {\n  const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');\n  return { privateKey, publicKey };\n}\n\nmodule.exports = { generateKeyPair, KEY_DIR, PRIV_PATH, PUB_PATH };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Bem\u00e6rk, at du ikke angiver nogen kurve, modulus-l\u00e6ngde eller hash. Ed25519 har pr\u00e6cis \u00e9t parameters\u00e6t, og det er hele pointen. Hvor RSA tvinger dig til at v\u00e6lge n\u00f8glel\u00e6ngde, og ECDSA kr\u00e6ver et kurvevalg, fjerner Ed25519 beslutningen og dermed muligheden for at v\u00e6lge forkert.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-3-eksporter-og-gem-noegler-som-pem\">Trin 3: Eksport\u00e9r og gem n\u00f8gler som PEM<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Et <code>KeyObject<\/code> lever kun i hukommelsen. For at gemme n\u00f8glerne p\u00e5 disk eksporterer du dem til PEM-format, som er Base64-tekst med tydelige header-linjer. Den private n\u00f8gle bruger PKCS#8-struktur, den offentlige bruger SPKI. Udvid <code>src\/keys.js<\/code> med en gem-funktion.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Tilf\u00f8j i src\/keys.js\nfunction saveKeyPair({ privateKey, publicKey }) {\n  if (!fs.existsSync(KEY_DIR)) {\n    fs.mkdirSync(KEY_DIR, { recursive: true });\n  }\n\n  const privPem = privateKey.export({ type: 'pkcs8', format: 'pem' });\n  const pubPem = publicKey.export({ type: 'spki', format: 'pem' });\n\n  \/\/ Privat noegle: kun ejeren maa laese (0600 paa Unix)\n  fs.writeFileSync(PRIV_PATH, privPem, { mode: 0o600 });\n  fs.writeFileSync(PUB_PATH, pubPem);\n\n  return { privPem, pubPem };\n}\n\nmodule.exports.saveKeyPair = saveKeyPair;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Filtilladelsen <code>0o600<\/code> betyder, at kun ejeren kan l\u00e6se og skrive den private n\u00f8gle. P\u00e5 et Unix-system er det f\u00f8rste forsvarslinje mod, at andre brugere p\u00e5 maskinen kan stj\u00e6le din signaturn\u00f8gle. En genereret offentlig n\u00f8gle ser s\u00e5dan ud, kort og l\u00e6sbar:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=\n-----END PUBLIC KEY-----<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hele den offentlige n\u00f8gle er en enkelt kort linje Base64. Det understreger, hvor lille Ed25519 er sammenlignet med en RSA-n\u00f8gle, der typisk fylder otte til tolv linjer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-4-indlaes-noegler-fra-disk\">Trin 4: Indl\u00e6s n\u00f8gler fra disk<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e5r n\u00f8glerne ligger p\u00e5 disk, skal du kunne l\u00e6se dem ind igen som <code>KeyObject<\/code>. Brug <code>crypto.createPrivateKey<\/code> og <code>crypto.createPublicKey<\/code>. De accepterer PEM-strenge direkte.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Tilf\u00f8j i src\/keys.js\nfunction loadPrivateKey() {\n  const pem = fs.readFileSync(PRIV_PATH, 'utf8');\n  return crypto.createPrivateKey(pem);\n}\n\nfunction loadPublicKey() {\n  const pem = fs.readFileSync(PUB_PATH, 'utf8');\n  return crypto.createPublicKey(pem);\n}\n\nmodule.exports.loadPrivateKey = loadPrivateKey;\nmodule.exports.loadPublicKey = loadPublicKey;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Et nyttigt trick: du kan altid udlede den offentlige n\u00f8gle fra den private. <code>crypto.createPublicKey(privateKey)<\/code> giver det matchende offentlige <code>KeyObject<\/code>. Det er praktisk, hvis du kun har den private n\u00f8gle ved h\u00e5nden og vil dele den offentlige del med andre.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-5-signer-en-besked-med-crypto-sign\">Trin 5: Sign\u00e9r en besked med crypto.sign<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nu kommer kernen. For Ed25519 er signering \u00e9t kald: <code>crypto.sign(null, data, privateKey)<\/code>. Det f\u00f8rste argument, normalt en hash-algoritme, skal v\u00e6re <code>null<\/code>. Ed25519 hasher selv beskeden internt med SHA-512 som en del af skemaet, s\u00e5 du m\u00e5 ikke selv angive en hash. Skriv <code>src\/sign.js<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/sign.js\nconst crypto = require('node:crypto');\n\n\/\/ Signerer en besked med en Ed25519 privat noegle.\n\/\/ message: Buffer eller string. privateKey: Ed25519 KeyObject.\n\/\/ Returnerer en 64-byte signatur (Buffer).\nfunction signMessage(message, privateKey) {\n  const data = Buffer.isBuffer(message) ? message : Buffer.from(message, 'utf8');\n  \/\/ null = ingen pre-hash. Korrekt for Ed25519.\n  return crypto.sign(null, data, privateKey);\n}\n\nmodule.exports = { signMessage };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L\u00e6g m\u00e6rke til, at vi normaliserer input til en <code>Buffer<\/code>. Hvis du blander tekst og bin\u00e6re data uden eksplicit kodning, risikerer du, at samme logiske besked giver forskellige bytes p\u00e5 afsender- og modtagerside, hvilket f\u00e5r verifikationen til at fejle. Resultatet er altid n\u00f8jagtig 64 bytes, uanset hvor lang beskeden er.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-6-verificer-signaturen\">Trin 6: Verific\u00e9r signaturen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Verifikation spejler signering: <code>crypto.verify(null, data, publicKey, signature)<\/code> returnerer en boolean. Igen er det f\u00f8rste argument <code>null<\/code>. Funktionen er konstant-tids-sikker internt, s\u00e5 du beh\u00f8ver ikke selv t\u00e6nke p\u00e5 timing-angreb her. Skriv <code>src\/verify.js<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/verify.js\nconst crypto = require('node:crypto');\n\n\/\/ Verificerer en Ed25519-signatur.\n\/\/ message: original besked. signature: 64-byte Buffer. publicKey: KeyObject.\n\/\/ Returnerer true hvis signaturen er gyldig.\nfunction verifyMessage(message, signature, publicKey) {\n  const data = Buffer.isBuffer(message) ? message : Buffer.from(message, 'utf8');\n  return crypto.verify(null, data, publicKey, signature);\n}\n\nmodule.exports = { verifyMessage };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lad os teste signering og verifikation sammen. Lav en hurtig pr\u00f8vefil, <code>test-quick.js<\/code>, og k\u00f8r den.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ test-quick.js\nconst { generateKeyPair } = require('.\/src\/keys');\nconst { signMessage } = require('.\/src\/sign');\nconst { verifyMessage } = require('.\/src\/verify');\n\nconst { privateKey, publicKey } = generateKeyPair();\nconst besked = 'Overfoer 500 DKK til konto 1234';\n\nconst sig = signMessage(besked, privateKey);\nconsole.log('Signatur (hex):', sig.toString('hex'));\nconsole.log('Signaturlaengde:', sig.length, 'bytes');\nconsole.log('Gyldig:', verifyMessage(besked, sig, publicKey));\n\n\/\/ Manipuleret besked skal fejle\nconsole.log('Manipuleret:', verifyMessage('Overfoer 5000 DKK til konto 1234', sig, publicKey));<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>$ node test-quick.js\nSignatur (hex): 3a1f...c904\nSignaturlaengde: 64 bytes\nGyldig: true\nManipuleret: false<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Den \u00e6gte besked verificerer som <code>true<\/code>, og en enkelt \u00e6ndret ciffer i bel\u00f8bet giver <code>false<\/code>. Det er digitale signaturer i praksis: selv den mindste \u00e6ndring i de signerede data ugyldigg\u00f8r signaturen. Princippet bygger p\u00e5 de samme egenskaber, vi gennemg\u00e5r i <a href=\"https:\/\/shattered.io\/dk\/cryptography\/hashfunktioner\/\">Hashfunktioner: egenskaber, form\u00e5l og praktisk brug<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-7-byg-et-signeret-token-format\">Trin 7: Byg et signeret token-format<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">En r\u00e5 signatur er sj\u00e6ldent nok i praksis. Du vil pakke en payload, en signatur og lidt metadata ind i \u00e9t transportabelt token. Vi bygger et kompakt format inspireret af JWT, men med Ed25519 og uden de kendte JWT-faldgruber. Formatet er to base64url-segmenter adskilt af et punktum: <code>payload.signatur<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En vigtig detalje: vi serialiserer payload med sorterede n\u00f8gler, s\u00e5 samme logiske objekt altid giver samme bytes. Uden kanonisk serialisering kan to ens objekter producere forskellig JSON og dermed forskellige signaturer. Skriv <code>src\/token.js<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/token.js\nconst { signMessage } = require('.\/sign');\nconst { verifyMessage } = require('.\/verify');\n\n\/\/ Kanonisk JSON: sorterede noegler giver deterministisk output\nfunction canonicalJSON(obj) {\n  if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);\n  if (Array.isArray(obj)) return '[' + obj.map(canonicalJSON).join(',') + ']';\n  const keys = Object.keys(obj).sort();\n  return '{' + keys.map(function (k) {\n    return JSON.stringify(k) + ':' + canonicalJSON(obj[k]);\n  }).join(',') + '}';\n}\n\nfunction base64url(buf) {\n  return buf.toString('base64url');\n}\n\nfunction fromBase64url(str) {\n  return Buffer.from(str, 'base64url');\n}\n\nfunction createToken(payload, privateKey) {\n  const json = canonicalJSON(payload);\n  const payloadB64 = base64url(Buffer.from(json, 'utf8'));\n  const sig = signMessage(payloadB64, privateKey);\n  return payloadB64 + '.' + base64url(sig);\n}\n\nfunction verifyToken(token, publicKey) {\n  const parts = token.split('.');\n  if (parts.length !== 2) return { valid: false, reason: 'forkert format' };\n  const payloadB64 = parts[0];\n  const sig = fromBase64url(parts[1]);\n  if (sig.length !== 64) return { valid: false, reason: 'forkert signaturlaengde' };\n  const valid = verifyMessage(payloadB64, sig, publicKey);\n  if (!valid) return { valid: false, reason: 'ugyldig signatur' };\n  const payload = JSON.parse(fromBase64url(payloadB64).toString('utf8'));\n  return { valid: true, payload: payload };\n}\n\nmodule.exports = { createToken, verifyToken, canonicalJSON };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Vi signerer den base64url-kodede payload, ikke det r\u00e5 objekt. Det betyder, at verifikatoren signerer pr\u00e6cis de samme bytes, som blev sendt over ledningen, uden at skulle re-serialisere JSON. Det eliminerer en hel klasse af subtile fejl, hvor afsender og modtager er uenige om mellemrum eller n\u00f8gler\u00e6kkef\u00f8lge.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-8-tilfoej-udloebstid-og-claims\">Trin 8: Tilf\u00f8j udl\u00f8bstid og claims<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Et token uden udl\u00f8bstid er en sikkerhedsrisiko. Hvis det l\u00e6kker, g\u00e6lder det for evigt. Udvid token-laget med standardiserede claims: <code>iat<\/code> (issued at), <code>exp<\/code> (expiry) og <code>sub<\/code> (subject). Vi tilf\u00f8jer en hj\u00e6lper, der bygger payload og tjekker udl\u00f8b ved verifikation.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Tilf\u00f8j i src\/token.js\nfunction issue(subject, claims, privateKey, ttlSeconds) {\n  ttlSeconds = ttlSeconds || 3600;\n  const now = Math.floor(Date.now() \/ 1000);\n  const payload = Object.assign({\n    sub: subject,\n    iat: now,\n    exp: now + ttlSeconds,\n  }, claims);\n  return createToken(payload, privateKey);\n}\n\nfunction validate(token, publicKey) {\n  const result = verifyToken(token, publicKey);\n  if (!result.valid) return result;\n  const now = Math.floor(Date.now() \/ 1000);\n  if (result.payload.exp && now > result.payload.exp) {\n    return { valid: false, reason: 'token udloebet' };\n  }\n  return result;\n}\n\nmodule.exports.issue = issue;\nmodule.exports.validate = validate;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Nu kan du udstede et token for en bruger med rettigheder og en levetid p\u00e5 en time. Verifikatoren afviser b\u00e5de manipulerede og udl\u00f8bne tokens. Det er det samme m\u00f8nster, vi bruger i vores guides om <a href=\"https:\/\/shattered.io\/dk\/security\/kodeordssikkerhed\/\">kodeordssikkerhed<\/a> og sessionsh\u00e5ndtering, blot med asymmetriske n\u00f8gler i stedet for en delt hemmelighed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-9-arbejd-med-raa-32-byte-noegler-og-jwk\">Trin 9: Arbejd med r\u00e5 32-byte-n\u00f8gler og JWK<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nogle gange skal du udveksle n\u00f8gler med et system, der ikke forst\u00e5r PEM, for eksempel en mobilklient eller et bibliotek i et andet sprog, der forventer de r\u00e5 32 bytes. Node.js eksponerer ikke r\u00e5 Ed25519-bytes direkte, men du kan komme til dem via JWK-formatet, hvor felterne <code>x<\/code> (offentlig) og <code>d<\/code> (privat) er base64url-kodede r\u00e5 n\u00f8gler.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ raw-keys.js: udtraek raa bytes via JWK\nconst crypto = require('node:crypto');\nconst { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');\n\nconst pubJwk = publicKey.export({ format: 'jwk' });\nconst privJwk = privateKey.export({ format: 'jwk' });\n\nconst rawPublic = Buffer.from(pubJwk.x, 'base64url');   \/\/ 32 bytes\nconst rawPrivate = Buffer.from(privJwk.d, 'base64url'); \/\/ 32 bytes\n\nconsole.log('Raa offentlig noegle:', rawPublic.length, 'bytes');\nconsole.log('Raa privat noegle:', rawPrivate.length, 'bytes');\n\n\/\/ Genskab et KeyObject fra raa offentlige bytes\nfunction publicKeyFromRaw(raw32) {\n  return crypto.createPublicKey({\n    key: { kty: 'OKP', crv: 'Ed25519', x: raw32.toString('base64url') },\n    format: 'jwk',\n  });\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>$ node raw-keys.js\nRaa offentlig noegle: 32 bytes\nRaa privat noegle: 32 bytes<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Felttypen <code>OKP<\/code> st\u00e5r for Octet Key Pair og er JWK-kategorien for Edwards-kurver. Med denne konvertering kan din Node-server tale med en hvilken som helst Ed25519-implementering, fra libsodium i C til Web Crypto i browseren, s\u00e5 l\u00e6nge begge sider er enige om de samme 32 bytes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-10-saml-det-i-et-cli-vaerktoej\">Trin 10: Saml det i et CLI-v\u00e6rkt\u00f8j<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nu binder vi modulerne sammen til et brugbart kommandolinjev\u00e6rkt\u00f8j. <code>index.js<\/code> h\u00e5ndterer tre kommandoer: <code>keygen<\/code>, <code>sign<\/code> og <code>verify<\/code>. Det er det punkt, hvor projektet bliver til noget, du faktisk kan bruge i en build-pipeline til at signere artefakter.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ index.js\nconst fs = require('node:fs');\nconst {\n  generateKeyPair, saveKeyPair, loadPrivateKey, loadPublicKey, PUB_PATH,\n} = require('.\/src\/keys');\nconst { issue, validate } = require('.\/src\/token');\n\nconst command = process.argv[2];\nconst args = process.argv.slice(3);\n\nfunction main() {\n  if (command === 'keygen') {\n    const pair = generateKeyPair();\n    saveKeyPair(pair);\n    console.log('Noeglepar gemt i .\/keys\/');\n    console.log('Offentlig noegle:\\n' + fs.readFileSync(PUB_PATH, 'utf8'));\n  } else if (command === 'sign') {\n    const subject = args[0] || 'anonym';\n    const priv = loadPrivateKey();\n    const token = issue(subject, { role: args[1] || 'user' }, priv, 3600);\n    console.log(token);\n  } else if (command === 'verify') {\n    const token = args[0];\n    if (!token) { console.error('Brug: verify TOKEN'); process.exit(1); }\n    const pub = loadPublicKey();\n    const result = validate(token, pub);\n    console.log(JSON.stringify(result, null, 2));\n    process.exit(result.valid ? 0 : 1);\n  } else {\n    console.log('Kommandoer: keygen | sign SUBJECT ROLE | verify TOKEN');\n  }\n}\n\nmain();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00f8r hele flowet fra terminalen. F\u00f8rst genererer du n\u00f8glerne, s\u00e5 udsteder du et token, og endelig verificerer du det.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ npm run keygen\nNoeglepar gemt i .\/keys\/\n\n$ node index.js sign sam@firma.dk admin\neyJleHAiOjE3OD...wMH0.k3Jf9aQ...x2c\n\n$ node index.js verify eyJleHAiOjE3OD...wMH0.k3Jf9aQ...x2c\n{\n  \"valid\": true,\n  \"payload\": {\n    \"exp\": 1789000000,\n    \"iat\": 1788996400,\n    \"role\": \"admin\",\n    \"sub\": \"sam@firma.dk\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Exit-koden fra <code>verify<\/code> er 0 ved gyldigt token og 1 ved ugyldigt. Det g\u00f8r v\u00e6rkt\u00f8jet nemt at bruge i shell-scripts og CI-pipelines, hvor du vil afbryde en build, hvis en signatur ikke holder.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-11-eksponer-en-verifikations-api-med-express\">Trin 11: Ekspon\u00e9r en verifikations-API med Express<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Det sidste trin g\u00f8r projektet til en tjeneste. Vi bygger et lille Express-API, der udsteder tokens og beskytter en rute bag signaturverifikation. Installer Express f\u00f8rst.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ npm install express<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js\nconst express = require('express');\nconst { loadPrivateKey, loadPublicKey } = require('.\/src\/keys');\nconst { issue, validate } = require('.\/src\/token');\n\nconst app = express();\napp.use(express.json());\n\nconst privateKey = loadPrivateKey();\nconst publicKey = loadPublicKey();\n\n\/\/ Udsteder et signeret token\napp.post('\/login', function (req, res) {\n  const body = req.body || {};\n  if (!body.user) return res.status(400).json({ error: 'mangler user' });\n  const token = issue(body.user, { role: body.role || 'user' }, privateKey, 3600);\n  res.json({ token: token });\n});\n\n\/\/ Middleware der kraever et gyldigt Ed25519-token\nfunction requireToken(req, res, next) {\n  const auth = req.headers.authorization || '';\n  const token = auth.indexOf('Bearer ') === 0 ? auth.slice(7) : null;\n  if (!token) return res.status(401).json({ error: 'mangler token' });\n\n  const result = validate(token, publicKey);\n  if (!result.valid) return res.status(401).json({ error: result.reason });\n  req.user = result.payload;\n  next();\n}\n\napp.get('\/beskyttet', requireToken, function (req, res) {\n  res.json({ besked: 'Adgang givet', bruger: req.user.sub, rolle: req.user.role });\n});\n\napp.listen(3000, function () {\n  console.log('API koerer paa http:\/\/localhost:3000');\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Start serveren med <code>npm run serve<\/code>, og afpr\u00f8v den med curl. Serveren udsteder et token, og den beskyttede rute kr\u00e6ver, at signaturen verificerer.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -s -X POST http:\/\/localhost:3000\/login \\\n    -H 'Content-Type: application\/json' \\\n    -d '{\"user\":\"sam@firma.dk\",\"role\":\"admin\"}'\n{\"token\":\"eyJleHAiOj...x2c\"}\n\n$ TOKEN=\"eyJleHAiOj...x2c\"\n$ curl -s http:\/\/localhost:3000\/beskyttet -H \"Authorization: Bearer $TOKEN\"\n{\"besked\":\"Adgang givet\",\"bruger\":\"sam@firma.dk\",\"rolle\":\"admin\"}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En vigtig arkitektonisk fordel ved Ed25519 frem for en delt HMAC-hemmelighed: kun udstederen har den private n\u00f8gle. Enhver tjeneste kan verificere tokens med den offentlige n\u00f8gle uden at kunne udstede falske tokens. Det er ideelt til mikrotjenester, hvor du vil distribuere verifikation bredt, men holde signering centralt.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-12-skriv-negative-tests\">Trin 12: Skriv negative tests<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">God signaturkode er defineret af, hvad den afviser, ikke kun hvad den accepterer. Skriv tests, der bevidst fors\u00f8ger at snyde verifikatoren. Brug Node.js&#8217; indbyggede testrunner, s\u00e5 du undg\u00e5r endnu en afh\u00e6ngighed.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ test\/token.test.js\nconst { test } = require('node:test');\nconst assert = require('node:assert');\nconst crypto = require('node:crypto');\nconst { issue, validate } = require('..\/src\/token');\n\nconst a = crypto.generateKeyPairSync('ed25519');\nconst b = crypto.generateKeyPairSync('ed25519');\n\ntest('gyldigt token verificerer', function () {\n  const t = issue('sam', { role: 'admin' }, a.privateKey);\n  const r = validate(t, a.publicKey);\n  assert.strictEqual(r.valid, true);\n  assert.strictEqual(r.payload.role, 'admin');\n});\n\ntest('forkert offentlig noegle afvises', function () {\n  const t = issue('sam', {}, a.privateKey);\n  assert.strictEqual(validate(t, b.publicKey).valid, false);\n});\n\ntest('manipuleret payload afvises', function () {\n  const t = issue('sam', { role: 'user' }, a.privateKey);\n  const s = t.split('.')[1];\n  const tampered = Buffer.from('{\"role\":\"admin\"}').toString('base64url') + '.' + s;\n  assert.strictEqual(validate(tampered, a.publicKey).valid, false);\n});\n\ntest('udloebet token afvises', function () {\n  const t = issue('sam', {}, a.privateKey, -10); \/\/ udloebet for 10 sek siden\n  assert.strictEqual(validate(t, a.publicKey).reason, 'token udloebet');\n});<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>$ node --test\nok 1 - gyldigt token verificerer\nok 2 - forkert offentlig noegle afvises\nok 3 - manipuleret payload afvises\nok 4 - udloebet token afvises\n# tests 4\n# pass 4\n# fail 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Alle fire tests best\u00e5s. De d\u00e6kker de fire vigtigste angrebsscenarier: forkert n\u00f8gle, manipuleret indhold, forkert format og udl\u00f8b. N\u00e5r disse er gr\u00f8nne, ved du, at din verifikation faktisk h\u00e5ndh\u00e6ver det, den lover.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"5-faelder-der-oedelaegger-ed25519-signaturer\">5 f\u00e6lder, der \u00f8del\u00e6gger Ed25519-signaturer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">De fleste Ed25519-fejl i Node.js stammer ikke fra kryptografien, men fra hvordan udviklere h\u00e5ndterer data omkring den. Her er de fem hyppigste, vi ser i kodegennemgange.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faelde-1-at-angive-en-hash-i-stedet-for-null\">F\u00e6lde 1: At angive en hash i stedet for null<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Den klart hyppigste fejl er at skrive <code>crypto.sign('sha512', data, key)<\/code> i stedet for <code>crypto.sign(null, data, key)<\/code>. Ed25519 hasher selv internt, og hvis du angiver en algoritme, kaster Node enten en fejl eller producerer en signatur, der ikke kan verificeres af andre korrekte implementeringer. Brug altid <code>null<\/code> som f\u00f8rste argument for Ed25519.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faelde-2-at-blande-base64-og-base64url\">F\u00e6lde 2: At blande base64 og base64url<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Standard-base64 bruger tegnene plus og skr\u00e5streg samt lighedstegn-padding, som ikke er URL-sikre. Hvis du koder med base64 og afkoder med base64url (eller omvendt), bliver signaturbytes forvansket, og verifikationen fejler tilsyneladende tilf\u00e6ldigt. V\u00e6lg \u00e9t format konsekvent. Node.js underst\u00f8tter <code>'base64url'<\/code> direkte som Buffer-kodning.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faelde-3-ikke-deterministisk-json\">F\u00e6lde 3: Ikke-deterministisk JSON<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Hvis du signerer <code>JSON.stringify(obj)<\/code> direkte og senere re-serialiserer objektet til verifikation, kan n\u00f8gler\u00e6kkef\u00f8lgen \u00e6ndre sig, og signaturen holder ikke. Sign\u00e9r altid de n\u00f8jagtige bytes, der transporteres, eller brug kanonisk serialisering med sorterede n\u00f8gler, som vi gjorde i trin 7.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faelde-4-at-forveksle-noegletyper\">F\u00e6lde 4: At forveksle n\u00f8gletyper<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">At sende en privat n\u00f8gle til <code>crypto.verify<\/code> eller en offentlig n\u00f8gle til <code>crypto.sign<\/code> giver en uklar fejlmeddelelse. Navngiv dine variabler tydeligt (<code>privateKey<\/code>, <code>publicKey<\/code>) og tjek <code>keyObject.type<\/code>, som er enten <code>'private'<\/code> eller <code>'public'<\/code>, hvis du er i tvivl.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faelde-5-at-committe-den-private-noegle\">F\u00e6lde 5: At committe den private n\u00f8gle<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">En privat n\u00f8gle i Git, i en Docker-image eller i en loglinje er kompromitteret for altid, ogs\u00e5 selvom du sletter den bagefter. Brug <code>.gitignore<\/code>, milj\u00f8variabler eller en secrets-manager. Hvis en privat n\u00f8gle l\u00e6kker, er der kun \u00e9n l\u00f8sning: rot\u00e9r til et nyt n\u00f8glepar og afvis det gamle. Dette m\u00f8nster g\u00e5r igen i alle vores guides om <a href=\"https:\/\/shattered.io\/dk\/security\/https-og-tls\/\">HTTPS og TLS<\/a> og n\u00f8gleh\u00e5ndtering.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"avancerede-tips-til-produktion\">Avancerede tips til produktion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e5r grundkoden virker, adskiller produktionsklar signering sig p\u00e5 et par punkter. Disse tips kommer fra reelle driftserfaringer med Ed25519 i tjenester med h\u00f8j trafik.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>N\u00f8glerotation med key id.<\/strong> Tilf\u00f8j et <code>kid<\/code>-felt til dine tokens, der peger p\u00e5 hvilken offentlig n\u00f8gle der skal bruges. S\u00e5 kan du udrulle en ny n\u00f8gle og udfase den gamle uden at ugyldigg\u00f8re eksisterende tokens p\u00e5 \u00e9n gang.<\/li><li><strong>Hold private n\u00f8gler ude af appen.<\/strong> I produktion b\u00f8r den private n\u00f8gle ligge i et hardware-sikkerhedsmodul (HSM) eller en cloud-KMS, der signerer p\u00e5 foresp\u00f8rgsel, s\u00e5 r\u00e5bytes aldrig forlader det sikre modul.<\/li><li><strong>Brug asynkron signering under belastning.<\/strong> <code>crypto.sign<\/code> har ogs\u00e5 en callback-baseret variant. Under h\u00f8j last undg\u00e5r du at blokere event-loopet ved at signere asynkront.<\/li><li><strong>S\u00e6t korte levetider.<\/strong> Foretr\u00e6k tokens, der lever minutter til timer, kombineret med refresh-flow, frem for tokens, der g\u00e6lder i dage.<\/li><li><strong>Log aldrig n\u00f8gler eller signaturer i klartekst.<\/strong> Mask\u00e9r dem i logs, og overv\u00e5g for utilsigtet eksponering.<\/li><li><strong>Overvej post-kvante-overgangen.<\/strong> Til data, der skal forblive verificerbar i 10 \u00e5r eller mere, s\u00e5 f\u00f8lg NISTs ML-DSA-standard og planl\u00e6g hybride signaturer p\u00e5 sigt.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fejlfinding-8-typiske-problemer-og-loesninger\">Fejlfinding: 8 typiske problemer og l\u00f8sninger<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e5r noget ikke virker, skyldes det n\u00e6sten altid \u00e9t af f\u00f8lgende. Brug denne tabel som tjekliste, f\u00f8r du graver dybere.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Symptom<\/th><th>Sandsynlig \u00e5rsag<\/th><th>L\u00f8sning<\/th><\/tr><\/thead><tbody><tr><td>Verify returnerer altid false<\/td><td>Hash angivet i stedet for null<\/td><td>Brug <code>crypto.sign(null, ...)<\/code> og <code>crypto.verify(null, ...)<\/code><\/td><\/tr><tr><td>ERR_OSSL-fejl ved sign<\/td><td>Offentlig n\u00f8gle sendt til sign<\/td><td>Send den private n\u00f8gle til signering<\/td><\/tr><tr><td>Verify false efter transport<\/td><td>base64 mod base64url forveksling<\/td><td>Brug samme kodning begge steder<\/td><\/tr><tr><td>Signaturl\u00e6ngde ikke 64<\/td><td>Trunkeret eller forkert afkodet<\/td><td>Tjek at <code>sig.length<\/code> er 64 f\u00f8r verify<\/td><\/tr><tr><td>Verify false for samme objekt<\/td><td>Ikke-deterministisk JSON<\/td><td>Sign\u00e9r de transporterede bytes direkte<\/td><\/tr><tr><td>ERR_INVALID_ARG_TYPE<\/td><td>String sendt hvor Buffer ventes<\/td><td>Konvert\u00e9r med <code>Buffer.from(x, 'utf8')<\/code><\/td><\/tr><tr><td>Kan ikke l\u00e6se private.pem<\/td><td>Filtilladelse eller forkert sti<\/td><td>Tjek 0600-rettighed og absolut sti<\/td><\/tr><tr><td>JWK x eller d er undefined<\/td><td>Forkert n\u00f8gletype eksporteret<\/td><td>x findes p\u00e5 public, d kun p\u00e5 private<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Et hurtigt diagnostisk trick: hvis verifikation fejler, s\u00e5 log b\u00e5de <code>signature.length<\/code> og de f\u00f8rste bytes af den signerede besked p\u00e5 begge sider. I ni ud af ti tilf\u00e6lde afsl\u00f8rer det med det samme, at de to sider ikke er enige om pr\u00e6cis hvilke bytes der blev signeret.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"noegleformater-i-node-js-en-kort-reference\">N\u00f8gleformater i Node.js: en kort reference<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Forvirring om n\u00f8gleformater er en hyppig kilde til frustration. Denne tabel opsummerer, hvorn\u00e5r du bruger hvilket format med Ed25519 i Node.js.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Format<\/th><th>Type<\/th><th>Hvorn\u00e5r<\/th><th>Node-kald<\/th><\/tr><\/thead><tbody><tr><td>PEM (PKCS#8)<\/td><td>Privat, tekst<\/td><td>Lagring p\u00e5 disk, config<\/td><td><code>export type pkcs8, format pem<\/code><\/td><\/tr><tr><td>PEM (SPKI)<\/td><td>Offentlig, tekst<\/td><td>Deling, config<\/td><td><code>export type spki, format pem<\/code><\/td><\/tr><tr><td>DER<\/td><td>Bin\u00e6r<\/td><td>Kompakt bin\u00e6r lagring<\/td><td><code>export type spki, format der<\/code><\/td><\/tr><tr><td>JWK<\/td><td>JSON<\/td><td>Web, interop, r\u00e5 bytes<\/td><td><code>export format jwk<\/code><\/td><\/tr><tr><td>R\u00e5 32 bytes<\/td><td>Bin\u00e6r<\/td><td>libsodium, andre sprog<\/td><td>Via JWK x- eller d-felt<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Til langt de fleste Node-til-Node-scenarier er PEM det enkleste valg. Skift kun til JWK eller r\u00e5 bytes, n\u00e5r du skal interoperere med et system, der kr\u00e6ver det. Fors\u00f8g aldrig at konstruere PEM-strenge manuelt; lad <code>export<\/code> g\u00f8re arbejdet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"det-komplette-projekt-struktur-og-koersel\">Det komplette projekt: struktur og k\u00f8rsel<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Du har nu et fuldt fungerende projekt. Mappestrukturen ser s\u00e5dan ud, og hver fil har \u00e9t ansvar.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ed25519-signaturer\/\n  index.js          # CLI-indgang\n  server.js         # Express-API\n  package.json\n  .gitignore\n  keys\/             # Genererede noegler (ignoreret af Git)\n    private.pem\n    public.pem\n  src\/\n    keys.js         # Generering, lagring, indlaesning\n    sign.js         # signMessage\n    verify.js       # verifyMessage\n    token.js        # createToken, issue, validate\n  test\/\n    token.test.js   # Negative og positive tests<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hele flowet fra nul til k\u00f8rende tjeneste er fire kommandoer. Det viser, hvor lidt kode der skal til at bygge robust signaturh\u00e5ndtering med det indbyggede <code>crypto<\/code>-modul.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ npm install        # Express\n$ npm run keygen     # Generer Ed25519-noeglepar\n$ node --test        # Koer alle tests\n$ npm run serve      # Start API paa port 3000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Med under 200 linjer kode har du n\u00f8glegenerering, et signeret token-format med udl\u00f8b, et CLI og et beskyttet API, alt sammen baseret p\u00e5 Ed25519 og uden en eneste kryptografi-afh\u00e6ngighed fra tredjepart. Det er styrken ved at bruge Node.js&#8217; indbyggede primitiver korrekt.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"saadan-passer-ed25519-ind-i-den-stoerre-sikkerhedsmodel\">S\u00e5dan passer Ed25519 ind i den st\u00f8rre sikkerhedsmodel<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">En digital signatur er kun \u00e9t lag. Den beviser, hvem der signerede og at indholdet er u\u00e6ndret, men den krypterer ikke data og beskytter ikke mod, at en angriber afspiller et gyldigt token, du allerede har udstedt. Derfor kombinerer du i praksis signaturer med TLS til transportkryptering, korte levetider mod replay og rotation mod n\u00f8glekompromittering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Signaturer bygger p\u00e5 de samme hash-fundamenter, som vi gennemg\u00e5r i <a href=\"https:\/\/shattered.io\/dk\/cryptography\/sha-256\/\">SHA-256 forklaret<\/a>. Faktisk hasher Ed25519 internt med SHA-512, og hele sikkerheden afh\u00e6nger af, at den underliggende hashfunktion er kollisionsresistent. Da SHA-1 blev brudt i 2017, som vi beskriver i <a href=\"https:\/\/shattered.io\/dk\/cryptography\/sha1-kollision\/\">SHA-1-kollisionen<\/a>, var det netop signatursystemer, der var i st\u00f8rst fare. Ed25519 bruger SHA-512 specifikt for at undg\u00e5 den slags svagheder.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Den vigtigste fremtidssikring i 2026 handler om kvantecomputere. Ed25519 er sikker mod alt, hvad klassiske computere kan, men en stor kvantecomputer ville bryde den. NIST har standardiseret post-kvante-signaturer (ML-DSA, tidligere Dilithium), og branchen bev\u00e6ger sig mod hybride l\u00f8sninger, der kombinerer Ed25519 med en post-kvante-algoritme. Til alt, der skal verificeres inden for det n\u00e6ste \u00e5rti, er Ed25519 dog stadig et fremragende og veldokumenteret valg.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ofte-stillede-spoergsmaal-om-ed25519-i-node-js\">Ofte stillede sp\u00f8rgsm\u00e5l om Ed25519 i Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"skal-jeg-installere-et-bibliotek-for-at-bruge-ed25519-i-node-js\">Skal jeg installere et bibliotek for at bruge Ed25519 i Node.js?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nej. Det indbyggede <code>crypto<\/code>-modul underst\u00f8tter Ed25519 fuldt ud fra Node.js 12 og frem, inklusive n\u00f8glegenerering, signering og verifikation. Du beh\u00f8ver ingen pakker som tweetnacl eller libsodium-wrappers til standardbrug. Vi brugte kun Express, og det kun til det valgfrie API-trin.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hvorfor-skal-foerste-argument-til-crypto-sign-vaere-null\">Hvorfor skal f\u00f8rste argument til crypto.sign v\u00e6re null?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fordi Ed25519 udf\u00f8rer sin egen hashing internt med SHA-512 som en del af algoritmen. Hvis du angiver en hash som <code>'sha256'<\/code> eller <code>'sha512'<\/code>, forstyrrer du skemaet, og resultatet kan enten fejle eller blive uverificerbart for andre korrekte implementeringer. <code>null<\/code> betyder simpelthen ingen ekstern pre-hash.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"er-ed25519-bedre-end-rsa-til-jwt-lignende-tokens\">Er Ed25519 bedre end RSA til JWT-lignende tokens?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For de fleste moderne brugssituationer, ja. Ed25519-signaturer fylder 64 bytes mod RSA-2048&#8217;s 256 bytes, signering er hurtigere, og der er f\u00e6rre konfigurationsvalg at fejle. RSA har stadig sin plads, hvor ekstrem verifikationshastighed eller bagudkompatibilitet kr\u00e6ves, men til nye systemer er Ed25519 ofte det renere valg.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"kan-jeg-verificere-en-node-js-ed25519-signatur-i-browseren\">Kan jeg verificere en Node.js Ed25519-signatur i browseren?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ja. Web Crypto API i moderne browsere underst\u00f8tter Ed25519. Eksport\u00e9r den offentlige n\u00f8gle som JWK fra Node, import\u00e9r den i browseren med <code>crypto.subtle.importKey<\/code>, og verific\u00e9r med <code>crypto.subtle.verify<\/code>. S\u00f8rg for, at begge sider er enige om de n\u00f8jagtige bytes, der blev signeret.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hvad-er-forskellen-paa-ed25519-og-x25519\">Hvad er forskellen p\u00e5 Ed25519 og X25519?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">De bygger p\u00e5 samme kurve (Curve25519), men l\u00f8ser forskellige problemer. Ed25519 er til digitale signaturer (bevis p\u00e5 autenticitet). X25519 er til n\u00f8gleudveksling (Diffie-Hellman, til at etablere en delt hemmelighed). Brug Ed25519, n\u00e5r du vil signere; brug X25519, n\u00e5r du vil oprette en krypteret kanal.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"er-ed25519-lovligt-og-godkendt-til-erhvervsbrug-i-eu\">Er Ed25519 lovligt og godkendt til erhvervsbrug i EU?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ja. Ed25519 er defineret i RFC 8032, standardiseret af NIST i FIPS 186-5 (2023) og bredt anbefalet af sikkerhedsmyndigheder. Der er ingen juridiske hindringer for at bruge det i danske eller nordiske virksomheder, og det opfylder anbefalingerne for st\u00e6rk kryptografi i de fleste compliance-rammer.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hvor-laenge-kan-jeg-stole-paa-en-ed25519-signatur\">Hvor l\u00e6nge kan jeg stole p\u00e5 en Ed25519-signatur?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Mod klassiske computere regnes Ed25519 som sikker i overskuelig fremtid med sine cirka 128 bit sikkerhed. Den reelle usikkerhed er kvantecomputere. Til data, der skal forblive verificerbar ud over de n\u00e6ste 10 til 15 \u00e5r, b\u00f8r du planl\u00e6gge en overgang til post-kvante-signaturer eller hybride ordninger.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hvordan-roterer-jeg-en-ed25519-noegle-uden-at-bryde-eksisterende-tokens\">Hvordan roterer jeg en Ed25519-n\u00f8gle uden at bryde eksisterende tokens?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Tilf\u00f8j et n\u00f8gle-id (<code>kid<\/code>) til hvert token, og lad verifikatoren sl\u00e5 den rigtige offentlige n\u00f8gle op ud fra det. N\u00e5r du introducerer en ny n\u00f8gle, accepterer du begge i en overgangsperiode, indtil alle gamle tokens er udl\u00f8bet, og derefter fjerner du den gamle n\u00f8gle.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"relateret-indhold\">Relateret indhold<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/digitale-signaturer\/\">Digitale signaturer: hvordan hashing og n\u00f8gler skaber tillid<\/a><\/li><li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/sha-256\/\">SHA-256 forklaret: hj\u00f8rnestenen i moderne hashing<\/a><\/li><li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/hashfunktioner\/\">Hashfunktioner: egenskaber, form\u00e5l og praktisk brug<\/a><\/li><li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/sha1-kollision\/\">SHA-1-kollisionen: da SHAttered br\u00f8d en hashfunktion<\/a><\/li><li><a href=\"https:\/\/shattered.io\/dk\/security\/https-og-tls\/\">HTTPS og TLS: s\u00e5dan beskyttes din forbindelse<\/a><\/li><li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/\">Kryptografi: hashfunktioner, SHA og digital tillid<\/a><\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"konklusion-ed25519-er-den-pragmatiske-standard\">Konklusion: Ed25519 er den pragmatiske standard<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Du har bygget et komplet signatursystem i Node.js fra bunden: n\u00f8glegenerering, PEM-lagring, signering, verifikation, et signeret token-format med udl\u00f8b, et CLI, et beskyttet API og en testsuite, der afviser de fire vigtigste angreb. Alt sammen med det indbyggede <code>crypto<\/code>-modul og under 200 linjer kode.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ed25519 er popul\u00e6r af en god grund. Den fjerner de farlige valg, holder n\u00f8gler og signaturer sm\u00e5, k\u00f8rer hurtigt og er bredt underst\u00f8ttet fra OpenSSH til browsere. Husk de tre regler, der forhindrer de fleste fejl: brug <code>null<\/code> som hash-argument, sign\u00e9r de n\u00f8jagtige bytes du transporterer, og lad aldrig den private n\u00f8gle forlade det sikre sted. F\u00f8lg dem, og du har signaturh\u00e5ndtering, der holder i produktion.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Kilder og videre l\u00e6sning: <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc8032\" target=\"_blank\" rel=\"noopener\">RFC 8032 (EdDSA)<\/a>, <a href=\"https:\/\/nodejs.org\/api\/crypto.html\" target=\"_blank\" rel=\"noopener\">Node.js crypto-dokumentation<\/a>, <a href=\"https:\/\/csrc.nist.gov\/pubs\/fips\/186-5\/final\" target=\"_blank\" rel=\"noopener\">NIST FIPS 186-5<\/a>, <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc8709.html\" target=\"_blank\" rel=\"noopener\">RFC 8709 (Ed25519 i SSH)<\/a> og <a href=\"https:\/\/ed25519.cr.yp.to\/\" target=\"_blank\" rel=\"noopener\">den officielle Ed25519-side<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Digitale signaturer er rygraden i moderne tillid p\u00e5 nettet. De beviser, at en besked stammer fra en bestemt afsender, og at ingen har \u00e6ndret den undervejs. Blandt de mange signatur-algoritmer\u2026<\/p>\n","protected":false},"author":6,"featured_media":68,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-67","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cryptography"],"_links":{"self":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/67","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/comments?post=67"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/67\/revisions"}],"predecessor-version":[{"id":69,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/67\/revisions\/69"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media\/68"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media?parent=67"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/categories?post=67"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/tags?post=67"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}