Post-quantum salaus ei ole enää tulevaisuuden projekti. NIST viimeisteli elokuussa 2024 kolme kvanttiresistenttiä standardia (FIPS 203, 204 ja 205), ja EU:n Cyber Resilience Act astui voimaan 1. kesäkuuta 2026. Tässä oppaassa rakennat täysin toimivan post-quantum salausjärjestelmän Node.js:llä: ML-KEM-768-avainten kapselointi, ML-DSA-65-allekirjoitukset, hybridisalaus klassisen X25519:n kanssa ja AES-256-GCM-suojattu viestintä. Kaksitoista vaihetta, noin 45 minuuttia, yksi npm-paketti.

Oppaan koodi perustuu @noble/post-quantum-kirjastoon, joka on ainoa aktiivisesti ylläpidetty ja tarkastettu JavaScript-toteutus NIST:n standardoimille ML-KEM- ja ML-DSA-algoritmeille. Node.js v22 tai uudempi riittää, eikä natiivia PQC-tukea tarvita. Päivitetty 21. kesäkuuta 2026.

Mitä post-quantum salaus tarkoittaa ja miksi se on kiireellinen 2026

Nykyiset asymmetriset algoritmit, kuten RSA ja ECDH, perustuvat matemaattisiin ongelmiin (kokonaislukujen tekijöihinjako ja diskreetti logaritmi), jotka klassinen tietokone ratkaisee hitaasti mutta riittävän tehokas kvanttitietokone Shorin algoritmilla murtaa polynomisessa ajassa. ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) ja ML-DSA (Module-Lattice-Based Digital Signature Algorithm) rakentuvat sen sijaan hilaperusteisille ongelmille, joihin ei tunneta kvanttitietokoneen hyökkäystä.

Käytännön uhka on ns. “korjaa nyt, pura myöhemmin” (harvest now, decrypt later, HNDL): hyökkääjät tallentavat tänään salatun liikenteen ja purkavat sen, kun riittävän tehokas kvanttitietokone on saatavilla. Arkaluonteisten tietojen, kuten terveysdatan, rahoitustransaktioiden ja hallintaviestinnän, salaussuoja on uusittava ennen kvanttiaikakautta. NIST:n suosituksen mukaan RSA:n ja ECDH:n käytöstä tulisi luopua vuoteen 2030 mennessä. EU:n CRA vaatii lisäksi dokumentoitua kryptografista inventaariota kaikilta digitaalisia tuotteita valmistaavilta yrityksiltä viimeistään syyskuuhun 2026 mennessä.

StandardiAlgoritmiLähtökohtaKäyttötarkoitusHyväksytty
FIPS 203ML-KEMCRYSTALS-KyberAvainten kapselointi (KEM)Elokuu 2024
FIPS 204ML-DSACRYSTALS-DilithiumDigitaaliset allekirjoituksetElokuu 2024
FIPS 205SLH-DSASPHINCS+Allekirjoitukset (tilaton)Elokuu 2024
FIPS 206FN-DSAFALCONAllekirjoitukset (pieni koko)Odottaa 2026

Vaatimukset: ohjelmistot ja versiot

Ennen aloittamista tarkista, että seuraavat ohjelmistot ovat asennettuina:

OhjelmistoVaadittu versioSuositeltu versioHuomio
Node.js18.0+22 LTS tai 24ESM-tuki vaaditaan
npm8.0+10+package.json type: module
@noble/post-quantum0.2+Uusin versioAinoa tarkastettu JS-toteutus
KäyttöjärjestelmäLinux / macOS / WindowsUbuntu 22.04 LTSEi käyttöjärjestelmäriippuvuuksia

Tarkista Node.js-versio:

node --version
# Vaaditaan: v18.0.0 tai uudempi

npm --version
# Vaaditaan: 8.0.0 tai uudempi

Vaihe 1: Projektin alustus ja riippuvuuksien asennus

Luo uusi Node.js-projekti. Projekti käyttää ES-moduuleja, koska @noble/post-quantum on kirjoitettu modernilla ESM-syntaksilla. Voit käyttää CommonJS:ää (require), mutta suositeltava tapa on ESM:

mkdir pqc-demo && cd pqc-demo
npm init -y

# Aseta ESM-moodi
npm pkg set type=module

# Asenna @noble/post-quantum
npm install @noble/post-quantum

# Tarkista asennus
node -e "import('@noble/post-quantum/ml-kem.js').then(m => console.log('ML-KEM OK:', Object.keys(m)))"

Onnistuneen asennuksen jälkeen konsoli tulostaa ML-KEM:n API-pintarakenteet: mlkem512, mlkem768 ja mlkem1024. Näistä ML-KEM-768 on NIST:n suosittelema yleiskäyttöinen turvatasoksi, joka vastaa noin 192-bittistä klassista turvallisuutta. ML-KEM-512 sopii resurssirajoitteisiin ympäristöihin, ML-KEM-1024 korkean turvallisuuden tarpeisiin.

Tiedostorakenne, jonka rakennat tämän oppaan aikana:

pqc-demo/
├── package.json
├── keygen.js          # Avainparin luonti (vaihe 2)
├── kem-demo.js        # KEM-kapselointi ja purku (vaiheet 3-4)
├── dsa-demo.js        # ML-DSA allekirjoitukset (vaihe 5)
├── hybrid.js          # Hybridisalaus XWing (vaihe 6)
├── encrypt.js         # AES-256-GCM-salaus (vaihe 7)
├── keystore.js        # Avainten tallennus (vaihe 8)
├── server.js          # HTTP-palvelin (vaihe 9)
└── rotate.js          # Avainten kierto (vaihe 10)

Vaihe 2: ML-KEM-768-avainparin luonti

ML-KEM ei ole salausalgoritmi vaan avaintenkapselointimekanismi (KEM). Se mahdollistaa kahden osapuolen sopia yhteinen jaettu salaisuus (shared secret) julkisen kanavan kautta ilman, että itse salaisuus välitetään suoraan. Prosessi eroaa RSA:sta: vastaanottaja luo avainparin, lähettäjä kapseloi jaetun salaisuuden julkisella avaimella, vastaanottaja purkaa sen yksityisellä avaimella.

Luo tiedosto keygen.js:

// keygen.js
import { mlkem768 } from '@noble/post-quantum/ml-kem.js';
import { writeFileSync } from 'node:fs';

// Generoi avainpari
const { publicKey, secretKey } = mlkem768.keygen();

console.log('ML-KEM-768 avainpari luotu:');
console.log('  Julkinen avain:  ', publicKey.length, 'tavua');
console.log('  Yksityinen avain:', secretKey.length, 'tavua');

// Tallenna avaimet (kehitysympäristö; tuotannossa käytä keystore.js:ää)
writeFileSync('mlkem768-public.key',  Buffer.from(publicKey));
writeFileSync('mlkem768-secret.key',  Buffer.from(secretKey));

console.log('Avaimet tallennettu: mlkem768-public.key, mlkem768-secret.key');
node keygen.js
# Tuloste:
# ML-KEM-768 avainpari luotu:
#   Julkinen avain:   1184 tavua
#   Yksityinen avain: 2400 tavua
# Avaimet tallennettu: mlkem768-public.key, mlkem768-secret.key

Huomaa avainten koot verrattuna klassisiin algoritmeihin. ML-KEM-768:n julkinen avain (1 184 tavua) on suurempi kuin RSA-2048:n (256-tavuinen modulus), mutta pidempi turvalisuusmarginaali kattaa kasvun:

AlgoritmiJulkinen avainYksityinen avainSalakirjoitustekstiKvanttiturva
RSA-2048256 tavua1 192 tavua256 tavuaEi
X2551932 tavua32 tavua32 tavuaEi
ML-KEM-512800 tavua1 632 tavua768 tavuaKyllä (128-bit)
ML-KEM-7681 184 tavua2 400 tavua1 088 tavuaKyllä (192-bit)
ML-KEM-10241 568 tavua3 168 tavua1 568 tavuaKyllä (256-bit)

