Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PQC: Implement draft RFC for ML-DSA with Ed25519 #13

Merged
merged 5 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading