@noble/post-quantum 0.7.0 → 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 +90 -16
- package/_crystals.js +1 -1
- package/falcon.d.ts +1 -1
- package/falcon.js +121 -62
- package/hybrid.d.ts +33 -12
- package/hybrid.js +100 -41
- package/index.js +1 -1
- package/ml-dsa.d.ts +3 -3
- package/ml-dsa.js +57 -20
- package/ml-kem.js +85 -34
- package/package.json +15 -11
- package/slh-dsa.js +34 -11
- package/src/_crystals.ts +1 -1
- package/src/falcon.ts +127 -70
- package/src/hybrid.ts +108 -39
- package/src/index.ts +1 -1
- package/src/ml-dsa.ts +70 -25
- package/src/ml-kem.ts +76 -35
- package/src/slh-dsa.ts +44 -13
- package/src/utils.ts +115 -10
- package/src/webcrypto.ts +322 -0
- package/utils.d.ts +36 -2
- package/utils.js +105 -12
- package/webcrypto.d.ts +91 -0
- package/webcrypto.js +213 -0
package/hybrid.js
CHANGED
|
@@ -83,7 +83,7 @@ import { abool, afunction, asciiToBytes, bytesToNumberBE, bytesToNumberLE, conca
|
|
|
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
88
|
import { aobject, astring, cleanBytes, copyBytes, randomBytes, splitCoder, validateSigOpts, validateVerOpts, } from "./utils.js";
|
|
89
89
|
const validateKEM = (kem, title) => {
|
|
@@ -148,6 +148,12 @@ function ecKeygen(curve, allowZeroKey = false) {
|
|
|
148
148
|
/**
|
|
149
149
|
* Wraps an ECDH-capable curve as a KEM.
|
|
150
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
|
+
*
|
|
151
157
|
* On SEC 1 / Weierstrass curves, that means the compressed shared-point body without the
|
|
152
158
|
* 1-byte `0x02` / `0x03` prefix.
|
|
153
159
|
* The X25519 path also leaves RFC 7748's optional all-zero shared-secret check to callers.
|
|
@@ -163,12 +169,12 @@ function ecKeygen(curve, allowZeroKey = false) {
|
|
|
163
169
|
* Wrap an ECDH-capable curve as a generic KEM.
|
|
164
170
|
* ```ts
|
|
165
171
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
166
|
-
* import {
|
|
167
|
-
* const kem =
|
|
172
|
+
* import { _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
173
|
+
* const kem = _ecdhKem(x25519);
|
|
168
174
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
169
175
|
* ```
|
|
170
176
|
*/
|
|
171
|
-
export function
|
|
177
|
+
export function _ecdhKem(curve, allowZeroKey = false) {
|
|
172
178
|
const kg = ecKeygen(curve, allowZeroKey);
|
|
173
179
|
if (!curve.getSharedSecret)
|
|
174
180
|
throw new Error('wrong curve'); // ed25519 doesn't have one!
|
|
@@ -233,7 +239,7 @@ export function ecSigner(curve, allowZeroKey = false) {
|
|
|
233
239
|
keygen: kg.keygen,
|
|
234
240
|
getPublicKey: kg.getPublicKey,
|
|
235
241
|
sign: (message, secretKey, opts = {}) => {
|
|
236
|
-
validateSigOpts(opts);
|
|
242
|
+
opts = validateSigOpts(opts);
|
|
237
243
|
// This generic wrapper intentionally keeps the Signer contract to message + key only.
|
|
238
244
|
// Backend-specific knobs like ECDSA extraEntropy or Ed25519ctx context cannot be forwarded
|
|
239
245
|
// uniformly through combineSigners(), so callers that need them must use the curve directly.
|
|
@@ -248,20 +254,28 @@ export function ecSigner(curve, allowZeroKey = false) {
|
|
|
248
254
|
* generic opts and lets wrapped-curve malformed-input errors escape unchanged.
|
|
249
255
|
*/
|
|
250
256
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
251
|
-
validateVerOpts(opts);
|
|
257
|
+
opts = validateVerOpts(opts);
|
|
252
258
|
if (opts.context !== undefined)
|
|
253
259
|
throw new Error('ecSigner does not support context; use the underlying curve directly');
|
|
254
260
|
return curve.verify(signature, message, publicKey);
|
|
255
261
|
},
|
|
256
262
|
};
|
|
257
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
|
+
}
|
|
258
270
|
function splitLengths(lst, name) {
|
|
259
271
|
// Preserve caller order exactly; raw numeric fields still decode as splitCoder() subarray views.
|
|
260
|
-
|
|
272
|
+
const coder = splitCoder(name, ...lst.map((i) => {
|
|
261
273
|
if (typeof i.lengths[name] !== 'number')
|
|
262
274
|
throw new Error('wrong length: ' + name);
|
|
263
|
-
return i.lengths[name];
|
|
275
|
+
return positiveLength(i.lengths[name], name);
|
|
264
276
|
}));
|
|
277
|
+
positiveLength(coder.bytesLen, name);
|
|
278
|
+
return coder;
|
|
265
279
|
}
|
|
266
280
|
// It is XOF for most cases, but can be more complex!
|
|
267
281
|
/**
|
|
@@ -290,11 +304,9 @@ expandSeed_, ...ck_) {
|
|
|
290
304
|
const seedCoder = splitLengths(ck, 'seed');
|
|
291
305
|
const pkCoder = splitLengths(ck, 'publicKey');
|
|
292
306
|
// Allows to use identity functions for combiner/expandSeed
|
|
293
|
-
|
|
294
|
-
realSeedLen = seedCoder.bytesLen;
|
|
295
|
-
anumber(realSeedLen);
|
|
307
|
+
const rootSeedLen = positiveLength(realSeedLen === undefined ? seedCoder.bytesLen : realSeedLen, 'realSeedLen');
|
|
296
308
|
function expandDecapsulationKey(seed) {
|
|
297
|
-
abytes(seed,
|
|
309
|
+
abytes(seed, rootSeedLen);
|
|
298
310
|
const expandedRaw = expandSeed(seed, seedCoder.bytesLen);
|
|
299
311
|
// Identity/subarray expanders can hand back caller-owned seed storage. Detach those outputs so
|
|
300
312
|
// later cleanup can wipe the expanded schedule without mutating the caller's root seed bytes.
|
|
@@ -332,7 +344,7 @@ expandSeed_, ...ck_) {
|
|
|
332
344
|
const keygen = (seed) => {
|
|
333
345
|
// Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
|
|
334
346
|
// caller mutation of the seed cannot silently change the secret key (and vice versa).
|
|
335
|
-
const root = seed === undefined ? randomBytes(
|
|
347
|
+
const root = seed === undefined ? randomBytes(rootSeedLen) : copyBytes(seed);
|
|
336
348
|
let res;
|
|
337
349
|
try {
|
|
338
350
|
const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
|
|
@@ -355,7 +367,7 @@ expandSeed_, ...ck_) {
|
|
|
355
367
|
}
|
|
356
368
|
};
|
|
357
369
|
return {
|
|
358
|
-
info: { lengths: { seed:
|
|
370
|
+
info: { lengths: { seed: rootSeedLen, publicKey: pkCoder.bytesLen, secretKey: rootSeedLen } },
|
|
359
371
|
// Composite secret keys are root seeds, so public-key derivation reruns key expansion from
|
|
360
372
|
// that seed instead of decoding a packed child-secret-key structure.
|
|
361
373
|
getPublicKey: (secretKey) => {
|
|
@@ -366,18 +378,24 @@ expandSeed_, ...ck_) {
|
|
|
366
378
|
},
|
|
367
379
|
keygen,
|
|
368
380
|
expandDecapsulationKey,
|
|
369
|
-
realSeedLen,
|
|
381
|
+
realSeedLen: rootSeedLen,
|
|
370
382
|
};
|
|
371
383
|
}
|
|
372
384
|
// This generic function that combines multiple KEMs into single one
|
|
373
385
|
/**
|
|
374
386
|
* Combines multiple KEMs into one composite KEM.
|
|
375
|
-
* @param realSeedLen -
|
|
376
|
-
*
|
|
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.
|
|
377
392
|
* @param expandSeed - Seed expander used to derive per-KEM seeds.
|
|
378
393
|
* @param combiner - Combines the per-KEM outputs into one shared secret.
|
|
379
|
-
* @param kems - KEM
|
|
394
|
+
* @param kems - At least one KEM implementation. A construction advertised as hybrid normally
|
|
395
|
+
* supplies two or more.
|
|
380
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}
|
|
381
399
|
* @example
|
|
382
400
|
* Combine multiple KEMs into one composite KEM.
|
|
383
401
|
* ```ts
|
|
@@ -399,30 +417,52 @@ export function combineKEMS(realSeedLen, // how much bytes expandSeed expects
|
|
|
399
417
|
realMsgLen, // how much bytes combiner returns
|
|
400
418
|
expandSeed, combiner, ...kems) {
|
|
401
419
|
if (realSeedLen !== undefined)
|
|
402
|
-
|
|
420
|
+
positiveLength(realSeedLen, 'realSeedLen');
|
|
403
421
|
if (realMsgLen !== undefined)
|
|
404
|
-
|
|
422
|
+
positiveLength(realMsgLen, 'realMsgLen');
|
|
405
423
|
if (typeof expandSeed !== 'function')
|
|
406
424
|
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
407
425
|
if (typeof combiner !== 'function')
|
|
408
426
|
throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
|
|
409
427
|
const rawCombiner = combiner;
|
|
410
428
|
const rawKems = kems;
|
|
429
|
+
if (rawKems.length === 0)
|
|
430
|
+
throw new RangeError('combineKEMS requires at least one KEM');
|
|
411
431
|
for (let i = 0; i < rawKems.length; i++)
|
|
412
432
|
validateKEM(rawKems[i], `kems[${i}]`);
|
|
413
433
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
|
|
414
434
|
const ctCoder = splitLengths(rawKems, 'cipherText');
|
|
415
435
|
const pkCoder = splitLengths(rawKems, 'publicKey');
|
|
416
436
|
const msgCoder = splitLengths(rawKems, 'msg');
|
|
417
|
-
|
|
418
|
-
realMsgLen = msgCoder.bytesLen;
|
|
419
|
-
anumber(realMsgLen, 'realMsgLen');
|
|
437
|
+
const sharedSecretLen = positiveLength(realMsgLen === undefined ? msgCoder.bytesLen : realMsgLen, 'realMsgLen');
|
|
420
438
|
const lengths = Object.freeze({
|
|
421
439
|
...keys.info.lengths,
|
|
422
|
-
msg:
|
|
440
|
+
msg: sharedSecretLen,
|
|
423
441
|
msgRand: msgCoder.bytesLen,
|
|
424
442
|
cipherText: ctCoder.bytesLen,
|
|
425
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
|
+
};
|
|
426
466
|
return Object.freeze({
|
|
427
467
|
lengths,
|
|
428
468
|
getPublicKey: keys.getPublicKey,
|
|
@@ -438,11 +478,14 @@ expandSeed, combiner, ...kems) {
|
|
|
438
478
|
sharedSecret.push(enc.sharedSecret);
|
|
439
479
|
cipherText.push(enc.cipherText);
|
|
440
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);
|
|
441
484
|
return {
|
|
442
485
|
// Detach the combiner result before cleanup: a caller-provided combiner may alias one of
|
|
443
486
|
// the child sharedSecret buffers, and those child buffers are zeroized immediately below.
|
|
444
|
-
sharedSecret:
|
|
445
|
-
cipherText:
|
|
487
|
+
sharedSecret: combine(pks, cipherText, sharedSecret),
|
|
488
|
+
cipherText: encodedCipherText,
|
|
446
489
|
};
|
|
447
490
|
}
|
|
448
491
|
finally {
|
|
@@ -454,11 +497,16 @@ expandSeed, combiner, ...kems) {
|
|
|
454
497
|
decapsulate(ct, seed) {
|
|
455
498
|
const cts = ctCoder.decode(ct);
|
|
456
499
|
const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
|
|
457
|
-
const sharedSecret =
|
|
500
|
+
const sharedSecret = [];
|
|
458
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]));
|
|
459
507
|
// Detach the decapsulation result before cleanup: the combiner may hand back one of the
|
|
460
508
|
// child shared-secret buffers, and those temporary buffers are zeroized below.
|
|
461
|
-
return
|
|
509
|
+
return combine(publicKey, cts, sharedSecret);
|
|
462
510
|
}
|
|
463
511
|
finally {
|
|
464
512
|
// Decapsulation only needs the expanded child secret keys and child shared secrets for this
|
|
@@ -472,10 +520,15 @@ expandSeed, combiner, ...kems) {
|
|
|
472
520
|
// realSeedLen: how much bytes expandSeed expects.
|
|
473
521
|
/**
|
|
474
522
|
* Combines multiple signers into one composite signer.
|
|
475
|
-
* @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.
|
|
476
526
|
* @param expandSeed - Seed expander used to derive per-signer seeds.
|
|
477
|
-
* @param signers -
|
|
527
|
+
* @param signers - At least one signer. A construction advertised as hybrid normally supplies two
|
|
528
|
+
* or more.
|
|
478
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}
|
|
479
532
|
* @example
|
|
480
533
|
* Combine multiple signers into one composite signer.
|
|
481
534
|
* ```ts
|
|
@@ -492,10 +545,12 @@ expandSeed, combiner, ...kems) {
|
|
|
492
545
|
*/
|
|
493
546
|
export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
494
547
|
if (realSeedLen !== undefined)
|
|
495
|
-
|
|
548
|
+
positiveLength(realSeedLen, 'realSeedLen');
|
|
496
549
|
if (typeof expandSeed !== 'function')
|
|
497
550
|
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
498
551
|
const rawSigners = signers;
|
|
552
|
+
if (rawSigners.length === 0)
|
|
553
|
+
throw new RangeError('combineSigners requires at least one signer');
|
|
499
554
|
for (let i = 0; i < rawSigners.length; i++)
|
|
500
555
|
validateSigner(rawSigners[i], `signers[${i}]`);
|
|
501
556
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
|
|
@@ -506,7 +561,7 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
|
506
561
|
getPublicKey: keys.getPublicKey,
|
|
507
562
|
keygen: keys.keygen,
|
|
508
563
|
sign(message, seed, opts = {}) {
|
|
509
|
-
validateSigOpts(opts);
|
|
564
|
+
opts = validateSigOpts(opts);
|
|
510
565
|
// This generic wrapper intentionally keeps the composite signer contract to message + root
|
|
511
566
|
// seed only. Per-signer opts like context or extraEntropy cannot be preserved uniformly
|
|
512
567
|
// across mixed backends, so callers that need them must use the underlying signer directly.
|
|
@@ -530,7 +585,7 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
|
530
585
|
* does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
|
|
531
586
|
*/
|
|
532
587
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
533
|
-
validateVerOpts(opts);
|
|
588
|
+
opts = validateVerOpts(opts);
|
|
534
589
|
if (opts.context !== undefined)
|
|
535
590
|
throw new Error('combineSigners does not support context; use the underlying signer directly');
|
|
536
591
|
// Malformed signature *length* is a verification failure, not a thrown type error —
|
|
@@ -563,14 +618,16 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
|
563
618
|
* @param xof - XOF used for seed expansion.
|
|
564
619
|
* @param kdf - Hash used for the final combiner.
|
|
565
620
|
* @returns Hybrid KEM.
|
|
621
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
622
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
566
623
|
* @example
|
|
567
624
|
* Build a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM.
|
|
568
625
|
* ```ts
|
|
569
626
|
* import { p256 } from '@noble/curves/nist.js';
|
|
570
627
|
* import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
571
|
-
* import { QSF,
|
|
628
|
+
* import { QSF, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
572
629
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
573
|
-
* const kem = QSF('example', ml_kem768,
|
|
630
|
+
* const kem = QSF('example', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256);
|
|
574
631
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
575
632
|
* ```
|
|
576
633
|
*/
|
|
@@ -587,9 +644,9 @@ export function QSF(label, pqc, curveKEM, xof, kdf) {
|
|
|
587
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);
|
|
588
645
|
}
|
|
589
646
|
/** QSF preset combining ML-KEM-768 with P-256. */
|
|
590
|
-
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))();
|
|
591
648
|
/** QSF preset combining ML-KEM-1024 with P-384. */
|
|
592
|
-
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))();
|
|
593
650
|
/**
|
|
594
651
|
* Builds the "KitchenSink" hybrid KEM combiner.
|
|
595
652
|
* The current builder always derives a fixed 32-byte output,
|
|
@@ -604,15 +661,17 @@ export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-10
|
|
|
604
661
|
* @param xof - XOF used for seed expansion.
|
|
605
662
|
* @param hash - Hash used for HKDF extraction and expansion.
|
|
606
663
|
* @returns Hybrid KEM.
|
|
664
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
665
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
607
666
|
* @example
|
|
608
667
|
* Build the "KitchenSink" hybrid KEM combiner.
|
|
609
668
|
* ```ts
|
|
610
669
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
611
670
|
* import { shake256 } from '@noble/hashes/sha3.js';
|
|
612
|
-
* import { createKitchenSink,
|
|
671
|
+
* import { createKitchenSink, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
613
672
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
614
673
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
615
|
-
* const kem = createKitchenSink('example', ml_kem768,
|
|
674
|
+
* const kem = createKitchenSink('example', ml_kem768, _ecdhKem(x25519), shake256, sha256);
|
|
616
675
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
617
676
|
* ```
|
|
618
677
|
*/
|
|
@@ -637,9 +696,9 @@ export function createKitchenSink(label, pqc, curveKEM, xof, hash) {
|
|
|
637
696
|
return res;
|
|
638
697
|
}, pqc, curveKEM);
|
|
639
698
|
}
|
|
640
|
-
// Internal alias only: this stays exactly `
|
|
699
|
+
// Internal alias only: this stays exactly `_ecdhKem(x25519)`
|
|
641
700
|
// and inherits that wrapper's mutation/oracle behavior.
|
|
642
|
-
const x25519kem = /* @__PURE__ */
|
|
701
|
+
const x25519kem = /* @__PURE__ */ _ecdhKem(x25519);
|
|
643
702
|
/** KitchenSink preset combining ML-KEM-768 with X25519.
|
|
644
703
|
* Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed.
|
|
645
704
|
*/
|
package/index.js
CHANGED
|
@@ -19,7 +19,7 @@ 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
|
```
|
package/ml-dsa.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CHash } from '@noble/hashes/utils.js';
|
|
2
|
-
import { type CryptoKeys, type Signer, type SigOpts, type TArg, type TRet
|
|
2
|
+
import { type CryptoKeys, type Signer, type SigOpts, type TArg, type TRet } from './utils.ts';
|
|
3
3
|
/** Internal ML-DSA options. */
|
|
4
4
|
export type DSAInternalOpts = {
|
|
5
5
|
/**
|
|
@@ -12,8 +12,8 @@ export type DSAInternalOpts = {
|
|
|
12
12
|
/** ML-DSA signer surface with access to the internal message formatting mode. */
|
|
13
13
|
export type DSAInternal = CryptoKeys & {
|
|
14
14
|
lengths: Signer['lengths'];
|
|
15
|
-
sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts?: TArg<SigOpts & DSAInternalOpts>) => TRet<Uint8Array>;
|
|
16
|
-
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;
|
|
17
17
|
};
|
|
18
18
|
/** Public ML-DSA signer surface. */
|
|
19
19
|
export type DSA = Signer & {
|
package/ml-dsa.js
CHANGED
|
@@ -11,11 +11,26 @@
|
|
|
11
11
|
import { abool } from '@noble/curves/utils.js';
|
|
12
12
|
import { shake256 } from '@noble/hashes/sha3.js';
|
|
13
13
|
import { genCrystals, XOF128, XOF256 } from "./_crystals.js";
|
|
14
|
-
import { abytes, checkHash, cleanBytes, equalBytes, getMessage, getMessagePrehash, randomBytes, splitCoder,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
import { abytes, checkHash, cleanBytes, equalBytes, getMessage, getMessagePrehash, randomBytes, splitCoder, validateSigOpts, validateVerOpts, checkOptKeys, vecCoder, } from "./utils.js";
|
|
15
|
+
/**
|
|
16
|
+
* Keys each internal surface accepts.
|
|
17
|
+
*
|
|
18
|
+
* `context` is deliberately absent from both. The internal functions never read it: the
|
|
19
|
+
* public wrappers consume it when they format `M'` and must not pass it down, because a
|
|
20
|
+
* key that is accepted and then not acted on is the same silent downgrade this validation
|
|
21
|
+
* exists to prevent. `externalMu` is the mirror case, existing here and rejected above.
|
|
22
|
+
* `extraEntropy` is signing-only, so verification does not take it either.
|
|
23
|
+
*/
|
|
24
|
+
const INTERNAL_SIG_OPT_KEYS = /* @__PURE__ */ Object.freeze([
|
|
25
|
+
'extraEntropy',
|
|
26
|
+
'externalMu',
|
|
27
|
+
]);
|
|
28
|
+
const INTERNAL_VER_OPT_KEYS = /* @__PURE__ */ Object.freeze(['externalMu']);
|
|
29
|
+
function validateInternalOpts(opts, allowed) {
|
|
30
|
+
const normalized = checkOptKeys(opts, allowed);
|
|
31
|
+
if (normalized.externalMu !== undefined)
|
|
32
|
+
abool(normalized.externalMu, 'opts.externalMu');
|
|
33
|
+
return normalized;
|
|
19
34
|
}
|
|
20
35
|
// Constants
|
|
21
36
|
// FIPS 204 fixes ML-DSA over R = Z[X]/(X^256 + 1), so every polynomial has 256 coefficients.
|
|
@@ -423,8 +438,8 @@ function getDilithium(opts_) {
|
|
|
423
438
|
},
|
|
424
439
|
// NOTE: random is optional.
|
|
425
440
|
sign: (msg, secretKey, opts = {}) => {
|
|
426
|
-
validateSigOpts(opts);
|
|
427
|
-
validateInternalOpts(opts);
|
|
441
|
+
opts = validateSigOpts(opts, INTERNAL_SIG_OPT_KEYS);
|
|
442
|
+
opts = validateInternalOpts(opts, INTERNAL_SIG_OPT_KEYS);
|
|
428
443
|
const { extraEntropy: random, externalMu = false } = opts;
|
|
429
444
|
// FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
|
|
430
445
|
if (externalMu)
|
|
@@ -516,8 +531,13 @@ function getDilithium(opts_) {
|
|
|
516
531
|
const cs1 = s1.map((i) => MultiplyNTTs(i, cHat));
|
|
517
532
|
for (let i = 0; i < L; i++) {
|
|
518
533
|
polyAdd(crystals.NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
|
|
519
|
-
if (polyChknorm(cs1[i], GAMMA1 - BETA))
|
|
534
|
+
if (polyChknorm(cs1[i], GAMMA1 - BETA)) {
|
|
535
|
+
// Rejected. Wipe this iteration's secret-derived buffers before retrying; the
|
|
536
|
+
// accepted path wipes the same set, and only the persistent key material (s1, s2,
|
|
537
|
+
// t0, A, rhoprime) is kept for the next iteration and cleaned at the very end.
|
|
538
|
+
cleanBytes(cTilde, cs1, cHat, w1, w, z, y);
|
|
520
539
|
continue main_loop; // ||z||∞ ≥ γ1 − β
|
|
540
|
+
}
|
|
521
541
|
}
|
|
522
542
|
// cs1 is now z (▷ Signer’s response)
|
|
523
543
|
let cnt = 0;
|
|
@@ -525,19 +545,25 @@ function getDilithium(opts_) {
|
|
|
525
545
|
for (let i = 0; i < K; i++) {
|
|
526
546
|
const cs2 = crystals.NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
|
|
527
547
|
const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩)
|
|
528
|
-
if (polyChknorm(r0, GAMMA2 - BETA))
|
|
548
|
+
if (polyChknorm(r0, GAMMA2 - BETA)) {
|
|
549
|
+
cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h, cs2, r0);
|
|
529
550
|
continue main_loop; // ||r0||∞ ≥ γ2 − β
|
|
551
|
+
}
|
|
530
552
|
const ct0 = crystals.NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
|
|
531
|
-
if (polyChknorm(ct0, GAMMA2))
|
|
553
|
+
if (polyChknorm(ct0, GAMMA2)) {
|
|
554
|
+
cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h, cs2, r0, ct0);
|
|
532
555
|
continue main_loop;
|
|
556
|
+
}
|
|
533
557
|
polyAdd(r0, ct0);
|
|
534
558
|
// ▷ Signer’s hint
|
|
535
559
|
const hint = polyMakeHint(r0, w1[i]); // h ← MakeHint(−⟨⟨ct0⟩⟩, w− ⟨⟨cs2⟩⟩ + ⟨⟨ct0⟩⟩)
|
|
536
560
|
h.push(hint.v);
|
|
537
561
|
cnt += hint.cnt;
|
|
538
562
|
}
|
|
539
|
-
if (cnt > OMEGA)
|
|
563
|
+
if (cnt > OMEGA) {
|
|
564
|
+
cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h);
|
|
540
565
|
continue; // the number of 1’s in h is greater than ω
|
|
566
|
+
}
|
|
541
567
|
x256.clean();
|
|
542
568
|
const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h)
|
|
543
569
|
// rho, _K, tr is subarray of secretKey, cannot clean.
|
|
@@ -553,7 +579,7 @@ function getDilithium(opts_) {
|
|
|
553
579
|
throw new Error('Unreachable code path reached, report this error');
|
|
554
580
|
},
|
|
555
581
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
556
|
-
validateInternalOpts(opts);
|
|
582
|
+
opts = validateInternalOpts(opts, INTERNAL_VER_OPT_KEYS);
|
|
557
583
|
const { externalMu = false } = opts;
|
|
558
584
|
// FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
|
|
559
585
|
if (externalMu)
|
|
@@ -622,16 +648,21 @@ function getDilithium(opts_) {
|
|
|
622
648
|
lengths: internal.lengths,
|
|
623
649
|
getPublicKey: internal.getPublicKey,
|
|
624
650
|
sign: (msg, secretKey, opts = {}) => {
|
|
625
|
-
validateSigOpts(opts);
|
|
651
|
+
opts = validateSigOpts(opts);
|
|
626
652
|
const M = getMessage(msg, opts.context);
|
|
627
|
-
|
|
653
|
+
// `context` is consumed by getMessage() above; forwarding it would make the internal
|
|
654
|
+
// surface accept a key it never reads.
|
|
655
|
+
const res = internal.sign(M, secretKey, {
|
|
656
|
+
extraEntropy: opts.extraEntropy,
|
|
657
|
+
externalMu: false,
|
|
658
|
+
});
|
|
628
659
|
cleanBytes(M);
|
|
629
660
|
return res;
|
|
630
661
|
},
|
|
631
662
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
632
|
-
validateVerOpts(opts);
|
|
663
|
+
opts = validateVerOpts(opts);
|
|
633
664
|
abytes(sig, undefined, 'signature');
|
|
634
|
-
return internal.verify(sig, getMessage(msg, opts.context), publicKey);
|
|
665
|
+
return internal.verify(sig, getMessage(msg, opts.context), publicKey, { externalMu: false });
|
|
635
666
|
},
|
|
636
667
|
prehash: (hash) => {
|
|
637
668
|
checkHash(hash, securityLevel);
|
|
@@ -643,16 +674,22 @@ function getDilithium(opts_) {
|
|
|
643
674
|
keygen: internal.keygen,
|
|
644
675
|
getPublicKey: internal.getPublicKey,
|
|
645
676
|
sign: (msg, secretKey, opts = {}) => {
|
|
646
|
-
validateSigOpts(opts);
|
|
677
|
+
opts = validateSigOpts(opts);
|
|
647
678
|
const M = getMessagePrehash(rawHash, msg, opts.context);
|
|
648
|
-
|
|
679
|
+
// As above: getMessagePrehash() consumes `context`, so it must not travel further.
|
|
680
|
+
const res = internal.sign(M, secretKey, {
|
|
681
|
+
extraEntropy: opts.extraEntropy,
|
|
682
|
+
externalMu: false,
|
|
683
|
+
});
|
|
649
684
|
cleanBytes(M);
|
|
650
685
|
return res;
|
|
651
686
|
},
|
|
652
687
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
653
|
-
validateVerOpts(opts);
|
|
688
|
+
opts = validateVerOpts(opts);
|
|
654
689
|
abytes(sig, undefined, 'signature');
|
|
655
|
-
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey
|
|
690
|
+
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey, {
|
|
691
|
+
externalMu: false,
|
|
692
|
+
});
|
|
656
693
|
},
|
|
657
694
|
});
|
|
658
695
|
},
|