diff --git a/.github/test-suite/config.json.template b/.github/test-suite/config.json.template index 8e9a9d3f..32c74d17 100644 --- a/.github/test-suite/config.json.template +++ b/.github/test-suite/config.json.template @@ -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 } } } } }" } }, { diff --git a/openpgp.d.ts b/openpgp.d.ts index 1fe07652..f71bb224 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -678,7 +678,7 @@ export type EllipticCurveName = 'ed25519Legacy' | 'curve25519Legacy' | 'nistP256 interface GenerateKeyOptions { userIDs: MaybeArray; passphrase?: string; - type?: 'ecc' | 'rsa' | 'curve25519' | 'curve448'; + type?: 'ecc' | 'rsa' | 'curve25519' | 'curve448' | 'pqc'; curve?: EllipticCurveName; rsaBits?: number; keyExpirationTime?: number; diff --git a/package-lock.json b/package-lock.json index 93038482..a48b2c99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 9bdd2139..ba45c577 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index f63491d5..fcdd32ca 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -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.'); } diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index e5c0da1f..ad73ea30 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -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 { diff --git a/src/crypto/public_key/elliptic/eddsa_legacy.js b/src/crypto/public_key/elliptic/eddsa_legacy.js index 9ab45da8..dec55aec 100644 --- a/src/crypto/public_key/elliptic/eddsa_legacy.js +++ b/src/crypto/public_key/elliptic/eddsa_legacy.js @@ -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); } diff --git a/src/crypto/public_key/index.js b/src/crypto/public_key/index.js index 970c972b..67403de5 100644 --- a/src/crypto/public_key/index.js +++ b/src/crypto/public_key/index.js @@ -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'; diff --git a/src/crypto/public_key/post_quantum/index.js b/src/crypto/public_key/post_quantum/index.js new file mode 100644 index 00000000..cf803ef8 --- /dev/null +++ b/src/crypto/public_key/post_quantum/index.js @@ -0,0 +1,7 @@ +import * as kem from './kem/index'; +import * as signature from './signature'; + +export { + kem, + signature +}; diff --git a/src/crypto/public_key/post_quantum/kem/ecc_kem.js b/src/crypto/public_key/post_quantum/kem/ecc_kem.js new file mode 100644 index 00000000..1233eba8 --- /dev/null +++ b/src/crypto/public_key/post_quantum/kem/ecc_kem.js @@ -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'); + } +} diff --git a/src/crypto/public_key/post_quantum/kem/index.js b/src/crypto/public_key/post_quantum/kem/index.js new file mode 100644 index 00000000..399750ad --- /dev/null +++ b/src/crypto/public_key/post_quantum/kem/index.js @@ -0,0 +1,2 @@ +export { generate, encrypt, decrypt, validateParams } from './kem'; +export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem'; diff --git a/src/crypto/public_key/post_quantum/kem/kem.js b/src/crypto/public_key/post_quantum/kem/kem.js new file mode 100644 index 00000000..897c7c85 --- /dev/null +++ b/src/crypto/public_key/post_quantum/kem/kem.js @@ -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; +} diff --git a/src/crypto/public_key/post_quantum/kem/ml_kem.js b/src/crypto/public_key/post_quantum/kem/ml_kem.js new file mode 100644 index 00000000..0651547b --- /dev/null +++ b/src/crypto/public_key/post_quantum/kem/ml_kem.js @@ -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'); + } +} diff --git a/src/crypto/public_key/post_quantum/noble_post_quantum.ts b/src/crypto/public_key/post_quantum/noble_post_quantum.ts new file mode 100644 index 00000000..de77098e --- /dev/null +++ b/src/crypto/public_key/post_quantum/noble_post_quantum.ts @@ -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'; + diff --git a/src/crypto/public_key/post_quantum/signature/ecc_dsa.js b/src/crypto/public_key/post_quantum/signature/ecc_dsa.js new file mode 100644 index 00000000..9bf3dfbe --- /dev/null +++ b/src/crypto/public_key/post_quantum/signature/ecc_dsa.js @@ -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'); + } +} diff --git a/src/crypto/public_key/post_quantum/signature/index.js b/src/crypto/public_key/post_quantum/signature/index.js new file mode 100644 index 00000000..3d195f63 --- /dev/null +++ b/src/crypto/public_key/post_quantum/signature/index.js @@ -0,0 +1,2 @@ +export { generate, sign, verify, validateParams, isCompatibleHashAlgo } from './signature'; +export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa'; diff --git a/src/crypto/public_key/post_quantum/signature/ml_dsa.js b/src/crypto/public_key/post_quantum/signature/ml_dsa.js new file mode 100644 index 00000000..0165e71e --- /dev/null +++ b/src/crypto/public_key/post_quantum/signature/ml_dsa.js @@ -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'); + } +} diff --git a/src/crypto/public_key/post_quantum/signature/signature.js b/src/crypto/public_key/post_quantum/signature/signature.js new file mode 100644 index 00000000..4db9de59 --- /dev/null +++ b/src/crypto/public_key/post_quantum/signature/signature.js @@ -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; +} diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 15b933d9..fdb39ebc 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -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.'); } diff --git a/src/enums.d.ts b/src/enums.d.ts index 067be092..e2f09ddb 100644 --- a/src/enums.d.ts +++ b/src/enums.d.ts @@ -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 { diff --git a/src/enums.js b/src/enums.js index 89ff6e4a..e44872ce 100644 --- a/src/enums.js +++ b/src/enums.js @@ -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 */ diff --git a/src/key/helper.js b/src/key/helper.js index 311f96f1..b352f062 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -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 diff --git a/src/packet/public_key.js b/src/packet/public_key.js index 9f95eb02..2c9f61b6 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -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; diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 25fa7153..c4d060d5 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -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 diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 9d913ce3..6f1181a7 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -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; diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index d6d34092..be7c6c44 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -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'); diff --git a/test/crypto/index.js b/test/crypto/index.js index 2e195766..12297ee0 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -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(); }); diff --git a/test/crypto/postQuantum.js b/test/crypto/postQuantum.js new file mode 100644 index 00000000..3c14ffe7 --- /dev/null +++ b/test/crypto/postQuantum.js @@ -0,0 +1,1083 @@ +/* eslint-disable max-lines */ +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'; +import { sign, verify } from '../../src/crypto/signature.js'; + +// Test vector from https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#appendix-A.4.1 +const mldsaEd25519AndMlkemX25519PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +-----END PGP PRIVATE KEY BLOCK-----`; + +const mldsaEd25519AndMlkemX25519PublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +AAAAAAAAAAAABA8TGSIp +-----END PGP PUBLIC KEY BLOCK-----`; + +// Test vector from: https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#appendix-A.1.1 +const ed25519AndMlkemX25519PrivateKeyV6 = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUsGZ3SFgBsAAAAg3LSTXMTIYPje/3KOQ480cxsp1t0/1w2687B8uqUTCvwArfra +hBTuKijHaDe4/1ZcaYn7Z67De15iWPC/vGa3J4DCngYfGwgAAAA/BYJndIWAAwsJ +BwIVCAIWAAKbAwIeCSKhBseJ4X2dvcp7PIM6PAY/6wNT+ArZEf4nho+wZF34A+lH +BScJAgcCAAAAADQ7EIBsYsSttPe/Uf3gEmjU8NG0Ej59FY8N8de0aowAompVXg4Q +T9j1IsjmU28Ex/k1PTewAx5OoeQpQbJ4jjH+R/OZ730kkv+c+1mea5dpJdINzS5Q +UUMgdXNlciAoVGVzdCBLZXkpIDxwcWMtdGVzdC1rZXlAZXhhbXBsZS5jb20+wosG +ExsIAAAALAWCZ3SFgAIZASKhBseJ4X2dvcp7PIM6PAY/6wNT+ArZEf4nho+wZF34 +A+lHAAAAAD4xEPlvPfA18bTluf3pkoZII9dcKltRjMIkbZqDg5el9xpB50br+7lg +CeQ4FOlsdrb9u7+twdm46fd6pR74naBnq6puYgsQnlLpTfnWqX6BB7sHx8RrBmd0 +hYAjAAAEwCIVC0MM9yTsGbi+Vd+byq3jJwhXETaUBKV1yAI0Q7BfXkJC3YN0sGUL +B1LIvZBSlFEx/9op4ncn+0J9IKeKzuu449mu2UdNA/XK+5wl+sGGdTVNrJq3s7R/ +BktVNWBMG0t+eYBtItQaxwYucsOntcO344uH1BA+ZHIALxsHN6KrZPCeqfF9ppTB +8UAlvVR0JATPtyyuxFw6MUex06GlYzirMXafH8jK5KqZJbjBKWwPo7bIsBBbLpqk +9yQxb7YYJ6VQMGkitrRwWNwQxTGDTee8/FyUMIYklGMewmG65RkKrBMKICsBX7lD +1cQ7fisEp7Y+WoOVqass6FuOWKJLIhyrg+A93oUsNBhGz/iEgJehP2g8AQPNMnO+ +IicgIwQdV+YJRreWKvQosYMHxtopQRWKRXmmrGKPFYeozvoo6qR+JImvIDgy0gV/ +eEun8iVoQbxuBXPIQXJOicOFPiWjXisG/0G3rOyRE9o0ivlxZPku2qKayfFtMwxb +NWg54ieV/uRfrjRRHOnL0SaxigY8N0FIwaTB4uMeyqphG+Ri7nSgnLVxaES9/tzN +iKlgsuAuC5k7E+XPAonGy9Ikm+oHJ9hg0XKNzvyNR/uwWhB2WFmFDRB3ldVE3/wh +LLe7yPmptIFWZ3J2LaaFqTyUhoNW6spRkUWeNMgo3MxRS3QgmBGXqou6/7RT2tdV +rWIY8WJO5SmMnZcrJ6oX3qKYgFiJJgCTuUegr/cNNJNLE/shyedkdxfG6jaQSzrL +xOWg0fBHtrd1snWWryCofwi7ray3HjNnKjNddfiChjWkLpu5H2dYM/CzdnmmOlk9 +71vJgeZ6SrivUlwLTHDIkVqVr5F+iPXHU+Wd6hcUxrIH4eUXTJFIUYiamSqb35cg +AAzOBBS/DKwD46pOURchwRNRUbu47qiJE4AT6Ia0dTFkCSRezaJXBNe9+oisPDct +QTx/VZaktkvK4hc+DLwuMiQuQMoev0CgzLlr7XpxvCaEGYOiFTJydFtlhepmheZ3 +k0puE5lY8AOJBEd1PKPEDzwqD0wOBxaMTEQGiPqH39ENs4q08BcOXjANqwtjxVot +JIurlBcxgsZ16RZOyiaRcCs358AMrExMScNfU+HBIONqxRkzCPOqn3sCMzQO+dwJ +uxNUoEYDcUAtYos8+1BdaHvGaeNMY3EuxQWeAJQEN4dpYfe29qlcYOwcHRtYosmL +c7OaHMQ7wnCzdJeyiHobAIgVZGZvAyW6w9cTlQd2pTdsDgSmO8or69gYPKxsmLei +b3JoytejtBhnaexyfeCFhksHzJlu48l65fDFE3h5Pmqj8vRGfFik2qbIzrU11ala +/IiJD7RnwTQCYuO++yKBa7rC1qXCXBFZixBwwTavHeEAPNnMmWxDfna2frNnFKRo +dDaTM7o76Gs7rgTIeLAzQfZih1yAJpSwjEcVDZdIRaRTrkK/4PJzVHGb4wwkOwfL +oPEBCMuAYTxfqJVEgdBi6pUsPaEOQzkUeIeOVgoFnMhP3+N22fMWkCBc/LYOThtP +XFWtHkq5jmmj+aiuyGOaadccBKWTvxloBly25QADfbYI2oImtVu1JDjFCf6PQ74U +WSEf9EvmTgsMQq/TPICLDUyoTkteLTQAwE2+uDYPxbo85xlZ2/yGneciXS8MvfqB +z8ZOI/y0C3xRsn7ZFZ2nEAaP9RUboQSSkc/gerixe47HC7X+MP6h7UAy49+ndvRO +6AHx2zZzPiDlZ0NgX3p6Aem45zjfMT7+wosGGBsIAAAALAWCZ3SFgAKbDCKhBseJ +4X2dvcp7PIM6PAY/6wNT+ArZEf4nho+wZF34A+lHAAAAAMitELFBvWMJM18SWIqP +kabwnOQbeR9GdZt2dJZF0YO8qGRmLO+T7GIaYj9TlzHonA3y01AAlYdfvolBM2gL +pujFf59V4t3iZxgQfgvs0+vrcSsE +-----END PGP PRIVATE KEY BLOCK-----`; + +// Test vector from: https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#appendix-A.2.1 +const ed25519AndMlkemX25519PrivateKeyV4 = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUkEZ3SFgBuhDibMGc69QTyzKYr3R7MMaQOZuU0Bwg82JVcL+NGHswA4zWTCi+mw +P/WoL5x2SDW3bNe2kpcypEWdTapexJdgWxDlzS5QUUMgdXNlciAoVGVzdCBLZXkp +IDxwcWMtdGVzdC1rZXlAZXhhbXBsZS5jb20+wsAABBMbCAB2BYJndIWAAwsJBwmQ +cQL/7Tuc8S01FAAAAAAAHAAQc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ+Ul +WyqYGQo/YJguDbFrolcCFQgCFgACGQECmwMCHgkWIQQ0Ll2y3jRSFcsslE9xAv/t +O5zxLQUnCQIHAgAAiv81HEeRhisNla+h8zV+vQ9HbCb5/ukgpBG527xzg2ULCaTJ +s/DvNGl/cC+EhWQ8Bk1+dKSr9fwFBmxb3fkxTEYEx8RpBGd0hYAjsIfqoDHr3VUD +76sj9JP+04jzHeAhTXnoPF3N7Cs281UB+Awo6DCHkI8GuMzoPE4BOsuYu1RyWZL7 +xZTGtTqua8iSHLRoqLppdL7u0M5IUafYaacURmnGARVgK6FvljpMVX33rAn3yzAV +2STMln8wxhRJ2Qol0Ss+m4L2hyt74h0a05D9M8Iyl2J+w8G54jmcaRxmNoBlOnes +M052o7TlwcZjwKJ2po1Uw8SNqoqAKUBnu5wA87MfJ699igCog2mT8De1W7VA5V4m +hWkk3IaGUG+Yahrox3Kx1Vgeeg3d9grjNZAHCMJEt8wFKmH+Ca8GNljDAEOGQrDs +FzZz0Lg7Gx/96Wuv2gNktLCgPHS+ZHn3J6pXe5klpWA8oFi9uLkj2JHyeqG7x1Kv +K3MQFb4RjJbBIE5GKkdQ0xLA+I6vwr5wBm4li27Xps81NwvwSmHlU0bvOCl4Bwdc +p7gqQ0t6JX0qs18jW8Du5l08FgTYBT3qHEwj9BezKqVCUqfXG7bR+DoN+CR4kxZx +Wa/EhXmvqhxJGllP2G+QQFSS2Zn6SFXf58OaMswI1jEPu4RjbEK698d8JiFuKXUA +sH1MfGdF5aXw5rUF4wYIBA9yUjsukD5dhGdPRxeD4hdCSiZjKTEFdDnSZwMTiSx8 +AFE04w2jR8OKCiN+g8TtBzYq10TAARs29D0thFAuMr9dkYFbFAXdIFgCYl9w1Rpw +g6QwpWJl+lUTc6+QiXTq+qRjQq4zIHv3KBf3y8PM9VsWhISCqrbtRXlqQylRAREx +Sg0eqA8pIm1n0WdpWpRDNmNERsRDy8ksBpjEskLuOn2/syUrGz8IKipIgWx/KURs +UQDOdkWoa2gboXlf7CsD+oVemcK3YWPoa0G2WVHlLCDrNr/Yl86e120PeDDmbKKB +xY9xSwF/sZu5K3/mGYBwUmZdfGty1nOexsr0agYZSkEjBkRCWYJYqi+BswClWxr6 +1kpYxL6aK5HyCMvoGAa8BbIyp4ZXKXFn61KoW8UtUzUcVcZ7cnD6KwQDCgvt8zkl +mbraF6fCZx18gSgKJ6EqDBc8xM91jItq5rAonI6OyZ1pAGHNp0ucMwD2h5INVcpG +5Svbcbgp8g4Nhb2YC6SbGsrMYg7r56vq4V+PAWToljJbRZZYg86SDMnN2B7vtGIZ +7EzerETOqs5FIXyENGku5MB/dswa+0qDZZJd4SvIAx4QZBMD0btXIFYOVZVGsKDf +l2aqQrz9dFzWGBWdukVEK1fupVeqNLSRGrunEUtuM0+duXgr+R9opk96lAIdyiar +BgglwnoE98KrWbg3VQoxpkWisiOEQZlnx6h84WUzoCDmtiNOSwGyOR0BekNxwAy3 +aA3bgX1hUBk4pH1OlEK7M2w9EiVXu23Ka0dBrGJPoIgcBcbAQyC34CeptLxHfLJK +1jYkt8JlEM29EUpthHK5mm4ACbMi6hSaKY2zhDp2NGAXBGTW8DZDdHhWIZtfdgjC +9cExwJ1Y+Q5jOkhSAodehgXFEmkJJ8diLIEr8R7aoE/BRIGgsl/P9TOoSziAukuR +022b9VpaxFmAw5VKwR1p+LGSO5NNk8pHNDHO8Z1vq2ucB6XUJZP4+KPae2VsP3sz +XOt9s+Q8MgD8dFKGx/K6MnUo8d4SiSN6xwZrRTOI8isx7mKUottWlFnIp6AbcIcW +v2muZAinRVJ+QOF+0tPZ+UlFXff0TOQboC0lStCNTWQH9VssBXnVyzpmkelDStd3 +cXl7xiULVi0u5cKqBBgbCABgBYJndIWACZBxAv/tO5zxLTUUAAAAAAAcABBzYWx0 +QG5vdGF0aW9ucy5vcGVucGdwanMub3JngVWn5tMqxzNf8aApl434UAKbDBYhBDQu +XbLeNFIVyyyUT3EC/+07nPEtAADn/QwnUsJ2e8wOM1CRL7UfYfta21hApsSrb/KP +aFZa4v1Zz2bq/w+2YWC5UFY9hPTVriMS9ve0oUncLSoTJfMCmwk= +-----END PGP PRIVATE KEY BLOCK-----`; + +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 { data: expectedBinaryKeyV6 } = await openpgp.unarmor(ed25519AndMlkemX25519PrivateKeyV6); + const { data: expectedBinaryKeyV4 } = await openpgp.unarmor(ed25519AndMlkemX25519PrivateKeyV4); + + const privateKeyV6 = await openpgp.readKey({ armoredKey: ed25519AndMlkemX25519PrivateKeyV6 }); + expect(privateKeyV6.write()).to.deep.equal(expectedBinaryKeyV6); + + const privateKeyV4 = await openpgp.readKey({ armoredKey: ed25519AndMlkemX25519PrivateKeyV4 }); + expect(privateKeyV4.write()).to.deep.equal(expectedBinaryKeyV4); + }); + + it('ML-KEM + X25519 - Test vector', async function () { + // Test vector from: https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#appendix-A.1 + const armoredMessage = `-----BEGIN PGP MESSAGE----- + +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 +ngqFrEkdOzaOHiIZarV74y3G3wZyzXobjp+dpA== +-----END PGP MESSAGE-----`; + + const privateKey = await openpgp.readKey({ armoredKey: ed25519AndMlkemX25519PrivateKeyV6 }); + + const { data: decryptedData } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage }), + decryptionKeys: privateKey + }); + expect(decryptedData).to.equal('Testing\n'); + }); + + it('ML-KEM + X25519 - Test vector (v4 key)', async function () { + // Test vectors from: https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#appendix-A.2 + const armoredMessageSEIPDv1 = `-----BEGIN PGP MESSAGE----- + +wcPUA6T5X5he1hpRI8oKxrVQiCkB27ePKVHeA4pTYMKZA6u1l8syrP2+sEULDgvB +GmH6+0mTw07VEh6J1i1+3ymnnTqLhkv3YqdBtiC81+PL05YPCymPZaWf0ajq+4sM +dnBfLJ3BPrsJw03sVHIBh+L3qolG0CliIzGKxIPz9F5RBSvDdSIwCNg9hnfZjpMu +kcmceYISpWjJR+LeAieyYOTZ+Qhx71jYQ2svfpwW+XAw03uMpZkvqkOJmYr8uUca +i8x2j4G6EUXuu9NswSPPirCqU6OZVdpoHUZusFyRZz89V10fQr9hrnJOGw0VtPGz +SMEulSosvnvnK2BQ2ccJVNn0s/mk+fttQLpBBsKCH0UK8norIXt5ahxdj9sSwBTf +q6cPlHz1o9OnFSuewFkapA4PuLxhf4YY8ZTsC9LUZLiMf8MrMza7gbtnEbBzW3bx +y6QD6I+PneJl/8M5a7ECrlFuR2p3Kyt6MTiY+6sxJ1GOVhpNS24iO/LRxAUe8DRi +tFC46oEQFByp98SIWt3JoJKHcjQzLKTjRWfYhZDiUnBkoM6nYaAZdItcFsBDG3IV +1UstcmfcCugJyWi8V8XHVKdhWe3bWc1WrhieDCVpfSBD2NnRMGG+g90WtHcwhntf +n/mwyR/GLG+gRc16I5hPm84lS34+/txx745yDXdTx/szZAqQw0VW47CwF17A3wdw +cX1UDnUnf7/llFKqg8Zn/GXGIEreo5q/83Ib7dehm50APhtaKnQzoPbPPu21lw+8 +/3gisYxQbmrphEpU2KWWWrG5g8P3JG/D9wlHwDhhXPCdNFB7wthQbaDQ1WnN3WlV +BEtWMOqLTIovjrxHbn5judqLYQQgZPbMguzj5JXrQM7wVu4o0edv967oI6ZRBg4y +BtwlXWF3cgMvvAFfH8fXGXAtJw5Gz4/gxzan1q1JcOm6Akgp55J37LlPeXIKHiyX +W/J8qEbk0XgSSa5VduPfnP7AzrLWtyT5B9lqPszmR7euLXvb+tCuNa/G/ldceJO8 +6MKJVXYuYnk2qpuNWV1NPlCUH583LH2xgJ1YNk6U7ID4opcBMJuVM+MTA7cmLObE +Fdoa2TvJu5rpyrnqedHPNgE98P95kZJ/UtaIqJL+zzGkD5rip70DJPuQ6CkVwX1o +pTx2EKwi3c8H5QZtZUkLYeh8x/1LidZeLBdMLut1Lc7BmD0j0wU0PrwaSLsWAYmW +L16/cY9xPwIqmC8nST8rH94QoBl5eFkm3HrDYjJZrNybk0RKP5oLKG8QoUO2kXyW +9TfKNplK0YCfGTgKcTK6luDSM2cCdwlPdspwCRLPX7L7LZYaK8nEfAFD6Al8ZC45 +meBQgY4SuDlucygbFAHitrXLv4ukBDNRFxe+2dVzig5ryHZj97H5vp89aYH1W3gh +GS/k57744ziY/ACTz8cRxVKgM1oTYjDsNKvmM2J6ij7vRxuMBvM/kO7g+0jD2hPV +eWyIS1VKeT1LG7sRBd+GVQ9jHKpP/7YonXYUrOkpCdG/5YOX6Doo3VlVTRi00QmC +t8716eEJLd7ivorFYFELY5eOEUcjmNLSwEgBr2kPm1Mkn8RS8mCNT188nXgTzZk7 +jZ1rurNkkqQM53xMaLmQImS+N20GPoa2RdHW+R/veP7LugLO7gozMi/zz9+kcd0Y +wPahWnYsZ4uHg/zbgFMIH/iwQ04nv58gOLJfJafGarwotFvBIfl4Nd607lmVJTc0 +OjVhhisWL5WDIC82+DDu0yDLH/huzY4W7/ks3Hn/UpEwzq+A1/bY3MbQomew5hTI +99z/IeulfiT8/0POofN3lvMvTzeGuMMsBiMFp2nbCEHrwWCN9uaE3eAbj3E0OtAM +AIIfxypiV+bYm0IA58t7Ur3kMG1KZmcKG4DF5zrL1u5ArX/T7258Z7shYff7WtNU +Wfo= +-----END PGP MESSAGE-----`; + const armoredMessageSEIPDv2 = `-----BEGIN PGP MESSAGE----- + +wcPhBhUE5R2/6lGTaYi1Qo//pPlfmF7WGlEjlejDztYnd2xigU3Okc86MsGI+wTe +RO1LNVy4L03KG04LC5S7Ahh1ADVuNqplgbBCjHeiWsMeBIXfwUYH35+X0TB6P++e +pA/flGSeFj2F/ubBL3Xo5r7OGeOD55hijwNjwKj9tEhTkIOa0LFaNwJblCsTTW/Y +3rRQB1SzvyPk9Qf/iyN5t17/89j/piKJXulgLXnLUONBYeqA+gSV/0FhsYHambvL +ucx5AE6GUJzsFxdjCwVR7/7zdCU6jsfvPSeZry+7CSuTAFYqrh3x8+62Kio0vcoH +irJrIRQsQOo6ygvmLJS5vQUF7lwNimpXzTjWjqQBuAdwSuYPFVDPd4xIOgmT/oJt +MCA8UOKxdoANh+XnjqZAsL5EjTPf3UmGLhbNj46XssvtUSuW4qvgFVoR0FNOEBrt +9Tyt7fDzdZkD+RkyLK13igSLXVzB4Ofn9E6dddurICZkfNtQofV/t+qJUmc0qQKs +cFrwFO3xt4UrgLFH8SnWJ9Rt6tLaahUD2pY/YNkSSC1+HFPbFSsXTcnfOt5vfEQg +UVmP5TRVX36qE5EqRt4zZwBzv+Ph0lMWQueKXscHGz6+cFR76nktsiOFTlwYtudf +fCrvf+hxuGn0mKk3qlTkmF5vOt+NNiqXt+nzJ7bqdkH3kAQ2qG9UHi0Ey1X7ykI4 +MKqjNXp3ovwx20hUYrPMRo/XXz7s8ZiqX5q544kpjwxU0n9mvWYEjr3hePtAK4YD +6WRtqukyuxSPvonhdyq+x/awcg2AQe5tPH+eMTt/cm1yBdzgvbNxUcy5+87TQJhJ +Ia425biPs4kZku1NP2pN/kVeT8Me56zhdaJF2OwcUGOSjbkgo/F4WyE5bYK7gM5G +/40hnmYIGWitGLoQmN3jyEIceSJsLazJ503iO8ZSRjNwMN6SCOTFn0PMaJAHwa15 +jshrrUHJpZri/4Lv8cX1/A5OMULSyKNX3PVk6aZPtzXDOm1MCC0M1vIboFvD1qEx +feSnmaN+xVXjVsA76C0cqQ85XLM8KIqzJXLQnG+4ebsa1OrYreo/1FFlRouY42uS +VY+IZqi3Z2iQou9DKdYGWQAGjB1rdeWe/J6R9BK5n9E5KZ2z9HlJngW3FXX7yxxm +ZogvoGgEwnKuxfjl38sgtQh8bhXOXS9roNm8uDwuk1zwd9SOx6vcL9h6TBaLPw3g +mwYrawlUONtMBjbU2KGmKqx94V0yMIK1FEA9LLB4akWO4Gnh/qUQbq6Tptb6zZQL +w3GQv4VzOEwSg84Vz7dQWw3hg4/vRSL+TQ1KH5hS2CuCcpVjJvC9hpatnd4DRsVi +/bLy1BRO2VJXFyHERR+gVvN7xo+bAnhC6aasDHH+hqyNagwQBS1upcCr7p+s672W +7GS7IVAiLvXcsxS3A5xpkmcMb/+2BY34HKw2MsakDJlwY7zYcqeHTqkseHNrarj3 +AhJ5u1RYFJJWTxn3J8F55qP7QtIje2ykrm06wwwV1Yfd7DTzIKELZT9Qjyhf9nEQ +enlNwJgVGPNS87iYII1jS7fP8K6YyfknyDKNzDjPCJEKL7g40sBjAgkCDJ38fP5P +GQmlf6F96zl/4ACgraWthTKdhMHXMxRVYxO3vgFpnQwEMxje1zZBkaN355PqAYU5 +1NuIJCocAghd1k5ygIWF9XGsACGfgvmxCSMV+iMm4E4nI/j6IuJndr3pXrUwo+gS +g2Akz4b/QFZ46wJbFXGGzEo2rGmpXLoyc8lcIIcU7klRo0g0jafd44YOz21x7ZYI +UAYRNdYOY5bMdFgXWUYRmXc7VtLJDS5X44nuCA8JZtSu2yilq9vYHqzN0RyMMijj +/D5P3hAxUV3oUfs68oxnGh49k2pii+Bg5iXLvn6Ahxp2rIksECWlkXVKCH91x6nE +Fy70NCeqH2b6JeETZ1xFQEiEInk6B9WE558S9Mi6yjeSXdV65yNK2km5 +-----END PGP MESSAGE-----`; + const privateKey = await openpgp.readKey({ armoredKey: ed25519AndMlkemX25519PrivateKeyV4 }); + + const { data: decryptedDataSEIPDv1 } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: armoredMessageSEIPDv1 }), + decryptionKeys: privateKey + }); + expect(decryptedDataSEIPDv1).to.equal('Testing\n'); + + const { data: decryptedDataSEIPDv2 } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: armoredMessageSEIPDv2 }), + decryptionKeys: privateKey + }); + expect(decryptedDataSEIPDv2).to.equal('Testing\n'); + }); + + it('ML-DSA + Ed25519 - Generate/sign/verify', async function () { + const digest = new Uint8Array(32).fill(1); + const hashAlgo = openpgp.enums.hash.sha3_256; + + const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.pqc_mldsa_ed25519); + const signature = await sign(openpgp.enums.publicKey.pqc_mldsa_ed25519, hashAlgo, publicParams, privateParams, null, digest); + const verified = await verify(openpgp.enums.publicKey.pqc_mldsa_ed25519, hashAlgo, signature, publicParams, null, null, digest); + expect(verified).to.be.true; + }); + + it('ML-DSA + Ed25519 - private key is correctly serialized using the seed instead of the expanded secret key material', async function () { + const armoredKey = mldsaEd25519AndMlkemX25519PrivateKey; + + const { data: expectedBinaryKey } = await openpgp.unarmor(armoredKey); + + const privateKey = await openpgp.readKey({ armoredKey }); + expect(privateKey.write()).to.deep.equal(expectedBinaryKey); + }); + + it('ML-DSA + Ed25519 - Test vector', async function () { + const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey }); + const publicKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PublicKey }); + + // `getSigningKey()` internally verifies the ML-DSA binding sigs + const signingKey1 = await privateKey.getSigningKey(); + const signingKey2 = await publicKey.getSigningKey(); + + expect(signingKey1.getKeyID().equals(signingKey2.getKeyID())).to.be.true; + }); + + it('ML-DSA + Ed25519 - throws on unexpected signature algorithm', async () => { + // 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 + const messageWithInvalidSignature = await openpgp.readMessage({ armoredMessage: `-----BEGIN PGP MESSAGE----- + +xDYGAQseECid884kLeNBI6G5jVZW91Kj4uFLakk/+TD7JzIfEl6aaIAzi+n7 +faOuBl6mV5MkLwHLCnUAaELdqXRlc3TCzLUGAR4LAAAAKQWCaELdqSKhBqPi +4UtqST/5MPsnMh8SXppogDOL6ft9o64GXqZXkyQvAAAAAPYsECid884kLeNB +I6G5jVZW91LY/a27CHnHutEk2vCqYzgv9dc1OwygAWExIkhP90L1R2nME5KI +ZQeKkZ5Ail+iK9EhA+HdAtmHZpdAZYFZ+34BpbxAjFGk43DyOdmVyc5g+yfU +yoRGIwnQwUeeCjZ0i9gVeeO02Sm/5Td1w+XLcRjxlt6/FsM2zR5BkX5Qjn0M +whsVRNSkm3G/9sJBqHQt7nqlWgctq8cq0Ak2BhZ6ASVnkeX7E3Mn1E3njsLE +o4FUkE5f3OMYwZTt38pYzSuKQMiVt8M4I4XeF5pGaIlDLOaqQe4hMT9CLq1U +OrJ+OtLAZ5wUroriZNgrR9Fx/J4AdG4ldqYt65Ntb8e+QvT78uQLJESHJuJP +ij4ntbtiu4gfOP03Fje8fVV+KGUE1AhJ6JVF8ETjN1Szgn9sP0D64ARD4E/i +OjHIDozvoVV25vf5jxYmCNvFPaK+SbwqXu3r14dbAmzTNtujmaBoBVjMNHX6 +cfcHSPubriE0xI8bOljdPjFgetKbLqsTkRtX+sfcclLUIjX+2NXSMs1aWBy/ +KaiAkRNplORsECVxy+EEeKAUuW1Vs/kHBmRzvyAiSiphPnx7Slvy2y1fuaZA +Rs/kSkp7VTmeaYD5C2rg0AlEWehU3zaKBFjAlGgczaHYuM1GpvXvLxjiB2gi +jB7PMbB2nnIYWCfo+KKWzft3rIMvSqprGtlOrCuhI1qbkopbeZ9MxwOswgJR +m37bDuONBLprXeTTwbmhledrzmxBauK0uPy9TAcuw7/j73882CySSizFKA9l +vkvCyCIkdk+vSy500sjlwa335Um5JSr84CBmYy6th4jqoErfj8j/d6y/ay+4 +/kFVwoFwha2eE6fqh9hH/A7wkm/xYMYadwaJkAwmvdloxC/o4wZ5dPjfM1VV +c7j2aqVurE/ktslEkevO85SgPjo5SdfuVIlErWoOOg1+hvdMCtuuKSvikchV +BBa3xff+F3OpqtFE2NPR1d3LsWlWArjtLNsVww7YuQYCy5MOCgckGB566SaC +jbxXnLo2kuXry9JpXYzHvxqnxN8Po9Dy85vRvChxyZiJD4hS9/56rtpDlm+3 +2i+V9QF/lEjPUnzy0x/KtWR3D5AVgeY6K/flb3hideHnV/1RrRnyGsXn9s+Q +FVploQ8O1h/xpfPr1H2RI3H0YPA905+lW9c6AYC20QwBd3sX72aA9B1EEZNT +VXBAW6qx5kmgNca+hxbGWlIN0OdBmkU12aTGlpa7/bY2BsoXFQDkq72POB2K +53u2Y40cJfq+p0f3qMh3RDxd22ML7JIoEwRU9pMZx6OqVJYuMMmekJjQnO41 +XIRKQBwJ8K6RRee70HOfd5moRrp77U1N3qbck8j2Tp6eVwAsPddvjJ/+aK/L +pO3t+TVT872bnTxT/zhwvRnIFmwUi/BLGN2wryPx3pxtmYmGLx4DXHx0E7S5 +9IOMZ71nHZyJr2mzqfualDXeEhy1rqpjaMF/++S8JDkXuazaRwV5aEAKQvCf +aNuuilhCqmUnE34yNM+E+LJxcNrXAukqJ1TRH2RNX6zNVR3L59Cw52vZNd2L +nxfPd87WapW6hk9u4MbPh5yMaqNaujiQcEgwJyeoa+pfqUz4Dh4zyKVjqJTR +U9uTnWXipqVOAf7/Wnl8VhvdjnRQ2p/Ps+mjYWVGux3/eX05aRgWe8lJIppJ +zP1pG/Huk3gbzcySp0VXlay6Fywi72HMzYXVRv2GxXTxPMCpDhVx2pud6q9E +oTibBJLiKKvjiss7qCWGQ0SGflGdzzPjHNp9IxsaDGssZpunN7qLOMAvhw67 +wls0j/OhaR3p2War5lUvEbAP4VRUjKsChmjkdaMKEGXXMTDbAsXAuWwEEJ2Y +bA2t5v4Ykn96o+wQ7rXLNdbyRgqy6b5f7edYVdT+3pYyO7IXxcMg7ifA+9Fe +ltbZvFScOkx5buAeQxGvlGmrazxiOVr48r/jBkHEKKy/DwDLu0IaKymJpOC/ +LsgoWGDcV4LCZLo75FoDDeUaqrTkmY4v99nsILF6sXcxPng46jrK3azZzeAG +iiP5DY50gPxWXq+8FPeiXaPOzKYZ2xzjUIj9q2e7WGK7Kz9KxoVQVW0bTFfR +o/ld+/mSgdtZXZTaBT394HDRz8G9XMNKzxnT9+7fGWUUJqPF02mQyRNMX92D +ROrsASAhG7PAvTax9PjZXQPjjcRzINdEDZnktxixRMZsnb7EEX1HDW2IEpCV +LBh2/R4yZZrBUNU7WtACJItMcEfgIPkxq3QrGCSFEcifdHHkeOpMa0BBNBWX +toAjFddLwWcbCMOTwMG8pW0qlXaolw5LvyEbiQfBkKzTGHg+ZqwuIw0ON465 +xgBF2r9156SLyF71mawqa4/1Rq5dgFzuMByWhvWcIamd309UFz4A0AepMAJl +BwTHFzgUvG4Dh2q10PxuhMHzkVVCpbHniiUWqWQkQB2LlC3pKImqzn0sEYDb +XvPtnhveLL+oTvOlkEjYzsbto7yfLwuAEP/480NutxE5utF5ovFGjzRwYSBT +JBDFQC/vgLiobZHpK2UbnywYQ82hEu5W9U+IzjhWc6zgS7+09XAXhOBCzIIJ +6DBCksVVl0cUMc49Aj3LUANspP8turo4CUl+KGcyfmBvsxyKsIJlhwpzL6YF +JJdhQCzrseUsPbyfYnLa5A1rSU5uUl4okS/ecMNrfgTStJW9/CjUE1knaB+h +IRaSGnfKp+9Fts6RQt9afLi7L7KMiWg9CAsejuSoGqNCJQth6VtVULbKBS1X +BDyHG2LH0MGgWLeWb5JQB0NoZBWrrxrOPuNLLq5q3XxxeaHDX29Fq7Toz1pP +Wbd69io60+N1wS16DPe8qkKsDDzfYl6vinQMZG9TCeTfT9ONmbwq5963mgNg +WwQ2d3irnWaEErJV8rqGfBr6Csv/p8Xd1PJtGrFr+YXzsTlnVBrY74yc6Kur +ygGcYYr0Q4Tjt3WhTrmme0hIVTbAPqeNHhRokfN8J9fgoddp4aDePdw6CnRj +XAxA7V7kKC+/sLkSV5nGdK6p8AJQWtChJSLVAWgxdfMyPukjHb9T2Reunj9V +ZnzyIANbQm1CEywo1DpvSuTXWYH53BVfRGzTlxbBXFAKXlzVXZWgyEju9wUC +VuU0YW7GPIiDnLSGnjAhT5bhTmcpp+Gvcb4lw89iScB08k+g9nC2ZZTIYnkX +9qafbfclUdQdBRZNKhrsHw52h3bigmoJnh4HpuwGfsN1qQfeeD0aaFFJHAYk +3UzAc/1GcTttC+0oSzio+U3zeBBwmpDNhUVELqghbZZxlOcR1sMtzNiuXocW +YNnKIvk0sZ7RkpQtojUH9Mz3BNRUO9Pt0oXFOJnYaHoMjGJMgQGWVtSlET4T +XbNV9AKMR3k39as/b6gdK0ZH31TB6lZ4czjsyXsASBr8HVyM60l0jVJOrOZq +xPUT9z7Z/14gfEl55eYmRjXuUqMxcdJozVH826MU8a3OsrPb/lQxS/EQW3Qu +u/JfkG6DahDcQY2ctQcHEcYJudeXEnSRjTsHGlOb+FxdKg0rr5DX8OpY5LLg +1pMLqOESW+luYnraDWB0gN8QWJ/zhgDwx0CTNst94KwLoFsjrWFi7SYeJ/ng ++GL55MCXkT96z9XnDrvEAWSx2JraDdbWKbUajBS0P2DMsZjeVUy05phO/JjZ +CEweOo9/rW8ftkbgMf1qxqey9eZbbYeHASybYjXPvqYccacr+vV2ON/AwCbW +b374EgcjJ3g7nT83M798yFTtw9bxpa4O2gEL3omTGj7LfB2b4pDSfckEy3Xe +GgT4RGpKbnv3P1DpDmnUScFyJ8jUg2hCnYvJ8wTN21BGgNx88Ky7sVYY5T3p +U8ZtCj86lUbhFa5WNjf31ssy+I4WRoFJTdY2WpiV6JLOk4KCL28+B3ooo1mo +6DCNxMnT252EMINAECeVD+v8P+mxkMKr5ydIAZvxtHIxc5HmNbMjq8gjCw30 +6dyq2a70PuiNxwgIZu+dxrPqTyglVJI2eiXqQvwDkA6I+jTVGine3ZrSQqu2 +MmKJUYu5BIiiWTX2aXn4UwE6d3LN/13ddpE8PTMBZjTB9Qe1p1XUVPm5nGYu +zvbi3VYWVvYGHTQ1joUbUYsmgcD1oQVMg0w/DPZJMk6v0xwWsEOCp8puBC9j +dU1o4TiAUEmaotq1o4i5VNg1GNqEoBpzltIKDNGoiJ7jd+3XC/gZ/dVTqc3F +OYgBrDNPmQ7TAOf3Guefcpd/ZTRRP7YiUhjEqrxQhIsxXjrpy/V5furtFBVk +WFYxKgA1Qe5vZxSqTWKIjsqKSls6XE6jX7cIAqrLw6DXLDnGCEKf1wmgpuK2 +ApHXJofrxdehrHkPhwhs37ZPW9QeL/1B3aqGUu5IBouEn34ybE5SrT8SQGUK +AHK2D0BvjA4PhYX0JfRvX7sXQzm2pLo+He6rxbim0BfIz38la43x8xAoga/X ++hYaZ67sU1Vqa5W9yuj+JlWBmfUISFOH1gAAAAAAAAAAAAAAAAAAAAAAAAAA +BQsQGR4j +-----END PGP MESSAGE-----` }); + const publicKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PublicKey }); + + await expect(openpgp.verify({ message: messageWithInvalidSignature, verificationKeys: publicKey, expectSigned: true })).to.be.rejectedWith(/Unexpected hash algorithm for PQC signature/); + }); + + it('ML-DSA + ML-KEM - Test vector: decrypt/verify', async function () { + // Test vector from: https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#appendix-A.3 + const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey }); + + const publicKey = privateKey.toPublic(); + + const armoredMessage = `-----BEGIN PGP MESSAGE----- + +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 +ak/MpQwGKTcC2WrrqWhgq/WTs+8NCk5Vfa2P +-----END PGP MESSAGE-----`; + + const { data: decryptedData, signatures: [{ verified }] } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage }), + decryptionKeys: privateKey, + verificationKeys: publicKey + }); + expect(decryptedData).to.equal('Testing\n'); + expect(await verified).to.be.true; + }); + + it('ML-DSA + ML-KEM - encrypt/sign and decrypt/verify', async function () { + const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey }); + const publicKey = privateKey.toPublic(); + + const plaintext = 'Testing\n'; + const encrypted = await openpgp.encrypt({ message: await openpgp.createMessage({ text: plaintext }), signingKeys: privateKey, encryptionKeys: publicKey }); + const { data, signatures: [{ verified }] } = await openpgp.decrypt({ message: await openpgp.readMessage({ armoredMessage: encrypted }), verificationKeys: publicKey, decryptionKeys: privateKey }); + expect(data).to.equal(plaintext); + await expect(verified).to.eventually.be.true; + }); +}); diff --git a/test/crypto/validate.js b/test/crypto/validate.js index 7fd74124..0d3f5c47 100644 --- a/test/crypto/validate.js +++ b/test/crypto/validate.js @@ -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 () => { diff --git a/test/general/key.js b/test/general/key.js index 1654a18e..d8eaa22a 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -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 }),