WIP: Add ML-KEM

This commit is contained in:
larabr 2023-11-01 17:07:52 +01:00 committed by Daniel Huigens
parent c7ae1d88d8
commit b9b5667a3f
19 changed files with 539 additions and 16 deletions

27
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@noble/ciphers": "^1.0.0", "@noble/ciphers": "^1.0.0",
"@noble/curves": "^1.6.0", "@noble/curves": "^1.6.0",
"@noble/hashes": "^1.5.0", "@noble/hashes": "^1.5.0",
"@noble/post-quantum": "^0.2.0",
"@openpgp/jsdoc": "^3.6.11", "@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/seek-bzip": "^1.0.5-git",
"@openpgp/tweetnacl": "^1.0.4-1", "@openpgp/tweetnacl": "^1.0.4-1",
@ -968,6 +969,32 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/@noble/post-quantum": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.2.0.tgz",
"integrity": "sha512-6dXxLXv9qCdj22zTBIRN1J8RrF+OUWQD1vJHNcqCu4JAlSo7KnaRVc+ikDPqvgky43Rn7NGQoWqeo4wv8TAJ/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.4.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/post-quantum/node_modules/@noble/hashes": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View File

@ -65,6 +65,7 @@
"@noble/ciphers": "^1.0.0", "@noble/ciphers": "^1.0.0",
"@noble/curves": "^1.6.0", "@noble/curves": "^1.6.0",
"@noble/hashes": "^1.5.0", "@noble/hashes": "^1.5.0",
"@noble/post-quantum": "^0.2.0",
"@openpgp/jsdoc": "^3.6.11", "@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/seek-bzip": "^1.0.5-git",
"@openpgp/tweetnacl": "^1.0.4-1", "@openpgp/tweetnacl": "^1.0.4-1",

View File

