@noble/post-quantum 0.6.1 → 0.7.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 +191 -104
- package/_crystals.d.ts +8 -3
- package/_crystals.js +38 -10
- package/falcon.d.ts +1 -2
- package/falcon.js +202 -115
- package/hybrid.d.ts +38 -28
- package/hybrid.js +215 -86
- package/index.d.ts +0 -1
- package/index.js +1 -2
- package/ml-dsa.d.ts +31 -5
- package/ml-dsa.js +118 -34
- package/ml-kem.d.ts +45 -4
- package/ml-kem.js +206 -64
- package/package.json +16 -20
- package/slh-dsa.d.ts +23 -3
- package/slh-dsa.js +127 -60
- package/src/_crystals.ts +45 -11
- package/src/falcon.ts +206 -121
- package/src/hybrid.ts +217 -83
- package/src/index.ts +1 -1
- package/src/ml-dsa.ts +140 -43
- package/src/ml-kem.ts +239 -66
- package/src/slh-dsa.ts +149 -69
- package/src/utils.ts +186 -25
- package/src/webcrypto.ts +322 -0
- package/utils.d.ts +54 -4
- package/utils.js +167 -28
- package/webcrypto.d.ts +91 -0
- package/webcrypto.js +213 -0
- package/_crystals.d.ts.map +0 -1
- package/_crystals.js.map +0 -1
- package/falcon.d.ts.map +0 -1
- package/falcon.js.map +0 -1
- package/hybrid.d.ts.map +0 -1
- package/hybrid.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/ml-dsa.d.ts.map +0 -1
- package/ml-dsa.js.map +0 -1
- package/ml-kem.d.ts.map +0 -1
- package/ml-kem.js.map +0 -1
- package/slh-dsa.d.ts.map +0 -1
- package/slh-dsa.js.map +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
package/hybrid.js
CHANGED
|
@@ -79,15 +79,38 @@ import {} from '@noble/curves/abstract/montgomery.js';
|
|
|
79
79
|
import {} from '@noble/curves/abstract/weierstrass.js';
|
|
80
80
|
import { x25519 } from '@noble/curves/ed25519.js';
|
|
81
81
|
import { p256, p384 } from '@noble/curves/nist.js';
|
|
82
|
-
import { asciiToBytes, bytesToNumberBE, bytesToNumberLE, concatBytes, numberToBytesBE, } from '@noble/curves/utils.js';
|
|
82
|
+
import { abool, afunction, asciiToBytes, bytesToNumberBE, bytesToNumberLE, concatBytes, numberToBytesBE, } from '@noble/curves/utils.js';
|
|
83
83
|
import { expand, extract } from '@noble/hashes/hkdf.js';
|
|
84
84
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
85
85
|
import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
86
|
-
import { abytes, ahash, anumber } from '@noble/hashes/utils.js';
|
|
86
|
+
import { abytes, ahash, anumber, isBytes } from '@noble/hashes/utils.js';
|
|
87
87
|
import { ml_kem1024, ml_kem768 } from "./ml-kem.js";
|
|
88
|
-
import { cleanBytes, copyBytes, randomBytes, splitCoder, validateSigOpts, validateVerOpts, } from "./utils.js";
|
|
88
|
+
import { aobject, astring, cleanBytes, copyBytes, randomBytes, splitCoder, validateSigOpts, validateVerOpts, } from "./utils.js";
|
|
89
|
+
const validateKEM = (kem, title) => {
|
|
90
|
+
const k = aobject(kem, title);
|
|
91
|
+
aobject(k.lengths, `${title}.lengths`);
|
|
92
|
+
afunction(k.keygen, `${title}.keygen`);
|
|
93
|
+
afunction(k.getPublicKey, `${title}.getPublicKey`);
|
|
94
|
+
afunction(k.encapsulate, `${title}.encapsulate`);
|
|
95
|
+
afunction(k.decapsulate, `${title}.decapsulate`);
|
|
96
|
+
return k;
|
|
97
|
+
};
|
|
98
|
+
const validateSigner = (signer, title) => {
|
|
99
|
+
const s = aobject(signer, title);
|
|
100
|
+
aobject(s.lengths, `${title}.lengths`);
|
|
101
|
+
afunction(s.keygen, `${title}.keygen`);
|
|
102
|
+
afunction(s.getPublicKey, `${title}.getPublicKey`);
|
|
103
|
+
afunction(s.sign, `${title}.sign`);
|
|
104
|
+
afunction(s.verify, `${title}.verify`);
|
|
105
|
+
return s;
|
|
106
|
+
};
|
|
89
107
|
// Can re-use if decide to signatures support, on other hand getSecretKey is specific and ugly
|
|
90
108
|
function ecKeygen(curve, allowZeroKey = false) {
|
|
109
|
+
const c = aobject(curve, 'curve');
|
|
110
|
+
aobject(c.lengths, 'curve.lengths');
|
|
111
|
+
afunction(c.keygen, 'curve.keygen');
|
|
112
|
+
afunction(c.getPublicKey, 'curve.getPublicKey');
|
|
113
|
+
abool(allowZeroKey, 'allowZeroKey');
|
|
91
114
|
const lengths = curve.lengths;
|
|
92
115
|
let keygen = curve.keygen;
|
|
93
116
|
if (allowZeroKey) {
|
|
@@ -125,6 +148,12 @@ function ecKeygen(curve, allowZeroKey = false) {
|
|
|
125
148
|
/**
|
|
126
149
|
* Wraps an ECDH-capable curve as a KEM.
|
|
127
150
|
* Shared secrets stay in the wrapped curve's raw ECDH byte format with no built-in KDF.
|
|
151
|
+
*
|
|
152
|
+
* SECURITY: this is a low-level component adapter, not a standalone IND-CCA-secure KEM. It does
|
|
153
|
+
* not bind the encapsulation or recipient public key into the secret, so distinct accepted point
|
|
154
|
+
* encodings can produce the same output. Use it only inside a construction whose specified
|
|
155
|
+
* combiner binds those values, or use a standardized DHKEM with labeled extract-and-expand.
|
|
156
|
+
*
|
|
128
157
|
* On SEC 1 / Weierstrass curves, that means the compressed shared-point body without the
|
|
129
158
|
* 1-byte `0x02` / `0x03` prefix.
|
|
130
159
|
* The X25519 path also leaves RFC 7748's optional all-zero shared-secret check to callers.
|
|
@@ -140,15 +169,20 @@ function ecKeygen(curve, allowZeroKey = false) {
|
|
|
140
169
|
* Wrap an ECDH-capable curve as a generic KEM.
|
|
141
170
|
* ```ts
|
|
142
171
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
143
|
-
* import {
|
|
144
|
-
* const kem =
|
|
172
|
+
* import { _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
173
|
+
* const kem = _ecdhKem(x25519);
|
|
145
174
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
146
175
|
* ```
|
|
147
176
|
*/
|
|
148
|
-
export function
|
|
177
|
+
export function _ecdhKem(curve, allowZeroKey = false) {
|
|
149
178
|
const kg = ecKeygen(curve, allowZeroKey);
|
|
150
179
|
if (!curve.getSharedSecret)
|
|
151
180
|
throw new Error('wrong curve'); // ed25519 doesn't have one!
|
|
181
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
182
|
+
const decapsulate = (cipherText, secretKey) => {
|
|
183
|
+
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
184
|
+
return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res);
|
|
185
|
+
};
|
|
152
186
|
return {
|
|
153
187
|
lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
|
|
154
188
|
keygen: kg.keygen,
|
|
@@ -159,8 +193,8 @@ export function ecdhKem(curve, allowZeroKey = false) {
|
|
|
159
193
|
const seed = copyBytes(rand);
|
|
160
194
|
let ek = undefined;
|
|
161
195
|
try {
|
|
162
|
-
ek =
|
|
163
|
-
const sharedSecret =
|
|
196
|
+
ek = kg.keygen(seed).secretKey;
|
|
197
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
164
198
|
const cipherText = curve.getPublicKey(ek);
|
|
165
199
|
return { sharedSecret, cipherText };
|
|
166
200
|
}
|
|
@@ -172,10 +206,7 @@ export function ecdhKem(curve, allowZeroKey = false) {
|
|
|
172
206
|
cleanBytes(ek);
|
|
173
207
|
}
|
|
174
208
|
},
|
|
175
|
-
decapsulate
|
|
176
|
-
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
177
|
-
return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res);
|
|
178
|
-
},
|
|
209
|
+
decapsulate,
|
|
179
210
|
};
|
|
180
211
|
}
|
|
181
212
|
/**
|
|
@@ -208,7 +239,7 @@ export function ecSigner(curve, allowZeroKey = false) {
|
|
|
208
239
|
keygen: kg.keygen,
|
|
209
240
|
getPublicKey: kg.getPublicKey,
|
|
210
241
|
sign: (message, secretKey, opts = {}) => {
|
|
211
|
-
validateSigOpts(opts);
|
|
242
|
+
opts = validateSigOpts(opts);
|
|
212
243
|
// This generic wrapper intentionally keeps the Signer contract to message + key only.
|
|
213
244
|
// Backend-specific knobs like ECDSA extraEntropy or Ed25519ctx context cannot be forwarded
|
|
214
245
|
// uniformly through combineSigners(), so callers that need them must use the curve directly.
|
|
@@ -223,20 +254,28 @@ export function ecSigner(curve, allowZeroKey = false) {
|
|
|
223
254
|
* generic opts and lets wrapped-curve malformed-input errors escape unchanged.
|
|
224
255
|
*/
|
|
225
256
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
226
|
-
validateVerOpts(opts);
|
|
257
|
+
opts = validateVerOpts(opts);
|
|
227
258
|
if (opts.context !== undefined)
|
|
228
259
|
throw new Error('ecSigner does not support context; use the underlying curve directly');
|
|
229
260
|
return curve.verify(signature, message, publicKey);
|
|
230
261
|
},
|
|
231
262
|
};
|
|
232
263
|
}
|
|
264
|
+
function positiveLength(value, title) {
|
|
265
|
+
const length = anumber(value, title);
|
|
266
|
+
if (length === 0)
|
|
267
|
+
throw new RangeError(`"${title}" expected integer greater than 0, got 0`);
|
|
268
|
+
return length;
|
|
269
|
+
}
|
|
233
270
|
function splitLengths(lst, name) {
|
|
234
271
|
// Preserve caller order exactly; raw numeric fields still decode as splitCoder() subarray views.
|
|
235
|
-
|
|
272
|
+
const coder = splitCoder(name, ...lst.map((i) => {
|
|
236
273
|
if (typeof i.lengths[name] !== 'number')
|
|
237
274
|
throw new Error('wrong length: ' + name);
|
|
238
|
-
return i.lengths[name];
|
|
275
|
+
return positiveLength(i.lengths[name], name);
|
|
239
276
|
}));
|
|
277
|
+
positiveLength(coder.bytesLen, name);
|
|
278
|
+
return coder;
|
|
240
279
|
}
|
|
241
280
|
// It is XOF for most cases, but can be more complex!
|
|
242
281
|
/**
|
|
@@ -265,11 +304,9 @@ expandSeed_, ...ck_) {
|
|
|
265
304
|
const seedCoder = splitLengths(ck, 'seed');
|
|
266
305
|
const pkCoder = splitLengths(ck, 'publicKey');
|
|
267
306
|
// Allows to use identity functions for combiner/expandSeed
|
|
268
|
-
|
|
269
|
-
realSeedLen = seedCoder.bytesLen;
|
|
270
|
-
anumber(realSeedLen);
|
|
307
|
+
const rootSeedLen = positiveLength(realSeedLen === undefined ? seedCoder.bytesLen : realSeedLen, 'realSeedLen');
|
|
271
308
|
function expandDecapsulationKey(seed) {
|
|
272
|
-
abytes(seed,
|
|
309
|
+
abytes(seed, rootSeedLen);
|
|
273
310
|
const expandedRaw = expandSeed(seed, seedCoder.bytesLen);
|
|
274
311
|
// Identity/subarray expanders can hand back caller-owned seed storage. Detach those outputs so
|
|
275
312
|
// later cleanup can wipe the expanded schedule without mutating the caller's root seed bytes.
|
|
@@ -303,39 +340,62 @@ expandSeed_, ...ck_) {
|
|
|
303
340
|
cleanBytes(secretKey);
|
|
304
341
|
}
|
|
305
342
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
|
|
343
|
+
// Standalone (not a method) so getPublicKey / destructured usage never depends on `this`.
|
|
344
|
+
const keygen = (seed) => {
|
|
345
|
+
// Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
|
|
346
|
+
// caller mutation of the seed cannot silently change the secret key (and vice versa).
|
|
347
|
+
const root = seed === undefined ? randomBytes(rootSeedLen) : copyBytes(seed);
|
|
348
|
+
let res;
|
|
349
|
+
try {
|
|
350
|
+
const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
|
|
315
351
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
352
|
+
res = {
|
|
353
|
+
secretKey: root,
|
|
354
|
+
publicKey: pkCoder.encode(pk),
|
|
355
|
+
};
|
|
318
356
|
}
|
|
319
357
|
finally {
|
|
320
|
-
|
|
321
|
-
// The exported secretKey is the caller/root seed itself; child secret keys are internal
|
|
358
|
+
// The exported secretKey is the (detached) root seed; child secret keys are internal
|
|
322
359
|
// expansion outputs that are cleaned whether encoding succeeds or throws.
|
|
323
|
-
cleanBytes(secretKey);
|
|
360
|
+
cleanBytes(pk, secretKey);
|
|
324
361
|
}
|
|
362
|
+
return res;
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
if (!res)
|
|
366
|
+
cleanBytes(root);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
return {
|
|
370
|
+
info: { lengths: { seed: rootSeedLen, publicKey: pkCoder.bytesLen, secretKey: rootSeedLen } },
|
|
371
|
+
// Composite secret keys are root seeds, so public-key derivation reruns key expansion from
|
|
372
|
+
// that seed instead of decoding a packed child-secret-key structure.
|
|
373
|
+
getPublicKey: (secretKey) => {
|
|
374
|
+
const keys = keygen(secretKey);
|
|
375
|
+
// keygen detaches its exported root; getPublicKey discards that half of the result.
|
|
376
|
+
cleanBytes(keys.secretKey);
|
|
377
|
+
return keys.publicKey;
|
|
325
378
|
},
|
|
379
|
+
keygen,
|
|
326
380
|
expandDecapsulationKey,
|
|
327
|
-
realSeedLen,
|
|
381
|
+
realSeedLen: rootSeedLen,
|
|
328
382
|
};
|
|
329
383
|
}
|
|
330
384
|
// This generic function that combines multiple KEMs into single one
|
|
331
385
|
/**
|
|
332
386
|
* Combines multiple KEMs into one composite KEM.
|
|
333
|
-
* @param realSeedLen -
|
|
334
|
-
*
|
|
387
|
+
* @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
|
|
388
|
+
* the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
|
|
389
|
+
* size.
|
|
390
|
+
* @param realMsgLen - Positive shared-secret length returned by `combiner`, or `undefined` to use
|
|
391
|
+
* the sum of component message lengths.
|
|
335
392
|
* @param expandSeed - Seed expander used to derive per-KEM seeds.
|
|
336
393
|
* @param combiner - Combines the per-KEM outputs into one shared secret.
|
|
337
|
-
* @param kems - KEM
|
|
394
|
+
* @param kems - At least one KEM implementation. A construction advertised as hybrid normally
|
|
395
|
+
* supplies two or more.
|
|
338
396
|
* @returns Composite KEM.
|
|
397
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
398
|
+
* @throws If there are no components or any required length resolves to zero. {@link RangeError}
|
|
339
399
|
* @example
|
|
340
400
|
* Combine multiple KEMs into one composite KEM.
|
|
341
401
|
* ```ts
|
|
@@ -356,21 +416,53 @@ expandSeed_, ...ck_) {
|
|
|
356
416
|
export function combineKEMS(realSeedLen, // how much bytes expandSeed expects
|
|
357
417
|
realMsgLen, // how much bytes combiner returns
|
|
358
418
|
expandSeed, combiner, ...kems) {
|
|
419
|
+
if (realSeedLen !== undefined)
|
|
420
|
+
positiveLength(realSeedLen, 'realSeedLen');
|
|
421
|
+
if (realMsgLen !== undefined)
|
|
422
|
+
positiveLength(realMsgLen, 'realMsgLen');
|
|
423
|
+
if (typeof expandSeed !== 'function')
|
|
424
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
425
|
+
if (typeof combiner !== 'function')
|
|
426
|
+
throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
|
|
359
427
|
const rawCombiner = combiner;
|
|
360
428
|
const rawKems = kems;
|
|
429
|
+
if (rawKems.length === 0)
|
|
430
|
+
throw new RangeError('combineKEMS requires at least one KEM');
|
|
431
|
+
for (let i = 0; i < rawKems.length; i++)
|
|
432
|
+
validateKEM(rawKems[i], `kems[${i}]`);
|
|
361
433
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
|
|
362
434
|
const ctCoder = splitLengths(rawKems, 'cipherText');
|
|
363
435
|
const pkCoder = splitLengths(rawKems, 'publicKey');
|
|
364
436
|
const msgCoder = splitLengths(rawKems, 'msg');
|
|
365
|
-
|
|
366
|
-
realMsgLen = msgCoder.bytesLen;
|
|
367
|
-
anumber(realMsgLen);
|
|
437
|
+
const sharedSecretLen = positiveLength(realMsgLen === undefined ? msgCoder.bytesLen : realMsgLen, 'realMsgLen');
|
|
368
438
|
const lengths = Object.freeze({
|
|
369
439
|
...keys.info.lengths,
|
|
370
|
-
msg:
|
|
440
|
+
msg: sharedSecretLen,
|
|
371
441
|
msgRand: msgCoder.bytesLen,
|
|
372
442
|
cipherText: ctCoder.bytesLen,
|
|
373
443
|
});
|
|
444
|
+
const combine = (publicKeys, cipherTexts, sharedSecrets) => {
|
|
445
|
+
const combined = rawCombiner(publicKeys, cipherTexts, sharedSecrets);
|
|
446
|
+
try {
|
|
447
|
+
return copyBytes(abytes(combined, sharedSecretLen, 'sharedSecret'));
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (isBytes(combined)) {
|
|
451
|
+
// A combiner may return any callback argument. Public keys during encapsulation and
|
|
452
|
+
// ciphertexts during decapsulation are views into caller-owned inputs, so wipe an invalid
|
|
453
|
+
// byte result only when its range does not overlap either public argument vector. Child
|
|
454
|
+
// shared-secret aliases are already wiped by the operation's outer finally block.
|
|
455
|
+
const overlaps = (value) => combined.buffer === value.buffer &&
|
|
456
|
+
combined.byteOffset < value.byteOffset + value.byteLength &&
|
|
457
|
+
value.byteOffset < combined.byteOffset + combined.byteLength;
|
|
458
|
+
const aliasesPublicInput = publicKeys.some(overlaps) ||
|
|
459
|
+
cipherTexts.some(overlaps);
|
|
460
|
+
if (!aliasesPublicInput)
|
|
461
|
+
cleanBytes(combined);
|
|
462
|
+
}
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
374
466
|
return Object.freeze({
|
|
375
467
|
lengths,
|
|
376
468
|
getPublicKey: keys.getPublicKey,
|
|
@@ -386,11 +478,14 @@ expandSeed, combiner, ...kems) {
|
|
|
386
478
|
sharedSecret.push(enc.sharedSecret);
|
|
387
479
|
cipherText.push(enc.cipherText);
|
|
388
480
|
}
|
|
481
|
+
// Validate and detach public ciphertexts before deriving a final secret from them. This
|
|
482
|
+
// also ensures a malformed child cannot make us allocate and then strand a combined key.
|
|
483
|
+
const encodedCipherText = ctCoder.encode(cipherText);
|
|
389
484
|
return {
|
|
390
485
|
// Detach the combiner result before cleanup: a caller-provided combiner may alias one of
|
|
391
486
|
// the child sharedSecret buffers, and those child buffers are zeroized immediately below.
|
|
392
|
-
sharedSecret:
|
|
393
|
-
cipherText:
|
|
487
|
+
sharedSecret: combine(pks, cipherText, sharedSecret),
|
|
488
|
+
cipherText: encodedCipherText,
|
|
394
489
|
};
|
|
395
490
|
}
|
|
396
491
|
finally {
|
|
@@ -402,11 +497,16 @@ expandSeed, combiner, ...kems) {
|
|
|
402
497
|
decapsulate(ct, seed) {
|
|
403
498
|
const cts = ctCoder.decode(ct);
|
|
404
499
|
const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
|
|
405
|
-
const sharedSecret =
|
|
500
|
+
const sharedSecret = [];
|
|
406
501
|
try {
|
|
502
|
+
// Child decapsulate() is inside the try: it can throw on an attacker-supplied ciphertext
|
|
503
|
+
// (e.g. a low-order X25519 point), and by then the expanded child secret keys — plus any
|
|
504
|
+
// child shared secrets already produced — are live and must still be wiped.
|
|
505
|
+
for (let i = 0; i < rawKems.length; i++)
|
|
506
|
+
sharedSecret.push(rawKems[i].decapsulate(cts[i], secretKey[i]));
|
|
407
507
|
// Detach the decapsulation result before cleanup: the combiner may hand back one of the
|
|
408
508
|
// child shared-secret buffers, and those temporary buffers are zeroized below.
|
|
409
|
-
return
|
|
509
|
+
return combine(publicKey, cts, sharedSecret);
|
|
410
510
|
}
|
|
411
511
|
finally {
|
|
412
512
|
// Decapsulation only needs the expanded child secret keys and child shared secrets for this
|
|
@@ -420,10 +520,15 @@ expandSeed, combiner, ...kems) {
|
|
|
420
520
|
// realSeedLen: how much bytes expandSeed expects.
|
|
421
521
|
/**
|
|
422
522
|
* Combines multiple signers into one composite signer.
|
|
423
|
-
* @param realSeedLen -
|
|
523
|
+
* @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
|
|
524
|
+
* the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
|
|
525
|
+
* size.
|
|
424
526
|
* @param expandSeed - Seed expander used to derive per-signer seeds.
|
|
425
|
-
* @param signers -
|
|
527
|
+
* @param signers - At least one signer. A construction advertised as hybrid normally supplies two
|
|
528
|
+
* or more.
|
|
426
529
|
* @returns Composite signer.
|
|
530
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
531
|
+
* @throws If there are no components or any required length resolves to zero. {@link RangeError}
|
|
427
532
|
* @example
|
|
428
533
|
* Combine multiple signers into one composite signer.
|
|
429
534
|
* ```ts
|
|
@@ -431,11 +536,23 @@ expandSeed, combiner, ...kems) {
|
|
|
431
536
|
* import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
|
|
432
537
|
* import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
433
538
|
* const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
|
|
434
|
-
* const
|
|
539
|
+
* const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
|
|
540
|
+
* const { secretKey, publicKey } = hybrid.keygen(seed);
|
|
541
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
542
|
+
* const sig = hybrid.sign(msg, secretKey);
|
|
543
|
+
* const isValid = hybrid.verify(sig, msg, publicKey);
|
|
435
544
|
* ```
|
|
436
545
|
*/
|
|
437
546
|
export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
547
|
+
if (realSeedLen !== undefined)
|
|
548
|
+
positiveLength(realSeedLen, 'realSeedLen');
|
|
549
|
+
if (typeof expandSeed !== 'function')
|
|
550
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
438
551
|
const rawSigners = signers;
|
|
552
|
+
if (rawSigners.length === 0)
|
|
553
|
+
throw new RangeError('combineSigners requires at least one signer');
|
|
554
|
+
for (let i = 0; i < rawSigners.length; i++)
|
|
555
|
+
validateSigner(rawSigners[i], `signers[${i}]`);
|
|
439
556
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
|
|
440
557
|
const sigCoder = splitLengths(rawSigners, 'signature');
|
|
441
558
|
const pkCoder = splitLengths(rawSigners, 'publicKey');
|
|
@@ -444,7 +561,7 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
|
444
561
|
getPublicKey: keys.getPublicKey,
|
|
445
562
|
keygen: keys.keygen,
|
|
446
563
|
sign(message, seed, opts = {}) {
|
|
447
|
-
validateSigOpts(opts);
|
|
564
|
+
opts = validateSigOpts(opts);
|
|
448
565
|
// This generic wrapper intentionally keeps the composite signer contract to message + root
|
|
449
566
|
// seed only. Per-signer opts like context or extraEntropy cannot be preserved uniformly
|
|
450
567
|
// across mixed backends, so callers that need them must use the underlying signer directly.
|
|
@@ -464,14 +581,21 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
|
464
581
|
}
|
|
465
582
|
},
|
|
466
583
|
/** Verify one combined signature.
|
|
467
|
-
*
|
|
468
|
-
*
|
|
584
|
+
* Wrong-length aggregate signatures return `false` (matching ml-dsa / slh-dsa behavior), as
|
|
585
|
+
* does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
|
|
469
586
|
*/
|
|
470
587
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
471
|
-
validateVerOpts(opts);
|
|
588
|
+
opts = validateVerOpts(opts);
|
|
472
589
|
if (opts.context !== undefined)
|
|
473
590
|
throw new Error('combineSigners does not support context; use the underlying signer directly');
|
|
591
|
+
// Malformed signature *length* is a verification failure, not a thrown type error —
|
|
592
|
+
// consistent with ml-dsa / slh-dsa. Must run before sigCoder.decode, which throws.
|
|
593
|
+
// Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
|
|
594
|
+
abytes(signature, undefined, 'signature');
|
|
595
|
+
// A signature failure must not hide malformed aggregate public-key bytes.
|
|
474
596
|
const pks = pkCoder.decode(publicKey);
|
|
597
|
+
if (signature.length !== sigCoder.bytesLen)
|
|
598
|
+
return false;
|
|
475
599
|
const sigs = sigCoder.decode(signature);
|
|
476
600
|
for (let i = 0; i < rawSigners.length; i++) {
|
|
477
601
|
if (!rawSigners[i].verify(sigs[i], message, pks[i]))
|
|
@@ -494,26 +618,35 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
|
494
618
|
* @param xof - XOF used for seed expansion.
|
|
495
619
|
* @param kdf - Hash used for the final combiner.
|
|
496
620
|
* @returns Hybrid KEM.
|
|
621
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
622
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
497
623
|
* @example
|
|
498
624
|
* Build a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM.
|
|
499
625
|
* ```ts
|
|
500
626
|
* import { p256 } from '@noble/curves/nist.js';
|
|
501
627
|
* import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
502
|
-
* import { QSF,
|
|
628
|
+
* import { QSF, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
503
629
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
504
|
-
* const kem = QSF('example', ml_kem768,
|
|
630
|
+
* const kem = QSF('example', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256);
|
|
505
631
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
506
632
|
* ```
|
|
507
633
|
*/
|
|
508
634
|
export function QSF(label, pqc, curveKEM, xof, kdf) {
|
|
635
|
+
astring(label, 'label');
|
|
636
|
+
validateKEM(pqc, 'pqc');
|
|
637
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
638
|
+
if (typeof xof !== 'function' || typeof xof.create !== 'function')
|
|
639
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
509
640
|
ahash(xof);
|
|
641
|
+
if (typeof kdf !== 'function' || typeof kdf.create !== 'function')
|
|
642
|
+
throw new TypeError('"kdf" expected hash function, got type=' + typeof kdf);
|
|
510
643
|
ahash(kdf);
|
|
511
644
|
return combineKEMS(32, kdf.outputLen, expandSeedXof(xof), (pk, ct, ss) => kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))), pqc, curveKEM);
|
|
512
645
|
}
|
|
513
646
|
/** QSF preset combining ML-KEM-768 with P-256. */
|
|
514
|
-
export const QSF_ml_kem768_p256 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem768,
|
|
647
|
+
export const QSF_ml_kem768_p256 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256))();
|
|
515
648
|
/** QSF preset combining ML-KEM-1024 with P-384. */
|
|
516
|
-
export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem1024,
|
|
649
|
+
export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem1024, _ecdhKem(p384, true), shake256, sha3_256))();
|
|
517
650
|
/**
|
|
518
651
|
* Builds the "KitchenSink" hybrid KEM combiner.
|
|
519
652
|
* The current builder always derives a fixed 32-byte output,
|
|
@@ -528,20 +661,29 @@ export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-10
|
|
|
528
661
|
* @param xof - XOF used for seed expansion.
|
|
529
662
|
* @param hash - Hash used for HKDF extraction and expansion.
|
|
530
663
|
* @returns Hybrid KEM.
|
|
664
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
665
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
531
666
|
* @example
|
|
532
667
|
* Build the "KitchenSink" hybrid KEM combiner.
|
|
533
668
|
* ```ts
|
|
534
669
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
535
670
|
* import { shake256 } from '@noble/hashes/sha3.js';
|
|
536
|
-
* import { createKitchenSink,
|
|
671
|
+
* import { createKitchenSink, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
537
672
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
538
673
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
539
|
-
* const kem = createKitchenSink('example', ml_kem768,
|
|
674
|
+
* const kem = createKitchenSink('example', ml_kem768, _ecdhKem(x25519), shake256, sha256);
|
|
540
675
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
541
676
|
* ```
|
|
542
677
|
*/
|
|
543
678
|
export function createKitchenSink(label, pqc, curveKEM, xof, hash) {
|
|
679
|
+
astring(label, 'label');
|
|
680
|
+
validateKEM(pqc, 'pqc');
|
|
681
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
682
|
+
if (typeof xof !== 'function' || typeof xof.create !== 'function')
|
|
683
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
544
684
|
ahash(xof);
|
|
685
|
+
if (typeof hash !== 'function' || typeof hash.create !== 'function')
|
|
686
|
+
throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
|
|
545
687
|
ahash(hash);
|
|
546
688
|
return combineKEMS(32, 32, expandSeedXof(xof), (pk, ct, ss) => {
|
|
547
689
|
const preimage = concatBytes(ss[0], ss[1], ct[0], pk[0], ct[1], pk[1], asciiToBytes(label));
|
|
@@ -554,9 +696,9 @@ export function createKitchenSink(label, pqc, curveKEM, xof, hash) {
|
|
|
554
696
|
return res;
|
|
555
697
|
}, pqc, curveKEM);
|
|
556
698
|
}
|
|
557
|
-
// Internal alias only: this stays exactly `
|
|
699
|
+
// Internal alias only: this stays exactly `_ecdhKem(x25519)`
|
|
558
700
|
// and inherits that wrapper's mutation/oracle behavior.
|
|
559
|
-
const x25519kem = /* @__PURE__ */
|
|
701
|
+
const x25519kem = /* @__PURE__ */ _ecdhKem(x25519);
|
|
560
702
|
/** KitchenSink preset combining ML-KEM-768 with X25519.
|
|
561
703
|
* Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed.
|
|
562
704
|
*/
|
|
@@ -596,6 +738,11 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
|
|
|
596
738
|
const publicKey = curve.getPublicKey(secretKey, false);
|
|
597
739
|
return { secretKey, publicKey };
|
|
598
740
|
}
|
|
741
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
742
|
+
const decapsulate = (cipherText, secretKey) => {
|
|
743
|
+
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
744
|
+
return full.subarray(1);
|
|
745
|
+
};
|
|
599
746
|
return {
|
|
600
747
|
lengths: {
|
|
601
748
|
secretKey: scalarLen,
|
|
@@ -616,7 +763,7 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
|
|
|
616
763
|
let ek = undefined;
|
|
617
764
|
try {
|
|
618
765
|
ek = rejectionSampling(rand).secretKey;
|
|
619
|
-
const sharedSecret =
|
|
766
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
620
767
|
const cipherText = curve.getPublicKey(ek, false);
|
|
621
768
|
return { sharedSecret, cipherText };
|
|
622
769
|
}
|
|
@@ -627,10 +774,7 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
|
|
|
627
774
|
cleanBytes(ek);
|
|
628
775
|
}
|
|
629
776
|
},
|
|
630
|
-
decapsulate
|
|
631
|
-
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
632
|
-
return full.subarray(1);
|
|
633
|
-
},
|
|
777
|
+
decapsulate,
|
|
634
778
|
};
|
|
635
779
|
}
|
|
636
780
|
/**
|
|
@@ -650,29 +794,14 @@ function concreteHybridKem(label, mlkem, curve, nseed) {
|
|
|
650
794
|
const totalSeedLen = mlkemSeedLen + nseed;
|
|
651
795
|
return combineKEMS(32, 32, (seed) => {
|
|
652
796
|
abytes(seed, 32);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
797
|
+
// One SHAKE256 stream split by the seed coder as mlkemSeed (64) || curveSeed (nseed).
|
|
798
|
+
// Returned directly: the previous concatBytes of two adjacent subarrays produced an
|
|
799
|
+
// identical copy while leaving this original buffer unwiped; expandDecapsulationKey
|
|
800
|
+
// wipes the returned buffer after the child seeds are copied out.
|
|
801
|
+
return shake256(seed, { dkLen: totalSeedLen });
|
|
657
802
|
}, (pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))), mlkem, curveKem);
|
|
658
803
|
}
|
|
659
804
|
/** P-256 + ML-KEM-768 hybrid preset. */
|
|
660
805
|
export const ml_kem768_p256 = /* @__PURE__ */ (() => concreteHybridKem('MLKEM768-P256', ml_kem768, p256, 128))();
|
|
661
806
|
/** P-384 + ML-KEM-1024 hybrid preset. */
|
|
662
807
|
export const ml_kem1024_p384 = /* @__PURE__ */ (() => concreteHybridKem('MLKEM1024-P384', ml_kem1024, p384, 48))();
|
|
663
|
-
// Legacy aliases
|
|
664
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
665
|
-
export const XWing = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
666
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
667
|
-
export const MLKEM768X25519 = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
668
|
-
/** Legacy alias for `ml_kem768_p256`. */
|
|
669
|
-
export const MLKEM768P256 = /* @__PURE__ */ (() => ml_kem768_p256)();
|
|
670
|
-
/** Legacy alias for `ml_kem1024_p384`. */
|
|
671
|
-
export const MLKEM1024P384 = /* @__PURE__ */ (() => ml_kem1024_p384)();
|
|
672
|
-
/** Legacy alias for `QSF_ml_kem768_p256`. */
|
|
673
|
-
export const QSFMLKEM768P256 = /* @__PURE__ */ (() => QSF_ml_kem768_p256)();
|
|
674
|
-
/** Legacy alias for `QSF_ml_kem1024_p384`. */
|
|
675
|
-
export const QSFMLKEM1024P384 = /* @__PURE__ */ (() => QSF_ml_kem1024_p384)();
|
|
676
|
-
/** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
|
|
677
|
-
export const KitchenSinkMLKEM768X25519 = /* @__PURE__ */ (() => KitchenSink_ml_kem768_x25519)();
|
|
678
|
-
//# sourceMappingURL=hybrid.js.map
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -19,11 +19,10 @@ import {
|
|
|
19
19
|
} from '@noble/post-quantum/falcon.js';
|
|
20
20
|
import {
|
|
21
21
|
ml_kem768_x25519, ml_kem768_p256, ml_kem1024_p384,
|
|
22
|
-
KitchenSink_ml_kem768_x25519,
|
|
22
|
+
KitchenSink_ml_kem768_x25519,
|
|
23
23
|
QSF_ml_kem768_p256, QSF_ml_kem1024_p384,
|
|
24
24
|
} from '@noble/post-quantum/hybrid.js';
|
|
25
25
|
```
|
|
26
26
|
*/
|
|
27
27
|
throw new Error('root module cannot be imported: import submodules instead. Check out README');
|
|
28
28
|
export {};
|
|
29
|
-
//# sourceMappingURL=index.js.map
|
package/ml-dsa.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { CHash } from '@noble/hashes/utils.js';
|
|
2
|
+
import { type CryptoKeys, type Signer, type SigOpts, type TArg, type TRet } from './utils.ts';
|
|
2
3
|
/** Internal ML-DSA options. */
|
|
3
4
|
export type DSAInternalOpts = {
|
|
4
5
|
/**
|
|
@@ -11,12 +12,19 @@ export type DSAInternalOpts = {
|
|
|
11
12
|
/** ML-DSA signer surface with access to the internal message formatting mode. */
|
|
12
13
|
export type DSAInternal = CryptoKeys & {
|
|
13
14
|
lengths: Signer['lengths'];
|
|
14
|
-
sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts?: TArg<SigOpts & DSAInternalOpts>) => TRet<Uint8Array>;
|
|
15
|
-
verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, pubKey: TArg<Uint8Array>, opts?: TArg<
|
|
15
|
+
sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts?: TArg<Omit<SigOpts, 'context'> & DSAInternalOpts>) => TRet<Uint8Array>;
|
|
16
|
+
verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, pubKey: TArg<Uint8Array>, opts?: TArg<DSAInternalOpts>) => boolean;
|
|
16
17
|
};
|
|
17
18
|
/** Public ML-DSA signer surface. */
|
|
18
19
|
export type DSA = Signer & {
|
|
19
20
|
internal: TRet<DSAInternal>;
|
|
21
|
+
securityLevel: number;
|
|
22
|
+
/**
|
|
23
|
+
* HashML-DSA (FIPS 204 §5.4) variant which signs a pre-hashed message.
|
|
24
|
+
* @param hash - Approved hash, checked against the parameter set security level.
|
|
25
|
+
* @returns Signer which pre-hashes `msg` before formatting `M'`.
|
|
26
|
+
*/
|
|
27
|
+
prehash: (hash: TArg<CHash>) => TRet<Signer>;
|
|
20
28
|
};
|
|
21
29
|
/** Various lattice params. */
|
|
22
30
|
/** Public ML-DSA parameter-set description. */
|
|
@@ -45,10 +53,28 @@ export type DSAParam = {
|
|
|
45
53
|
* while `C_TILDE_BYTES`, `TR_BYTES`, `CRH_BYTES`, and `securityLevel` live in the preset wrappers.
|
|
46
54
|
*/
|
|
47
55
|
export declare const PARAMS: Record<string, DSAParam>;
|
|
48
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD.
|
|
58
|
+
* @example
|
|
59
|
+
* Generate deterministic ML-DSA-44 keys, sign one message, and verify the signature.
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
62
|
+
* import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
63
|
+
* const seed = new Uint8Array(ml_dsa44.lengths.seed!);
|
|
64
|
+
* const { secretKey, publicKey } = ml_dsa44.keygen(seed);
|
|
65
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
66
|
+
* const sig = ml_dsa44.sign(msg, secretKey);
|
|
67
|
+
* const isValid = ml_dsa44.verify(sig, msg, publicKey);
|
|
68
|
+
* const recovered = ml_dsa44.getPublicKey(secretKey);
|
|
69
|
+
* const context = new Uint8Array([1, 2, 3]);
|
|
70
|
+
* const prehash = ml_dsa44.prehash(sha256);
|
|
71
|
+
* const preSig = prehash.sign(msg, secretKey, { context });
|
|
72
|
+
* const preValid = prehash.verify(preSig, msg, publicKey, { context });
|
|
73
|
+
* const internalSig = ml_dsa44.internal.sign(msg, secretKey);
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
49
76
|
export declare const ml_dsa44: TRet<DSA>;
|
|
50
77
|
/** ML-DSA-65 for 192-bit security level. Not recommended after 2030, as per ASD. */
|
|
51
78
|
export declare const ml_dsa65: TRet<DSA>;
|
|
52
79
|
/** ML-DSA-87 for 256-bit security level. OK after 2030, as per ASD. */
|
|
53
80
|
export declare const ml_dsa87: TRet<DSA>;
|
|
54
|
-
//# sourceMappingURL=ml-dsa.d.ts.map
|