PQC: implement ML-KEM and ML-DSA from RFC draft-10 (past last call)

See MRs #10, #13, #19 for the incremental changes .

Reference: https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/10/ .

Co-authored-by: Daniel Huigens <d.huigens@protonmail.com>
This commit is contained in:
larabr 2024-11-25 11:24:48 +01:00 committed by larabr
parent 09263d1a4a
commit 2e0f652e85
No known key found for this signature in database
GPG key ID: 2A4BEC40729185DD
30 changed files with 1736 additions and 48 deletions

View file

@ -4,7 +4,8 @@
"id": "sop-openpgpjs-branch",
"path": "__SOP_OPENPGPJS__",
"env": {
"OPENPGPJS_PATH": "__OPENPGPJS_BRANCH__"
"OPENPGPJS_PATH": "__OPENPGPJS_BRANCH__",
"OPENPGPJS_CUSTOM_PROFILES": "{\"generate-key\": { \"post-quantum\": { \"description\": \"generate post-quantum v6 keys (relying on ML-DSA + ML-KEM)\", \"options\": { \"type\": \"pqc\", \"config\": { \"v6Keys\": true } } } } }"
}
},
{

2
openpgp.d.ts vendored
View file

@ -678,7 +678,7 @@ export type EllipticCurveName = 'ed25519Legacy' | 'curve25519Legacy' | 'nistP256
interface GenerateKeyOptions {
userIDs: MaybeArray<UserID>;
passphrase?: string;
type?: 'ecc' | 'rsa' | 'curve25519' | 'curve448';
type?: 'ecc' | 'rsa' | 'curve25519' | 'curve448' | 'pqc';
curve?: EllipticCurveName;
rsaBits?: number;
keyExpirationTime?: number;

27
package-lock.json generated
View file

@ -12,6 +12,7 @@
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.6",
"@noble/hashes": "^1.8.0",
"@noble/post-quantum": "^0.2.1",
"@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git",
"@openpgp/tweetnacl": "^1.0.4-2",
@ -1000,6 +1001,32 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/post-quantum": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.2.1.tgz",
"integrity": "sha512-ImgfMp9notXSEocz464o1AefYfFWEkkszKMGO+ZiTn73yIBFeNyEHKQUMS+SheJwSNymldSts6YyVcQDjcnVVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.6.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/post-quantum/node_modules/@noble/hashes": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz",
"integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View file

@ -66,6 +66,7 @@
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.6",
"@noble/hashes": "^1.8.0",
"@noble/post-quantum": "^0.2.1",
"@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git",
"@openpgp/tweetnacl": "^1.0.4-2",

View file

@ -23,7 +23,7 @@
* @module crypto/crypto
*/
import { rsa, elliptic, elgamal, dsa, hmac } from './public_key';
import { rsa, elliptic, elgamal, dsa, hmac, postQuantum } from './public_key';
import { getRandomBytes } from './random';
import { getCipherParams } from './cipher';
import ECDHSymkey from '../type/ecdh_symkey';
@ -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 postQuantum.kem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data);
const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey });
return { eccCipherText, mlkemCipherText, C };
}
default:
return [];
}
@ -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;
@ -146,7 +152,7 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
throw new Error('AES session key expected');
}
return elliptic.ecdhX.decrypt(
algo, ephemeralPublicKey, C.wrappedKey, A, k);
keyAlgo, ephemeralPublicKey, C.wrappedKey, A, k);
}
case enums.publicKey.aead: {
const { cipher: algo } = publicKeyParams;
@ -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 postQuantum.kem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, C.wrappedKey);
}
default:
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;
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.');
}
@ -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:
@ -301,6 +323,18 @@ 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 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 mldsaSeed = util.readExactSubarray(bytes, read, read + 32); read += mldsaSeed.length;
const { mldsaSecretKey } = await postQuantum.signature.mldsaExpandSecretSeed(algo, mldsaSeed);
return { read, privateParams: { eccSecretKey, mldsaSecretKey, mldsaSeed } };
}
default:
throw new UnsupportedError('Unknown public key encryption algorithm.');
}
@ -364,6 +398,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.');
}
@ -383,9 +423,21 @@ 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
[enums.publicKey.pqc_mldsa_ed25519]: new Set(['mldsaSecretKey']) // only `mldsaSeed` 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);
@ -450,6 +502,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 postQuantum.kem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSeed, mlkemSecretKey, mlkemPublicKey }) => ({
privateParams: { eccSecretKey, mlkemSeed, mlkemSecretKey },
publicParams: { eccPublicKey, mlkemPublicKey }
}));
case enums.publicKey.pqc_mldsa_ed25519:
return postQuantum.signature.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }) => ({
privateParams: { eccSecretKey, mldsaSeed, mldsaSecretKey },
publicParams: { eccPublicKey, mldsaPublicKey }
}));
case enums.publicKey.dsa:
case enums.publicKey.elgamal:
throw new Error('Unsupported algorithm for key generation.');
@ -541,6 +603,16 @@ export async function validateParams(algo, publicParams, privateParams) {
return keySize === keyMaterial.length &&
util.equalsUint8Array(digest, await computeDigest(enums.hash.sha256, hashSeed));
}
case enums.publicKey.pqc_mlkem_x25519: {
const { eccSecretKey, mlkemSeed } = privateParams;
const { eccPublicKey, mlkemPublicKey } = publicParams;
return postQuantum.kem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed);
}
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccSecretKey, mldsaSeed } = privateParams;
const { eccPublicKey, mldsaPublicKey } = publicParams;
return postQuantum.signature.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed);
}
default:
throw new Error('Unknown public key algorithm.');
}