@ -96,6 +96,12 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, pri
const c = await modeInstance.encrypt(data, iv, new Uint8Array()); const c = await modeInstance.encrypt(data, iv, new Uint8Array());
return { aeadMode: new AEADEnum(aeadMode), iv, c: new ShortByteString(c) }; 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: default:
return []; return [];
} }
@ -115,8 +121,8 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, pri
* @throws {Error} on sensitive decryption error, unless `randomPayload` is given * @throws {Error} on sensitive decryption error, unless `randomPayload` is given
* @async * @async
*/ */
export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, sessionKeyParams, fingerprint, randomPayload) { export async function publicKeyDecrypt(keyAlgo, publicKeyParams, privateKeyParams, sessionKeyParams, fingerprint, randomPayload) {
switch (algo) { switch (keyAlgo) {
case enums.publicKey.rsaEncryptSign: case enums.publicKey.rsaEncryptSign:
case enums.publicKey.rsaEncrypt: { case enums.publicKey.rsaEncrypt: {
const { c } = sessionKeyParams; const { c } = sessionKeyParams;
@ -146,7 +152,7 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
throw new Error('AES session key expected'); throw new Error('AES session key expected');
} }
return publicKey.elliptic.ecdhX.decrypt( return publicKey.elliptic.ecdhX.decrypt(
algo, ephemeralPublicKey, C.wrappedKey, A, k); keyAlgo, ephemeralPublicKey, C.wrappedKey, A, k);
} }
case enums.publicKey.aead: { case enums.publicKey.aead: {
const { cipher: algo } = publicKeyParams; const { cipher: algo } = publicKeyParams;
@ -159,6 +165,12 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
const modeInstance = await mode(algoValue, keyMaterial); const modeInstance = await mode(algoValue, keyMaterial);
return modeInstance.decrypt(c.data, iv, new Uint8Array()); 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: default:
throw new Error('Unknown public key encryption algorithm.'); throw new Error('Unknown public key encryption algorithm.');
} }
@ -230,6 +242,16 @@ export function parsePublicKeyParams(algo, bytes) {
const digest = bytes.subarray(read, read + digestLength); read += digestLength; const digest = bytes.subarray(read, read + digestLength); read += digestLength;
return { read: read, publicParams: { cipher: algo, digest } }; 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: default:
throw new UnsupportedError('Unknown public key encryption algorithm.'); throw new UnsupportedError('Unknown public key encryption algorithm.');
} }
@ -242,7 +264,7 @@ export function parsePublicKeyParams(algo, bytes) {
* @param {Object} publicParams - (ECC and symmetric only) public params, needed to format some private params * @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. * @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; let read = 0;
switch (algo) { switch (algo) {
case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncrypt:
@ -301,6 +323,17 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) {
const keyMaterial = bytes.subarray(read, read + keySize); read += keySize; const keyMaterial = bytes.subarray(read, read + keySize); read += keySize;
return { read, privateParams: { hashSeed, keyMaterial } }; 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: default:
throw new UnsupportedError('Unknown public key encryption algorithm.'); throw new UnsupportedError('Unknown public key encryption algorithm.');
} }
@ -364,6 +397,12 @@ export function parseEncSessionKeyParams(algo, bytes) {
return { aeadMode, iv, c }; 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: default:
throw new UnsupportedError('Unknown public key encryption algorithm.'); throw new UnsupportedError('Unknown public key encryption algorithm.');
} }
@ -383,9 +422,20 @@ export function serializeParams(algo, params) {
enums.publicKey.ed448, enums.publicKey.ed448,
enums.publicKey.x448, enums.publicKey.x448,
enums.publicKey.aead, 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 => { const orderedParams = Object.keys(params).map(name => {
if (excludedFields[algo]?.has(name)) {
return new Uint8Array();
}
const param = params[name]; const param = params[name];
if (!util.isUint8Array(param)) return param.write(); if (!util.isUint8Array(param)) return param.write();
return algosWithNativeRepresentation.has(algo) ? param : util.uint8ArrayToMPI(param); return algosWithNativeRepresentation.has(algo) ? param : util.uint8ArrayToMPI(param);
@ -450,6 +500,16 @@ export async function generateParams(algo, bits, oid, symmetric) {
const keyMaterial = generateSessionKey(symmetric); const keyMaterial = generateSessionKey(symmetric);
return createSymmetricParams(keyMaterial, new SymAlgoEnum(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.dsa:
case enums.publicKey.elgamal: case enums.publicKey.elgamal:
throw new Error('Unsupported algorithm for key generation.'); throw new Error('Unsupported algorithm for key generation.');
@ -541,6 +601,16 @@ export async function validateParams(algo, publicParams, privateParams) {
return keySize === keyMaterial.length && return keySize === keyMaterial.length &&
util.equalsUint8Array(digest, await hash.sha256(hashSeed)); 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: default:
throw new Error('Unknown public key algorithm.'); throw new Error('Unknown public key algorithm.');
} }

View File

@ -8,6 +8,7 @@ import * as elgamal from './elgamal';
import * as elliptic from './elliptic'; import * as elliptic from './elliptic';
import * as dsa from './dsa'; import * as dsa from './dsa';
import * as hmac from './hmac'; import * as hmac from './hmac';
import * as postQuantum from './post_quantum';
export default { export default {
/** @see module:crypto/public_key/rsa */ /** @see module:crypto/public_key/rsa */
@ -19,5 +20,7 @@ export default {
/** @see module:crypto/public_key/dsa */ /** @see module:crypto/public_key/dsa */
dsa: dsa, dsa: dsa,
/** @see module:crypto/public_key/hmac */ /** @see module:crypto/public_key/hmac */
hmac: hmac hmac: hmac,
/** @see module:crypto/public_key/post_quantum */
postQuantum
}; };

View File

@ -0,0 +1,5 @@
import * as kem from './kem/index';
export {
kem
};

View File

@ -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');
}
}

View File

@ -0,0 +1,2 @@
export { generate, encrypt, decrypt, validateParams } from './kem';
export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem';

View File

@ -0,0 +1,52 @@
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';
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) {
const { kmac256 } = await import('@noble/hashes/sha3-addons');
const key = util.concatUint8Array([mlkemKeyShare, ecdhKeyShare]);
const encData = util.concatUint8Array([
mlkemCipherText,
ecdhCipherText,
mlkemPublicKey,
ecdhPublicKey,
new Uint8Array([algo])
]);
const domainSeparation = util.encodeUTF8('OpenPGPCompositeKDFv1');
const kek = kmac256(key, encData, { personalization: domainSeparation }); // output length: 256 bits
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;
}

View File

@ -0,0 +1,72 @@
import enums from '../../../../enums';
import util from '../../../../util';
import { getRandomBytes } from '../../../random';
export async function generate(algo) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519: {
const mlkemSeed = getRandomBytes(64);
const { mlkemSecretKey, mlkemPublicKey } = await expandSecretSeed(algo, mlkemSeed);
return { mlkemSeed, mlkemSecretKey, mlkemPublicKey };
}
default:
throw new Error('Unsupported KEM algorithm');
}
}
/**
* Expand ML-KEM secret seed and retrieve the secret and public key material
* @param {module:enums.publicKey} algo - Public key algorithm
* @param {Uint8Array} seed - secret seed to expand
* @returns {Promise<{ mlkemPublicKey: Uint8Array, mlkemSecretKey: Uint8Array }>}
*/
export async function expandSecretSeed(algo, seed) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519: {
const { ml_kem768 } = await import('@noble/post-quantum/ml-kem');
const { publicKey: encapsulationKey, secretKey: decapsulationKey } = ml_kem768.keygen(seed);
return { mlkemPublicKey: encapsulationKey, mlkemSecretKey: decapsulationKey };
}
default:
throw new Error('Unsupported KEM algorithm');
}
}
export async function encaps(algo, mlkemRecipientPublicKey) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519: {
const { ml_kem768 } = await import('@noble/post-quantum/ml-kem');
const { cipherText: mlkemCipherText, sharedSecret: mlkemKeyShare } = ml_kem768.encapsulate(mlkemRecipientPublicKey);
return { mlkemCipherText, mlkemKeyShare };
}
default:
throw new Error('Unsupported KEM algorithm');
}
}
export async function decaps(algo, mlkemCipherText, mlkemSecretKey) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519: {
const { ml_kem768 } = await import('@noble/post-quantum/ml-kem');
const mlkemKeyShare = ml_kem768.decapsulate(mlkemCipherText, mlkemSecretKey);
return mlkemKeyShare;
}
default:
throw new Error('Unsupported KEM algorithm');
}
}
export async function validateParams(algo, mlkemPublicKey, mlkemSeed) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519: {
const { mlkemPublicKey: expectedPublicKey } = await expandSecretSeed(algo, mlkemSeed);
return util.equalsUint8Array(mlkemPublicKey, expectedPublicKey);
}
default:
throw new Error('Unsupported KEM algorithm');
}
}

