@noble/post-quantum 0.4.1 → 0.5.1
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.
- package/README.md +53 -33
- package/_crystals.d.ts +1 -1
- package/_crystals.d.ts.map +1 -1
- package/_crystals.js +31 -46
- package/_crystals.js.map +1 -1
- package/hybrid.d.ts +102 -0
- package/hybrid.d.ts.map +1 -0
- package/hybrid.js +283 -0
- package/hybrid.js.map +1 -0
- package/index.d.ts +1 -0
- package/index.js +4 -4
- package/index.js.map +1 -1
- package/ml-dsa.d.ts +16 -8
- package/ml-dsa.d.ts.map +1 -1
- package/ml-dsa.js +126 -68
- package/ml-dsa.js.map +1 -1
- package/ml-kem.d.ts +1 -14
- package/ml-kem.d.ts.map +1 -1
- package/ml-kem.js +70 -54
- package/ml-kem.js.map +1 -1
- package/package.json +38 -85
- package/slh-dsa.d.ts +4 -3
- package/slh-dsa.d.ts.map +1 -1
- package/slh-dsa.js +113 -86
- package/slh-dsa.js.map +1 -1
- package/src/_crystals.ts +30 -41
- package/src/hybrid.ts +373 -0
- package/src/index.ts +3 -3
- package/src/ml-dsa.ts +129 -42
- package/src/ml-kem.ts +52 -49
- package/src/slh-dsa.ts +97 -56
- package/src/utils.ts +86 -50
- package/utils.d.ts +53 -11
- package/utils.d.ts.map +1 -1
- package/utils.js +54 -60
- package/utils.js.map +1 -1
- package/esm/_crystals.d.ts +0 -34
- package/esm/_crystals.d.ts.map +0 -1
- package/esm/_crystals.js +0 -141
- package/esm/_crystals.js.map +0 -1
- package/esm/index.d.ts +0 -2
- package/esm/index.d.ts.map +0 -1
- package/esm/index.js +0 -21
- package/esm/index.js.map +0 -1
- package/esm/ml-dsa.d.ts +0 -25
- package/esm/ml-dsa.d.ts.map +0 -1
- package/esm/ml-dsa.js +0 -525
- package/esm/ml-dsa.js.map +0 -1
- package/esm/ml-kem.d.ts +0 -34
- package/esm/ml-kem.d.ts.map +0 -1
- package/esm/ml-kem.js +0 -306
- package/esm/ml-kem.js.map +0 -1
- package/esm/package.json +0 -10
- package/esm/slh-dsa.d.ts +0 -62
- package/esm/slh-dsa.d.ts.map +0 -1
- package/esm/slh-dsa.js +0 -596
- package/esm/slh-dsa.js.map +0 -1
- package/esm/utils.d.ts +0 -40
- package/esm/utils.d.ts.map +0 -1
- package/esm/utils.js +0 -133
- package/esm/utils.js.map +0 -1
- package/src/package.json +0 -3
package/src/hybrid.ts
ADDED
@@ -0,0 +1,373 @@
|
|
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!, '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
|
+
name,
|
168
|
+
...lst.map((i) => {
|
169
|
+
if (typeof i.lengths[name] !== 'number') throw new Error('wrong length: ' + name);
|
170
|
+
return i.lengths[name];
|
171
|
+
})
|
172
|
+
);
|
173
|
+
}
|
174
|
+
|
175
|
+
export type ExpandSeed = (seed: Uint8Array, len: number) => Uint8Array;
|
176
|
+
type XOF = CHashXOF<any, { dkLen: number }>;
|
177
|
+
|
178
|
+
// It is XOF for most cases, but can be more complex!
|
179
|
+
export function expandSeedXof(xof: XOF): ExpandSeed {
|
180
|
+
return (seed: Uint8Array, seedLen: number) => xof(seed, { dkLen: seedLen });
|
181
|
+
}
|
182
|
+
|
183
|
+
export type Combiner = (
|
184
|
+
publicKeys: Uint8Array[],
|
185
|
+
cipherTexts: Uint8Array[],
|
186
|
+
sharedSecrets: Uint8Array[]
|
187
|
+
) => Uint8Array;
|
188
|
+
|
189
|
+
function combineKeys(
|
190
|
+
realSeedLen: number | undefined, // how much bytes expandSeed expects
|
191
|
+
expandSeed: ExpandSeed,
|
192
|
+
...ck: CryptoKeys[]
|
193
|
+
) {
|
194
|
+
const seedCoder = splitLengths(ck, 'seed');
|
195
|
+
const pkCoder = splitLengths(ck, 'publicKey');
|
196
|
+
// Allows to use identity functions for combiner/expandSeed
|
197
|
+
if (realSeedLen === undefined) realSeedLen = seedCoder.bytesLen;
|
198
|
+
anumber(realSeedLen);
|
199
|
+
function expandDecapsulationKey(seed: Uint8Array) {
|
200
|
+
abytes(seed, realSeedLen!);
|
201
|
+
const expanded = seedCoder.decode(expandSeed(seed, seedCoder.bytesLen));
|
202
|
+
const keys = ck.map((i, j) => i.keygen(expanded[j]));
|
203
|
+
const secretKey = keys.map((i) => i.secretKey);
|
204
|
+
const publicKey = keys.map((i) => i.publicKey);
|
205
|
+
return { secretKey, publicKey };
|
206
|
+
}
|
207
|
+
return {
|
208
|
+
info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
|
209
|
+
getPublicKey(secretKey: Uint8Array) {
|
210
|
+
return this.keygen(secretKey).publicKey;
|
211
|
+
},
|
212
|
+
keygen(seed: Uint8Array = randomBytes(realSeedLen)) {
|
213
|
+
const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
|
214
|
+
const publicKey = pkCoder.encode(pk);
|
215
|
+
cleanBytes(pk);
|
216
|
+
cleanBytes(secretKey);
|
217
|
+
return { secretKey: seed, publicKey };
|
218
|
+
},
|
219
|
+
expandDecapsulationKey,
|
220
|
+
realSeedLen,
|
221
|
+
};
|
222
|
+
}
|
223
|
+
|
224
|
+
// This generic function that combines multiple KEMs into single one
|
225
|
+
export function combineKEMS(
|
226
|
+
realSeedLen: number | undefined, // how much bytes expandSeed expects
|
227
|
+
realMsgLen: number | undefined, // how much bytes combiner returns
|
228
|
+
expandSeed: ExpandSeed,
|
229
|
+
combiner: Combiner,
|
230
|
+
...kems: KEM[]
|
231
|
+
): KEM {
|
232
|
+
const keys = combineKeys(realSeedLen, expandSeed, ...kems);
|
233
|
+
const ctCoder = splitLengths(kems, 'cipherText');
|
234
|
+
const pkCoder = splitLengths(kems, 'publicKey');
|
235
|
+
const msgCoder = splitLengths(kems, 'msg');
|
236
|
+
if (realMsgLen === undefined) realMsgLen = msgCoder.bytesLen;
|
237
|
+
anumber(realMsgLen);
|
238
|
+
return {
|
239
|
+
lengths: {
|
240
|
+
...keys.info.lengths,
|
241
|
+
msg: realMsgLen,
|
242
|
+
msgRand: msgCoder.bytesLen,
|
243
|
+
cipherText: ctCoder.bytesLen,
|
244
|
+
},
|
245
|
+
getPublicKey: keys.getPublicKey,
|
246
|
+
keygen: keys.keygen,
|
247
|
+
encapsulate(pk: Uint8Array, randomness: Uint8Array = randomBytes(msgCoder.bytesLen)) {
|
248
|
+
const pks = pkCoder.decode(pk);
|
249
|
+
const rand = msgCoder.decode(randomness);
|
250
|
+
const enc = kems.map((i, j) => i.encapsulate(pks[j], rand[j]));
|
251
|
+
const sharedSecret = enc.map((i) => i.sharedSecret);
|
252
|
+
const cipherText = enc.map((i) => i.cipherText);
|
253
|
+
const res = {
|
254
|
+
sharedSecret: combiner(pks, cipherText, sharedSecret),
|
255
|
+
cipherText: ctCoder.encode(cipherText),
|
256
|
+
};
|
257
|
+
cleanBytes(sharedSecret, cipherText, pks);
|
258
|
+
return res;
|
259
|
+
},
|
260
|
+
decapsulate(ct: Uint8Array, seed: Uint8Array) {
|
261
|
+
const cts = ctCoder.decode(ct);
|
262
|
+
const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
|
263
|
+
const sharedSecret = kems.map((i, j) => i.decapsulate(cts[j], secretKey[j]));
|
264
|
+
return combiner(publicKey, cts, sharedSecret);
|
265
|
+
},
|
266
|
+
};
|
267
|
+
}
|
268
|
+
// There is no specs for this, but can be useful
|
269
|
+
// realSeedLen: how much bytes expandSeed expects.
|
270
|
+
export function combineSigners(
|
271
|
+
realSeedLen: number | undefined,
|
272
|
+
expandSeed: ExpandSeed,
|
273
|
+
...signers: Signer[]
|
274
|
+
): Signer {
|
275
|
+
const keys = combineKeys(realSeedLen, expandSeed, ...signers);
|
276
|
+
const sigCoder = splitLengths(signers, 'signature');
|
277
|
+
const pkCoder = splitLengths(signers, 'publicKey');
|
278
|
+
return {
|
279
|
+
lengths: { ...keys.info.lengths, signature: sigCoder.bytesLen, signRand: 0 },
|
280
|
+
getPublicKey: keys.getPublicKey,
|
281
|
+
keygen: keys.keygen,
|
282
|
+
sign(message, seed) {
|
283
|
+
const { secretKey } = keys.expandDecapsulationKey(seed);
|
284
|
+
// NOTE: we probably can make different hashes for different algorithms
|
285
|
+
// same way as we do for kem, but not sure if this a good idea.
|
286
|
+
const sigs = signers.map((i, j) => i.sign(message, secretKey[j]));
|
287
|
+
return sigCoder.encode(sigs);
|
288
|
+
},
|
289
|
+
verify: (signature, message, publicKey) => {
|
290
|
+
const pks = pkCoder.decode(publicKey);
|
291
|
+
const sigs = sigCoder.decode(signature);
|
292
|
+
for (let i = 0; i < signers.length; i++) {
|
293
|
+
if (!signers[i].verify(sigs[i], message, pks[i])) return false;
|
294
|
+
}
|
295
|
+
return true;
|
296
|
+
},
|
297
|
+
};
|
298
|
+
}
|
299
|
+
|
300
|
+
export function QSF(label: string, pqc: KEM, curveKEM: KEM, xof: XOF, kdf: CHash): KEM {
|
301
|
+
ahash(xof);
|
302
|
+
ahash(kdf);
|
303
|
+
return combineKEMS(
|
304
|
+
32,
|
305
|
+
32,
|
306
|
+
expandSeedXof(xof),
|
307
|
+
(pk, ct, ss) => kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
|
308
|
+
pqc,
|
309
|
+
curveKEM
|
310
|
+
);
|
311
|
+
}
|
312
|
+
|
313
|
+
export const QSFMLKEM768P256: KEM = QSF(
|
314
|
+
'QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)',
|
315
|
+
ml_kem768,
|
316
|
+
ecdhKem(p256, true),
|
317
|
+
shake256,
|
318
|
+
sha3_256
|
319
|
+
);
|
320
|
+
|
321
|
+
export const QSFMLKEM1024P384: KEM = QSF(
|
322
|
+
'QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)',
|
323
|
+
ml_kem1024,
|
324
|
+
ecdhKem(p384, true),
|
325
|
+
shake256,
|
326
|
+
sha3_256
|
327
|
+
);
|
328
|
+
|
329
|
+
export function KitchenSink(label: string, pqc: KEM, curveKEM: KEM, xof: XOF, hash: CHash): KEM {
|
330
|
+
ahash(xof);
|
331
|
+
ahash(hash);
|
332
|
+
return combineKEMS(
|
333
|
+
32,
|
334
|
+
32,
|
335
|
+
expandSeedXof(xof),
|
336
|
+
(pk, ct, ss) => {
|
337
|
+
const preimage = concatBytes(ss[0], ss[1], ct[0], pk[0], ct[1], pk[1], asciiToBytes(label));
|
338
|
+
const len = 32;
|
339
|
+
const ikm = concatBytes(asciiToBytes('hybrid_prk'), preimage);
|
340
|
+
const prk = extract(hash, ikm);
|
341
|
+
const info = concatBytes(
|
342
|
+
numberToBytesBE(len, 2),
|
343
|
+
asciiToBytes('shared_secret'),
|
344
|
+
asciiToBytes('')
|
345
|
+
);
|
346
|
+
const res = expand(hash, prk, info, len);
|
347
|
+
cleanBytes(prk, info, ikm, preimage);
|
348
|
+
return res;
|
349
|
+
},
|
350
|
+
pqc,
|
351
|
+
curveKEM
|
352
|
+
);
|
353
|
+
}
|
354
|
+
|
355
|
+
const x25519kem = ecdhKem(x25519);
|
356
|
+
export const KitchenSinkMLKEM768X25519: KEM = KitchenSink(
|
357
|
+
'KitchenSink-KEM(ML-KEM-768,X25519)-XOF(SHAKE256)-KDF(HKDF-SHA-256)',
|
358
|
+
ml_kem768,
|
359
|
+
x25519kem,
|
360
|
+
shake256,
|
361
|
+
sha256
|
362
|
+
);
|
363
|
+
|
364
|
+
// Always X25519 and ML-KEM - 768, no point to export
|
365
|
+
export const XWing: KEM = combineKEMS(
|
366
|
+
32,
|
367
|
+
32,
|
368
|
+
expandSeedXof(shake256),
|
369
|
+
// Awesome label, so much escaping hell in a single line.
|
370
|
+
(pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes('\\.//^\\'))),
|
371
|
+
ml_kem768,
|
372
|
+
x25519kem
|
373
|
+
);
|
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,22 +8,49 @@
|
|
8
8
|
* @module
|
9
9
|
*/
|
10
10
|
/*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
|
11
|
-
import {
|
11
|
+
import { abool } from '@noble/curves/utils.js';
|
12
|
+
import { shake256 } from '@noble/hashes/sha3.js';
|
13
|
+
import type { CHash } from '@noble/hashes/utils.js';
|
12
14
|
import { genCrystals, type XOF, XOF128, XOF256 } from './_crystals.ts';
|
13
15
|
import {
|
16
|
+
abytes,
|
14
17
|
type BytesCoderLen,
|
18
|
+
checkHash,
|
15
19
|
cleanBytes,
|
16
|
-
|
17
|
-
ensureBytes,
|
20
|
+
type CryptoKeys,
|
18
21
|
equalBytes,
|
19
22
|
getMessage,
|
20
23
|
getMessagePrehash,
|
21
24
|
randomBytes,
|
22
25
|
type Signer,
|
26
|
+
type SigOpts,
|
23
27
|
splitCoder,
|
28
|
+
validateOpts,
|
29
|
+
validateSigOpts,
|
30
|
+
validateVerOpts,
|
24
31
|
vecCoder,
|
32
|
+
type VerOpts,
|
25
33
|
} from './utils.ts';
|
26
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 };
|
53
|
+
|
27
54
|
// Constants
|
28
55
|
const N = 256;
|
29
56
|
// 2**23 − 2**13 + 1, 23 bits: multiply will be 46. We have enough precision in JS to avoid bigints
|
@@ -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');
|
@@ -239,8 +267,9 @@ function getDilithium(opts: DilithiumOpts) {
|
|
239
267
|
const W1Coder = polyCoder(GAMMA2 === GAMMA2_1 ? 6 : 4);
|
240
268
|
const W1Vec = vecCoder(W1Coder, K);
|
241
269
|
// Main structures
|
242
|
-
const publicCoder = splitCoder(32, vecCoder(T1Coder, K));
|
270
|
+
const publicCoder = splitCoder('publicKey', 32, vecCoder(T1Coder, K));
|
243
271
|
const secretCoder = splitCoder(
|
272
|
+
'secretKey',
|
244
273
|
32,
|
245
274
|
32,
|
246
275
|
TR_BYTES,
|
@@ -248,7 +277,7 @@ function getDilithium(opts: DilithiumOpts) {
|
|
248
277
|
vecCoder(ETACoder, K),
|
249
278
|
vecCoder(T0Coder, K)
|
250
279
|
);
|
251
|
-
const sigCoder = splitCoder(C_TILDE_BYTES, vecCoder(ZCoder, L), hintCoder);
|
280
|
+
const sigCoder = splitCoder('signature', C_TILDE_BYTES, vecCoder(ZCoder, L), hintCoder);
|
252
281
|
const CoefFromHalfByte =
|
253
282
|
ETA === 2
|
254
283
|
? (n: number) => (n < 15 ? 2 - (n % 5) : false)
|
@@ -322,18 +351,25 @@ function getDilithium(opts: DilithiumOpts) {
|
|
322
351
|
};
|
323
352
|
|
324
353
|
const signRandBytes = 32;
|
325
|
-
const seedCoder = splitCoder(32, 64, 32);
|
354
|
+
const seedCoder = splitCoder('seed', 32, 64, 32);
|
326
355
|
// API & argument positions are exactly as in FIPS204.
|
327
|
-
const internal:
|
328
|
-
|
356
|
+
const internal: DSAInternal = {
|
357
|
+
info: { type: 'internal-ml-dsa' },
|
358
|
+
lengths: {
|
359
|
+
secretKey: secretCoder.bytesLen,
|
360
|
+
publicKey: publicCoder.bytesLen,
|
361
|
+
seed: 32,
|
362
|
+
signature: sigCoder.bytesLen,
|
363
|
+
signRand: signRandBytes,
|
364
|
+
},
|
329
365
|
keygen: (seed?: Uint8Array) => {
|
330
366
|
// H(𝜉||IntegerToBytes(𝑘, 1)||IntegerToBytes(ℓ, 1), 128) 2: ▷ expand seed
|
331
367
|
const seedDst = new Uint8Array(32 + 2);
|
332
368
|
const randSeed = seed === undefined;
|
333
369
|
if (randSeed) seed = randomBytes(32);
|
334
|
-
|
370
|
+
abytes(seed!, 32, 'seed');
|
335
371
|
seedDst.set(seed!);
|
336
|
-
if (randSeed) seed
|
372
|
+
if (randSeed) cleanBytes(seed!);
|
337
373
|
seedDst[32] = K;
|
338
374
|
seedDst[33] = L;
|
339
375
|
const [rho, rhoPrime, K_] = seedCoder.decode(
|
@@ -352,7 +388,7 @@ function getDilithium(opts: DilithiumOpts) {
|
|
352
388
|
const t = newPoly(N);
|
353
389
|
for (let i = 0; i < K; i++) {
|
354
390
|
// t ← NTT−1(A*NTT(s1)) + s2
|
355
|
-
t
|
391
|
+
cleanBytes(t); // don't-reallocate
|
356
392
|
for (let j = 0; j < L; j++) {
|
357
393
|
const aij = RejNTTPoly(xof.get(j, i)); // super slow!
|
358
394
|
polyAdd(t, MultiplyNTTs(aij, s1Hat[j]));
|
@@ -373,8 +409,32 @@ function getDilithium(opts: DilithiumOpts) {
|
|
373
409
|
cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr, seedDst);
|
374
410
|
return { publicKey, secretKey };
|
375
411
|
},
|
412
|
+
getPublicKey: (secretKey: Uint8Array) => {
|
413
|
+
const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
|
414
|
+
const xof = XOF128(rho);
|
415
|
+
const s1Hat = s1.map((p) => NTT.encode(p.slice()));
|
416
|
+
const t1: Poly[] = [];
|
417
|
+
const tmp = newPoly(N);
|
418
|
+
for (let i = 0; i < K; i++) {
|
419
|
+
tmp.fill(0);
|
420
|
+
for (let j = 0; j < L; j++) {
|
421
|
+
const aij = RejNTTPoly(xof.get(j, i)); // A_ij in NTT
|
422
|
+
polyAdd(tmp, MultiplyNTTs(aij, s1Hat[j])); // += A_ij * s1_j
|
423
|
+
}
|
424
|
+
NTT.decode(tmp); // NTT⁻¹
|
425
|
+
polyAdd(tmp, s2[i]); // t_i = A·s1 + s2
|
426
|
+
const { r1 } = polyPowerRound(tmp); // r1 = t1, r0 ≈ t0
|
427
|
+
t1.push(r1);
|
428
|
+
}
|
429
|
+
xof.clean();
|
430
|
+
cleanBytes(tmp, s1Hat, _t0, s1, s2);
|
431
|
+
return publicCoder.encode([rho, t1]);
|
432
|
+
},
|
376
433
|
// NOTE: random is optional.
|
377
|
-
sign: (
|
434
|
+
sign: (msg: Uint8Array, secretKey: Uint8Array, opts: SigOpts & DSAInternalOpts = {}) => {
|
435
|
+
validateSigOpts(opts);
|
436
|
+
validateInternalOpts(opts);
|
437
|
+
let { extraEntropy: random, externalMu = false } = opts;
|
378
438
|
// This part can be pre-cached per secretKey, but there is only minor performance improvement,
|
379
439
|
// since we re-use a lot of variables to computation.
|
380
440
|
const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
|
@@ -398,8 +458,13 @@ function getDilithium(opts: DilithiumOpts) {
|
|
398
458
|
: shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 6: µ ← H(tr||M, 512) ▷ Compute message representative µ
|
399
459
|
|
400
460
|
// Compute private random seed
|
401
|
-
const rnd =
|
402
|
-
|
461
|
+
const rnd =
|
462
|
+
random === false
|
463
|
+
? new Uint8Array(32)
|
464
|
+
: random === undefined
|
465
|
+
? randomBytes(signRandBytes)
|
466
|
+
: random;
|
467
|
+
abytes(rnd, 32, 'extraEntropy');
|
403
468
|
const rhoprime = shake256
|
404
469
|
.create({ dkLen: CRH_BYTES })
|
405
470
|
.update(_K)
|
@@ -407,7 +472,7 @@ function getDilithium(opts: DilithiumOpts) {
|
|
407
472
|
.update(mu)
|
408
473
|
.digest(); // ρ′← H(K||rnd||µ, 512)
|
409
474
|
|
410
|
-
|
475
|
+
abytes(rhoprime, CRH_BYTES);
|
411
476
|
const x256 = XOF256(rhoprime, ZCoder.bytesLen);
|
412
477
|
// Rejection sampling loop
|
413
478
|
main_loop: for (let kappa = 0; ; ) {
|
@@ -464,7 +529,14 @@ function getDilithium(opts: DilithiumOpts) {
|
|
464
529
|
// @ts-ignore
|
465
530
|
throw new Error('Unreachable code path reached, report this error');
|
466
531
|
},
|
467
|
-
verify: (
|
532
|
+
verify: (
|
533
|
+
sig: Uint8Array,
|
534
|
+
msg: Uint8Array,
|
535
|
+
publicKey: Uint8Array,
|
536
|
+
opts: DSAInternalOpts = {}
|
537
|
+
) => {
|
538
|
+
validateInternalOpts(opts);
|
539
|
+
const { externalMu = false } = opts;
|
468
540
|
// ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M.
|
469
541
|
const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk)
|
470
542
|
const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
|
@@ -512,61 +584,76 @@ function getDilithium(opts: DilithiumOpts) {
|
|
512
584
|
},
|
513
585
|
};
|
514
586
|
return {
|
587
|
+
info: { type: 'ml-dsa' },
|
515
588
|
internal,
|
589
|
+
securityLevel: securityLevel,
|
516
590
|
keygen: internal.keygen,
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
M.
|
591
|
+
lengths: internal.lengths,
|
592
|
+
getPublicKey: internal.getPublicKey,
|
593
|
+
sign: (msg: Uint8Array, secretKey: Uint8Array, opts: SigOpts = {}) => {
|
594
|
+
validateSigOpts(opts);
|
595
|
+
const M = getMessage(msg, opts.context);
|
596
|
+
const res = internal.sign(M, secretKey, opts);
|
597
|
+
cleanBytes(M);
|
522
598
|
return res;
|
523
599
|
},
|
524
|
-
verify: (
|
525
|
-
|
600
|
+
verify: (sig: Uint8Array, msg: Uint8Array, publicKey: Uint8Array, opts: VerOpts = {}) => {
|
601
|
+
validateVerOpts(opts);
|
602
|
+
return internal.verify(sig, getMessage(msg, opts.context), publicKey);
|
603
|
+
},
|
604
|
+
prehash: (hash: CHash) => {
|
605
|
+
checkHash(hash, securityLevel);
|
606
|
+
return {
|
607
|
+
info: { type: 'hashml-dsa' },
|
608
|
+
securityLevel: securityLevel,
|
609
|
+
lengths: internal.lengths,
|
610
|
+
keygen: internal.keygen,
|
611
|
+
getPublicKey: internal.getPublicKey,
|
612
|
+
sign: (msg: Uint8Array, secretKey: Uint8Array, opts: SigOpts = {}) => {
|
613
|
+
validateSigOpts(opts);
|
614
|
+
const M = getMessagePrehash(hash, msg, opts.context);
|
615
|
+
const res = internal.sign(M, secretKey, opts);
|
616
|
+
cleanBytes(M);
|
617
|
+
return res;
|
618
|
+
},
|
619
|
+
verify: (sig: Uint8Array, msg: Uint8Array, publicKey: Uint8Array, opts: VerOpts = {}) => {
|
620
|
+
validateVerOpts(opts);
|
621
|
+
return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey);
|
622
|
+
},
|
623
|
+
};
|
526
624
|
},
|
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
625
|
};
|
539
626
|
}
|
540
627
|
|
541
|
-
/** Signer API, containing internal methods */
|
542
|
-
export type SignerWithInternal = Signer & { internal: Signer };
|
543
|
-
|
544
628
|
/** ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD. */
|
545
|
-
export const ml_dsa44:
|
629
|
+
export const ml_dsa44: DSA = /* @__PURE__ */ getDilithium({
|
546
630
|
...PARAMS[2],
|
547
631
|
CRH_BYTES: 64,
|
548
632
|
TR_BYTES: 64,
|
549
633
|
C_TILDE_BYTES: 32,
|
550
634
|
XOF128,
|
551
635
|
XOF256,
|
636
|
+
securityLevel: 128,
|
552
637
|
});
|
553
638
|
|
554
639
|
/** ML-DSA-65 for 192-bit security level. Not recommended after 2030, as per ASD. */
|
555
|
-
export const ml_dsa65:
|
640
|
+
export const ml_dsa65: DSA = /* @__PURE__ */ getDilithium({
|
556
641
|
...PARAMS[3],
|
557
642
|
CRH_BYTES: 64,
|
558
643
|
TR_BYTES: 64,
|
559
644
|
C_TILDE_BYTES: 48,
|
560
645
|
XOF128,
|
561
646
|
XOF256,
|
647
|
+
securityLevel: 192,
|
562
648
|
});
|
563
649
|
|
564
650
|
/** ML-DSA-87 for 256-bit security level. OK after 2030, as per ASD. */
|
565
|
-
export const ml_dsa87:
|
651
|
+
export const ml_dsa87: DSA = /* @__PURE__ */ getDilithium({
|
566
652
|
...PARAMS[5],
|
567
653
|
CRH_BYTES: 64,
|
568
654
|
TR_BYTES: 64,
|
569
655
|
C_TILDE_BYTES: 64,
|
570
656
|
XOF128,
|
571
657
|
XOF256,
|
658
|
+
securityLevel: 256,
|
572
659
|
});
|