View file

@ -22,7 +22,6 @@
import util from '../../../util';
import enums from '../../../enums';
import { getHashByteLength } from '../../hash';
import { getRandomBytes } from '../../random';
import { b64ToUint8Array, uint8ArrayToB64 } from '../../../encoding/base64';
@ -89,12 +88,6 @@ export async function generate(algo) {
* @async
*/
export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashed) {
if (getHashByteLength(hashAlgo) < getHashByteLength(getPreferredHashAlgo(algo))) {
// Enforce digest sizes:
// - Ed25519: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.4-4
// - Ed448: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.5-4
throw new Error('Hash algorithm too weak for EdDSA.');
}
switch (algo) {
case enums.publicKey.ed25519:
try {
@ -140,12 +133,6 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe
* @async
*/
export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) {
if (getHashByteLength(hashAlgo) < getHashByteLength(getPreferredHashAlgo(algo))) {
// Enforce digest sizes:
// - Ed25519: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.4-4
// - Ed448: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.5-4
throw new Error('Hash algorithm too weak for EdDSA.');
}
switch (algo) {
case enums.publicKey.ed25519:
try {

View file

@ -23,7 +23,6 @@
import util from '../../../util';
import enums from '../../../enums';
import { getHashByteLength } from '../../hash';
import { CurveWithOID, checkPublicPointEnconding } from './oid_curves';
import { sign as eddsaSign, verify as eddsaVerify, validateParams as eddsaValidateParams } from './eddsa';
@ -44,12 +43,6 @@ import { sign as eddsaSign, verify as eddsaVerify, validateParams as eddsaValida
export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed) {
const curve = new CurveWithOID(oid);
checkPublicPointEnconding(curve, publicKey);
if (getHashByteLength(hashAlgo) < getHashByteLength(enums.hash.sha256)) {
// Enforce digest sizes, since the constraint was already present in RFC4880bis:
// see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2
// and https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.3-3
throw new Error('Hash algorithm too weak for EdDSA.');
}
const { RS: signature } = await eddsaSign(enums.publicKey.ed25519, hashAlgo, message, publicKey.subarray(1), privateKey, hashed);
// EdDSA signature params are returned in little-endian format
return {
@ -73,12 +66,6 @@ export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed
export async function verify(oid, hashAlgo, { r, s }, m, publicKey, hashed) {
const curve = new CurveWithOID(oid);
checkPublicPointEnconding(curve, publicKey);
if (getHashByteLength(hashAlgo) < getHashByteLength(enums.hash.sha256)) {
// Enforce digest sizes, since the constraint was already present in RFC4880bis:
// see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2
// and https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.3-3
throw new Error('Hash algorithm too weak for EdDSA.');
}
const RS = util.concatUint8Array([r, s]);
return eddsaVerify(enums.publicKey.ed25519, hashAlgo, { RS }, m, publicKey.subarray(1), hashed);
}

View file

@ -8,3 +8,4 @@ export * as elgamal from './elgamal';
export * as elliptic from './elliptic';
export * as dsa from './dsa';
export * as hmac from './hmac';
export * as postQuantum from './post_quantum';

View file

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

View file

@ -0,0 +1,51 @@
import * as ecdhX from '../../elliptic/ecdh_x';
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: eccKeyShare } = await ecdhX.generateEphemeralEncryptionMaterial(enums.publicKey.x25519, 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 eccKeyShare = await ecdhX.recomputeSharedSecret(enums.publicKey.x25519, eccCipherText, eccPublicKey, eccSecretKey);
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,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 { computeDigest } 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, mlkemKeyShare, eccKeyShare, eccCipherText, eccPublicKey);
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, mlkemKeyShare, eccKeyShare, eccCipherText, eccPublicKey);
const sessionKey = await aesKW.unwrap(enums.symmetric.aes256, kek, encryptedSessionKeyData);
return sessionKey;
}
/**
* KEM key combiner
*/
async function multiKeyCombine(algo, mlkemKeyShare, ecdhKeyShare, ecdhCipherText, ecdhPublicKey) {
const domSep = util.encodeUTF8('OpenPGPCompositeKDFv1');
const encData = util.concatUint8Array([
mlkemKeyShare,
ecdhKeyShare,
ecdhCipherText,
ecdhPublicKey,
new Uint8Array([algo]),
domSep,
new Uint8Array([domSep.length])
]);
const kek = await computeDigest(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;
}

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

@ -0,0 +1,10 @@
/**
* This file is needed to dynamic import noble-post-quantum libs.
* Separate dynamic imports are not convenient as they result in multiple chunks,
* which ultimately share a lot of code and need to be imported together
* when it comes to Proton's ML-DSA + ML-KEM keys.
*/
export { ml_kem768 } from '@noble/post-quantum/ml-kem';
export { ml_dsa65 } from '@noble/post-quantum/ml-dsa';

View file

@ -0,0 +1,46 @@
import * as eddsa from '../../elliptic/eddsa';
import enums from '../../../../enums';
export async function generate(algo) {
switch (algo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const { A, seed } = await eddsa.generate(enums.publicKey.ed25519);
return {
eccPublicKey: A,
eccSecretKey: seed
};
}
default:
throw new Error('Unsupported signature algorithm');
}
}
export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest) {
switch (signatureAlgo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const { RS: eccSignature } = await eddsa.sign(enums.publicKey.ed25519, hashAlgo, null, eccPublicKey, eccSecretKey, dataDigest);
return { eccSignature };
}
default:
throw new Error('Unsupported signature algorithm');
}
}
export async function verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature) {
switch (signatureAlgo) {
case enums.publicKey.pqc_mldsa_ed25519:
return eddsa.verify(enums.publicKey.ed25519, hashAlgo, { RS: eccSignature }, null, eccPublicKey, dataDigest);
default:
throw new Error('Unsupported signature algorithm');
}
}
export async function validateParams(algo, eccPublicKey, eccSecretKey) {
switch (algo) {
case enums.publicKey.pqc_mldsa_ed25519:
return eddsa.validateParams(enums.publicKey.ed25519, eccPublicKey, eccSecretKey);
default:
throw new Error('Unsupported signature algorithm');
}
}

View file

@ -0,0 +1,2 @@
export { generate, sign, verify, validateParams, isCompatibleHashAlgo } from './signature';
export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa';

View file

@ -0,0 +1,69 @@
import enums from '../../../../enums';
import util from '../../../../util';
import { getRandomBytes } from '../../../random';
export async function generate(algo) {
switch (algo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const mldsaSeed = getRandomBytes(32);
const { mldsaSecretKey, mldsaPublicKey } = await expandSecretSeed(algo, mldsaSeed);
return { mldsaSeed, mldsaSecretKey, mldsaPublicKey };
}
default:
throw new Error('Unsupported signature algorithm');
}
}
/**
* Expand ML-DSA 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<{ mldsaPublicKey: Uint8Array, mldsaSecretKey: Uint8Array }>}
*/
export async function expandSecretSeed(algo, seed) {
switch (algo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const { ml_dsa65 } = await import('../noble_post_quantum');
const { secretKey: mldsaSecretKey, publicKey: mldsaPublicKey } = ml_dsa65.keygen(seed);
return { mldsaSecretKey, mldsaPublicKey };
}
default:
throw new Error('Unsupported signature algorithm');
}
}
export async function sign(algo, mldsaSecretKey, dataDigest) {
switch (algo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const { ml_dsa65 } = await import('../noble_post_quantum');
const mldsaSignature = ml_dsa65.sign(mldsaSecretKey, dataDigest);
return { mldsaSignature };
}
default:
throw new Error('Unsupported signature algorithm');
}
}
export async function verify(algo, mldsaPublicKey, dataDigest, mldsaSignature) {
switch (algo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const { ml_dsa65 } = await import('../noble_post_quantum');
return ml_dsa65.verify(mldsaPublicKey, dataDigest, mldsaSignature);
}
default:
throw new Error('Unsupported signature algorithm');
}
}
export async function validateParams(algo, mldsaPublicKey, mldsaSeed) {
switch (algo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const { mldsaPublicKey: expectedPublicKey } = await expandSecretSeed(algo, mldsaSeed);
return util.equalsUint8Array(mldsaPublicKey, expectedPublicKey);
}
default:
throw new Error('Unsupported signature algorithm');
}
}

