@noble/post-quantum 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +47 -32
  2. package/_crystals.d.ts +1 -1
  3. package/_crystals.d.ts.map +1 -1
  4. package/_crystals.js +31 -46
  5. package/_crystals.js.map +1 -1
  6. package/hybrid.d.ts +102 -0
  7. package/hybrid.d.ts.map +1 -0
  8. package/hybrid.js +283 -0
  9. package/hybrid.js.map +1 -0
  10. package/index.d.ts +1 -0
  11. package/index.js +4 -4
  12. package/index.js.map +1 -1
  13. package/ml-dsa.d.ts +16 -8
  14. package/ml-dsa.d.ts.map +1 -1
  15. package/ml-dsa.js +126 -68
  16. package/ml-dsa.js.map +1 -1
  17. package/ml-kem.d.ts +1 -14
  18. package/ml-kem.d.ts.map +1 -1
  19. package/ml-kem.js +70 -54
  20. package/ml-kem.js.map +1 -1
  21. package/package.json +39 -85
  22. package/slh-dsa.d.ts +4 -3
  23. package/slh-dsa.d.ts.map +1 -1
  24. package/slh-dsa.js +113 -86
  25. package/slh-dsa.js.map +1 -1
  26. package/src/_crystals.ts +30 -41
  27. package/src/hybrid.ts +372 -0
  28. package/src/index.ts +3 -3
  29. package/src/ml-dsa.ts +125 -39
  30. package/src/ml-kem.ts +49 -46
  31. package/src/slh-dsa.ts +90 -50
  32. package/src/utils.ts +85 -50
  33. package/utils.d.ts +52 -10
  34. package/utils.d.ts.map +1 -1
  35. package/utils.js +54 -60
  36. package/utils.js.map +1 -1
  37. package/esm/_crystals.d.ts +0 -34
  38. package/esm/_crystals.d.ts.map +0 -1
  39. package/esm/_crystals.js +0 -141
  40. package/esm/_crystals.js.map +0 -1
  41. package/esm/index.d.ts +0 -2
  42. package/esm/index.d.ts.map +0 -1
  43. package/esm/index.js +0 -21
  44. package/esm/index.js.map +0 -1
  45. package/esm/ml-dsa.d.ts +0 -25
  46. package/esm/ml-dsa.d.ts.map +0 -1
  47. package/esm/ml-dsa.js +0 -525
  48. package/esm/ml-dsa.js.map +0 -1
  49. package/esm/ml-kem.d.ts +0 -34
  50. package/esm/ml-kem.d.ts.map +0 -1
  51. package/esm/ml-kem.js +0 -306
  52. package/esm/ml-kem.js.map +0 -1
  53. package/esm/package.json +0 -10
  54. package/esm/slh-dsa.d.ts +0 -62
  55. package/esm/slh-dsa.d.ts.map +0 -1
  56. package/esm/slh-dsa.js +0 -596
  57. package/esm/slh-dsa.js.map +0 -1
  58. package/esm/utils.d.ts +0 -40
  59. package/esm/utils.d.ts.map +0 -1
  60. package/esm/utils.js +0 -133
  61. package/esm/utils.js.map +0 -1
  62. package/src/package.json +0 -3
