mirror of
https://github.com/openpgpjs/openpgpjs.git
synced 2025-07-04 03:52:29 +00:00
WIP: Add ML-KEM
This commit is contained in:
parent
c7ae1d88d8
commit
b9b5667a3f
27
package-lock.json
generated
27
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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.');
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
5
src/crypto/public_key/post_quantum/index.js
Normal file
5
src/crypto/public_key/post_quantum/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as kem from './kem/index';
|
||||||
|
|
||||||
|
export {
|
||||||
|
kem
|
||||||
|
};
|
62
src/crypto/public_key/post_quantum/kem/ecc_kem.js
Normal file
62
src/crypto/public_key/post_quantum/kem/ecc_kem.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
2
src/crypto/public_key/post_quantum/kem/index.js
Normal file
2
src/crypto/public_key/post_quantum/kem/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { generate, encrypt, decrypt, validateParams } from './kem';
|
||||||
|
export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem';
|
52
src/crypto/public_key/post_quantum/kem/kem.js
Normal file
52
src/crypto/public_key/post_quantum/kem/kem.js
Normal 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;
|
||||||
|
}
|
72
src/crypto/public_key/post_quantum/kem/ml_kem.js
Normal file
72
src/crypto/public_key/post_quantum/kem/ml_kem.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@ -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 */
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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
158
test/crypto/postQuantum.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
@ -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 () => {
|
||||||
|
@ -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 }),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user