View file

@ -0,0 +1,60 @@
import enums from '../../../../enums';
import * as mldsa from './ml_dsa';
import * as eccdsa from './ecc_dsa';
import { getHashByteLength } from '../../../hash';
export async function generate(algo) {
switch (algo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccSecretKey, eccPublicKey } = await eccdsa.generate(algo);
const { mldsaSeed, mldsaSecretKey, mldsaPublicKey } = await mldsa.generate(algo);
return { eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey };
}
default:
throw new Error('Unsupported signature algorithm');
}
}
export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, dataDigest) {
switch (signatureAlgo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccSignature } = await eccdsa.sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest);
const { mldsaSignature } = await mldsa.sign(signatureAlgo, mldsaSecretKey, dataDigest);
return { eccSignature, mldsaSignature };
}
default:
throw new Error('Unsupported signature algorithm');
}
}
export async function verify(signatureAlgo, hashAlgo, eccPublicKey, mldsaPublicKey, dataDigest, { eccSignature, mldsaSignature }) {
switch (signatureAlgo) {
case enums.publicKey.pqc_mldsa_ed25519: {
const eccVerifiedPromise = eccdsa.verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature);
const mldsaVerifiedPromise = mldsa.verify(signatureAlgo, mldsaPublicKey, dataDigest, mldsaSignature);
const verified = await eccVerifiedPromise && await mldsaVerifiedPromise;
return verified;
}
default:
throw new Error('Unsupported signature algorithm');
}
}
export function isCompatibleHashAlgo(signatureAlgo, hashAlgo) {
// The signature hash algo MUST have digest larger than 256 bits
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4
switch (signatureAlgo) {
case enums.publicKey.pqc_mldsa_ed25519:
return getHashByteLength(hashAlgo) >= 32;
default:
throw new Error('Unsupported signature algorithm');
}
}
export async function validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed) {
const eccValidationPromise = eccdsa.validateParams(algo, eccPublicKey, eccSecretKey);
const mldsaValidationPromise = mldsa.validateParams(algo, mldsaPublicKey, mldsaSeed);
const valid = await eccValidationPromise && await mldsaValidationPromise;
return valid;
}

