Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PQC: Implement draft RFC for ML-KEM with X25519 #10

Merged
merged 9 commits into from
Nov 25, 2024
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@noble/curves": "^1.6.0",
"@noble/ed25519": "^1.7.3",
"@noble/hashes": "^1.5.0",
"@noble/post-quantum": "^0.2.1",
"@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git",
"@openpgp/tweetnacl": "^1.0.4-1",
Expand Down
80 changes: 75 additions & 5 deletions src/crypto/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, pri
const c = await modeInstance.encrypt(data, iv, new Uint8Array());
return { aeadMode: new AEADEnum(aeadMode), iv, c: new ShortByteString(c) };
}
case enums.publicKey.pqc_mlkem_x25519: {
const { eccPublicKey, mlkemPublicKey } = publicParams;
const { eccCipherText, mlkemCipherText, wrappedKey } = await publicKey.postQuantum.kem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data);
const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey });
return { eccCipherText, mlkemCipherText, C };
}
default:
return [];
}
Expand All @@ -115,8 +121,8 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, pri
* @throws {Error} on sensitive decryption error, unless `randomPayload` is given
* @async
*/
export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, sessionKeyParams, fingerprint, randomPayload) {
switch (algo) {
export async function publicKeyDecrypt(keyAlgo, publicKeyParams, privateKeyParams, sessionKeyParams, fingerprint, randomPayload) {
switch (keyAlgo) {
case enums.publicKey.rsaEncryptSign:
case enums.publicKey.rsaEncrypt: {
const { c } = sessionKeyParams;
Expand Down Expand Up @@ -146,7 +152,7 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
throw new Error('AES session key expected');
}
return publicKey.elliptic.ecdhX.decrypt(
algo, ephemeralPublicKey, C.wrappedKey, A, k);
keyAlgo, ephemeralPublicKey, C.wrappedKey, A, k);
}
case enums.publicKey.aead: {
const { cipher: algo } = publicKeyParams;
Expand All @@ -159,6 +165,12 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
const modeInstance = await mode(algoValue, keyMaterial);
return modeInstance.decrypt(c.data, iv, new Uint8Array());
}
case enums.publicKey.pqc_mlkem_x25519: {
const { eccSecretKey, mlkemSecretKey } = privateKeyParams;
const { eccPublicKey, mlkemPublicKey } = publicKeyParams;
const { eccCipherText, mlkemCipherText, C } = sessionKeyParams;
return publicKey.postQuantum.kem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, C.wrappedKey);
}
default:
throw new Error('Unknown public key encryption algorithm.');
}
Expand Down Expand Up @@ -230,6 +242,16 @@ export function parsePublicKeyParams(algo, bytes) {
const digest = bytes.subarray(read, read + digestLength); read += digestLength;
return { read: read, publicParams: { cipher: algo, digest } };
}
case enums.publicKey.pqc_mlkem_x25519: {
const eccPublicKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccPublicKey.length;
const mlkemPublicKey = util.readExactSubarray(bytes, read, read + 1184); read += mlkemPublicKey.length;
return { read, publicParams: { eccPublicKey, mlkemPublicKey } };
}
case enums.publicKey.pqc_mldsa_ed25519: {
const eccPublicKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.ed25519)); read += eccPublicKey.length;
const mldsaPublicKey = util.readExactSubarray(bytes, read, read + 1952); read += mldsaPublicKey.length;
return { read, publicParams: { eccPublicKey, mldsaPublicKey } };
}
default:
throw new UnsupportedError('Unknown public key encryption algorithm.');
}
Expand All @@ -242,7 +264,7 @@ export function parsePublicKeyParams(algo, bytes) {
* @param {Object} publicParams - (ECC and symmetric only) public params, needed to format some private params
* @returns {{ read: Number, privateParams: Object }} Number of read bytes plus the key parameters referenced by name.
*/
export function parsePrivateKeyParams(algo, bytes, publicParams) {
export async function parsePrivateKeyParams(algo, bytes, publicParams) {
let read = 0;
switch (algo) {
case enums.publicKey.rsaEncrypt:
Expand Down Expand Up @@ -301,6 +323,17 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) {
const keyMaterial = bytes.subarray(read, read + keySize); read += keySize;
return { read, privateParams: { hashSeed, keyMaterial } };
}
case enums.publicKey.pqc_mlkem_x25519: {
const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccSecretKey.length;
const mlkemSeed = util.readExactSubarray(bytes, read, read + 64); read += mlkemSeed.length;
const { mlkemSecretKey } = await publicKey.postQuantum.kem.mlkemExpandSecretSeed(algo, mlkemSeed);
return { read, privateParams: { eccSecretKey, mlkemSecretKey, mlkemSeed } };
}
case enums.publicKey.pqc_mldsa_ed25519: {
const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.ed25519)); read += eccSecretKey.length;
const mldsaSecretKey = util.readExactSubarray(bytes, read, read + 4032); read += mldsaSecretKey.length;
return { read, privateParams: { eccSecretKey, mldsaSecretKey } };
}
default:
throw new UnsupportedError('Unknown public key encryption algorithm.');
}
Expand Down Expand Up @@ -364,6 +397,12 @@ export function parseEncSessionKeyParams(algo, bytes) {

return { aeadMode, iv, c };
}
case enums.publicKey.pqc_mlkem_x25519: {
const eccCipherText = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccCipherText.length;
const mlkemCipherText = util.readExactSubarray(bytes, read, read + 1088); read += mlkemCipherText.length;
const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read));
return { eccCipherText, mlkemCipherText, C }; // eccCipherText || mlkemCipherText || len(C) || C
}
default:
throw new UnsupportedError('Unknown public key encryption algorithm.');
}
Expand All @@ -383,9 +422,20 @@ export function serializeParams(algo, params) {
enums.publicKey.ed448,
enums.publicKey.x448,
enums.publicKey.aead,
enums.publicKey.hmac
enums.publicKey.hmac,
enums.publicKey.pqc_mlkem_x25519,
enums.publicKey.pqc_mldsa_ed25519
]);

const excludedFields = {
[enums.publicKey.pqc_mlkem_x25519]: new Set(['mlkemSecretKey']) // only `mlkemSeed` is serialized
};

const orderedParams = Object.keys(params).map(name => {
if (excludedFields[algo]?.has(name)) {
return new Uint8Array();
}

const param = params[name];
if (!util.isUint8Array(param)) return param.write();
return algosWithNativeRepresentation.has(algo) ? param : util.uint8ArrayToMPI(param);
Expand Down Expand Up @@ -450,6 +500,16 @@ export async function generateParams(algo, bits, oid, symmetric) {
const keyMaterial = generateSessionKey(symmetric);
return createSymmetricParams(keyMaterial, new SymAlgoEnum(symmetric));
}
case enums.publicKey.pqc_mlkem_x25519:
return publicKey.postQuantum.kem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSeed, mlkemSecretKey, mlkemPublicKey }) => ({
privateParams: { eccSecretKey, mlkemSeed, mlkemSecretKey },
publicParams: { eccPublicKey, mlkemPublicKey }
}));
case enums.publicKey.pqc_mldsa_ed25519:
return publicKey.postQuantum.signature.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSecretKey, mldsaPublicKey }) => ({
privateParams: { eccSecretKey, mldsaSecretKey },
publicParams: { eccPublicKey, mldsaPublicKey }
}));
case enums.publicKey.dsa:
case enums.publicKey.elgamal:
throw new Error('Unsupported algorithm for key generation.');
Expand Down Expand Up @@ -541,6 +601,16 @@ export async function validateParams(algo, publicParams, privateParams) {
return keySize === keyMaterial.length &&
util.equalsUint8Array(digest, await hash.sha256(hashSeed));
}
case enums.publicKey.pqc_mlkem_x25519: {
const { eccSecretKey, mlkemSeed } = privateParams;
const { eccPublicKey, mlkemPublicKey } = publicParams;
return publicKey.postQuantum.kem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed);
}
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccSecretKey, mldsaSecretKey } = privateParams;
const { eccPublicKey, mldsaPublicKey } = publicParams;
return publicKey.postQuantum.signature.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSecretKey);
}
default:
throw new Error('Unknown public key algorithm.');
}
Expand Down
5 changes: 4 additions & 1 deletion src/crypto/public_key/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as elgamal from './elgamal';
import * as elliptic from './elliptic';
import * as dsa from './dsa';
import * as hmac from './hmac';
import * as postQuantum from './post_quantum';

export default {
/** @see module:crypto/public_key/rsa */
Expand All @@ -19,5 +20,7 @@ export default {
/** @see module:crypto/public_key/dsa */
dsa: dsa,
/** @see module:crypto/public_key/hmac */
hmac: hmac
hmac: hmac,
/** @see module:crypto/public_key/post_quantum */
postQuantum
};
5 changes: 5 additions & 0 deletions src/crypto/public_key/post_quantum/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as kem from './kem/index';

export {
kem
};
62 changes: 62 additions & 0 deletions src/crypto/public_key/post_quantum/kem/ecc_kem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as ecdhX from '../../elliptic/ecdh_x';
import hash from '../../../hash';
import util from '../../../../util';
import enums from '../../../../enums';

export async function generate(algo) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519: {
const { A, k } = await ecdhX.generate(enums.publicKey.x25519);
return {
eccPublicKey: A,
eccSecretKey: k
};
}
default:
throw new Error('Unsupported KEM algorithm');
}
}

export async function encaps(eccAlgo, eccRecipientPublicKey) {
switch (eccAlgo) {
case enums.publicKey.pqc_mlkem_x25519: {
const { ephemeralPublicKey: eccCipherText, sharedSecret: eccSharedSecret } = await ecdhX.generateEphemeralEncryptionMaterial(enums.publicKey.x25519, eccRecipientPublicKey);
const eccKeyShare = await hash.sha3_256(util.concatUint8Array([
eccSharedSecret,
eccCipherText,
eccRecipientPublicKey
]));
return {
eccCipherText,
eccKeyShare
};
}
default:
throw new Error('Unsupported KEM algorithm');
}
}

export async function decaps(eccAlgo, eccCipherText, eccSecretKey, eccPublicKey) {
switch (eccAlgo) {
case enums.publicKey.pqc_mlkem_x25519: {
const eccSharedSecret = await ecdhX.recomputeSharedSecret(enums.publicKey.x25519, eccCipherText, eccPublicKey, eccSecretKey);
const eccKeyShare = await hash.sha3_256(util.concatUint8Array([
eccSharedSecret,
eccCipherText,
eccPublicKey
]));
return eccKeyShare;
}
default:
throw new Error('Unsupported KEM algorithm');
}
}

export async function validateParams(algo, eccPublicKey, eccSecretKey) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519:
return ecdhX.validateParams(enums.publicKey.x25519, eccPublicKey, eccSecretKey);
default:
throw new Error('Unsupported KEM algorithm');
}
}
2 changes: 2 additions & 0 deletions src/crypto/public_key/post_quantum/kem/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { generate, encrypt, decrypt, validateParams } from './kem';
export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem';
55 changes: 55 additions & 0 deletions src/crypto/public_key/post_quantum/kem/kem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as eccKem from './ecc_kem';
import * as mlKem from './ml_kem';
import * as aesKW from '../../../aes_kw';
import util from '../../../../util';
import enums from '../../../../enums';
import hash from '../../../hash';

export async function generate(algo) {
const { eccPublicKey, eccSecretKey } = await eccKem.generate(algo);
const { mlkemPublicKey, mlkemSeed, mlkemSecretKey } = await mlKem.generate(algo);

return { eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed, mlkemSecretKey };
}

export async function encrypt(algo, eccPublicKey, mlkemPublicKey, sessioneKeyData) {
const { eccKeyShare, eccCipherText } = await eccKem.encaps(algo, eccPublicKey);
const { mlkemKeyShare, mlkemCipherText } = await mlKem.encaps(algo, mlkemPublicKey);
const kek = await multiKeyCombine(algo, eccKeyShare, eccCipherText, eccPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey);
const wrappedKey = await aesKW.wrap(enums.symmetric.aes256, kek, sessioneKeyData); // C
return { eccCipherText, mlkemCipherText, wrappedKey };
}

export async function decrypt(algo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, encryptedSessionKeyData) {
const eccKeyShare = await eccKem.decaps(algo, eccCipherText, eccSecretKey, eccPublicKey);
const mlkemKeyShare = await mlKem.decaps(algo, mlkemCipherText, mlkemSecretKey);
const kek = await multiKeyCombine(algo, eccKeyShare, eccCipherText, eccPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey);
const sessionKey = await aesKW.unwrap(enums.symmetric.aes256, kek, encryptedSessionKeyData);
return sessionKey;
}

async function multiKeyCombine(algo, ecdhKeyShare, ecdhCipherText, ecdhPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey) {
// LAMPS-aligned and NIST compatible combiner, proposed in: https://mailarchive.ietf.org/arch/msg/openpgp/NMTCy707LICtxIhP3Xt1U5C8MF0/
// 2a. KDF(mlkemSS || tradSS || tradCT || tradPK || Domain)
// where Domain is "Domain" for LAMPS, and "mlkemCT || mlkemPK || algId" for OpenPGP
const encData = util.concatUint8Array([
mlkemKeyShare,
ecdhKeyShare,
ecdhCipherText,
ecdhPublicKey,
// domSep
mlkemCipherText,
mlkemPublicKey,
new Uint8Array([algo])
]);

const kek = await hash.digest(enums.hash.sha3_256, encData);
return kek;
}

export async function validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed) {
const eccValidationPromise = eccKem.validateParams(algo, eccPublicKey, eccSecretKey);
const mlkemValidationPromise = mlKem.validateParams(algo, mlkemPublicKey, mlkemSeed);
const valid = await eccValidationPromise && await mlkemValidationPromise;
return valid;
}
Loading
Loading