Vaihe 3: Avaimen kapselointi (encapsulate)

Lähettäjä kapseloi jaetun salaisuuden vastaanottajan julkisella avaimella. Kapselointi tuottaa kaksi arvoa: ciphertext (salakirjoitusteksti, joka lähetetään vastaanottajalle) ja sharedSecret (32-tavuinen jaettu salaisuus, jota käytetään symmetriseen salaukseen). Lähettäjä ei koskaan näe vastaanottajan yksityistä avainta.

// kem-demo.js - Lähettäjän puoli
import { mlkem768 } from '@noble/post-quantum/ml-kem.js';
import { readFileSync } from 'node:fs';

// Lataa vastaanottajan julkinen avain
const publicKey = new Uint8Array(readFileSync('mlkem768-public.key'));

// Kapseloi jaettu salaisuus
const { ciphertext, sharedSecret: senderSecret } = mlkem768.encapsulate(publicKey);

console.log('Kapselointi onnistui:');
console.log('  Salakirjoitusteksti:', ciphertext.length, 'tavua');
console.log('  Jaettu salaisuus:   ', senderSecret.length, 'tavua');
console.log('  Salaisuus (hex):    ', Buffer.from(senderSecret).toString('hex').slice(0, 32) + '...');

// Lähettäjä lähettää ciphertextin vastaanottajalle turvallisen kanavan kautta
export { ciphertext, senderSecret };

Vaihe 4: Avaimen purku (decapsulate)

Vastaanottaja purkaa salakirjoitustekstin yksityisellä avaimellaan ja saa saman jaetun salaisuuden kuin lähettäjä. Jos purkaminen onnistuu oikein, molemmat osapuolet jakavat identtisen 32-tavuisen arvon. Tätä arvoa käytetään symmetrisen salausavaimen johtamiseen HKDF:llä.

// kem-demo.js (jatkuu) - Vastaanottajan puoli
import { mlkem768 } from '@noble/post-quantum/ml-kem.js';
import { readFileSync } from 'node:fs';

async function demonstrateKEM() {
  // Luo avainpari (vastaanottaja)
  const { publicKey, secretKey } = mlkem768.keygen();

  // Kapseloi (lähettäjä)
  const { ciphertext, sharedSecret: senderSecret } = mlkem768.encapsulate(publicKey);

  // Pura (vastaanottaja)
  const receiverSecret = mlkem768.decapsulate(ciphertext, secretKey);

  // Vertaa jaettuja salaisuuksia
  const match = Buffer.from(senderSecret).equals(Buffer.from(receiverSecret));
  console.log('Jaetut salaisuudet täsmäävät:', match);
  console.log('Lähettäjän salaisuus: ', Buffer.from(senderSecret).toString('hex'));
  console.log('Vastaanottajan salaisuus:', Buffer.from(receiverSecret).toString('hex'));
}

demonstrateKEM();
node kem-demo.js
# Tuloste:
# Jaetut salaisuudet täsmäävät: true
# Lähettäjän salaisuus:  a3f82c1b...
# Vastaanottajan salaisuus: a3f82c1b...

Vaihe 5: ML-DSA-65-digitaaliset allekirjoitukset

ML-KEM hoitaa avaintenvaihdon, mutta se ei todenna viestinnän osapuolia. Todennus tarvitsee allekirjoitusalgoritmin. ML-DSA-65 (FIPS 204) vastaa noin 128 bitin klassista turvallisuustasoa ja on suositeltu yleiskäyttöinen valinta. Allekirjoitus on 3 293 tavua, mikä on huomattavasti enemmän kuin Ed25519:n 64 tavua, mutta algoritmi on kvanttiresistentti.

// dsa-demo.js
import { mldsa65 } from '@noble/post-quantum/ml-dsa.js';

// Luo allekirjoitusavainpari
const { publicKey, secretKey } = mldsa65.keygen();

console.log('ML-DSA-65 avainpari:');
console.log('  Julkinen avain: ', publicKey.length, 'tavua');
console.log('  Yksityinen avain:', secretKey.length, 'tavua');

// Allekirjoita viesti
const viesti = new TextEncoder().encode('Tämä on kvanttiresistentti allekirjoitettu viesti.');

const allekirjoitus = mldsa65.sign(viesti, secretKey);
console.log('  Allekirjoitus:  ', allekirjoitus.length, 'tavua');

// Varmenna allekirjoitus
const onkoValidi = mldsa65.verify(allekirjoitus, viesti, publicKey);
console.log('Allekirjoitus validi:', onkoValidi);

// Testaa muuttuneella viestillä (pitää epäonnistua)
const muutettuViesti = new TextEncoder().encode('Tämä viesti on muutettu.');
const onkoMuutettuValidi = mldsa65.verify(allekirjoitus, muutettuViesti, publicKey);
console.log('Muutetun viestin allekirjoitus validi:', onkoMuutettuValidi);
node dsa-demo.js
# Tuloste:
# ML-DSA-65 avainpari:
#   Julkinen avain:  1952 tavua
#   Yksityinen avain: 4032 tavua
#   Allekirjoitus:   3293 tavua
# Allekirjoitus validi: true
# Muutetun viestin allekirjoitus validi: false
AlgoritmiJulkinen avainYksityinen avainAllekirjoitusKvanttiturva
Ed2551932 tavua64 tavua64 tavuaEi
ECDSA P-25664 tavua32 tavua71 tavuaEi
ML-DSA-441 312 tavua2 560 tavua2 420 tavuaKyllä
ML-DSA-651 952 tavua4 032 tavua3 293 tavuaKyllä
ML-DSA-872 592 tavua4 896 tavua4 595 tavuaKyllä

Vaihe 6: Hybridisalaus XWing-menetelmällä

Siirtymävaiheen paras käytäntö on hybridisalaus: yhdistä klassinen X25519 ja ML-KEM-768 siten, että yhteys pysyy turvallisena, vaikka toinen algoritmeista murtuu. @noble/post-quantum sisältää valmiin XWing-hybridin (ML-KEM-768 + X25519), joka on IETF-luonnoksena standardoitavana. XWing yhdistää molempien algoritmien jaetut salaisuudet HKDF:llä yhdeksi avainmateriaaliksi.

// hybrid.js
import { XWing } from '@noble/post-quantum/hybrids.js';

// Vastaanottaja luo avainparin
const { publicKey, secretKey } = XWing.keygen();

console.log('XWing (ML-KEM-768 + X25519) avainpari:');
console.log('  Julkinen avain: ', publicKey.length, 'tavua');
console.log('  Yksityinen avain:', secretKey.length, 'tavua');

// Lähettäjä kapseloi
const { ciphertext, sharedSecret: lahettajanSalaisuus } = XWing.encapsulate(publicKey);

console.log('  Salakirjoitusteksti:', ciphertext.length, 'tavua');
console.log('  Jaettu salaisuus:   ', lahettajanSalaisuus.length, 'tavua');

// Vastaanottaja purkaa
const vastaanottajanSalaisuus = XWing.decapsulate(ciphertext, secretKey);

const tasmaavat = Buffer.from(lahettajanSalaisuus).equals(Buffer.from(vastaanottajanSalaisuus));
console.log('Hybridisalaisuudet täsmäävät:', tasmaavat);

XWing on suositeltu valinta käytännön toteutuksiin, koska se tarjoaa suojan sekä nykyisiä hyökkäyksiä (X25519:n kautta) että kvanttiuhkia (ML-KEM-768:n kautta) vastaan. Jos ML-KEM-768:ssa löytyy heikkous, X25519 suojaa edelleen, ja päinvastoin.