package/src/hybrid.ts ADDED
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Post-Quantum Hybrid Cryptography
3
+ *
4
+ * The current implementation is flawed and likely redundant. We should offer
5
+ * a small, generic API to compose hybrid schemes instead of reimplementing
6
+ * protocol-specific logic (SSH, GPG, etc.) with ad hoc encodings.
7
+ *
8
+ * 1. Core Issues
9
+ * - sign/verify: implemented as two separate operations with different keys.
10
+ * - EC getSharedSecret: could be refactored into a proper KEM.
11
+ * - Multiple calls: keys, signatures, and shared secrets could be
12
+ * concatenated to reduce the number of API invocations.
13
+ * - Reinvention: most libraries add strange domain separations and
14
+ * encodings instead of simple byte concatenation.
15
+ *
16
+ * 2. API Goals
17
+ * - Provide primitives to build hybrids generically.
18
+ * - Avoid embedding SSH- or GPG-specific formats in the core API.
19
+ *
20
+ * 3. Edge Cases
21
+ * • Variable-length signatures:
22
+ * - DER-encoded (Weierstrass curves).
23
+ * - Falcon (unpadded).
24
+ * - Concatenation works only if length is fixed; otherwise a length
25
+ * prefix is required (but that breaks compatibility).
26
+ *
27
+ * • getSharedSecret:
28
+ * - Default: non-KEM (authenticated ECDH).
29
+ * - KEM conversion: generate a random SK to remove implicit auth.
30
+ *
31
+ * 4. Common Pitfalls
32
+ * - Seed expansion:
33
+ * • Expanding a small seed into multiple keys reduces entropy.
34
+ * • API should allow identity mapping (no expansion).
35
+ *
36
+ * - Skipping full point encoding:
37
+ * • Some omit the compression byte (parity) for WebCrypto compatibility.
38
+ * • Better: hash the raw secret; coordinate output is already non-uniform.
39
+ * • Some curves (e.g., X448) produce secrets that must be re-hashed to match
40
+ * symmetric-key lengths.
41
+ *
42
+ * - Combiner inconsistencies:
43
+ * • Different domain separations and encodings across libraries.
44
+ * • Should live at the application layer, since key lengths vary.
45
+ *
46
+ * 5. Protocol Examples
47
+ * - SSH:
48
+ * • Concatenate keys.
49
+ * • Combiner: SHA-512.
50
+ *
51
+ * - GPG:
52
+ * • Concatenate keys.
53
+ * • Combiner: SHA3-256(kemShare || ecdhShare || ciphertext || pubKey || algId || domSep || len(domSep))
54
+ *
55
+ * - TLS:
56
+ * • Transcript-based derivation (HKDF).
57
+ *
58
+ * 6. Relevant Specs & Implementations
59
+ * - IETF Hybrid KEM drafts:
60
+ * • draft-irtf-cfrg-hybrid-kems
61
+ * • draft-connolly-cfrg-xwing-kem
62
+ * • draft-westerbaan-tls-xyber768d00
63
+ *
64
+ * - PQC Libraries:
65
+ * • superdilithium (cyph/pqcrypto.js) – low adoption.
66
+ * • hybrid-pqc (DogeProtocol, quantumcoinproject) – complex encodings.
67
+ *
68
+ * 7. Signatures
69
+ * - Ed25519: fixed-size, easy to support.
70
+ * - Variable-size: introduces custom format requirements; best left to
71
+ * higher-level code.
72
+ *
73
+ * @module
74
+ */
75
+ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
76
+ import { type EdDSA } from '@noble/curves/abstract/edwards.js';
77
+ import { type MontgomeryECDH } from '@noble/curves/abstract/montgomery.js';
78
+ import { type ECDSA } from '@noble/curves/abstract/weierstrass.js';
79
+ import { x25519 } from '@noble/curves/ed25519.js';
80
+ import { p256, p384 } from '@noble/curves/nist.js';
81
+ import {
82
+ asciiToBytes,
83
+ bytesToNumberBE,
84
+ bytesToNumberLE,
85
+ concatBytes,
86
+ numberToBytesBE,
87
+ } from '@noble/curves/utils.js';
88
+ import { expand, extract } from '@noble/hashes/hkdf.js';
89
+ import { sha256 } from '@noble/hashes/sha2.js';
90
+ import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
91
+ import { abytes, ahash, anumber, type CHash, type CHashXOF } from '@noble/hashes/utils.js';
92
+ import { ml_kem1024, ml_kem768 } from './ml-kem.ts';
93
+ import {
94
+ cleanBytes,
95
+ randomBytes,
96
+ splitCoder,
97
+ type CryptoKeys,
98
+ type KEM,
99
+ type Signer,
100
+ } from './utils.ts';
101
+
102
+ type CurveAll = ECDSA | EdDSA | MontgomeryECDH;
103
+ type CurveECDH = ECDSA | MontgomeryECDH;
104
+ type CurveSign = ECDSA | EdDSA;
105
+
106
+ // Can re-use if decide to signatures support, on other hand getSecretKey is specific and ugly
107
+ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
108
+ const lengths = curve.lengths;
109
+ let keygen = curve.keygen;
110
+ if (allowZeroKey) {
111
+ // This is ugly, but we need to return exact results here.
112
+ const wCurve = curve as typeof p256;
113
+ const Fn = wCurve.Point.Fn;
114
+ if (!Fn) throw new Error('No Point.Fn');
115
+ keygen = (seed: Uint8Array = randomBytes(lengths.seed)) => {
116
+ abytes(seed, lengths.seed!);
117
+ const seedScalar = Fn.isLE ? bytesToNumberLE(seed) : bytesToNumberBE(seed);
118
+ const secretKey = Fn.toBytes(Fn.create(seedScalar)); // Fixes modulo bias, but not zero
119
+ return { secretKey, publicKey: curve.getPublicKey(secretKey) };
120
+ };
121
+ }
122
+ return {
123
+ lengths: { secretKey: lengths.secretKey, publicKey: lengths.publicKey, seed: lengths.seed },
124
+ keygen,
125
+ getPublicKey: (secretKey: Uint8Array) => curve.getPublicKey(secretKey),
126
+ };
127
+ }
128
+
129
+ export const ecdhKem = (curve: CurveECDH, allowZeroKey: boolean = false): KEM => {
130
+ const kg = ecKeygen(curve, allowZeroKey);
131
+ if (!curve.getSharedSecret) throw new Error('wrong curve'); // ed25519 doesn't have one!
132
+ return {
133
+ lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
134
+ keygen: kg.keygen,
135
+ getPublicKey: kg.getPublicKey,
136
+ encapsulate(publicKey: Uint8Array, rand: Uint8Array = randomBytes(curve.lengths.secretKey)) {
137
+ const ek = this.keygen(rand).secretKey;
138
+ const sharedSecret = this.decapsulate(publicKey, ek);
139
+ const cipherText = curve.getPublicKey(ek);
140
+ cleanBytes(ek);
141
+ return { sharedSecret, cipherText };
142
+ },
143
+ decapsulate(cipherText: Uint8Array, secretKey: Uint8Array) {
144
+ const res = curve.getSharedSecret(secretKey, cipherText);
145
+ return curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res;
146
+ },
147
+ };
148
+ };
149
+
150
+ export const ecSigner = (curve: CurveSign, allowZeroKey: boolean = false): Signer => {
151
+ const kg = ecKeygen(curve, allowZeroKey);
152
+ if (!curve.sign || !curve.verify) throw new Error('wrong curve'); // ed25519 doesn't have one!
153
+ return {
154
+ lengths: { ...kg.lengths, signature: curve.lengths.signature, signRand: 0 },
155
+ keygen: kg.keygen,
156
+ getPublicKey: kg.getPublicKey,
157
+ sign: (message, secretKey) => curve.sign(message, secretKey),
158
+ verify: (signature, message, publicKey) => curve.verify(signature, message, publicKey),
159
+ };
160
+ };
161
+
162
+ function splitLengths<K extends string, T extends { lengths: Partial<Record<K, number>> }>(
163
+ lst: T[],
164
+ name: K
165
+ ) {
166
+ return splitCoder(
167
+ ...lst.map((i) => {
168
+ if (typeof i.lengths[name] !== 'number') throw new Error('wrong length: ' + name);
169
+ return i.lengths[name];
170
+ })
171
+ );
172
+ }
173
+
174
+ export type ExpandSeed = (seed: Uint8Array, len: number) => Uint8Array;
175
+ type XOF = CHashXOF<any, { dkLen: number }>;
176
+
177
+ // It is XOF for most cases, but can be more complex!
178
+ export function expandSeedXof(xof: XOF): ExpandSeed {
179
+ return (seed: Uint8Array, seedLen: number) => xof(seed, { dkLen: seedLen });
180
+ }
181
+
182
+ export type Combiner = (
183
+ publicKeys: Uint8Array[],
184
+ cipherTexts: Uint8Array[],
185
+ sharedSecrets: Uint8Array[]
186
+ ) => Uint8Array;
187
+
188
+ function combineKeys(
189
+ realSeedLen: number | undefined, // how much bytes expandSeed expects
190
+ expandSeed: ExpandSeed,
191
+ ...ck: CryptoKeys[]
192
+ ) {
193
+ const seedCoder = splitLengths(ck, 'seed');
194
+ const pkCoder = splitLengths(ck, 'publicKey');
195
+ // Allows to use identity functions for combiner/expandSeed
196
+ if (realSeedLen === undefined) realSeedLen = seedCoder.bytesLen;
197
+ anumber(realSeedLen);
198
+ function expandDecapsulationKey(seed: Uint8Array) {
199
+ abytes(seed, realSeedLen!);
200
+ const expanded = seedCoder.decode(expandSeed(seed, seedCoder.bytesLen));
201
+ const keys = ck.map((i, j) => i.keygen(expanded[j]));
202
+ const secretKey = keys.map((i) => i.secretKey);
203
+ const publicKey = keys.map((i) => i.publicKey);
204
+ return { secretKey, publicKey };
205
+ }
206
+ return {
207
+ info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
208
+ getPublicKey(secretKey: Uint8Array) {
209
+ return this.keygen(secretKey).publicKey;
210
+ },
211
+ keygen(seed: Uint8Array = randomBytes(realSeedLen)) {
212
+ const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
213
+ const publicKey = pkCoder.encode(pk);
214
+ cleanBytes(pk);
215
+ cleanBytes(secretKey);
216
+ return { secretKey: seed, publicKey };
217
+ },
218
+ expandDecapsulationKey,
219
+ realSeedLen,
220
+ };
221
+ }
222
+
223
+ // This generic function that combines multiple KEMs into single one
224
+ export function combineKEMS(
225
+ realSeedLen: number | undefined, // how much bytes expandSeed expects
226
+ realMsgLen: number | undefined, // how much bytes combiner returns
227
+ expandSeed: ExpandSeed,
228
+ combiner: Combiner,
229
+ ...kems: KEM[]
230
+ ): KEM {
231
+ const keys = combineKeys(realSeedLen, expandSeed, ...kems);
232
+ const ctCoder = splitLengths(kems, 'cipherText');
233
+ const pkCoder = splitLengths(kems, 'publicKey');
234
+ const msgCoder = splitLengths(kems, 'msg');
235
+ if (realMsgLen === undefined) realMsgLen = msgCoder.bytesLen;
236
+ anumber(realMsgLen);
237
+ return {
238
+ lengths: {
239
+ ...keys.info.lengths,
240
+ msg: realMsgLen,
241
+ msgRand: msgCoder.bytesLen,
242
+ cipherText: ctCoder.bytesLen,
243
+ },
244
+ getPublicKey: keys.getPublicKey,
245
+ keygen: keys.keygen,
246
+ encapsulate(pk: Uint8Array, randomness: Uint8Array = randomBytes(msgCoder.bytesLen)) {
247
+ const pks = pkCoder.decode(pk);
248
+ const rand = msgCoder.decode(randomness);
249
+ const enc = kems.map((i, j) => i.encapsulate(pks[j], rand[j]));
250
+ const sharedSecret = enc.map((i) => i.sharedSecret);
251
+ const cipherText = enc.map((i) => i.cipherText);
252
+ const res = {
253
+ sharedSecret: combiner(pks, cipherText, sharedSecret),
254
+ cipherText: ctCoder.encode(cipherText),
255
+ };
256
+ cleanBytes(sharedSecret, cipherText, pks);
257
+ return res;
258
+ },
259
+ decapsulate(ct: Uint8Array, seed: Uint8Array) {
260
+ const cts = ctCoder.decode(ct);
261
+ const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
262
+ const sharedSecret = kems.map((i, j) => i.decapsulate(cts[j], secretKey[j]));
263
+ return combiner(publicKey, cts, sharedSecret);
264
+ },
265
+ };
266
+ }
267
+ // There is no specs for this, but can be useful
268
+ // realSeedLen: how much bytes expandSeed expects.
269
+ export function combineSigners(
270
+ realSeedLen: number | undefined,
271
+ expandSeed: ExpandSeed,
272
+ ...signers: Signer[]
273
+ ): Signer {
274
+ const keys = combineKeys(realSeedLen, expandSeed, ...signers);
275
+ const sigCoder = splitLengths(signers, 'signature');
276
+ const pkCoder = splitLengths(signers, 'publicKey');
277
+ return {
278
+ lengths: { ...keys.info.lengths, signature: sigCoder.bytesLen, signRand: 0 },
279
+ getPublicKey: keys.getPublicKey,
280
+ keygen: keys.keygen,
281
+ sign(message, seed) {
282
+ const { secretKey } = keys.expandDecapsulationKey(seed);
283
+ // NOTE: we probably can make different hashes for different algorithms
284
+ // same way as we do for kem, but not sure if this a good idea.
285
+ const sigs = signers.map((i, j) => i.sign(message, secretKey[j]));
286
+ return sigCoder.encode(sigs);
287
+ },
288
+ verify: (signature, message, publicKey) => {
289
+ const pks = pkCoder.decode(publicKey);
290
+ const sigs = sigCoder.decode(signature);
291
+ for (let i = 0; i < signers.length; i++) {
292
+ if (!signers[i].verify(sigs[i], message, pks[i])) return false;
293
+ }
294
+ return true;
295
+ },
296
+ };
297
+ }
298
+
299
+ export function QSF(label: string, pqc: KEM, curveKEM: KEM, xof: XOF, kdf: CHash): KEM {
300
+ ahash(xof);
301
+ ahash(kdf);
302
+ return combineKEMS(
303
+ 32,
304
+ 32,
305
+ expandSeedXof(xof),
306
+ (pk, ct, ss) => kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
307
+ pqc,
308
+ curveKEM
309
+ );
310
+ }
311
+
312
+ export const QSFMLKEM768P256: KEM = QSF(
313
+ 'QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)',
314
+ ml_kem768,
315
+ ecdhKem(p256, true),
316
+ shake256,
317
+ sha3_256
318
+ );
319
+
320
+ export const QSFMLKEM1024P384: KEM = QSF(
321
+ 'QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)',
322
+ ml_kem1024,
323
+ ecdhKem(p384, true),
324
+ shake256,
325
+ sha3_256
326
+ );
327
+
328
+ export function KitchenSink(label: string, pqc: KEM, curveKEM: KEM, xof: XOF, hash: CHash): KEM {
329
+ ahash(xof);
330
+ ahash(hash);
331
+ return combineKEMS(
332
+ 32,
333
+ 32,
334
+ expandSeedXof(xof),
335
+ (pk, ct, ss) => {
336
+ const preimage = concatBytes(ss[0], ss[1], ct[0], pk[0], ct[1], pk[1], asciiToBytes(label));
337
+ const len = 32;
338
+ const ikm = concatBytes(asciiToBytes('hybrid_prk'), preimage);
339
+ const prk = extract(hash, ikm);
340
+ const info = concatBytes(
341
+ numberToBytesBE(len, 2),
342
+ asciiToBytes('shared_secret'),
343
+ asciiToBytes('')
344
+ );
345
+ const res = expand(hash, prk, info, len);
346
+ cleanBytes(prk, info, ikm, preimage);
347
+ return res;
348
+ },
349
+ pqc,
350
+ curveKEM
351
+ );
352
+ }
353
+
354
+ const x25519kem = ecdhKem(x25519);
355
+ export const KitchenSinkMLKEM768X25519: KEM = KitchenSink(
356
+ 'KitchenSink-KEM(ML-KEM-768,X25519)-XOF(SHAKE256)-KDF(HKDF-SHA-256)',
357
+ ml_kem768,
358
+ x25519kem,
359
+ shake256,
360
+ sha256
361
+ );
362
+
363
+ // Always X25519 and ML-KEM - 768, no point to export
364
+ export const XWing: KEM = combineKEMS(
365
+ 32,
366
+ 32,
367
+ expandSeedXof(shake256),
368
+ // Awesome label, so much escaping hell in a single line.
369
+ (pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes('\\.//^\\'))),
370
+ ml_kem768,
371
+ x25519kem
372
+ );
package/src/index.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * @module
5
5
  * @example
6
6
  ```js
7
- import { ml_kem512, ml_kem768, ml_kem1024 } from '@noble/post-quantum/ml-kem';
8
- import { ml_dsa44, ml_dsa65, ml_dsa87 } from '@noble/post-quantum/ml-dsa';
7
+ import { ml_kem512, ml_kem768, ml_kem1024 } from '@noble/post-quantum/ml-kem.js';
8
+ import { ml_dsa44, ml_dsa65, ml_dsa87 } from '@noble/post-quantum/ml-dsa.js';
9
9
  import {
10
10
  slh_dsa_sha2_128f, slh_dsa_sha2_128s,
11
11
  slh_dsa_sha2_192f, slh_dsa_sha2_192s,
@@ -13,7 +13,7 @@ import {
13
13
  slh_dsa_shake_128f, slh_dsa_shake_128s,
14
14
  slh_dsa_shake_192f, slh_dsa_shake_192s,
15
15
  slh_dsa_shake_256f, slh_dsa_shake_256s,
16
- } from '@noble/post-quantum/slh-dsa';
16
+ } from '@noble/post-quantum/slh-dsa.js';
17
17
  ```
18
18
  */
19
19
  throw new Error('root module cannot be imported: import submodules instead. Check out README');
package/src/ml-dsa.ts CHANGED
@@ -8,21 +8,48 @@
8
8
  * @module
