WIP: Add ML-DSA

This commit is contained in:
larabr 2023-11-03 13:14:36 +01:00 committed by Daniel Huigens
parent b9b5667a3f
commit 0468a41f98
15 changed files with 1355 additions and 26 deletions

5
package-lock.json generated
View File

@ -3698,7 +3698,6 @@
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
@ -3709,7 +3708,6 @@
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
@ -7028,8 +7026,7 @@
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
]
},
"node_modules/ignore": {
"version": "5.3.2",

View File

@ -331,8 +331,9 @@ export async function parsePrivateKeyParams(algo, bytes, publicParams) {
}
case enums.publicKey.pqc_mldsa_ed25519: {
const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.ed25519)); read += eccSecretKey.length;
const mldsaSecretKey = util.readExactSubarray(bytes, read, read + 4032); read += mldsaSecretKey.length;
return { read, privateParams: { eccSecretKey, mldsaSecretKey } };
const mldsaSeed = util.readExactSubarray(bytes, read, read + 32); read += mldsaSeed.length;
const { mldsaSecretKey } = await publicKey.postQuantum.signature.mldsaExpandSecretSeed(algo, mldsaSeed);
return { read, privateParams: { eccSecretKey, mldsaSecretKey, mldsaSeed } };
}
default:
throw new UnsupportedError('Unknown public key encryption algorithm.');
@ -429,6 +430,7 @@ export function serializeParams(algo, params) {
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 => {
@ -506,8 +508,8 @@ export async function generateParams(algo, bits, oid, symmetric) {
publicParams: { eccPublicKey, mlkemPublicKey }
}));
case enums.publicKey.pqc_mldsa_ed25519:
return publicKey.postQuantum.signature.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSecretKey, mldsaPublicKey }) => ({
privateParams: { eccSecretKey, mldsaSecretKey },
return publicKey.postQuantum.signature.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }) => ({
privateParams: { eccSecretKey, mldsaSeed, mldsaSecretKey },
publicParams: { eccPublicKey, mldsaPublicKey }
}));
case enums.publicKey.dsa:
@ -607,9 +609,9 @@ export async function validateParams(algo, publicParams, privateParams) {
return publicKey.postQuantum.kem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed);
}
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccSecretKey, mldsaSecretKey } = privateParams;
const { eccSecretKey, mldsaSeed } = privateParams;
const { eccPublicKey, mldsaPublicKey } = publicParams;
return publicKey.postQuantum.signature.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSecretKey);
return publicKey.postQuantum.signature.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed);
}
default:
throw new Error('Unknown public key algorithm.');

View File

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

View File

@ -0,0 +1,49 @@
// TODOOOOO is this file needed? vs inlining calls in signature.js?
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 } from './signature';
export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa';

View File

@ -0,0 +1,71 @@
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/ml-dsa');
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/ml-dsa');
const dataDigestWithContext = util.concatUint8Array([new Uint8Array([0, 0]), dataDigest]);
const mldsaSignature = ml_dsa65.sign(mldsaSecretKey, dataDigestWithContext);
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/ml-dsa');
const dataDigestWithContext = util.concatUint8Array([new Uint8Array([0, 0]), dataDigest]);
return ml_dsa65.verify(mldsaPublicKey, dataDigestWithContext, 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,48 @@
import enums from '../../../../enums';
import * as mldsa from './ml_dsa';
import * as eccdsa from './ecc_dsa';
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 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

@ -70,6 +70,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 * publicKey.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.');
}
@ -134,6 +140,10 @@ export async function verify(algo, hashAlgo, signature, publicParams, privatePar
const { keyMaterial } = privateParams;
return publicKey.hmac.verify(algo.getValue(), keyMaterial, signature.mac.data, hashed);
}
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccPublicKey, mldsaPublicKey } = publicParams;
return publicKey.postQuantum.signature.verify(algo, hashAlgo, eccPublicKey, mldsaPublicKey, hashed, signature);
}
default:
throw new Error('Unknown signature algorithm.');
}
@ -195,6 +205,11 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da
const mac = await publicKey.hmac.sign(algo.getValue(), keyMaterial, hashed);
return { mac: new ShortByteString(mac) };
}
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccPublicKey } = publicKeyParams;
const { eccSecretKey, mldsaSecretKey } = privateKeyParams;
return publicKey.postQuantum.signature.sign(algo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, hashed);
}
default:
throw new Error('Unknown signature algorithm.');
}

View File

@ -98,6 +98,9 @@ export default {
ed448: 28,
/** Post-quantum ML-KEM-768 + X25519 (Encrypt only) */
pqc_mlkem_x25519: 105,
/** Post-quantum ML-DSA-64 + Ed25519 (Sign only) */
pqc_mldsa_ed25519: 107,
/** Persistent symmetric keys: encryption algorithm */
aead: 100,
/** Persistent symmetric keys: authentication algorithm */

View File

@ -399,7 +399,7 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) {
switch (options.type) {
case 'pqc':
if (options.sign) {
throw new Error('Post-quantum signing algorithms are not yet supported.');
options.algorithm = enums.publicKey.pqc_mldsa_ed25519;
} else {
options.algorithm = enums.publicKey.pqc_mlkem_x25519;
}
@ -462,6 +462,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`');
}

View File

@ -138,9 +138,13 @@ 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.
// The composite ML-KEM + ECDH schemes MUST be used only with v6 keys.
if (this.version !== 6 && this.algorithm === enums.publicKey.pqc_mlkem_x25519) {
throw new Error('Unexpected key version: ML-KEM algorithms can only be used with v6 keys');
if (this.version !== 6 && (
this.algorithm === enums.publicKey.pqc_mldsa_ed25519 ||
this.algorithm === enums.publicKey.pqc_mlkem_x25519
)) {
throw new Error('Unexpected key version: ML-DSA and ML-KEM algorithms can only be used with v6 keys');
}
this.publicParams = publicParams;
pos += read;

View File

@ -532,7 +532,10 @@ 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_mlkem_x25519) {
if (this.version !== 6 && (
this.algorithm === enums.publicKey.pqc_mldsa_ed25519 ||
this.algorithm === enums.publicKey.pqc_mlkem_x25519
)) {
throw new Error(`Cannot generate v${this.version} keys of type 'pqc'. Generate a v6 key instead`);
}
const { privateParams, publicParams } = await crypto.generateParams(this.algorithm, bits, curve, symmetric);

File diff suppressed because it is too large Load Diff

View File

@ -315,31 +315,41 @@ export default () => {
});
describe('PQC parameter validation', function() {
let pqcSigningKey;
let pqcEncryptionSubkey;
before(async () => {
const key = await generatePrivateKeyObject({ type: 'symmetric', subkeys: [{ type: 'pqc', config: { v6Keys: true } }] });
pqcEncryptionSubkey = key.subkeys[0];
pqcSigningKey = await generatePrivateKeyObject({ type: 'pqc', config: { v6Keys: true } });
pqcEncryptionSubkey = pqcSigningKey.subkeys[0];
});
async function cloneSubeyPacket(subkey) {
const subkeyPacket = new openpgp.SecretSubkeyPacket();
await subkeyPacket.read(subkey.keyPacket.write());
return subkeyPacket;
}
it('generated params are valid', async function() {
await expect(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 cloneSubeyPacket(pqcEncryptionSubkey);
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 cloneSubeyPacket(pqcEncryptionSubkey);
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');

View File

@ -4607,15 +4607,19 @@ I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg==
expect(v6Key.subkeys).to.have.length(1);
});
it('should throw when trying to add a ML-KEM PQC key to a v4 key', async function() {
it('should throw when trying to add a ML-KEM or 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-KEM subkey
await expect(v4Key.addSubkey({ type: 'pqc', sign: false })).to.be.rejectedWith(/Cannot generate v4 keys of type 'pqc'/);
expect(v4Key.subkeys).to.have.length(1);
// try adding an ML-DSA subkey
await expect(v4Key.addSubkey({ type: 'pqc', sign: true })).to.be.rejectedWith(/Cannot generate v4 keys of type 'pqc'/);
expect(v4Key.subkeys).to.have.length(1);
});
it('should throw when trying to encrypt a subkey separately from key', async function() {