ECDSA (Elliptic Curve Digital Signature Algorithm) on elliptisiin käyriin perustuva digitaalinen allekirjoitusalgoritmi, joka tarjoaa RSA-2048:aa vastaavan turvallisuuden murto-osalla avainkoosta. Node.js sisältää natiivin ECDSA-tuen node:crypto-moduulin kautta, ja se kattaa NIST P-256-, P-384- ja P-521-käyrät sekä Bitcoin-ekosysteemin käyttämän secp256k1-käyrän. Tässä oppaassa rakennat täydellisen ECDSA-allekirjoitusjärjestelmän 12 vaiheessa noin 35 minuutissa.

ECDSA on vakiostandardi JWT ES256/ES384/ES512-tokeneissa, TLS-sertifikaateissa, koodiallekirjoituksessa ja lohkoketjujärjestelmissä. NIST FIPS 186-5 -standardi, joka julkaistiin helmikuussa 2023, vahvistaa P-256:n, P-384:n ja P-521:n suositeltaviksi käyriksi viranomaiskäyttöön. P-256-avain on vain 32 tavua, kun RSA-2048 vaatii 256 tavua, minkä vuoksi ECDSA sopii erityisesti suorituskykykriittisiin ympäristöihin.

Edellytykset

Ennen aloittamista varmista, että seuraavat työkalut on asennettu:

TyökaluVaadittu versioTarkistuskomento
Node.js18.4.0 tai uudempinode --version
npm9.0.0 tai uudempinpm --version
OpenSSL3.0.0 tai uudempiopenssl version
jsonwebtoken9.0.0 tai uudempinpm list jsonwebtoken
asn1.js (valinnainen)5.4.1 tai uudempinpm list asn1.js

ECDSA-tuki sisältyy Node.js:n sisäänrakennettuun node:crypto-moduuliin, joten erillisiä kryptografiakirjastoja ei tarvita perustoimintoihin. Node.js 18.4.0 toi mukanaan parannetun Ed25519/Ed448-tuen, mutta ECDSA on tuettu jo Node.js 0.10:stä asti. Varmista kuitenkin, että käytät vähintään versiota 18 saadaksesi täyden Web Crypto API -yhteensopivuuden.

Vaihe 1: Projektin alustus

Luo uusi Node.js-projekti ja asenna tarvittavat riippuvuudet. ECDSA-perustoiminnot eivät vaadi ulkoisia paketteja, mutta JWT-integraatio vaatii jsonwebtoken-paketin.

mkdir ecdsa-tutorial && cd ecdsa-tutorial
npm init -y
npm install jsonwebtoken

# Luo projektirakenne
mkdir -p src/keys src/examples
touch src/keygen.js src/sign.js src/verify.js src/jwt-example.js src/secp256k1.js

Varmista, että Node.js-ympäristösi tukee ECDSA:ta ajamalla seuraava komento. Se tulostaa käytettävissä olevat elliptiset käyrät:

node -e "const crypto = require('node:crypto'); console.log(crypto.getCurves().filter(c => c.includes('prime') || c.includes('sec') || c.includes('384') || c.includes('521')));"

Odotettu tuloste on lista, joka sisältää ainakin prime256v1, secp384r1, secp521r1 ja secp256k1. Jos lista on tyhjä, Node.js:si on käännetty OpenSSL-tuesta ilman elliptisten käyrien tukea, mikä on erittäin harvinaista virallisissa Node.js-julkaisuissa.

Vaihe 2: ECDSA P-256 -avainparin luominen

P-256 (tunnetaan myös nimillä prime256v1 ja secp256r1) on yleisin ECDSA-käyrä. Se tarjoaa 128-bittisen turvallisuustason, mikä vastaa AES-128:aa ja RSA-3072:ta. P-256 on TLS 1.3:n suosituin allekirjoituskäyrä, ja se on oletuksena useimmissa HTTPS-palvelimissa. Node.js luo P-256-avainparin crypto.generateKeyPairSync-funktiolla, joka on synkroninen operaatio. Asynkronista versiota crypto.generateKeyPair kannattaa suosia tuotantokoodissa, jotta tapahtumakierto ei blockaudu avainparin luonnin ajaksi.

// src/keygen.js
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');

function generateECDSAKeyPair(curve = 'prime256v1') {
  const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
    namedCurve: curve,
    publicKeyEncoding: {
      type: 'spki',
      format: 'pem'
    },
    privateKeyEncoding: {
      type: 'pkcs8',
      format: 'pem'
    }
  });
  return { privateKey, publicKey };
}

// Luo avainpari ja tallenna tiedostoihin
const { privateKey, publicKey } = generateECDSAKeyPair('prime256v1');

fs.writeFileSync(path.join('src', 'keys', 'ec-private.pem'), privateKey);
fs.writeFileSync(path.join('src', 'keys', 'ec-public.pem'), publicKey);

console.log('Yksityinen avain (PKCS8 PEM):');
console.log(privateKey);
console.log('Julkinen avain (SPKI PEM):');
console.log(publicKey);

Aja skripti komennolla node src/keygen.js. Odotettu tuloste näyttää seuraavalta:

Yksityinen avain (PKCS8 PEM):
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg...
-----END PRIVATE KEY-----

Julkinen avain (SPKI PEM):
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----

P-256-yksityinen avain on 32 tavua (256 bittiä), kun taas RSA-2048-yksityinen avain on 256 tavua. PEM-formaatti Base64-enkoodaa nämä tavut, joten tiedostot ovat jonkin verran suurempia kuin raakadata. type: 'spki' (Subject Public Key Info) julkiselle avaimelle ja type: 'pkcs8' yksityiselle avaimelle ovat yleisimmät ja yhteensopivimmat formaatit, jotka toimivat OpenSSL:n ja muiden kryptografiakirjastojen kanssa.

Vaihe 3: P-384 ja P-521 -käyrät

Organisaatiot, joilla on korkeammat turvallisuusvaatimukset kuten puolustus-, hallitus- ja rahoituslaitokset, käyttävät P-384:ää tai P-521:ta. P-384 tarjoaa 192-bittisen ja P-521 jopa 256-bittisen turvallisuustason. Vaikka kasvavat avainkoot hidastavat operaatioita, parannukset turvallisuustasoon ovat merkittävät. Suomessa julkishallinnon järjestelmät, jotka noudattavat Kyberturvallisuuslakia 124/2025, hyödyntävät yleisesti P-256:ta tai P-384:ää allekirjoituksiin.

KäyräTurvallisuustasoAllekirjoituksen koko (DER)Julkisen avaimen kokoTyypilliset käyttökohteet
P-256 (prime256v1)128 bittiä70-72 tavua65 tavua (kompressoimaton)TLS, JWT ES256, yleinen web
P-384 (secp384r1)192 bittiä102-104 tavua97 tavuaJWT ES384, hallituksen PKI
P-521 (secp521r1)256 bittiä137-139 tavua133 tavuaJWT ES512, korkean turvallisuuden sovellukset
secp256k1128 bittiä70-72 tavua65 tavuaBitcoin, Ethereum, lohkoketjut
RSA-2048 (vertailu)112 bittiä256 tavua270 tavuaVanha infrastruktuuri
// Luo avainparit kaikille käyrille ja vertaile kokoja
const crypto = require('node:crypto');

