Skip to content

Commit

Permalink
PQC: Implement draft RFC for ML-DSA with Ed25519 (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
larabr authored Nov 25, 2024
1 parent 1d68e66 commit a5eb714
Show file tree
Hide file tree
Showing 16 changed files with 1,314 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .github/test-suite/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -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 } } } } }"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion openpgp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,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;
Expand Down
16 changes: 9 additions & 7 deletions src/crypto/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down Expand Up @@ -428,7 +429,8 @@ export function serializeParams(algo, params) {
]);

const excludedFields = {
[enums.publicKey.pqc_mlkem_x25519]: new Set(['mlkemSecretKey']) // only `mlkemSeed` is serialized
[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 => {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.');
Expand Down
4 changes: 3 additions & 1 deletion src/crypto/public_key/post_quantum/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as kem from './kem/index';
import * as signature from './signature';

export {
kem
kem,
signature
};
46 changes: 46 additions & 0 deletions src/crypto/public_key/post_quantum/signature/ecc_dsa.js
Original file line number Diff line number Diff line change
@@ -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');
}
}
2 changes: 2 additions & 0 deletions src/crypto/public_key/post_quantum/signature/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { generate, sign, verify, validateParams, getRequiredHashAlgo } from './signature';
export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa';
69 changes: 69 additions & 0 deletions src/crypto/public_key/post_quantum/signature/ml_dsa.js
Original file line number Diff line number Diff line change
@@ -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/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 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/ml-dsa');
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');
}
}
70 changes: 70 additions & 0 deletions src/crypto/public_key/post_quantum/signature/signature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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) {
if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) {
// The signature hash algo MUST be set to the specified algorithm, see
// https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1.
throw new Error('Unexpected hash algorithm for PQC signature');
}

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 }) {
if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) {
// The signature hash algo MUST be set to the specified algorithm, see
// https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1.
throw new Error('Unexpected hash algorithm for PQC signature');
}

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 getRequiredHashAlgo(signatureAlgo) {
// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1.
switch (signatureAlgo) {
case enums.publicKey.pqc_mldsa_ed25519:
return enums.hash.sha3_256;
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;
}
15 changes: 15 additions & 0 deletions src/crypto/signature.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -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.');
}
Expand Down
3 changes: 3 additions & 0 deletions src/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,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 */
Expand Down
9 changes: 8 additions & 1 deletion src/key/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ export async function createBindingSignature(subkey, primaryKey, options, config
* @async
*/
export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date = new Date(), targetUserIDs = [], config) {
if (signingKeyPacket.algorithm === enums.publicKey.pqc_mldsa_ed25519) {
// For PQC, the returned hash algo MUST be set to the specified algorithm, see
// https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1.
return crypto.publicKey.postQuantum.signature.getRequiredHashAlgo(signingKeyPacket.algorithm);
}

/**
* If `preferredSenderAlgo` appears in the prefs of all recipients, we pick it; otherwise, we use the
* strongest supported algo (`defaultAlgo` is always implicitly supported by all keys).
Expand Down Expand Up @@ -405,7 +411,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;
}
Expand Down Expand Up @@ -468,6 +474,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`');
}
Expand Down
8 changes: 6 additions & 2 deletions src/packet/public_key.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/packet/secret_key.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit a5eb714

Please sign in to comment.