En digital signatur bevisar att ett meddelande kom från en specifik avsändare och inte ändrades under vägen. År 2026 signeras allt från Kubernetes-manifest och firmware-uppdateringar till API-svar och npm-paket med elliptiska kurvor. Node.js 24 med OpenSSL 3.5 ger inbyggt stöd för ECDSA (P-256, P-384) och Ed25519 direkt via node:crypto, utan externa beroenden.
Den här guiden tar dig igenom 12 konkreta steg för att implementera digitala signaturer i Node.js. Du lär dig generera nyckelpar, signera godtycklig data, verifiera signaturer och hantera nycklar i PEM- och JWK-format. I slutet har du en komplett, produktionsklar API-signeringstjänst med Express.js. Förväntat tidsåtgång: 35 minuter.
Vad är digitala signaturer och varför de är kritiska 2026
En digital signatur är ett kryptografiskt bevis på autenticitet och integritet. Den fungerar med ett nyckelpar: du signerar med din privata nyckel, motparten verifierar med din publika nyckel. Ingen kan förfalska din signatur utan åtkomst till den privata nyckeln, och ingen kan ändra det signerade meddelandet utan att signaturen bryts.
I 2026 används digitala signaturer i praktiskt taget varje lager av modern infrastruktur. TLS-certifikat signeras med ECDSA P-256 eller P-384. Docker-avbildningar signeras med Notary och Sigstore. GitHub-commits kan signeras med Ed25519 SSH-nycklar. JWT-tokens signeras med RS256 eller ES256. Firmware-uppdateringar i inbyggda system verifieras med ECDSA innan de exekveras. Enligt NIST gäller rekommendationen att använda P-256 eller starkare för all ny kod som kräver minst 128 bitars säkerhetsnivå (se NIST FIPS 186-5).
Det matematiska fundamentet är diskret logaritmproblemet på elliptiska kurvor. Givet en publik nyckel Q = dG (produkten av skalären d och basgeneratorn G på kurvan) är det beräkningsmässigt omöjligt att räkna ut d utan att lösa det diskreta logaritmproblemet, som anses hårt för de kurvor NIST standardiserat. ECDSA förlitar sig dessutom på en slumpmässig nonce k per signatur. Återanvändning av k är katastrofalt: en angripare kan återskapa din privata nyckel direkt från bara två signaturer med samma k-värde. Det är precis vad som hände med Sony PlayStation 3 2010. Node.js hanterar k-generering säkert internt med en kryptografiskt säker slumptalsgenerator, vilket vi går igenom i steg 4.
ECDSA, EdDSA och RSA: algoritmer jämförda
Tre algoritmer dominerar digital signering i 2026: ECDSA (Elliptic Curve Digital Signature Algorithm), EdDSA med Ed25519, och RSA. Alla tre stöds av Node.js 24:s node:crypto-modul. De skiljer sig i nyckelstorlek, signaturstorlek, prestanda och säkerhetsegenskaper.
| Algoritm | Nyckelstorlek (bit) | Signaturstorlek (byte) | Säkerhetsnivå (bit) | NIST-status 2026 |
|---|---|---|---|---|
| ECDSA P-256 | 256 | 64 (DER: ~72) | 128 | Rekommenderad |
| ECDSA P-384 | 384 | 96 (DER: ~104) | 192 | Rekommenderad |
| Ed25519 (EdDSA) | 255 | 64 (alltid fast) | 128 | Rekommenderad |
| RSA-2048 | 2048 | 256 | 112 | Till 2030 (avvecklas) |
| RSA-3072 | 3072 | 384 | 128 | Rekommenderad (tung) |
| ECDSA P-192 | 192 | 48 | 96 | Förbjuden i OpenSSL 3.5 |
Ed25519 är det modernaste valet för de flesta ändamål. Signeringen är deterministisk (ingen slumpmässig nonce behövs), signaturstorleken är alltid precis 64 byte, och implementationer är enklare att skriva säkert. ECDSA P-256 är det vanligaste valet i TLS-ekosystemet eftersom stödet finns i praktiskt taget alla plattformar och webbläsare. P-384 ger en högre säkerhetsmarginal för system med längre livslängd, som myndighets- och finanssystem.
RSA-2048 bör undvikas för ny kod. NIST rekommenderar övergång till elliptiska kurvor eller Ed25519. Dessutom är RSA-signaturer 256 byte mot ECDSA:s 64-72 byte, vilket ger märkbar bandbreddsskillnad vid hög volym. Node.js 24 med OpenSSL 3.5 och säkerhetsnivå 2 förbjuder automatiskt ECC-nycklar kortare än 224 bitar, vilket innebär att P-192 inte längre fungerar.
Förutsättningar: versioner och verktyg
Innan du börjar behöver du följande:
- Node.js 24.11.0 LTS (eller senare, stöds till april 2028). Node.js 24 innehåller OpenSSL 3.5 med FIPS 140-3-stöd och inbyggd ML-DSA (post-kvantum). Undvik äldre LTS-grenar; Node.js 18 har OpenSSL 1.1.1 som saknar flera moderna funktioner.
- npm 10+ (ingår i Node.js 24). Den här guiden behöver bara
expressochexpress-validatorför det slutliga projektet. - Grundläggande JavaScript-kunskaper. Guiden förutsätter att du förstår asynkron programmering, Promises och Buffer.
- OpenSSL 3.5 (ingår med Node.js 24, behöver inte installeras separat). Verifiera med
node -e "console.log(process.versions.openssl)". - Textredigerare med JavaScript-stöd (VS Code, Neovim, WebStorm).
Kontrollera din Node.js-version:
node --version
# Förväntat: v24.11.0 eller senare
node -e "console.log(process.versions.openssl)"
# Förväntat: 3.5.x
node -e "const c = require('node:crypto'); console.log(c.getCurves().includes('prime256v1'))"
# Förväntat: true
Steg 1-3: Projektstruktur och installation
Skapa ett nytt Node.js-projekt med en tydlig katalogstruktur. Hela guiden bygger på denna grund.
Steg 1: Skapa projektkatalogen
mkdir node-ecdsa-signatures
cd node-ecdsa-signatures
npm init -y
# Skapa katalogstruktur
mkdir -p src/keys src/examples src/api
Steg 2: Uppdatera package.json för att använda ES-moduler:
{
"name": "node-ecdsa-signatures",
"version": "1.0.0",
"type": "module",
"engines": { "node": ">=24.0.0" },
"scripts": {
"start": "node src/api/server.js",
"keygen": "node src/examples/keygen.js",
"sign": "node src/examples/sign.js",
"verify": "node src/examples/verify.js"
}
}
Steg 3: Installera beroenden för API-projektet:
npm install express express-validator
Notera att node:crypto är inbyggt i Node.js. Du behöver inga externa kryptografibibliotek för ECDSA eller Ed25519. Undvik äldre bibliotek som elliptic eller tredjepartspaket för produktionskod. Den inbyggda modulen är testad, underhållen av Node.js-teamet och håller sig synkroniserad med OpenSSL-säkerhetsuppdateringar.
Steg 4-6: Generera ECDSA-nyckelpar
Ett ECDSA-nyckelpar består av en privat nyckel (ett stort slumpmässigt heltal d) och en publik nyckel (punkten Q = dG på kurvan). Den privata nyckeln måste hållas hemlig. Den publika nyckeln kan delas fritt.
Steg 4: Generera ett P-256-nyckelpar.
Skapa filen src/examples/keygen.js:
import { generateKeyPairSync, generateKeyPair } from 'node:crypto';
import { writeFileSync } from 'node:fs';
// Synkron nyckelgenerering för skript vid serverstart
function generateP256Keys() {
const { privateKey, publicKey } = generateKeyPairSync('ec', {
namedCurve: 'P-256',
publicKeyEncoding: {
type: 'spki', // SubjectPublicKeyInfo: standard PEM-format
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8', // PrivateKeyInfo: standard PEM-format
format: 'pem'
// Lägg till cipher + passphrase för krypterad privat nyckel:
// cipher: 'aes-256-cbc',
// passphrase: process.env.KEY_PASSPHRASE
}
});
return { privateKey, publicKey };
}
// Asynkron nyckelgenerering blockerar inte event loop (rekommenderat för servers)
async function generateP384KeysAsync() {
return new Promise((resolve, reject) => {
generateKeyPair('ec', {
namedCurve: 'P-384',
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
}, (err, publicKey, privateKey) => {
if (err) reject(err);
else resolve({ privateKey, publicKey });
});
});
}
// Generera och spara nycklar till disk
const p256 = generateP256Keys();
writeFileSync('src/keys/p256-private.pem', p256.privateKey, { mode: 0o600 });
writeFileSync('src/keys/p256-public.pem', p256.publicKey);
const p384 = await generateP384KeysAsync();
writeFileSync('src/keys/p384-private.pem', p384.privateKey, { mode: 0o600 });
writeFileSync('src/keys/p384-public.pem', p384.publicKey);
console.log('P-256 publik nyckel (PEM):\n', p256.publicKey);
console.log('Nycklar sparade till src/keys/');
Körresultat från npm run keygen:
P-256 publik nyckel (PEM):
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9G4LPwCqb5f3Z2PxQ+8mJv1hY7k
Ao3TwD2WXKl5sRg8EzT9bWvHPh3NLPQ7RdB+ZUdX8lKtF1mWwNqAR9J3MA==
-----END PUBLIC KEY-----
Nycklar sparade till src/keys/
Steg 5: Förstå PEM-formatet. PEM-filer är Base64-kodade DER-strukturer med BEGIN/END-huvuden. spki (SubjectPublicKeyInfo) är standardformatet för publika nycklar i X.509-certifikat och TLS. pkcs8 är standardformatet för privata nycklar. Dessa format är interoperabla med OpenSSL, Java, Go och de flesta andra plattformar.
Steg 6: Skydda den privata nyckeln. Filrättigheter 0o600 begränsar läsning till ägaren. För produktionssystem bör du dessutom kryptera nyckeln med en lösenfras via cipher: 'aes-256-cbc' och passphrase-parametern. Lagra sedan lösenfransen i en hemlighetshanterare som HashiCorp Vault, AWS Secrets Manager eller Azure Key Vault, inte i miljövariabler som klartext.
Steg 7-8: Signera data med ECDSA
Signeringsprocessen hashar meddelandet med SHA-256 (för P-256) eller SHA-384 (för P-384), beräknar sedan signaturen över hashvärdet med den privata nyckeln. Resultatet är ett signaturvärde som kan exporteras i DER-format (binärt, variabel längd) eller IEEE P1363-format (r och s konkatenerade, fast längd).
Steg 7: Implementera signeringslogiken.
Skapa filen src/examples/sign.js:
import { createSign, createPrivateKey } from 'node:crypto';
import { readFileSync } from 'node:fs';
// Läs in privat nyckel från PEM-fil och skapa KeyObject (cachelagra för återanvändning)
const privateKeyPem = readFileSync('src/keys/p256-private.pem', 'utf8');
const privateKey = createPrivateKey(privateKeyPem);
export function signMessage(message, key = privateKey) {
// createSign tar hash-algoritmen som argument
// 'SHA256' är korrekt för P-256; använd 'SHA384' för P-384
const sign = createSign('SHA256');
// update() accepterar string (UTF-8), Buffer eller Uint8Array
sign.update(message);
sign.end();
// sign() returnerar signaturen
// dsaEncoding: 'ieee-p1363' ger r||s (64 byte för P-256, fast storlek)
// dsaEncoding: 'der' (standard) ger DER-kodad signatur (~70-72 byte, variabel)
const signature = sign.sign({
key,
dsaEncoding: 'ieee-p1363' // IEEE P1363: enhetlig storlek, enklare för JWT och API
}, 'base64url');
return signature;
}
// Signera ett JSON-objekt som API-nyttolast
const payload = JSON.stringify({
sub: 'user-abc123',
action: 'transfer',
amount: 5000,
currency: 'SEK',
timestamp: Date.now()
});
const signature = signMessage(payload);
console.log('Meddelande:', payload);
console.log('Signatur (Base64url):', signature);
console.log('Signaturstorlek (byte):', Buffer.from(signature, 'base64url').length);
// Förväntad output: 64 byte för P-256 med ieee-p1363
Steg 8: Signera filer och binärdata. För binärdata, bilder, dokument och firmware skickar du en Buffer direkt eller använder stream-API:t:
import { createSign, createPrivateKey } from 'node:crypto';
import { readFileSync, createReadStream } from 'node:fs';
const privateKey = createPrivateKey(readFileSync('src/keys/p256-private.pem'));
// Signera en stor fil som stream (minnessäkert, blockerar inte event loop)
export async function signLargeFile(filePath) {
return new Promise((resolve, reject) => {
const sign = createSign('SHA256');
const stream = createReadStream(filePath);
stream.pipe(sign);
stream.on('end', () => {
sign.end();
resolve(sign.sign({ key: privateKey, dsaEncoding: 'ieee-p1363' }, 'base64url'));
});
stream.on('error', reject);
});
}
const sig = await signLargeFile('src/keys/p256-public.pem');
console.log('Filsignatur (Base64url):', sig);
Steg 9-10: Verifiera ECDSA-signaturer
Verifieringen tar meddelandet, signaturen och den publika nyckeln som indata. Den räknar om hashvärdet och kontrollerar matematiskt att signaturen stämmer. Resultatet är true eller false.
Steg 9: Implementera verifieringslogik.
Skapa filen src/examples/verify.js:
import { createVerify, createPublicKey } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { signMessage } from './sign.js';
const publicKeyPem = readFileSync('src/keys/p256-public.pem', 'utf8');
const publicKey = createPublicKey(publicKeyPem);
export function verifySignature(message, signature, key = publicKey) {
try {
const verify = createVerify('SHA256');
verify.update(message);
verify.end();
const isValid = verify.verify(
{ key, dsaEncoding: 'ieee-p1363' },
signature,
'base64url'
);
return isValid;
} catch (err) {
// verify() kastar ett fel om signaturen är felaktigt formaterad (inte bara ogiltig)
// Logga felet men returnera false för att inte avslöja intern information
console.error('Verifieringsfel (ogiltigt format):', err.code);
return false;
}
}
// Test 1: giltig signatur
const testPayload = 'Kritiskt meddelande att verifiera';
const sig = signMessage(testPayload);
console.log('Giltig?', verifySignature(testPayload, sig)); // true
// Test 2: manipulerat meddelande
console.log('Manipulerat?', verifySignature(testPayload + ' !', sig)); // false
// Test 3: slumpmässig signatur
const randomSig = Buffer.alloc(64).toString('base64url'); // 64 noll-byte
console.log('Slumpsignatur?', verifySignature(testPayload, randomSig)); // false
// Test 4: trunkerad signatur (63 byte, fel längd för ieee-p1363 P-256)
const truncated = Buffer.alloc(63).toString('base64url');
console.log('Trunkerad?', verifySignature(testPayload, truncated)); // false (returnerar false via catch)
Steg 10: Tidningsattacker och säker jämförelse. createVerify().verify() i Node.js utför sin jämförelse internt i OpenSSL, som är designad för att motstå tidsattacker. Implementera aldrig en egen byte-för-byte-jämförelse av signaturer med === eller Buffer.compare(). Använd crypto.timingSafeEqual() om du behöver jämföra råa hashvärden eller hemligheter av konstant längd direkt i din kod.
Steg 11: Ed25519-signaturer, det deterministiska alternativet
Ed25519 (EdDSA med Curve25519) skiljer sig från ECDSA på ett fundamentalt sätt: signeringen är deterministisk. Samma privata nyckel och samma meddelande ger alltid exakt samma signatur. Det eliminerar hela klassen av nonce-återanvändningsangrepp som drabbade ECDSA-implementationer.
Node.js 24 har dessutom utökat Ed25519-stödet med ett valfritt context-argument för domänseparation och stöd för råa nyckelformat (raw-private, raw-public), tillagt i Node.js 26-grenen.
import {
generateKeyPairSync,
sign,
verify,
createPrivateKey,
createPublicKey
} from 'node:crypto';
import { writeFileSync, readFileSync } from 'node:fs';
// Generera Ed25519-nyckelpar (mycket snabbt, deterministisk signering)
const { privateKey: privPem, publicKey: pubPem } = generateKeyPairSync('ed25519', {
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
writeFileSync('src/keys/ed25519-private.pem', privPem, { mode: 0o600 });
writeFileSync('src/keys/ed25519-public.pem', pubPem);
const privKey = createPrivateKey(readFileSync('src/keys/ed25519-private.pem'));
const pubKey = createPublicKey(readFileSync('src/keys/ed25519-public.pem'));
const message = Buffer.from('Hemlighetsfullt meddelande från Stockholm');
// Ed25519 använder one-shot sign()/verify() i stället för createSign/createVerify
// VIKTIGT: algoritm-argumentet MÅSTE vara null för Ed25519
const signature = sign(null, message, privKey);
console.log('Ed25519-signatur (hex):', signature.toString('hex'));
console.log('Signaturstorlek (byte):', signature.length); // Alltid exakt 64 byte
const isValid = verify(null, message, pubKey, signature);
console.log('Verifiering giltig:', isValid); // true
// Deterministisk egenskap: samma input ger alltid exakt samma signatur
const sig2 = sign(null, message, privKey);
console.log('Deterministisk:', signature.equals(sig2)); // true (alltid)
// Test: manipulerat meddelande
const tampered = Buffer.from('Hemlighetsfullt meddelande från Göteborg');
console.log('Manipulerat:', verify(null, tampered, pubKey, signature)); // false
Ed25519 är det rekommenderade valet för nya projekt där du kontrollerar båda parter (till exempel intern mikrotjänstkommunikation, SSH-nycklar, API-token). ECDSA P-256 är det rätta valet när du behöver interoperabilitet med befintlig TLS-infrastruktur, äldre system eller plattformar som ännu inte stöder Ed25519.
Steg 12: Nyckelhantering i PEM- och JWK-format
PEM-format fungerar utmärkt för filbaserat nyckellagring och OpenSSL-interoperabilitet. JWK (JSON Web Key) är standardformatet för nyckelutbyte via API:er och i JWT-ekosystemet. Node.js 24 kan exportera och importera båda formaten.
import { generateKeyPairSync, createPublicKey, createPrivateKey, createHash } from 'node:crypto';
// Generera nyckelpar direkt i JWK-format
const { privateKey: privJwk, publicKey: pubJwk } = generateKeyPairSync('ec', {
namedCurve: 'P-256',
publicKeyEncoding: { type: 'jwk' },
privateKeyEncoding: { type: 'jwk' }
});
console.log('Publik JWK:\n', JSON.stringify(pubJwk, null, 2));
// {
// "kty": "EC",
// "crv": "P-256",
// "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9OzXzlmWc",
// "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
// }
// Konvertera PEM till JWK
export function pemToJwk(pemStr, type = 'public') {
const keyFn = type === 'public' ? createPublicKey : createPrivateKey;
return keyFn(pemStr).export({ format: 'jwk' });
}
// Konvertera JWK till PEM
export function jwkToPem(jwk, type = 'public') {
const keyFn = type === 'public' ? createPublicKey : createPrivateKey;
return keyFn({ key: jwk, format: 'jwk' }).export({
type: type === 'public' ? 'spki' : 'pkcs8',
format: 'pem'
});
}
// RFC 7638: nyckelfingeravtryck (Key ID) för JWKS-ändamål
export function keyThumbprint(jwk) {
const canonical = JSON.stringify({ // fälten MÅSTE vara i alfabetisk ordning
crv: jwk.crv,
kty: jwk.kty,
x: jwk.x,
y: jwk.y
});
return createHash('sha256').update(canonical).digest('base64url');
}
console.log('Nyckel-ID (kid):', keyThumbprint(pubJwk));
ECDSA vs RSA vs Ed25519: prestandajämförelse
Prestandaskillnaderna mellan algoritmer är stora och relevanta för system med hög genomströmning. Tabellen nedan visar relativa prestanda på modern server-hårdvara. Exakta tal varierar med hårdvara, Node.js-version och nyckelstorlek, men förhållandena är konsekventa och välkända från OpenSSL-benchmarks.
| Algoritm | Signering (relativ hastighet) | Verifiering (relativ hastighet) | Signaturstorlek | Nyckelgeneration |
|---|---|---|---|---|
| Ed25519 | Snabbast | Snabb | 64 byte (fast) | Snabb (<1 ms) |
| ECDSA P-256 | Snabb (~80% av Ed25519) | Lite långsammare | 64-72 byte | Snabb (<1 ms) |
| ECDSA P-384 | Medel (~40% av Ed25519) | Medel | 96-104 byte | Medel (~2 ms) |
| RSA-2048 | Långsam (~4% av Ed25519) | Snabb (verifiering) | 256 byte | Långsam (sekunder) |
| RSA-3072 | Mycket långsam | Snabb (verifiering) | 384 byte | Mycket långsam |
RSA-signering är 20-25 gånger långsammare än Ed25519-signering. RSA-verifiering är dock relativt snabb eftersom verifieringen bara kräver en potensupphöjning med den lilla publika exponenten (typiskt 65537). För system som signerar ofta men verifierar sällan är RSA-signering en flaskhals. För TLS-handskakningar, API-signering, JWT-generering och kodnyckelrotation är Ed25519 eller ECDSA P-256 alltid det rätta valet.
Node.js 24 med OpenSSL 3.5 drar full nytta av hårdvaruaccelerering. På processorer med AVX2-stöd (Intel Haswell och senare, AMD Zen och senare) körs Ed25519-operationer delvis i parallell via SIMD-instruktioner. Det är ytterligare ett skäl att välja moderna elliptiska kurvor framför RSA.
Vanliga fallgropar och säkerhetsrisker
Digital signering i Node.js är enkel att implementera fel. Dessa sex misstag orsakar de allvarligaste sårbarheterna.
Fallgrop 1: Återanvändning av ECDSA-nonce (k-värde)
Det farligaste möjliga felet med ECDSA är att använda samma nonce k för två signaturer med samma privata nyckel. Det avslöjar omedelbart den privata nyckeln via enkel algebra. Det är precis vad som hände med PlayStation 3 2010 och med den tidiga Bitcoin-klienten på Android. Node.js hanterar k-generering automatiskt via OpenSSL:s CSPRNG, men om du av misstag hårdkodar k i ett test eller porterar en C-implementation felaktigt är konsekvensen total kompromiss av den privata nyckeln. Använd alltid Node.js-inbyggda signeringsrutiner och implementera aldrig ECDSA manuellt. Ed25519 eliminerar detta problem helt eftersom signeringen är deterministisk.
Fallgrop 2: Använda SHA-1 eller MD5 med ECDSA
Node.js accepterar tekniskt createSign('SHA1') med ECDSA-nycklar i äldre versioner, men OpenSSL 3.5 på säkerhetsnivå 2 (standardläge i Node.js 24) blockerar SHA-1 och MD5. Mer grundläggande: SHA-1 är kryptoanalytiskt bruten sedan SHAttered-kollisionen 2017. Använd alltid SHA-256 med P-256 och SHA-384 med P-384. Se mer om SHA-kollisioner i SHAttered: den första praktiska SHA-1-kollisionen.
Fallgrop 3: Jämföra signaturer med === eller Buffer.compare()
Om du manuellt jämför signaturer med sigA === sigB är koden sårbar för tidsattacker. Angriparen kan mäta hur lång tid jämförelsen tar och byte-för-byte sluta sig till den förväntade signaturen. Använd alltid crypto.timingSafeEqual() för byte-för-byte-känsliga jämförelser, eller låt createVerify().verify() göra hela jobbet.
Fallgrop 4: Lagra den privata nyckeln i miljövariabler som klartext
Miljövariabler är synliga i procestabellen, loggar, core-dumpar och docker inspect-utdata. En PEM-privat nyckel på 227 tecken i en miljövariabel är ett vanligt säkerhetsproblem som dyker upp i kodgranskningar. Kryptera alltid privata nycklar med en stark lösenfras (cipher: 'aes-256-cbc') och lagra lösenfransen i en dedikerad hemlighetshanterare. Alternativt, använd ett HSM eller molnnyckeltjänst (AWS KMS, Google Cloud KMS, Azure Key Vault) där den privata nyckeln aldrig lämnar hårdvaran.
Fallgrop 5: Blanda DER- och IEEE-P1363-signatuurformat
Node.js stöder två signatuurformat för ECDSA: der (standard, ASN.1-kodning, variabel längd) och ieee-p1363 (r||s, fast längd). En P-256-signatur är 64 byte i ieee-p1363 men 70-72 byte i DER. Om du signerar med ett format och verifierar med ett annat misslyckas verifieringen med ett kryptiskt fel. Välj ett format och dokumentera det i din kod.
Fallgrop 6: Skapa KeyObject vid varje request
Att anropa createPrivateKey(readFileSync(...)) vid varje inkommande request innebär att du läser disk och parsear PEM-data upprepat. I ett system med 1000 requests/sekund medför det onödig I/O och CPU-overhead. Ladda och cachelagra KeyObject en gång vid serverstart och återanvänd det för alla signeringsoperationer.
| Risk | Allvarlighet | Åtgärd |
|---|---|---|
| ECDSA nonce-återanvändning | Kritisk | Använd Node.js inbyggd signering |
| SHA-1 med ECDSA | Hög | Kräv SHA-256 eller SHA-384 |
| Tidsbaserad signaturjämförelse | Medium | Använd timingSafeEqual() eller verify() |
| Privat nyckel i env-variabel | Hög | Kryptera nyckel, använd secrets manager |
| Blandat DER/P1363-format | Medium | Definiera ett format, dokumentera det |
| KeyObject-parsning per request | Låg | Cachelagra KeyObject vid serverstart |
Komplett projekt: API-signeringstjänst med Express.js
Det här projektet implementerar en Express.js-tjänst som signerar API-svar med ECDSA P-256 och verifierar inkommande signerade förfrågningar. Det är en grundläggande arkitektur för säker kommunikation mellan mikrotjänster.
// src/api/server.js
import express from 'express';
import { createSign, createVerify, createPrivateKey, createPublicKey } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { body, validationResult } from 'express-validator';
const app = express();
app.use(express.json({ limit: '10kb' }));
// Ladda nycklar en gång vid start (inte vid varje request)
const PRIVATE_KEY = createPrivateKey(readFileSync('src/keys/p256-private.pem'));
const PUBLIC_KEY = createPublicKey(readFileSync('src/keys/p256-public.pem'));
function signPayload(payload) {
const sign = createSign('SHA256');
sign.update(typeof payload === 'string' ? payload : JSON.stringify(payload));
sign.end();
return sign.sign({ key: PRIVATE_KEY, dsaEncoding: 'ieee-p1363' }, 'base64url');
}
function verifyPayload(payload, signature) {
try {
const verify = createVerify('SHA256');
verify.update(typeof payload === 'string' ? payload : JSON.stringify(payload));
verify.end();
return verify.verify(
{ key: PUBLIC_KEY, dsaEncoding: 'ieee-p1363' },
signature,
'base64url'
);
} catch {
return false;
}
}
// Middleware: verifiera inkommande signerade förfrågningar
function requireSignature(req, res, next) {
const signature = req.headers['x-signature'];
const timestamp = req.headers['x-timestamp'];
if (!signature || !timestamp) {
return res.status(401).json({ error: 'X-Signature och X-Timestamp är obligatoriska' });
}
// Avvisa tidsstämplar utanför 60-sekunders fönster (förhindrar replay-attacker)
if (Math.abs(Date.now() - parseInt(timestamp)) > 60_000) {
return res.status(401).json({ error: 'Tidsstämpel utanför 60-sekunders fönster' });
}
const canonicalPayload = JSON.stringify(req.body) + timestamp;
if (!verifyPayload(canonicalPayload, signature)) {
return res.status(401).json({ error: 'Ogiltig signatur' });
}
next();
}
// GET /api/public-key: publicera publik nyckel för klienter att hämta och cacha
app.get('/api/public-key', (req, res) => {
res.json({
format: 'jwk',
key: PUBLIC_KEY.export({ format: 'jwk' }),
algorithm: 'ES256',
curve: 'P-256'
});
});
// POST /api/sign: signera ett meddelande
app.post('/api/sign',
body('message').isString().trim().notEmpty().isLength({ max: 10000 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
const { message } = req.body;
const timestamp = Date.now().toString();
const canonical = message + timestamp;
const signature = signPayload(canonical);
res.json({ message, signature, timestamp, algorithm: 'ES256' });
}
);
// POST /api/verify: verifiera ett signerat meddelande
app.post('/api/verify',
body('message').isString().notEmpty(),
body('signature').isString().notEmpty(),
body('timestamp').isNumeric(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
const { message, signature, timestamp } = req.body;
if (Math.abs(Date.now() - parseInt(timestamp)) > 300_000) {
return res.status(400).json({ valid: false, reason: 'Signatur äldre än 5 minuter' });
}
const canonical = message + timestamp;
const valid = verifyPayload(canonical, signature);
res.json({ valid, message, timestamp });
}
);
// POST /api/protected: skyddad endpoint som kräver giltig signatur
app.post('/api/protected', requireSignature, (req, res) => {
res.json({ success: true, data: req.body, message: 'Autentiserad förfrågan processad' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API-signeringstjänst körs på port ${PORT}`);
console.log(`Publik nyckel: http://localhost:${PORT}/api/public-key`);
});
Testa tjänsten med curl:
# Starta servern
npm start
# Hämta publik nyckel i JWK-format
curl -s http://localhost:3000/api/public-key | python3 -m json.tool
# Signera ett meddelande
curl -s -X POST http://localhost:3000/api/sign \
-H 'Content-Type: application/json' \
-d '{"message": "Viktig API-begäran 2026-06-17"}' | python3 -m json.tool
# Förväntad respons:
# {
# "message": "Viktig API-begäran 2026-06-17",
# "signature": "mT9xRkL2...base64url...",
# "timestamp": "1781700000000",
# "algorithm": "ES256"
# }
# Verifiera signaturen (ersätt med faktiska värden från föregående svar)
curl -s -X POST http://localhost:3000/api/verify \
-H 'Content-Type: application/json' \
-d '{
"message": "Viktig API-begäran 2026-06-17",
"signature": "mT9xRkL2...base64url...",
"timestamp": "1781700000000"
}' | python3 -m json.tool
# { "valid": true, "message": "...", "timestamp": "..." }
Felsökning: 8 vanliga problem och lösningar
Här är de åtta vanligaste problemen vid implementering av ECDSA i Node.js och hur du löser dem.
Problem 1: ERR_OSSL_EVP_UNSUPPORTED
Felet uppstår när du försöker använda en kryptoalgoritm blockerad av OpenSSL:s aktiva konfiguration. Vanligaste orsak: P-192 eller en annan kurva under 224 bitar som OpenSSL 3.5 blockerar vid säkerhetsnivå 2. Lösning: byt till P-256 eller P-384. Temporär workaround för testmiljöer: NODE_OPTIONS=--openssl-legacy-provider men gör aldrig det i produktion.
Problem 2: Error: error:0200100D:rsa routines::key size too small
OpenSSL 3.5 i Node.js 24 tillåter inte RSA-nycklar kortare än 2048 bitar vid säkerhetsnivå 2. Gammal kod som genererar RSA-1024-nycklar misslyckas. Lösning: uppgradera till RSA-2048 minimum, eller byt till ECDSA P-256 som ger samma säkerhetsnivå med en 8 gånger kortare nyckel.
Problem 3: Verifieringen returnerar alltid false
Vanligaste orsak: du signerade med dsaEncoding: 'ieee-p1363' men verifierar med standard-'der' (eller vice versa). Dubbelkolla att samma dsaEncoding-värde används i både signerings- och verifieringsanropet. En annan vanlig orsak: meddelandet kodas som UTF-8-sträng vid signering men som Buffer vid verifiering, vilket ger olika byte-sekvenser för icke-ASCII-tecken.
Problem 4: Error: error:09091064:PEM routines:PEM_read_bio:no start line
PEM-filen är skadad, trunkerad eller har fel radbrytningar (CRLF i stället för LF på Windows). Kontrollera filen med openssl ec -in p256-private.pem -noout -text. Om filen läses via miljövariabel kan \n-tecken ha konverterats till bokstavliga n. Lösning: läs alltid PEM-filer från disk med readFileSync.
Problem 5: TypeError: The “algorithm” argument is invalid for Ed25519
För Ed25519 måste algoritm-argumentet i sign() och verify() vara null. Ed25519 specificerar sin interna hashfunktion som en del av algoritmdefinitionen. Om du skickar 'SHA256' kastar Node.js ett fel. Kontrollera att du inte blandar ihop ECDSA-kod med Ed25519-kod.
Problem 6: Mycket långsam RSA-nyckelgenerering blockerar event loop
RSA-nyckelgenerering (>= 3072 bitar) kan ta 1-3 sekunder och blockerar event loop om du använder generateKeyPairSync(). Lösning: använd alltid den asynkrona generateKeyPair()-varianten för RSA. För ECDSA och Ed25519 är nyckelgenereringen tillräckligt snabb för synkron version vid serverstart, men aldrig i request handlers.
Problem 7: JWK-import misslyckas med ERR_CRYPTO_INVALID_JWK
JWK-validering i Node.js 24 är strikt. Vanligaste orsaker: saknat crv-fält, felaktig Base64url-kodning av x/y-koordinaterna, eller att du försöker importera en privat JWK (med d-fältet) med createPublicKey(). Lösning: validera alltid JWK-objektet innan import och använd createPrivateKey() för privata JWK:ar.
Problem 8: Signaturen skiljer sig åt vid upprepade anrop med ECDSA P-256
Det är normalt och förväntat! ECDSA genererar ett nytt slumpmässigt k-värde (nonce) för varje signatur. Varje signatur av samma meddelande med samma nyckel ser annorlunda ut men verifieras korrekt. Det är en grundläggande säkerhetsegenskap. Om du behöver deterministiska signaturer (samma input ger alltid samma output), använd Ed25519 i stället.
Avancerade tips för produktion
Dessa rekommendationer gäller system som hanterar signaturer i skarp drift med höga krav på tillgänglighet och säkerhet.
Nyckelrotation utan nedtid med JWKS
Implementera nyckelrotation med ett JWKS-endpoint (JSON Web Key Set) som exponerar flera aktiva publika nycklar. Varje signatur inkluderar ett kid-fält (Key ID, beräknat med RFC 7638-fingeravtrycket från föregående steg) som pekar på vilket nyckelpar som användes. Verifieraren hämtar JWKS och väljer rätt publik nyckel baserat på kid. Ny nyckel läggs till JWKS innan den börjar användas för signering. Gammal nyckel tas bort från JWKS efter att alla signaturer med sin maximala livslängd har löpt ut. Ett typiskt rotationsintervall för API-signeringsnycklar är 90 dagar. Node.js Security Best Practices rekommenderar regelbunden nyckelrotation för alla produktionssystem.
FIPS 140-3-läge i Node.js 24
Node.js 24 med OpenSSL 3.5 stöder FIPS 140-3-läge för myndigheter och finanssystem som kräver certifierad kryptografi. Aktivera med --enable-fips-flaggan eller crypto.setFips(1). I FIPS-läge blockeras alla icke-godkända algoritmer, inklusive MD5, SHA-1 och RC4. ECDSA P-256, P-384 och Ed25519 är alla godkända under FIPS 140-3. Läs mer i OpenSSL 3.5 changelog.
Signaturtidsstämplar och replay-skydd
En giltig signatur är giltig för alltid om du inte lägger till en tidsdimension. Inkludera alltid en timestamp (Unix-millisekunder) och ett nonce (kryptografisk slumpbytes, 16 byte) i det signerade meddelandet. Servern avvisar signaturer äldre än 60-300 sekunder och kontrollerar att nonce inte redan använts (med Redis eller en in-memory-mängd med TTL). Det är precis vad API-signeringstjänsten i det fullständiga projektet ovan implementerar.
Post-kvantum förberedelser med Node.js 24
Node.js 24 introducerade stöd för ML-DSA (CRYSTALS-Dilithium), NIST:s standardiserade post-kvantumsigneringsalgoritm, via crypto.sign() och crypto.verify() med nyckeltypen ml-dsa44, ml-dsa65 eller ml-dsa87. ML-DSA-signaturer är 2420-4627 byte jämfört med 64 byte för Ed25519, men de tål angrepp från kvantnodatorer. För system med 10+ år livslängd är det värt att planera en hybridmigrering med både ECDSA och ML-DSA i parallell, det som kallas hybrid post-quantum. Bakgrunden till NIST:s val finns på NIST FIPS 186-5-sidan. OWASP:s rekommendationer för kryptografisk lagring finns på OWASP Cryptographic Storage Cheat Sheet.
Relaterade artiklar
Relaterad läsning på shattered.io
- OpenSSL 3.5: nycklar och certifikat i 12 steg [2026] — generera X.509-certifikat och hantera CA-kedjor med OpenSSL-kommandoraden.
- HMAC-SHA256 i Node.js: 10 steg, 20 min [2026] — meddelandeautentiseringskoder för API-säkerhet utan asymmetrisk kryptografi.
- AES-256-kryptering i Node.js: 12 steg [2026] — symmetrisk kryptering för skydd av data i vila.
- RSA-kryptering i Node.js: 11 steg [2026] — asymmetrisk kryptering och nyckelutbyte med RSA.
- SHA-256 förklarad: 256-bitars fingeravtryck i SHA-2 — det matematiska fundamentet bakom ECDSA:s hashsteg.
- Kryptografi: hashfunktioner, SHA och digitalt förtroende — övergripande guide till kryptografiska grunder.
Vanliga frågor om ECDSA och digitala signaturer i Node.js
Vilken kurva ska jag använda, P-256 eller P-384?
Välj P-256 för de flesta ändamål, inklusive API-signering, JWT och TLS-certifikat. P-256 ger 128 bitars säkerhetsnivå, är hårdvaruaccelererad på moderna processorer och stöds universellt i webbläsare och plattformar. Välj P-384 om ditt system hanterar data med längre klassificeringskrav (statlig sekretess, finansiella transaktioner med 15+ år lagringskrav) eller om säkerhetspolicyn kräver 192 bitars säkerhetsnivå.
Vad är skillnaden mellan ECDSA och HMAC?
HMAC (Hash-based Message Authentication Code) är ett symmetriskt schema: båda parter delar en hemlig nyckel. Det bevisar integritet och autenticitet men kräver att mottagaren känner till den hemliga nyckeln, vilket innebär att mottagaren tekniskt kan skapa en ny giltig HMAC. ECDSA är asymmetriskt: bara innehavaren av den privata nyckeln kan skapa en signatur, men vem som helst med den publika nyckeln kan verifiera den. ECDSA ger äkta oavvislighet (non-repudiation). Välj HMAC för intern kommunikation mellan betrodda tjänster; välj ECDSA när du behöver bevis som kan presenteras för tredje part.
Ska jag använda node:crypto eller ett externt bibliotek som elliptic?
Använd alltid node:crypto för produktionskod på server-sidan. Det är underhållet av Node.js-teamet, håller sig uppdaterat med OpenSSL-säkerhetsuppdateringar och granskas av kryptografer. Biblioteket elliptic är ett tredjepartsbibliotek med öppna underhållsfrågor som identifierades 2024. Det finns legitima användningsfall för tredjepartsbibliotek i webbläsarmiljön (före WebCrypto-stöd var fullt utbrett), men på server-sidan är node:crypto alltid det rätta valet.
Kan ECDSA-signaturer verifieras av andra programmeringsspråk?
Ja. ECDSA med P-256 och SHA-256 (ES256 i JWA-terminologi) är en öppen standard som implementeras i Go (crypto/ecdsa), Python (cryptography-paketet), Java (java.security), Rust (p256-crate), C/C++ (OpenSSL) och praktiskt taget alla moderna programmeringsspråk. Nyckeln är att använda standardiserade format (PEM, JWK) och dokumentera vilken signaturkodning du valt (DER eller IEEE P1363). JWK-format med base64url-kodning ger bäst portabilitet i API-sammanhang.
Hur lång tid tar ECDSA-signering i Node.js?
På moderna serverprocessorer tar en P-256-signering med Node.js typiskt under 0,1 millisekunder. Ed25519-signering är marginellt snabbare. Tiotusentals signeringsoperationer per sekund är möjliga i en enda tråd. Nyckelgenerering tar längre (millisekunder för EC, sekunder för RSA) men den bör bara ske en gång vid serverstart. Att skapa ett nytt KeyObject från PEM-text vid varje request kostar onödig CPU; cachelagra objektet vid uppstart.
Vad händer med ECDSA när kvantnodatorer blir tillgängliga?
Shors algoritm på en tillräckligt stor kvantnodator bryter ECDSA, RSA och alla algoritmer baserade på diskret logaritmproblemet. NIST publicerade i 2024 slutliga standarder för post-kvantumsäkra algoritmer: ML-DSA (signaturer) och ML-KEM (nyckelkapsling). Node.js 24 stöder båda. För system med data som behöver skyddas i 10+ år bör en migrationsplan finnas. För typiska API-signaturer med korta livslängder är ECDSA P-256 säkert i det förutsebara perspektivet fram till 2030 och troligtvis längre.
Hur verifierar jag att min implementation är korrekt?
Skriv tester som verifierar dessa fem scenarion: (1) en giltig signatur returnerar true, (2) ett manipulerat meddelande returnerar false, (3) en slumpmässig signatur returnerar false, (4) en signatur skapad med en annan privat nyckel returnerar false, och (5) signaturen har förväntad storlek och format. Dessa fem tester täcker de vanligaste implementationsfelen. Kör dessutom dina tester mot kända testvektorer från NIST CAVP-programmet om ditt system kräver formell validering.