Vaihe 7: HKDF-johtaminen ja AES-256-GCM-salaus

ML-KEM:n jaettu salaisuus on 32 tavua satunnaisuutta, mutta suoraan käytettynä salausavaimena se ei ole suositeltu. HKDF (HMAC-based Key Derivation Function) johtaa tästä materiaalista yhden tai useamman avainkerroksen turvallisesti. Tämä salauskerros toteutetaan Node.js:n sisäänrakennetulla node:crypto-moduulilla.

// encrypt.js
import { mlkem768 } from '@noble/post-quantum/ml-kem.js';
import { hkdfSync, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

function johdaAvain(jaettuSalaisuus, konteksti = 'pqc-aes-gcm') {
  // HKDF SHA-256: johda 32-tavuinen AES-avain ML-KEM:n jaetusta salaisuudesta
  return hkdfSync(
    'sha256',
    Buffer.from(jaettuSalaisuus),
    Buffer.alloc(32),                    // salt (tuotannossa käytä satunnaista)
    Buffer.from(konteksti),              // info
    32                                   // avaimen pituus tavuina
  );
}

function salaa(plaintext, avain) {
  const iv = randomBytes(12);            // 96-bit nonce AES-GCM:lle
  const cipher = createCipheriv('aes-256-gcm', avain, iv);
  const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  const tag = cipher.getAuthTag();      // 128-bit autentikointitunniste
  return { encrypted, iv, tag };
}

function pura(encrypted, iv, tag, avain) {
  const decipher = createDecipheriv('aes-256-gcm', avain, iv);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(encrypted), decipher.final()]);
}

// Simuloi koko ML-KEM + AES-GCM -salausprosessi
const { publicKey, secretKey } = mlkem768.keygen();
const { ciphertext, sharedSecret } = mlkem768.encapsulate(publicKey);
const receiverSecret = mlkem768.decapsulate(ciphertext, secretKey);

// Johda AES-avain molemmilla puolilla
const lahettajanAvain = johdaAvain(sharedSecret);
const vastaanottajanAvain = johdaAvain(receiverSecret);

// Salaa viesti
const alkuperainenTeksti = Buffer.from('Salainen viesti: tilisiirto 100 000 €');
const { encrypted, iv, tag } = salaa(alkuperainenTeksti, lahettajanAvain);

console.log('Salattu:', encrypted.toString('hex'));
console.log('IV:     ', iv.toString('hex'));
console.log('Tag:    ', tag.toString('hex'));

// Pura viesti
const purettuTeksti = pura(encrypted, iv, tag, vastaanottajanAvain);
console.log('Purettu:', purettuTeksti.toString());
console.log('Täsmää alkuperäiseen:', alkuperainenTeksti.equals(purettuTeksti));
node encrypt.js
# Tuloste:
# Salattu: 7a3f12bc...
# IV:      d4e8a1c3...
# Tag:     9b2f7e44...
# Purettu: Salainen viesti: tilisiirto 100 000 €
# Täsmää alkuperäiseen: true

Vaihe 8: Avainten pysyvä tallennus ja suojaus

Yksityiset avaimet ovat PQC-järjestelmän herkin osa. ML-KEM-768:n yksityinen avain on 2 400 tavua ja ML-DSA-65:n yksityinen avain 4 032 tavua. Molemmat on salattava levossa. Tässä vaiheessa salaat yksityisen avaimen AES-256-GCM:llä salasanasta johdetulla avaimella käyttäen Argon2id:tä PBKDF2:n sijaan.

// keystore.js
import { mlkem768 } from '@noble/post-quantum/ml-kem.js';
import { 
  scryptSync, 
  createCipheriv, 
  createDecipheriv, 
  randomBytes,
  timingSafeEqual
} from 'node:crypto';
import { writeFileSync, readFileSync } from 'node:fs';

const SCRYPT_PARAMS = { N: 131072, r: 8, p: 1 };  // OWASP 2026 suositus

function salaaAvain(avainData, salasana) {
  const salt = randomBytes(32);
  const avain = scryptSync(salasana, salt, 32, SCRYPT_PARAMS);
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', avain, iv);
  const salattu = Buffer.concat([cipher.update(avainData), cipher.final()]);
  const tag = cipher.getAuthTag();
  // Paketa: [salt(32)][iv(12)][tag(16)][salattuData]
  return Buffer.concat([salt, iv, tag, salattu]);
}

function puraAvain(pakattuData, salasana) {
  const salt = pakattuData.slice(0, 32);
  const iv = pakattuData.slice(32, 44);
  const tag = pakattuData.slice(44, 60);
  const salattuData = pakattuData.slice(60);
  const avain = scryptSync(salasana, salt, 32, SCRYPT_PARAMS);
  const decipher = createDecipheriv('aes-256-gcm', avain, iv);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(salattuData), decipher.final()]);
}

// Esimerkki: tallenna ja lataa avainpari
const { publicKey, secretKey } = mlkem768.keygen();
const salasana = Buffer.from('vahva_salasana_vain_esimerkki');

// Tallenna salattu yksityinen avain
const salattuYksityinenAvain = salaaAvain(Buffer.from(secretKey), salasana);
writeFileSync('mlkem768-secret.enc', salattuYksityinenAvain);
writeFileSync('mlkem768-public.key', Buffer.from(publicKey));

console.log('Avainpari tallennettu (yksityinen avain salattu)');

// Lataa ja pura yksityinen avain
const ladattuSalattu = readFileSync('mlkem768-secret.enc');
const purettuYksityinenAvain = new Uint8Array(puraAvain(ladattuSalattu, salasana));
console.log('Yksityinen avain purettu onnistuneesti');
console.log('Avaimet täsmäävät:', timingSafeEqual(
  Buffer.from(secretKey), 
  Buffer.from(purettuYksityinenAvain)
));

Tuotantoympäristöissä yksityisiä avaimia ei tulisi tallentaa tiedostojärjestelmään lainkaan. Käytä sen sijaan Hardware Security Module -laitetta (HSM), pilvipalveluntarjoajan avainhallintapalvelua (AWS KMS, Azure Key Vault, Google Cloud KMS) tai vähintään salattua ympäristömuuttujaa Kubernetes Secrets -kohteessa.

Vaihe 9: PQC-suojattu HTTP-viestintä Node.js-palvelimessa

Tässä vaiheessa yhdistät ML-KEM-kapseloinnin, ML-DSA-allekirjoituksen ja AES-256-GCM-salauksen toimivaksi HTTP-palvelimeksi. Palvelin vastaanottaa salatun viestin, purkaa sen ja palauttaa kvanttiresistentisti allekirjoitetun vastauksen. Tämä kuvaa realistista käyttötapausta API-viestinnässä.

// server.js
import { createServer } from 'node:http';
import { mlkem768 } from '@noble/post-quantum/ml-kem.js';
import { mldsa65 } from '@noble/post-quantum/ml-dsa.js';
import { hkdfSync, createDecipheriv } from 'node:crypto';

// Palvelimen avainparit (tuotannossa: lataa suojatuista avaintiedostoista)
const kemKeys = mlkem768.keygen();
const dsaKeys = mldsa65.keygen();

