Digitale signaturer er rygraden i moderne tillid på nettet. De beviser, at en besked stammer fra en bestemt afsender, og at ingen har ændret den undervejs. Blandt de mange signatur-algoritmer skiller Ed25519 sig ud: nøglerne fylder 32 bytes, signaturen 64 bytes, og hele skemaet er bygget til at være svært at bruge forkert. I denne tutorial bygger du et komplet, fungerende projekt i Node.js, der genererer nøgler, signerer data og verificerer signaturer, helt uden tredjepartsbiblioteker.
Vi bruger udelukkende det indbyggede crypto-modul, som følger med Node.js. Du skriver kode til nøglegenerering, PEM-lagring, signering, verifikation, et CLI-værktøj og et lille API, der validerer signerede tokens. Undervejs gennemgår vi de fælder, danske udviklere oftest falder i, plus 8 konkrete fejlfindings-scenarier. Alt er testet mod Node.js 24 LTS og virker på Node.js 22 og nyere.
Hvad er Ed25519, og hvorfor digitale signaturer i Node.js?
Ed25519 er en konkret instans af signatur-skemaet EdDSA (Edwards-curve Digital Signature Algorithm), defineret i RFC 8032. Det bygger på den elliptiske kurve Curve25519, oprindeligt designet af kryptografen Daniel J. Bernstein. NIST standardiserede EdDSA i FIPS 186-5 i 2023, hvilket åbnede døren for brug i regulerede miljøer i USA og dermed indirekte også i mange europæiske compliance-rammer.
En digital signatur løser tre problemer på én gang. Den giver autenticitet (beskeden kom fra indehaveren af den private nøgle), integritet (indholdet er uændret) og uafviselighed (afsenderen kan ikke senere nægte at have signeret). Hvis du vil have det konceptuelle fundament på plads først, har vi en grundig forklaring i artiklen Digitale signaturer: hvordan hashing og nøgler skaber tillid.
Hvorfor Ed25519 frem for de ældre alternativer? Tre grunde dominerer. For det første er signaturerne deterministiske: samme nøgle plus samme besked giver altid samme signatur, så du undgår hele klassen af fejl, der stammer fra dårlig tilfældighed under signering. ECDSA blev berygtet, da Sony i 2010 genbrugte den samme tilfældige værdi og dermed lækkede PlayStation 3-signaturnøglen. For det andet er nøgler og signaturer faste i størrelse og små. For det tredje kører verifikation hurtigt, hvilket betyder noget, når en server validerer tusindvis af tokens i sekundet.
Ed25519 leverer cirka 128 bit sikkerhed. Det er på niveau med RSA-3072 og AES-128, og det regnes som solidt mod alle kendte klassiske angreb. En vigtig advarsel: Ed25519 er ikke kvanteresistent. En tilstrækkelig stor, fejltolerant kvantecomputer ville kunne bryde den med Shors algoritme. Til langtidsholdbar arkivsignering bør du følge med i overgangen til post-kvante-signaturer som ML-DSA (Dilithium), men til langt de fleste produktionsbehov i 2026 er Ed25519 det rigtige valg.
Ed25519 mod ECDSA og RSA: tal der betyder noget
Før du skriver kode, hjælper det at se de konkrete forskelle. Tabellen nedenfor sammenligner de tre mest udbredte signatur-familier på de parametre, der påvirker dit design: nøgle- og signaturstørrelse, sikkerhedsniveau og praktisk hastighed. Tallene for nøgle- og signaturstørrelser er faste egenskaber ved algoritmerne, ikke estimater.
| Egenskab | Ed25519 | ECDSA (P-256) | RSA-2048 | RSA-4096 |
|---|---|---|---|---|
| Offentlig nøgle | 32 bytes | ~65 bytes | ~270 bytes | ~540 bytes |
| Privat nøgle (seed) | 32 bytes | 32 bytes | ~1190 bytes | ~2350 bytes |
| Signaturstørrelse | 64 bytes | ~72 bytes | 256 bytes | 512 bytes |
| Sikkerhedsniveau | ~128 bit | ~128 bit | ~112 bit | ~140 bit |
| Deterministisk signatur | Ja (indbygget) | Nej (kræver RFC 6979) | Ja (for RSASSA) | Ja |
| Signeringshastighed | Meget hurtig | Hurtig | Langsom | Meget langsom |
| Verifikationshastighed | Hurtig | Hurtig | Meget hurtig | Hurtig |
| Kvanteresistent | Nej | Nej | Nej | Nej |
Det praktiske budskab: en Ed25519-signatur fylder 64 bytes mod 256 bytes for RSA-2048 og 512 bytes for RSA-4096. Når du sender signaturer i HTTP-headers, QR-koder eller embeddede enheder, er den forskel afgørende. RSA vinder kun ét sted, nemlig ren verifikationshastighed med lille offentlig eksponent, men det opvejes sjældent af de meget større nøgler og signaturer.
Ed25519 er i dag udbredt i steder, du måske bruger dagligt. OpenSSH tilbyder ssh-ed25519-nøgler som standard-anbefaling, GitHub accepterer dem til Git-godkendelse, TLS 1.3 understøtter dem til certifikater, Tor bruger dem til løg-tjenester, krypteringsværktøjet age bygger på X25519, og Signal-protokollen bruger en variant kaldet XEdDSA. Den brede ibrugtagning betyder modent økosystem og god interoperabilitet på tværs af sprog.
Forudsætninger og versioner
Du skal bruge et lille, præcist sæt værktøjer. Ed25519-understøttelsen i Node.js’ crypto-modul har været stabil længe, så kravene er beskedne. Kontrollér din opsætning mod denne liste, før du går videre.
- Node.js 22 LTS eller nyere. Tutorialen er testet på Node.js 24 LTS, som er den anbefalede produktionslinje i 2026 (LTS frem til april 2028). Node.js 22 fungerer også fuldt ud.
- npm 10 eller nyere, der følger med Node.js-installationen.
- En terminal (Terminal på macOS/Linux, PowerShell eller Windows Terminal på Windows).
- En teksteditor som VS Code.
- Grundlæggende JavaScript: funktioner,
Bufferog asynkron kode. Du behøver ingen kryptografi-baggrund. - Ingen eksterne pakker til selve kryptografien. Vi tilføjer kun Express i det sidste API-trin.
Bekræft din Node-version, så du ved, at API’erne i denne guide er til rådighed:
$ node --version
v24.4.0
$ npm --version
10.9.2
Hvis node --version viser v20 eller ældre, så opgrader. De ældre linjer kører koden her, men de er enten uden for sikkerhedssupport eller på vej ud, og du vil have de nyeste rettelser i crypto-modulet. Tabellen viser kompatibilitet for de relevante API-kald.
| Node.js-version | Status i 2026 | Ed25519 nøglegen. | crypto.sign/verify (null) | JWK-eksport |
|---|---|---|---|---|
| Node 18 | Udfaset | Ja | Ja | Ja |
| Node 20 | Vedligehold | Ja | Ja | Ja |
| Node 22 LTS | Aktiv LTS | Ja | Ja | Ja |
| Node 24 LTS | Anbefalet LTS | Ja | Ja | Ja |
| Node 26 Current | Nyeste | Ja | Ja | Ja |
Trin 1: Opsæt projektet
Start med en ren projektmappe og en minimal struktur. Vi adskiller nøglehåndtering, signering og verifikation i hver sin fil, så koden bliver let at genbruge i et CLI og et API senere.
$ mkdir ed25519-signaturer && cd ed25519-signaturer
$ npm init -y
$ mkdir src keys
$ touch src/keys.js src/sign.js src/verify.js src/token.js index.js
Åbn package.json, og tilføj et par scripts, så du kan køre kommandoer uden at huske filstier. Vi bruger CommonJS gennem hele tutorialen, fordi det kører uden ekstra konfiguration på alle understøttede Node-versioner.
{
"name": "ed25519-signaturer",
"version": "1.0.0",
"description": "Digitale signaturer med Ed25519 i Node.js",
"main": "index.js",
"scripts": {
"keygen": "node index.js keygen",
"sign": "node index.js sign",
"verify": "node index.js verify",
"serve": "node server.js"
},
"license": "MIT"
}
Tilføj med det samme en .gitignore, så du aldrig kommer til at committe private nøgler. Dette er ikke valgfrit. En lækket privat nøgle i et Git-repo er en af de hyppigste årsager til kompromitterede signaturnøgler.
# .gitignore
node_modules/
keys/
*.pem
.env
Trin 2: Generér et Ed25519-nøglepar
Nøglegenerering er én funktion. Node.js’ crypto.generateKeyPairSync('ed25519') returnerer et privat og et offentligt KeyObject. Den synkrone variant er fin til et CLI; til en server, der genererer nøgler under belastning, ville du bruge den asynkrone generateKeyPair. Skriv følgende i src/keys.js.
// src/keys.js
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const KEY_DIR = path.join(__dirname, '..', 'keys');
const PRIV_PATH = path.join(KEY_DIR, 'private.pem');
const PUB_PATH = path.join(KEY_DIR, 'public.pem');
function generateKeyPair() {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
return { privateKey, publicKey };
}
module.exports = { generateKeyPair, KEY_DIR, PRIV_PATH, PUB_PATH };
Bemærk, at du ikke angiver nogen kurve, modulus-længde eller hash. Ed25519 har præcis ét parametersæt, og det er hele pointen. Hvor RSA tvinger dig til at vælge nøglelængde, og ECDSA kræver et kurvevalg, fjerner Ed25519 beslutningen og dermed muligheden for at vælge forkert.
Trin 3: Eksportér og gem nøgler som PEM
Et KeyObject lever kun i hukommelsen. For at gemme nøglerne på disk eksporterer du dem til PEM-format, som er Base64-tekst med tydelige header-linjer. Den private nøgle bruger PKCS#8-struktur, den offentlige bruger SPKI. Udvid src/keys.js med en gem-funktion.
// Tilføj i src/keys.js
function saveKeyPair({ privateKey, publicKey }) {
if (!fs.existsSync(KEY_DIR)) {
fs.mkdirSync(KEY_DIR, { recursive: true });
}
const privPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
const pubPem = publicKey.export({ type: 'spki', format: 'pem' });
// Privat noegle: kun ejeren maa laese (0600 paa Unix)
fs.writeFileSync(PRIV_PATH, privPem, { mode: 0o600 });
fs.writeFileSync(PUB_PATH, pubPem);
return { privPem, pubPem };
}
module.exports.saveKeyPair = saveKeyPair;
Filtilladelsen 0o600 betyder, at kun ejeren kan læse og skrive den private nøgle. På et Unix-system er det første forsvarslinje mod, at andre brugere på maskinen kan stjæle din signaturnøgle. En genereret offentlig nøgle ser sådan ud, kort og læsbar:
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=
-----END PUBLIC KEY-----
Hele den offentlige nøgle er en enkelt kort linje Base64. Det understreger, hvor lille Ed25519 er sammenlignet med en RSA-nøgle, der typisk fylder otte til tolv linjer.
Trin 4: Indlæs nøgler fra disk
Når nøglerne ligger på disk, skal du kunne læse dem ind igen som KeyObject. Brug crypto.createPrivateKey og crypto.createPublicKey. De accepterer PEM-strenge direkte.
// Tilføj i src/keys.js
function loadPrivateKey() {
const pem = fs.readFileSync(PRIV_PATH, 'utf8');
return crypto.createPrivateKey(pem);
}
function loadPublicKey() {
const pem = fs.readFileSync(PUB_PATH, 'utf8');
return crypto.createPublicKey(pem);
}
module.exports.loadPrivateKey = loadPrivateKey;
module.exports.loadPublicKey = loadPublicKey;
Et nyttigt trick: du kan altid udlede den offentlige nøgle fra den private. crypto.createPublicKey(privateKey) giver det matchende offentlige KeyObject. Det er praktisk, hvis du kun har den private nøgle ved hånden og vil dele den offentlige del med andre.
Trin 5: Signér en besked med crypto.sign
Nu kommer kernen. For Ed25519 er signering ét kald: crypto.sign(null, data, privateKey). Det første argument, normalt en hash-algoritme, skal være null. Ed25519 hasher selv beskeden internt med SHA-512 som en del af skemaet, så du må ikke selv angive en hash. Skriv src/sign.js.
// src/sign.js
const crypto = require('node:crypto');
// Signerer en besked med en Ed25519 privat noegle.
// message: Buffer eller string. privateKey: Ed25519 KeyObject.
// Returnerer en 64-byte signatur (Buffer).
function signMessage(message, privateKey) {
const data = Buffer.isBuffer(message) ? message : Buffer.from(message, 'utf8');
// null = ingen pre-hash. Korrekt for Ed25519.
return crypto.sign(null, data, privateKey);
}
module.exports = { signMessage };
Læg mærke til, at vi normaliserer input til en Buffer. Hvis du blander tekst og binære data uden eksplicit kodning, risikerer du, at samme logiske besked giver forskellige bytes på afsender- og modtagerside, hvilket får verifikationen til at fejle. Resultatet er altid nøjagtig 64 bytes, uanset hvor lang beskeden er.
Trin 6: Verificér signaturen
Verifikation spejler signering: crypto.verify(null, data, publicKey, signature) returnerer en boolean. Igen er det første argument null. Funktionen er konstant-tids-sikker internt, så du behøver ikke selv tænke på timing-angreb her. Skriv src/verify.js.
// src/verify.js
const crypto = require('node:crypto');
// Verificerer en Ed25519-signatur.
// message: original besked. signature: 64-byte Buffer. publicKey: KeyObject.
// Returnerer true hvis signaturen er gyldig.
function verifyMessage(message, signature, publicKey) {
const data = Buffer.isBuffer(message) ? message : Buffer.from(message, 'utf8');
return crypto.verify(null, data, publicKey, signature);
}
module.exports = { verifyMessage };
Lad os teste signering og verifikation sammen. Lav en hurtig prøvefil, test-quick.js, og kør den.
// test-quick.js
const { generateKeyPair } = require('./src/keys');
const { signMessage } = require('./src/sign');
const { verifyMessage } = require('./src/verify');
const { privateKey, publicKey } = generateKeyPair();
const besked = 'Overfoer 500 DKK til konto 1234';
const sig = signMessage(besked, privateKey);
console.log('Signatur (hex):', sig.toString('hex'));
console.log('Signaturlaengde:', sig.length, 'bytes');
console.log('Gyldig:', verifyMessage(besked, sig, publicKey));
// Manipuleret besked skal fejle
console.log('Manipuleret:', verifyMessage('Overfoer 5000 DKK til konto 1234', sig, publicKey));
$ node test-quick.js
Signatur (hex): 3a1f...c904
Signaturlaengde: 64 bytes
Gyldig: true
Manipuleret: false
Den ægte besked verificerer som true, og en enkelt ændret ciffer i beløbet giver false. Det er digitale signaturer i praksis: selv den mindste ændring i de signerede data ugyldiggør signaturen. Princippet bygger på de samme egenskaber, vi gennemgår i Hashfunktioner: egenskaber, formål og praktisk brug.
Trin 7: Byg et signeret token-format
En rå signatur er sjældent nok i praksis. Du vil pakke en payload, en signatur og lidt metadata ind i ét transportabelt token. Vi bygger et kompakt format inspireret af JWT, men med Ed25519 og uden de kendte JWT-faldgruber. Formatet er to base64url-segmenter adskilt af et punktum: payload.signatur.
En vigtig detalje: vi serialiserer payload med sorterede nøgler, så samme logiske objekt altid giver samme bytes. Uden kanonisk serialisering kan to ens objekter producere forskellig JSON og dermed forskellige signaturer. Skriv src/token.js.
// src/token.js
const { signMessage } = require('./sign');
const { verifyMessage } = require('./verify');
// Kanonisk JSON: sorterede noegler giver deterministisk output
function canonicalJSON(obj) {
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
if (Array.isArray(obj)) return '[' + obj.map(canonicalJSON).join(',') + ']';
const keys = Object.keys(obj).sort();
return '{' + keys.map(function (k) {
return JSON.stringify(k) + ':' + canonicalJSON(obj[k]);
}).join(',') + '}';
}
function base64url(buf) {
return buf.toString('base64url');
}
function fromBase64url(str) {
return Buffer.from(str, 'base64url');
}
function createToken(payload, privateKey) {
const json = canonicalJSON(payload);
const payloadB64 = base64url(Buffer.from(json, 'utf8'));
const sig = signMessage(payloadB64, privateKey);
return payloadB64 + '.' + base64url(sig);
}
function verifyToken(token, publicKey) {
const parts = token.split('.');
if (parts.length !== 2) return { valid: false, reason: 'forkert format' };
const payloadB64 = parts[0];
const sig = fromBase64url(parts[1]);
if (sig.length !== 64) return { valid: false, reason: 'forkert signaturlaengde' };
const valid = verifyMessage(payloadB64, sig, publicKey);
if (!valid) return { valid: false, reason: 'ugyldig signatur' };
const payload = JSON.parse(fromBase64url(payloadB64).toString('utf8'));
return { valid: true, payload: payload };
}
module.exports = { createToken, verifyToken, canonicalJSON };
Vi signerer den base64url-kodede payload, ikke det rå objekt. Det betyder, at verifikatoren signerer præcis de samme bytes, som blev sendt over ledningen, uden at skulle re-serialisere JSON. Det eliminerer en hel klasse af subtile fejl, hvor afsender og modtager er uenige om mellemrum eller nøglerækkefølge.
Trin 8: Tilføj udløbstid og claims
Et token uden udløbstid er en sikkerhedsrisiko. Hvis det lækker, gælder det for evigt. Udvid token-laget med standardiserede claims: iat (issued at), exp (expiry) og sub (subject). Vi tilføjer en hjælper, der bygger payload og tjekker udløb ved verifikation.
// Tilføj i src/token.js
function issue(subject, claims, privateKey, ttlSeconds) {
ttlSeconds = ttlSeconds || 3600;
const now = Math.floor(Date.now() / 1000);
const payload = Object.assign({
sub: subject,
iat: now,
exp: now + ttlSeconds,
}, claims);
return createToken(payload, privateKey);
}
function validate(token, publicKey) {
const result = verifyToken(token, publicKey);
if (!result.valid) return result;
const now = Math.floor(Date.now() / 1000);
if (result.payload.exp && now > result.payload.exp) {
return { valid: false, reason: 'token udloebet' };
}
return result;
}
module.exports.issue = issue;
module.exports.validate = validate;
Nu kan du udstede et token for en bruger med rettigheder og en levetid på en time. Verifikatoren afviser både manipulerede og udløbne tokens. Det er det samme mønster, vi bruger i vores guides om kodeordssikkerhed og sessionshåndtering, blot med asymmetriske nøgler i stedet for en delt hemmelighed.
Trin 9: Arbejd med rå 32-byte-nøgler og JWK
Nogle gange skal du udveksle nøgler med et system, der ikke forstår PEM, for eksempel en mobilklient eller et bibliotek i et andet sprog, der forventer de rå 32 bytes. Node.js eksponerer ikke rå Ed25519-bytes direkte, men du kan komme til dem via JWK-formatet, hvor felterne x (offentlig) og d (privat) er base64url-kodede rå nøgler.
// raw-keys.js: udtraek raa bytes via JWK
const crypto = require('node:crypto');
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
const pubJwk = publicKey.export({ format: 'jwk' });
const privJwk = privateKey.export({ format: 'jwk' });
const rawPublic = Buffer.from(pubJwk.x, 'base64url'); // 32 bytes
const rawPrivate = Buffer.from(privJwk.d, 'base64url'); // 32 bytes
console.log('Raa offentlig noegle:', rawPublic.length, 'bytes');
console.log('Raa privat noegle:', rawPrivate.length, 'bytes');
// Genskab et KeyObject fra raa offentlige bytes
function publicKeyFromRaw(raw32) {
return crypto.createPublicKey({
key: { kty: 'OKP', crv: 'Ed25519', x: raw32.toString('base64url') },
format: 'jwk',
});
}
$ node raw-keys.js
Raa offentlig noegle: 32 bytes
Raa privat noegle: 32 bytes
Felttypen OKP står for Octet Key Pair og er JWK-kategorien for Edwards-kurver. Med denne konvertering kan din Node-server tale med en hvilken som helst Ed25519-implementering, fra libsodium i C til Web Crypto i browseren, så længe begge sider er enige om de samme 32 bytes.
Trin 10: Saml det i et CLI-værktøj
Nu binder vi modulerne sammen til et brugbart kommandolinjeværktøj. index.js håndterer tre kommandoer: keygen, sign og verify. Det er det punkt, hvor projektet bliver til noget, du faktisk kan bruge i en build-pipeline til at signere artefakter.
// index.js
const fs = require('node:fs');
const {
generateKeyPair, saveKeyPair, loadPrivateKey, loadPublicKey, PUB_PATH,
} = require('./src/keys');
const { issue, validate } = require('./src/token');
const command = process.argv[2];
const args = process.argv.slice(3);
function main() {
if (command === 'keygen') {
const pair = generateKeyPair();
saveKeyPair(pair);
console.log('Noeglepar gemt i ./keys/');
console.log('Offentlig noegle:\n' + fs.readFileSync(PUB_PATH, 'utf8'));
} else if (command === 'sign') {
const subject = args[0] || 'anonym';
const priv = loadPrivateKey();
const token = issue(subject, { role: args[1] || 'user' }, priv, 3600);
console.log(token);
} else if (command === 'verify') {
const token = args[0];
if (!token) { console.error('Brug: verify TOKEN'); process.exit(1); }
const pub = loadPublicKey();
const result = validate(token, pub);
console.log(JSON.stringify(result, null, 2));
process.exit(result.valid ? 0 : 1);
} else {
console.log('Kommandoer: keygen | sign SUBJECT ROLE | verify TOKEN');
}
}
main();
Kør hele flowet fra terminalen. Først genererer du nøglerne, så udsteder du et token, og endelig verificerer du det.
$ npm run keygen
Noeglepar gemt i ./keys/
$ node index.js sign [email protected] admin
eyJleHAiOjE3OD...wMH0.k3Jf9aQ...x2c
$ node index.js verify eyJleHAiOjE3OD...wMH0.k3Jf9aQ...x2c
{
"valid": true,
"payload": {
"exp": 1789000000,
"iat": 1788996400,
"role": "admin",
"sub": "[email protected]"
}
}
Exit-koden fra verify er 0 ved gyldigt token og 1 ved ugyldigt. Det gør værktøjet nemt at bruge i shell-scripts og CI-pipelines, hvor du vil afbryde en build, hvis en signatur ikke holder.
Trin 11: Eksponér en verifikations-API med Express
Det sidste trin gør projektet til en tjeneste. Vi bygger et lille Express-API, der udsteder tokens og beskytter en rute bag signaturverifikation. Installer Express først.
$ npm install express
// server.js
const express = require('express');
const { loadPrivateKey, loadPublicKey } = require('./src/keys');
const { issue, validate } = require('./src/token');
const app = express();
app.use(express.json());
const privateKey = loadPrivateKey();
const publicKey = loadPublicKey();
// Udsteder et signeret token
app.post('/login', function (req, res) {
const body = req.body || {};
if (!body.user) return res.status(400).json({ error: 'mangler user' });
const token = issue(body.user, { role: body.role || 'user' }, privateKey, 3600);
res.json({ token: token });
});
// Middleware der kraever et gyldigt Ed25519-token
function requireToken(req, res, next) {
const auth = req.headers.authorization || '';
const token = auth.indexOf('Bearer ') === 0 ? auth.slice(7) : null;
if (!token) return res.status(401).json({ error: 'mangler token' });
const result = validate(token, publicKey);
if (!result.valid) return res.status(401).json({ error: result.reason });
req.user = result.payload;
next();
}
app.get('/beskyttet', requireToken, function (req, res) {
res.json({ besked: 'Adgang givet', bruger: req.user.sub, rolle: req.user.role });
});
app.listen(3000, function () {
console.log('API koerer paa http://localhost:3000');
});
Start serveren med npm run serve, og afprøv den med curl. Serveren udsteder et token, og den beskyttede rute kræver, at signaturen verificerer.
$ curl -s -X POST http://localhost:3000/login \
-H 'Content-Type: application/json' \
-d '{"user":"[email protected]","role":"admin"}'
{"token":"eyJleHAiOj...x2c"}
$ TOKEN="eyJleHAiOj...x2c"
$ curl -s http://localhost:3000/beskyttet -H "Authorization: Bearer $TOKEN"
{"besked":"Adgang givet","bruger":"[email protected]","rolle":"admin"}
En vigtig arkitektonisk fordel ved Ed25519 frem for en delt HMAC-hemmelighed: kun udstederen har den private nøgle. Enhver tjeneste kan verificere tokens med den offentlige nøgle uden at kunne udstede falske tokens. Det er ideelt til mikrotjenester, hvor du vil distribuere verifikation bredt, men holde signering centralt.
Trin 12: Skriv negative tests
God signaturkode er defineret af, hvad den afviser, ikke kun hvad den accepterer. Skriv tests, der bevidst forsøger at snyde verifikatoren. Brug Node.js’ indbyggede testrunner, så du undgår endnu en afhængighed.
// test/token.test.js
const { test } = require('node:test');
const assert = require('node:assert');
const crypto = require('node:crypto');
const { issue, validate } = require('../src/token');
const a = crypto.generateKeyPairSync('ed25519');
const b = crypto.generateKeyPairSync('ed25519');
test('gyldigt token verificerer', function () {
const t = issue('sam', { role: 'admin' }, a.privateKey);
const r = validate(t, a.publicKey);
assert.strictEqual(r.valid, true);
assert.strictEqual(r.payload.role, 'admin');
});
test('forkert offentlig noegle afvises', function () {
const t = issue('sam', {}, a.privateKey);
assert.strictEqual(validate(t, b.publicKey).valid, false);
});
test('manipuleret payload afvises', function () {
const t = issue('sam', { role: 'user' }, a.privateKey);
const s = t.split('.')[1];
const tampered = Buffer.from('{"role":"admin"}').toString('base64url') + '.' + s;
assert.strictEqual(validate(tampered, a.publicKey).valid, false);
});
test('udloebet token afvises', function () {
const t = issue('sam', {}, a.privateKey, -10); // udloebet for 10 sek siden
assert.strictEqual(validate(t, a.publicKey).reason, 'token udloebet');
});
$ node --test
ok 1 - gyldigt token verificerer
ok 2 - forkert offentlig noegle afvises
ok 3 - manipuleret payload afvises
ok 4 - udloebet token afvises
# tests 4
# pass 4
# fail 0
Alle fire tests bestås. De dækker de fire vigtigste angrebsscenarier: forkert nøgle, manipuleret indhold, forkert format og udløb. Når disse er grønne, ved du, at din verifikation faktisk håndhæver det, den lover.
5 fælder, der ødelægger Ed25519-signaturer
De fleste Ed25519-fejl i Node.js stammer ikke fra kryptografien, men fra hvordan udviklere håndterer data omkring den. Her er de fem hyppigste, vi ser i kodegennemgange.
Fælde 1: At angive en hash i stedet for null
Den klart hyppigste fejl er at skrive crypto.sign('sha512', data, key) i stedet for crypto.sign(null, data, key). Ed25519 hasher selv internt, og hvis du angiver en algoritme, kaster Node enten en fejl eller producerer en signatur, der ikke kan verificeres af andre korrekte implementeringer. Brug altid null som første argument for Ed25519.
Fælde 2: At blande base64 og base64url
Standard-base64 bruger tegnene plus og skråstreg samt lighedstegn-padding, som ikke er URL-sikre. Hvis du koder med base64 og afkoder med base64url (eller omvendt), bliver signaturbytes forvansket, og verifikationen fejler tilsyneladende tilfældigt. Vælg ét format konsekvent. Node.js understøtter 'base64url' direkte som Buffer-kodning.
Fælde 3: Ikke-deterministisk JSON
Hvis du signerer JSON.stringify(obj) direkte og senere re-serialiserer objektet til verifikation, kan nøglerækkefølgen ændre sig, og signaturen holder ikke. Signér altid de nøjagtige bytes, der transporteres, eller brug kanonisk serialisering med sorterede nøgler, som vi gjorde i trin 7.
Fælde 4: At forveksle nøgletyper
At sende en privat nøgle til crypto.verify eller en offentlig nøgle til crypto.sign giver en uklar fejlmeddelelse. Navngiv dine variabler tydeligt (privateKey, publicKey) og tjek keyObject.type, som er enten 'private' eller 'public', hvis du er i tvivl.
Fælde 5: At committe den private nøgle
En privat nøgle i Git, i en Docker-image eller i en loglinje er kompromitteret for altid, også selvom du sletter den bagefter. Brug .gitignore, miljøvariabler eller en secrets-manager. Hvis en privat nøgle lækker, er der kun én løsning: rotér til et nyt nøglepar og afvis det gamle. Dette mønster går igen i alle vores guides om HTTPS og TLS og nøglehåndtering.
Avancerede tips til produktion
Når grundkoden virker, adskiller produktionsklar signering sig på et par punkter. Disse tips kommer fra reelle driftserfaringer med Ed25519 i tjenester med høj trafik.
- Nøglerotation med key id. Tilføj et
kid-felt til dine tokens, der peger på hvilken offentlig nøgle der skal bruges. Så kan du udrulle en ny nøgle og udfase den gamle uden at ugyldiggøre eksisterende tokens på én gang. - Hold private nøgler ude af appen. I produktion bør den private nøgle ligge i et hardware-sikkerhedsmodul (HSM) eller en cloud-KMS, der signerer på forespørgsel, så råbytes aldrig forlader det sikre modul.
- Brug asynkron signering under belastning.
crypto.signhar også en callback-baseret variant. Under høj last undgår du at blokere event-loopet ved at signere asynkront. - Sæt korte levetider. Foretræk tokens, der lever minutter til timer, kombineret med refresh-flow, frem for tokens, der gælder i dage.
- Log aldrig nøgler eller signaturer i klartekst. Maskér dem i logs, og overvåg for utilsigtet eksponering.
- Overvej post-kvante-overgangen. Til data, der skal forblive verificerbar i 10 år eller mere, så følg NISTs ML-DSA-standard og planlæg hybride signaturer på sigt.
Fejlfinding: 8 typiske problemer og løsninger
Når noget ikke virker, skyldes det næsten altid ét af følgende. Brug denne tabel som tjekliste, før du graver dybere.
| Symptom | Sandsynlig årsag | Løsning |
|---|---|---|
| Verify returnerer altid false | Hash angivet i stedet for null | Brug crypto.sign(null, ...) og crypto.verify(null, ...) |
| ERR_OSSL-fejl ved sign | Offentlig nøgle sendt til sign | Send den private nøgle til signering |
| Verify false efter transport | base64 mod base64url forveksling | Brug samme kodning begge steder |
| Signaturlængde ikke 64 | Trunkeret eller forkert afkodet | Tjek at sig.length er 64 før verify |
| Verify false for samme objekt | Ikke-deterministisk JSON | Signér de transporterede bytes direkte |
| ERR_INVALID_ARG_TYPE | String sendt hvor Buffer ventes | Konvertér med Buffer.from(x, 'utf8') |
| Kan ikke læse private.pem | Filtilladelse eller forkert sti | Tjek 0600-rettighed og absolut sti |
| JWK x eller d er undefined | Forkert nøgletype eksporteret | x findes på public, d kun på private |
Et hurtigt diagnostisk trick: hvis verifikation fejler, så log både signature.length og de første bytes af den signerede besked på begge sider. I ni ud af ti tilfælde afslører det med det samme, at de to sider ikke er enige om præcis hvilke bytes der blev signeret.
Nøgleformater i Node.js: en kort reference
Forvirring om nøgleformater er en hyppig kilde til frustration. Denne tabel opsummerer, hvornår du bruger hvilket format med Ed25519 i Node.js.
| Format | Type | Hvornår | Node-kald |
|---|---|---|---|
| PEM (PKCS#8) | Privat, tekst | Lagring på disk, config | export type pkcs8, format pem |
| PEM (SPKI) | Offentlig, tekst | Deling, config | export type spki, format pem |
| DER | Binær | Kompakt binær lagring | export type spki, format der |
| JWK | JSON | Web, interop, rå bytes | export format jwk |
| Rå 32 bytes | Binær | libsodium, andre sprog | Via JWK x- eller d-felt |
Til langt de fleste Node-til-Node-scenarier er PEM det enkleste valg. Skift kun til JWK eller rå bytes, når du skal interoperere med et system, der kræver det. Forsøg aldrig at konstruere PEM-strenge manuelt; lad export gøre arbejdet.
Det komplette projekt: struktur og kørsel
Du har nu et fuldt fungerende projekt. Mappestrukturen ser sådan ud, og hver fil har ét ansvar.
ed25519-signaturer/
index.js # CLI-indgang
server.js # Express-API
package.json
.gitignore
keys/ # Genererede noegler (ignoreret af Git)
private.pem
public.pem
src/
keys.js # Generering, lagring, indlaesning
sign.js # signMessage
verify.js # verifyMessage
token.js # createToken, issue, validate
test/
token.test.js # Negative og positive tests
Hele flowet fra nul til kørende tjeneste er fire kommandoer. Det viser, hvor lidt kode der skal til at bygge robust signaturhåndtering med det indbyggede crypto-modul.
$ npm install # Express
$ npm run keygen # Generer Ed25519-noeglepar
$ node --test # Koer alle tests
$ npm run serve # Start API paa port 3000
Med under 200 linjer kode har du nøglegenerering, et signeret token-format med udløb, et CLI og et beskyttet API, alt sammen baseret på Ed25519 og uden en eneste kryptografi-afhængighed fra tredjepart. Det er styrken ved at bruge Node.js’ indbyggede primitiver korrekt.
Sådan passer Ed25519 ind i den større sikkerhedsmodel
En digital signatur er kun ét lag. Den beviser, hvem der signerede og at indholdet er uændret, men den krypterer ikke data og beskytter ikke mod, at en angriber afspiller et gyldigt token, du allerede har udstedt. Derfor kombinerer du i praksis signaturer med TLS til transportkryptering, korte levetider mod replay og rotation mod nøglekompromittering.
Signaturer bygger på de samme hash-fundamenter, som vi gennemgår i SHA-256 forklaret. Faktisk hasher Ed25519 internt med SHA-512, og hele sikkerheden afhænger af, at den underliggende hashfunktion er kollisionsresistent. Da SHA-1 blev brudt i 2017, som vi beskriver i SHA-1-kollisionen, var det netop signatursystemer, der var i størst fare. Ed25519 bruger SHA-512 specifikt for at undgå den slags svagheder.
Den vigtigste fremtidssikring i 2026 handler om kvantecomputere. Ed25519 er sikker mod alt, hvad klassiske computere kan, men en stor kvantecomputer ville bryde den. NIST har standardiseret post-kvante-signaturer (ML-DSA, tidligere Dilithium), og branchen bevæger sig mod hybride løsninger, der kombinerer Ed25519 med en post-kvante-algoritme. Til alt, der skal verificeres inden for det næste årti, er Ed25519 dog stadig et fremragende og veldokumenteret valg.
Ofte stillede spørgsmål om Ed25519 i Node.js
Skal jeg installere et bibliotek for at bruge Ed25519 i Node.js?
Nej. Det indbyggede crypto-modul understøtter Ed25519 fuldt ud fra Node.js 12 og frem, inklusive nøglegenerering, signering og verifikation. Du behøver ingen pakker som tweetnacl eller libsodium-wrappers til standardbrug. Vi brugte kun Express, og det kun til det valgfrie API-trin.
Hvorfor skal første argument til crypto.sign være null?
Fordi Ed25519 udfører sin egen hashing internt med SHA-512 som en del af algoritmen. Hvis du angiver en hash som 'sha256' eller 'sha512', forstyrrer du skemaet, og resultatet kan enten fejle eller blive uverificerbart for andre korrekte implementeringer. null betyder simpelthen ingen ekstern pre-hash.
Er Ed25519 bedre end RSA til JWT-lignende tokens?
For de fleste moderne brugssituationer, ja. Ed25519-signaturer fylder 64 bytes mod RSA-2048’s 256 bytes, signering er hurtigere, og der er færre konfigurationsvalg at fejle. RSA har stadig sin plads, hvor ekstrem verifikationshastighed eller bagudkompatibilitet kræves, men til nye systemer er Ed25519 ofte det renere valg.
Kan jeg verificere en Node.js Ed25519-signatur i browseren?
Ja. Web Crypto API i moderne browsere understøtter Ed25519. Eksportér den offentlige nøgle som JWK fra Node, importér den i browseren med crypto.subtle.importKey, og verificér med crypto.subtle.verify. Sørg for, at begge sider er enige om de nøjagtige bytes, der blev signeret.
Hvad er forskellen på Ed25519 og X25519?
De bygger på samme kurve (Curve25519), men løser forskellige problemer. Ed25519 er til digitale signaturer (bevis på autenticitet). X25519 er til nøgleudveksling (Diffie-Hellman, til at etablere en delt hemmelighed). Brug Ed25519, når du vil signere; brug X25519, når du vil oprette en krypteret kanal.
Er Ed25519 lovligt og godkendt til erhvervsbrug i EU?
Ja. Ed25519 er defineret i RFC 8032, standardiseret af NIST i FIPS 186-5 (2023) og bredt anbefalet af sikkerhedsmyndigheder. Der er ingen juridiske hindringer for at bruge det i danske eller nordiske virksomheder, og det opfylder anbefalingerne for stærk kryptografi i de fleste compliance-rammer.
Hvor længe kan jeg stole på en Ed25519-signatur?
Mod klassiske computere regnes Ed25519 som sikker i overskuelig fremtid med sine cirka 128 bit sikkerhed. Den reelle usikkerhed er kvantecomputere. Til data, der skal forblive verificerbar ud over de næste 10 til 15 år, bør du planlægge en overgang til post-kvante-signaturer eller hybride ordninger.
Hvordan roterer jeg en Ed25519-nøgle uden at bryde eksisterende tokens?
Tilføj et nøgle-id (kid) til hvert token, og lad verifikatoren slå den rigtige offentlige nøgle op ud fra det. Når du introducerer en ny nøgle, accepterer du begge i en overgangsperiode, indtil alle gamle tokens er udløbet, og derefter fjerner du den gamle nøgle.
Relateret indhold
- Digitale signaturer: hvordan hashing og nøgler skaber tillid
- SHA-256 forklaret: hjørnestenen i moderne hashing
- Hashfunktioner: egenskaber, formål og praktisk brug
- SHA-1-kollisionen: da SHAttered brød en hashfunktion
- HTTPS og TLS: sådan beskyttes din forbindelse
- Kryptografi: hashfunktioner, SHA og digital tillid
Konklusion: Ed25519 er den pragmatiske standard
Du har bygget et komplet signatursystem i Node.js fra bunden: nøglegenerering, PEM-lagring, signering, verifikation, et signeret token-format med udløb, et CLI, et beskyttet API og en testsuite, der afviser de fire vigtigste angreb. Alt sammen med det indbyggede crypto-modul og under 200 linjer kode.
Ed25519 er populær af en god grund. Den fjerner de farlige valg, holder nøgler og signaturer små, kører hurtigt og er bredt understøttet fra OpenSSH til browsere. Husk de tre regler, der forhindrer de fleste fejl: brug null som hash-argument, signér de nøjagtige bytes du transporterer, og lad aldrig den private nøgle forlade det sikre sted. Følg dem, og du har signaturhåndtering, der holder i produktion.
Kilder og videre læsning: RFC 8032 (EdDSA), Node.js crypto-dokumentation, NIST FIPS 186-5, RFC 8709 (Ed25519 i SSH) og den officielle Ed25519-side.