View File

@ -96,6 +96,8 @@ export default {
ed25519: 27, ed25519: 27,
/** Ed448 (Sign only) */ /** Ed448 (Sign only) */
ed448: 28, ed448: 28,
/** Post-quantum ML-KEM-768 + X25519 (Encrypt only) */
pqc_mlkem_x25519: 105,
/** Persistent symmetric keys: encryption algorithm */ /** Persistent symmetric keys: encryption algorithm */
aead: 100, aead: 100,
/** Persistent symmetric keys: authentication algorithm */ /** Persistent symmetric keys: authentication algorithm */

View File

@ -397,6 +397,13 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) {
options.sign = options.sign || false; options.sign = options.sign || false;
switch (options.type) { switch (options.type) {
case 'pqc':
if (options.sign) {
throw new Error('Post-quantum signing algorithms are not yet supported.');
} else {
options.algorithm = enums.publicKey.pqc_mlkem_x25519;
}
break;
case 'ecc': // NB: this case also handles legacy eddsa and x25519 keys, based on `options.curve` case 'ecc': // NB: this case also handles legacy eddsa and x25519 keys, based on `options.curve`
try { try {
options.curve = enums.write(enums.curve, options.curve); options.curve = enums.write(enums.curve, options.curve);
@ -474,6 +481,7 @@ export function validateEncryptionKeyPacket(keyPacket, signature, config) {
case enums.publicKey.x25519: case enums.publicKey.x25519:
case enums.publicKey.x448: case enums.publicKey.x448:
case enums.publicKey.aead: case enums.publicKey.aead:
case enums.publicKey.pqc_mlkem_x25519:
if (!signature.keyFlags && !config.allowMissingKeyFlags) { if (!signature.keyFlags && !config.allowMissingKeyFlags) {
throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`');
} }
@ -496,7 +504,8 @@ export function validateDecryptionKeyPacket(keyPacket, signature, config) {
case enums.publicKey.elgamal: case enums.publicKey.elgamal:
case enums.publicKey.ecdh: case enums.publicKey.ecdh:
case enums.publicKey.x25519: case enums.publicKey.x25519:
case enums.publicKey.x448: { case enums.publicKey.x448:
case enums.publicKey.pqc_mlkem_x25519: {
const isValidSigningKeyPacket = !signature.keyFlags || (signature.keyFlags[0] & enums.keyFlags.signData) !== 0; const isValidSigningKeyPacket = !signature.keyFlags || (signature.keyFlags[0] & enums.keyFlags.signData) !== 0;
if (isValidSigningKeyPacket && config.allowInsecureDecryptionWithSigningKeys) { if (isValidSigningKeyPacket && config.allowInsecureDecryptionWithSigningKeys) {
// This is only relevant for RSA keys, all other signing algorithms cannot decrypt // This is only relevant for RSA keys, all other signing algorithms cannot decrypt

View File

@ -138,6 +138,10 @@ class PublicKeyPacket {
) { ) {
throw new Error('Legacy curve25519 cannot be used with v6 keys'); throw new Error('Legacy curve25519 cannot be used with v6 keys');
} }
// The composite ML-KEM + ECDH schemes MUST be used only with v6 keys.
if (this.version !== 6 && this.algorithm === enums.publicKey.pqc_mlkem_x25519) {
throw new Error('Unexpected key version: ML-KEM algorithms can only be used with v6 keys');
}
this.publicParams = publicParams; this.publicParams = publicParams;
pos += read; pos += read;

View File

@ -21,6 +21,12 @@ import enums from '../enums';
import util from '../util'; import util from '../util';
import { UnsupportedError } from './packet'; import { UnsupportedError } from './packet';
const algosWithV3CleartextSessionKeyAlgorithm = new Set([
enums.publicKey.x25519,
enums.publicKey.x448,
enums.publicKey.pqc_mlkem_x25519
]);
/** /**
* Public-Key Encrypted Session Key Packets (Tag 1) * Public-Key Encrypted Session Key Packets (Tag 1)
* *
@ -128,7 +134,7 @@ class PublicKeyEncryptedSessionKeyPacket {
} }
this.publicKeyAlgorithm = bytes[offset++]; this.publicKeyAlgorithm = bytes[offset++];
this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset)); this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset));
if (this.publicKeyAlgorithm === enums.publicKey.x25519 || this.publicKeyAlgorithm === enums.publicKey.x448) { if (algosWithV3CleartextSessionKeyAlgorithm.has(this.publicKeyAlgorithm)) {
if (this.version === 3) { if (this.version === 3) {
this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm); this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm);
} else if (this.encrypted.C.algorithm !== null) { } else if (this.encrypted.C.algorithm !== null) {
@ -211,7 +217,7 @@ class PublicKeyEncryptedSessionKeyPacket {
if (this.version === 3) { if (this.version === 3) {
// v3 Montgomery curves have cleartext cipher algo // v3 Montgomery curves have cleartext cipher algo
const hasEncryptedAlgo = this.publicKeyAlgorithm !== enums.publicKey.x25519 && this.publicKeyAlgorithm !== enums.publicKey.x448; const hasEncryptedAlgo = !algosWithV3CleartextSessionKeyAlgorithm.has(this.publicKeyAlgorithm);
this.sessionKeyAlgorithm = hasEncryptedAlgo ? sessionKeyAlgorithm : this.sessionKeyAlgorithm; this.sessionKeyAlgorithm = hasEncryptedAlgo ? sessionKeyAlgorithm : this.sessionKeyAlgorithm;
if (sessionKey.length !== crypto.getCipherParams(this.sessionKeyAlgorithm).keySize) { if (sessionKey.length !== crypto.getCipherParams(this.sessionKeyAlgorithm).keySize) {
@ -240,6 +246,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) {
]); ]);
case enums.publicKey.x25519: case enums.publicKey.x25519:
case enums.publicKey.x448: case enums.publicKey.x448:
case enums.publicKey.pqc_mlkem_x25519:
return sessionKeyData; return sessionKeyData;
default: default:
throw new Error('Unsupported public key algorithm'); throw new Error('Unsupported public key algorithm');
@ -288,6 +295,7 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) {
} }
case enums.publicKey.x25519: case enums.publicKey.x25519:
case enums.publicKey.x448: case enums.publicKey.x448:
case enums.publicKey.pqc_mlkem_x25519:
return { return {
sessionKeyAlgorithm: null, sessionKeyAlgorithm: null,
sessionKey: decryptedData sessionKey: decryptedData

View File

@ -221,7 +221,7 @@ class SecretKeyPacket extends PublicKeyPacket {
} }
} }
try { try {
const { read, privateParams } = crypto.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); const { read, privateParams } = await crypto.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams);
if (read < cleartext.length) { if (read < cleartext.length) {
throw new Error('Error reading MPIs'); throw new Error('Error reading MPIs');
} }
@ -479,7 +479,7 @@ class SecretKeyPacket extends PublicKeyPacket {
} }
try { try {
const { privateParams } = crypto.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); const { privateParams } = await crypto.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams);
this.privateParams = privateParams; this.privateParams = privateParams;
} catch (err) { } catch (err) {
throw new Error('Error reading MPIs'); throw new Error('Error reading MPIs');
@ -532,6 +532,9 @@ class SecretKeyPacket extends PublicKeyPacket {
)) { )) {
throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${curve}. Generate a key of type 'curve25519' instead`); throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${curve}. Generate a key of type 'curve25519' instead`);
} }
if (this.version !== 6 && this.algorithm === enums.publicKey.pqc_mlkem_x25519) {
throw new Error(`Cannot generate v${this.version} keys of type 'pqc'. Generate a v6 key instead`);
}
const { privateParams, publicParams } = await crypto.generateParams(this.algorithm, bits, curve, symmetric); const { privateParams, publicParams } = await crypto.generateParams(this.algorithm, bits, curve, symmetric);
this.privateParams = privateParams; this.privateParams = privateParams;
this.publicParams = publicParams; this.publicParams = publicParams;

View File

@ -6,7 +6,7 @@ import openpgp from '../initOpenpgp.js';
import crypto from '../../src/crypto'; import crypto from '../../src/crypto';
import util from '../../src/util.js'; import util from '../../src/util.js';
export default () => describe('API functional testing', function() { export default () => describe('API functional testing', async function() {
const RSAPublicKeyMaterial = util.concatUint8Array([ const RSAPublicKeyMaterial = util.concatUint8Array([
new Uint8Array([0x08,0x00,0xac,0x15,0xb3,0xd6,0xd2,0x0f,0xf0,0x7a,0xdd,0x21,0xb7, new Uint8Array([0x08,0x00,0xac,0x15,0xb3,0xd6,0xd2,0x0f,0xf0,0x7a,0xdd,0x21,0xb7,
0xbf,0x61,0xfa,0xca,0x93,0x86,0xc8,0x55,0x5a,0x4b,0xa6,0xa4,0x1a, 0xbf,0x61,0xfa,0xca,0x93,0x86,0xc8,0x55,0x5a,0x4b,0xa6,0xa4,0x1a,
@ -196,15 +196,15 @@ export default () => describe('API functional testing', function() {
const algoRSA = openpgp.enums.publicKey.rsaEncryptSign; const algoRSA = openpgp.enums.publicKey.rsaEncryptSign;
const RSAPublicParams = crypto.parsePublicKeyParams(algoRSA, RSAPublicKeyMaterial).publicParams; const RSAPublicParams = crypto.parsePublicKeyParams(algoRSA, RSAPublicKeyMaterial).publicParams;
const RSAPrivateParams = crypto.parsePrivateKeyParams(algoRSA, RSAPrivateKeyMaterial).privateParams; const RSAPrivateParams = (await crypto.parsePrivateKeyParams(algoRSA, RSAPrivateKeyMaterial)).privateParams;
const algoDSA = openpgp.enums.publicKey.dsa; const algoDSA = openpgp.enums.publicKey.dsa;
const DSAPublicParams = crypto.parsePublicKeyParams(algoDSA, DSAPublicKeyMaterial).publicParams; const DSAPublicParams = crypto.parsePublicKeyParams(algoDSA, DSAPublicKeyMaterial).publicParams;
const DSAPrivateParams = crypto.parsePrivateKeyParams(algoDSA, DSAPrivateKeyMaterial).privateParams; const DSAPrivateParams = (await crypto.parsePrivateKeyParams(algoDSA, DSAPrivateKeyMaterial)).privateParams;
const algoElGamal = openpgp.enums.publicKey.elgamal; const algoElGamal = openpgp.enums.publicKey.elgamal;
const elGamalPublicParams = crypto.parsePublicKeyParams(algoElGamal, elGamalPublicKeyMaterial).publicParams; const elGamalPublicParams = crypto.parsePublicKeyParams(algoElGamal, elGamalPublicKeyMaterial).publicParams;
const elGamalPrivateParams = crypto.parsePrivateKeyParams(algoElGamal, elGamalPrivateKeyMaterial).privateParams; const elGamalPrivateParams = (await crypto.parsePrivateKeyParams(algoElGamal, elGamalPrivateKeyMaterial)).privateParams;
const data = util.stringToUint8Array('foobar'); const data = util.stringToUint8Array('foobar');

View File

@ -14,6 +14,7 @@ import testEAX from './eax';
import testOCB from './ocb'; import testOCB from './ocb';
import testRSA from './rsa'; import testRSA from './rsa';
import testValidate from './validate'; import testValidate from './validate';
import testPQC from './postQuantum';
export default () => describe('Crypto', function () { export default () => describe('Crypto', function () {
testBigInteger(); testBigInteger();
@ -32,4 +33,5 @@ export default () => describe('Crypto', function () {
testOCB(); testOCB();
testRSA(); testRSA();
testValidate(); testValidate();
testPQC();
}); });

158
test/crypto/postQuantum.js Normal file
View File

@ -0,0 +1,158 @@
import { use as chaiUse, expect } from 'chai';
import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import
chaiUse(chaiAsPromised);
import openpgp from '../initOpenpgp.js';
import { generateParams, publicKeyEncrypt, publicKeyDecrypt } from '../../src/crypto/crypto.js';
export default () => describe('PQC', function () {
it('ML-KEM + X25519 - Generate/encrypt/decrypt', async function () {
const sessionKey = { data: new Uint8Array(16).fill(1), algorithm: 'aes128' };
const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.pqc_mlkem_x25519);
const encryptedSessionKeyParams = await publicKeyEncrypt(openpgp.enums.publicKey.pqc_mlkem_x25519, undefined, publicParams, null, sessionKey.data);
const decryptedSessionKey = await publicKeyDecrypt(openpgp.enums.publicKey.pqc_mlkem_x25519, publicParams, privateParams, encryptedSessionKeyParams);
expect(decryptedSessionKey).to.deep.equal(sessionKey.data);
});
it('ML-KEM + X25519 - private key is correctly serialized using the seed instead of the expanded secret key material', async function () {
const armoredKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
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
BoWQifC9dbHD5JNv0/6CMXFZagQABA==
-----END PGP PRIVATE KEY BLOCK-----`;
const { data: expectedBinaryKey } = await openpgp.unarmor(armoredKey);
const privateKey = await openpgp.readKey({ armoredKey });
expect(privateKey.write()).to.deep.equal(expectedBinaryKey);
});
it('ML-KEM + X25519 - Test vector', async function () {
const armoredMessage = `-----BEGIN PGP MESSAGE-----
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
-----END PGP MESSAGE-----`;
const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK-----
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
BoWQifC9dbHD5JNv0/6CMXFZagQABA==
-----END PGP PRIVATE KEY BLOCK-----`
});
const { data: decryptedData } = await openpgp.decrypt({
message: await openpgp.readMessage({ armoredMessage }),
decryptionKeys: privateKey
});
expect(decryptedData).to.equal('Testing\n');
});
});

View File

@ -81,7 +81,7 @@ async function cloneKeyPacket(key) {
} }
async function generatePrivateKeyObject(options) { async function generatePrivateKeyObject(options) {
const config = { rejectCurves: new Set() }; const config = { rejectCurves: new Set(), ...options.config };
const { privateKey } = await openpgp.generateKey({ ...options, userIDs: [{ name: 'Test', email: 'test@test.com' }], format: 'object', config }); const { privateKey } = await openpgp.generateKey({ ...options, userIDs: [{ name: 'Test', email: 'test@test.com' }], format: 'object', config });
return privateKey; return privateKey;
} }
@ -314,6 +314,38 @@ export default () => {
}); });
}); });
describe('PQC parameter validation', function() {
let pqcEncryptionSubkey;
before(async () => {
const key = await generatePrivateKeyObject({ type: 'symmetric', subkeys: [{ type: 'pqc', config: { v6Keys: true } }] });
pqcEncryptionSubkey = key.subkeys[0];
});
async function cloneSubeyPacket(subkey) {
const subkeyPacket = new openpgp.SecretSubkeyPacket();
await subkeyPacket.read(subkey.keyPacket.write());
return subkeyPacket;
}
it('generated params are valid', async function() {
await expect(pqcEncryptionSubkey.keyPacket.validate()).to.not.be.rejected;
});
it('detect invalid ML-KEM public key part', async function() {
const keyPacket = await cloneSubeyPacket(pqcEncryptionSubkey);
const { mlkemPublicKey } = keyPacket.publicParams;
mlkemPublicKey[0]++;
await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid');
});
it('detect invalid ECC-KEM key part', async function() {
const keyPacket = await cloneSubeyPacket(pqcEncryptionSubkey);
const { eccPublicKey } = keyPacket.publicParams;
eccPublicKey[0]++;
await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid');
});
});
describe('DSA parameter validation', function() { describe('DSA parameter validation', function() {
let dsaKey; let dsaKey;
before(async () => { before(async () => {

View File

@ -4607,6 +4607,17 @@ I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg==
expect(v6Key.subkeys).to.have.length(1); expect(v6Key.subkeys).to.have.length(1);
}); });
it('should throw when trying to add a ML-KEM PQC key to a v4 key', async function() {
const v4Key = await openpgp.decryptKey({
privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }),
passphrase: 'hello world'
});
expect(v4Key.keyPacket.version).to.equal(4);
expect(v4Key.subkeys).to.have.length(1);
await expect(v4Key.addSubkey({ type: 'pqc', sign: false })).to.be.rejectedWith(/Cannot generate v4 keys of type 'pqc'/);
expect(v4Key.subkeys).to.have.length(1);
});
it('should throw when trying to encrypt a subkey separately from key', async function() { it('should throw when trying to encrypt a subkey separately from key', async function() {
const privateKey = await openpgp.decryptKey({ const privateKey = await openpgp.decryptKey({
privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }), privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }),