Viestin eheys ja alkuperän todennus ovat web-sovellusten näkymättömiä peruspilareita. Kun Stripe lähettää webhookin maksusta, kun GitHub ilmoittaa uudesta pushista tai kun kaksi mikropalvelua vaihtaa tietoja, vastaanottajan on voitava varmistua kahdesta asiasta: dataa ei ole muutettu matkalla, ja se tulee oikealta lähettäjältä. HMAC (Hash-based Message Authentication Code) ratkaisee molemmat ongelmat yhdellä kompaktilla tunnisteella. Tässä oppaassa rakennat Node.js:llä toimivan HMAC-todennuksen alusta tuotantokuntoon 10 vaiheessa, noin 30 minuutissa.

Node.js sisältää HMAC-tuen suoraan vakiomoduulissa node:crypto, joten ulkoisia kirjastoja ei tarvita. Käymme läpi avaimen turvallisen generoinnin, aikahyökkäyksiltä suojaavan vertailun crypto.timingSafeEqual-funktiolla, webhookien allekirjoituksen varmennuksen sekä selainyhteensopivan WebCrypto-toteutuksen. Lopuksi kokoamme täydellisen Express-projektin, kirjoitamme testit ja käymme läpi yli kahdeksan vianmäärityskohtaa. Päivitetty 14. kesäkuuta 2026.

Mikä HMAC on ja miksi sitä tarvitaan Node.js:ssä

HMAC on standardin RFC 2104 määrittelemä avaimellinen viestintodennuskoodi. Se yhdistää kryptografisen tiivistefunktion (kuten SHA-256) ja salaisen avaimen tuottaakseen kiinteämittaisen tunnisteen, jota kutsutaan tagiksi. Vain ne osapuolet, jotka tuntevat saman salaisen avaimen, voivat tuottaa kelvollisen tagin tietylle viestille. Tämä erottaa HMAC:n tavallisesta tiivisteestä: pelkkä SHA-256 todistaa vain, että data ei ole muuttunut, kun taas HMAC todistaa lisäksi, että data tulee avaimen haltijalta.

HMAC-rakenne on suunniteltu kestämään niin sanottuja pituuslaajennushyökkäyksiä (length extension attacks), jotka vaivaavat naiiveja toteutuksia tyyppiä hash(salaisuus + viesti). RFC 2104:n mukainen rakenne käyttää avainta kahdessa kierroksessa sisemmän ja ulomman padding-arvon (ipad ja opad) kanssa, mikä tekee siitä todistettavasti turvallisen, kun taustalla oleva tiivistefunktio on vahva. Juuri tästä syystä HMAC-SHA256 on alan de facto -standardi rajapintojen allekirjoituksessa.

Node.js:ssä HMAC on osa node:crypto-moduulia, joka on kääre OpenSSL:n tiiviste-, HMAC-, salaus- ja allekirjoitustoiminnoille. Moduuli on vakaa ja saatavilla ilman asennuksia. Node.js 24.x “Krypton” siirtyi pitkäaikaistuettuun LTS-vaiheeseen 28. lokakuuta 2025, ja se saa tukea huhtikuuhun 2028 asti, joten se on suositeltava ajoympäristö tuotantoon vuonna 2026. Uudemmissa julkaisuissa (dokumentaatio kattaa jo v26-sarjan) crypto-rajapinta on yhteneväinen, joten tämän oppaan koodi toimii sellaisenaan sekä Node 24 LTS:ssä että tuoreemmissa versioissa.

Käytännön käyttökohteita on runsaasti. Stripe allekirjoittaa webhook-tapahtumat HMAC-SHA256:lla ja toimittaa allekirjoituksen Stripe-Signature-otsakkeessa. GitHub käyttää samaa algoritmia ja lähettää tagin X-Hub-Signature-256-otsakkeessa. Lisäksi HMAC sopii API-avainten allekirjoitukseen, istuntoevästeiden eheyden suojaukseen, kertakäyttöisten latauslinkkien luontiin ja mikropalveluiden väliseen todennukseen. Jos haluat syventyä siihen, miten tiivistefunktiot toimivat HMAC:n alla, lue erillinen artikkelimme kryptografisista tiivistefunktioista.

HMAC vs tavallinen tiiviste vs digitaalinen allekirjoitus

Ennen koodausta on tärkeää ymmärtää, milloin HMAC on oikea työkalu ja milloin tarvitset jotain muuta. Kaikki kolme mekanismia takaavat eheyden, mutta ne eroavat avaintenhallinnan ja kiistämättömyyden suhteen. HMAC käyttää symmetristä salaisuutta: sama avain sekä luo että varmentaa tagin. Digitaalinen allekirjoitus, kuten ECDSA tai EdDSA, käyttää julkista ja yksityistä avainparia, jolloin kuka tahansa voi varmentaa allekirjoituksen mutta vain yksityisen avaimen haltija voi luoda sen.

OminaisuusTavallinen tiiviste (SHA-256)HMAC-SHA256Digitaalinen allekirjoitus (EdDSA)
Takaa eheydenKylläKylläKyllä
Todentaa lähettäjänEiKyllä (avaimen haltija)Kyllä (yksityinen avain)
AvaintyyppiEi avaintaYksi jaettu salaisuusJulkinen + yksityinen avain
KiistämättömyysEiEiKyllä
SuorituskykyNopeinNopeaHitain
Tyypillinen käyttöTiedoston eheysWebhookit, API-allekirjoitusSertifikaatit, ohjelmistojulkaisut

Valitse HMAC, kun molemmat osapuolet voivat turvallisesti jakaa yhden salaisuuden, kuten palveluntarjoaja ja sen integroija. Tämä on tyypillinen tilanne webhookeissa ja sisäisissä rajapinnoissa. Valitse digitaalinen allekirjoitus, kun varmentajia on monta tai kun tarvitset kiistämättömyyttä, eli todistetta siitä, että vain tietty taho on voinut luoda viestin. Esimerkiksi ohjelmistopakettien julkaisuissa allekirjoitus on välttämätön, koska julkista avainta jaetaan tuhansille käyttäjille. Lue lisää erosta artikkelistamme digitaalisista allekirjoituksista.

Tavallinen tiiviste yksinään ei koskaan todenna lähettäjää. Yleinen virhe on luulla, että sha256(data) suojaa peukaloinnilta, mutta jos hyökkääjä voi muuttaa dataa, hän voi laskea myös uuden tiivisteen. HMAC sulkee tämän aukon vaatimalla salaisen avaimen. Tästä syystä esimerkiksi turvalliset istunnot Node.js:ssä nojaavat juuri HMAC-allekirjoitettuihin evästeisiin sen sijaan, että ne luottaisivat pelkkään tiivisteeseen.

Esivaatimukset ja versiot

Tämä opas olettaa perustason JavaScript- ja Node.js-osaamisen sekä komentorivin käytön. Et tarvitse aiempaa kryptografiakokemusta. Kaikki HMAC-toiminnallisuus tulee Node.js:n vakiokirjastosta, joten ainoa pakollinen riippuvuus on Node itse. Express ja Vitest tarvitaan vain täydellisen projektin ja testien vaiheissa.

TyökaluSuositeltu versio (2026)Tarkoitus
Node.js24.11.0 LTS (“Krypton”) tai uudempiAjoympäristö ja node:crypto-moduuli
npm11.x (Node 24:n mukana)Pakettienhallinta
node:cryptoSisäänrakennettuHMAC, timingSafeEqual, WebCrypto
Express5.xWebhook-vastaanotin (valinnainen)
Vitest3.xYksikkötestaus (valinnainen)
dotenv17.xSalaisuuksien lataus ympäristöstä

Tarkista asennettu Node.js-versio ennen aloitusta. Jos käytössäsi on vanhempi kuin 24 LTS, päivitä se ennen jatkamista, koska vanhentuneet versiot eivät enää saa tietoturvapäivityksiä. Versionhallinta onnistuu helpoiten nvm-työkalulla.

$ node --version
v24.11.0

$ npm --version
11.0.0

Yksi tärkeä periaate kulkee mukana koko oppaan ajan: salaisia avaimia ei koskaan kovakoodata lähdekoodiin eikä committoida versionhallintaan. Lataamme ne ympäristömuuttujista. Tämä on perusedellytys turvalliselle tuotantokäytölle, ja palaamme aiheeseen tarkemmin vaiheessa 10.

Vaihe 1: Projektin alustus ja ensimmäinen HMAC

Aloita luomalla uusi projektikansio ja alustamalla se npm:llä. Asetamme projektin käyttämään ES-moduuleita lisäämällä "type": "module" package.json-tiedostoon, jotta voimme käyttää modernia import-syntaksia.

$ mkdir hmac-nodejs && cd hmac-nodejs
$ npm init -y
$ npm pkg set type=module

Luo tiedosto hmac.js ja kirjoita ensimmäinen HMAC-laskelma. Funktio crypto.createHmac(algoritmi, avain) palauttaa Hmac-olion, jota päivitetään update()-metodilla ja viimeistellään digest()-metodilla. Digest voidaan koodata heksaksi, base64:ksi tai palauttaa Bufferina.

// hmac.js
import { createHmac } from 'node:crypto';

const secret = 'super-salainen-avain-vain-testiin';
const message = 'maksu:1000:EUR:tilaus-42';

const tag = createHmac('sha256', secret)
  .update(message)
  .digest('hex');

console.log('Viesti:', message);
console.log('HMAC-SHA256:', tag);

Aja tiedosto komennolla node hmac.js. Saat aina saman 64 merkin heksatunnisteen samalla viestillä ja avaimella, koska HMAC on deterministinen. Jos muutat viestiä yhdellä merkillä, koko tagi muuttuu täysin. Tämä lumivyöryefekti on tiivistefunktioiden ydinominaisuus.

$ node hmac.js
Viesti: maksu:1000:EUR:tilaus-42
HMAC-SHA256: 9f4c1e2a7b8d3f6e0a1c2b3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f50617

SHA-256 tuottaa aina 256-bittisen eli 32-tavuisen tagin, joka heksana on 64 merkkiä. Jos vaihdat algoritmin sha512:ksi, saat 128 merkin tagin. Algoritmin valinnasta puhumme lisää suorituskykyosiossa, mutta SHA-256 on turvallinen ja nopea oletus lähes kaikkeen. Jos haluat ymmärtää SHA-256:n sisäisen toiminnan, katso syvällinen artikkelimme SHA-256:sta.

Vaihe 2: Salaisen avaimen turvallinen generointi

Edellisen vaiheen merkkijonosalaisuus kelpaa kokeiluun, mutta tuotannossa avaimen on oltava satunnainen ja riittävän pitkä. NIST SP 800-107 suosittaa, että HMAC-avain on vähintään yhtä pitkä kuin tiivisteen ulostulo. SHA-256:lla tämä tarkoittaa vähintään 256 bittiä eli 32 tavua, ja SHA-512:lla vähintään 512 bittiä eli 64 tavua. Liian lyhyt avain heikentää koko mekanismin turvallisuutta.

Generoi avain kryptografisesti turvallisella satunnaislähteellä crypto.randomBytes. Älä koskaan käytä Math.random()-funktiota avaimiin, koska se ei ole kryptografisesti turvallinen. Seuraava skripti tuottaa 32-tavuisen avaimen ja tulostaa sen heksana, jonka voit tallentaa ympäristömuuttujaan.

// generate-key.js
import { randomBytes } from 'node:crypto';

// 32 tavua = 256 bittiä, NIST SP 800-107 -minimi SHA-256:lle
const key = randomBytes(32).toString('hex');

console.log('HMAC_SECRET=' + key);
$ node generate-key.js
HMAC_SECRET=3b1f8c0d5e2a9476b8c1d0e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d

