mirror of
https://github.com/openpgpjs/openpgpjs.git
synced 2025-03-30 15:08:32 +00:00
PQC: Implement draft RFC for ML-DSA with Ed25519 (#13)
Implements Draft 6 (https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/06/). Also, chunk ML-KEM and ML-DSA together in lightweight bundle. Noble-curves had to be updated to v1.7.0 to ensure the same version of noble-hashes is used as noble-post-quantum, making it possible to reuse the sha3 code/chunk across libs.
This commit is contained in:
parent
f1f7ca972f
commit
8e497f419b
3
.github/test-suite/config.json.template
vendored
3
.github/test-suite/config.json.template
vendored
@ -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 } } } } }"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
8
openpgp.d.ts
vendored
8
openpgp.d.ts
vendored
@ -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;
|
||||
@ -825,7 +825,7 @@ export namespace enums {
|
||||
aeadEncryptedData = 20
|
||||
}
|
||||
|
||||
export type publicKeyNames = 'rsaEncryptSign' | 'rsaEncrypt' | 'rsaSign' | 'elgamal' | 'dsa' | 'ecdh' | 'ecdsa' | 'eddsaLegacy' | 'aedh' | 'aedsa' | 'ed25519' | 'x25519' | 'ed448' | 'x448';
|
||||
export type publicKeyNames = 'rsaEncryptSign' | 'rsaEncrypt' | 'rsaSign' | 'elgamal' | 'dsa' | 'ecdh' | 'ecdsa' | 'eddsaLegacy' | 'aedh' | 'aedsa' | 'ed25519' | 'x25519' | 'ed448' | 'x448' | 'pqc_mlkem_x25519' | 'pqc_mldsa_ed25519';
|
||||
export enum publicKey {
|
||||
rsaEncryptSign = 1,
|
||||
rsaEncrypt = 2,
|
||||
@ -840,7 +840,9 @@ export namespace enums {
|
||||
x25519 = 25,
|
||||
x448 = 26,
|
||||
ed25519 = 27,
|
||||
ed448 = 28
|
||||
ed448 = 28,
|
||||
pqc_mlkem_x25519 = 105,
|
||||
pqc_mldsa_ed25519 = 107
|
||||
}
|
||||
|
||||
export enum curve {
|
||||
|
31
package-lock.json
generated
31
package-lock.json
generated
@ -10,7 +10,7 @@
|
||||
"license": "LGPL-3.0+",
|
||||
"devDependencies": {
|
||||
"@noble/ciphers": "^1.0.0",
|
||||
"@noble/curves": "^1.6.0",
|
||||
"@noble/curves": "^1.7.0",
|
||||
"@noble/ed25519": "^1.7.3",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
"@noble/post-quantum": "^0.2.1",
|
||||
@ -944,12 +944,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz",
|
||||
"integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz",
|
||||
"integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.5.0"
|
||||
"@noble/hashes": "1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
@ -971,10 +972,11 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz",
|
||||
"integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==",
|
||||
"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"
|
||||
},
|
||||
@ -995,19 +997,6 @@
|
||||
"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",
|
||||
|
@ -63,7 +63,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@noble/ciphers": "^1.0.0",
|
||||
"@noble/curves": "^1.6.0",
|
||||
"@noble/curves": "^1.7.0",
|
||||
"@noble/ed25519": "^1.7.3",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
"@noble/post-quantum": "^0.2.1",
|
||||
|
@ -247,6 +247,11 @@ export function parsePublicKeyParams(algo, bytes) {
|
||||
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.');
|
||||
}
|
||||
@ -324,6 +329,12 @@ export async function parsePrivateKeyParams(algo, bytes, publicParams) {
|
||||
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.');
|
||||
}
|
||||
@ -414,10 +425,12 @@ export function serializeParams(algo, params) {
|
||||
enums.publicKey.aead,
|
||||
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_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 => {
|
||||
@ -494,6 +507,11 @@ export async function generateParams(algo, bits, oid, symmetric) {
|
||||
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.');
|
||||
@ -590,6 +608,11 @@ export async function validateParams(algo, publicParams, 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.');
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import * as kem from './kem/index';
|
||||
import * as signature from './signature';
|
||||
|
||||
export {
|
||||
kem
|
||||
kem,
|
||||
signature
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ export async function generate(algo) {
|
||||
export async function expandSecretSeed(algo, seed) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mlkem_x25519: {
|
||||
const { ml_kem768 } = await import('@noble/post-quantum/ml-kem');
|
||||
const { ml_kem768 } = await import('../noble_post_quantum');
|
||||
const { publicKey: encapsulationKey, secretKey: decapsulationKey } = ml_kem768.keygen(seed);
|
||||
|
||||
return { mlkemPublicKey: encapsulationKey, mlkemSecretKey: decapsulationKey };
|
||||
@ -37,7 +37,7 @@ export async function expandSecretSeed(algo, seed) {
|
||||
export async function encaps(algo, mlkemRecipientPublicKey) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mlkem_x25519: {
|
||||
const { ml_kem768 } = await import('@noble/post-quantum/ml-kem');
|
||||
const { ml_kem768 } = await import('../noble_post_quantum');
|
||||
const { cipherText: mlkemCipherText, sharedSecret: mlkemKeyShare } = ml_kem768.encapsulate(mlkemRecipientPublicKey);
|
||||
|
||||
return { mlkemCipherText, mlkemKeyShare };
|
||||
@ -50,7 +50,7 @@ export async function encaps(algo, mlkemRecipientPublicKey) {
|
||||
export async function decaps(algo, mlkemCipherText, mlkemSecretKey) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mlkem_x25519: {
|
||||
const { ml_kem768 } = await import('@noble/post-quantum/ml-kem');
|
||||
const { ml_kem768 } = await import('../noble_post_quantum');
|
||||
const mlkemKeyShare = ml_kem768.decapsulate(mlkemCipherText, mlkemSecretKey);
|
||||
|
||||
return mlkemKeyShare;
|
||||
|
10
src/crypto/public_key/post_quantum/noble_post_quantum.ts
Normal file
10
src/crypto/public_key/post_quantum/noble_post_quantum.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* This file is needed to dynamic import noble-post-quantum libs.
|
||||
* Separate dynamic imports are not convenient as they result in multiple chunks,
|
||||
* which ultimately share a lot of code and need to be imported together
|
||||
* when it comes to Proton's ML-DSA + ML-KEM keys.
|
||||
*/
|
||||
|
||||
export { ml_kem768 } from '@noble/post-quantum/ml-kem';
|
||||
export { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
|
||||
|
46
src/crypto/public_key/post_quantum/signature/ecc_dsa.js
Normal file
46
src/crypto/public_key/post_quantum/signature/ecc_dsa.js
Normal file
@ -0,0 +1,46 @@
|
||||
import * as eddsa from '../../elliptic/eddsa';
|
||||
import enums from '../../../../enums';
|
||||
|
||||
export async function generate(algo) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mldsa_ed25519: {
|
||||
const { A, seed } = await eddsa.generate(enums.publicKey.ed25519);
|
||||
return {
|
||||
eccPublicKey: A,
|
||||
eccSecretKey: seed
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported signature algorithm');
|
||||
}
|
||||
}
|
||||
|
||||
export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest) {
|
||||
switch (signatureAlgo) {
|
||||
case enums.publicKey.pqc_mldsa_ed25519: {
|
||||
const { RS: eccSignature } = await eddsa.sign(enums.publicKey.ed25519, hashAlgo, null, eccPublicKey, eccSecretKey, dataDigest);
|
||||
|
||||
return { eccSignature };
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported signature algorithm');
|
||||
}
|
||||
}
|
||||
|
||||
export async function verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature) {
|
||||
switch (signatureAlgo) {
|
||||
case enums.publicKey.pqc_mldsa_ed25519:
|
||||
return eddsa.verify(enums.publicKey.ed25519, hashAlgo, { RS: eccSignature }, null, eccPublicKey, dataDigest);
|
||||
default:
|
||||
throw new Error('Unsupported signature algorithm');
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateParams(algo, eccPublicKey, eccSecretKey) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mldsa_ed25519:
|
||||
return eddsa.validateParams(enums.publicKey.ed25519, eccPublicKey, eccSecretKey);
|
||||
default:
|
||||
throw new Error('Unsupported signature algorithm');
|
||||
}
|
||||
}
|
2
src/crypto/public_key/post_quantum/signature/index.js
Normal file
2
src/crypto/public_key/post_quantum/signature/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export { generate, sign, verify, validateParams, getRequiredHashAlgo } from './signature';
|
||||
export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa';
|
69
src/crypto/public_key/post_quantum/signature/ml_dsa.js
Normal file
69
src/crypto/public_key/post_quantum/signature/ml_dsa.js
Normal file
@ -0,0 +1,69 @@
|
||||
import enums from '../../../../enums';
|
||||
import util from '../../../../util';
|
||||
import { getRandomBytes } from '../../../random';
|
||||
|
||||
export async function generate(algo) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mldsa_ed25519: {
|
||||
const mldsaSeed = getRandomBytes(32);
|
||||
const { mldsaSecretKey, mldsaPublicKey } = await expandSecretSeed(algo, mldsaSeed);
|
||||
|
||||
return { mldsaSeed, mldsaSecretKey, mldsaPublicKey };
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported signature algorithm');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand ML-DSA secret seed and retrieve the secret and public key material
|
||||
* @param {module:enums.publicKey} algo - Public key algorithm
|
||||
* @param {Uint8Array} seed - secret seed to expand
|
||||
* @returns {Promise<{ mldsaPublicKey: Uint8Array, mldsaSecretKey: Uint8Array }>}
|
||||
*/
|
||||
export async function expandSecretSeed(algo, seed) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mldsa_ed25519: {
|
||||
const { ml_dsa65 } = await import('../noble_post_quantum');
|
||||
const { secretKey: mldsaSecretKey, publicKey: mldsaPublicKey } = ml_dsa65.keygen(seed);
|
||||
|
||||
return { mldsaSecretKey, mldsaPublicKey };
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported signature algorithm');
|
||||
}
|
||||
}
|
||||
|
||||
export async function sign(algo, mldsaSecretKey, dataDigest) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mldsa_ed25519: {
|
||||
const { ml_dsa65 } = await import('../noble_post_quantum');
|
||||
const mldsaSignature = ml_dsa65.sign(mldsaSecretKey, dataDigest);
|
||||
return { mldsaSignature };
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported signature algorithm');
|
||||
}
|
||||
}
|
||||
|
||||
export async function verify(algo, mldsaPublicKey, dataDigest, mldsaSignature) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mldsa_ed25519: {
|
||||
const { ml_dsa65 } = await import('../noble_post_quantum');
|
||||
return ml_dsa65.verify(mldsaPublicKey, dataDigest, mldsaSignature);
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported signature algorithm');
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateParams(algo, mldsaPublicKey, mldsaSeed) {
|
||||
switch (algo) {
|
||||
case enums.publicKey.pqc_mldsa_ed25519: {
|
||||
const { mldsaPublicKey: expectedPublicKey } = await expandSecretSeed(algo, mldsaSeed);
|
||||
return util.equalsUint8Array(mldsaPublicKey, expectedPublicKey);
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported signature algorithm');
|
||||
}
|
||||
}
|
70
src/crypto/public_key/post_quantum/signature/signature.js
Normal file
70
src/crypto/public_key/post_quantum/signature/signature.js
Normal file
@ -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;
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
* @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';
|
||||
@ -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 * 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 hmac.verify(algo.getValue(), keyMaterial, signature.mac.data, hashed);
|
||||
}
|
||||
case enums.publicKey.pqc_mldsa_ed25519: {
|
||||
const { eccPublicKey, mldsaPublicKey } = publicParams;
|
||||
return 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 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 postQuantum.signature.sign(algo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, hashed);
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown signature algorithm.');
|
||||
}
|
||||
|
@ -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 */
|
||||
|
@ -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';
|
||||
|
||||
@ -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 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).
|
||||
@ -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;
|
||||
}
|
||||
@ -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`');
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 generateParams(this.algorithm, bits, curve, symmetric);
|
||||
|
@ -267,9 +267,9 @@ export default () => describe('ECDH key exchange @lightweight', function () {
|
||||
for (const { vector } of vectors) {
|
||||
const lowOrderPoint = util.hexToUint8Array(vector);
|
||||
const { A: K_A, k: a } = await elliptic_curves.ecdhX.generate(openpgp.enums.publicKey.x448);
|
||||
await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x448, data, lowOrderPoint)).to.be.rejectedWith(/Invalid private or public key received|expected valid u|low order point/);
|
||||
await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x448, data, lowOrderPoint)).to.be.rejectedWith(/invalid private or public key received|expected valid u|low order point/);
|
||||
const dummyWrappedKey = new Uint8Array(32); // expected to be unused
|
||||
await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x448, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejectedWith(/Invalid private or public key received|expected valid u|low order point/);
|
||||
await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x448, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejectedWith(/invalid private or public key received|expected valid u|low order point/);
|
||||
}
|
||||
});
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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');
|
||||
|
@ -4612,15 +4612,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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user