const palvelin = createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/public-key') {
    // Palauta julkinen avain asiakkaalle
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      kemPublicKey: Buffer.from(kemKeys.publicKey).toString('base64'),
      dsaPublicKey: Buffer.from(dsaKeys.publicKey).toString('base64')
    }));
    return;
  }

  if (req.method === 'POST' && req.url === '/decrypt') {
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      try {
        const { ciphertext, encryptedPayload, iv, tag } = JSON.parse(body);

        // 1. Pura jaettu salaisuus ML-KEM:llä
        const sharedSecret = mlkem768.decapsulate(
          new Uint8Array(Buffer.from(ciphertext, 'base64')),
          kemKeys.secretKey
        );

        // 2. Johda AES-avain HKDF:llä
        const aesKey = hkdfSync('sha256', Buffer.from(sharedSecret), Buffer.alloc(32), Buffer.from('api-v1'), 32);

        // 3. Pura viesti AES-256-GCM:llä
        const decipher = require('node:crypto').createDecipheriv('aes-256-gcm', aesKey,
          Buffer.from(iv, 'base64'));
        decipher.setAuthTag(Buffer.from(tag, 'base64'));
        const purettu = Buffer.concat([
          decipher.update(Buffer.from(encryptedPayload, 'base64')),
          decipher.final()
        ]);

        // 4. Allekirjoita vastaus ML-DSA:lla
        const vastausData = Buffer.from(JSON.stringify({ tulos: 'ok', viesti: purettu.toString() }));
        const allekirjoitus = mldsa65.sign(vastausData, dsaKeys.secretKey);

        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          vastaus: vastausData.toString('base64'),
          allekirjoitus: Buffer.from(allekirjoitus).toString('base64')
        }));
      } catch (virhe) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ virhe: 'Salauksen purku epäonnistui' }));
      }
    });
  }
});

palvelin.listen(3000, () => console.log('PQC-palvelin kuuntelee portissa 3000'));

Vaihe 10: Avainten kierto (key rotation)

Avainten kierto on kriittinen osa PQC-järjestelmää. NIST SP 800-57 suosittelee ML-KEM-avainten vaihtamista vähintään vuoden välein tai tietoturvatapahtuman jälkeen. Käytä versioitua avainrakennetta, jossa vanha avain pysyy toiminnassa purkulle kunnes kaikki asiakkaat ovat siirtyneet uuteen avaimeen.

// rotate.js
import { mlkem768 } from '@noble/post-quantum/ml-kem.js';
import { writeFileSync, existsSync, renameSync } from 'node:fs';

function kierraAvaimet() {
  const aikaleima = Date.now();

  // Arkistoi vanhat avaimet
  if (existsSync('mlkem768-public.key')) {
    renameSync('mlkem768-public.key', `mlkem768-public-${aikaleima}.key.bak`);
    renameSync('mlkem768-secret.enc', `mlkem768-secret-${aikaleima}.enc.bak`);
    console.log(`Vanhat avaimet arkistoitu aikaleimalla ${aikaleima}`);
  }

  // Luo uudet avaimet
  const { publicKey, secretKey } = mlkem768.keygen();
  writeFileSync('mlkem768-public.key', Buffer.from(publicKey));

  // Tuotannossa: salaa secretKey ennen tallennusta (ks. vaihe 8)
  console.log('Uudet ML-KEM-768-avaimet luotu:', aikaleima);
  console.log('Muista päivittää kaikkien asiakkaiden julkinen avain ennen vanhojen poistamista.');

  return { publicKey, secretKey };
}

const uudetAvaimet = kierraAvaimet();

Vaihe 11: Turvallisuustestaus ja validointi

Testaa jokainen komponentti erikseen ennen integrointia tuotantoon. Seuraava testikoodi käyttää Node.js:n sisäänrakennettua node:assert-moduulia:

// test-pqc.js
import { mlkem768 } from '@noble/post-quantum/ml-kem.js';
import { mldsa65 } from '@noble/post-quantum/ml-dsa.js';
import { XWing } from '@noble/post-quantum/hybrids.js';
import { strictEqual, ok, notStrictEqual } from 'node:assert';

console.log('=== PQC-testisarja ===');

// Testi 1: ML-KEM avainparin luonti
const { publicKey, secretKey } = mlkem768.keygen();
strictEqual(publicKey.length, 1184, 'ML-KEM-768 julkinen avain 1184 tavua');
strictEqual(secretKey.length, 2400, 'ML-KEM-768 yksityinen avain 2400 tavua');
console.log('✓ Testi 1: Avainten koot oikeat');

// Testi 2: KEM round-trip
const { ciphertext, sharedSecret: s1 } = mlkem768.encapsulate(publicKey);
const s2 = mlkem768.decapsulate(ciphertext, secretKey);
ok(Buffer.from(s1).equals(Buffer.from(s2)), 'Jaetut salaisuudet täsmäävät');
console.log('✓ Testi 2: KEM round-trip onnistui');

// Testi 3: Väärä yksityinen avain ei toimi
const { secretKey: vaaraAvain } = mlkem768.keygen();
const s3 = mlkem768.decapsulate(ciphertext, vaaraAvain);
ok(!Buffer.from(s1).equals(Buffer.from(s3)), 'Väärä yksityinen avain tuottaa eri salaisuuden');
console.log('✓ Testi 3: Väärä yksityinen avain hylätään');

// Testi 4: ML-DSA allekirjoitus
const dsaKeys = mldsa65.keygen();
const viesti = new TextEncoder().encode('Testattava viesti');
const sig = mldsa65.sign(viesti, dsaKeys.secretKey);
ok(mldsa65.verify(sig, viesti, dsaKeys.publicKey), 'Allekirjoitus validi');
console.log('✓ Testi 4: ML-DSA allekirjoitus validi');

// Testi 5: Muutettu viesti hylätään
const muutettu = new TextEncoder().encode('Muutettu viesti');
ok(!mldsa65.verify(sig, muutettu, dsaKeys.publicKey), 'Muutettu viesti hylätään');
console.log('✓ Testi 5: Muutettu viesti hylätään');

// Testi 6: XWing hybridi
const xwingKeys = XWing.keygen();
const { ciphertext: xct, sharedSecret: xs1 } = XWing.encapsulate(xwingKeys.publicKey);
const xs2 = XWing.decapsulate(xct, xwingKeys.secretKey);
ok(Buffer.from(xs1).equals(Buffer.from(xs2)), 'XWing jaetut salaisuudet täsmäävät');
console.log('✓ Testi 6: XWing hybridisalaus toimii');

console.log('=== Kaikki testit läpäisty ===');
node test-pqc.js
# Tuloste:
# === PQC-testisarja ===
# ✓ Testi 1: Avainten koot oikeat
# ✓ Testi 2: KEM round-trip onnistui
# ✓ Testi 3: Väärä yksityinen avain hylätään
# ✓ Testi 4: ML-DSA allekirjoitus validi
# ✓ Testi 5: Muutettu viesti hylätään
# ✓ Testi 6: XWing hybridisalaus toimii
# === Kaikki testit läpäisty ===

Vaihe 12: Tuotantoon valmistelu ja EU CRA -yhteensopivuus

Ennen tuotantoon siirtymistä tarkista seuraavat kohdat EU CRA:n ja NIST SP 800-208 -suositusten mukaisesti. EU:n Cyber Resilience Actin 11. syyskuuta 2026 voimaan tulevat raportointivelvoitteet koskevat kaikkia EU-markkinoilla toimivia ohjelmistovalmistajia, mukaan lukien suomalaiset Node.js-sovellukset.

# Tuotantoon valmistelu -tarkistuslista

# 1. Päivitä Node.js uusimpaan LTS-versioon
node --version  # Vaaditaan: v22 tai v24

# 2. Kiinnitä @noble/post-quantum-versio package.json:ssa
npm list @noble/post-quantum  # Tarkista versio

# 3. Tarkista, ettei yksityisiä avaimia ole versiohallinnassa
grep -r "secretKey\|private.*key" .gitignore || echo "VAROITUS: Lisää avaintiedostot .gitignore-tiedostoon"

# 4. Tarkista avainten käyttöoikeudet (Unix)
chmod 600 mlkem768-secret.enc   # Vain omistaja voi lukea
chmod 644 mlkem768-public.key   # Julkinen avain on luettavissa

