mirror of
https://github.com/ProtonMail/openpgpjs.git
synced 2026-01-16 23:00:23 +00:00
PQC: implement ML-KEM and ML-DSA from RFC draft-10 (past last call)
See MRs #10, #13, #19 for the incremental changes . Reference: https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/10/ . Co-authored-by: Daniel Huigens <d.huigens@protonmail.com>
This commit is contained in:
parent
09263d1a4a
commit
2e0f652e85
30 changed files with 1736 additions and 48 deletions
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 } } } } }"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
2
openpgp.d.ts
vendored
2
openpgp.d.ts
vendored
|
|
@ -678,7 +678,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;
|
||||
|
|
|
|||
27
package-lock.json
generated
27
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
7
src/crypto/public_key/post_quantum/index.js
Normal file
7
src/crypto/public_key/post_quantum/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import * as kem from './kem/index';
|
||||
import * as signature from './signature';
|
||||
|
||||
export {
|
||||
kem,
|
||||
signature
|
||||
};
|
||||
51
src/crypto/public_key/post_quantum/kem/ecc_kem.js
Normal file
51
src/crypto/public_key/post_quantum/kem/ecc_kem.js
Normal file
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
2
src/crypto/public_key/post_quantum/kem/index.js
Normal file
2
src/crypto/public_key/post_quantum/kem/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { generate, encrypt, decrypt, validateParams } from './kem';
|
||||
export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem';
|
||||
55
src/crypto/public_key/post_quantum/kem/kem.js
Normal file
55
src/crypto/public_key/post_quantum/kem/kem.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
72
src/crypto/public_key/post_quantum/kem/ml_kem.js
Normal file
72
src/crypto/public_key/post_quantum/kem/ml_kem.js
Normal file
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
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, isCompatibleHashAlgo } 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');
|
||||
}
|
||||
}
|
||||
60
src/crypto/public_key/post_quantum/signature/signature.js
Normal file
60
src/crypto/public_key/post_quantum/signature/signature.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
|||
4
src/enums.d.ts
vendored
4
src/enums.d.ts
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
1083
test/crypto/postQuantum.js
Normal file
1083
test/crypto/postQuantum.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue