{"id":91,"date":"2026-06-15T16:36:50","date_gmt":"2026-06-15T16:36:50","guid":{"rendered":"https:\/\/shattered.io\/dk\/2026\/06\/15\/hmac-webhook-signaturer-nodejs\/"},"modified":"2026-06-15T16:38:34","modified_gmt":"2026-06-15T16:38:34","slug":"hmac-webhook-signaturer-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/dk\/hmac-webhook-signaturer-nodejs\/","title":{"rendered":"HMAC i Node.js: webhook-signaturer i 12 trin [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">En webhook uden signatur er en \u00e5ben d\u00f8r. Enhver, der kender din URL, kan sende en falsk betaling, en falsk ordre eller en falsk &#8220;konto slettet&#8221;-besked, og din server vil behandle den som \u00e6gte. HMAC (Hash-based Message Authentication Code) lukker den d\u00f8r. Med en delt hemmelig n\u00f8gle og en hashfunktion kan du bevise to ting p\u00e5 \u00e9n gang: at beskeden kommer fra den rette afsender, og at ingen har \u00e6ndret et eneste byte undervejs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Denne tutorial bygger en komplet, k\u00f8rende HMAC-l\u00f8sning i Node.js fra bunden i 12 trin. Du l\u00e6rer at generere en st\u00e6rk n\u00f8gle, beregne en HMAC med <code>crypto.createHmac<\/code>, verificere den i konstant tid med <code>crypto.timingSafeEqual<\/code>, og bygge en Express-server, der signerer udg\u00e5ende webhooks og afviser forfalskede indg\u00e5ende. Du f\u00e5r ogs\u00e5 6 faldgruber, 8 fejlfindingspunkter, avancerede tips og et fuldt projekt, du kan kopiere direkte. Alt er testet p\u00e5 Node.js 22 LTS og bruger udelukkende det indbyggede <code>crypto<\/code>-modul, s\u00e5 du skal ikke installere et eneste kryptobibliotek.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"hvad-er-hmac-og-hvorfor-bruge-det\">Hvad er HMAC, og hvorfor bruge det?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">HMAC er en n\u00f8glebaseret beskedautentificeringskode. Den kombinerer en kryptografisk hashfunktion, for eksempel SHA-256, med en hemmelig n\u00f8gle, som kun afsender og modtager kender. Resultatet er en kort streng bytes, en MAC, der f\u00f8lger med beskeden. Modtageren beregner sin egen MAC over den modtagne besked med samme n\u00f8gle. Hvis de to MAC&#8217;er er identiske, ved modtageren, at beskeden er autentisk og u\u00e6ndret. Standarden er defineret i RFC 2104 og i NIST&#8217;s FIPS 198-1, og den er en af de mest udbredte byggeklodser i moderne API-sikkerhed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bem\u00e6rk en vigtig skelnen: HMAC er ikke kryptering. Den skjuler ikke indholdet af din besked. Den giver integritet og autenticitet, ikke fortrolighed. Vil du b\u00e5de skjule og autentificere data, kombinerer du HMAC med kryptering, eller du bruger en AEAD-ordning som AES-GCM. HMAC l\u00f8ser et andet, men lige s\u00e5 kritisk problem: &#8220;kan jeg stole p\u00e5, at denne besked virkelig kom fra den, jeg tror, og at den ikke er pillet ved?&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Du m\u00f8der HMAC overalt, selvom du ikke altid l\u00e6gger m\u00e6rke til det. Stripe, GitHub, Shopify og Slack signerer alle deres webhooks med HMAC-SHA256. AWS bruger HMAC i Signature Version 4 til at signere API-kald. JSON Web Tokens med algoritmen HS256 er reelt en HMAC-SHA256 over header og payload. N\u00e5r du forst\u00e5r m\u00f8nsteret i denne tutorial, kan du verificere alle disse signaturer korrekt, og du undg\u00e5r de timingfejl, der g\u00f8r mange hjemmelavede implementeringer usikre.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Hvorfor ikke bare en almindelig hash? Fordi en almindelig SHA-256 ikke har nogen n\u00f8gle. Enhver, der kan \u00e6ndre beskeden, kan beregne en ny gyldig hash og sende den med. HMAC&#8217;s hemmelige n\u00f8gle er det, der g\u00f8r forfalskning umulig uden adgang til n\u00f8glen. Forskellen mellem en hash og en HMAC er n\u00f8glen, og den forskel er hele pointen.<\/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 beh\u00f8ver meget lidt for at f\u00f8lge med, fordi alt det kryptografiske kommer fra Node.js&#8217; kerne. S\u00f8rg for at have f\u00f8lgende p\u00e5 plads, f\u00f8r du g\u00e5r i gang.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Node.js 20 LTS eller nyere.<\/strong> Eksemplerne er testet p\u00e5 Node.js 22 LTS. Tjek din version med <code>node --version<\/code>. K\u00f8rer du noget \u00e6ldre, s\u00e5 opgrader, da gamle udgaver mangler sikkerhedsrettelser i <code>crypto<\/code>.<\/li>\n<li><strong>npm 10 eller nyere.<\/strong> F\u00f8lger med Node.js. Tjek med <code>npm --version<\/code>.<\/li>\n<li><strong>Express 4.18 eller 5.x<\/strong> til webhook-serveren i trin 7 og 8. Det er den eneste eksterne afh\u00e6ngighed i hele projektet.<\/li>\n<li><strong>En editor og en terminal.<\/strong> VS Code eller hvad du foretr\u00e6kker.<\/li>\n<li><strong>Grundl\u00e6ggende JavaScript.<\/strong> Du skal kunne l\u00e6se async-funktioner, Buffer-objekter og en simpel Express-rute.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Det indbyggede <code>crypto<\/code>-modul leverer hele HMAC-funktionaliteten. Du importerer det med <code>const crypto = require('node:crypto')<\/code> eller, hvis du bruger ES-moduler, <code>import crypto from 'node:crypto'<\/code>. Pr\u00e6fikset <code>node:<\/code> er den anbefalede m\u00e5de at importere kernemoduler p\u00e5, fordi det g\u00f8r det tydeligt, at modulet kommer fra runtime og ikke fra en npm-pakke.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Funktion<\/th><th>Form\u00e5l<\/th><th>Returnerer<\/th><\/tr><\/thead><tbody>\n<tr><td><code>crypto.createHmac(alg, key)<\/code><\/td><td>Opretter et HMAC-objekt med valgt hash og n\u00f8gle<\/td><td>Hmac-objekt (stream-lignende)<\/td><\/tr>\n<tr><td><code>hmac.update(data)<\/code><\/td><td>Tilf\u00f8rer data til beregningen, kan kaldes flere gange<\/td><td>Samme Hmac-objekt<\/td><\/tr>\n<tr><td><code>hmac.digest(encoding)<\/code><\/td><td>Afslutter og udregner MAC&#8217;en<\/td><td>Buffer eller streng<\/td><\/tr>\n<tr><td><code>crypto.timingSafeEqual(a, b)<\/code><\/td><td>Sammenligner to buffere i konstant tid<\/td><td>Boolean<\/td><\/tr>\n<tr><td><code>crypto.randomBytes(n)<\/code><\/td><td>Genererer kryptografisk tilf\u00e6ldige bytes<\/td><td>Buffer<\/td><\/tr>\n<\/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 mappe og initialiser et npm-projekt. Du installerer kun Express; alt andet er indbygget.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir node-hmac-webhooks &amp;&amp; cd node-hmac-webhooks\nnpm init -y\nnpm install express@4.18.2\n\n# Tjek at versionerne er som forventet\nnode --version    # f.eks. v22.11.0\nnpm --version     # f.eks. 10.9.0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c5bn <code>package.json<\/code> og tilf\u00f8j <code>\"type\": \"commonjs\"<\/code>, hvis det ikke allerede st\u00e5r der, s\u00e5 <code>require<\/code> virker i alle eksempler. Vil du bruge ES-moduler i stedet, s\u00e5 s\u00e6t <code>\"type\": \"module\"<\/code> og erstat hvert <code>require<\/code> med <code>import<\/code>. Resten af koden er identisk. Opret nu en mappe <code>src<\/code>, hvor vi l\u00e6gger filerne fra de n\u00e6ste trin.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-2-generer-en-staerk-hemmelig-noegle\">Trin 2: Gener\u00e9r en st\u00e6rk hemmelig n\u00f8gle<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">HMAC&#8217;s sikkerhed afh\u00e6nger fuldst\u00e6ndigt af n\u00f8glen. Skriv den aldrig i h\u00e5nden, og brug aldrig en kort streng som &#8220;secret123&#8221;. Gener\u00e9r i stedet mindst 32 tilf\u00e6ldige bytes med <code>crypto.randomBytes<\/code>, hvilket svarer til hashens outputst\u00f8rrelse for SHA-256 og er et fornuftigt minimum.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/generate-key.js\nconst crypto = require('node:crypto');\n\n\/\/ 32 bytes = 256 bit, et solidt minimum til HMAC-SHA256\nconst key = crypto.randomBytes(32);\n\nconsole.log('Hex:   ', key.toString('hex'));\nconsole.log('Base64:', key.toString('base64'));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00f8r <code>node src\/generate-key.js<\/code>. Du f\u00e5r noget i stil med dette:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Hex:    9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\nBase64: n4bQgYhMfWWaL+qgwVtKFaO\/TxsrC4Is0V1sFbDwCgg=<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Gem n\u00f8glen som en milj\u00f8variabel, aldrig i kildekoden og aldrig i et git-repository. L\u00e6g den i en <code>.env<\/code>-fil, der st\u00e5r i <code>.gitignore<\/code>, eller hent den fra en hemmelighedstjeneste i produktion. I koden l\u00e6ser du den med <code>process.env.HMAC_SECRET<\/code>. Begge parter, afsender og modtager, skal have pr\u00e6cis samme n\u00f8gle, for HMAC er symmetrisk.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-3-beregn-din-foerste-hmac\">Trin 3: Beregn din f\u00f8rste HMAC<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nu til kernen. Du opretter et HMAC-objekt med <code>crypto.createHmac<\/code>, fodrer det med data via <code>update<\/code>, og afslutter med <code>digest<\/code>. L\u00e6g m\u00e6rke til, at outputkodningen v\u00e6lges i <code>digest<\/code>; uden et argument f\u00e5r du en r\u00e5 Buffer, med <code>'hex'<\/code> eller <code>'base64'<\/code> f\u00e5r du en streng.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/sign.js\nconst crypto = require('node:crypto');\n\nfunction sign(message, secret) {\n  return crypto\n    .createHmac('sha256', secret)\n    .update(message)\n    .digest('hex');\n}\n\nconst secret = 'min-hemmelige-noegle';\nconst message = '{\"event\":\"payment.succeeded\",\"amount\":4999}';\n\nconsole.log(sign(message, secret));\n\/\/ 3f8a... en 64-tegns hex-streng (256 bit)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Algoritmenavnet, her <code>'sha256'<\/code>, bestemmer b\u00e5de sikkerheden og l\u00e6ngden p\u00e5 outputtet. SHA-256 giver 32 bytes, alts\u00e5 64 hex-tegn. Vil du have en l\u00e6ngere MAC, v\u00e6lger du <code>'sha384'<\/code> eller <code>'sha512'<\/code>. For de fleste webhooks og API-signaturer er SHA-256 standardvalget, og det er, hvad Stripe, GitHub og de fleste andre bruger.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"vaelg-den-rette-hashfunktion\">V\u00e6lg den rette hashfunktion<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">HMAC&#8217;s styrke arver fra den underliggende hash. Undg\u00e5 MD5 og SHA-1, der er for\u00e6ldede. Hold dig til SHA-2-familien. Tabellen viser de praktiske valg og deres outputst\u00f8rrelser.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Algoritme<\/th><th>Output (bytes)<\/th><th>Output (hex-tegn)<\/th><th>Typisk brug<\/th><\/tr><\/thead><tbody>\n<tr><td>HMAC-SHA256<\/td><td>32<\/td><td>64<\/td><td>Webhooks, JWT HS256, standardvalg<\/td><\/tr>\n<tr><td>HMAC-SHA384<\/td><td>48<\/td><td>96<\/td><td>H\u00f8jere sikkerhedsmargin<\/td><\/tr>\n<tr><td>HMAC-SHA512<\/td><td>64<\/td><td>128<\/td><td>Store sikkerhedskrav, hurtig p\u00e5 64-bit-CPU<\/td><\/tr>\n<tr><td>HMAC-SHA1<\/td><td>20<\/td><td>40<\/td><td>Frar\u00e5des, kun til \u00e6ldre integrationer<\/td><\/tr>\n<tr><td>HMAC-MD5<\/td><td>16<\/td><td>32<\/td><td>Brug aldrig, for\u00e6ldet<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-4-brug-streaming-til-store-payloads\">Trin 4: Brug streaming til store payloads<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hmac-objektet er stream-lignende. Du kan kalde <code>update<\/code> flere gange, hvilket er den idiomatiske m\u00e5de at signere store request-bodies eller filer p\u00e5, uden at holde det hele i hukommelsen p\u00e5 \u00e9n gang. Hver <code>update<\/code> tilf\u00f8jer bytes til den l\u00f8bende beregning.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/sign-stream.js\nconst crypto = require('node:crypto');\nconst fs = require('node:fs');\n\nfunction signFile(path, secret) {\n  return new Promise((resolve, reject) =&gt; {\n    const hmac = crypto.createHmac('sha256', secret);\n    const stream = fs.createReadStream(path);\n    stream.on('data', (chunk) =&gt; hmac.update(chunk));\n    stream.on('end', () =&gt; resolve(hmac.digest('hex')));\n    stream.on('error', reject);\n  });\n}\n\nsignFile('.\/stor-fil.bin', process.env.HMAC_SECRET)\n  .then((mac) =&gt; console.log('Fil-MAC:', mac))\n  .catch(console.error);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Til sm\u00e5 beskeder er one-shot-formen fra trin 3 helt fin. Til filer p\u00e5 flere hundrede megabyte sparer streaming-formen hukommelse og holder din proces stabil. Resultatet er bit for bit det samme, uanset om du kalder <code>update<\/code> \u00e9n eller hundrede gange, s\u00e5 l\u00e6nge de samlede bytes er identiske.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-5-verificer-en-signatur-i-konstant-tid\">Trin 5: Verific\u00e9r en signatur i konstant tid<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Her laver de fleste begyndere den farligste fejl. Det er fristende at sammenligne den forventede og den modtagne MAC med <code>===<\/code>, men det er en sikkerhedsbrist. JavaScripts strengsammenligning afslutter, s\u00e5 snart den finder den f\u00f8rste forskel. En angriber kan m\u00e5le, hvor lang tid sammenligningen tager, og dermed g\u00e6tte signaturen \u00e9t tegn ad gangen. Det kaldes et timingangreb.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L\u00f8sningen er <code>crypto.timingSafeEqual<\/code>, der altid bruger lige lang tid uanset hvor mange bytes der matcher. Funktionen kr\u00e6ver to buffere af n\u00f8jagtig samme l\u00e6ngde og kaster en fejl, hvis de ikke er det. Derfor skal du dekode begge signaturer til buffere og afvise l\u00e6ngdeforskelle, f\u00f8r du sammenligner.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/verify.js\nconst crypto = require('node:crypto');\n\nfunction verify(message, receivedHex, secret) {\n  const expected = crypto\n    .createHmac('sha256', secret)\n    .update(message)\n    .digest(); \/\/ r\u00e5 Buffer\n\n  let received;\n  try {\n    received = Buffer.from(receivedHex, 'hex');\n  } catch {\n    return false; \/\/ ugyldig hex-kodning\n  }\n\n  \/\/ Forskellig l\u00e6ngde betyder altid afvisning, og undg\u00e5r at\n  \/\/ timingSafeEqual kaster en RangeError\n  if (received.length !== expected.length) {\n    return false;\n  }\n\n  return crypto.timingSafeEqual(expected, received);\n}\n\nmodule.exports = { verify };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L\u00e6g m\u00e6rke til tre detaljer. Vi kalder <code>digest()<\/code> uden argument for at f\u00e5 en Buffer, s\u00e5 vi kan sammenligne bytes direkte. Vi pakker <code>Buffer.from<\/code> i en try\/catch, fordi en forvansket hex-streng kan give problemer. Og vi tjekker l\u00e6ngden f\u00f8rst, fordi <code>timingSafeEqual<\/code> kaster en <code>RangeError<\/code>, hvis bufferne ikke er lige lange. Med disse tre ting p\u00e5 plads har du en korrekt og sikker verifikation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-6-test-signering-og-verifikation-sammen\">Trin 6: Test signering og verifikation sammen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00f8r vi bygger en server, s\u00e5 bevis at de to halvdele passer sammen. En korrekt signatur skal accepteres, og enhver \u00e6ndring af beskeden eller signaturen skal afvises.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/roundtrip.js\nconst crypto = require('node:crypto');\nconst { verify } = require('.\/verify');\n\nconst secret = crypto.randomBytes(32);\nconst message = '{\"event\":\"order.created\",\"id\":\"ord_123\"}';\n\nconst signature = crypto\n  .createHmac('sha256', secret)\n  .update(message)\n  .digest('hex');\n\nconsole.log('Gyldig:        ', verify(message, signature, secret));\nconsole.log('\u00c6ndret besked: ', verify(message + ' ', signature, secret));\nconsole.log('\u00c6ndret sig.:   ', verify(message, signature.slice(0, -1) + '0', secret));\nconsole.log('Forkert noegle:', verify(message, signature, crypto.randomBytes(32)));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Det forventede output bekr\u00e6fter, at kun den \u00e6gte kombination g\u00e5r igennem:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Gyldig:         true\n\u00c6ndret besked:  false\n\u00c6ndret sig.:    false\nForkert noegle: false<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tre falske og \u00e9t sandt: pr\u00e6cis som det skal v\u00e6re. Et enkelt ekstra mellemrum i beskeden \u00e6ndrer hele MAC&#8217;en, fordi hashfunktionen er f\u00f8lsom over for hvert eneste byte. Det er denne egenskab, der giver dig integritetsbeskyttelse.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-7-byg-en-express-server-der-signerer-udgaaende-webhooks\">Trin 7: Byg en Express-server, der signerer udg\u00e5ende webhooks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nu bygger vi afsendersiden. N\u00e5r din applikation sender en webhook til en kunde, vedl\u00e6gger du en signaturheader, s\u00e5 kunden kan verificere afsenderen. Konventionen er at l\u00e6gge MAC&#8217;en i en header som <code>X-Signature<\/code> og ofte et timestamp i <code>X-Timestamp<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/sender.js\nconst crypto = require('node:crypto');\nconst https = require('node:https');\n\nconst SECRET = process.env.HMAC_SECRET;\n\nfunction sendWebhook(url, payload) {\n  const body = JSON.stringify(payload);\n  const timestamp = Math.floor(Date.now() \/ 1000).toString();\n\n  \/\/ Signer timestamp og body sammen, saa timestamp ikke kan aendres\n  const signature = crypto\n    .createHmac('sha256', SECRET)\n    .update(timestamp + '.' + body)\n    .digest('hex');\n\n  const req = https.request(url, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application\/json',\n      'X-Timestamp': timestamp,\n      'X-Signature': 'sha256=' + signature,\n    },\n  });\n\n  req.write(body);\n  req.end();\n}\n\nmodule.exports = { sendWebhook };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Bem\u00e6rk to designvalg, der f\u00f8lger industristandarden. Vi signerer <code>timestamp + '.' + body<\/code> sammen, ikke kun body, s\u00e5 en angriber ikke kan genbruge en gammel signatur med et nyt timestamp. Og vi pr\u00e6fikser signaturen med <code>sha256=<\/code>, pr\u00e6cis som GitHub g\u00f8r, s\u00e5 modtageren ved, hvilken algoritme der blev brugt. Det g\u00f8r det nemt at skifte algoritme senere uden at br\u00e6kke eksisterende integrationer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-8-verificer-indgaaende-webhooks-med-den-raa-body\">Trin 8: Verific\u00e9r indg\u00e5ende webhooks med den r\u00e5 body<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Modtagersiden indeholder den mest oversete f\u00e6lde i hele emnet: du skal verificere over de n\u00f8jagtige r\u00e5 bytes, der kom ind, ikke over et parset JavaScript-objekt. N\u00e5r Express parser JSON og du derefter laver <code>JSON.stringify<\/code> igen, kan r\u00e6kkef\u00f8lgen af felter, mellemrum og Unicode-escapes \u00e6ndre sig. Selv \u00e9n \u00e6ndret byte giver en helt anden MAC, og verifikationen fejler, selvom beskeden er \u00e6gte.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L\u00f8sningen er at gemme den r\u00e5 body, f\u00f8r den parses. Med <code>express.json<\/code> kan du g\u00f8re det via <code>verify<\/code>-callbacken, der f\u00e5r adgang til den r\u00e5 Buffer.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/receiver.js\nconst express = require('express');\nconst crypto = require('node:crypto');\n\nconst app = express();\nconst SECRET = process.env.HMAC_SECRET;\nconst MAX_AGE_SECONDS = 300; \/\/ afvis webhooks aeldre end 5 minutter\n\n\/\/ Gem den raa body, mens JSON stadig parses normalt\napp.use(express.json({\n  verify: (req, _res, buf) =&gt; { req.rawBody = buf; },\n}));\n\nfunction isValid(req) {\n  const timestamp = req.get('X-Timestamp');\n  const header = req.get('X-Signature') || '';\n  const received = header.replace('sha256=', '');\n\n  if (!timestamp || !received) return false;\n\n  \/\/ Replay-beskyttelse: afvis gamle eller fremtidige timestamps\n  const age = Math.floor(Date.now() \/ 1000) - Number(timestamp);\n  if (!Number.isFinite(age) || Math.abs(age) &gt; MAX_AGE_SECONDS) return false;\n\n  const signed = timestamp + '.' + req.rawBody.toString('utf8');\n  const expected = crypto.createHmac('sha256', SECRET).update(signed).digest();\n  const receivedBuf = Buffer.from(received, 'hex');\n\n  if (receivedBuf.length !== expected.length) return false;\n  return crypto.timingSafeEqual(expected, receivedBuf);\n}\n\napp.post('\/webhook', (req, res) =&gt; {\n  if (!isValid(req)) {\n    return res.status(401).json({ error: 'ugyldig signatur' });\n  }\n  \/\/ Foerst her er det sikkert at handle paa beskeden\n  console.log('Gyldig webhook:', req.body.event);\n  res.status(200).json({ received: true });\n});\n\napp.listen(3000, () =&gt; console.log('Lytter paa port 3000'));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verifikationen sker, f\u00f8r nogen forretningslogik k\u00f8rer. En fejlet signatur kortslutter requesten med 401, s\u00e5 uautentificeret input aldrig udl\u00f8ser bivirkninger. Det er det rigtige sted at placere kontrollen, helt ude ved transportgr\u00e6nsen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-9-haandter-baade-hex-og-base64-kodning\">Trin 9: H\u00e5ndt\u00e9r b\u00e5de hex- og base64-kodning<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Forskellige udbydere koder deres signaturer forskelligt. GitHub bruger hex, Slack bruger hex med pr\u00e6fiks, og nogle bruger base64. V\u00e6lg \u00e9n kanonisk kodning til din egen API, og dokument\u00e9r den tydeligt. Skal du verificere en tredjeparts webhook, s\u00e5 match pr\u00e6cis deres kodning. Her er en hj\u00e6lpefunktion, der h\u00e5ndterer begge.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/decode.js\nconst crypto = require('node:crypto');\n\nfunction safeCompare(expectedBuf, received, encoding) {\n  let receivedBuf;\n  try {\n    receivedBuf = Buffer.from(received, encoding); \/\/ 'hex' eller 'base64'\n  } catch {\n    return false;\n  }\n  if (receivedBuf.length !== expectedBuf.length) return false;\n  return crypto.timingSafeEqual(expectedBuf, receivedBuf);\n}\n\nmodule.exports = { safeCompare };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En subtil ting ved <code>Buffer.from<\/code> med <code>'base64'<\/code>: ugyldige tegn ignoreres i stedet for at kaste en fejl, s\u00e5 en forvansket base64-streng kan give en buffer med forkert l\u00e6ngde. L\u00e6ngdetjekket fanger det og afviser requesten, hvilket er pr\u00e6cis den adf\u00e6rd vi vil have. Derfor er l\u00e6ngdetjekket f\u00f8r <code>timingSafeEqual<\/code> ikke bare en formalitet, det er en del af sikkerheden.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-10-tilfoej-timestamp-og-replay-beskyttelse\">Trin 10: Tilf\u00f8j timestamp og replay-beskyttelse<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">En gyldig signatur beviser kun, at beskeden er \u00e6gte, ikke at den er ny. Uden timestamp kan en angriber, der opsnapper en gyldig webhook, sende den igen og igen. Hvis beskeden var &#8220;overf\u00f8r 5.000 kr.&#8221;, er det et alvorligt problem. L\u00f8sningen, som vi allerede byggede ind i trin 8, er at inkludere et timestamp i det signerede data og afvise beskeder, der er for gamle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Et vindue p\u00e5 5 minutter er en almindelig balance mellem at tillade netv\u00e6rksforsinkelse og at begr\u00e6nse replay-vinduet. Vil du have st\u00e6rkere beskyttelse, gemmer du hvert sets <code>X-Signature<\/code> i en kortlivet cache, for eksempel Redis med en TTL svarende til vinduet, og afviser en signatur, du allerede har set. S\u00e5 kan en besked kun behandles \u00e9n gang.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/replay-guard.js\nconst seen = new Map(); \/\/ i produktion: brug Redis med TTL\n\nfunction checkReplay(signature, maxAgeSeconds = 300) {\n  const now = Date.now();\n  \/\/ Ryd op i gamle poster\n  for (const [sig, ts] of seen) {\n    if (now - ts &gt; maxAgeSeconds * 1000) seen.delete(sig);\n  }\n  if (seen.has(signature)) return false; \/\/ allerede behandlet\n  seen.set(signature, now);\n  return true;\n}\n\nmodule.exports = { checkReplay };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kombinationen af timestamp-vindue og set-baseret duplikatkontrol giver robust replay-beskyttelse. Timestamp-vinduet holder cachen lille, fordi du kun beh\u00f8ver at huske signaturer inden for vinduet. Det er det samme m\u00f8nster, Stripe anbefaler i sin webhook-dokumentation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-11-roter-noegler-uden-nedetid\">Trin 11: Roter n\u00f8gler uden nedetid<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00f8gler skal kunne udskiftes, hvis de l\u00e6kker, eller blot som rutine. Problemet er, at du ikke kan skifte n\u00f8gle p\u00e5 begge sider i n\u00f8jagtig samme millisekund. L\u00f8sningen er at underst\u00f8tte flere gyldige n\u00f8gler i en overgangsperiode. Modtageren accepterer en signatur, hvis den matcher mod en hvilken som helst af de aktive n\u00f8gler.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/rotate.js\nconst crypto = require('node:crypto');\n\n\/\/ Flere aktive noegler i en overgangsperiode, nyeste foerst\nconst KEYS = (process.env.HMAC_KEYS || '').split(',').filter(Boolean);\n\nfunction verifyWithRotation(signedData, receivedHex) {\n  const received = Buffer.from(receivedHex, 'hex');\n  for (const key of KEYS) {\n    const expected = crypto.createHmac('sha256', key).update(signedData).digest();\n    if (received.length === expected.length &amp;&amp;\n        crypto.timingSafeEqual(expected, received)) {\n      return true;\n    }\n  }\n  return false;\n}\n\nmodule.exports = { verifyWithRotation };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Fremgangsm\u00e5den er enkel. Tilf\u00f8j den nye n\u00f8gle til <code>HMAC_KEYS<\/code> ved siden af den gamle. Skift afsenderen til at signere med den nye n\u00f8gle. N\u00e5r al trafik bruger den nye n\u00f8gle, fjerner du den gamle. P\u00e5 intet tidspunkt afvises gyldige beskeder. Bem\u00e6rk, at vi stadig sammenligner i konstant tid for hver kandidatn\u00f8gle, s\u00e5 rotationen ikke \u00e5bner et nyt timingangreb.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-12-skriv-automatiske-tests\">Trin 12: Skriv automatiske tests<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sikkerhedskode uden tests er en ulykke, der venter p\u00e5 at ske. Node.js har en indbygget test-runner siden version 18, s\u00e5 du beh\u00f8ver intet ekstra bibliotek. Skriv mindst tre tests: en gyldig signatur skal accepteres, en manipuleret body skal afvises, og en ugyldig l\u00e6ngde skal afvises uden at kaste.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/verify.test.js\nconst test = require('node:test');\nconst assert = require('node:assert');\nconst crypto = require('node:crypto');\nconst { verify } = require('.\/verify');\n\nconst secret = crypto.randomBytes(32);\nconst msg = '{\"event\":\"ping\"}';\nconst sig = crypto.createHmac('sha256', secret).update(msg).digest('hex');\n\ntest('accepterer gyldig signatur', () =&gt; {\n  assert.strictEqual(verify(msg, sig, secret), true);\n});\n\ntest('afviser manipuleret body', () =&gt; {\n  assert.strictEqual(verify(msg + 'x', sig, secret), false);\n});\n\ntest('afviser forkert laengde uden at kaste', () =&gt; {\n  assert.strictEqual(verify(msg, 'abc', secret), false);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00f8r testene med <code>node --test<\/code>. Du f\u00e5r en samlet rapport over best\u00e5ede og fejlede tests. Tilf\u00f8j gerne flere: test base64-kodning, test rotation med to n\u00f8gler, og test at et fremtidigt timestamp afvises. Jo flere kanttilf\u00e6lde du d\u00e6kker, jo mere sikker er din implementering.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"det-komplette-koerende-projekt\">Det komplette, k\u00f8rende projekt<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Her er et samlet, selvst\u00e6ndigt eksempel, der binder afsender og modtager sammen i \u00e9n fil, s\u00e5 du kan k\u00f8re det direkte og se hele flowet. S\u00e6t f\u00f8rst milj\u00f8variablen, og k\u00f8r derefter filen.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app.js  -&gt;  koer med:  HMAC_SECRET=$(openssl rand -hex 32) node app.js\nconst express = require('express');\nconst crypto = require('node:crypto');\n\nconst app = express();\nconst SECRET = process.env.HMAC_SECRET;\nconst MAX_AGE = 300;\n\nif (!SECRET) {\n  console.error('Saet HMAC_SECRET, fx: HMAC_SECRET=$(openssl rand -hex 32)');\n  process.exit(1);\n}\n\napp.use(express.json({ verify: (req, _res, buf) =&gt; { req.rawBody = buf; } }));\n\nfunction signPayload(timestamp, body) {\n  return crypto.createHmac('sha256', SECRET)\n    .update(timestamp + '.' + body).digest('hex');\n}\n\nfunction isValid(req) {\n  const ts = req.get('X-Timestamp');\n  const received = (req.get('X-Signature') || '').replace('sha256=', '');\n  if (!ts || !received) return false;\n\n  const age = Math.floor(Date.now() \/ 1000) - Number(ts);\n  if (!Number.isFinite(age) || Math.abs(age) &gt; MAX_AGE) return false;\n\n  const expected = crypto.createHmac('sha256', SECRET)\n    .update(ts + '.' + req.rawBody.toString('utf8')).digest();\n  const receivedBuf = Buffer.from(received, 'hex');\n  if (receivedBuf.length !== expected.length) return false;\n  return crypto.timingSafeEqual(expected, receivedBuf);\n}\n\napp.post('\/webhook', (req, res) =&gt; {\n  if (!isValid(req)) return res.status(401).json({ error: 'ugyldig signatur' });\n  res.json({ received: true, event: req.body.event });\n});\n\nconst server = app.listen(3000, async () =&gt; {\n  \/\/ Selvtest: send en korrekt signeret webhook til os selv\n  const body = JSON.stringify({ event: 'selvtest' });\n  const ts = Math.floor(Date.now() \/ 1000).toString();\n  const sig = signPayload(ts, body);\n\n  const res = await fetch('http:\/\/localhost:3000\/webhook', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application\/json',\n      'X-Timestamp': ts,\n      'X-Signature': 'sha256=' + sig,\n    },\n    body,\n  });\n  console.log('Selvtest status:', res.status, await res.json());\n  server.close();\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00f8r det, og du ser <code>Selvtest status: 200 { received: true, event: 'selvtest' }<\/code>. Skift en enkelt byte i <code>body<\/code> efter signeringen, og status bliver 401. Det er hele HMAC-m\u00f8nsteret i omkring 50 linjer kode, og det bruger udelukkende Node.js-kerne og Express.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"6-almindelige-faldgruber\">6 almindelige faldgruber<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">De fleste HMAC-fejl er ikke i kryptografien, men i, hvordan koden bruger den. Her er de seks, der oftest rammer udviklere i praksis.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>At sammenligne med <code>===<\/code> i stedet for <code>timingSafeEqual<\/code>.<\/strong> Det er den klassiske timingbrist. Almindelig strengsammenligning afsl\u00f8rer via tidsforbrug, hvor mange tegn der matchede, og \u00e5bner for, at en angriber g\u00e6tter signaturen byte for byte.<\/li>\n<li><strong>At verificere over den parsede body i stedet for de r\u00e5 bytes.<\/strong> <code>JSON.parse<\/code> efterfulgt af <code>JSON.stringify<\/code> \u00e6ndrer ofte bytes, og din ellers \u00e6gte webhook bliver afvist. Gem altid den r\u00e5 Buffer.<\/li>\n<li><strong>At glemme l\u00e6ngdetjekket f\u00f8r <code>timingSafeEqual<\/code>.<\/strong> Funktionen kaster en <code>RangeError<\/code>, hvis bufferne har forskellig l\u00e6ngde, og en ufanget fejl kan crashe din handler eller afsl\u00f8re detaljer i en stack trace.<\/li>\n<li><strong>At bruge en svag eller kort n\u00f8gle.<\/strong> &#8220;secret&#8221; eller &#8220;test123&#8221; giver ingen reel beskyttelse. Brug mindst 32 tilf\u00e6ldige bytes fra <code>crypto.randomBytes<\/code>.<\/li>\n<li><strong>At signere body uden timestamp.<\/strong> Uden timestamp og replay-beskyttelse kan en opsnappet, gyldig webhook genafspilles. Inklud\u00e9r altid et timestamp i det signerede data.<\/li>\n<li><strong>At l\u00e6gge n\u00f8glen i kildekoden eller git.<\/strong> En n\u00f8gle i et repository er en l\u00e6kket n\u00f8gle. Brug milj\u00f8variabler eller en hemmelighedstjeneste, og roter straks, hvis en n\u00f8gle nogensinde er blevet committet.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fejlfinding-8-typiske-problemer\">Fejlfinding: 8 typiske problemer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e5r verifikationen fejler, er \u00e5rsagen n\u00e6sten altid \u00e9n af nedenst\u00e5ende. Tabellen kobler symptom til \u00e5rsag og l\u00f8sning, s\u00e5 du hurtigt kan finde fejlen.<\/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>\n<tr><td>Gyldig webhook afvises altid<\/td><td>Verifikation over parset body, ikke r\u00e5 bytes<\/td><td>Gem rawBody via express.json verify-callback<\/td><\/tr>\n<tr><td>RangeError fra timingSafeEqual<\/td><td>Buffere af forskellig l\u00e6ngde<\/td><td>Tjek l\u00e6ngden, og afvis, f\u00f8r du sammenligner<\/td><\/tr>\n<tr><td>Signaturer matcher aldrig<\/td><td>Forskellig n\u00f8gle p\u00e5 de to sider<\/td><td>Bekr\u00e6ft, at samme HMAC_SECRET bruges begge steder<\/td><\/tr>\n<tr><td>Matcher lokalt, fejler i produktion<\/td><td>Forskellig kodning, hex mod base64<\/td><td>Dokument\u00e9r og match \u00e9n kanonisk kodning<\/td><\/tr>\n<tr><td>Alt afvises efter n\u00f8gleskift<\/td><td>Afsender og modtager ude af sync<\/td><td>Brug n\u00f8glerotation med flere aktive n\u00f8gler<\/td><\/tr>\n<tr><td>Gamle beskeder accepteres<\/td><td>Manglende timestamp-kontrol<\/td><td>Tilf\u00f8j timestamp-vindue og replay-guard<\/td><\/tr>\n<tr><td>Forkert MAC for \u00e6, \u00f8, \u00e5 i body<\/td><td>Forkert tegnkodning ved toString<\/td><td>Brug konsekvent utf8 begge steder<\/td><\/tr>\n<tr><td>Header er undefined<\/td><td>Proxy fjerner X-Signature-header<\/td><td>Tillad headeren eksplicit i proxy eller load balancer<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Et godt fejlfindingstrick: log den forventede og den modtagne MAC side om side i et testmilj\u00f8, aldrig i produktion. St\u00e5r de forskelligt allerede i de f\u00f8rste par tegn, er det typisk en kodnings- eller n\u00f8glefejl. Er de identiske bortset fra l\u00e6ngden, er det en kodningsforskel. Log aldrig selve n\u00f8glen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"avancerede-tips\">Avancerede tips<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"brug-webcrypto-til-portabel-kode\">Brug WebCrypto til portabel kode<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Hvis din kode ogs\u00e5 skal k\u00f8re i browseren eller i edge-runtimes som Cloudflare Workers og Deno, kan du bruge WebCrypto-API&#8217;et via <code>crypto.subtle<\/code> i stedet for <code>createHmac<\/code>. Det er asynkront og en smule mere omst\u00e6ndeligt, men virker p\u00e5 tv\u00e6rs af alle moderne JavaScript-runtimes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ WebCrypto-variant, virker i browser, Node, Deno og Workers\nasync function signSubtle(message, secretBytes) {\n  const key = await crypto.subtle.importKey(\n    'raw', secretBytes,\n    { name: 'HMAC', hash: 'SHA-256' },\n    false, ['sign'],\n  );\n  const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message));\n  return Buffer.from(mac).toString('hex');\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hmac-som-noegleafledning\">HMAC som n\u00f8gleafledning<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">HMAC er byggestenen i HKDF, den standardiserede n\u00f8gleafledningsfunktion i RFC 5869. Har du brug for at udlede flere n\u00f8gler fra \u00e9n hovedn\u00f8gle, s\u00e5 brug <code>crypto.hkdfSync<\/code> i stedet for at finde p\u00e5 din egen konstruktion. Den bruger HMAC under motorhjelmen og er gennemt\u00e6nkt af kryptografer, hvilket din egen l\u00f8sning sj\u00e6ldent er.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"overvej-aead-naar-du-ogsaa-skal-kryptere\">Overvej AEAD, n\u00e5r du ogs\u00e5 skal kryptere<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Skal data b\u00e5de skjules og autentificeres, s\u00e5 lad v\u00e6re med at klistre HMAC oven p\u00e5 din egen kryptering. Brug i stedet en AEAD-ordning som AES-256-GCM, der giver kryptering og integritet i \u00e9t trin og lukker de f\u00e6lder, der opst\u00e5r, n\u00e5r man kombinerer kryptering og MAC i h\u00e5nden. Node.js underst\u00f8tter AES-GCM direkte via <code>crypto.createCipheriv<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"hmac-mod-digitale-signaturer-hvornaar-vaelger-du-hvad\">HMAC mod digitale signaturer: hvorn\u00e5r v\u00e6lger du hvad?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">HMAC og digitale signaturer l\u00f8ser besl\u00e6gtede problemer, men de er ikke ens. HMAC bruger \u00e9n delt hemmelig n\u00f8gle; begge parter kan b\u00e5de signere og verificere. En digital signatur, for eksempel Ed25519 eller ECDSA, bruger et n\u00f8glepar; kun indehaveren af den private n\u00f8gle kan signere, og alle med den offentlige n\u00f8gle kan verificere. Valget afh\u00e6nger af, om de to parter stoler p\u00e5 hinanden med samme hemmelighed.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Egenskab<\/th><th>HMAC<\/th><th>Digital signatur (Ed25519)<\/th><\/tr><\/thead><tbody>\n<tr><td>N\u00f8gletype<\/td><td>\u00c9n delt hemmelig n\u00f8gle<\/td><td>Privat\/offentligt n\u00f8glepar<\/td><\/tr>\n<tr><td>Hvem kan signere<\/td><td>Begge parter<\/td><td>Kun indehaver af privat n\u00f8gle<\/td><\/tr>\n<tr><td>Hvem kan verificere<\/td><td>Begge parter<\/td><td>Alle med offentlig n\u00f8gle<\/td><\/tr>\n<tr><td>Uafviselighed<\/td><td>Nej<\/td><td>Ja<\/td><\/tr>\n<tr><td>Hastighed<\/td><td>Meget hurtig<\/td><td>Langsommere, men stadig hurtig<\/td><\/tr>\n<tr><td>Typisk brug<\/td><td>Webhooks, API-signering, JWT HS256<\/td><td>Software-signering, certifikater, blockchain<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Tommelfingerreglen: stoler begge parter p\u00e5 samme hemmelighed, og er hastighed vigtig, s\u00e5 v\u00e6lg HMAC. Skal verifikation kunne ske offentligt, eller skal du kunne bevise over for en tredjepart, hvem der signerede, s\u00e5 v\u00e6lg en digital signatur. For webhooks mellem to systemer, du selv kontrollerer, er HMAC det enkle og rigtige valg. Vil du dykke ned i den anden side, s\u00e5 l\u00e6s vores gennemgang af <a href=\"https:\/\/shattered.io\/dk\/cryptography\/ed25519-signaturer-nodejs\/\">Ed25519-signaturer i Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ofte-stillede-spoergsmaal\">Ofte stillede sp\u00f8rgsm\u00e5l<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"er-hmac-stadig-sikkert-i-2026\">Er HMAC stadig sikkert i 2026?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ja. HMAC-SHA256 betragtes som sikkert og anbefales fortsat af NIST i FIPS 198-1. I mods\u00e6tning til mange andre kryptografiske konstruktioner er HMAC overraskende robust, selv n\u00e5r den underliggende hash f\u00e5r svagheder. HMAC-SHA1 er stadig teoretisk modstandsdygtigt, selvom SHA-1 selv er brudt til kollisioner, men du b\u00f8r alligevel bruge SHA-256 eller bedre til ny kode.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hvad-er-forskellen-paa-hmac-og-en-almindelig-hash\">Hvad er forskellen p\u00e5 HMAC og en almindelig hash?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">En almindelig hash som SHA-256 har ingen n\u00f8gle, s\u00e5 enhver kan beregne den. En angriber kan \u00e6ndre beskeden og lave en ny gyldig hash. HMAC tilf\u00f8jer en hemmelig n\u00f8gle, s\u00e5 kun den, der kender n\u00f8glen, kan lave en gyldig MAC. N\u00f8glen er forskellen, og den er hele grunden til, at HMAC kan autentificere.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hvorfor-kan-jeg-ikke-bare-bruge-sammenligning\">Hvorfor kan jeg ikke bare bruge ===-sammenligning?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fordi <code>===<\/code> afslutter ved f\u00f8rste forskel og dermed bruger forskellig tid afh\u00e6ngigt af, hvor mange tegn der matchede. En angriber kan m\u00e5le den tid og gradvist g\u00e6tte den korrekte signatur. <code>crypto.timingSafeEqual<\/code> bruger altid samme tid og lukker det hul.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"skal-jeg-signere-request-body-eller-det-parsede-objekt\">Skal jeg signere request-body eller det parsede objekt?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Altid de r\u00e5 bytes af request-body, aldrig det parsede objekt. JSON-parsing og genserialisering kan \u00e6ndre bytes, og s\u00e5 fejler verifikationen, selvom beskeden er \u00e6gte. Gem den r\u00e5 Buffer, f\u00f8r du parser, og verific\u00e9r over den.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hvor-lang-skal-min-hmac-noegle-vaere\">Hvor lang skal min HMAC-n\u00f8gle v\u00e6re?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Mindst lige s\u00e5 lang som hashens output. Til HMAC-SHA256 betyder det mindst 32 bytes (256 bit). Gener\u00e9r n\u00f8glen med <code>crypto.randomBytes(32)<\/code>, og gem den som en milj\u00f8variabel. L\u00e6ngere n\u00f8gler skader ikke, men giver ringe ekstra sikkerhed ud over hashens blokst\u00f8rrelse.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"kan-hmac-erstatte-digitale-signaturer\">Kan HMAC erstatte digitale signaturer?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Kun n\u00e5r begge parter trygt kan dele \u00e9n hemmelig n\u00f8gle. HMAC giver ikke uafviselighed, fordi begge parter kan lave gyldige MAC&#8217;er, s\u00e5 du kan ikke bevise over for en tredjepart, hvem der signerede. Skal du have offentlig verifikation eller uafviselighed, s\u00e5 brug en digital signatur som Ed25519 i stedet.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"virker-det-samme-moenster-til-stripe-og-github-webhooks\">Virker det samme m\u00f8nster til Stripe- og GitHub-webhooks?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ja, med sm\u00e5 tilpasninger. Begge bruger HMAC-SHA256 over den r\u00e5 body. GitHub sender signaturen i headeren <code>X-Hub-Signature-256<\/code> med pr\u00e6fiks <code>sha256=<\/code>, og Stripe l\u00e6gger timestamp og signatur i <code>Stripe-Signature<\/code>. L\u00e6s hver udbyders header-format, men selve verifikationen, createHmac plus timingSafeEqual over de r\u00e5 bytes, er n\u00f8jagtig den samme.<\/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\">\n<li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/ed25519-signaturer-nodejs\/\">Ed25519 i Node.js: signaturer i 12 trin<\/a><\/li>\n<li><a href=\"https:\/\/shattered.io\/dk\/security\/sikker-session-nodejs\/\">Sikker session i Node.js: 12 trin p\u00e5 30 min<\/a><\/li>\n<li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/hashfunktioner\/\">Hashfunktioner: egenskaber, form\u00e5l og praktisk brug<\/a><\/li>\n<li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/sha-256\/\">SHA-256 forklaret: hj\u00f8rnestenen i moderne hashing<\/a><\/li>\n<li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/digitale-signaturer\/\">Digitale signaturer: hvordan hashing og n\u00f8gler skaber tillid<\/a><\/li>\n<li><a href=\"https:\/\/shattered.io\/dk\/security\/ssl-tls-certifikat-certbot-2026\/\">Gratis SSL\/TLS-certifikat: 12 trin med Certbot<\/a><\/li>\n<li><a href=\"https:\/\/shattered.io\/dk\/cryptography\/\">Kryptografi: hashfunktioner, SHA og digital tillid<\/a><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"eksterne-kilder\">Eksterne kilder<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/nodejs.org\/api\/crypto.html\" target=\"_blank\" rel=\"noopener\">Node.js crypto-modulets dokumentation<\/a><\/li>\n<li><a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc2104\" target=\"_blank\" rel=\"noopener\">RFC 2104: HMAC-specifikationen<\/a><\/li>\n<li><a href=\"https:\/\/csrc.nist.gov\/pubs\/fips\/198-1\/final\" target=\"_blank\" rel=\"noopener\">NIST FIPS 198-1: The Keyed-Hash Message Authentication Code<\/a><\/li>\n<li><a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6234\" target=\"_blank\" rel=\"noopener\">RFC 6234: US Secure Hash Algorithms (SHA)<\/a><\/li>\n<li><a href=\"https:\/\/nodejs.org\/en\/blog\/vulnerability\/\" target=\"_blank\" rel=\"noopener\">Node.js sikkerhedsmeddelelser<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Du har nu en komplet, sikker HMAC-l\u00f8sning i Node.js: n\u00f8glegenerering, signering, konstant-tids-verifikation, en Express-server til b\u00e5de udg\u00e5ende og indg\u00e5ende webhooks, replay-beskyttelse, n\u00f8glerotation og tests. Det hele k\u00f8rer p\u00e5 Node.js-kerne plus Express, uden et eneste eksternt kryptobibliotek. Kopi\u00e9r det komplette projekt fra denne tutorial, s\u00e6t din egen n\u00f8gle, og du har et fundament, der matcher den m\u00e5de, Stripe, GitHub og Slack signerer deres webhooks p\u00e5.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>En webhook uden signatur er en \u00e5ben d\u00f8r. Enhver, der kender din URL, kan sende en falsk betaling, en falsk ordre eller en falsk &#8220;konto slettet&#8221;-besked, og din server vil\u2026<\/p>\n","protected":false},"author":7,"featured_media":92,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-91","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\/91","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\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/comments?post=91"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/91\/revisions"}],"predecessor-version":[{"id":93,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/91\/revisions\/93"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media\/92"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media?parent=91"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/categories?post=91"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/tags?post=91"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}