Skip to content

Commit

Permalink
WIP: PQC: Implement draft RFC for SLH-DSA (shake128s variant only)
Browse files Browse the repository at this point in the history
Implements Draft 6
(https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/06/).

NB: signing is currently too slow to be usable (10+ seconds).
  • Loading branch information
larabr committed Jan 17, 2025
1 parent 48ae083 commit 6cdf589
Show file tree
Hide file tree
Showing 18 changed files with 913 additions and 36 deletions.
40 changes: 31 additions & 9 deletions src/crypto/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import OID from '../type/oid';
import { UnsupportedError } from '../packet/packet';
import ECDHXSymmetricKey from '../type/ecdh_x_symkey';

export { isPostQuantumAlgo } from './public_key/post_quantum';

/**
* Encrypts data using specified algorithm and public key parameters.
* See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms.
Expand Down Expand Up @@ -98,7 +100,7 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, pri
}
case enums.publicKey.pqc_mlkem_x25519: {
const { eccPublicKey, mlkemPublicKey } = publicParams;
const { eccCipherText, mlkemCipherText, wrappedKey } = await publicKey.postQuantum.kem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data);
const { eccCipherText, mlkemCipherText, wrappedKey } = await publicKey.postQuantum.mlkem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data);
const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey });
return { eccCipherText, mlkemCipherText, C };
}
Expand Down Expand Up @@ -169,7 +171,7 @@ export async function publicKeyDecrypt(keyAlgo, publicKeyParams, privateKeyParam
const { eccSecretKey, mlkemSecretKey } = privateKeyParams;
const { eccPublicKey, mlkemPublicKey } = publicKeyParams;
const { eccCipherText, mlkemCipherText, C } = sessionKeyParams;
return publicKey.postQuantum.kem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, C.wrappedKey);
return publicKey.postQuantum.mlkem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, C.wrappedKey);
}
default:
throw new Error('Unknown public key encryption algorithm.');
Expand Down Expand Up @@ -252,6 +254,10 @@ export function parsePublicKeyParams(algo, bytes) {
const mldsaPublicKey = util.readExactSubarray(bytes, read, read + 1952); read += mldsaPublicKey.length;
return { read, publicParams: { eccPublicKey, mldsaPublicKey } };
}
case enums.publicKey.pqc_slhdsa_shake128s: {
const slhdsaPublicKey = util.readExactSubarray(bytes, read, read + 32); read += slhdsaPublicKey.length;
return { read, publicParams: { slhdsaPublicKey } };
}
default:
throw new UnsupportedError('Unknown public key encryption algorithm.');
}
Expand Down Expand Up @@ -326,15 +332,20 @@ export async function parsePrivateKeyParams(algo, bytes, publicParams) {
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 publicKey.postQuantum.kem.mlkemExpandSecretSeed(algo, mlkemSeed);
const { mlkemSecretKey } = await publicKey.postQuantum.mlkem.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 publicKey.postQuantum.signature.mldsaExpandSecretSeed(algo, mldsaSeed);
const { mldsaSecretKey } = await publicKey.postQuantum.mldsa.mldsaExpandSecretSeed(algo, mldsaSeed);
return { read, privateParams: { eccSecretKey, mldsaSecretKey, mldsaSeed } };
}
case enums.publicKey.pqc_slhdsa_shake128s: {
const slhdsaSecretKey = util.readExactSubarray(bytes, read, read + 64); read += slhdsaSecretKey.length;

return { read, privateParams: { slhdsaSecretKey } };
}
default:
throw new UnsupportedError('Unknown public key encryption algorithm.');
}
Expand Down Expand Up @@ -425,7 +436,8 @@ export function serializeParams(algo, params) {
enums.publicKey.aead,
enums.publicKey.hmac,
enums.publicKey.pqc_mlkem_x25519,
enums.publicKey.pqc_mldsa_ed25519
enums.publicKey.pqc_mldsa_ed25519,
enums.publicKey.pqc_slhdsa_shake128s
]);

const excludedFields = {
Expand Down Expand Up @@ -503,15 +515,20 @@ export async function generateParams(algo, bits, oid, symmetric) {
return createSymmetricParams(keyMaterial, new SymAlgoEnum(symmetric));
}
case enums.publicKey.pqc_mlkem_x25519:
return publicKey.postQuantum.kem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSeed, mlkemSecretKey, mlkemPublicKey }) => ({
return publicKey.postQuantum.mlkem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSeed, mlkemSecretKey, mlkemPublicKey }) => ({
privateParams: { eccSecretKey, mlkemSeed, mlkemSecretKey },
publicParams: { eccPublicKey, mlkemPublicKey }
}));
case enums.publicKey.pqc_mldsa_ed25519:
return publicKey.postQuantum.signature.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }) => ({
return publicKey.postQuantum.mldsa.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }) => ({
privateParams: { eccSecretKey, mldsaSeed, mldsaSecretKey },
publicParams: { eccPublicKey, mldsaPublicKey }
}));
case enums.publicKey.pqc_slhdsa_shake128s:
return publicKey.postQuantum.slhdsa.generate(algo).then(({ slhdsaSecretKey, slhdsaPublicKey }) => ({
privateParams: { slhdsaSecretKey },
publicParams: { slhdsaPublicKey }
}));
case enums.publicKey.dsa:
case enums.publicKey.elgamal:
throw new Error('Unsupported algorithm for key generation.');
Expand Down Expand Up @@ -606,12 +623,17 @@ export async function validateParams(algo, publicParams, privateParams) {
case enums.publicKey.pqc_mlkem_x25519: {
const { eccSecretKey, mlkemSeed } = privateParams;
const { eccPublicKey, mlkemPublicKey } = publicParams;
return publicKey.postQuantum.kem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed);
return publicKey.postQuantum.mlkem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed);
}
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccSecretKey, mldsaSeed } = privateParams;
const { eccPublicKey, mldsaPublicKey } = publicParams;
return publicKey.postQuantum.signature.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed);
return publicKey.postQuantum.mldsa.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed);
}
case enums.publicKey.pqc_slhdsa_shake128s: {
const { slhdsaSecretKey } = privateParams;
const { slhdsaPublicKey } = publicParams;
return publicKey.postQuantum.slhdsa.validateParams(algo, slhdsaPublicKey, slhdsaSecretKey);
}
default:
throw new Error('Unknown public key algorithm.');
Expand Down
32 changes: 28 additions & 4 deletions src/crypto/public_key/post_quantum/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import * as kem from './kem/index';
import * as signature from './signature';
import * as mlkem from './ml_kem';
import * as mldsa from './ml_dsa';
import * as slhdsa from './slh_dsa';
import enums from '../../../enums';

export {
kem,
signature
mlkem,
mldsa,
slhdsa
};

const pqcAlgos = new Set([
enums.publicKey.pqc_mldsa_ed25519,
enums.publicKey.pqc_mlkem_x25519,
enums.publicKey.pqc_slhdsa_shake128s
]);

export function isPostQuantumAlgo(algo) {
return pqcAlgos.has(algo);
}

export function getRequiredHashAlgo(signatureAlgo) {
switch (signatureAlgo) {
case enums.publicKey.pqc_mldsa_ed25519:
return mldsa.getRequiredHashAlgo(signatureAlgo);
case enums.publicKey.pqc_slhdsa_shake128s:
return slhdsa.getRequiredHashAlgo(signatureAlgo);
default:
throw new Error('Unsupported signature algorithm');
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import enums from '../../../../enums';
import * as mldsa from './ml_dsa';
import * as mldsa from './ml_dsa_pure';
import * as eccdsa from './ecc_dsa';

export async function generate(algo) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { generate, sign, verify, validateParams, getRequiredHashAlgo } from './signature';
export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa';
export { generate, sign, verify, validateParams, getRequiredHashAlgo } from './combiner';
export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa_pure';
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { generate, encrypt, decrypt, validateParams } from './kem';
export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem';
export { generate, encrypt, decrypt, validateParams } from './kem_combiner';
export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem_pure';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as eccKem from './ecc_kem';
import * as mlKem from './ml_kem';
import * as mlKem from './ml_kem_pure';
import * as aesKW from '../../../aes_kw';
import util from '../../../../util';
import enums from '../../../../enums';
Expand Down
1 change: 0 additions & 1 deletion src/crypto/public_key/post_quantum/noble_post_quantum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@

export { ml_kem768 } from '@noble/post-quantum/ml-kem';
export { ml_dsa65 } from '@noble/post-quantum/ml-dsa';

75 changes: 75 additions & 0 deletions src/crypto/public_key/post_quantum/slh_dsa/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import enums from '../../../../enums';
import { getRandomBytes } from '../../../random';


export async function generate(algo) {
switch (algo) {
case enums.publicKey.pqc_slhdsa_shake128s: {
const { slh_dsa_shake_128s } = await import('@noble/post-quantum/slh-dsa');
const { secretKey: slhdsaSecretKey, publicKey: slhdsaPublicKey } = slh_dsa_shake_128s.keygen();

return { slhdsaSecretKey, slhdsaPublicKey };
}
default:
throw new Error('Unsupported signature algorithm');
}
}

export async function sign(signatureAlgo, hashAlgo, slhdsaSecretKey, dataDigest) {
if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) {
// The signature hash algo MUST be set to the specified algorithm, see
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#section-6.1.1
throw new Error('Unexpected hash algorithm for PQC signature');
}

switch (signatureAlgo) {
case enums.publicKey.pqc_slhdsa_shake128s: {
const { slh_dsa_shake_128s } = await import('@noble/post-quantum/slh-dsa');
const slhdsaSignature = slh_dsa_shake_128s.sign(slhdsaSecretKey, dataDigest);
return { slhdsaSignature };
}
default:
throw new Error('Unsupported signature algorithm');
}
}

export async function verify(signatureAlgo, hashAlgo, slhdsaPublicKey, dataDigest, { slhdsaSignature }) {
if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) {
// The signature hash algo MUST be set to the specified algorithm, see
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#section-6.1.1
throw new Error('Unexpected hash algorithm for PQC signature');
}

switch (signatureAlgo) {
case enums.publicKey.pqc_slhdsa_shake128s: {
const { slh_dsa_shake_128s } = await import('@noble/post-quantum/slh-dsa');
return slh_dsa_shake_128s.verify(slhdsaPublicKey, dataDigest, slhdsaSignature);
}
default:
throw new Error('Unsupported signature algorithm');
}
}

export async function validateParams(algo, slhdsaPublicKey, slhdsaSecretKey) {
switch (algo) {
case enums.publicKey.pqc_slhdsa_shake128s: {
// TODO check if more performant validation is possible via public key re-derivation
const randomBytes = getRandomBytes(16);
const { slhdsaSignature } = await sign(algo, slhdsaSecretKey, randomBytes);
const trialSignatureVerified = await verify(algo, slhdsaPublicKey, randomBytes, slhdsaSignature);
return trialSignatureVerified;
}
default:
throw new Error('Unsupported signature algorithm');
}
}

export function getRequiredHashAlgo(signatureAlgo) {
// See https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#section-6.1.1
switch (signatureAlgo) {
case enums.publicKey.pqc_slhdsa_shake128s:
return enums.hash.sha3_256;
default:
throw new Error('Unsupported signature algorithm');
}
}
16 changes: 14 additions & 2 deletions src/crypto/signature.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export function parseSignatureParams(algo, signature) {
const mldsaSignature = util.readExactSubarray(signature, read, read + 3309); read += mldsaSignature.length;
return { read, signatureParams: { eccSignature, mldsaSignature } };
}
case enums.publicKey.pqc_slhdsa_shake128s: {
const slhdsaSignature = util.readExactSubarray(signature, read, read + 7856); read += slhdsaSignature.length;
return { read, signatureParams: { slhdsaSignature } };
}
default:
throw new UnsupportedError('Unknown signature algorithm.');
}
Expand Down Expand Up @@ -142,7 +146,11 @@ export async function verify(algo, hashAlgo, signature, publicParams, privatePar
}
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccPublicKey, mldsaPublicKey } = publicParams;
return publicKey.postQuantum.signature.verify(algo, hashAlgo, eccPublicKey, mldsaPublicKey, hashed, signature);
return publicKey.postQuantum.mldsa.verify(algo, hashAlgo, eccPublicKey, mldsaPublicKey, hashed, signature);
}
case enums.publicKey.pqc_slhdsa_shake128s: {
const { slhdsaPublicKey } = publicParams;
return publicKey.postQuantum.slhdsa.verify(algo, hashAlgo, slhdsaPublicKey, hashed, signature);
}
default:
throw new Error('Unknown signature algorithm.');
Expand Down Expand Up @@ -208,7 +216,11 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da
case enums.publicKey.pqc_mldsa_ed25519: {
const { eccPublicKey } = publicKeyParams;
const { eccSecretKey, mldsaSecretKey } = privateKeyParams;
return publicKey.postQuantum.signature.sign(algo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, hashed);
return publicKey.postQuantum.mldsa.sign(algo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, hashed);
}
case enums.publicKey.pqc_slhdsa_shake128s: {
const { slhdsaSecretKey } = privateKeyParams;
return publicKey.postQuantum.slhdsa.sign(algo, hashAlgo, slhdsaSecretKey, hashed);
}
default:
throw new Error('Unknown signature algorithm.');
Expand Down
3 changes: 2 additions & 1 deletion src/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ export default {
pqc_mlkem_x25519: 105,
/** Post-quantum ML-DSA-64 + Ed25519 (Sign only) */
pqc_mldsa_ed25519: 107,

/** Post-quantum SLH-DSA-128S (Sign only) */
pqc_slhdsa_shake128s: 109,
/** Persistent symmetric keys: encryption algorithm */
aead: 100,
/** Persistent symmetric keys: authentication algorithm */
Expand Down
9 changes: 5 additions & 4 deletions src/key/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ export async function createBindingSignature(subkey, primaryKey, options, config
* @async
*/
export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date = new Date(), targetUserIDs = [], config) {
if (signingKeyPacket.algorithm === enums.publicKey.pqc_mldsa_ed25519) {
// For PQC, the returned hash algo MUST be set to the specified algorithm, see
// https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1.
return crypto.publicKey.postQuantum.signature.getRequiredHashAlgo(signingKeyPacket.algorithm);
// 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.
if (crypto.isPostQuantumAlgo(signingKeyPacket.algorithm)) {
return crypto.publicKey.postQuantum.getRequiredHashAlgo(signingKeyPacket.algorithm);
}

/**
Expand Down Expand Up @@ -475,6 +475,7 @@ export function validateSigningKeyPacket(keyPacket, signature, config) {
case enums.publicKey.ed448:
case enums.publicKey.hmac:
case enums.publicKey.pqc_mldsa_ed25519:
case enums.publicKey.pqc_slhdsa_shake128s:
if (!signature.keyFlags && !config.allowMissingKeyFlags) {
throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`');
}
Expand Down
7 changes: 2 additions & 5 deletions src/packet/public_key.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,8 @@ class PublicKeyPacket {
}
// 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_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');
if (this.version !== 6 && crypto.isPostQuantumAlgo(this.algorithm)) {
throw new Error('Unexpected key version: post-quantum algorithms can only be used with v6 keys');
}
this.publicParams = publicParams;
pos += read;
Expand Down
5 changes: 1 addition & 4 deletions src/packet/secret_key.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,7 @@ 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 ||
this.algorithm === enums.publicKey.pqc_mlkem_x25519
)) {
if (this.version !== 6 && crypto.isPostQuantumAlgo(this.algorithm)) {
throw new Error(`Cannot generate v${this.version} keys of type 'pqc'. Generate a v6 key instead`);
}
const { privateParams, publicParams } = await crypto.generateParams(this.algorithm, bits, curve, symmetric);
Expand Down
Loading

0 comments on commit 6cdf589

Please sign in to comment.