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ä.
| Standardi | Algoritmi | Lähtökohta | Käyttötarkoitus | Hyväksytty |
|---|---|---|---|---|
| FIPS 203 | ML-KEM | CRYSTALS-Kyber | Avainten kapselointi (KEM) | Elokuu 2024 |
| FIPS 204 | ML-DSA | CRYSTALS-Dilithium | Digitaaliset allekirjoitukset | Elokuu 2024 |
| FIPS 205 | SLH-DSA | SPHINCS+ | Allekirjoitukset (tilaton) | Elokuu 2024 |
| FIPS 206 | FN-DSA | FALCON | Allekirjoitukset (pieni koko) | Odottaa 2026 |
Vaatimukset: ohjelmistot ja versiot
Ennen aloittamista tarkista, että seuraavat ohjelmistot ovat asennettuina:
| Ohjelmisto | Vaadittu versio | Suositeltu versio | Huomio |
|---|---|---|---|
| Node.js | 18.0+ | 22 LTS tai 24 | ESM-tuki vaaditaan |
| npm | 8.0+ | 10+ | package.json type: module |
| @noble/post-quantum | 0.2+ | Uusin versio | Ainoa tarkastettu JS-toteutus |
| Käyttöjärjestelmä | Linux / macOS / Windows | Ubuntu 22.04 LTS | Ei 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:
| Algoritmi | Julkinen avain | Yksityinen avain | Salakirjoitusteksti | Kvanttiturva |
|---|---|---|---|---|
| RSA-2048 | 256 tavua | 1 192 tavua | 256 tavua | Ei |
| X25519 | 32 tavua | 32 tavua | 32 tavua | Ei |
| ML-KEM-512 | 800 tavua | 1 632 tavua | 768 tavua | Kyllä (128-bit) |
| ML-KEM-768 | 1 184 tavua | 2 400 tavua | 1 088 tavua | Kyllä (192-bit) |
| ML-KEM-1024 | 1 568 tavua | 3 168 tavua | 1 568 tavua | Kyllä (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
| Algoritmi | Julkinen avain | Yksityinen avain | Allekirjoitus | Kvanttiturva |
|---|---|---|---|---|
| Ed25519 | 32 tavua | 64 tavua | 64 tavua | Ei |
| ECDSA P-256 | 64 tavua | 32 tavua | 71 tavua | Ei |
| ML-DSA-44 | 1 312 tavua | 2 560 tavua | 2 420 tavua | Kyllä |
| ML-DSA-65 | 1 952 tavua | 4 032 tavua | 3 293 tavua | Kyllä |
| ML-DSA-87 | 2 592 tavua | 4 896 tavua | 4 595 tavua | Kyllä |
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
- Node.js WebCrypto API: 12 vaihetta, 35 min [2026] – natiivin WebCrypto-rajapinnan käyttö
- Ed25519 Node.js:ssä: 10 vaihetta, 30 min [2026] – klassinen EdDSA-allekirjoitus
- ECDSA Node.js:ssä: 12 vaihetta, 35 min [2026] – elliptisen käyrän allekirjoitus
- BLAKE3-hajautus Node.js:ssä: 10 vaihetta, 30 min [2026] – nopea modernihajautusalgoritmi
- HMAC Node.js:ssä: 10 vaihetta, 30 min [2026] – viestitodennus HMAC:lla
- OAuth 2.0 Node.js:ssä Passport.js:llä: 12 vaihetta, 30 min [2026] – turvallinen todennus
- Kryptografia: tiivistefunktiot ja luottamuksen perusta – kryptografian perusteet
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ä.
| Metriikka | RSA-2048 | ECDH P-256 | X25519 | ML-KEM-768 | ML-KEM-1024 |
|---|---|---|---|---|---|
| Julkinen avain | 256 tavua | 64 tavua | 32 tavua | 1 184 tavua | 1 568 tavua |
| Yksityinen avain | 1 192 tavua | 32 tavua | 32 tavua | 2 400 tavua | 3 168 tavua |
| KEM/DH-viesti | 256 tavua | 64 tavua | 32 tavua | 1 088 tavua | 1 568 tavua |
| Istuntoviive | Hidas | Nopea | Erittäin nopea | Erittäin nopea | Erittäin nopea |
| Kvanttiturva | Ei | Ei | Ei | Kyllä (192-bit) | Kyllä (256-bit) |
| NIST-standardi | – | – | – | FIPS 203 | FIPS 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 pakettianpm 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ävaihe | Aikaikkuna | Toimenpide | Prioriteetti |
|---|---|---|---|
| Inventaario | Kesä–syyskuu 2026 | Kartoita kaikki crypto-käytöt, dokumentoi CRA:ta varten | Kriittinen |
| Hybridisiirtymä | 2026–2027 | XWing + ML-DSA uusiin yhteyksiin, klassiset rinnalle | Korkea |
| Puhdas PQC | 2028–2030 | Poista RSA/ECDH, pidä vain PQC-algoritmit | Suunniteltu |
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.