const curves = [
  { name: 'prime256v1', hash: 'sha256', jwtAlg: 'ES256' },
  { name: 'secp384r1', hash: 'sha384', jwtAlg: 'ES384' },
  { name: 'secp521r1', hash: 'sha512', jwtAlg: 'ES512' }
];

curves.forEach(({ name, hash, jwtAlg }) => {
  const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
    namedCurve: name,
    publicKeyEncoding: { type: 'spki', format: 'pem' },
    privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
  });

  // Tee testiallekirjoitus ja mittaa koko
  const sign = crypto.createSign(hash);
  sign.update(Buffer.from('testitekstiä'));
  const sig = sign.sign({ key: privateKey, dsaEncoding: 'der' });

  console.log(`\nKäyrä: ${name} (${jwtAlg})`);
  console.log(`  Allekirjoituksen koko (DER): ${sig.length} tavua`);
  console.log(`  Yksityinen avain PEM-koko: ${privateKey.length} merkkiä`);
  console.log(`  Julkinen avain PEM-koko: ${publicKey.length} merkkiä`);
});

Node.js käyttää OpenSSL:n kääntämiä toteutuksia kaikista NIST-käyristä. Jos sinun täytyy tarkistaa, mitä käyriä Node.js-ympäristösi tukee, aja node -e "require('node:crypto').getCurves().forEach(c => console.log(c))". Virallinen Node.js-dokumentaatio käyristä löytyy osoitteesta nodejs.org/api/crypto.html.

Vaihe 4: Digitaalisen allekirjoituksen luominen

ECDSA-allekirjoitus koostuu kahdesta suuresta luvusta r ja s, jotka yhdessä todentavat, että allekirjoittajalla oli hallussaan yksityinen avain allekirjoitushetkellä. Node.js:ssä käytetään crypto.createSign()-metodia, joka ensin tiivistää datan valitulla hajautusfunktiolla ja sen jälkeen allekirjoittaa tiivisteen ECDSA-algoritmilla.

ECDSA vaatii aina tiivisteen allekirjoitettavasta datasta, toisin kuin Ed25519, joka allekirjoittaa raakadatan suoraan. Tämä on tärkeä ero: P-256:lle suositellaan SHA-256:ta, P-384:lle SHA-384:ää ja P-521:lle SHA-512:ta. Nämä yhdistelmät ovat NIST FIPS 186-5 -standardin suosittelemia ja varmistavat, että kokonaisturvallisuustaso vastaa käyrän tarjoamaa suojausta.

// src/sign.js
const crypto = require('node:crypto');
const fs = require('node:fs');

const privateKeyPem = fs.readFileSync('src/keys/ec-private.pem', 'utf8');
const privateKey = crypto.createPrivateKey(privateKeyPem);

function signMessage(message, privateKey, hashAlgorithm = 'sha256') {
  const sign = crypto.createSign(hashAlgorithm);
  sign.update(message);
  sign.end();

  // DER-formaatti on oletusarvo (ASN.1-enkoodattu)
  const signature = sign.sign({
    key: privateKey,
    dsaEncoding: 'der'
  });

  return signature;
}

const message = 'Tämä viesti allekirjoitetaan ECDSA P-256:lla';
const signature = signMessage(message, privateKey, 'sha256');

console.log('Allekirjoitus (DER, hex):');
console.log(signature.toString('hex'));
console.log(`\nAllekirjoituksen koko: ${signature.length} tavua`);
console.log(`\nAllekirjoitus (Base64):`);
console.log(signature.toString('base64'));

// Tallenna allekirjoitus tiedostoon
fs.writeFileSync('src/keys/signature.der', signature);

Odotettu tuloste osoittaa, että DER-enkoodattu P-256-allekirjoitus on 70-72 tavua:

Allekirjoitus (DER, hex):
3045022100a1b2c3...f0a1b2022067d3e4...ab12cd

Allekirjoituksen koko: 71 tavua

Allekirjoitus (Base64):
MEUCIQC[...base64-enkoodattu DER...]

Tärkeä huomio: ECDSA-allekirjoitukset eivät ole deterministisiä oletusarvoina. Sama viesti samalla avaimella tuottaa eri allekirjoituksen joka kerta, koska algoritmi käyttää satunnaista nonce-arvoa k. Jos haluat deterministisiä allekirjoituksia, käytä RFC 6979 -standardin mukaista k-arvon johdatusta. Lue lisää RFC 6979:stä osoitteesta rfc-editor.org/rfc/rfc6979.

Vaihe 5: Allekirjoituksen vahvistaminen

Allekirjoituksen vahvistaminen vaatii ainoastaan julkisen avaimen, alkuperäisen viestin ja allekirjoituksen. Vahvistusprosessi rekonstruoi pistearitmeetiikalla pisteyhdistelmän ja tarkistaa, vastaako se allekirjoituksessa olevaa r-arvoa. Yksityistä avainta ei tarvita vahvistukseen, mikä mahdollistaa julkisen avaimen jakamisen avoimesti esimerkiksi JWKS-päätepisteessä tai X.509-sertifikaatissa.

// src/verify.js
const crypto = require('node:crypto');
const fs = require('node:fs');

const publicKeyPem = fs.readFileSync('src/keys/ec-public.pem', 'utf8');
const signature = fs.readFileSync('src/keys/signature.der');
const publicKey = crypto.createPublicKey(publicKeyPem);

function verifySignature(message, signature, publicKey, hashAlgorithm = 'sha256') {
  const verify = crypto.createVerify(hashAlgorithm);
  verify.update(message);
  verify.end();

  return verify.verify(
    { key: publicKey, dsaEncoding: 'der' },
    signature
  );
}

const message = 'Tämä viesti allekirjoitetaan ECDSA P-256:lla';
const isValid = verifySignature(message, signature, publicKey, 'sha256');

console.log(`Allekirjoitus kelpaa: ${isValid}`);

// Testaa väärällä viestillä
const wrongMessage = 'Tämä on väärä viesti';
const isValidWrong = verifySignature(wrongMessage, signature, publicKey, 'sha256');
console.log(`Väärä viesti kelpaa: ${isValidWrong}`);

// Testaa muutetulla allekirjoituksella
const tamperedSig = Buffer.from(signature);
tamperedSig[10] ^= 0xFF;
try {
  const isValidTampered = verifySignature(message, tamperedSig, publicKey, 'sha256');
  console.log(`Muutettu allekirjoitus kelpaa: ${isValidTampered}`);
} catch (err) {
  console.log(`Muutettu allekirjoitus aiheutti virheen: ${err.message}`);
}

Odotettu tuloste vahvistaa, että validointi toimii oikein:

Allekirjoitus kelpaa: true
Väärä viesti kelpaa: false
Muutettu allekirjoitus aiheutti virheen: error:1E08010C:DECODER routines::unsupported