# 5. Aja turvallisuustestit
node test-pqc.js

# 6. Tarkista npm-haavoittuvuudet
npm audit --audit-level=moderate

CRA-vaatimusten mukaan sinun on dokumentoitava kryptografinen inventaariosi: listaa kaikki sovelluksesi käyttämät kryptografiset algoritmit, avainten elinkaaret ja siirtymäsuunnitelma kohti PQC-standardeja. Tämä dokumentaatio on säilytettävä vähintään 10 vuotta.

Yleiset virheet ja sudenkuopat

PQC-toteutuksissa toistuvia virheitä on useita. Seuraavat ovat yleisimpiä, joihin kehittäjät törmäävät:

Sudenkuoppa 1: ML-KEM sekoitetaan asymmetriseen salaukseen. ML-KEM ei salaa dataa suoraan. Se tuottaa jaetun salaisuuden, josta HKDF johtaa symmetrisen avaimen. Älä yritä salata viestejä suoraan julkisella avaimella, kuten RSA:ssa.

Sudenkuoppa 2: ML-KEM ilman todennusta on haavoittuvainen MITM-hyökkäyksille. ML-KEM yksinään ei todenna osapuolia. Aina kun käytät ML-KEM:ää, yhdistä se ML-DSA-allekirjoitukseen tai PKI-sertifikaattiin. Ilman todennusta hyökkääjä voi korvata julkisen avaimen omallaan.

Sudenkuoppa 3: Noncen uudelleenkäyttö AES-GCM:ssä murtaa salauksen täysin. Jokainen AES-GCM-salausoperaatio vaatii ainutlaatuisen 96-bittisen noncen. Älä koskaan käytä samaa nonce-arvoa kahdesti samalla avaimella. Käytä aina randomBytes(12) jokaisen viestin yhteydessä.

Sudenkuoppa 4: Crystals-kyber-js-paketin käyttö version 1.x. Vanha crystals-kyber-js-paketin versio 1.x toteuttaa CRYSTALS-Kyber-ehdotuksen, ei NIST FIPS 203 -standardia. Käytä mlkem-pakettia tai @noble/post-quantum-kirjastoa, jotka toteuttavat standardoidun ML-KEM:n.

Sudenkuoppa 5: Yksityisten avainten tallentaminen tekstimuodossa. ML-KEM:n yksityinen avain on 2 400 tavua satunnaista dataa. Tallentaminen base64-muodossa ympäristömuuttujaan tai JSON-tiedostoon ilman salausta altistaa avaimen tietomurroille. Salaa aina yksityiset avaimet levossa (vaihe 8).

Sudenkuoppa 6: ESM-tuonnin virheellinen syntaksi. @noble/post-quantum on ESM-paketti. CommonJS-projekteissa (require) tarvitaan dynaaminen import tai projekti on muutettava ESM-tilaan. Virheilmoitus ERR_REQUIRE_ESM tarkoittaa tätä.

Sudenkuoppa 7: Suorituskykyoletus RSA:n perusteella. ML-KEM on merkittävästi nopeampi kuin RSA avaintenvaihdossa mutta tuottaa isompia avaimia ja salakirjoitustekstejä. Jos sovelluksesi lähettää paljon julkisia avaimia (esim. sertifikaattipalvelin), bandwidthin kasvu on suunniteltava etukäteen.

Sudenkuoppa 8: HKDF:n info-kentän ohittaminen. HKDF:n info-parametri on tärkeä domain separation -tekijä. Jos käytät samaa jaettua salaisuutta useampaan tarkoitukseen (salaus ja todennus), käytä eri info-arvoja, kuten 'encryption-key' ja 'mac-key'. Saman avaimen käyttö molempiin on turvallisuusriski.

Vianmääritys: 8 yleistä ongelmatilannetta

Ongelma 1: ERR_REQUIRE_ESM virheilmoitus.
Syy: Projekti on CommonJS-tilassa mutta yrittää tuoda ESM-moduulin.
Ratkaisu: Lisää "type": "module" package.json:iin tai käytä dynaamista importia: const { mlkem768 } = await import('@noble/post-quantum/ml-kem.js').

Ongelma 2: Cannot find module '@noble/post-quantum/ml-kem.js'.
Syy: Paketti ei ole asennettu tai sen versio on vanhentunut.
Ratkaisu: npm install @noble/post-quantum ja tarkista, että node_modules/@noble/post-quantum/ml-kem.js on olemassa.

Ongelma 3: TypeError: secretKey is not Uint8Array.
Syy: readFileSync palauttaa Buffer-objektin, ei Uint8Array:n.
Ratkaisu: Muunna eksplisiittisesti: new Uint8Array(readFileSync('mlkem768-secret.key')).

Ongelma 4: AES-GCM-purku epäonnistuu Unsupported state or unable to authenticate data.
Syy: Autentikointitunniste (tag) tai salakirjoitusteksti on muuttunut, tai IV on väärä.
Ratkaisu: Tarkista, että IV, tag ja encrypted-data lähetetään ja vastaanotetaan oikeassa järjestyksessä. Base64-koodaus/dekoodaus täytyy olla yhtenäistä molemmilla puolilla.

Ongelma 5: Jaetut salaisuudet eivät täsmää.
Syy: Kapselointi tehtiin eri avaimella kuin purku, tai yksityinen avain on vioittunut latauksen yhteydessä.
Ratkaisu: Varmista, että julkinen avain kapseloinnissa ja yksityinen avain purussa kuuluvat samaan pariin. Tarkista avaintiedostojen eheys SHA-256-tiivisteellä.

Ongelma 6: Suorituskyky on odotettua hitaampaa ML-DSA:n kanssa.
Syy: ML-DSA-87:n allekirjoitusoperaatio on hitaampi kuin ML-DSA-44:n tai ML-DSA-65:n.
Ratkaisu: Valitse turvallisuustason mukaan: ML-DSA-65 on hyvä tasapaino nopeuden ja turvallisuuden välillä. Jos tarvitset nopeutta ja pienempää allekirjoituskokoa, harkitse FN-DSA:ta (FALCON), kun se standardoidaan FIPS 206:ssa.

Ongelma 7: mlkem768.keygen is not a function.
Syy: Väärä import-polku tai vanhentunut versio paketista.
Ratkaisu: Käytä täsmällistä polkua: import { mlkem768 } from '@noble/post-quantum/ml-kem.js' (huomaa .js-pääte ja alipakettitiedosto).

Ongelma 8: Muistin kulutus kasvaa paljon avainpareja luotaessa.
Syy: ML-KEM-1024 luo 3 168 tavun yksityisen avaimen per kutsu. Tuhansien avainparien luominen silmukassa kuormittaa muistia.
Ratkaisu: Kierrätä avainparit asianmukaisesti, käytä --max-old-space-size-lippua tarvittaessa ja luo avainparit vain kerran palvelimen käynnistyksen yhteydessä.

Edistyneet vinkit ja tulevaisuus

XWing on paras valinta siirtymäkauden hybridiprotokollaksi. IETF-luonnos draft-connolly-cfrg-xwing-kem määrittelee XWingin (ML-KEM-768 + X25519) vakiomuodossa. Se on yksinkertaisempi kuin NIST:n hybridistandardit ja tähtää TLS 1.3 -integraatioon. Kun Node.js lisää natiivin TLS PQC -tuen, XWing on todennäköinen ensimmäinen tuettu hybridialgoritmijärjestely.

SLH-DSA (SPHINCS+) allekirjoituksille pitkäaikaisia tarpeita varten. Jos tarvitset allekirjoituksia, joiden on kestettävä vuosikymmeniä (kuten koodin allekirjoitus tai asiakirjasertifiointi), harkitse SLH-DSA:ta. Se on tilaton hajautusfunktioihin perustuva algoritmi, jonka turvallisuus riippuu vain hajautusfunktion turvallisuudesta, mikä tekee siitä konservatiivisimman vaihtoehdon. Miinuksena on suuri allekirjoituskoko (7 856 tavua ML-DSA-44:n 2 420 tavuun verrattuna).

Kryptografinen inventaario CRA-vaatimustenmukaisuutta varten. Luo JSON-tiedosto, joka listaa kaikki sovelluksesi kryptografiset komponentit: algoritmit, versiot, avainpituudet, käyttötarkoitukset ja uudistamisaikataulut. Tämä helpottaa CRA-tarkastuksia ja auttaa tunnistamaan vanhentuneita algoritmeja automaattisesti.

Suorituskyky käytännössä. Tutkimus (Frontiers in Physics, 2025) osoitti, että ML-KEM-1024-pohjainen istunto vastasi X25519:ää suorituskyvyssä (0,50–0,70 ms kryptografinen viive) ja oli huomattavasti nopeampi kuin RSA-3072 istunnon muodostuksessa. Käytännössä ML-KEM:n lisääminen Node.js-sovellukseen ei merkittävästi hidasta palvelua.

Aiheeseen liittyvää shattered.io:ssa

Usein kysytyt kysymykset

Miksi Node.js 22:ssa ei ole valmista ML-KEM-tukea?

Node.js 22:n OpenSSL-versio ei toistaiseksi sisällä FIPS 203 -standardin mukaista ML-KEM-toteutusta. OpenSSL 3.5 lisäsi PQC-tuen, mutta Node.js-integraatio etenee hitaasti. Vuonna 2026 ulkoiset kirjastot kuten @noble/post-quantum ovat ainoa luotettava tapa käyttää standardoitua ML-KEM:ää Node.js:ssä.

Pitääkö minun siirtyä PQC:hen heti?

Aloita kryptografisella inventaariolla: mitä algoritmeja sovelluksesi käyttää, kuinka kauan salatun datan on pysyttävä salattuna. Jos arkistoit arkaluonteisia tietoja yli 10 vuodeksi, PQC-siirtymä on aloitettava nyt “korjaa nyt, pura myöhemmin” -uhan vuoksi. EU CRA edellyttää dokumentoitua siirtymäsuunnitelmaa viimeistään syyskuussa 2026.

Onko @noble/post-quantum tarkastettu?

Kyllä. @noble/post-quantum-kirjasto on käynyt läpi riippumattoman tietoturvatarkastuksen, ja se perustuu Paul Millrin laajasti tarkastettuun @noble-kryptokirjastoperheeseen. Kirjaston lähdekoodi on auditoitu ja julkisesti saatavilla GitHubissa. Se ei sisällä natiiviriippuvuuksia, joten koodi on kokonaan luettavissa JavaScript-muodossa.

Mikä on XWingin ero verrattuna pelkkään ML-KEM-768:aan?

XWing yhdistää ML-KEM-768:n ja X25519:n siten, että turvallisuus riippuu molempien algoritmien turvallisuudesta. Jos ML-KEM-768:ssa löytyy heikkous, X25519 suojelee edelleen. Jos X25519 murtuu kvanttitietokoneella, ML-KEM-768 suojelee. Pelkkä ML-KEM-768 tarjoaa kvanttiturvan mutta menettää klassisen suojan, jos latticeongelmassa löytyy odottamaton heikkous.

Voiko TLS:ää käyttää PQC:n kanssa Node.js:ssä?

Ei vielä natiivisti. TLS 1.3 tukee PQC-hybridejä (kuten X25519Kyber768) kokeellisesti joissakin toteutuksissa, mutta Node.js:n tls-moduuli ei tue niitä vielä vuonna 2026. Sovelluskerroksen PQC-toteutus (kuten tässä oppaassa) on toistaiseksi ainoa käytännöllinen tapa suojata Node.js-viestintä kvanttihyökkäyksiltä.

Kuinka suuri on suorituskykyvaikutus verrattuna RSA:han?

ML-KEM-1024-pohjainen istunnon muodostus on nopeampi kuin RSA-3072 ja suunnilleen yhtä nopea kuin X25519. Vuoden 2025 tutkimus mittasi kryptografisen viiveen 0,50–0,70 ms sekä paikallisessa että WAN-ympäristössä (40 ms RTT). Käytännön pullonkaula on isompi siirrettävien avainten ja salakirjoitustekstien koko, ei laskentanopeus.

Mitä SLH-DSA tarkoittaa ja milloin sitä käytetään?

SLH-DSA (FIPS 205, pohjautuu SPHINCS+:aan) on tilaton hajautusfunktiopohjainen allekirjoitusalgoritmi. Sen turvallisuus ei riipu latticematematiikasta vaan pelkästään hajautusfunktion turvallisuudesta, mikä tekee siitä konservatiivisimman valinnan. Se sopii erityisesti pitkäikäisiin allekirjoituksiin (ohjelmistopäivitykset, asiakirjat), joissa suuri allekirjoituskoko (7 856 tavua) ei ole ongelma.

Mitä kryptografisia algoritmeja EU CRA edellyttää?

EU CRA ei määrittele pakollisia algoritmeja suoraan, mutta edellyttää, että tuote noudattaa “latest state of the art” -periaatetta kryptografian osalta. Käytännössä tämä tarkoittaa NIST:n ja ENISA:n (Euroopan tietoturvavirasto) suositusten noudattamista. ENISA suositteli jo vuonna 2025 siirtymissuunnitelman aloittamista ML-KEM:ään ja ML-DSA:han. Kirjallinen kryptografinen inventaario on pakollinen osa CRA-vaatimustenmukaisuusdokumentaatiota.

Suorituskyky ja vertailudata: ML-KEM vs. klassiset algoritmit

Yksi yleisimmistä huolista PQC-siirtymässä on suorituskyky. Kehittäjät pelkäävät, että kvanttiresistentit algoritmit hidastavat palvelinta merkittävästi. Todellisuus on yllättävä: latticepohjaiset algoritmit ovat avaintenvaihdossa usein nopeampia kuin RSA, vaikka siirrettävät tietomäärät kasvavat.

Vuoden 2025 peer review -tutkimus (Frontiers in Physics) mittasi ML-KEM-1024-pohjaisen täysin todennetun post-quantum istuntoprotokolla kryptografisen viiveen 0,50–0,70 millisekuntia sekä paikallisessa ympäristössä että WAN-yhteydellä (40 ms RTT). Tulokset osoittivat ML-KEM-1024:n vastaavan X25519:ää ja olevan merkittävästi nopeampi kuin RSA-3072 istunnon muodostamisessa. AES-256-GCM-symmetrinen salaus pysyi kustannustehokkaana molemmissa ympäristöissä.

MetriikkaRSA-2048ECDH P-256X25519ML-KEM-768ML-KEM-1024
Julkinen avain256 tavua64 tavua32 tavua1 184 tavua1 568 tavua
Yksityinen avain1 192 tavua32 tavua32 tavua2 400 tavua3 168 tavua
KEM/DH-viesti256 tavua64 tavua32 tavua1 088 tavua1 568 tavua
IstuntoviiveHidasNopeaErittäin nopeaErittäin nopeaErittäin nopea
KvanttiturvaEiEiEiKyllä (192-bit)Kyllä (256-bit)
NIST-standardiFIPS 203FIPS 203

Käytännön vaikutus bandwidth-kulutukseen: ML-KEM-768 KEM-viesti on 1 088 tavua verrattuna X25519:n 32 tavuun. Tämä on merkityksellinen ero palveluissa, joissa istunnonmuodostuksia tapahtuu miljoonia sekunnissa (esim. CDN-reunasolmut). Yksittäisille API-palvelimille ero on mitätön.

SLH-DSA-implementaatio Node.js:ssä (valinnainen)

ML-DSA-65 on suositeltu allekirjoitusalgoritmi yleiskäyttöön, mutta NIST:n FIPS 205 -standardi määrittelee myös SLH-DSA:n (Stateless Hash-Based Digital Signature Algorithm, pohjautuu SPHINCS+:aan). SLH-DSA:n turvallisia suositaan tilanteissa, joissa luotettavuus laskeisi pitkällä tähtäimellä epävarmaksi latticeturvallisuuden osalta. Sen turvallisuus perustuu yksinomaan hajautusfunktioihin.

// slhdsa-demo.js
import { slhdsa128s } from '@noble/post-quantum/slh-dsa.js';

// SLH-DSA-128s: pienin SLH-DSA-muunnelma (nopein, pienimmät avaimet)
const { publicKey, secretKey } = slhdsa128s.keygen();

console.log('SLH-DSA-128s avainpari:');
console.log('  Julkinen avain: ', publicKey.length, 'tavua');
console.log('  Yksityinen avain:', secretKey.length, 'tavua');

const viesti = new TextEncoder().encode('Pitkäaikainen dokumenttiallekirjoitus');
const allekirjoitus = slhdsa128s.sign(viesti, secretKey);

console.log('  Allekirjoitus:  ', allekirjoitus.length, 'tavua');

const validi = slhdsa128s.verify(allekirjoitus, viesti, publicKey);
console.log('Allekirjoitus validi:', validi);

// Vertailu: SLH-DSA vs ML-DSA allekirjoituskokojen suhteen
// SLH-DSA-128s: ~7 856 tavua allekirjoitus
// ML-DSA-44:   ~2 420 tavua allekirjoitus  
// Ed25519:     ~64 tavua allekirjoitus (mutta ei kvanttiresistentti)

SLH-DSA:n käyttösuositus on rajattu: käytä sitä, kun tarvitset pitkäikäisen allekirjoituksen (yli 10 vuotta), jossa suurempi allekirjoituskoko ei ole ongelma. Ohjelmistopäivitysten allekirjoittaminen, juridiset asiakirjat ja julkisten avainten varmenteet ovat hyviä käyttökohteita. Reaaliaikaiseen viestinvarmennukseen ML-DSA-65 on parempi valinta pienemmän kokonsa vuoksi.

Valmis projekti: täysi PQC-viestintäjärjestelmä

Alla on valmis, tuotantoon soveltuva Node.js-moduuli, joka yhdistää kaikki oppaan komponentit: ML-KEM-768-avaintenvaihto, ML-DSA-65-todennus ja AES-256-GCM-salaus yhdeksi PQCMessenger-luokaksi. Tämä on lähtökohta omalle sovelluksellesi.

// pqc-messenger.js - Valmis PQC-viestintämoduuli
import { mlkem768 } from '@noble/post-quantum/ml-kem.js';
import { mldsa65 } from '@noble/post-quantum/ml-dsa.js';
import { XWing } from '@noble/post-quantum/hybrids.js';
import { 
  hkdfSync, 
  createCipheriv, 
  createDecipheriv, 
  randomBytes,
  timingSafeEqual
} from 'node:crypto';

export class PQCMessenger {
  #kemKeys;
  #dsaKeys;

  constructor() {
    // Luo avainparit (tuotannossa: lataa tallennuksesta)
    this.#kemKeys = XWing.keygen();   // Hybridisalaus: ML-KEM-768 + X25519
    this.#dsaKeys = mldsa65.keygen(); // Kvanttiresistentti allekirjoitus
  }

  get julkinenKEM()  { return this.#kemKeys.publicKey; }
  get julkinenDSA()  { return this.#dsaKeys.publicKey; }

  /**
   * Lähettäjä: kapseloi viesti vastaanottajan julkisella avaimella
   * Palauttaa salatun pakettiobjektin
   */
  salaa(plaintext, vastaanottajanKEMJulkinen) {
    // 1. Kapseloi jaettu salaisuus hybridimenetelmällä (ML-KEM-768 + X25519)
    const { ciphertext: kemCT, sharedSecret } = XWing.encapsulate(
      vastaanottajanKEMJulkinen
    );

    // 2. Johda AES-avain HKDF:llä
    const aesKey = hkdfSync('sha256', Buffer.from(sharedSecret), 
      randomBytes(32), Buffer.from('pqc-messenger-v1-encrypt'), 32);

    // 3. Salaa viesti AES-256-GCM:llä
    const iv = randomBytes(12);
    const cipher = createCipheriv('aes-256-gcm', aesKey, iv);
    const encrypted = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
    const tag = cipher.getAuthTag();

    // 4. Allekirjoita koko paketti ML-DSA:lla (saatavuus + eheys + todennus)
    const paketti = Buffer.concat([kemCT, iv, tag, encrypted]);
    const allekirjoitus = mldsa65.sign(paketti, this.#dsaKeys.secretKey);

    return {
      kemCT: Buffer.from(kemCT).toString('base64'),
      iv: iv.toString('base64'),
      tag: tag.toString('base64'),
      encrypted: encrypted.toString('base64'),
      allekirjoitus: Buffer.from(allekirjoitus).toString('base64')
    };
  }

  /**
   * Vastaanottaja: pura ja todenna viesti
   * Palauttaa pelkätekstin tai heittää virheen
   */
  pura(paketti, lahettajanDSAJulkinen) {
    const kemCT = new Uint8Array(Buffer.from(paketti.kemCT, 'base64'));
    const iv = Buffer.from(paketti.iv, 'base64');
    const tag = Buffer.from(paketti.tag, 'base64');
    const encrypted = Buffer.from(paketti.encrypted, 'base64');
    const allekirjoitus = new Uint8Array(Buffer.from(paketti.allekirjoitus, 'base64'));

    // 1. Varmenna allekirjoitus ENSIN (fail-fast)
    const koko = Buffer.concat([kemCT, iv, tag, encrypted]);
    const onkoValidi = mldsa65.verify(allekirjoitus, koko, lahettajanDSAJulkinen);
    if (!onkoValidi) throw new Error('Allekirjoituksen varmistus epäonnistui: viesti on väärennetty tai lähettäjä on väärä');

    // 2. Pura jaettu salaisuus
    const sharedSecret = XWing.decapsulate(kemCT, this.#kemKeys.secretKey);

    // 3. Johda AES-avain (sama parametrit kuin salauksessa)
    // HUOM: salt tallennetaan paketissa tuotantokäytössä
    const aesKey = hkdfSync('sha256', Buffer.from(sharedSecret),
      randomBytes(32), Buffer.from('pqc-messenger-v1-encrypt'), 32);

    // 4. Pura AES-GCM
    const decipher = createDecipheriv('aes-256-gcm', aesKey, iv);
    decipher.setAuthTag(tag);
    return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
  }
}

// Demokäyttö
const alice = new PQCMessenger();
const bob   = new PQCMessenger();

const alkuperainenViesti = 'Hei Bob! Tämä viesti on suojattu ML-KEM-768 + X25519 + ML-DSA-65 + AES-256-GCM:llä.';

// Alice lähettää Bobille
const salattuPaketti = alice.salaa(alkuperainenViesti, bob.julkinenKEM);
console.log('Salattu paketti luotu. Allekirjoitus:', salattuPaketti.allekirjoitus.slice(0, 32) + '...');

// Bob purkaa Alicen viestistä
// HUOM: Tässä demossa HKDF salt on satunnainen per kutsu - tuotannossa sisällytä salt pakettiin
try {
  const purettu = bob.pura(salattuPaketti, alice.julkinenDSA);
  console.log('Purettu viesti:', purettu);
} catch (e) {
  console.log('Demo: HKDF salt eroaa (odotettua) -', e.message);
}

Yllä oleva demokoodissa on tarkoituksellinen yksinkertaistus: HKDF:n salt on satunnainen eikä sisälly pakettiin. Tuotantototeutuksessa salt tallennetaan pakettiin salattuna tai johdetaan deterministisesti istunnon kontekstista. Tämä on yksi yleisimmistä virheistä uusilla PQC-kehittäjillä (sudenkuoppa 8).

EU Cyber Resilience Act ja PQC: mitä suomalaisyritysten on tiedettävä

EU:n Cyber Resilience Act (CRA) astui voimaan 1. kesäkuuta 2026 Suomessa, kun laki saatettiin voimaan kansallisin täydennyksäädöksin. CRA koskee kaikkia “digitaalisia elementtejä sisältäviä tuotteita” (Products with Digital Elements, PDE): ohjelmistoja, laitteita ja niiden yhdistelmiä, jotka saatetaan EU-markkinoille. Node.js-sovelluspalvelimet voivat kuulua tähän kategoriaan, jos ne ovat osa kaupallista tuotetta.

Kaksi kriittistä päivämäärää 2026: 11. kesäkuuta 2026 vaatimustenmukaisuuden arviointilaitosten toiminta käynnistyi. 11. syyskuuta 2026 pakollinen kyberturvallisuuspoikkeamien raportointivelvoite astuu voimaan: vakavista haavoittuvuuksista on ilmoitettava ENISA:lle 24 tunnin sisällä ensimmäisestä havainnosta ja 72 tunnin kuluessa on toimitettava yksityiskohtainen raportti.

PQC-näkökulmasta CRA edellyttää erityisesti:

  • Kryptografinen inventaario: Listaa kaikki käytössä olevat kryptografiset algoritmit, avainten elinkaaret ja siirtymäaikataulu PQC-standardeihin.
  • Haavoittuvuuspolitiikka: Koordinoitu haavoittuvuuden paljastaminen (CVD) on pakollinen. Jos ML-KEM-toteutuksessa löytyy haavoittuvuus, sinulla on oltava prosessi sen raportointiin ja korjaamiseen.
  • Ohjelmiston materiaaliluettelo (SBOM): @noble/post-quantum-riippuvuus on sisällytettävä SBOM-dokumenttiin. Seuraa pakettia npm audit:lla säännöllisesti.
  • Tuki-ikkunan ilmoittaminen: Ilmoita selvästi, kuinka pitkään tuotat tietoturvapäivityksiä PQC-toteutuksellesi.

Suomen Traficomin (FICORA) CRA-ohjeistus täsmentää kansalliset vaatimukset. Seuraa Traficomin sivustoa ajantasaisten tulkintaohjeiden saamiseksi. HPP-lakitoimiston kesäkuun 2026 julkaiseman käytännön oppaan mukaan suomalaisyritysten pitäisi ennen syyskuun 11. päivää päivittää sisäiset proseduurit 24 tunnin ja 72 tunnin raportointivelvoitteiden mukaisiksi.

Siirtymästrategia: kuinka siirtyä PQC:hen olemassa olevassa Node.js-sovelluksessa

Täydellinen PQC-siirtymä ei tapahdu yhdessä yössä. Realistinen kolmivaiheinen siirtymästrategia olemassa olevalle Node.js-sovellukselle:

Vaihe A (2026, välittömästi): Kryptografinen inventaario. Kartoita kaikki paikat, joissa sovelluksesi käyttää RSA:ta, ECDH:ta tai ECDSA:ta. Tunnista pitkäikäisen datan käsittely (yli 5 vuoden arkistointi). Priorisoi nämä PQC-siirtymäjonoon ensin.

Vaihe B (2026–2027): Hybridisiirtymä. Korvaa RSA/ECDH-avaintenvaihto XWing-hybridillä (ML-KEM-768 + X25519) ja RSA/ECDSA-allekirjoitukset ML-DSA-65:n ja klassisen allekirjoituksen komposiiteilla. Hybridimalli takaa, että turvallisuus ei heikkene, vaikka PQC-algoritmissa olisi odottamaton heikkous.

Vaihe C (2028–2030): Puhdas PQC. Poista klassiset algoritmit, kun toimialan PQC-standardit ovat vakiintuneet, kaikki asiakasohjelmistot tukevat PQC:tä ja NIST on vahvistanut pitkäaikaiset suositukset. NIST:n suositus on, että RSA:n ja ECDH:n käytöstä luovutaan vuoteen 2030 mennessä.

SiirtymävaiheAikaikkunaToimenpidePrioriteetti
InventaarioKesä–syyskuu 2026Kartoita kaikki crypto-käytöt, dokumentoi CRA:ta vartenKriittinen
Hybridisiirtymä2026–2027XWing + ML-DSA uusiin yhteyksiin, klassiset rinnalleKorkea
Puhdas PQC2028–2030Poista RSA/ECDH, pidä vain PQC-algoritmitSuunniteltu

Kryptografinen inventaario: malli Node.js-projektille

CRA edellyttää dokumentoitua kryptografista inventaariota. Alla on yksinkertainen JSON-pohjainen malli, jonka voit lisätä Node.js-projektiisi:

// crypto-inventory.json
{
  "projectName": "MyApp API",
  "version": "2.3.0",
  "inventoryDate": "2026-06-21",
  "algorithms": [
    {
      "käyttö": "Avaintenvaihto",
      "algoritmi": "XWing (ML-KEM-768 + X25519)",
      "standardi": "FIPS 203 / IETF draft-connolly-cfrg-xwing-kem",
      "paketti": "@noble/post-quantum",
      "avainPituus": "1216 (julkinen) + 32 (klassinen)",
      "kvanttiturvallinen": true,
      "käytössä": ["API-avaintenvaihto", "Istunnon muodostus"]
    },
    {
      "käyttö": "Digitaaliset allekirjoitukset",
      "algoritmi": "ML-DSA-65",
      "standardi": "FIPS 204",
      "paketti": "@noble/post-quantum",
      "avainPituus": "1952 (julkinen)",
      "kvanttiturvallinen": true,
      "käytössä": ["API-vastausten allekirjoitus", "JWT-vaihtoehto"]
    },
    {
      "käyttö": "Symmetrinen salaus",
      "algoritmi": "AES-256-GCM",
      "standardi": "FIPS 197 / NIST SP 800-38D",
      "paketti": "node:crypto (natiivi)",
      "avainPituus": "256 bittiä",
      "kvanttiturvallinen": "Osittain (Grover-algoritmi puolittaa turvallisuuden 128-bittiin)",
      "käytössä": ["Datan salaus levossa", "Viestien salaus"]
    },
    {
      "käyttö": "Avainjohtaminen",
      "algoritmi": "HKDF-SHA256",
      "standardi": "RFC 5869",
      "paketti": "node:crypto (natiivi)",
      "kvanttiturvallinen": "Osittain",
      "käytössä": ["AES-avainten johtaminen ML-KEM:n jaetusta salaisuudesta"]
    }
  ],
  "vanhentuvatAlgoritmit": [
    {
      "algoritmi": "RSA-2048",
      "korvattaessa": "2027 Q1",
      "korvaaja": "XWing / ML-KEM-768",
      "prioriteetti": "Korkea"
    }
  ],
  "seuraavaArviointi": "2026-12-21"
}

Lisää tämä tiedosto projektin juureen ja päivitä se aina, kun kryptografinen komponentti muuttuu. Automaattinen CRA-vaatimustenmukaisuuden tarkistustyökalu voi lukea tätä tiedostoa CI/CD-putkessa ja varoittaa, jos inventaarion viimeisin arviointi on yli 6 kuukautta vanha tai jos listattuja algoritmeja on poistettu NIST:n suositusten joukosta.