9
9
  */
10
10
  /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
11
- import { shake256 } from '@noble/hashes/sha3';
11
+ import { shake256 } from '@noble/hashes/sha3.js';
12
+ import type { CHash } from '@noble/hashes/utils.js';
12
13
  import { genCrystals, type XOF, XOF128, XOF256 } from './_crystals.ts';
13
14
  import {
15
+ abytes,
14
16
  type BytesCoderLen,
17
+ checkHash,
18
+ validateSigOpts,
19
+ validateVerOpts,
15
20
  cleanBytes,
16
- EMPTY,
17
- ensureBytes,
21
+ type CryptoKeys,
18
22
  equalBytes,
19
23
  getMessage,
20
24
  getMessagePrehash,
21
25
  randomBytes,
22
26
  type Signer,
27
+ type SigOpts,
23
28
  splitCoder,
24
29
  vecCoder,
30
+ type VerOpts,
31
+ validateOpts,
25
32
  } from './utils.ts';
33
+ import { abool } from '@noble/curves/utils.js';
34
+
35
+ export type DSAInternalOpts = { externalMu?: boolean };
36
+ function validateInternalOpts(opts: DSAInternalOpts) {
37
+ validateOpts(opts);
38
+ if (opts.externalMu !== undefined) abool(opts.externalMu, 'opts.externalMu');
39
+ }
40
+
41
+ /** Signer API, containing internal methods */
42
+ export type DSAInternal = CryptoKeys & {
43
+ lengths: Signer['lengths'];
44
+ sign: (msg: Uint8Array, secretKey: Uint8Array, opts?: SigOpts & DSAInternalOpts) => Uint8Array;
45
+ verify: (
46
+ sig: Uint8Array,
47
+ msg: Uint8Array,
48
+ pubKey: Uint8Array,
49
+ opts?: VerOpts & DSAInternalOpts
50
+ ) => boolean;
51
+ };
52
+ export type DSA = Signer & { internal: DSAInternal };
26
53
 