Huomaa, että muutettu allekirjoitus voi aiheuttaa joko false-paluuarvon tai poikkeuksen riippuen siitä, mihin kohtaan muutos osui. DER-enkoodauksessa allekirjoitus on rakenteistettu ASN.1-tietoa, joten joidenkin tavujen muuttaminen rikkoo rakenteen ja aiheuttaa parsintavirheen jo ennen kryptografista tarkistusta. Tämä on normaalia käyttäytymistä ja osoittaa, että ECDSA suojaa sekä datan eheyttä että aitouden.

Vaihe 6: DER- ja IEEE P1363 -formaatit

ECDSA-allekirjoituksilla on kaksi pääformaattia: DER (Distinguished Encoding Rules) ja IEEE P1363. Node.js tukee molempia dsaEncoding-parametrin kautta. DER on oletusarvo ja se käyttää ASN.1-rakennetta, joka lisää muutaman tavun ylärasitteen mutta on yleisimmin käytetty formaatti muiden järjestelmien kanssa. IEEE P1363 on yksinkertaisempi r||s-ketjutus kiinteällä pituudella. Formaattien sekoittaminen on yleisin yhteensopivuusongelma ECDSA-integraatioissa.

// Vertaile DER ja IEEE P1363 -formaatteja
const crypto = require('node:crypto');

const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
  namedCurve: 'prime256v1',
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

const message = Buffer.from('Testidataa ECDSA-formaattivertailuun');

// DER-allekirjoitus (ASN.1)
const signDER = crypto.createSign('sha256');
signDER.update(message);
const sigDER = signDER.sign({ key: privateKey, dsaEncoding: 'der' });

// IEEE P1363 -allekirjoitus (r||s, kiinteä pituus)
const signP1363 = crypto.createSign('sha256');
signP1363.update(message);
const sigP1363 = signP1363.sign({ key: privateKey, dsaEncoding: 'ieee-p1363' });

console.log(`DER-allekirjoituksen koko: ${sigDER.length} tavua`);
console.log(`IEEE P1363 -allekirjoituksen koko: ${sigP1363.length} tavua`);

// Vahvistus oikealla formaatilla
const verifyDER = crypto.createVerify('sha256');
verifyDER.update(message);
const validDER = verifyDER.verify({ key: publicKey, dsaEncoding: 'der' }, sigDER);

const verifyP1363 = crypto.createVerify('sha256');
verifyP1363.update(message);
const validP1363 = verifyP1363.verify({ key: publicKey, dsaEncoding: 'ieee-p1363' }, sigP1363);

console.log(`DER kelpaa: ${validDER}`);
console.log(`IEEE P1363 kelpaa: ${validP1363}`);

P-256-käyrällä IEEE P1363 tuottaa aina tarkalleen 64 tavun allekirjoituksen (32 tavua r + 32 tavua s), kun taas DER tuottaa 70-72 tavua johtuen ASN.1-otsikkorakenteesta. P-384:llä IEEE P1363 on 96 tavua ja P-521:llä 132 tavua. Käytä IEEE P1363 -formaattia JWT-tokeneissa (ES256, ES384, ES512), koska JWT-standardi RFC 7518 vaatii kiinteän pituiset r||s-allekirjoitukset. Lue JWT-allekirjoitusstandardista lisää osoitteesta datatracker.ietf.org/doc/html/rfc7518.

Vaihe 7: ECDSA JWT-tokeneissa (ES256)

JWT-tokenit käyttävät ECDSA:ta algoritmien ES256, ES384 ja ES512 kautta, jotka vastaavat P-256+SHA-256, P-384+SHA-384 ja P-521+SHA-512 -yhdistelmiä. ECDSA-pohjaiset JWT-tokenit ovat turvallisempia kuin HMAC-pohjaiset (HS256), koska vahvistus ei vaadi jaettua salaisuutta. Tämä mahdollistaa julkisen avaimen jakamisen avoimesti esimerkiksi JWKS-päätepisteessä, mikä on hajautettujen mikropalveluarkkitehtuurien vakioratkaisu.

// src/jwt-example.js
const crypto = require('node:crypto');
const jwt = require('jsonwebtoken');

// Luo ES256-avainpari JWT:tä varten
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
  namedCurve: 'prime256v1',
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

// Luo JWT-token ES256-algoritmilla
const payload = {
  sub: '1234567890',
  name: 'Matti Meikäläinen',
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + 3600,
  roles: ['admin', 'user']
};

const token = jwt.sign(payload, privateKey, {
  algorithm: 'ES256',
  header: { kid: 'avain-2026-06' }
});

console.log('JWT ES256 -token:');
console.log(token);
console.log(`\nTokenin pituus: ${token.length} merkkiä`);

// Jaa vain julkinen avain tokenin vahvistajille
const decoded = jwt.verify(token, publicKey, { algorithms: ['ES256'] });
console.log('\nVahvistettu payload:');
console.log(JSON.stringify(decoded, null, 2));

// Yritä vahvistaa väärällä julkisella avaimella
const { publicKey: wrongPublicKey } = crypto.generateKeyPairSync('ec', {
  namedCurve: 'prime256v1',
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

try {
  jwt.verify(token, wrongPublicKey, { algorithms: ['ES256'] });
} catch (err) {
  console.log(`\nVäärä avain estetty: ${err.message}`);
}

ES256 on suositeltavin JWT-algoritmi uusiin sovelluksiin, koska se yhdistää hyvän suorituskyvyn ja turvallisuuden. Verratuna HS256:een (HMAC-SHA256), ES256 mahdollistaa hajautetun vahvistuksen ilman jaettua salaisuutta. Jos olet rakentanut JWT-sovelluksia aiemmin, lue lisää JWT-todennuksesta Node.js:ssä -oppaastamme, jossa käymme läpi myös HS256-lähestymistavan. Avaintunniste kid-kentässä mahdollistaa sujuvan avainrotaation, kun palvelu julkaisee useita aktiivisia avaimia JWKS-päätepisteessään.

Vaihe 8: Avainten turvallinen tallennus

Yksityisten avainten turvallinen tallennus on kriittinen osa ECDSA-toteutusta. Älä koskaan tallenna yksityisiä avaimia versionhallintaan, ympäristömuuttujiin suoraan tai selkokielisenä tiedostojärjestelmään. Käytä sen sijaan salasanasuojattua PKCS8-enkoodausta kehitysympäristöissä ja HSM-laitteistoja tai salausholvijärjestelmiä kuten HashiCorp Vault tai AWS KMS tuotannossa.

// Turvallinen avainten tallennus salasanasuojauksella
const crypto = require('node:crypto');
const fs = require('node:fs');

const passphrase = process.env.KEY_PASSPHRASE;
if (!passphrase) {
  throw new Error('KEY_PASSPHRASE-ympäristömuuttuja puuttuu');
}

// Luo avainpari AES-256-CBC-salauksella
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
  namedCurve: 'prime256v1',
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem',
    cipher: 'aes-256-cbc',
    passphrase: passphrase
  }
});

// Tallenna rajoitetuilla oikeuksilla (600 = vain omistaja voi lukea/kirjoittaa)
fs.writeFileSync('src/keys/ec-private-encrypted.pem', privateKey, { mode: 0o600 });
fs.writeFileSync('src/keys/ec-public.pem', publicKey);
console.log('Salattu yksityinen avain tallennettu (oikeudet 600)');

// Lataa salattu avain oikealla salasanalla
const encryptedPem = fs.readFileSync('src/keys/ec-private-encrypted.pem', 'utf8');
const loadedKey = crypto.createPrivateKey({
  key: encryptedPem,
  passphrase: passphrase
});

const keyDetails = loadedKey.asymmetricKeyDetails;
console.log(`Avain ladattu: tyyppi=${loadedKey.asymmetricKeyType}, käyrä=${keyDetails.namedCurve}`);

// Varmista, että salaus toimii latauksen jälkeen
const sign = crypto.createSign('sha256');
sign.update('Testitekstiä');
const sig = sign.sign(loadedKey);
console.log(`Allekirjoitus ladatulla salatulla avaimella onnistui (${sig.length} tavua)`);

Tuotantoympäristöissä yksityiset avaimet tulisi tallentaa aina salatussa muodossa. AES-256-CBC on vakiostandardi PEM-tiedostojen salaukseen. Tallenna salasana erilliseen turvalliseen paikkaan kuten salausholvia käyttäen, ei samaan tiedostojärjestelmään yksityisen avaimen kanssa. Tiedostojärjestelmän käyttöoikeuksien rajoittaminen mode: 0o600-parametrilla estää muita käyttäjiä lukemasta avainta Unix-järjestelmissä.

Vaihe 9: secp256k1 Bitcoin-lohkoketjuun

secp256k1 on Satoshi Nakamoton valitsema käyrä Bitcoiniin ja se on sittemmin levinnyt Ethereumiin ja muihin lohkoketjuihin. secp256k1 eroaa NIST P-256:sta käyrän parametreissa: secp256k1 on Koblitzin käyrä muodossa y² = x³ + 7, kun taas P-256 on satunnaistettu käyrä muodossa y² = x³ + ax + b. secp256k1:llä on tiettyjä laskennallisia etuja Koblitzin rakenteen ansiosta, minkä vuoksi se on lohkoketjuekosysteemissä laajasti käytössä. Node.js tukee secp256k1:ää OpenSSL:n kautta, mutta tuki saattaa puuttua joissakin rajoitetuissa käyttöympäristöissä.

// src/secp256k1.js - Bitcoin-yhteensopiva ECDSA
const crypto = require('node:crypto');

// Tarkista secp256k1-tuki
const supportedCurves = crypto.getCurves();
if (!supportedCurves.includes('secp256k1')) {
  throw new Error('secp256k1 ei ole tuettu tässä OpenSSL-versiossa');
}

// Luo secp256k1-avainpari
const { privateKey: secp256k1PrivKey, publicKey: secp256k1PubKey } =
  crypto.generateKeyPairSync('ec', {
    namedCurve: 'secp256k1',
    publicKeyEncoding: { type: 'spki', format: 'pem' },
    privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
  });

// Allekirjoita Bitcoin-transaktion kaltainen data
const transactionData = JSON.stringify({
  from: '1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf',
  to: '1BoatSLRHtKNngkdXEeobR76b53LETtpyT',
  amount: '0.001',
  nonce: 42
});

const sign = crypto.createSign('sha256');
sign.update(transactionData);
const txSignature = sign.sign({ key: secp256k1PrivKey, dsaEncoding: 'der' });

console.log(`Transaktiodata: ${transactionData}`);
console.log(`Allekirjoitus (hex): ${txSignature.toString('hex')}`);
console.log(`Allekirjoituksen koko: ${txSignature.length} tavua`);

// Vahvista transaktio
const verify = crypto.createVerify('sha256');
verify.update(transactionData);
const isValid = verify.verify({ key: secp256k1PubKey, dsaEncoding: 'der' }, txSignature);
console.log(`Transaktio kelpaa: ${isValid}`);

Huomaa, että Bitcoin ja Ethereum käyttävät ECDSA:ta transaktioiden allekirjoittamiseen secp256k1-käyrällä, mutta niiden protokollat vaativat erityisen enkoodauksen ja tiivisteformaatin. Todellisessa lohkoketjusovelluksessa kannattaa käyttää erikoistuneita kirjastoja kuten bitcoinjs-lib tai ethers.js, jotka hoitavat kaiken tarvittavan formatoinnin automaattisesti. Node.js:n natiivi node:crypto tuottaa matemaattisesti oikean ECDSA-allekirjoituksen, mutta transaktiospesifinen formaatointityö kuuluu ekosysteemikirjastoille. OpenSSL:n ecparam-komento mahdollistaa secp256k1-avainten luomisen myös komentoriviltä, lisätietoa osoitteesta openssl.org/docs/man3.0/man1/openssl-ecparam.html.

Vaihe 10: Suorituskykyvertailu RSA:han

ECDSA:n merkittävin etu on suorituskyky pienemmällä avainkoolla. P-256-allekirjoitus on noin 2,7-kertaa nopeampi kuin RSA-2048-allekirjoitus, ja avainten luonti on jopa 10-kertaa nopeampaa. Tämä näkyy erityisesti TLS-kättelyissä, joissa suuri määrä lyhyitä yhteyksiä hyötyy nopeammasta allekirjoituksesta. RSA-2048 on kuitenkin vahvistuksessa nopeampi pienen julkisen eksponenttinsa (65537) vuoksi.

// Suorituskykyvertailu: ECDSA P-256 vs RSA-2048
const crypto = require('node:crypto');

function benchmark(name, fn, iterations = 500) {
  const start = process.hrtime.bigint();
  for (let i = 0; i < iterations; i++) fn();
  const end = process.hrtime.bigint();
  const totalMs = Number(end - start) / 1_000_000;
  const opsPerSec = Math.round(iterations / (totalMs / 1000));
  console.log(`${name}: ${opsPerSec.toLocaleString('fi-FI')} ops/s`);
  return opsPerSec;
}

const ecKeys = crypto.generateKeyPairSync('ec', {
  namedCurve: 'prime256v1',
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

const rsaKeys = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

const message = Buffer.from('Suorituskykytesti');
const ecSig = (() => { const s = crypto.createSign('sha256'); s.update(message); return s.sign(ecKeys.privateKey); })();
const rsaSig = (() => { const s = crypto.createSign('sha256'); s.update(message); return s.sign(rsaKeys.privateKey); })();

console.log('\n=== Allekirjoitus (ops/s, suurempi = parempi) ===');
benchmark('ECDSA P-256', () => { const s = crypto.createSign('sha256'); s.update(message); s.sign(ecKeys.privateKey); });
benchmark('RSA-2048    ', () => { const s = crypto.createSign('sha256'); s.update(message); s.sign(rsaKeys.privateKey); });

console.log('\n=== Vahvistus (ops/s, suurempi = parempi) ===');
benchmark('ECDSA P-256', () => { const v = crypto.createVerify('sha256'); v.update(message); v.verify(ecKeys.publicKey, ecSig); });
benchmark('RSA-2048    ', () => { const v = crypto.createVerify('sha256'); v.update(message); v.verify(rsaKeys.publicKey, rsaSig); });

console.log(`\nAllekirjoitusten koot:`);
console.log(`ECDSA P-256 DER: ${ecSig.length} tavua`);
console.log(`RSA-2048:        ${rsaSig.length} tavua`);

Tyypilliset tulokset Node.js 22:lla Intel Core i7 -prosessorilla: ECDSA P-256 allekirjoittaa noin 45 000-50 000 ja vahvistaa noin 15 000-18 000 operaatiota sekunnissa. RSA-2048 allekirjoittaa noin 4 000-5 000 mutta vahvistaa jopa 100 000-120 000 operaatiota sekunnissa julkisen eksponentin pienen arvon vuoksi. TLS-palvelimissa, joissa palvelin allekirjoittaa mutta asiakkaat vahvistavat, ECDSA tarjoaa selvästi paremman palvelimen suorituskyvyn. ECDSA:n allekirjoitukset ovat myös 3,6-kertaa pienempiä kuin RSA-2048:n allekirjoitukset.

Vaihe 11: Yleisimmät virheet ja ansat

ECDSA:n käyttöönotto on suoraviivainen, mutta tietyt virheet voivat johtaa tietoturva-aukkoihin tai toimintahäiriöihin. Tässä ovat yleisimmät sudenkuopat, joihin kehittäjät törmäävät Node.js-toteutuksissa.

Ansa 1: Nonce-arvon uudelleenkäyttö

ECDSA:n vakavin haavoittuvuus on nonce-arvon k uudelleenkäyttö. Jos sama k allekirjoittaa kaksi eri viestiä, yksityinen avain voidaan laskea algebrallisesti. Tämä on suora matemaattinen seuraus ECDSA:n algoritmista. Sony PlayStation 3:n firmware-allekirjoitusjärjestelmä käytti vakio-k-arvoa, mikä mahdollisti hakkereille konsolin koodiallekirjoitusjärjestelmän murtamisen vuonna 2010. Node.js:n crypto.createSign() käyttää OpenSSL:n kryptografisesti turvallista satunnaislukugeneraattoria automaattisesti, joten tätä ongelmaa ei esiinny standardia toteutusta käytettäessä. Älä koskaan yritä toteuttaa omaa k-arvon generointia.

Ansa 2: dsaEncoding-formaatin sekoittaminen

Jos allekirjoittaja käyttää DER-formaattia mutta vahvistaja odottaa IEEE P1363 -formaattia, vahvistus epäonnistuu hiljaisesti (palauttaa false). Tämä on yleisin yhteensopivuusongelma eri järjestelmien välillä. JWT-kirjastot odottavat IEEE P1363 -formaattia, mutta Node.js:n oletusarvo on DER. Varmista aina, että allekirjoittajan ja vahvistajan dsaEncoding-parametrit vastaavat toisiaan. Kun integroit kolmannen osapuolen palveluun, tarkista aina, mitä formaattia kyseinen palvelu odottaa.

VirhetilanneOireetRatkaisu
Väärä dsaEncodingverify() palauttaa false, allekirjoitus on oikeaVarmista ‘der’ tai ‘ieee-p1363’ molemmissa päissä
Väärä hajautusalgoritmiverify() palauttaa false tai OpenSSL-virheKäytä samaa sha256/sha384/sha512 molemmissa
Avain väärällä käyrälläerror:100AE081:elliptic curve routinesVarmista namedCurve on sama generointivaiheessa
PEM-muotoilu rikkierror:PEM routines:bad end lineTarkista että BEGIN/END-otsikot ovat ehjiä
Salattu avain ilman salasanaaerror:06065064:digital envelope routinesAnna passphrase createPrivateKey()-kutsussa
Väärä avaintyyppi JWT:ssäJsonWebTokenError: secretOrPublicKey is invalidKäytä EC-avainta ES256:lle, ei RSA-avainta
secp256k1 ei tuettuError: unknown group name secp256k1Päivitä OpenSSL 3.0+:aan tai tarkista käyrätuki
Suuri tiedosto blockaa tapahtumakierronPalvelin jumiutuu allekirjoituksen ajaksiKäytä streaming-rajapintaa tai generateKeyPair (async)

Väärä hajautusalgoritmi on erittäin yleinen virhe erityisesti siirryttäessä P-256:sta P-384:ään. P-256:n kanssa käytetään SHA-256:ta, P-384:n kanssa SHA-384:ää ja P-521:n kanssa SHA-512:ta. Näitä yhdistelmiä suosittelee NIST FIPS 186-5 -standardi. Voit käyttää mitä tahansa SHA-2-varianttia minkä tahansa käyrän kanssa, mutta käyrän turvallisuustaso hukkuu jos käytät liian heikkoa hajautusfunktiota. Esimerkiksi P-521 + SHA-256 toimii teknisesti, mutta SHA-256:n 128-bittinen tuloste rajoittaa kokonaisturvallisuuden 128 bittiin vaikka käyrä tarjoaisi 256-bittisen turvallisuuden.

Vaihe 12: Turvallisuuden parhaat käytännöt

ECDSA:n turvallinen käyttö edellyttää useita lisävarotoimia avaingeneroinnin ja allekirjoitusprosessin lisäksi. Seuraavat käytännöt ovat alan standardeja vuoden 2026 suosituksiin perustuen.

Avainten kierrätys: Rotoi ECDSA-avaimet säännöllisesti. TLS-sertifikaateille suositellaan enintään 1 vuoden voimassaoloaikaa (Let’s Encrypt käyttää 90 päivää). JWT-avaimille suositellaan kierrätystä 6-12 kuukauden välein. Toteuta JWKS-päätepiste, jotta asiakkaat voivat automaattisesti noutaa uudet julkiset avaimet ilman palvelukatkosta.

Käytä P-256:ta oletuksena: P-256 on turvallinen, nopea ja yhteensopiva kaikkien modernien järjestelmien kanssa. P-384 ja P-521 ovat tarpeellisia vain tiukkimman turvallisuusluokituksen sovelluksiin, joissa kvanttilaskentaresistanssi on tärkeintä. Valitse käyrä sovelluksesi turvallisuusvaatimusten perusteella, ei “suurempi on aina parempi” -periaatteella.

Älä toteuta omaa kryptografiaa: Käytä aina Node.js:n sisäänrakennettua node:crypto-moduulia tai vakiintuneita kirjastoja. Oman ECDSA-toteutuksen kirjoittaminen on erittäin vaarallista, koska pienikin virhe nonce-generoinnissa tai pistearitmeetiikassa voi johtaa yksityisen avaimen paljastumiseen.

Validoi allekirjoitukset aina palvelimella: Älä luota asiakaspuolen allekirjoitusvalidointiin yksin. Palvelin tulee aina vahvistaa allekirjoituksen aitous, vaikka asiakas tarkistaisi sen myös omalta puoleltaan.

NIST FIPS 186-5 -standardi osoitteessa csrc.nist.gov/publications/detail/fips/186/5/final on ensisijainen viite ECDSA-käyrän valinnassa viranomaiskäyttöön. Suomessa Kyberturvallisuuslain 124/2025 noudattavissa järjestelmissä P-256 tai P-384 on suositeltava minimitaso kryptografisille allekirjoituksille.

Vianmääritys: 8 yleistä ongelmaa

ECDSA-toteutuksissa törmää toistuvasti samoihin ongelmiin. Tässä kattava lista ratkaisuineen.

Ongelma 1: “error:1E08010C:DECODER routines::unsupported”
Syy: Yritit ladata yksityistä avainta väärällä formaatilla tai avain on vahingoittunut. Ratkaisu: Tarkista, että PEM-tiedosto alkaa -----BEGIN PRIVATE KEY----- (PKCS8) eikä -----BEGIN EC PRIVATE KEY----- (SEC1, vanhempi formaatti). Node.js suosii PKCS8-formaattia, ja crypto.generateKeyPairSync tuottaa PKCS8:aa oletuksena kun käytetään type: 'pkcs8'.

Ongelma 2: verify() palauttaa aina false
Syy: Yleisimmin väärä dsaEncoding. Allekirjoitus luotiin DER-formaatissa mutta vahvistus odottaa IEEE P1363 -formaattia tai päinvastoin. Ratkaisu: Lisää dsaEncoding: 'der' sekä sign.sign()- että verify.verify()-kutsuihin. Tai jätä se pois molemmista, jolloin oletuksena käytetään DER:ää molemmissa.

Ongelma 3: “JsonWebTokenError: invalid algorithm”
Syy: Yrität käyttää EC-avainta HS256-algoritmin kanssa tai RSA-avainta ES256:n kanssa. Ratkaisu: ES256 vaatii EC-avaimen P-256-käyrällä. Luo avain namedCurve: 'prime256v1' -parametrilla ja käytä algorithm: 'ES256' jwt.sign()-kutsussa. ES384 vaatii P-384-käyrän, ES512 vaatii P-521-käyrän.

Ongelma 4: “Error: unknown group name secp256k1”
Syy: OpenSSL-versio ei tue secp256k1:ää tai nimi on kirjoitettu väärin. Ratkaisu: Tarkista openssl ecparam -list_curves | grep secp256k1. Node.js:n virallisissa binary-julkaisuissa secp256k1 on tuettu, mutta joissakin Linux-jakeluissa OpenSSL on käännetty ilman sitä.

Ongelma 5: Salattu PEM-avain ei avaudu
Syy: Väärä salasana tai salasanaa ei annettu. Ratkaisu: Kun avain on salattu, latauksessa täytyy antaa passphrase: crypto.createPrivateKey({ key: pem, passphrase: 'salasana' }). Tarkista myös, että passphrase on täsmälleen sama merkkijono jolla salaus tehtiin, mukaan lukien isot/pienet kirjaimet.

Ongelma 6: Allekirjoitus vaihtelee joka kerta
Syy: ECDSA on oletusarvoinen ei-deterministinen. Tämä on odotettua ja oikeaa toimintaa, koska algoritmi käyttää satunnaista nonce-arvoa. Ratkaisu: Tämä on normaali toiminta. Jos tarvitset deterministisiä allekirjoituksia testaukseen tai auditointiin, käytä Ed25519:ää tai RFC 6979 -yhteensopivaa kirjastoa.

Ongelma 7: “error:100AE081:elliptic curve routines::group2pkparameters failure”
Syy: Käyrä ei täsmää avaimen sisäisen käyrän kanssa. Ratkaisu: Tarkista avaimen käyrä: node -e "const k = require('crypto').createPrivateKey(require('fs').readFileSync('avain.pem')); console.log(k.asymmetricKeyDetails)". Varmista, että namedCurve vastaa sitä mitä käytät allekirjoituksessa.

Ongelma 8: Suorituskyky on huono suurilla tiedostoilla
Syy: Koko tiedoston lataaminen muistiin ennen allekirjoitusta kuluttaa muistia ja aikaa. Ratkaisu: Käytä streaming-rajapintaa: const sign = crypto.createSign('sha256'); readStream.pipe(sign); sign.on('finish', () => { const sig = sign.sign(key); });. Näin tiedoston tiivistys tapahtuu virtauksena ilman koko tiedoston lataamista muistiin kerralla.

Edistyneet tekniikat

Kun perusvaiheet ovat hallussa, seuraavat edistyneet tekniikat parantavat ECDSA-toteutuksesi turvallisuutta, yhteensopivuutta ja ylläpidettävyyttä.

Avainten luominen OpenSSL-komentoriviltä: Voit luoda ECDSA-avaimet myös OpenSSL:llä, mikä on hyödyllistä skriptauksessa ja CI/CD-putkissa. Komento openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out private.pem luo PKCS8-formaatissa olevan P-256-yksityisen avaimen. Julkinen avain saadaan komennolla openssl pkey -in private.pem -pubout -out public.pem. Node.js voi lukea suoraan OpenSSL:n tuottamat PEM-tiedostot ilman muunnoksia.

JWKS-päätepiste avainten julkaisemiseen: Tuotantosovelluksissa julkiset avaimet tulisi julkaista JWKS (JSON Web Key Set) -päätepisteenä, josta asiakkaat voivat automaattisesti noutaa uudet avaimet avainrotaation yhteydessä. Toteuta GET /.well-known/jwks.json -päätepiste, joka palauttaa avaimet JWK-muodossa. Node.js:n crypto.createPublicKey(pem).export({ format: 'jwk' }) tuottaa JWK-formaatin automaattisesti ilman lisäkirjastoja.

Avaintunnisteet avainrotaatioon: Käytä kid-kenttiä JWT-otsikossa osoittamaan, millä avaimella token on allekirjoitettu. Tämä mahdollistaa useiden aktiivisten avainten käytön samanaikaisesti, mikä on kriittistä sujuvan avainrotaation toteuttamiselle ilman palvelukatkosta. Pidä kaksi rinnakkaista aktiivista avainta siirtymävaiheessa: vanha vahvistaa vanhat tokenit, uusi allekirjoittaa uudet.

Web Crypto API -vaihtoehto: Node.js 16+ sisältää Web Crypto API:n (globalThis.crypto.subtle), joka on selain-yhteensopiva rajapinta. Tämä mahdollistaa saman koodin käytön sekä Node.js:ssä että selaimessa: crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']). Web Crypto API on asynkroninen, mikä sopii paremmin Node.js:n tapahtumapohjaiseen arkkitehtuuriin kuin synkroniset generateKeyPairSync-kutsut. Lue lisää Node.js WebCrypto API -oppaastamme.

Allekirjoitusten tallentaminen tietokantaan: Base64url-enkoodaus on suositeltava formaatti allekirjoitusten tallentamiseen tietokantaan tai siirtämiseen HTTP-otsikoissa. Muunna allekirjoitus Base64url-muotoon: signature.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''). Tämä tuottaa URL-turvallisen merkkijonon ilman täytösmerkejä, joka toimii suoraan JWT-tokenin allekirjoitusosana.

Täydellinen projekti: Tiedostojen allekirjoitusjärjestelmä

Kootaan kaikki vaiheet yhteen toimivaksi tiedostojen allekirjoitusjärjestelmäksi, joka luo, allekirjoittaa ja vahvistaa tiedostoja ECDSA P-256:lla. Tämä on suoraan tuotantoon soveltuva pohjarakenne, jota voit laajentaa omien tarpeidesi mukaan.

// src/file-signer.js - Täydellinen tiedostojen allekirjoitusjärjestelmä
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');

class ECDSAFileSigner {
  constructor(curve = 'prime256v1') {
    this.curve = curve;
    this.hashAlgorithm = curve === 'secp521r1' ? 'sha512' :
                         curve === 'secp384r1' ? 'sha384' : 'sha256';
  }

  generateKeyPair(outputDir = '.') {
    const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
      namedCurve: this.curve,
      publicKeyEncoding: { type: 'spki', format: 'pem' },
      privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
    });

    const privPath = path.join(outputDir, 'signing-key.pem');
    const pubPath = path.join(outputDir, 'signing-key.pub.pem');
    fs.writeFileSync(privPath, privateKey, { mode: 0o600 });
    fs.writeFileSync(pubPath, publicKey);
    console.log(`Avainpari luotu käyrällä ${this.curve}`);
    return { privateKey, publicKey };
  }

  signFile(filePath, privateKeyPem) {
    const fileData = fs.readFileSync(filePath);
    const sign = crypto.createSign(this.hashAlgorithm);
    sign.update(fileData);
    const signature = sign.sign({ key: privateKeyPem, dsaEncoding: 'der' });

    const metadata = {
      algorithm: 'ECDSA',
      curve: this.curve,
      hash: this.hashAlgorithm,
      encoding: 'der',
      timestamp: new Date().toISOString(),
      fileSize: fileData.length,
      signature: signature.toString('base64')
    };

    const sigPath = filePath + '.sig';
    fs.writeFileSync(sigPath, JSON.stringify(metadata, null, 2));
    console.log(`Allekirjoitettu: ${sigPath}`);
    return sigPath;
  }

  verifyFile(filePath, sigPath, publicKeyPem) {
    const fileData = fs.readFileSync(filePath);
    const metadata = JSON.parse(fs.readFileSync(sigPath, 'utf8'));
    const signature = Buffer.from(metadata.signature, 'base64');

    const verify = crypto.createVerify(metadata.hash);
    verify.update(fileData);
    const isValid = verify.verify(
      { key: publicKeyPem, dsaEncoding: metadata.encoding },
      signature
    );

    console.log(`Tiedosto: ${filePath}`);
    console.log(`Algoritmi: ${metadata.algorithm} ${metadata.curve} + ${metadata.hash.toUpperCase()}`);
    console.log(`Allekirjoitusaika: ${metadata.timestamp}`);
    console.log(`Kelpaa: ${isValid ? 'KYLLÄ' : 'EI'}`);
    return isValid;
  }
}

// Käyttöesimerkki
const signer = new ECDSAFileSigner('prime256v1');
const { privateKey, publicKey } = signer.generateKeyPair('src/keys');

const testFile = 'src/testdoc.txt';
fs.writeFileSync(testFile, 'Sähköisesti allekirjoitettu asiakirja\nPäivämäärä: 2026-06-19');

const sigFile = signer.signFile(testFile, privateKey);
console.log('\n--- Vahvistus (alkuperäinen) ---');
signer.verifyFile(testFile, sigFile, publicKey);

// Testaa manipulointi
fs.appendFileSync(testFile, '\nManipuloitu lisäys');
console.log('\n--- Vahvistus (manipuloitu) ---');
signer.verifyFile(testFile, sigFile, publicKey);

Tämä täydellinen projekti osoittaa ECDSA:n käytännön sovelluksen: tiedostojen eheyden varmistamisen. Allekirjoitusmetadata tallennetaan JSON-formaatissa, mikä tekee siitä helposti tarkistettavan ja yhteensopivan muiden järjestelmien kanssa. Sama lähestymistapa toimii ohjelmistojulkaisujen allekirjoittamiseen, API-pyyntöjen todentamiseen ja asiakirjojen sähköiseen allekirjoittamiseen.

Avainten rotaatio tuotannossa