View file

@ -3,11 +3,12 @@
* @module crypto/signature
*/
import { elliptic, rsa, dsa, hmac } from './public_key';
import { elliptic, rsa, dsa, hmac, postQuantum } from './public_key';
import enums from '../enums';
import util from '../util';
import ShortByteString from '../type/short_byte_string';
import { UnsupportedError } from '../packet/packet';
import { getHashByteLength } from './hash';
/**
* Parse signature in binary form to get the parameters.
@ -70,6 +71,12 @@ export function parseSignatureParams(algo, signature) {
const mac = new ShortByteString(); read += mac.read(signature.subarray(read));
return { read, signatureParams: { mac } };
}
case enums.publicKey.pqc_mldsa_ed25519: {
const eccSignatureSize = 2 * elliptic.eddsa.getPayloadSize(enums.publicKey.ed25519);
const eccSignature = util.readExactSubarray(signature, read, read + eccSignatureSize); read += eccSignature.length;
const mldsaSignature = util.readExactSubarray(signature, read, read + 3309); read += mldsaSignature.length;
return { read, signatureParams: { eccSignature, mldsaSignature } };
}
default:
throw new UnsupportedError('Unknown signature algorithm.');
}
@ -113,6 +120,12 @@ export async function verify(algo, hashAlgo, signature, publicParams, privatePar
return elliptic.ecdsa.verify(oid, hashAlgo, { r, s }, data, Q, hashed);
}
case enums.publicKey.eddsaLegacy: {
if (getHashByteLength(hashAlgo) < getHashByteLength(enums.hash.sha256)) {
// Enforce digest sizes, since the constraint was already present in RFC4880bis:
// see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2
// and https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.3-3
throw new Error('Hash algorithm too weak for EdDSALegacy.');
}
const { oid, Q } = publicParams;
const curveSize = new elliptic.CurveWithOID(oid).payloadSize;
// When dealing little-endian MPI data, we always need to left-pad it, as done with big-endian values:
@ -123,6 +136,13 @@ export async function verify(algo, hashAlgo, signature, publicParams, privatePar
}
case enums.publicKey.ed25519:
case enums.publicKey.ed448: {
if (getHashByteLength(hashAlgo) < getHashByteLength(elliptic.eddsa.getPreferredHashAlgo(algo))) {
// Enforce digest sizes:
// - Ed25519: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.4-4
// - Ed448: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.5-4
throw new Error('Hash algorithm too weak for EdDSA.');
}
const { A } = publicParams;
return elliptic.eddsa.verify(algo, hashAlgo, signature, data, A, hashed);
}
@ -134,6 +154,15 @@ export async function verify(algo, hashAlgo, signature, publicParams, privatePar
const { keyMaterial } = privateParams;
return hmac.verify(algo.getValue(), keyMaterial, signature.mac.data, hashed);
}
case enums.publicKey.pqc_mldsa_ed25519: {
if (!postQuantum.signature.isCompatibleHashAlgo(algo, hashAlgo)) {
// The signature hash algo MUST have digest larger than 256 bits
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4
throw new Error('Unexpected hash algorithm for PQC signature: digest size too short');
}
const { eccPublicKey, mldsaPublicKey } = publicParams;
return postQuantum.signature.verify(algo, hashAlgo, eccPublicKey, mldsaPublicKey, hashed, signature);
}
default:
throw new Error('Unknown signature algorithm.');
}
@ -179,12 +208,24 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da
return elliptic.ecdsa.sign(oid, hashAlgo, data, Q, d, hashed);
}
case enums.publicKey.eddsaLegacy: {
if (getHashByteLength(hashAlgo) < getHashByteLength(enums.hash.sha256)) {
// Enforce digest sizes, since the constraint was already present in RFC4880bis:
// see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2
// and https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.3-3
throw new Error('Hash algorithm too weak for EdDSALegacy.');
}
const { oid, Q } = publicKeyParams;
const { seed } = privateKeyParams;
return elliptic.eddsaLegacy.sign(oid, hashAlgo, data, Q, seed, hashed);
}
case enums.publicKey.ed25519:
case enums.publicKey.ed448: {
if (getHashByteLength(hashAlgo) < getHashByteLength(elliptic.eddsa.getPreferredHashAlgo(algo))) {
// Enforce digest sizes:
// - Ed25519: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.4-4
// - Ed448: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.5-4
throw new Error('Hash algorithm too weak for EdDSA.');
}
const { A } = publicKeyParams;
const { seed } = privateKeyParams;
return elliptic.eddsa.sign(algo, hashAlgo, data, A, seed, hashed);
@ -195,6 +236,16 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da
const mac = await hmac.sign(algo.getValue(), keyMaterial, hashed);
return { mac: new ShortByteString(mac) };
}
case enums.publicKey.pqc_mldsa_ed25519: {
if (!postQuantum.signature.isCompatibleHashAlgo(algo, hashAlgo)) {
// The signature hash algo MUST have digest larger than 256 bits
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4
throw new Error('Unexpected hash algorithm for PQC signature: digest size too short');
}
const { eccPublicKey } = publicKeyParams;
const { eccSecretKey, mldsaSecretKey } = privateKeyParams;
return postQuantum.signature.sign(algo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, hashed);
}
default:
throw new Error('Unknown signature algorithm.');
}

4
src/enums.d.ts vendored
View file

@ -89,8 +89,8 @@ declare namespace enums {
x448 = 26,
ed25519 = 27,
ed448 = 28,
pqc_mlkem_x25519 = 105,
pqc_mldsa_ed25519 = 107
pqc_mldsa_ed25519 = 30,
pqc_mlkem_x25519 = 35
}
export enum curve {

View file

@ -108,6 +108,10 @@ export default {
ed25519: 27,
/** Ed448 (Sign only) */
ed448: 28,
/** Post-quantum ML-DSA-64 + Ed25519 (Sign only) */
pqc_mldsa_ed25519: 30,
/** Post-quantum ML-KEM-768 + X25519 (Encrypt only) */
pqc_mlkem_x25519: 35,
/** Persistent symmetric keys: encryption algorithm */
aead: 100,
/** Persistent symmetric keys: authentication algorithm */