27
54
  // Constants
28
55
  const N = 256;
@@ -59,7 +86,7 @@ export const PARAMS: Record<string, DSAParam> = {
59
86
 
60
87
  // NOTE: there is a lot cases where negative numbers used (with smod instead of mod).
61
88
  type Poly = Int32Array;
62
- const newPoly = (n: number) => new Int32Array(n);
89
+ const newPoly = (n: number): Int32Array => new Int32Array(n);
63
90
 
64
91
  const { mod, smod, NTT, bitsCoder } = genCrystals({
65
92
  N,
@@ -139,11 +166,12 @@ type DilithiumOpts = {
139
166
  TR_BYTES: number;
140
167
  XOF128: XOF;
141
168
  XOF256: XOF;
169
+ securityLevel: number;
142
170
  };
143
171
 
144
172
  function getDilithium(opts: DilithiumOpts) {
145
173
  const { K, L, GAMMA1, GAMMA2, TAU, ETA, OMEGA } = opts;
146
- const { CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128, XOF256 } = opts;
174
+ const { CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128, XOF256, securityLevel } = opts;
147
175
 
148
176
  if (![2, 4].includes(ETA)) throw new Error('Wrong ETA');
149
177
  if (![1 << 17, 1 << 19].includes(GAMMA1)) throw new Error('Wrong GAMMA1');
@@ -324,16 +352,23 @@ function getDilithium(opts: DilithiumOpts) {
324
352
  const signRandBytes = 32;
325
353
  const seedCoder = splitCoder(32, 64, 32);
326
354
  // API & argument positions are exactly as in FIPS204.
327
- const internal: Signer = {
328
- signRandBytes,
355
+ const internal: DSAInternal = {
356
+ info: { type: 'internal-ml-dsa' },
357
+ lengths: {
358
+ secretKey: secretCoder.bytesLen,
359
+ publicKey: publicCoder.bytesLen,
360
+ seed: 32,
361
+ signature: sigCoder.bytesLen,
362
+ signRand: signRandBytes,
363
+ },
329
364
  keygen: (seed?: Uint8Array) => {
330
365
  // H(𝜉||IntegerToBytes(𝑘, 1)||IntegerToBytes(ℓ, 1), 128) 2: ▷ expand seed
331
366
  const seedDst = new Uint8Array(32 + 2);
332
367
  const randSeed = seed === undefined;
333
368
  if (randSeed) seed = randomBytes(32);
334
- ensureBytes(seed, 32);
369
+ abytes(seed!, 32);
335
370
  seedDst.set(seed!);
336
- if (randSeed) seed!.fill(0);
371
+ if (randSeed) cleanBytes(seed!);
337
372
  seedDst[32] = K;
338
373
  seedDst[33] = L;
339
374
  const [rho, rhoPrime, K_] = seedCoder.decode(
@@ -352,7 +387,7 @@ function getDilithium(opts: DilithiumOpts) {
352
387
  const t = newPoly(N);
353
388
  for (let i = 0; i < K; i++) {
354
389
  // t ← NTT−1(A*NTT(s1)) + s2
355
- t.fill(0); // don't-reallocate
390
+ cleanBytes(t); // don't-reallocate
356
391
  for (let j = 0; j < L; j++) {
357
392
  const aij = RejNTTPoly(xof.get(j, i)); // super slow!
358
393
  polyAdd(t, MultiplyNTTs(aij, s1Hat[j]));
@@ -373,8 +408,32 @@ function getDilithium(opts: DilithiumOpts) {
373
408
  cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr, seedDst);
374
409
  return { publicKey, secretKey };
375
410
  },
411
+ getPublicKey: (secretKey: Uint8Array) => {
412
+ const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
413
+ const xof = XOF128(rho);
414
+ const s1Hat = s1.map((p) => NTT.encode(p.slice()));
415
+ const t1: Poly[] = [];
416
+ const tmp = newPoly(N);
417
+ for (let i = 0; i < K; i++) {
418
+ tmp.fill(0);
419
+ for (let j = 0; j < L; j++) {
420
+ const aij = RejNTTPoly(xof.get(j, i)); // A_ij in NTT
421
+ polyAdd(tmp, MultiplyNTTs(aij, s1Hat[j])); // += A_ij * s1_j
422
+ }
423
+ NTT.decode(tmp); // NTT⁻¹
424
+ polyAdd(tmp, s2[i]); // t_i = A·s1 + s2
425
+ const { r1 } = polyPowerRound(tmp); // r1 = t1, r0 ≈ t0
426
+ t1.push(r1);
427
+ }
428
+ xof.clean();
429
+ cleanBytes(tmp, s1Hat, _t0, s1, s2);
430
+ return publicCoder.encode([rho, t1]);
431
+ },
376
432
  // NOTE: random is optional.
377
- sign: (secretKey: Uint8Array, msg: Uint8Array, random?: Uint8Array, externalMu = false) => {
433
+ sign: (msg: Uint8Array, secretKey: Uint8Array, opts: SigOpts & DSAInternalOpts = {}) => {
434
+ validateSigOpts(opts);
435
+ validateInternalOpts(opts);
436
+ let { extraEntropy: random, externalMu = false } = opts;
378
437
  // This part can be pre-cached per secretKey, but there is only minor performance improvement,
379
438
  // since we re-use a lot of variables to computation.
380
439
  const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
@@ -398,8 +457,13 @@ function getDilithium(opts: DilithiumOpts) {
398
457
  : shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 6: µ ← H(tr||M, 512) ▷ Compute message representative µ
399
458
 
400
459
  // Compute private random seed
401
- const rnd = random ? random : new Uint8Array(32);
402
- ensureBytes(rnd);
460
+ const rnd =
461
+ random === false
462
+ ? new Uint8Array(32)
463
+ : random === undefined
464
+ ? randomBytes(signRandBytes)
465
+ : random;
466
+ abytes(rnd, 32);
403
467
  const rhoprime = shake256
404
468
  .create({ dkLen: CRH_BYTES })
405
469
  .update(_K)
@@ -407,7 +471,7 @@ function getDilithium(opts: DilithiumOpts) {
407
471
  .update(mu)
408
472
  .digest(); // ρ′← H(K||rnd||µ, 512)
409
473
 
410
- ensureBytes(rhoprime, CRH_BYTES);
474
+ abytes(rhoprime, CRH_BYTES);
411
475
  const x256 = XOF256(rhoprime, ZCoder.bytesLen);
412
476
  // Rejection sampling loop
413
477
  main_loop: for (let kappa = 0; ; ) {
@@ -464,7 +528,14 @@ function getDilithium(opts: DilithiumOpts) {
464
528
  // @ts-ignore
465
529
  throw new Error('Unreachable code path reached, report this error');
466
530
  },
467
- verify: (publicKey: Uint8Array, msg: Uint8Array, sig: Uint8Array, externalMu = false) => {
531
+ verify: (
532
+ sig: Uint8Array,
533
+ msg: Uint8Array,
534
+ publicKey: Uint8Array,
535
+ opts: DSAInternalOpts = {}
536
+ ) => {
537
+ validateInternalOpts(opts);
538
+ const { externalMu = false } = opts;
468
539
  // ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M.
469
540
  const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk)
470
541
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
@@ -512,61 +583,76 @@ function getDilithium(opts: DilithiumOpts) {
512
583
  },
513
584
  };
514
585
  return {
586
+ info: { type: 'ml-dsa' },
515
587
  internal,
588
+ securityLevel: securityLevel,
516
589
  keygen: internal.keygen,
517
- signRandBytes: internal.signRandBytes,
518
- sign: (secretKey: Uint8Array, msg: Uint8Array, ctx = EMPTY, random?: Uint8Array) => {
519
- const M = getMessage(msg, ctx);
520
- const res = internal.sign(secretKey, M, random);
521
- M.fill(0);
590
+ lengths: internal.lengths,
591
+ getPublicKey: internal.getPublicKey,
592
+ sign: (msg: Uint8Array, secretKey: Uint8Array, opts: SigOpts = {}) => {
593
+ validateSigOpts(opts);
594
+ const M = getMessage(msg, opts.context);
595
+ const res = internal.sign(M, secretKey, opts);
596
+ cleanBytes(M);
522
597
  return res;
523
598
  },
524
- verify: (publicKey: Uint8Array, msg: Uint8Array, sig: Uint8Array, ctx = EMPTY) => {
525
- return internal.verify(publicKey, getMessage(msg, ctx), sig);
599
+ verify: (sig: Uint8Array, msg: Uint8Array, publicKey: Uint8Array, opts: VerOpts = {}) => {
600
+ validateVerOpts(opts);
601
+ return internal.verify(sig, getMessage(msg, opts.context), publicKey);
602
+ },
603
+ prehash: (hash: CHash) => {
604
+ checkHash(hash, securityLevel);
605
+ return {
606
+ info: { type: 'hashml-dsa' },
607
+ securityLevel: securityLevel,
608
+ lengths: internal.lengths,
609
+ keygen: internal.keygen,
610
+ getPublicKey: internal.getPublicKey,
611
+ sign: (msg: Uint8Array, secretKey: Uint8Array, opts: SigOpts = {}) => {
612
+ validateSigOpts(opts);
613
+ const M = getMessagePrehash(hash, msg, opts.context);
614
+ const res = internal.sign(M, secretKey, opts);
615
+ cleanBytes(M);
616
+ return res;
617
+ },
618
+ verify: (sig: Uint8Array, msg: Uint8Array, publicKey: Uint8Array, opts: VerOpts = {}) => {
619
+ validateVerOpts(opts);
620
+ return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey);
621
+ },
622
+ };
526
623
  },
527
- prehash: (hashName: string) => ({
528
- sign: (secretKey: Uint8Array, msg: Uint8Array, ctx = EMPTY, random?: Uint8Array) => {
529
- const M = getMessagePrehash(hashName, msg, ctx);
530
- const res = internal.sign(secretKey, M, random);
531
- M.fill(0);
532
- return res;
533
- },
534
- verify: (publicKey: Uint8Array, msg: Uint8Array, sig: Uint8Array, ctx = EMPTY) => {
535
- return internal.verify(publicKey, getMessagePrehash(hashName, msg, ctx), sig);
536
- },
537
- }),
538
624
  };
539
625
  }
540
626
 
541
- /** Signer API, containing internal methods */
542
- export type SignerWithInternal = Signer & { internal: Signer };
543
-
544
627
  /** ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD. */
545
- export const ml_dsa44: SignerWithInternal = /* @__PURE__ */ getDilithium({
628
+ export const ml_dsa44: DSA = /* @__PURE__ */ getDilithium({
546
629
  ...PARAMS[2],
547
630
  CRH_BYTES: 64,
548
631
  TR_BYTES: 64,
549
632
  C_TILDE_BYTES: 32,
550
633
  XOF128,
551
634
  XOF256,
635
+ securityLevel: 128,
552
636
  });
553
637
 
554
638
  /** ML-DSA-65 for 192-bit security level. Not recommended after 2030, as per ASD. */
555
- export const ml_dsa65: SignerWithInternal = /* @__PURE__ */ getDilithium({
639
+ export const ml_dsa65: DSA = /* @__PURE__ */ getDilithium({
556
640
  ...PARAMS[3],
557
641
  CRH_BYTES: 64,
558
642
  TR_BYTES: 64,
559
643
  C_TILDE_BYTES: 48,
560
644
  XOF128,
561
645
  XOF256,
646
+ securityLevel: 192,
562
647
  });
563
648
 
564
649
  /** ML-DSA-87 for 256-bit security level. OK after 2030, as per ASD. */
565
- export const ml_dsa87: SignerWithInternal = /* @__PURE__ */ getDilithium({
650
+ export const ml_dsa87: DSA = /* @__PURE__ */ getDilithium({
566
651
  ...PARAMS[5],
567
652
  CRH_BYTES: 64,
568
653
  TR_BYTES: 64,
569
654
  C_TILDE_BYTES: 64,
570
655
  XOF128,
571
656
  XOF256,
657
+ securityLevel: 256,
572
658
  });