Avainten rotaatio on yksi tärkeimmistä käytännöistä ECDSA-järjestelmien ylläpidossa. Oikein toteutettuna rotaatio ei aiheuta palvelukatkosta, koska vanhat tokenit pysyvät voimassa kunnes ne vanhentuvat luonnollisesti. Tässä on käytännön strategia avainrotaatiolle JWT-pohjaisessa sovelluksessa.

Rotaatiostrategia perustuu kolmeen vaiheeseen. Ensimmäisessä vaiheessa luot uuden avainparin ja julkaiset sen JWKS-päätepisteessä vanhan avaimen rinnalle. Molemmat avaimet ovat aktiivisia samanaikaisesti, ja JWT-otsikon kid-kenttä kertoo validaattorille, kumpaa avainta käytetään vahvistukseen. Toisessa vaiheessa uudet tokenit allekirjoitetaan uudella avaimella, mutta vanhat tokenit validoidaan edelleen vanhalla avaimella. Kolmannessa vaiheessa, kun kaikki vanhat tokenit ovat vanhentuneet (tyypillisesti 1-24 tuntia riippuen tokenin eliniästä), vanha avain poistetaan JWKS-päätepisteestä.

Automaattisen avainrotaation toteuttaminen Node.js:ssä on suoraviivaista. Tallenna avaimet tietokantaan tai Redis-välimuistiin yhdessä metadata-tietojen kuten luomisajan ja käytöstäpoistoajan kanssa. Rotaatioskripti luo uuden avainparin, merkitsee vanhan avaimen “poistumassa”-tilaan ja päivittää JWKS-päätepisteen vastauksen. Kun poistumisaika saavutetaan, avain poistetaan pysyvästi tallennustilasta.

Avainrotaation laiminlyönnillä on vakavia seurauksia. Jos yksityinen avain vaarantuu, kaikki sillä allekirjoitetut tokenit voivat paljastaa väärinkäyttöön. Tällöin sinun täytyy tehdä hätärotaatio: luo uusi avainpari, poista vanha avain välittömästi, ja mitätöi kaikki olemassa olevat tokenit pakottamalla käyttäjät kirjautumaan uudelleen. Hätärotaation protokollan tulisi olla dokumentoitu ja testattu ennen kuin sitä tarvitaan kriisitilanteessa.

ECDSA vs muut allekirjoitusalgoritmit

Käyrän ja algoritmin valinta riippuu sovelluksen tarpeista, yhteensopivuusvaatimuksista ja suorituskykyprioriteeteista. Tässä selkeä vertailu yleisimmistä vaihtoehdoista.

AlgoritmiAllekirjoitusnopeusAllekirjoituksen kokoKäyttökohteetSuositus 2026
ECDSA P-256~45 000 ops/s70-72 tavua (DER)TLS, JWT ES256, X.509Hyvä yleiskäyttöön
Ed25519~120 000 ops/s64 tavuaSSH, Signal, TLS 1.3Paras uusiin sovelluksiin
RSA-2048~5 000 ops/s256 tavuaVanha infrastruktuuriVältä uusissa projekteissa
RSA-4096~800 ops/s512 tavuaKorkean turvallisuuden PKIVältä, käytä P-384:ää
ECDSA secp256k1~40 000 ops/s70-72 tavua (DER)Bitcoin, EthereumVain lohkoketjuihin

Ed25519 on vuoden 2026 paras valinta uusiin sovelluksiin, joissa ei ole erityistä vaatimusta NIST-käyrille. ECDSA on parempi valinta silloin, kun tarvitset X.509-sertifikaattiyhteensopivuutta, JWT-standardien mukaisia ES256/ES384-algoritmeja tai Bitcoin/Ethereum-yhteensopivuutta secp256k1-käyrällä. Lue vertailumme Ed25519-allekirjoituksista Node.js:ssä löytääksesi lisää tietoa EdDSA-vaihtoehdosta. Kryptografisia hajautusfunktioita käsittelemme syvällisesti BLAKE3-hajautus Node.js:ssä -oppaassamme.

Usein kysytyt kysymykset

Mikä on ECDSA:n ja Ed25519:n tärkein ero?
ECDSA käyttää NIST-käyriä (P-256, P-384, P-521) Weierstrass-muodossa ja vaatii erillisen tiivisteoperaation ennen allekirjoitusta. Ed25519 käyttää Edwards25519-käyrää ja allekirjoittaa raakadatan suoraan. Ed25519 on noin 2-3 kertaa nopeampi ja vastustuskykyisempi sivukanava-hyökkäyksille. ECDSA:lla on parempi yhteensopivuus vanhojen järjestelmien ja X.509-sertifikaattien kanssa.

Mikä ECDSA-käyrä on turvallisin?
P-521 tarjoaa korkeimman turvallisuustason (256-bittinen), mutta P-256 riittää käytännössä kaikille sovelluksille vuoteen 2030 ja todennäköisesti pidemmälle. NIST suosittelee P-256:ta tai P-384:ää yleiskäyttöön. P-521:ä suositellaan vain kaikista korkeimmille turvallisuusvaatimuksille, kuten pitkäaikaisille asiakirja-allekirjoituksille, joiden on oltava voimassa vuosikymmenien kuluttua.

Voiko ECDSA:n yksityinen avain vuotaa?
Kyllä, mutta vain erityistilanteissa: nonce-arvon k uudelleenkäyttö tai heikko satunnaislukugeneraattori voi paljastaa yksityisen avaimen algebrallisella laskennalla. Node.js:n virallinen toteutus käyttää OpenSSL:n kryptografista satunnaislukugeneraattoria, joka on turvallinen. Älä koskaan toteuta omaa k-arvon generointia.

Miten ECDSA toimii TLS-sertifikaateissa?
TLS 1.3 suosii ECDSA-sertifikaatteja, koska niiden pienempi koko nopeuttaa TLS-kättelyä. Let’s Encrypt myöntää oletuksena RSA-sertifikaatteja mutta tukee myös ECDSA:ta. ECDSA-sertifikaattipyyntö (CSR): openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -keyout key.pem -out csr.pem -nodes.

Onko ECDSA kvanttiresistentti?
Ei. Kaikki elliptisiin käyriin perustuvat algoritmit ovat haavoittuvaisia Shorin algoritmille, jota riittävän suuri kvanttitietokone voisi ajaa. NIST standardisoi kvanttiturvallisia algoritmeja (CRYSTALS-Dilithium allekirjoituksille), jotka korvaavat ECDSA:n pitkällä aikavälillä. Lue lisää postkvanttikryptografiasta.

Miksi allekirjoitukseni ei kelpaa eri kielellä toteutetussa järjestelmässä?
Todennäköisin syy on eri dsaEncoding-formaatti. Java ja monet muut kielet käyttävät DER-formaattia oletuksena, mutta jotkut JavaScript-kirjastot odottavat IEEE P1363 -formaattia. Tarkista kohdekirjaston dokumentaatio ja varmista, että molemmat järjestelmät käyttävät samaa formaattia.

Pitäisikö käyttää node:crypto vai Web Crypto API:ta?
Node.js-sovelluksiin, jotka eivät tarvitse selainyhteensopivuutta, node:crypto on suositeltavampi sen synkronisten API-kutsujen ja laajemman ominaisuusjoukon vuoksi. Jos kirjoitat isomorfista koodia, joka toimii sekä Node.js:ssä että selaimessa, käytä Web Crypto API:ta (globalThis.crypto.subtle). Node.js 18+ tukee täyttä Web Crypto API:ta ilman erillisiä paketteja.

Miten testaan ECDSA-toteutukseni kattavasti?
Testaa vähintään: 1) allekirjoitus onnistuu oikealla avaimella, 2) vahvistus onnistuu oikealla julkisella avaimella, 3) vahvistus epäonnistuu väärällä viestillä, 4) vahvistus epäonnistuu väärällä julkisella avaimella, 5) vahvistus epäonnistuu muutetulla allekirjoituksella, 6) suuret tiedostot toimivat streaming-rajapinnalla, 7) avainten tallennus ja lataaminen toimii oikein salattuina avaimina. Lisää yksikkötestit kaikille virheskenaarioille, joita kuvataan vianmääritys-osiossa.

Miten ECDSA-allekirjoitukset eroavat sähköisestä allekirjoituksesta?
ECDSA on kryptografinen primitiivi, sähköinen allekirjoitus on juridinen ja tekninen kokonaisuus. Euroopan unionin eIDAS-asetus määrittelee kolme sähköisen allekirjoituksen tasoa: tavallinen, edistyksellinen (AdES) ja hyväksytty (QES). ECDSA P-256 tai P-384 muodostaa teknisen perustan edistykselliselle sähköiselle allekirjoitukselle, mutta juridisesti pätevä hyväksytty allekirjoitus vaatii lisäksi akkreditoidun luottamuspalveluntarjoajan myöntämän sertifikaatin. Suomessa hyväksyttyjä sähköisiä allekirjoituksia myöntää Väestörekisterikeskus DVV:n kautta.

Miksi ECDSA:ssa tarvitaan niin tarkka hajautusalgoritmin valinta?
ECDSA:n turvallisuustaso on matematiikassa kiinteästi sidottu sekä käyrän bittipituuteen että käytettyyn hajautusfunktioon. Jos käyrä tarjoaa 128-bittisen turvallisuuden (P-256) mutta hajautusfunktio tuottaa 256-bittisen tiivisteen (SHA-256), kokonaisturvallisuus pysyy 128 bitissä, koska käyrä on pullonkaula. Vastaavasti, jos käyrä tarjoaa 256-bittisen turvallisuuden (P-521) mutta käytät SHA-256:ta (128-bittinen turvallisuus), kokonaisturvallisuus tippuu 128 bittiin. Tämän vuoksi NIST FIPS 186-5 suosittelee parittamaan P-256:n SHA-256:n, P-384:n SHA-384:n ja P-521:n SHA-512:n kanssa, jotta turvallisuustasot vastaavat toisiaan molemmissa komponenteissa.

Aiheeseen liittyvää