← CCNP Security: SCOR core and operations
21 / 25 · 55 MIN

Cryptography: functions, trust and migration

Run post-quantum primitives and distinguish shared secrets, signatures, trust and service compatibility.

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))
IN PRACTICE

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

Take this idea with you

Correct primitives are part of a solution; trust, protocol and operation need their own evidence.

Create account

Reference: Migration to Post-Quantum Cryptography · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security

CCNP® and Cisco® are registered trademarks of Cisco Systems, Inc. and/or its affiliates. bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by Cisco. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.