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

Draft
wants to merge 12 commits into
base: v6
Choose a base branch
from
61 changes: 61 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@
"@noble/curves": "^1.4.0",
"@noble/ed25519": "^1.7.3",
"@noble/hashes": "^1.4.0",
"@noble/post-quantum": "^0.1.0",
"@openpgp/asmcrypto.js": "^3.1.0",
"@openpgp/crystals-kyber-js": "^1.1.1",
"@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git",
"@openpgp/tweetnacl": "^1.0.4-1",
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const wasmOptions = {

const getChunkFileName = (chunkInfo, extension) => {
// index files result in chunks named simply 'index', so we rename them to include the package name
if (chunkInfo.name === 'index') {
if (chunkInfo.name === 'index' && chunkInfo.facadeModuleId) {
const packageName = chunkInfo.facadeModuleId.split('/').at(-2); // assume index file is under the root folder
return `${packageName}.${extension}`;
}
Expand Down
68 changes: 64 additions & 4 deletions src/crypto/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 publicKey.postQuantum.kem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data);
const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey });
return { eccCipherText, mlkemCipherText, C };
}
default:
return [];
}
Expand All @@ -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;
Expand Down Expand Up @@ -146,7 +152,7 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
throw new Error('AES session key expected');
}
return publicKey.elliptic.ecdhX.decrypt(
algo, ephemeralPublicKey, C.wrappedKey, A, k);
keyAlgo, ephemeralPublicKey, C.wrappedKey, A, k);
}
case enums.publicKey.aead: {
const { cipher: algo } = publicKeyParams;
Expand All @@ -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 } = publicKeyParams;
const { eccCipherText, mlkemCipherText, C } = sessionKeyParams;
return publicKey.postQuantum.kem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, C.wrappedKey);
}
default:
throw new Error('Unknown public key encryption algorithm.');
}
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -301,6 +323,16 @@ 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 mlkemSecretKey = util.readExactSubarray(bytes, read, read + 2400); read += mlkemSecretKey.length;
return { read, privateParams: { eccSecretKey, mlkemSecretKey } };
}
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 } };
}
default:
throw new UnsupportedError('Unknown public key encryption algorithm.');
}
Expand Down Expand Up @@ -364,6 +396,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.');
}
Expand All @@ -383,7 +421,9 @@ 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 orderedParams = Object.keys(params).map(name => {
const param = params[name];
Expand Down Expand Up @@ -450,6 +490,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 publicKey.postQuantum.kem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey }) => ({
privateParams: { eccSecretKey, mlkemSecretKey },
publicParams: { eccPublicKey, mlkemPublicKey }
}));
case enums.publicKey.pqc_mldsa_ed25519:
return publicKey.postQuantum.signature.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSecretKey, mldsaPublicKey }) => ({
privateParams: { eccSecretKey, mldsaSecretKey },
publicParams: { eccPublicKey, mldsaPublicKey }
}));
case enums.publicKey.dsa:
case enums.publicKey.elgamal:
throw new Error('Unsupported algorithm for key generation.');
Expand Down Expand Up @@ -541,6 +591,16 @@ export async function validateParams(algo, publicParams, privateParams) {
return keySize === keyMaterial.length &&
util.equalsUint8Array(digest, await hash.sha256(hashSeed));
}
case enums.publicKey.pqc_mlkem_x25519: {
const { eccSecretKey, mlkemSecretKey } = privateParams;
const { eccPublicKey, mlkemPublicKey } = publicParams;
return publicKey.postQuantum.kem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSecretKey);
}
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccSecretKey, mldsaSecretKey } = privateParams;
const { eccPublicKey, mldsaPublicKey } = publicParams;
return publicKey.postQuantum.signature.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSecretKey);
}
default:
throw new Error('Unknown public key algorithm.');
}
Expand Down
77 changes: 47 additions & 30 deletions src/crypto/public_key/elliptic/ecdh_x.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,32 +87,23 @@ export async function validateParams(algo, A, k) {
* @async
*/
export async function encrypt(algo, data, recipientA) {
const { ephemeralPublicKey, ephemeralSecretKey } = await generateEphemeralKeyPair(algo);
const sharedSecret = await getSharedSecret(algo, ephemeralSecretKey, recipientA);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
recipientA,
sharedSecret
]);

switch (algo) {
case enums.publicKey.x25519: {
const ephemeralSecretKey = getRandomBytes(32);
const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA);
const { publicKey: ephemeralPublicKey } = x25519.box.keyPair.fromSecretKey(ephemeralSecretKey);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
recipientA,
sharedSecret
]);
const cipherAlgo = enums.symmetric.aes128;
const { keySize } = getCipherParams(cipherAlgo);
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
const wrappedKey = await aesKW.wrap(cipherAlgo, encryptionKey, data);
return { ephemeralPublicKey, wrappedKey };
}
case enums.publicKey.x448: {
const x448 = await util.getNobleCurve(enums.publicKey.x448);
const ephemeralSecretKey = x448.utils.randomPrivateKey();
const sharedSecret = x448.getSharedSecret(ephemeralSecretKey, recipientA);
const ephemeralPublicKey = x448.getPublicKey(ephemeralSecretKey);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
recipientA,
sharedSecret
]);
const cipherAlgo = enums.symmetric.aes256;
const { keySize } = getCipherParams(enums.symmetric.aes256);
const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize);
Expand All @@ -137,27 +128,21 @@ export async function encrypt(algo, data, recipientA) {
* @async
*/
export async function decrypt(algo, ephemeralPublicKey, wrappedKey, A, k) {
const sharedSecret = await getSharedSecret(algo, k, ephemeralPublicKey);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
A,
sharedSecret
]);

switch (algo) {
case enums.publicKey.x25519: {
const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
A,
sharedSecret
]);
const cipherAlgo = enums.symmetric.aes128;
const { keySize } = getCipherParams(cipherAlgo);
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
return aesKW.unwrap(cipherAlgo, encryptionKey, wrappedKey);
}
case enums.publicKey.x448: {
const x448 = await util.getNobleCurve(enums.publicKey.x448);
const sharedSecret = x448.getSharedSecret(k, ephemeralPublicKey);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
A,
sharedSecret
]);
const cipherAlgo = enums.symmetric.aes256;
const { keySize } = getCipherParams(enums.symmetric.aes256);
const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize);
Expand All @@ -180,3 +165,35 @@ export function getPayloadSize(algo) {
throw new Error('Unsupported ECDH algorithm');
}
}

export async function generateEphemeralKeyPair(algo) {
switch (algo) {
case enums.publicKey.x25519: {
const ephemeralSecretKey = getRandomBytes(getPayloadSize(algo));
const { publicKey: ephemeralPublicKey } = x25519.box.keyPair.fromSecretKey(ephemeralSecretKey);
return { ephemeralPublicKey, ephemeralSecretKey };
}
case enums.publicKey.x448: {
const x448 = await util.getNobleCurve(enums.publicKey.x448);
const ephemeralSecretKey = x448.utils.randomPrivateKey();
const ephemeralPublicKey = x448.getPublicKey(ephemeralSecretKey);
return { ephemeralSecretKey, ephemeralPublicKey };
}

default:
throw new Error('Unsupported ECDH algorithm');
}
}

export async function getSharedSecret(algo, secretKey, recipientPublicKey) {
switch (algo) {
case enums.publicKey.x25519:
return x25519.scalarMult(secretKey, recipientPublicKey);
case enums.publicKey.x448: {
const x448 = await util.getNobleCurve(enums.publicKey.x448);
return x448.getSharedSecret(secretKey, recipientPublicKey);
}
default:
throw new Error('Unsupported ECDH algorithm');
}
}
5 changes: 4 additions & 1 deletion src/crypto/public_key/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as elgamal from './elgamal';
import * as elliptic from './elliptic';
import * as dsa from './dsa';
import * as hmac from './hmac';
import * as postQuantum from './post_quantum';

export default {
/** @see module:crypto/public_key/rsa */
Expand All @@ -19,5 +20,7 @@ export default {
/** @see module:crypto/public_key/dsa */
dsa: dsa,
/** @see module:crypto/public_key/hmac */
hmac: hmac
hmac: hmac,
/** @see module:crypto/public_key/post_quantum */
postQuantum
};
Loading
Loading