1. Inventory the function before the algorithm
A fictional application sends confidential reports and receives signed update packages. These are different needs: protecting communication and verifying an update’s origin/integrity. Before choosing replacements, record protocol, library, key, certificate, validity, owner, partners and how long data needs protection. Technical discovery may find a library without revealing all its consumers; confirm dependencies with teams. The NCCoE initiative treats discovery and interoperability as migration components. Prioritize long-lived confidentiality and hard-to-update dependencies without inventing a date for relevant quantum capability or treating a library upgrade as complete migration.
2. Separate encapsulation from signing
The lab uses ML-KEM-768 to establish a secret and ML-DSA-65 to sign a synthetic message. In the recorded run, KEM ciphertext is 1088 bytes and the secret is 32; the signature is 3309 bytes. These are properties of these parameter sets and format rather than network-packet maxima. Encapsulating to a public key allows its corresponding private-key holder to obtain the secret. It does not establish the legitimate owner of that public key. The test substitutes another party’s key and successfully establishes a secret with that party. Identity needs a trusted binding to the key and an appropriate protocol.
3. Interpret failures and context
Decapsulating a ciphertext with one flipped bit but correct length returns a different 32-byte secret in this experiment. “The function did not throw” does not mean the receiver obtained the intended secret. The example derives keys with HKDF-SHA256 and compares HMAC over synthetic data to expose the mismatch. Changing the message or derivation info also prevents the expected comparison. Fixed salt and labels are instructional only; they are not a deployable authenticated protocol. In ML-DSA signing, changing message, verification key, signature bytes or context prevents verification. Diagnose each failure at the operation that actually observed it.
4. Rotate trust and preserve verification
The local registry initially trusts only current. A new key can produce a signature valid for itself, but that does not authorize it in the registry. After approving and adding next, test the new signature and retain the old key for the agreed overlap. Removing current changes local policy; it does not mathematically invalidate old signatures when someone still holds the public key. Plan historical artifact verification, compromise handling and identification of the key version used. Never distribute the private key as a remedy for an outdated verifier. The test keeps every key in memory, publishes no secrets and changes no system trust store.
5. Accept migration through evidence
Run node run.mjs with the recorded runtime and compare all 36 checks against the manifest. The experiment measures neither production latency, TLS, partner compatibility, HSM behavior nor side-channel resistance. Node 25.8.0 marks KEM APIs as release candidate; the pinned runtime enables reproduction rather than recommending production use. FIPS 203/204 pages link potential-update notices; this work certifies neither implementation conformance nor a FIPS module. For real migration, test messages, certificates, limits, recovery, observability and rollback with partners. Summary: select the correct function, authenticate key bindings and require service outcomes before declaring migration complete.
/** Original primitive-level experiment; not a deployable key exchange protocol. */
import * as c from 'node:crypto'
import fs from 'node:fs'
import assert from 'node:assert/strict'
const checks=[];const check=(label,ok)=>{assert(ok,label);checks.push(label);};
function rejected(label,fn){let did=false;try{fn;}catch{did=true;}check(label,did);}
const receiver=c.generateKeyPairSync('ml-kem-768'),other=c.generateKeyPairSync('ml-kem-768');
const first=c.encapsulate(receiver.publicKey),received=c.decapsulate(receiver.privateKey,first.ciphertext);
const second=c.encapsulate(receiver.publicKey),receivedSecond=c.decapsulate(receiver.privateKey,second.ciphertext);
const salt=Buffer.from('synthetic-course-salt'),info=Buffer.from('BigSavant primitive-lab confirmation v1');
const derive=(secret,label=info)=>Buffer.from(c.hkdfSync('sha256',secret,salt,label,32));
const mac=(key,msg)=>c.createHmac('sha256',key).update(msg).digest;
const equal=(a,b)=>a.length===b.length&&c.timingSafeEqual(a,b);
const message=Buffer.from('synthetic transaction R-84; amount=100');
const senderKey=derive(first.sharedKey),receiverKey=derive(received),tag=mac(senderKey,message);
check('ML-KEM key type is explicit',receiver.publicKey.asymmetricKeyType==='ml-kem-768');
check('ML-KEM-768 ciphertext is 1088 bytes',first.ciphertext.length===1088);
check('encapsulation returns 32-byte secret',first.sharedKey.length===32);
check('decapsulation agrees for the intended key',equal(first.sharedKey,received));
check('independent encapsulation changes ciphertext',!equal(first.ciphertext,second.ciphertext));
check('independent encapsulation changes shared secret',!equal(first.sharedKey,second.sharedKey));
check('second valid ciphertext decapsulates correctly',equal(second.sharedKey,receivedSecond));
check('HKDF derives equal confirmation keys',equal(senderKey,receiverKey));
check('matching message and key confirm MAC',equal(tag,mac(receiverKey,message)));
check('changed message fails MAC confirmation',!equal(tag,mac(receiverKey,Buffer.from('synthetic transaction R-84; amount=900'))));
check('different HKDF label separates derived keys',!equal(senderKey,derive(received,Buffer.from('different purpose'))));
check('different label fails confirmation',!equal(tag,mac(derive(received,Buffer.from('different purpose')),message)));
const altered=Buffer.from(first.ciphertext);altered[0]^=1;const alteredSecret=c.decapsulate(receiver.privateKey,altered);
check('same-length modified ciphertext can return a secret',alteredSecret.length===32);
check('modified ciphertext does not reproduce original secret',!equal(first.sharedKey,alteredSecret));
check('modified ciphertext fails original MAC confirmation',!equal(tag,mac(derive(alteredSecret),message)));
const wrongSecret=c.decapsulate(other.privateKey,first.ciphertext);
check('wrong private key does not reproduce original secret',!equal(first.sharedKey,wrongSecret));
check('wrong private key fails original MAC confirmation',!equal(tag,mac(derive(wrongSecret),message)));
rejected('truncated ML-KEM ciphertext rejected',=>c.decapsulate(receiver.privateKey,first.ciphertext.subarray(0,-1)));
const substituted=c.encapsulate(other.publicKey),substitutedSecret=c.decapsulate(other.privateKey,substituted.ciphertext);
check('substituted public key establishes a secret with that other key',equal(substituted.sharedKey,substitutedSecret));
check('substitution does not authenticate intended receiver',!equal(substituted.sharedKey,c.decapsulate(receiver.privateKey,substituted.ciphertext)));
const signing=c.generateKeyPairSync('ml-dsa-65'),replacement=c.generateKeyPairSync('ml-dsa-65'),context=Buffer.from('dr-course-release-v1');
const signature=c.sign(null,message,{key:signing.privateKey,context});
const verifies=(key=signing.publicKey,m=message,s=signature,ctx=context)=>c.verify(null,m,{key,context:ctx},s);
check('ML-DSA key type is explicit',signing.publicKey.asymmetricKeyType==='ml-dsa-65');
check('ML-DSA-65 signature is 3309 bytes',signature.length===3309);
check('original signature verifies with trusted key and context',verifies);
check('changed message fails signature',!verifies(signing.publicKey,Buffer.from('synthetic transaction R-84; amount=900')));
const damaged=Buffer.from(signature);damaged[0]^=1;
check('changed signature bytes fail verification',!verifies(signing.publicKey,message,damaged));
check('unrelated public key fails verification',!verifies(replacement.publicKey));
check('different signature context fails verification',!verifies(signing.publicKey,message,signature,Buffer.from('dr-course-other-v1')));
check('omitted signature context fails verification',!c.verify(null,message,signing.publicKey,signature));
const nextSignature=c.sign(null,message,{key:replacement.privateKey,context});
check('replacement key produces a valid signature for itself',c.verify(null,message,{key:replacement.publicKey,context},nextSignature));
check('replacement signature does not verify under original trusted key',!verifies(signing.publicKey,message,nextSignature));
const trust=new Map([['current',signing.publicKey]]);
check('replacement signer initially outside local trust registry',!trust.has('next'));
trust.set('next',replacement.publicKey);
check('registered replacement key verifies its signature',c.verify(null,message,{key:trust.get('next'),context},nextSignature));
check('old signer still verifies during overlap',c.verify(null,message,{key:trust.get('current'),context},signature));
trust.delete('current');
check('local removal changes trust registry but not cryptographic validity',!trust.has('current')&&verifies);
rejected('ML-KEM key is not accepted for signing',=>c.sign(null,message,receiver.privateKey));
rejected('ML-DSA key is not accepted for encapsulation',=>c.encapsulate(signing.publicKey));
console.log(JSON.stringify({passed:checks.length,failed:0,checks,node:process.version,openssl:process.versions.openssl,fipsMode:c.getFips,scriptSHA256:c.createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex'),actualMLKEM:true,actualMLDSA:true,actualHKDFHMAC:true,networkUsed:false,actualTLS:false,actualCiscoDevice:false,actualAAA:false,productionProtocolClaimed:false,fipsModuleValidationClaimed:false,observations:{kem:'ml-kem-768',sharedSecretBytes:32,ciphertextBytes:1088,signature:'ml-dsa-65',signatureBytes:3309,kdf:'HKDF-SHA256',confirmation:'HMAC-SHA256',modifiedCiphertext:'same length; different secret; original MAC fails',publicKeySubstitution:'other key agrees; intended receiver does not',trustRemoval:'local registry changed; original signature still mathematically valid',privateKeysWritten:false,secretsPrinted:false},scope:'Ephemeral in-memory primitive tests. Fixed instructional HKDF salt/labels and MAC comparison are not a complete authenticated protocol, key-confirmation standard, TLS/PQC migration, Cisco configuration or FIPS module validation.'},null,2))An altered ciphertext retains 1088 bytes and returns a secret; the expected HMAC fails. API return proves neither agreement nor identity.
Common pitfalls
KEM as a signature; no error as agreement; arbitrary public key as identity; standardized algorithm as a certified module.
Related topics: PQC and inventory · PKI and rotation · Interoperability and rollback
Correct primitives are part of a solution; trust, protocol and operation need their own evidence.
Reference: Migration to Post-Quantum Cryptography · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security