View file

@ -9,7 +9,7 @@ import {
SignaturePacket
} from '../packet';
import enums from '../enums';
import { getPreferredCurveHashAlgo, getHashByteLength } from '../crypto';
import { getPreferredCurveHashAlgo, getHashByteLength, publicKey } from '../crypto';
import util from '../util';
import defaultConfig from '../config';
@ -164,6 +164,10 @@ export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date =
enums.publicKey.ed448
]);
const pqcAlgos = new Set([
enums.publicKey.pqc_mldsa_ed25519
]);
if (eccAlgos.has(signingKeyPacket.algorithm)) {
// For ECC, the returned hash algo MUST be at least as strong as `preferredCurveHashAlgo`, see:
// - ECDSA: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.2-5
@ -186,6 +190,21 @@ export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date =
strongestSupportedAlgo :
preferredCurveAlgo;
}
} else if (pqcAlgos.has(signingKeyPacket.algorithm)) {
// For PQC, the returned hash algo MUST be at least 256 bit long, see:
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4 .
// Hence, we return the `preferredHashAlgo` as long as it's supported and long enough;
// Otherwise, we look at the strongest supported algo, and ultimately fallback the default algo (SHA-256).
const preferredSenderAlgoIsSupported = isSupportedHashAlgo(preferredSenderAlgo) && publicKey.postQuantum.signature.isCompatibleHashAlgo(signingKeyPacket.algorithm, preferredSenderAlgo);
if (preferredSenderAlgoIsSupported) {
return preferredSenderAlgo;
} else {
const strongestSupportedAlgo = getStrongestSupportedHashAlgo();
return publicKey.postQuantum.signature.isCompatibleHashAlgo(signingKeyPacket.algorithm, strongestSupportedAlgo) ?
strongestSupportedAlgo :
defaultAlgo;
}
}
// `preferredSenderAlgo` may be weaker than the default, but we do not guard against this,
@ -403,6 +422,13 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) {
}
switch (options.type) {
case 'pqc':
if (options.sign) {
options.algorithm = enums.publicKey.pqc_mldsa_ed25519;
} 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`
try {
options.curve = enums.write(enums.curve, options.curve);
@ -461,6 +487,7 @@ export function validateSigningKeyPacket(keyPacket, signature, config) {
case enums.publicKey.ed25519:
case enums.publicKey.ed448:
case enums.publicKey.hmac:
case enums.publicKey.pqc_mldsa_ed25519:
if (!signature.keyFlags && !config.allowMissingKeyFlags) {
throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`');
}
@ -480,6 +507,7 @@ export function validateEncryptionKeyPacket(keyPacket, signature, config) {
case enums.publicKey.x25519:
case enums.publicKey.x448:
case enums.publicKey.aead:
case enums.publicKey.pqc_mlkem_x25519:
if (!signature.keyFlags && !config.allowMissingKeyFlags) {
throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`');
}
@ -502,7 +530,8 @@ export function validateDecryptionKeyPacket(keyPacket, signature, config) {
case enums.publicKey.elgamal:
case enums.publicKey.ecdh:
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;
if (isValidSigningKeyPacket && config.allowInsecureDecryptionWithSigningKeys) {
// 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');
}
// The composite ML-DSA + EdDSA schemes MUST be used only with v6 keys.
if (this.version !== 6 && this.algorithm === enums.publicKey.pqc_mldsa_ed25519) {
throw new Error('Unexpected key version: ML-DSA algorithms can only be used with v6 keys');
}
this.publicParams = publicParams;
pos += read;

View file

@ -21,6 +21,12 @@ import enums from '../enums';
import util from '../util';
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)
*
@ -128,7 +134,7 @@ class PublicKeyEncryptedSessionKeyPacket {
}
this.publicKeyAlgorithm = bytes[offset++];
this.encrypted = 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) {
this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm);
} else if (this.encrypted.C.algorithm !== null) {
@ -211,7 +217,7 @@ class PublicKeyEncryptedSessionKeyPacket {
if (this.version === 3) {
// 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;
if (sessionKey.length !== getCipherParams(this.sessionKeyAlgorithm).keySize) {
@ -240,6 +246,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) {
]);
case enums.publicKey.x25519:
case enums.publicKey.x448:
case enums.publicKey.pqc_mlkem_x25519:
return sessionKeyData;
default:
throw new Error('Unsupported public key algorithm');
@ -288,6 +295,7 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) {
}
case enums.publicKey.x25519:
case enums.publicKey.x448:
case enums.publicKey.pqc_mlkem_x25519:
return {
sessionKeyAlgorithm: null,
sessionKey: decryptedData

View file

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

View file

@ -6,7 +6,7 @@ import openpgp from '../initOpenpgp.js';
import * as crypto from '../../src/crypto';
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([
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,
@ -196,15 +196,15 @@ export default () => describe('API functional testing', function() {
const algoRSA = openpgp.enums.publicKey.rsaEncryptSign;
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 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 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');

View file

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

1083
test/crypto/postQuantum.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -81,7 +81,7 @@ async function cloneKeyPacket(key) {
}
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 });
return privateKey;
}
@ -322,6 +322,48 @@ export default () => {
});
});
describe('PQC parameter validation', function() {
let pqcSigningKey;
let pqcEncryptionSubkey;
before(async () => {
pqcSigningKey = await generatePrivateKeyObject({ type: 'pqc', config: { v6Keys: true } });
pqcEncryptionSubkey = pqcSigningKey.subkeys[0];
});
it('generated params are valid', async function() {
await expect(pqcSigningKey.keyPacket.validate()).to.not.be.rejected;
await expect(pqcEncryptionSubkey.keyPacket.validate()).to.not.be.rejected;
});
it('detect invalid ML-KEM public key part', async function() {
const keyPacket = await cloneKeyPacket(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 cloneKeyPacket(pqcEncryptionSubkey);
const { eccPublicKey } = keyPacket.publicParams;
eccPublicKey[0]++;
await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid');
});
it('detect invalid ML-DSA public key part', async function() {
const keyPacket = await cloneKeyPacket(pqcSigningKey);
const { mldsaPublicKey } = keyPacket.publicParams;
mldsaPublicKey[0]++;
await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid');
});
it('detect invalid ECC part', async function() {
const keyPacket = await cloneKeyPacket(pqcSigningKey);
const { eccPublicKey } = keyPacket.publicParams;
eccPublicKey[0]++;
await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid');
});
});
describe('DSA parameter validation', function() {
let dsaKey;
before(async () => {

View file

@ -4612,6 +4612,18 @@ I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg==
expect(v6Key.subkeys).to.have.length(1);
});
it('should throw when trying to add a ML-DSA 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);
// try adding an ML-DSA subkey
await expect(v4Key.addSubkey({ type: 'pqc', sign: true })).to.be.rejectedWith(/Cannot generate v4 signing 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() {
const privateKey = await openpgp.decryptKey({
privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }),