Skip to content

Commit

Permalink
Fix benchmarks and build
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Aug 22, 2024
1 parent c05c4a0 commit f75f3ca
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 360 deletions.
1 change: 1 addition & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
node:
- 18
- 20
- 22
steps:
- uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4
- name: Use Node.js ${{ matrix.node }}
Expand Down
57 changes: 29 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Auditable & minimal JS implementation of public-key post-quantum cryptography.
- 🦾 ML-KEM & CRYSTALS-Kyber: lattice-based kem from FIPS-203
- 🔋 ML-DSA & CRYSTALS-Dilithium: lattice-based signatures from FIPS-204
- 🐈 SLH-DSA & SPHINCS+: hash-based signatures from FIPS-205
- 🪶 113KB (20KB gzipped) for everything including bundled hashes, 71KB (14KB gzipped) for ML-KEM build
- 🪶 77KB (15KB gzipped) for everything including bundled hashes

For discussions, questions and support, visit
[GitHub Discussions](https://github.com/paulmillr/noble-post-quantum/discussions)
Expand Down Expand Up @@ -45,12 +45,17 @@ A standalone file
[noble-post-quantum.js](https://github.com/paulmillr/noble-post-quantum/releases) is also available.

```js
// import * from '@noble/post-quantum'; // Error: use sub-imports, to ensure small app size
// import * from '@noble/post-quantum'; // Error: use sub-imports instead
import { ml_kem512, ml_kem768, ml_kem1024 } from '@noble/post-quantum/ml-kem';
import { ml_dsa44, ml_dsa65, ml_dsa87 } from '@noble/post-quantum/ml-dsa';
import {
slh_dsa_shake_128f, slh_dsa_shake_128s, slh_dsa_sha2_128f, slh_dsa_sha2_128s,
} from '@noble/ciphers/slh-dsa';
slh_dsa_shake_128f, slh_dsa_shake_128s,
slh_dsa_shake_192f, slh_dsa_shake_192s,
slh_dsa_shake_256f, slh_dsa_shake_256s,
slh_dsa_sha2_128f, slh_dsa_sha2_128s,
slh_dsa_sha2_192f, slh_dsa_sha2_192s,
slh_dsa_sha2_256f, slh_dsa_sha2_256s,
} from '@noble/post-quantum/slh-dsa';
// import { ml_kem768 } from 'npm:@noble/post-quantum@0.1.0/ml-kem'; // Deno
```

Expand All @@ -74,14 +79,14 @@ import {
| ML-DSA | Normal | 1.3 - 2.5KB | 2.5 - 4.5KB | 1990s | 2020s | Yes |
| SLH-DSA | Slow | 32 - 128B | 17 - 50KB | 1970s | 2020s | Yes |

Speed (higher is better):
JS speed (higher is better):

| OPs/sec | Keygen | Signing | Verification | Shared secret |
| ------------ | ------ | ------- | ------------ | ------------- |
| ECC ed25519 | 10270 | 5110 | 1050 | 1470 |
| ML-KEM-512 | 3050 | | | 2090 |
| ML-DSA44 | 580 | 170 | 550 | |
| SLH-DSA-128f | 200 | 8 | 140 | |
| SLH-DSA-SHA2-128f | 200 | 8 | 140 | |

We suggest to use ECC + ML-KEM for key agreement, SLH-DSA for pq signatures.

Expand All @@ -96,18 +101,19 @@ suffer less from quantum computers. For AES, simply update from AES-128 to AES-2

```ts
import { ml_kem512, ml_kem768, ml_kem1024 } from '@noble/post-quantum/ml-kem';
const aliceKeys = ml_kem768.keygen(); // [Alice] generates key pair (secret and public key)
const alicePub = aliceKeys.publicKey; // [Alice] sends public key to Bob (somehow)
// [Alice] generates secret & public keys, then sends publicKey to Bob
const aliceKeys = ml_kem768.keygen();
const alicePub = aliceKeys.publicKey;

// [Bob] generates shared secret for Alice publicKey
// bobShared never leaves [Bob] system and unknown to other parties
// bobShared never leaves [Bob] system and is unknown to other parties
const { cipherText, sharedSecret: bobShared } = ml_kem768.encapsulate(alicePub);

// Alice gets and decrypts cipherText from Bob
const aliceShared = ml_kem768.decapsulate(cipherText, aliceKeys.secretKey); // [Alice] decrypts sharedSecret from Bob
const aliceShared = ml_kem768.decapsulate(cipherText, aliceKeys.secretKey);

// Now, both Alice and Both have same sharedSecret key without exchanging in plainText
// aliceShared == bobShared
// Now, both Alice and Both have same sharedSecret key
// without exchanging in plainText: aliceShared == bobShared

// Warning: Can be MITM-ed
const carolKeys = kyber1024.keygen();
Expand All @@ -117,24 +123,18 @@ notDeepStrictEqual(aliceShared, carolShared); // Different key!

Lattice-based key encapsulation mechanism, defined in [FIPS-203](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf).

See [official site](https://www.pq-crystals.org/kyber/resources.shtml) and [repo](https://github.com/pq-crystals/kyber).

Key encapsulation is similar to DH / ECDH (think X25519), with important differences:

- We can't verify if it was "Bob" who've sent the shared secret.
In ECDH, it's always verified
- It is probabalistic and relies on quality of randomness (CSPRNG).
ECDH doesn't (to this extent).
- Decapsulation never throws an error, even when shared secret was
encrypted by a different public key. It will just return a different
shared secret

See [website](https://www.pq-crystals.org/kyber/resources.shtml) and [repo](https://github.com/pq-crystals/kyber).
There are some concerns with regards to security: see
[djb blog](https://blog.cr.yp.to/20231003-countcorrectly.html) and
[mailing list](https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/W2VOzy0wz_E).

Old, incompatible version (Kyber) is not provided. Open an issue if you need it.

> [!WARNING]
> Unlike ECDH, KEM doesn't verify whether it was "Bob" who've sent the ciphertext.
> Instead of throwing an error when the ciphertext is encrypted by a different pubkey,
> `decapsulate` will simply return a different shared secret.
> ML-KEM is also probabilistic and relies on quality of CSPRNG.
### ML-DSA / Dilithium signatures

```ts
Expand All @@ -147,7 +147,7 @@ const isValid = ml_dsa65.verify(aliceKeys.publicKey, msg, sig);
```

Lattice-based digital signature algorithm, defined in [FIPS-204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf). See
[official site](https://www.pq-crystals.org/dilithium/index.shtml) and
[website](https://www.pq-crystals.org/dilithium/index.shtml) and
[repo](https://github.com/pq-crystals/dilithium).
The internals are similar to ML-KEM, but keys and params are different.

Expand All @@ -161,15 +161,16 @@ import {
slh_dsa_sha2_128f, slh_dsa_sha2_128s,
slh_dsa_sha2_192f, slh_dsa_sha2_192s,
slh_dsa_sha2_256f, slh_dsa_sha2_256s,
} from '@noble/ciphers/slh-dsa';
} from '@noble/post-quantum/slh-dsa';

const aliceKeys = sph.keygen();
const msg = new Uint8Array(1);
const sig = sph.sign(aliceKeys.secretKey, msg);
const isValid = sph.verify(aliceKeys.publicKey, msg, sig);
```

Hash-based digital signature algorithm, defined in [FIPS-205](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.205.pdf). See [official site](https://sphincs.org) and [repo](https://github.com/sphincs/sphincsplus).
Hash-based digital signature algorithm, defined in [FIPS-205](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.205.pdf).
See [website](https://sphincs.org) and [repo](https://github.com/sphincs/sphincsplus).
We implement spec v3.1 with FIPS adjustments. Some wasm libraries use older specs.

## Security
Expand Down
186 changes: 37 additions & 149 deletions benchmark/ml-dsa.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
import { deepStrictEqual } from 'node:assert';
import { compare, utils } from 'micro-bmark';
import {
dilithium_v30,
dilithium_v31,
ml_dsa44,
ml_dsa65,
ml_dsa87,
dilithium_v31_aes,
} from '../ml-dsa.js';

import * as asanrom from '@asanrom/dilithium';
import * as theqrl from '@theqrl/dilithium5';

// wasm
import { default as dashline2 } from '@dashlane/pqc-sign-dilithium2-node';
import { default as dashline3 } from '@dashlane/pqc-sign-dilithium3-node';
import { default as dashline5 } from '@dashlane/pqc-sign-dilithium5-node';
let dl2, dl3, dl5;
async function initDashline() {
dl2 = await dashline2();
dl3 = await dashline3();
dl5 = await dashline5();
}

const seed = new Uint8Array(32).fill(1);
const msg = new Uint8Array(32).fill(2);

Expand All @@ -38,150 +20,56 @@ const getNoble = (lib) => ({
verify: (opts) => deepStrictEqual(lib.verify(opts.publicKey, msg, opts.signature), true),
});

const getAsanrom = (n) => {
const level = asanrom.DilithiumLevel.get(n);
return {
keygen: () => asanrom.DilithiumKeyPair.generate(level),
sign: (opts) => {
deepStrictEqual(
asanrom.DilithiumPrivateKey.fromBytes(opts.secretKey, level).sign(msg).getBytes(),
opts.signature
);
},
verify: (opts) => {
deepStrictEqual(
asanrom.DilithiumPublicKey.fromBytes(opts.publicKey, level).verifySignature(
msg,
asanrom.DilithiumSignature.fromBytes(opts.signature, level)
),
true
);
},
};
};

const getDashline = (fn) => {
return {
keygen: async () => await fn().keypair(),
sign: async (opts) =>
deepStrictEqual((await fn().sign(msg, opts.secretKey)).signature, opts.signature),
verify: async (opts) =>
deepStrictEqual(await fn().verify(opts.signature, msg, opts.publicKey), true),
};
};

const DILITHIUM = {
dilithium_v30_2: {
opts: getOpts(dilithium_v30.dilithium2),
asanrom: getAsanrom(2),
noble: getNoble(dilithium_v30.dilithium2),
},
dilithium_v30_3: {
opts: getOpts(dilithium_v30.dilithium3),
asanrom: getAsanrom(3),
noble: getNoble(dilithium_v30.dilithium3),
},
dilithium_v30_5: {
opts: getOpts(dilithium_v30.dilithium5),
asanrom: getAsanrom(5),
noble: getNoble(dilithium_v30.dilithium5),
},
dilithium_v31_2: {
opts: getOpts(dilithium_v31.dilithium2),
dashline: getDashline(() => dl2),
noble: getNoble(dilithium_v31.dilithium2),
},
dilithium_v31_3: {
opts: getOpts(dilithium_v31.dilithium3),
dashline: getDashline(() => dl3),
noble: getNoble(dilithium_v31.dilithium3),
},
dilithium_v31_5: {
opts: getOpts(dilithium_v31.dilithium5),
dashline: getDashline(() => dl5),
theqrl: {
keygen: () => {
const pk = new Uint8Array(theqrl.CryptoPublicKeyBytes);
const sk = new Uint8Array(theqrl.CryptoSecretKeyBytes);
theqrl.cryptoSignKeypair(Buffer.from(seed), pk, sk);
},
sign: (opts) =>
deepStrictEqual(
theqrl
.cryptoSign(Buffer.from(msg), Buffer.from(opts.secretKey), false)
.subarray(0, -msg.length),
opts.signature
),
verify: (opts) =>
deepStrictEqual(
theqrl.cryptoSignVerify(
Buffer.from(opts.signature),
Buffer.from(msg),
Buffer.from(opts.publicKey)
),
true
),
},
noble: getNoble(dilithium_v31.dilithium5),
},
dilithium_v31_aes_2: {
opts: getOpts(dilithium_v31_aes.dilithium2),
noble: getNoble(dilithium_v31_aes.dilithium2),
},
dilithium_v31_aes_3: {
opts: getOpts(dilithium_v31_aes.dilithium3),
noble: getNoble(dilithium_v31_aes.dilithium3),
},
dilithium_v31_aes_5: {
opts: getOpts(dilithium_v31_aes.dilithium5),
noble: getNoble(dilithium_v31_aes.dilithium5),
},
'ML-DSA44': {
const MLDSA = {
'v44': {
opts: getOpts(ml_dsa44),
noble: getNoble(ml_dsa44),
},
'ML-DSA65': {
'v65': {
opts: getOpts(ml_dsa65),
noble: getNoble(ml_dsa65),
},
'ML-DSA87': {
'v87': {
opts: getOpts(ml_dsa87),
noble: getNoble(ml_dsa87),
},
};
const FNS = ['keygen', 'sign', 'verify'];

const SAMPLES = 100;
export async function main() {
const onlyNoble = process.argv[2] === 'noble';
if (onlyNoble) {
for (const fn of FNS) {
await compare(
`==== ${fn} ====`,
SAMPLES,
Object.fromEntries(
Object.entries(DILITHIUM).map(([k, v]) => [k, v.noble[fn].bind(null, v.opts)])
)
);
}
return;
}
await initDashline();

for (const [algoName, libraries] of Object.entries(DILITHIUM)) {
for (const fn of FNS) {
const opts = libraries.opts;
await compare(
`==== ${algoName}/${fn} ====`,
SAMPLES,
Object.fromEntries(
Object.entries(libraries)
.filter(([k, v]) => k !== 'opts')
.map(([k, v]) => [k, v[fn].bind(null, opts)])
)
);
}
}
await compare('keygen', 100, {
'ML-DSA44': () => {
MLDSA.v44.noble.keygen();
},
'ML-DSA65': () => {
MLDSA.v65.noble.keygen();
},
'ML-DSA87': () => {
MLDSA.v87.noble.keygen();
},
});
await compare('sign', 100, {
'ML-DSA44': () => {
MLDSA.v44.noble.sign(MLDSA.v44.opts);
},
'ML-DSA65': () => {
MLDSA.v65.noble.sign(MLDSA.v65.opts);
},
'ML-DSA87': () => {
MLDSA.v87.noble.sign(MLDSA.v87.opts);
},
});
await compare('verify', 100, {
'ML-DSA44': () => {
MLDSA.v44.noble.verify(MLDSA.v44.opts);
},
'ML-DSA65': () => {
MLDSA.v65.noble.verify(MLDSA.v65.opts);
},
'ML-DSA87': () => {
MLDSA.v87.noble.verify(MLDSA.v87.opts);
},
});
utils.logMem();
}

Expand Down
Loading

0 comments on commit f75f3ca

Please sign in to comment.