Tallenna tulostettu rivi .env-tiedostoon ja lisää tiedosto .gitignore:hen heti. Lataa salaisuus koodissa ympäristöstä, älä koskaan suoraan tiedostosta lähdekoodissa. Asenna dotenv ja käytä sitä kehitysvaiheessa.

// key-config.js
import 'dotenv/config';

const secret = process.env.HMAC_SECRET;
if (!secret || secret.length < 64) {
  throw new Error('HMAC_SECRET puuttuu tai on liian lyhyt (vaaditaan 32 tavua heksana)');
}

// Buffer on parempi kuin merkkijono: vältät koodausvirheet
export const hmacKey = Buffer.from(secret, 'hex');

Huomaa kaksi yksityiskohtaa. Ensinnäkin validoimme, että salaisuus on olemassa ja riittävän pitkä jo sovelluksen käynnistyessä, jolloin virhe paljastuu heti eikä vasta ensimmäisen pyynnön kohdalla. Toiseksi muunnamme heksamerkkijonon Bufferiksi. Bufferin tai KeyObjectin käyttö merkkijonon sijaan vähentää koodausvirheitä, koska crypto-funktiot tulkitsevat merkkijonot UTF-8:na ellei toisin määritetä.

Vaihe 3: Aikaturvallinen vertailu timingSafeEqual-funktiolla

Tämä on oppaan tärkein tietoturvavaihe. Kun varmennat saapuvan tagin, sinun on verrattava sitä itse laskemaasi tagiin. Tavallinen vertailu === tai merkkijonojen == palaa heti ensimmäisen eroavan tavun kohdalla. Tämä luo mitattavan aikaeron, jota hyökkääjä voi käyttää tagin arvaamiseen tavu tavulta. Tällaista hyökkäystä kutsutaan aikahyökkäykseksi (timing attack).

Node.js tarjoaa juuri tähän tarkoitukseen funktion crypto.timingSafeEqual(a, b), joka vertaa kahta Bufferia vakioajassa riippumatta siitä, missä kohtaa ne eroavat. Funktio vaatii, että molemmat puskurit ovat samanpituiset, joten vertaa aina raakatavuja, älä eripituisia merkkijonoja.

// verify.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function sign(message, key) {
  return createHmac('sha256', key).update(message).digest();
}

export function verify(message, key, receivedTagHex) {
  const expected = sign(message, key);
  let received;
  try {
    received = Buffer.from(receivedTagHex, 'hex');
  } catch {
    return false;
  }
  // Pituuden on täsmättävä, muuten timingSafeEqual heittää poikkeuksen
  if (received.length !== expected.length) return false;
  return timingSafeEqual(expected, received);
}

Huomaa, että tarkistamme pituuden ennen timingSafeEqual-kutsua. Tämä pituustarkistus ei vuoda hyödyllistä tietoa, koska tagin pituus on julkinen vakio (SHA-256:lla aina 32 tavua). Jos pituudet eivät täsmää, syöte on selvästi virheellinen. Älä koskaan korvaa tätä funktiota omalla silmukalla, sillä JavaScriptin optimoinnit voivat tehdä siitä huomaamatta aikariippuvaisen.

// test-verify.js
import { verify, sign } from './verify.js';

const key = Buffer.from('3b1f8c0d5e2a9476b8c1d0e3f4a5b6c7', 'hex');
const msg = 'tilaus-42';

const goodTag = sign(msg, key).toString('hex');
console.log('Oikea tagi:', verify(msg, key, goodTag));            // true
console.log('Väärä tagi:', verify(msg, key, 'deadbeef'));          // false
console.log('Muutettu viesti:', verify('tilaus-43', key, goodTag)); // false
$ node test-verify.js
Oikea tagi: true
Väärä tagi: false
Muutettu viesti: false

Tämä vertailufunktio on koko todennusjärjestelmäsi sydän. Jos toteutat tämän väärin, vaikkapa tavallisella merkkijonovertailulla, koko HMAC-suojaus heikkenee merkittävästi. Pidä funktio yksinkertaisena, testattuna ja keskitettynä yhteen paikkaan.

Vaihe 4: Webhookin allekirjoituksen varmennus (GitHub-tyyli)

Yleisin HMAC:n käyttökohde on saapuvien webhookien varmennus. GitHub allekirjoittaa jokaisen webhook-toimituksen HMAC-SHA256:lla ja lähettää tagin otsakkeessa X-Hub-Signature-256 muodossa sha256=<heksatagi>. Vastaanottajan on laskettava sama HMAC pyynnön raakarungosta ja verrattava sitä vakioajassa.

Kriittinen yksityiskohta: HMAC on laskettava täsmälleen samoista raakatavuista, jotka lähettäjä allekirjoitti. Jos jäsennät JSON:n ja sarjallistat sen uudelleen, välilyönnit tai avainjärjestys voivat muuttua, ja allekirjoitus ei täsmää. Käytä siis aina pyynnön raakarunkoa, älä jäsennettyä oliota.

// github-webhook.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyGithubSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader) return false;

  const expected = 'sha256=' +
    createHmac('sha256', secret).update(rawBody).digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Tässä esimerkissä vertaamme koko otsakemerkkijonoa sha256=... mukaan lukien etuliite, mikä on turvallista ja yksinkertaista. Vaihtoehtoisesti voit irrottaa etuliitteen ja verrata pelkkiä tagitavuja. Kumpikin tapa toimii, kunhan vertailu tapahtuu timingSafeEqual-funktiolla.

Express 5:ssä raakarungon saa middlewarella, joka tallentaa puskurin ennen JSON-jäsennystä. Tämä on välttämätöntä, koska oletuksena express.json() kuluttaa rungon eikä raakatavuja ole enää saatavilla.

// raw-body.js
import express from 'express';

export function rawBodySaver(req, res, buf) {
  req.rawBody = buf; // tallenna raakatavut myöhempää HMAC-laskentaa varten
}

export const jsonWithRaw = express.json({ verify: rawBodySaver });

GitHubin virallinen ohje webhookien varmennukseen vahvistaa tämän lähestymistavan ja korostaa raakarungon käyttöä. Voit lukea sen GitHubin dokumentaatiosta. Sama periaate pätee Stripeen, jonka käymme läpi seuraavaksi.

Vaihe 5: Stripe-webhookin varmennus aikaleimalla

Stripe vie webhook-allekirjoituksen askeleen pidemmälle lisäämällä aikaleiman. Allekirjoitettava sisältö on muotoa <aikaleima>.<raakarunko>, ja itse allekirjoitus toimitetaan Stripe-Signature-otsakkeessa muodossa t=aikaleima,v1=tagi. Aikaleima estää toistohyökkäykset (replay attacks): jos sama, kerran kaapattu pyyntö lähetetään uudelleen tuntien kuluttua, vastaanottaja voi hylätä sen liian vanhana.

// stripe-webhook.js
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 300; // 5 minuuttia

export function verifyStripeSignature(rawBody, sigHeader, secret, nowSeconds) {
  const parts = Object.fromEntries(
    sigHeader.split(',').map((p) => p.split('='))
  );
  const timestamp = Number(parts.t);
  const received = parts.v1;
  if (!timestamp || !received) return false;

  // Toistohyökkäyssuojaus: hylkää liian vanhat pyynnöt
  if (Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) return false;

  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Funktio ottaa nykyhetken sekunteina parametrina, mikä tekee siitä helpon testata. Tuotannossa kutsut sitä arvolla Math.floor(Date.now() / 1000). Toleranssi viisi minuuttia on Stripen suosittelema oletus, joka sietää pientä kellojen eroa palvelimien välillä mutta torjuu vanhojen pyyntöjen uudelleenlähetyksen.

Käytännössä tuotannossa kannattaa käyttää Stripen omaa kirjastoa, joka hoitaa nämä yksityiskohdat puolestasi, mutta oman toteutuksen ymmärtäminen auttaa sinua varmentamaan minkä tahansa HMAC-pohjaisen webhookin. Stripen viralliset ohjeet löytyvät Stripen webhook-dokumentaatiosta. Toistohyökkäyssuojaus on kohta, jonka moni oma toteutus unohtaa, joten älä ohita sitä.

Vaihe 6: Lähtevien API-pyyntöjen allekirjoitus

HMAC ei ole vain saapuvien viestien varmennusta varten. Kun sovelluksesi kutsuu ulkoista rajapintaa, joka vaatii allekirjoitettuja pyyntöjä, sinun on luotava HMAC itse. Tyypillinen kaava allekirjoittaa kanonisen merkkijonon, joka koostuu HTTP-metodista, polusta, aikaleimasta ja rungosta. Tämä estää sekä peukaloinnin että toistohyökkäykset.

// sign-request.js
import { createHmac } from 'node:crypto';

export function signRequest({ method, path, body, key, timestamp }) {
  // Kanoninen muoto: kiinteä järjestys ja erotin
  const canonical = [
    method.toUpperCase(),
    path,
    timestamp,
    body ?? '',
  ].join('\n');

  const signature = createHmac('sha256', key)
    .update(canonical)
    .digest('hex');

  return {
    'X-Timestamp': String(timestamp),
    'X-Signature': `sha256=${signature}`,
  };
}

Olennaista on kanoninen muoto: lähettäjän ja vastaanottajan on rakennettava täsmälleen sama merkkijono, muuten allekirjoitukset eivät täsmää. Kiinteä kenttäjärjestys ja yksiselitteinen erotin (tässä rivinvaihto) ovat välttämättömiä. Vältä JSON:n serialisointia kanonisessa muodossa, ellei avainjärjestys ole determinististä, koska eri kielet ja kirjastot voivat järjestää avaimet eri tavoin.

// use-sign.js
import { signRequest } from './sign-request.js';

const headers = signRequest({
  method: 'POST',
  path: '/v1/orders',
  body: JSON.stringify({ amount: 1000, currency: 'EUR' }),
  key: Buffer.from(process.env.HMAC_SECRET, 'hex'),
  timestamp: Math.floor(Date.now() / 1000),
});

console.log(headers);
// {
//   'X-Timestamp': '1781469600',
//   'X-Signature': 'sha256=a1b2c3...'
// }

Vastaanottava palvelin toistaa saman kanonisen muodon omasta pyynnöstään, laskee HMAC:n ja vertaa sitä timingSafeEqual-funktiolla otsakkeen tagiin sekä tarkistaa aikaleiman tuoreuden. Näin saat symmetrisen, kevyen ja vahvan todennuksen kahden hallitsemasi palvelun välille ilman raskasta julkisen avaimen infrastruktuuria.

Vaihe 7: WebCrypto-yhteensopiva HMAC crypto.subtle-rajapinnalla

Jos haluat kirjoittaa koodia, joka toimii sekä Node.js:ssä että selaimessa tai Edge-ajoympäristöissä (kuten Cloudflare Workers tai Deno), käytä standardoitua WebCrypto-rajapintaa crypto.subtle. Se on saatavilla Node.js:ssä globaalina ja noudattaa samaa W3C-määritystä kuin selaimet. Rajapinta on asynkroninen ja perustuu lupauksiin (Promise).

// webcrypto-hmac.js
const enc = new TextEncoder();

export async function importKey(rawKeyBytes) {
  return crypto.subtle.importKey(
    'raw',
    rawKeyBytes,
    { name: 'HMAC', hash: 'SHA-256' },
    false,            // ei vietävissä
    ['sign', 'verify']
  );
}

export async function signWeb(message, cryptoKey) {
  const sig = await crypto.subtle.sign('HMAC', cryptoKey, enc.encode(message));
  return Buffer.from(sig).toString('hex');
}

export async function verifyWeb(message, cryptoKey, tagHex) {
  const tag = Buffer.from(tagHex, 'hex');
  // subtle.verify hoitaa vakioaikaisen vertailun sisäisesti
  return crypto.subtle.verify('HMAC', cryptoKey, tag, enc.encode(message));
}

WebCrypto-rajapinnan etu on, että crypto.subtle.verify hoitaa vakioaikaisen vertailun automaattisesti, joten erillistä timingSafeEqual-kutsua ei tarvita varmennuksessa. Lisäksi avain merkitään ei-vietäväksi (extractable: false), jolloin raaka-avainta ei voi vahingossa lukea takaisin muistista. Tämä on hyvä puolustava käytäntö.

// test-webcrypto.js
import { importKey, signWeb, verifyWeb } from './webcrypto-hmac.js';
import { randomBytes } from 'node:crypto';

const key = await importKey(randomBytes(32));
const tag = await signWeb('hei maailma', key);
console.log('Tagi:', tag);
console.log('Varmennus:', await verifyWeb('hei maailma', key, tag)); // true

Valitse node:crypto:n synkroninen createHmac, kun koodi ajetaan vain Node.js:ssä ja haluat yksinkertaisuutta. Valitse WebCrypto, kun tavoittelet siirrettävyyttä useaan ajoympäristöön. Molemmat tuottavat täsmälleen saman HMAC-tagin samalla avaimella ja viestillä, joten ne ovat keskenään yhteentoimivia.

Vaihe 8: Täydellinen Express-webhook-projekti

Nyt kokoamme palaset toimivaksi sovellukseksi. Rakennamme Express 5 -palvelimen, joka vastaanottaa webhookeja, varmentaa niiden HMAC-allekirjoituksen ja käsittelee vain kelvolliset pyynnöt. Asenna riippuvuudet ensin.

$ npm install express dotenv
// server.js
import 'dotenv/config';
import express from 'express';
import { verifyGithubSignature } from './github-webhook.js';

const app = express();
const secret = Buffer.from(process.env.HMAC_SECRET, 'hex');

// Tallenna raakatavut ennen JSON-jäsennystä
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; },
}));

app.post('/webhook', (req, res) => {
  const signature = req.get('X-Hub-Signature-256');

  if (!verifyGithubSignature(req.rawBody, signature, secret)) {
    console.warn('Hylätty: virheellinen allekirjoitus');
    return res.status(401).json({ error: 'invalid signature' });
  }

  // Allekirjoitus on kelvollinen, käsittele tapahtuma turvallisesti
  console.log('Hyväksytty tapahtuma:', req.body.action ?? 'tuntematon');
  res.status(200).json({ received: true });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Kuuntelee porttia ${PORT}`));

Palvelin hylkää kaikki pyynnöt, joiden allekirjoitus puuttuu tai ei täsmää, ja palauttaa HTTP 401:n. Vasta kelvollisen allekirjoituksen jälkeen sovellus käsittelee rungon sisällön. Tämä on oikea järjestys: varmenna ensin, käsittele sitten. Älä koskaan luota rungon dataan ennen allekirjoituksen varmennusta.

Testaa palvelinta paikallisesti laskemalla kelvollinen allekirjoitus ja lähettämällä se curlilla. Seuraava apuskripti tulostaa valmiin curl-komennon.

// make-request.js
import 'dotenv/config';
import { createHmac } from 'node:crypto';

const secret = Buffer.from(process.env.HMAC_SECRET, 'hex');
const body = JSON.stringify({ action: 'opened', number: 42 });
const sig = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');

console.log(`curl -X POST http://localhost:3000/webhook \\
  -H "Content-Type: application/json" \\
  -H "X-Hub-Signature-256: ${sig}" \\
  -d '${body}'`);
$ node make-request.js | sh
{"received":true}

# Palvelimen lokissa:
Hyväksytty tapahtuma: opened

Jos muutat runkoa tai allekirjoitusta yhdelläkin merkillä, palvelin vastaa 401 invalid signature. Tämä on koko HMAC-suojauksen konkreettinen lopputulos: vain oikealla salaisuudella allekirjoitetut pyynnöt pääsevät läpi.

Vaihe 9: Yksikkötestit Vitestillä

Kryptografinen koodi on juuri sitä koodia, joka on testattava huolellisesti, koska virheet eivät näy normaalissa käytössä vaan vasta hyökkäyksessä. Kirjoitamme testit varmennusfunktiolle Vitestillä. Asenna se kehitysriippuvuudeksi.

$ npm install -D vitest
$ npm pkg set scripts.test=vitest
// verify.test.js
import { describe, it, expect } from 'vitest';
import { createHmac, randomBytes } from 'node:crypto';
import { verifyGithubSignature } from './github-webhook.js';

const key = randomBytes(32);
const body = Buffer.from(JSON.stringify({ action: 'opened' }));
const validSig = 'sha256=' + createHmac('sha256', key).update(body).digest('hex');

describe('verifyGithubSignature', () => {
  it('hyväksyy kelvollisen allekirjoituksen', () => {
    expect(verifyGithubSignature(body, validSig, key)).toBe(true);
  });

  it('hylkää muutetun rungon', () => {
    const tampered = Buffer.from(JSON.stringify({ action: 'closed' }));
    expect(verifyGithubSignature(tampered, validSig, key)).toBe(false);
  });

  it('hylkää väärän avaimen', () => {
    expect(verifyGithubSignature(body, validSig, randomBytes(32))).toBe(false);
  });

  it('hylkää puuttuvan otsakkeen', () => {
    expect(verifyGithubSignature(body, undefined, key)).toBe(false);
  });

  it('hylkää väärän pituisen tagin', () => {
    expect(verifyGithubSignature(body, 'sha256=abcd', key)).toBe(false);
  });
});
$ npm test

 ✓ verify.test.js (5 tests) 12ms
   ✓ hyväksyy kelvollisen allekirjoituksen
   ✓ hylkää muutetun rungon
   ✓ hylkää väärän avaimen
   ✓ hylkää puuttuvan otsakkeen
   ✓ hylkää väärän pituisen tagin

 Test Files  1 passed (1)
      Tests  5 passed (5)

Nämä viisi testitapausta kattavat tärkeimmät polut: kelvollinen pyyntö, muutettu runko, väärä avain, puuttuva otsake ja väärän pituinen tagi. Lisää tarvittaessa testit aikaleiman toleranssille ja Stripe-tyyliselle allekirjoitukselle. Hyvä testikattavuus on paras suoja regressioilta, kun koodia muokataan myöhemmin.

Vaihe 10: Salaisuuksien hallinta ja avainten kierto tuotannossa

Viimeinen vaihe vie projektin tuotantokuntoon. Salaisuudet eivät kuulu koodiin eivätkä versionhallintaan. Käytä alustasi salaisuuksien hallintaa: pilvialustojen secret managereita, Kubernetesin Secret-resursseja tai erillistä holvia kuten HashiCorp Vault. Lataa salaisuus ympäristömuuttujasta käynnistyksessä ja validoi se heti.

Avaintenkierto on tärkeä mutta usein unohdettu osa. Tue useaa voimassa olevaa avainta samanaikaisesti, jotta voit vaihtaa avaimen ilman katkoa. Varmennuksessa kokeile saapuvaa tagia kaikkia voimassa olevia avaimia vasten, mutta allekirjoita aina uusimmalla avaimella.

// key-rotation.js
import { createHmac, timingSafeEqual } from 'node:crypto';

// Uusin ensin; allekirjoitus käyttää keys[0]
const keys = (process.env.HMAC_KEYS || '')
  .split(',')
  .filter(Boolean)
  .map((hex) => Buffer.from(hex, 'hex'));

export function signCurrent(message) {
  return createHmac('sha256', keys[0]).update(message).digest('hex');
}

export function verifyAnyKey(message, receivedHex) {
  const received = Buffer.from(receivedHex, 'hex');
  return keys.some((key) => {
    const expected = createHmac('sha256', key).update(message).digest();
    return received.length === expected.length &&
      timingSafeEqual(expected, received);
  });
}

Tällä rakenteella avaimenvaihto on hallittu: lisää uusi avain listan alkuun, anna molempien avainten olla voimassa siirtymäajan, ja poista vanha avain vasta kun kaikki sitä käyttäneet integraatiot ovat siirtyneet. Suosittele integraatiokumppaneille säännöllistä kiertoa, esimerkiksi 90 päivän välein, ja vaadi avaimen vaihto heti, jos epäilet vuotoa. Avaimen vuotaessa HMAC-suojaus murtuu täysin, joten vuodon havaitseminen ja nopea reagointi ovat kriittisiä.

Muista myös HTTPS. HMAC suojaa peukaloinnilta ja todentaa lähettäjän, mutta se ei salaa sisältöä. Aja webhook-vastaanotin aina TLS:n takana, jotta sisältö ei vuoda matkalla. Lue lisää siitä, miten salattu yhteys toimii, artikkelistamme HTTPS:stä ja TLS:stä.

Bonus: HMAC-allekirjoitetut kertakäyttöiset latauslinkit

Yksi tyylikäs HMAC:n käyttökohde on aikarajoitettujen, allekirjoitettujen latauslinkkien luonti. Sen sijaan, että ylläpitäisit tietokannassa kertakäyttöisiä tokeneita, voit allekirjoittaa resurssin tunnisteen ja vanhenemisajan HMAC:lla. Palvelin varmentaa allekirjoituksen pyynnön saapuessa eikä tarvitse erillistä tilaa. Tätä mallia käyttävät esimerkiksi monet pilvitallennuspalvelut esiallekirjoitetuissa URL-osoitteissaan.

// signed-url.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function createSignedUrl(fileId, expiresAt, key) {
  const payload = `${fileId}.${expiresAt}`;
  const sig = createHmac('sha256', key).update(payload).digest('hex');
  return `/download/${fileId}?expires=${expiresAt}&sig=${sig}`;
}

export function verifySignedUrl(fileId, expiresAt, sig, key, nowSeconds) {
  // Tarkista vanheneminen ennen kryptografista työtä
  if (nowSeconds > Number(expiresAt)) return { ok: false, reason: 'expired' };

  const payload = `${fileId}.${expiresAt}`;
  const expected = createHmac('sha256', key).update(payload).digest();
  const received = Buffer.from(sig, 'hex');
  if (received.length !== expected.length) return { ok: false, reason: 'invalid' };
  if (!timingSafeEqual(expected, received)) return { ok: false, reason: 'invalid' };
  return { ok: true };
}

Linkki vanhenee automaattisesti, koska vanhenemisaika on osa allekirjoitettua sisältöä. Hyökkääjä ei voi pidentää voimassaoloa muuttamatta allekirjoitusta, eikä hän voi tuottaa kelvollista allekirjoitusta ilman salaista avainta. Tämä tekee mallista sekä tilattoman että turvallisen. Huomaa, että tarkistamme vanhenemisen ennen HMAC-laskentaa, mikä säästää turhaa työtä selvästi vanhentuneissa pyynnöissä.

// use-signed-url.js
import { createSignedUrl, verifySignedUrl } from './signed-url.js';

const key = Buffer.from(process.env.HMAC_SECRET, 'hex');
const now = Math.floor(Date.now() / 1000);
const url = createSignedUrl('raportti-2026', now + 3600, key); // voimassa 1 h

console.log('Linkki:', url);
console.log('Varmennus:', verifySignedUrl('raportti-2026', now + 3600,
  url.split('sig=')[1], key, now)); // { ok: true }

Sama periaate sopii salasanan palautuslinkkeihin, sähköpostin vahvistuslinkkeihin ja kutsulinkkeihin. Lisää aina kontekstierotin (esimerkiksi download: tai reset:) allekirjoitettavaan sisältöön, jottei yhden tarkoituksen linkki kelpaa toisessa. Yhdistä tämä lyhyeen vanhenemisaikaan, niin saat vahvan ja kevyen suojan ilman tietokantatilaa.

5 yleistä virhettä HMAC-toteutuksissa

Nämä virheet toistuvat tuotantokoodissa ja heikentävät tai mitätöivät HMAC-suojauksen. Käy lista läpi ennen kuin viet koodisi tuotantoon.

  • Tavallinen vertailu aikahyökkäykselle alttiina. Käyttämällä === tai == tagien vertailuun avaat oven aikahyökkäykselle. Käytä aina crypto.timingSafeEqual-funktiota.
  • Uudelleenserialisoitu runko. Jos lasket HMAC:n jäsennetystä ja uudelleen sarjallistetusta JSON:sta etkä raakatavuista, allekirjoitus ei täsmää tai (pahempaa) ohitat eron sallimalla löysän vertailun. Käytä aina raakarunkoa.
  • Liian lyhyt tai heikko avain. Lyhyt tai Math.random()-pohjainen avain on arvattavissa. Käytä vähintään 32 satunnaista tavua kohteesta crypto.randomBytes.
  • Kovakoodattu salaisuus. Lähdekoodiin tai versionhallintaan päätynyt salaisuus on vuotanut salaisuus. Lataa se ympäristömuuttujasta tai secret managerista.
  • Toistohyökkäyssuojauksen puute. Ilman aikaleimaa ja tuoreustarkistusta kerran kaapattu kelvollinen pyyntö voidaan lähettää uudelleen. Sisällytä aikaleima allekirjoitettuun sisältöön ja hylkää vanhat pyynnöt.

Vianmääritys: yli 8 yleistä ongelmaa

Kun allekirjoitukset eivät täsmää tai koodi heittää virheen, käy tämä lista läpi. Suurin osa HMAC-ongelmista johtuu koodauksesta, raakarungosta tai pituuseroista.

OireTodennäköinen syyRatkaisu
Allekirjoitus ei koskaan täsmää webhookissaHMAC lasketaan jäsennetystä rungostaKäytä raakatavuja (req.rawBody)
timingSafeEqual heittää poikkeuksenPuskurit ovat eripituisetTarkista pituus ennen kutsua, palauta false jos eroaa
Tagi vaihtelee samalla syötteelläAvain tai koodaus muuttuu ajojen välilläLataa kiinteä avain Bufferina, varmista UTF-8
Toimii lokaalisti, ei tuotannossaYmpäristömuuttuja puuttuu tai eroaaValidoi HMAC_SECRET käynnistyksessä
Heksa- ja base64-tagi eivät täsmääEri koodaus lähettäjällä ja vastaanottajallaSovi yhteinen koodaus (yleensä heksa)
Stripe-pyyntö hylätään ainaAikaleimaa ei sisällytetä allekirjoitettuun sisältöönAllekirjoita muoto aikaleima.runko
Vanha pyyntö hyväksytään uudelleenToistohyökkäyssuojaus puuttuuLisää aikaleiman toleranssitarkistus
ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTHtimingSafeEqual sai eripituiset puskuritMuunna molemmat samaan koodaukseen ja tarkista pituus
Avaimenvaihto katkaisee integraationVain yksi avain voimassa kerrallaTue useaa avainta varmennuksessa

Jos mikään näistä ei ratkaise ongelmaa, tulosta sekä laskemasi että saapunut tagi väliaikaisesti lokiin (vain kehityksessä, ei tuotannossa) ja vertaa niitä silmämääräisesti. Useimmiten näet heti, että toinen on heksa ja toinen base64, tai että runko sisältää ylimääräisen rivinvaihdon. Poista debug-lokitus ennen tuotantoon vientiä, koska tagien lokitus voi vuotaa tietoa.

Suorituskyky ja algoritmin valinta

HMAC on kevyt operaatio. Moderni palvelin laskee satoja tuhansia HMAC-SHA256-tageja sekunnissa, joten suorituskyky on harvoin pullonkaula webhook-liikenteessä. Algoritmin valinta vaikuttaa silti hieman nopeuteen ja tagin pituuteen. SHA-256 on paras yleisvalinta: se on nopea, turvallinen ja yhteensopiva lähes kaikkien kumppanien kanssa.

AlgoritmiTagin pituusSuhteellinen nopeusSuositus
HMAC-SHA25632 tavua (64 heksamerkkiä)NopeaOletusvalinta lähes kaikkeen
HMAC-SHA51264 tavua (128 heksamerkkiä)Nopea 64-bittisillä alustoillaKun halutaan pidempi tagi
HMAC-SHA38448 tavuaKuten SHA-512Sertifikaattiyhteensopivuus
HMAC-SHA120 tavuaNopeaVältä uudessa koodissa
HMAC-MD516 tavuaNopeinÄlä käytä, vanhentunut

Vaikka HMAC-SHA1 on edelleen rakenteellisesti turvallisempi kuin pelkkä SHA-1 (HMAC ei kaadu samoin kuin tiivisteen törmäys), uudessa koodissa kannattaa silti valita SHA-256 tai vahvempi. SHA-1:n heikkoudet on dokumentoitu hyvin, ja jos haluat ymmärtää miksi, lue artikkelimme SHA-256:sta. MD5 ja SHA-1 tulevat vastaan vain vanhojen järjestelmien yhteensopivuudessa, eikä niitä pidä ottaa käyttöön uusissa integraatioissa.

Mikäli sovelluksesi laskee HMAC:n erittäin suurelle määrälle pieniä viestejä, huomaa että Hmac-olion uudelleenkäyttö on mahdotonta (jokainen tarvitsee uuden), mutta avaimen pitäminen Bufferina tai KeyObjectina vähentää toistuvaa koodausta. Käytännössä tämä optimointi merkitsee vain äärimmäisen suuren volyymin tapauksissa, ja lähes kaikessa tavallisessa käytössä oletustoteutus on enemmän kuin riittävän nopea.

Edistyneet vinkit kokeneille kehittäjille

Kun perusteet ovat hallussa, näillä tekniikoilla viet HMAC-toteutuksesi seuraavalle tasolle. Ne ovat valinnaisia mutta hyödyllisiä vaativissa ympäristöissä.

  • Käytä KeyObjectia raaka-avaimen sijaan. crypto.createSecretKey(buffer) luo KeyObjectin, joka kapseloi avaimen ja vähentää sen vahingossa tapahtuvaa kopiointia tai lokitusta. Anna se suoraan createHmac-funktiolle.
  • Erota viestialueet etuliitteellä. Jos käytät samaa avainta useaan tarkoitukseen, lisää allekirjoitettavaan sisältöön kiinteä konteksti-etuliite (domain separation), esimerkiksi webhook-v1:. Näin yhden kontekstin tagi ei kelpaa toisessa.
  • Streamaa suuret hyötykuormat. Hmac-olio tukee useaa update()-kutsua, joten voit syöttää tiedoston tai virran palasina ilman, että koko sisältö on muistissa kerralla.
  • Lisää nonce toistohyökkäysten estoon. Aikaleiman lisäksi voit vaatia kertakäyttöisen noncen, jonka tallennat lyhyeksi ajaksi (esimerkiksi Redisiin) ja hylkäät jo nähdyt arvot. Tämä torjuu toistot myös toleranssi-ikkunan sisällä.
  • Mittaa ja hälytä epäonnistumisista. Lokita epäonnistuneet varmennukset metriikkana. Äkillinen 401-piikki voi viestiä joko väärin määritetystä kumppanista tai aktiivisesta hyökkäysyrityksestä.
  • Harkitse HMAC:n ja salauksen yhdistämistä. Jos tarvitset sekä luottamuksellisuutta että eheyttä, käytä autentikoivaa salausta kuten AES-GCM:ää, joka sisältää HMAC:n kaltaisen todennuksen sisäänrakennettuna.

Nämä tekniikat eivät ole pakollisia perustoteutuksessa, mutta ne ovat arvokkaita, kun järjestelmäsi kasvaa tai kun se käsittelee korkean riskin tapahtumia, kuten maksuja. Domain separation ja nonce-pohjainen toistosuojaus ovat erityisen suositeltavia rahoitusalan integraatioissa.

Usein kysytyt kysymykset

Onko HMAC sama asia kuin salaus?

Ei. HMAC todentaa viestin alkuperän ja eheyden mutta ei salaa sisältöä. Kuka tahansa, joka näkee viestin, voi lukea sen, mutta vain salaisen avaimen haltija voi tuottaa kelvollisen tagin. Jos tarvitset sekä luottamuksellisuutta että todennusta, yhdistä HMAC salaukseen tai käytä autentikoivaa salausta kuten AES-GCM:ää.

Miksi en voi verrata tageja tavallisella ===-operaattorilla?

Tavallinen vertailu palaa heti ensimmäisen eroavan tavun kohdalla, mikä luo mitattavan aikaeron. Hyökkääjä voi käyttää tätä eroa arvatakseen tagin tavu kerrallaan. crypto.timingSafeEqual vertaa vakioajassa riippumatta siitä, missä kohtaa puskurit eroavat, ja torjuu näin aikahyökkäyksen.

Mikä on suositeltu HMAC-avaimen pituus?

NIST SP 800-107 suosittaa, että avain on vähintään yhtä pitkä kuin tiivisteen ulostulo. SHA-256:lla tämä on vähintään 32 tavua (256 bittiä) ja SHA-512:lla vähintään 64 tavua. Generoi avain aina kryptografisesti turvallisella satunnaislähteellä, kuten crypto.randomBytes(32).

Tarvitsenko ulkoisen kirjaston HMAC:iin Node.js:ssä?

Et. Node.js:n vakiomoduuli node:crypto sisältää täyden HMAC-tuen sekä synkronisen createHmac-rajapinnan että selainyhteensopivan WebCrypto-rajapinnan crypto.subtle kautta. Vanhentunutta crypto-js-kirjastoa ei suositella, koska natiivi moduuli on nopeampi, turvallisempi ja ylläpidetty.

Miksi webhookin allekirjoitus ei täsmää, vaikka avain on oikein?

Yleisin syy on, että HMAC lasketaan jäsennetystä ja uudelleen sarjallistetusta rungosta eikä alkuperäisistä raakatavuista. Jo yksi muuttunut välilyönti tai avainjärjestys rikkoo allekirjoituksen. Tallenna pyynnön raakarunko (esimerkiksi Expressin verify-takaisinkutsulla) ja laske HMAC suoraan siitä.

Miten suojaudun toistohyökkäyksiltä?

Sisällytä aikaleima allekirjoitettavaan sisältöön ja hylkää pyynnöt, joiden aikaleima on kauempana kuin toleranssi (esimerkiksi 5 minuuttia) nykyhetkestä. Lisäturvaa saat kertakäyttöisellä noncella, jonka tallennat lyhyeksi aikaa ja jonka jo nähneet arvot hylkäät. Näin sama kelvollinen pyyntö ei kelpaa uudelleen.

Voinko käyttää samaa HMAC-koodia Node.js:ssä ja selaimessa?

Kyllä. Käytä WebCrypto-rajapintaa crypto.subtle, joka on saatavilla sekä modernissa Node.js:ssä että selaimissa ja Edge-ajoympäristöissä. Se tuottaa täsmälleen saman tagin kuin node:crypto:n createHmac samalla avaimella ja viestillä, joten toteutukset ovat keskenään yhteentoimivia.

Mikä Node.js-versio kannattaa valita HMAC-tuotantokäyttöön 2026?

Valitse Node.js 24.x LTS (“Krypton”) tai uudempi. Versio 24 siirtyi pitkäaikaistuettuun LTS-vaiheeseen lokakuussa 2025 ja saa tietoturvapäivityksiä huhtikuuhun 2028 asti. Crypto-rajapinta on yhteneväinen tuoreemmissa julkaisuissa, joten tämän oppaan koodi toimii myös Node 26 -sarjassa.

Aiheeseen liittyvää luettavaa

Lähteet ja viralliset dokumentaatiot: Node.js crypto-dokumentaatio, RFC 2104 (HMAC), NIST SP 800-107, Node.js 24.11.0 -julkaisutiedot.